diff --git a/wp-admin/admin-ajax.php b/wp-admin/admin-ajax.php index 65398f182e..716e384da4 100644 --- a/wp-admin/admin-ajax.php +++ b/wp-admin/admin-ajax.php @@ -56,7 +56,7 @@ $core_actions_post = array( 'save-widget', 'set-post-thumbnail', 'date_format', 'time_format', 'wp-fullscreen-save-post', 'wp-remove-post-lock', 'dismiss-wp-pointer', 'upload-attachment', 'get-attachment', 'query-attachments', 'save-attachment', 'save-attachment-compat', 'send-link-to-editor', - 'send-attachment-to-editor', + 'send-attachment-to-editor', 'save-attachment-order', ); // Register core Ajax calls. diff --git a/wp-admin/includes/ajax-actions.php b/wp-admin/includes/ajax-actions.php index a6a37b6f98..e8814e82c3 100644 --- a/wp-admin/includes/ajax-actions.php +++ b/wp-admin/includes/ajax-actions.php @@ -1927,6 +1927,39 @@ function wp_ajax_save_attachment_compat() { wp_send_json_success( $attachment ); } +function wp_ajax_save_attachment_order() { + if ( ! isset( $_REQUEST['post_id'] ) ) + wp_send_json_error(); + + if ( ! $post_id = absint( $_REQUEST['post_id'] ) ) + wp_send_json_error(); + + if ( empty( $_REQUEST['attachments'] ) ) + wp_send_json_error(); + + check_ajax_referer( 'update-post_' . $post_id, 'nonce' ); + + $attachments = $_REQUEST['attachments']; + + if ( ! current_user_can( 'edit_post', $post_id ) ) + wp_send_json_error(); + + $post = get_post( $post_id, ARRAY_A ); + + foreach ( $attachments as $attachment_id => $menu_order ) { + if ( ! current_user_can( 'edit_post', $attachment_id ) ) + continue; + if ( ! $attachment = get_post( $attachment_id ) ) + continue; + if ( 'attachment' != $attachment->post_type ) + continue; + + wp_update_post( array( 'ID' => $attachment_id, 'menu_order' => $menu_order ) ); + } + + wp_send_json_success(); +} + /** * Generates the HTML to send an attachment to the editor. * Backwards compatible with the media_send_to_editor filter and the chain diff --git a/wp-includes/js/media-models.js b/wp-includes/js/media-models.js index 385ee5ce01..704ff4a121 100644 --- a/wp-includes/js/media-models.js +++ b/wp-includes/js/media-models.js @@ -523,6 +523,34 @@ window.wp = window.wp || {}; _requery: function() { if ( this.props.get('query') ) this.mirror( Query.get( this.props.toJSON() ) ); + }, + + // If this collection is sorted by `menuOrder`, recalculates and saves + // the menu order to the database. + saveMenuOrder: function() { + if ( 'menuOrder' !== this.props.get('orderby') ) + return; + + // Removes any uploading attachments, updates each attachment's + // menu order, and returns an object with an { id: menuOrder } + // mapping to pass to the request. + var attachments = this.chain().filter( function( attachment ) { + return ! _.isUndefined( attachment.id ); + }).map( function( attachment, index ) { + // Indices start at 1. + index = index + 1; + attachment.set( 'menuOrder', index ); + return [ attachment.id, index ]; + }).object().value(); + + if ( _.isEmpty( attachments ) ) + return; + + return media.post( 'save-attachment-order', { + nonce: media.model.settings.updatePostNonce, + post_id: media.model.settings.postId, + attachments: attachments + }); } }, { comparator: function( a, b, options ) { diff --git a/wp-includes/js/media-views.js b/wp-includes/js/media-views.js index 09a3a88de6..f995972444 100644 --- a/wp-includes/js/media-views.js +++ b/wp-includes/js/media-views.js @@ -14,6 +14,7 @@ // Copy the `postId` setting over to the model settings. media.model.settings.postId = media.view.settings.postId; + media.model.settings.updatePostNonce = media.view.settings.nonce.updatePost; // Check if the browser supports CSS 3.0 transitions $.support.transition = (function(){ @@ -267,7 +268,8 @@ content: 'browse', searchable: true, filterable: false, - uploads: true + uploads: true, + sortable: true }, initialize: function() { @@ -2690,7 +2692,6 @@ this.scroll = _.chain( this.scroll ).bind( this ).throttle( this.options.refreshSensitivity ).value(); this.initSortable(); - this.collection.props.on( 'change:orderby', this.refreshSortable, this ); _.bindAll( this, 'css' ); this.model.on( 'change:edge change:gutter', this.css, this ); @@ -2734,7 +2735,8 @@ }, initSortable: function() { - var collection = this.collection, + var view = this, + collection = this.collection, from; if ( ! this.options.sortable || ! $.fn.sortable ) @@ -2760,14 +2762,30 @@ // Update the model's index in the collection. // Do so silently, as the view is already accurate. update: function( event, ui ) { - var model = collection.at( from ); + var model = collection.at( from ), + comparator = collection.comparator; + // Temporarily disable the comparator to prevent `add` + // from re-sorting. + delete collection.comparator; + + // Silently shift the model to its new index. collection.remove( model, { silent: true }).add( model, { at: ui.item.index(), silent: true }); + + // Restore the comparator. + collection.comparator = comparator; + + // If the collection is sorted by menu order, + // update the menu order. + view.saveMenuOrder(); + + // Make sure any menu-order-related callbacks are bound. + view.refreshSortable(); } }); @@ -2776,6 +2794,9 @@ collection.props.on( 'change:orderby', function() { this.$el.sortable( 'option', 'disabled', !! collection.comparator ); }, this ); + + this.collection.props.on( 'change:orderby', this.refreshSortable, this ); + this.refreshSortable(); }, refreshSortable: function() { @@ -2783,7 +2804,29 @@ return; // If the `collection` has a `comparator`, disable sorting. - this.$el.sortable( 'option', 'disabled', !! this.collection.comparator ); + var collection = this.collection, + orderby = collection.props.get('orderby'), + enabled = 'menuOrder' === orderby || ! collection.comparator, + hasMenuOrder; + + this.$el.sortable( 'option', 'disabled', ! enabled ); + + // Check if any attachments have a specified menu order. + hasMenuOrder = this.collection.any( function( attachment ) { + return attachment.get('menuOrder'); + }); + + // Always unbind the `saveMenuOrder` callback to prevent multiple + // callbacks stacking up. + this.collection.off( 'change:uploading', this.saveMenuOrder, this ); + + if ( hasMenuOrder ) + this.collection.on( 'change:uploading', this.saveMenuOrder, this ); + + }, + + saveMenuOrder: function() { + this.collection.saveMenuOrder(); }, createAttachmentView: function( attachment ) { @@ -3049,7 +3092,7 @@ }).render() ); } - if ( this.options.sortable ) { + if ( this.options.sortable && ! this.options.filters ) { this.toolbar.set( 'dragInfo', new media.View({ el: $( '
' + l10n.dragInfo + '
' )[0], priority: -40 diff --git a/wp-includes/media.php b/wp-includes/media.php index 9752fd805c..f9cd28ce22 100644 --- a/wp-includes/media.php +++ b/wp-includes/media.php @@ -1433,6 +1433,7 @@ function wp_enqueue_media( $args = array() ) { if ( isset( $args['post'] ) ) { $post = get_post( $args['post'] ); $settings['postId'] = $post->ID; + $settings['nonce']['updatePost'] = wp_create_nonce( 'update-post_' . $post->ID ); } $hier = $post && is_post_type_hierarchical( $post->post_type );