From 8661fcf0d98af093ed21cc4336b338163bd94a42 Mon Sep 17 00:00:00 2001 From: Scott Taylor Date: Thu, 14 Aug 2014 18:30:49 +0000 Subject: [PATCH] Media Grid, support `MEDIA_TRASH`: * Add a setting to `_wpMediaViewsL10n.settings`: `mediaTrash` * In the attachment edit modal, properly toggle between Trash/Untrash * In `media.view.Attachment`, add a method for `untrashAttachment` * When creating the grid toolbar, switch the setting order of subviews so that `media.view.DeleteSelectedButton` can listen to the instance of `media.view.AttachmentFilters.All` to update the text in its UI. * Add a new filter to `media.view.AttachmentFilters.All`, `trash`, when `settings.mediaTrash` is true * Allow the cached queries in `Query.get()` to be flushed when race conditions exist and collections need to be refreshed. This is currently only being used when `MEDIA_TRASH` is set, to refresh the filtered/mirrored collections related to `all`, `trash`, and any already queried filter. * Cleanup the bootstrapping of `media.view.MediaFrame.Manage` * Allow `wp_ajax_query_attachments()` to return items from the trash when `MEDIA_TRASH` is `true` * Allow `wp_ajax_save_attachment()` to set `post_status` when `MEDIA_TRASH` is `true`. It allows `wp_delete_post()` to be called, which will trash the attachment instead of deleting when the flag is set. Props koop for the knowledge sharing and thought partnership. See #29145. git-svn-id: https://develop.svn.wordpress.org/trunk@29490 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-admin/includes/ajax-actions.php | 19 +++- src/wp-includes/css/media-views.css | 13 ++- src/wp-includes/js/media-grid.js | 62 ++++++++---- src/wp-includes/js/media-models.js | 40 ++++++-- src/wp-includes/js/media-views.js | 133 ++++++++++++++++++------- src/wp-includes/media-template.php | 4 + src/wp-includes/media.php | 4 + 7 files changed, 209 insertions(+), 66 deletions(-) diff --git a/src/wp-admin/includes/ajax-actions.php b/src/wp-admin/includes/ajax-actions.php index 05252004a2..de9263e5d8 100644 --- a/src/wp-admin/includes/ajax-actions.php +++ b/src/wp-admin/includes/ajax-actions.php @@ -2161,7 +2161,14 @@ function wp_ajax_query_attachments() { ) ) ); $query['post_type'] = 'attachment'; - $query['post_status'] = 'inherit'; + if ( MEDIA_TRASH + && ! empty( $_REQUEST['query']['post_status'] ) + && 'trash' === $_REQUEST['query']['post_status'] ) { + $query['post_status'] = 'trash'; + } else { + $query['post_status'] = 'inherit'; + } + if ( current_user_can( get_post_type_object( 'attachment' )->cap->read_private_posts ) ) $query['post_status'] .= ',private'; @@ -2216,6 +2223,9 @@ function wp_ajax_save_attachment() { if ( isset( $changes['description'] ) ) $post['post_content'] = $changes['description']; + if ( MEDIA_TRASH && isset( $changes['status'] ) ) + $post['post_status'] = $changes['status']; + if ( isset( $changes['alt'] ) ) { $alt = wp_unslash( $changes['alt'] ); if ( $alt != get_post_meta( $id, '_wp_attachment_image_alt', true ) ) { @@ -2243,7 +2253,12 @@ function wp_ajax_save_attachment() { } } - wp_update_post( $post ); + if ( MEDIA_TRASH && isset( $changes['status'] ) && 'trash' === $changes['status'] ) { + wp_delete_post( $id ); + } else { + wp_update_post( $post ); + } + wp_send_json_success(); } diff --git a/src/wp-includes/css/media-views.css b/src/wp-includes/css/media-views.css index 67e8ded8f5..847cbab372 100644 --- a/src/wp-includes/css/media-views.css +++ b/src/wp-includes/css/media-views.css @@ -1603,7 +1603,8 @@ .attachment-info .edit-attachment, .attachment-info .refresh-attachment, .attachment-info .delete-attachment, -.attachment-info .trash-attachment { +.attachment-info .trash-attachment, +.attachment-info .untrash-attachment { display: block; text-decoration: none; white-space: nowrap; @@ -1620,12 +1621,14 @@ } .media-modal .delete-attachment, -.media-modal .trash-attachment { +.media-modal .trash-attachment, +.media-modal .untrash-attachment { color: #bc0b0b; } .media-modal .delete-attachment:hover, -.media-modal .trash-attachment:hover { +.media-modal .trash-attachment:hover, +.media-modal .untrash-attachment:hover { color: red; } @@ -2743,7 +2746,9 @@ max-height: calc( 100% - 42px ); /* leave space for actions underneath */ } -.edit-attachment-frame .delete-attachment { +.edit-attachment-frame .delete-attachment, +.edit-attachment-frame .trash-attachment, +.edit-attachment-frame .untrash-attachment { float: right; margin-top: 7px; } diff --git a/src/wp-includes/js/media-grid.js b/src/wp-includes/js/media-grid.js index 7c6a9238cc..12e5a6bcea 100644 --- a/src/wp-includes/js/media-grid.js +++ b/src/wp-includes/js/media-grid.js @@ -183,7 +183,7 @@ // Create a new EditAttachment frame, passing along the library and the attachment model. wp.media( { frame: 'edit-attachments', - gridRouter: this.gridRouter, + controller: this, library: this.state().get('library'), model: model } ); @@ -230,6 +230,9 @@ }, bindDeferred: function() { + if ( ! this.browserView.dfd ) { + return; + } this.browserView.dfd.done( _.bind( this.startHistory, this ) ); }, @@ -352,15 +355,11 @@ regions: [ 'title', 'content' ], events: { - 'click': 'collapse', - 'click .delete-media-item': 'deleteMediaItem', 'click .left': 'previousMediaItem', 'click .right': 'nextMediaItem' }, initialize: function() { - var self = this; - media.view.Frame.prototype.initialize.apply( this, arguments ); _.defaults( this.options, { @@ -368,32 +367,42 @@ state: 'edit-attachment' }); - this.gridRouter = this.options.gridRouter; - + this.controller = this.options.controller; + this.gridRouter = this.controller.gridRouter; this.library = this.options.library; if ( this.options.model ) { this.model = this.options.model; } else { + // this is a hack this.model = this.library.at( 0 ); } - // Close the modal if the attachment is deleted. - this.listenTo( this.model, 'destroy', this.close, this ); - + this.bindHandlers(); this.createStates(); + this.createModal(); + + this.title.mode( 'default' ); + + this.options.hasPrevious = this.hasPrevious(); + this.options.hasNext = this.hasNext(); + }, + + bindHandlers: function() { + // Bind default title creation. + this.on( 'title:create:default', this.createTitle, this ); + + // Close the modal if the attachment is deleted. + this.listenTo( this.model, 'change:status destroy', this.close, this ); this.on( 'content:create:edit-metadata', this.editMetadataMode, this ); this.on( 'content:create:edit-image', this.editImageMode, this ); this.on( 'content:render:edit-image', this.editImageModeRender, this ); this.on( 'close', this.detach ); + }, - // Bind default title creation. - this.on( 'title:create:default', this.createTitle, this ); - this.title.mode( 'default' ); - - this.options.hasPrevious = this.hasPrevious(); - this.options.hasNext = this.hasNext(); + createModal: function() { + var self = this; // Initialize modal container view. if ( this.options.modal ) { @@ -609,16 +618,33 @@ media.view.DeleteSelectedButton = media.view.Button.extend({ initialize: function() { media.view.Button.prototype.initialize.apply( this, arguments ); + if ( this.options.filters ) { + this.listenTo( this.options.filters.model, 'change', this.filterChange ); + } this.listenTo( this.controller, 'selection:toggle', this.toggleDisabled ); }, + filterChange: function( model ) { + if ( 'trash' === model.get( 'status' ) ) { + this.model.set( 'text', l10n.untrashSelected ); + } else if ( media.view.settings.mediaTrash ) { + this.model.set( 'text', l10n.trashSelected ); + } else { + this.model.set( 'text', l10n.deleteSelected ); + } + }, + toggleDisabled: function() { - this.$el.attr( 'disabled', ! this.controller.state().get( 'selection' ).length ); + this.model.set( 'disabled', ! this.controller.state().get( 'selection' ).length ); }, render: function() { media.view.Button.prototype.render.apply( this, arguments ); - this.$el.addClass( 'delete-selected-button hidden' ); + if ( this.controller.isModeActive( 'select' ) ) { + this.$el.addClass( 'delete-selected-button' ); + } else { + this.$el.addClass( 'delete-selected-button hidden' ); + } return this; } }); diff --git a/src/wp-includes/js/media-models.js b/src/wp-includes/js/media-models.js index 78d7d7f9ca..d267de4699 100644 --- a/src/wp-includes/js/media-models.js +++ b/src/wp-includes/js/media-models.js @@ -824,9 +824,12 @@ window.wp = window.wp || {}; /** * @access private */ - _requery: function() { + _requery: function( cache ) { + var props; if ( this.props.get('query') ) { - this.mirror( Query.get( this.props.toJSON() ) ); + props = this.props.toJSON(); + props.cache = ( true !== cache ); + this.mirror( Query.get( props ) ); } }, /** @@ -947,6 +950,22 @@ window.wp = window.wp || {}; } return uploadedTo === attachment.get('uploadedTo'); + }, + /** + * @static + * @param {wp.media.model.Attachment} attachment + * + * @this wp.media.model.Attachments + * + * @returns {Boolean} + */ + status: function( attachment ) { + var status = this.props.get('status'); + if ( _.isUndefined( status ) ) { + return true; + } + + return status === attachment.get('status'); } } }); @@ -1144,7 +1163,8 @@ window.wp = window.wp || {}; 'type': 'post_mime_type', 'perPage': 'posts_per_page', 'menuOrder': 'menu_order', - 'uploadedTo': 'post_parent' + 'uploadedTo': 'post_parent', + 'status': 'post_status' }, /** * @static @@ -1169,11 +1189,13 @@ window.wp = window.wp || {}; var args = {}, orderby = Query.orderby, defaults = Query.defaultProps, - query; + query, + cache = !! props.cache; // Remove the `query` property. This isn't linked to a query, // this *is* the query. delete props.query; + delete props.cache; // Fill default args. _.defaults( props, defaults ); @@ -1207,9 +1229,13 @@ window.wp = window.wp || {}; args.orderby = orderby.valuemap[ props.orderby ] || props.orderby; // Search the query cache for matches. - query = _.find( queries, function( query ) { - return _.isEqual( query.args, args ); - }); + if ( cache ) { + query = _.find( queries, function( query ) { + return _.isEqual( query.args, args ); + }); + } else { + queries = []; + } // Otherwise, create a new query and add it to the cache. if ( ! query ) { diff --git a/src/wp-includes/js/media-views.js b/src/wp-includes/js/media-views.js index 2945df3541..5ac0e268eb 100644 --- a/src/wp-includes/js/media-views.js +++ b/src/wp-includes/js/media-views.js @@ -5671,6 +5671,7 @@ filters[ key ] = { text: text, props: { + status: null, type: key, uploadedTo: null, orderby: 'date', @@ -5682,6 +5683,7 @@ filters.all = { text: l10n.allMediaItems, props: { + status: null, type: null, uploadedTo: null, orderby: 'date', @@ -5694,6 +5696,7 @@ filters.uploaded = { text: l10n.uploadedToThisPost, props: { + status: null, type: null, uploadedTo: media.view.settings.post.id, orderby: 'menuOrder', @@ -5706,6 +5709,7 @@ filters.unattached = { text: l10n.unattached, props: { + status: null, uploadedTo: 0, type: null, orderby: 'menuOrder', @@ -5714,6 +5718,20 @@ priority: 50 }; + if ( media.view.settings.mediaTrash ) { + filters.trash = { + text: l10n.trash, + props: { + uploadedTo: null, + status: 'trash', + type: null, + orderby: 'date', + order: 'DESC' + }, + priority: 50 + }; + } + this.filters = filters; } }); @@ -5765,9 +5783,7 @@ }, createToolbar: function() { - var filters, - LibraryViewSwitcher, - FiltersConstructor; + var LibraryViewSwitcher, Filters; /** * @member {wp.media.view.Toolbar} @@ -5778,6 +5794,38 @@ this.views.add( this.toolbar ); + this.toolbar.set( 'spinner', new media.view.Spinner({ + priority: -60 + }) ); + + if ( -1 !== $.inArray( this.options.filters, [ 'uploaded', 'all' ] ) ) { + // "Filters" will return a , need to render - // screen reader text before - this.toolbar.set( 'filtersLabel', new media.view.Label({ - value: l10n.filterByType, - attributes: { - 'for': 'media-attachment-filters' - }, - priority: -80 - }).render() ); - this.toolbar.set( 'filters', new FiltersConstructor({ - controller: this.controller, - model: this.collection.props, - priority: -80 - }).render() ); - } - if ( this.options.search ) { // Search is an input, screen reader text needs to be rendered before this.toolbar.set( 'searchLabel', new media.view.Label({ @@ -6420,6 +6462,7 @@ 'change [data-setting] textarea': 'updateSetting', 'click .delete-attachment': 'deleteAttachment', 'click .trash-attachment': 'trashAttachment', + 'click .untrash-attachment': 'untrashAttachment', 'click .edit-attachment': 'editAttachment', 'click .refresh-attachment': 'refreshAttachment', 'keydown': 'toggleSelectionHandler' @@ -6453,9 +6496,29 @@ * @param {Object} event */ trashAttachment: function( event ) { + var library = this.controller.library; event.preventDefault(); - this.model.destroy(); + if ( media.view.settings.mediaTrash ) { + this.model.set( 'status', 'trash' ); + this.model.save().done( function() { + library._requery( true ); + } ); + } else { + this.model.destroy(); + } + }, + /** + * @param {Object} event + */ + untrashAttachment: function( event ) { + var library = this.controller.library; + event.preventDefault(); + + this.model.set( 'status', 'inherit' ); + this.model.save().done( function() { + library._requery( true ); + } ); }, /** * @param {Object} event diff --git a/src/wp-includes/media-template.php b/src/wp-includes/media-template.php index fa2dcf4a85..dee10bb02f 100644 --- a/src/wp-includes/media-template.php +++ b/src/wp-includes/media-template.php @@ -316,7 +316,11 @@ function wp_print_media_templates() { <# if ( ! data.uploading && data.can.remove ) { #> + <# if ( 'trash' === data.status ) { #> + + <# } else { #> + <# } #> diff --git a/src/wp-includes/media.php b/src/wp-includes/media.php index 39b98a4830..c1bd2855f1 100644 --- a/src/wp-includes/media.php +++ b/src/wp-includes/media.php @@ -2869,6 +2869,7 @@ function wp_enqueue_media( $args = array() ) { 'embedMimes' => $ext_mimes, 'contentWidth' => $content_width, 'months' => $months, + 'mediaTrash' => MEDIA_TRASH ? 1 : 0 ); $post = null; @@ -2931,11 +2932,14 @@ function wp_enqueue_media( $args = array() ) { 'noItemsFound' => __( 'No items found.' ), 'insertIntoPost' => $hier ? __( 'Insert into page' ) : __( 'Insert into post' ), 'unattached' => __( 'Unattached' ), + 'trash' => __( 'Trash' ), 'uploadedToThisPost' => $hier ? __( 'Uploaded to this page' ) : __( 'Uploaded to this post' ), 'warnDelete' => __( "You are about to permanently delete this item.\n 'Cancel' to stop, 'OK' to delete." ), 'warnBulkDelete' => __( "You are about to permanently delete these items.\n 'Cancel' to stop, 'OK' to delete." ), 'bulkSelect' => __( 'Bulk Select' ), 'cancelSelection' => __( 'Cancel Selection' ), + 'trashSelected' => __( 'Trash Selected' ), + 'untrashSelected' => __( 'Untrash Selected' ), 'deleteSelected' => __( 'Delete Selected' ), 'deletePermanently' => __( 'Delete Permanently' ), 'apply' => __( 'Apply' ),