From 2d5a59194da8c3ce99f143780ffd91cfb7027a40 Mon Sep 17 00:00:00 2001 From: Adam Silverstein Date: Mon, 10 Jul 2017 19:07:27 +0000 Subject: [PATCH] Media: library grid view - improve browser history support. Set view state properly when navigating history using the browser back/next button in the media library (grid view). Correctly handle navigating, search, image detail view and image edit mode. Also handle bookmarking/reloading. Props kucrut, joemcgill, afercia. Fixes #31846. git-svn-id: https://develop.svn.wordpress.org/trunk@41021 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/js/media-grid.js | 134 ++++++++++++------ src/wp-includes/js/media/routers/manage.js | 33 ++++- .../views/attachment/details-two-column.js | 10 +- .../js/media/views/frame/edit-attachments.js | 43 ++++-- .../js/media/views/frame/manage.js | 48 ++++--- 5 files changed, 186 insertions(+), 82 deletions(-) diff --git a/src/wp-includes/js/media-grid.js b/src/wp-includes/js/media-grid.js index edf429e896..b6128a3755 100644 --- a/src/wp-includes/js/media-grid.js +++ b/src/wp-includes/js/media-grid.js @@ -50,8 +50,10 @@ media.view.DeleteSelectedPermanentlyButton = require( './views/button/delete-sel */ var Router = Backbone.Router.extend({ routes: { - 'upload.php?item=:slug': 'showItem', - 'upload.php?search=:query': 'search' + 'upload.php?item=:slug&mode=edit': 'editItem', + 'upload.php?item=:slug': 'showItem', + 'upload.php?search=:query': 'search', + 'upload.php': 'reset' }, // Map routes against the page URL @@ -59,6 +61,14 @@ var Router = Backbone.Router.extend({ return 'upload.php' + url; }, + reset: function() { + var frame = wp.media.frames.edit; + + if ( frame ) { + frame.close(); + } + }, + // Respond to the search route by filling the search field and trigggering the input event search: function( query ) { jQuery( '#media-search-input' ).val( query ).trigger( 'input' ); @@ -67,21 +77,30 @@ var Router = Backbone.Router.extend({ // Show the modal with a specific item showItem: function( query ) { var media = wp.media, - library = media.frame.state().get('library'), + frame = media.frames.browse, + library = frame.state().get('library'), item; // Trigger the media frame to open the correct item item = library.findWhere( { id: parseInt( query, 10 ) } ); + item.set( 'skipHistory', true ); + if ( item ) { - media.frame.trigger( 'edit:attachment', item ); + frame.trigger( 'edit:attachment', item ); } else { item = media.attachment( query ); - media.frame.listenTo( item, 'change', function( model ) { - media.frame.stopListening( item ); - media.frame.trigger( 'edit:attachment', model ); + frame.listenTo( item, 'change', function( model ) { + frame.stopListening( item ); + frame.trigger( 'edit:attachment', model ); } ); item.fetch(); } + }, + + // Show the modal in edit mode with a specific item. + editItem: function( query ) { + this.showItem( query ); + wp.media.frames.edit.content.mode( 'edit-details' ); } }); @@ -107,8 +126,16 @@ var Details = wp.media.view.Attachment.Details, TwoColumn = Details.extend({ template: wp.template( 'attachment-details-two-column' ), + initialize: function() { + this.controller.on( 'content:activate:edit-details', _.bind( this.editAttachment, this ) ); + + Details.prototype.initialize.apply( this, arguments ); + }, + editAttachment: function( event ) { - event.preventDefault(); + if ( event ) { + event.preventDefault(); + } this.controller.content.mode( 'edit-image' ); }, @@ -401,13 +428,19 @@ EditAttachments = MediaFrame.extend({ // 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( 'refresh', this.rerender, this ); this.on( 'close', this.detach ); + + this.bindModelHandlers(); + this.listenTo( this.gridRouter, 'route:search', this.close, this ); + }, + + bindModelHandlers: function() { + // Close the modal if the attachment is deleted. + this.listenTo( this.model, 'change:status destroy', this.close, this ); }, createModal: function() { @@ -424,7 +457,6 @@ EditAttachments = MediaFrame.extend({ // Completely destroy the modal DOM element when closing it. this.modal.on( 'close', _.bind( function() { - this.modal.remove(); $( 'body' ).off( 'keydown.media-modal' ); /* remove the keydown event */ // Restore the original focus item if possible $( 'li.attachment[data-id="' + this.model.get( 'id' ) +'"]' ).focus(); @@ -442,7 +474,10 @@ EditAttachments = MediaFrame.extend({ */ createStates: function() { this.states.add([ - new wp.media.controller.EditAttachmentMetadata( { model: this.model } ) + new wp.media.controller.EditAttachmentMetadata({ + model: this.model, + library: this.library + }) ]); }, @@ -467,8 +502,8 @@ EditAttachments = MediaFrame.extend({ model: this.model }) ); - // Update browser url when navigating media details - if ( this.model ) { + // Update browser url when navigating media details, except on load. + if ( this.model && ! this.model.get( 'skipHistory' ) ) { this.gridRouter.navigate( this.gridRouter.baseUrl( '?item=' + this.model.id ) ); } }, @@ -494,6 +529,9 @@ EditAttachments = MediaFrame.extend({ frame: this, controller: editImageController } ); + + this.gridRouter.navigate( this.gridRouter.baseUrl( '?item=' + this.model.id + '&mode=edit' ) ); + }, editImageModeRender: function( view ) { @@ -508,7 +546,13 @@ EditAttachments = MediaFrame.extend({ /** * Rerender the view. */ - rerender: function() { + rerender: function( model ) { + this.stopListening( this.model ); + + this.model = model; + + this.bindModelHandlers(); + // Only rerender the `content` region. if ( this.content.mode() !== 'edit-metadata' ) { this.content.mode( 'edit-metadata' ); @@ -527,8 +571,7 @@ EditAttachments = MediaFrame.extend({ this.$( '.left' ).blur(); return; } - this.model = this.library.at( this.getCurrentIndex() - 1 ); - this.rerender(); + this.trigger( 'refresh', this.library.at( this.getCurrentIndex() - 1 ) ); this.$( '.left' ).focus(); }, @@ -540,8 +583,7 @@ EditAttachments = MediaFrame.extend({ this.$( '.right' ).blur(); return; } - this.model = this.library.at( this.getCurrentIndex() + 1 ); - this.rerender(); + this.trigger( 'refresh', this.library.at( this.getCurrentIndex() + 1 ) ); this.$( '.right' ).focus(); }, @@ -576,7 +618,9 @@ EditAttachments = MediaFrame.extend({ }, resetRoute: function() { - this.gridRouter.navigate( this.gridRouter.baseUrl( '' ) ); + var searchTerm = this.controller.browserView.toolbar.get( 'search' ).$el.val(), + url = '' !== searchTerm ? '?search=' + searchTerm : ''; + this.gridRouter.navigate( this.gridRouter.baseUrl( url ), { replace: true } ); } }); @@ -666,38 +710,42 @@ Manage = MediaFrame.extend({ this.bindRegionModeHandlers(); this.render(); this.bindSearchHandler(); + + wp.media.frames.browse = this; }, bindSearchHandler: function() { var search = this.$( '#media-search-input' ), - currentSearch = this.options.container.data( 'search' ), searchView = this.browserView.toolbar.get( 'search' ).$el, listMode = this.$( '.view-list' ), - input = _.debounce( function (e) { + input = _.throttle( function (e) { var val = $( e.currentTarget ).val(), url = ''; if ( val ) { url += '?search=' + val; + this.gridRouter.navigate( this.gridRouter.baseUrl( url ), { replace: true } ); } - this.gridRouter.navigate( this.gridRouter.baseUrl( url ) ); }, 1000 ); // Update the URL when entering search string (at most once per second) search.on( 'input', _.bind( input, this ) ); - searchView.val( currentSearch ).trigger( 'input' ); - this.gridRouter.on( 'route:search', function () { - var href = window.location.href; - if ( href.indexOf( 'mode=' ) > -1 ) { - href = href.replace( /mode=[^&]+/g, 'mode=list' ); - } else { - href += href.indexOf( '?' ) > -1 ? '&mode=list' : '?mode=list'; - } - href = href.replace( 'search=', 's=' ); - listMode.prop( 'href', href ); - } ); + this.gridRouter + .on( 'route:search', function () { + var href = window.location.href; + if ( href.indexOf( 'mode=' ) > -1 ) { + href = href.replace( /mode=[^&]+/g, 'mode=list' ); + } else { + href += href.indexOf( '?' ) > -1 ? '&mode=list' : '?mode=list'; + } + href = href.replace( 'search=', 's=' ); + listMode.prop( 'href', href ); + }) + .on( 'route:reset', function() { + searchView.val( '' ).trigger( 'input' ); + }); }, /** @@ -789,12 +837,16 @@ Manage = MediaFrame.extend({ */ openEditAttachmentModal: function( model ) { // Create a new EditAttachment frame, passing along the library and the attachment model. - wp.media( { - frame: 'edit-attachments', - controller: this, - library: this.state().get('library'), - model: model - } ); + if ( wp.media.frames.edit ) { + wp.media.frames.edit.open().trigger( 'refresh', model ); + } else { + wp.media.frames.edit = wp.media( { + frame: 'edit-attachments', + controller: this, + library: this.state().get('library'), + model: model + } ); + } }, /** diff --git a/src/wp-includes/js/media/routers/manage.js b/src/wp-includes/js/media/routers/manage.js index 73959699b1..4b88aa3e01 100644 --- a/src/wp-includes/js/media/routers/manage.js +++ b/src/wp-includes/js/media/routers/manage.js @@ -8,8 +8,10 @@ */ var Router = Backbone.Router.extend({ routes: { - 'upload.php?item=:slug': 'showItem', - 'upload.php?search=:query': 'search' + 'upload.php?item=:slug&mode=edit': 'editItem', + 'upload.php?item=:slug': 'showItem', + 'upload.php?search=:query': 'search', + 'upload.php': 'reset' }, // Map routes against the page URL @@ -17,6 +19,14 @@ var Router = Backbone.Router.extend({ return 'upload.php' + url; }, + reset: function() { + var frame = wp.media.frames.edit; + + if ( frame ) { + frame.close(); + } + }, + // Respond to the search route by filling the search field and trigggering the input event search: function( query ) { jQuery( '#media-search-input' ).val( query ).trigger( 'input' ); @@ -25,21 +35,30 @@ var Router = Backbone.Router.extend({ // Show the modal with a specific item showItem: function( query ) { var media = wp.media, - library = media.frame.state().get('library'), + frame = media.frames.browse, + library = frame.state().get('library'), item; // Trigger the media frame to open the correct item item = library.findWhere( { id: parseInt( query, 10 ) } ); + item.set( 'skipHistory', true ); + if ( item ) { - media.frame.trigger( 'edit:attachment', item ); + frame.trigger( 'edit:attachment', item ); } else { item = media.attachment( query ); - media.frame.listenTo( item, 'change', function( model ) { - media.frame.stopListening( item ); - media.frame.trigger( 'edit:attachment', model ); + frame.listenTo( item, 'change', function( model ) { + frame.stopListening( item ); + frame.trigger( 'edit:attachment', model ); } ); item.fetch(); } + }, + + // Show the modal in edit mode with a specific item. + editItem: function( query ) { + this.showItem( query ); + wp.media.frames.edit.content.mode( 'edit-details' ); } }); diff --git a/src/wp-includes/js/media/views/attachment/details-two-column.js b/src/wp-includes/js/media/views/attachment/details-two-column.js index 708287dd15..96a8fed8f2 100644 --- a/src/wp-includes/js/media/views/attachment/details-two-column.js +++ b/src/wp-includes/js/media/views/attachment/details-two-column.js @@ -17,8 +17,16 @@ var Details = wp.media.view.Attachment.Details, TwoColumn = Details.extend({ template: wp.template( 'attachment-details-two-column' ), + initialize: function() { + this.controller.on( 'content:activate:edit-details', _.bind( this.editAttachment, this ) ); + + Details.prototype.initialize.apply( this, arguments ); + }, + editAttachment: function( event ) { - event.preventDefault(); + if ( event ) { + event.preventDefault(); + } this.controller.content.mode( 'edit-image' ); }, diff --git a/src/wp-includes/js/media/views/frame/edit-attachments.js b/src/wp-includes/js/media/views/frame/edit-attachments.js index 3d29ff5111..c0f4e15a7b 100644 --- a/src/wp-includes/js/media/views/frame/edit-attachments.js +++ b/src/wp-includes/js/media/views/frame/edit-attachments.js @@ -59,13 +59,19 @@ EditAttachments = MediaFrame.extend({ // 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( 'refresh', this.rerender, this ); this.on( 'close', this.detach ); + + this.bindModelHandlers(); + this.listenTo( this.gridRouter, 'route:search', this.close, this ); + }, + + bindModelHandlers: function() { + // Close the modal if the attachment is deleted. + this.listenTo( this.model, 'change:status destroy', this.close, this ); }, createModal: function() { @@ -82,7 +88,6 @@ EditAttachments = MediaFrame.extend({ // Completely destroy the modal DOM element when closing it. this.modal.on( 'close', _.bind( function() { - this.modal.remove(); $( 'body' ).off( 'keydown.media-modal' ); /* remove the keydown event */ // Restore the original focus item if possible $( 'li.attachment[data-id="' + this.model.get( 'id' ) +'"]' ).focus(); @@ -100,7 +105,10 @@ EditAttachments = MediaFrame.extend({ */ createStates: function() { this.states.add([ - new wp.media.controller.EditAttachmentMetadata( { model: this.model } ) + new wp.media.controller.EditAttachmentMetadata({ + model: this.model, + library: this.library + }) ]); }, @@ -125,8 +133,8 @@ EditAttachments = MediaFrame.extend({ model: this.model }) ); - // Update browser url when navigating media details - if ( this.model ) { + // Update browser url when navigating media details, except on load. + if ( this.model && ! this.model.get( 'skipHistory' ) ) { this.gridRouter.navigate( this.gridRouter.baseUrl( '?item=' + this.model.id ) ); } }, @@ -152,6 +160,9 @@ EditAttachments = MediaFrame.extend({ frame: this, controller: editImageController } ); + + this.gridRouter.navigate( this.gridRouter.baseUrl( '?item=' + this.model.id + '&mode=edit' ) ); + }, editImageModeRender: function( view ) { @@ -166,7 +177,13 @@ EditAttachments = MediaFrame.extend({ /** * Rerender the view. */ - rerender: function() { + rerender: function( model ) { + this.stopListening( this.model ); + + this.model = model; + + this.bindModelHandlers(); + // Only rerender the `content` region. if ( this.content.mode() !== 'edit-metadata' ) { this.content.mode( 'edit-metadata' ); @@ -185,8 +202,7 @@ EditAttachments = MediaFrame.extend({ this.$( '.left' ).blur(); return; } - this.model = this.library.at( this.getCurrentIndex() - 1 ); - this.rerender(); + this.trigger( 'refresh', this.library.at( this.getCurrentIndex() - 1 ) ); this.$( '.left' ).focus(); }, @@ -198,8 +214,7 @@ EditAttachments = MediaFrame.extend({ this.$( '.right' ).blur(); return; } - this.model = this.library.at( this.getCurrentIndex() + 1 ); - this.rerender(); + this.trigger( 'refresh', this.library.at( this.getCurrentIndex() + 1 ) ); this.$( '.right' ).focus(); }, @@ -234,7 +249,9 @@ EditAttachments = MediaFrame.extend({ }, resetRoute: function() { - this.gridRouter.navigate( this.gridRouter.baseUrl( '' ) ); + var searchTerm = this.controller.browserView.toolbar.get( 'search' ).$el.val(), + url = '' !== searchTerm ? '?search=' + searchTerm : ''; + this.gridRouter.navigate( this.gridRouter.baseUrl( url ), { replace: true } ); } }); diff --git a/src/wp-includes/js/media/views/frame/manage.js b/src/wp-includes/js/media/views/frame/manage.js index a033d7bf28..c260feb7f6 100644 --- a/src/wp-includes/js/media/views/frame/manage.js +++ b/src/wp-includes/js/media/views/frame/manage.js @@ -81,38 +81,42 @@ Manage = MediaFrame.extend({ this.bindRegionModeHandlers(); this.render(); this.bindSearchHandler(); + + wp.media.frames.browse = this; }, bindSearchHandler: function() { var search = this.$( '#media-search-input' ), - currentSearch = this.options.container.data( 'search' ), searchView = this.browserView.toolbar.get( 'search' ).$el, listMode = this.$( '.view-list' ), - input = _.debounce( function (e) { + input = _.throttle( function (e) { var val = $( e.currentTarget ).val(), url = ''; if ( val ) { url += '?search=' + val; + this.gridRouter.navigate( this.gridRouter.baseUrl( url ), { replace: true } ); } - this.gridRouter.navigate( this.gridRouter.baseUrl( url ) ); }, 1000 ); // Update the URL when entering search string (at most once per second) search.on( 'input', _.bind( input, this ) ); - searchView.val( currentSearch ).trigger( 'input' ); - this.gridRouter.on( 'route:search', function () { - var href = window.location.href; - if ( href.indexOf( 'mode=' ) > -1 ) { - href = href.replace( /mode=[^&]+/g, 'mode=list' ); - } else { - href += href.indexOf( '?' ) > -1 ? '&mode=list' : '?mode=list'; - } - href = href.replace( 'search=', 's=' ); - listMode.prop( 'href', href ); - } ); + this.gridRouter + .on( 'route:search', function () { + var href = window.location.href; + if ( href.indexOf( 'mode=' ) > -1 ) { + href = href.replace( /mode=[^&]+/g, 'mode=list' ); + } else { + href += href.indexOf( '?' ) > -1 ? '&mode=list' : '?mode=list'; + } + href = href.replace( 'search=', 's=' ); + listMode.prop( 'href', href ); + }) + .on( 'route:reset', function() { + searchView.val( '' ).trigger( 'input' ); + }); }, /** @@ -204,12 +208,16 @@ Manage = MediaFrame.extend({ */ openEditAttachmentModal: function( model ) { // Create a new EditAttachment frame, passing along the library and the attachment model. - wp.media( { - frame: 'edit-attachments', - controller: this, - library: this.state().get('library'), - model: model - } ); + if ( wp.media.frames.edit ) { + wp.media.frames.edit.open().trigger( 'refresh', model ); + } else { + wp.media.frames.edit = wp.media( { + frame: 'edit-attachments', + controller: this, + library: this.state().get('library'), + model: model + } ); + } }, /**