diff -r 3d4e9c994f10 -r a86126ab1dd4 wp/wp-includes/js/media-views.js --- a/wp/wp-includes/js/media-views.js Tue Oct 22 16:11:46 2019 +0200 +++ b/wp/wp-includes/js/media-views.js Tue Dec 15 13:49:49 2020 +0100 @@ -115,7 +115,7 @@ // Copy the `post` setting over to the model settings. media.model.settings.post = media.view.settings.post; -// Check if the browser supports CSS 3.0 transitions +// Check if the browser supports CSS 3.0 transitions. $.support.transition = (function(){ var style = document.documentElement.style, transitions = { @@ -145,8 +145,8 @@ * Makes it easier to bind events using transitions. * * @param {string} selector - * @param {Number} sensitivity - * @returns {Promise} + * @param {number} sensitivity + * @return {Promise} */ media.transition = function( selector, sensitivity ) { var deferred = $.Deferred(); @@ -272,7 +272,7 @@ * * @class * - * @param {object} options Options hash for the region. + * @param {Object} options Options hash for the region. * @param {string} options.id Unique identifier for the region. * @param {Backbone.View} options.view A parent view the region exists within. * @param {string} options.selector jQuery selector for the region within the parent view. @@ -295,7 +295,7 @@ * @fires Region#activate * @fires Region#deactivate * - * @returns {wp.media.controller.Region} Returns itself to allow chaining. + * @return {wp.media.controller.Region} Returns itself to allow chaining. */ mode: function( mode ) { if ( ! mode ) { @@ -334,7 +334,7 @@ * @fires Region#create * @fires Region#render * - * @returns {wp.media.controller.Region} Returns itself to allow chaining + * @return {wp.media.controller.Region} Returns itself to allow chaining. */ render: function( mode ) { // If the mode isn't active, activate it. @@ -377,7 +377,7 @@ * * @since 3.5.0 * - * @returns {wp.media.View} + * @return {wp.media.View} */ get: function() { return this.view.views.first( this.selector ); @@ -390,7 +390,7 @@ * * @param {Array|Object} views * @param {Object} [options={}] - * @returns {wp.Backbone.Subviews} Subviews is returned to allow chaining + * @return {wp.Backbone.Subviews} Subviews is returned to allow chaining. */ set: function( views, options ) { if ( options ) { @@ -405,7 +405,7 @@ * @since 3.5.0 * * @param {string} event - * @returns {undefined|wp.media.controller.Region} Returns itself to allow chaining. + * @return {undefined|wp.media.controller.Region} Returns itself to allow chaining. */ trigger: function( event ) { var base, args; @@ -451,17 +451,14 @@ * @augments Backbone.Model * @mixin * @mixes Backbone.Events - * - * @param {Array} states */ -var StateMachine = function( states ) { - // @todo This is dead code. The states collection gets created in media.view.Frame._createStates. - this.states = new Backbone.Collection( states ); +var StateMachine = function() { + return { + // Use Backbone's self-propagating `extend` inheritance method. + extend: Backbone.Model.extend + }; }; -// Use Backbone's self-propagating `extend` inheritance method. -StateMachine.extend = Backbone.Model.extend; - _.extend( StateMachine.prototype, Backbone.Events,/** @lends wp.media.controller.StateMachine.prototype */{ /** * Fetch a state. @@ -471,13 +468,13 @@ * Implicitly creates states. * * Ensure that the `states` collection exists so the `StateMachine` - * can be used as a mixin. + * can be used as a mixin. * * @since 3.5.0 * * @param {string} id - * @returns {wp.media.controller.State} Returns a State model - * from the StateMachine collection + * @return {wp.media.controller.State} Returns a State model from + * the StateMachine collection. */ state: function( id ) { this.states = this.states || new Backbone.Collection(); @@ -505,7 +502,7 @@ * @fires wp.media.controller.State#deactivate * @fires wp.media.controller.State#activate * - * @returns {wp.media.controller.StateMachine} Returns itself to allow chaining + * @return {wp.media.controller.StateMachine} Returns itself to allow chaining. */ setState: function( id ) { var previous = this.state(); @@ -533,8 +530,8 @@ * * @since 3.5.0 * - * @returns {wp.media.controller.State} Returns a State model - * from the StateMachine collection + * @return {wp.media.controller.State} Returns a State model from + * the StateMachine collection. */ lastState: function() { if ( this._lastState ) { @@ -549,19 +546,19 @@ * @function on * @memberOf wp.media.controller.StateMachine * @instance - * @returns {wp.media.controller.StateMachine} Returns itself to allow chaining. + * @return {wp.media.controller.StateMachine} Returns itself to allow chaining. */ /** * @function off * @memberOf wp.media.controller.StateMachine * @instance - * @returns {wp.media.controller.StateMachine} Returns itself to allow chaining. + * @return {wp.media.controller.StateMachine} Returns itself to allow chaining. */ /** * @function trigger * @memberOf wp.media.controller.StateMachine * @instance - * @returns {wp.media.controller.StateMachine} Returns itself to allow chaining. + * @return {wp.media.controller.StateMachine} Returns itself to allow chaining. */ StateMachine.prototype[ method ] = function() { // Ensure that the `states` collection exists so the `StateMachine` @@ -653,24 +650,24 @@ reset: function() {}, /** + * @since 3.5.0 * @access private - * @since 3.5.0 */ _ready: function() { this._updateMenu(); }, /** + * @since 3.5.0 * @access private - * @since 3.5.0 */ _preActivate: function() { this.active = true; }, /** + * @since 3.5.0 * @access private - * @since 3.5.0 */ _postActivate: function() { this.on( 'change:menu', this._menu, this ); @@ -688,8 +685,8 @@ }, /** + * @since 3.5.0 * @access private - * @since 3.5.0 */ _deactivate: function() { this.active = false; @@ -703,24 +700,24 @@ }, /** + * @since 3.5.0 * @access private - * @since 3.5.0 */ _title: function() { this.frame.title.render( this.get('titleMode') || 'default' ); }, /** + * @since 3.5.0 * @access private - * @since 3.5.0 */ _renderTitle: function( view ) { view.$el.text( this.get('title') || '' ); }, /** + * @since 3.5.0 * @access private - * @since 3.5.0 */ _router: function() { var router = this.frame.router, @@ -741,8 +738,8 @@ }, /** + * @since 3.5.0 * @access private - * @since 3.5.0 */ _menu: function() { var menu = this.frame.menu, @@ -763,8 +760,8 @@ }, /** + * @since 3.5.0 * @access private - * @since 3.5.0 */ _updateMenu: function() { var previous = this.previous('menu'), @@ -782,8 +779,8 @@ /** * Create a view in the media menu for the state. * + * @since 3.5.0 * @access private - * @since 3.5.0 * * @param {media.view.Menu} view The menu view. */ @@ -851,10 +848,12 @@ return; } - // If the selection supports multiple items, validate the stored - // attachments based on the new selection's conditions. Record - // the attachments that are not included; we'll maintain a - // reference to those. Other attachments are considered in flux. + /* + * If the selection supports multiple items, validate the stored + * attachments based on the new selection's conditions. Record + * the attachments that are not included; we'll maintain a + * reference to those. Other attachments are considered in flux. + */ if ( selection.multiple ) { selection.reset( [], { silent: true }); selection.validateAll( manager.attachments ); @@ -1054,7 +1053,7 @@ * @since 3.5.0 * * @param {wp.media.model.Attachment} attachment - * @returns {Backbone.Model} + * @return {Backbone.Model} */ display: function( attachment ) { var displays = this._displays; @@ -1071,12 +1070,13 @@ * @since 3.6.0 * * @param {wp.media.model.Attachment} attachment - * @returns {Object} + * @return {Object} */ defaultDisplaySettings: function( attachment ) { var settings = _.clone( this._defaultDisplaySettings ); - if ( settings.canEmbed = this.canEmbed( attachment ) ) { + settings.canEmbed = this.canEmbed( attachment ); + if ( settings.canEmbed ) { settings.link = 'embed'; } else if ( ! this.isImageAttachment( attachment ) && settings.link === 'none' ) { settings.link = 'file'; @@ -1091,7 +1091,7 @@ * @since 4.4.1 * * @param {wp.media.model.Attachment} attachment - * @returns {Boolean} + * @return {boolean} */ isImageAttachment: function( attachment ) { // If uploading, we know the filename but not the mime type. @@ -1108,7 +1108,7 @@ * @since 3.6.0 * * @param {wp.media.model.Attachment} attachment - * @returns {Boolean} + * @return {boolean} */ canEmbed: function( attachment ) { // If uploading, we know the filename but not the mime type. @@ -1499,7 +1499,7 @@ * * @since 3.5.0 * - * @returns {void} + * @return {void} */ initialize: function() { if ( ! this.get('library') ) { @@ -1517,7 +1517,7 @@ * * @since 3.5.0 * - * @returns {void} + * @return {void} */ activate: function() { var library = this.get('library'), @@ -1740,8 +1740,8 @@ * @augments Backbone.Model * * @param {object} [attributes] The attributes hash passed to the state. - * @param {string} [attributes.id=library] Unique identifier. - * @param {string} attributes.title Title for the state. Displays in the frame's title region. + * @param {string} [attributes.id=library] Unique identifier. + * @param {string} attributes.title Title for the state. Displays in the frame's title region. * @param {boolean} [attributes.multiple=add] Whether multi-select is enabled. @todo 'add' doesn't seem do anything special, and gets used as a boolean. * @param {wp.media.model.Attachments} [attributes.library] The attachments collection to browse. * If one is not supplied, a collection of attachments of the specified type will be created. @@ -1759,8 +1759,8 @@ * @param {int} [attributes.priority=100] The priority for the state link in the media menu. * @param {boolean} [attributes.syncSelection=false] Whether the Attachments selection should be persisted from the last state. * Defaults to false because for this state, because the library of the Edit Gallery state is the selection. - * @param {string} attributes.type The collection's media type. (e.g. 'video'). - * @param {string} attributes.collectionType The collection type. (e.g. 'playlist'). + * @param {string} attributes.type The collection's media type. (e.g. 'video'). + * @param {string} attributes.collectionType The collection type. (e.g. 'playlist'). */ CollectionAdd = Library.extend(/** @lends wp.media.controller.CollectionAdd.prototype */{ defaults: _.defaults( { @@ -1812,9 +1812,11 @@ return !! this.mirroring.get( attachment.cid ) && ! edit.get( attachment.cid ) && Selection.prototype.validator.apply( this, arguments ); }; - // Reset the library to ensure that all attachments are re-added - // to the collection. Do so silently, as calling `observe` will - // trigger the `reset` event. + /* + * Reset the library to ensure that all attachments are re-added + * to the collection. Do so silently, as calling `observe` will + * trigger the `reset` event. + */ library.reset( library.mirroring.models, { silent: true }); library.observe( edit ); this.set('editLibrary', edit); @@ -2110,7 +2112,7 @@ * * @since 3.9.0 * - * @returns {void} + * @return {void} */ activate: function() { this.frame.on( 'toolbar:render:edit-image', _.bind( this.toolbar, this ) ); @@ -2121,7 +2123,7 @@ * * @since 3.9.0 * - * @returns {void} + * @return {void} */ deactivate: function() { this.frame.off( 'toolbar:render:edit-image' ); @@ -2136,7 +2138,7 @@ * * @since 3.9.0 * - * @returns {void} + * @return {void} */ toolbar: function() { var frame = this.frame, @@ -2291,9 +2293,11 @@ scanners: [] }; - // Scan is triggered with the list of `attributes` to set on the - // state, useful for the 'type' attribute and 'scanners' attribute, - // an array of promise objects for asynchronous scan operations. + /* + * Scan is triggered with the list of `attributes` to set on the + * state, useful for the 'type' attribute and 'scanners' attribute, + * an array of promise objects for asynchronous scan operations. + */ if ( this.props.get('url') ) { this.trigger( 'scan', attributes ); } @@ -2401,7 +2405,7 @@ * * @since 4.2.0 * - * @returns {void} + * @return {void} */ activate: function() { this.frame.on( 'content:create:crop', this.createCropContent, this ); @@ -2414,7 +2418,7 @@ * * @since 4.2.0 * - * @returns {void} + * @return {void} */ deactivate: function() { this.frame.toolbar.mode('browse'); @@ -2429,7 +2433,7 @@ * * @fires crop window * - * @returns {void} + * @return {void} */ createCropContent: function() { this.cropperView = new wp.media.view.Cropper({ @@ -2446,7 +2450,7 @@ * * @since 4.2.0 * - * @returns {void} + * @return {void} */ removeCropper: function() { this.imgSelect.cancelSelection(); @@ -2460,7 +2464,7 @@ * * @since 4.2.0 * - * @returns {void} + * @return {void} */ createCropToolbar: function() { var canSkipCrop, toolbarOptions; @@ -2522,7 +2526,7 @@ * * @since 4.2.0 * - * @returns {$.promise} A jQuery promise with the custom header crop details. + * @return {$.promise} A jQuery promise with the custom header crop details. */ doCrop: function( attachment ) { return wp.ajax.post( 'custom-header-crop', _.extend( @@ -2568,7 +2572,7 @@ * * @param {Object} attachment The attachment to crop. * - * @returns {$.promise} A jQuery promise that represents the crop image request. + * @return {$.promise} A jQuery promise that represents the crop image request. */ doCrop: function( attachment ) { var cropDetails = attachment.get( 'cropDetails' ), @@ -2688,12 +2692,14 @@ * before Backbone 0.9.8 came out. Figure out if Backbone core takes * care of this in Backbone.View now. * - * @returns {wp.media.View} Returns itself to allow chaining + * @return {wp.media.View} Returns itself to allow chaining. */ dispose: function() { - // Undelegating events, removing events from the model, and - // removing events from the controller mirror the code for - // `Backbone.View.dispose` in Backbone 0.9.8 development. + /* + * Undelegating events, removing events from the model, and + * removing events from the controller mirror the code for + * `Backbone.View.dispose` in Backbone 0.9.8 development. + */ this.undelegateEvents(); if ( this.model && this.model.off ) { @@ -2712,7 +2718,7 @@ return this; }, /** - * @returns {wp.media.View} Returns itself to allow chaining + * @return {wp.media.View} Returns itself to allow chaining. */ remove: function() { this.dispose(); @@ -2812,7 +2818,7 @@ /** * Reset all states on the frame to their defaults. * - * @returns {wp.media.view.Frame} Returns itself to allow chaining + * @return {wp.media.view.Frame} Returns itself to allow chaining. */ reset: function() { this.states.invoke( 'trigger', 'reset' ); @@ -2846,7 +2852,7 @@ * Activate a mode on the frame. * * @param string mode Mode ID. - * @returns {this} Returns itself to allow chaining. + * @return {this} Returns itself to allow chaining. */ activateMode: function( mode ) { // Bail if the mode is already active. @@ -2863,7 +2869,7 @@ * Deactivate a mode on the frame. * * @param string mode Mode ID. - * @returns {this} Returns itself to allow chaining. + * @return {this} Returns itself to allow chaining. */ deactivateMode: function( mode ) { // Bail if the mode isn't active. @@ -2884,7 +2890,7 @@ /** * Check if a mode is enabled on the frame. * - * @param string mode Mode ID. + * @param string mode Mode ID. * @return bool */ isModeActive: function( mode ) { @@ -2903,6 +2909,7 @@ /***/ (function(module, exports) { var Frame = wp.media.view.Frame, + l10n = wp.media.view.l10n, $ = jQuery, MediaFrame; @@ -2926,7 +2933,7 @@ regions: ['menu','title','content','toolbar','router'], events: { - 'click div.media-frame-title h1': 'toggleMenu' + 'click .media-frame-menu-toggle': 'toggleMenu' }, /** @@ -2936,7 +2943,7 @@ Frame.prototype.initialize.apply( this, arguments ); _.defaults( this.options, { - title: '', + title: l10n.mediaFrameDefaultTitle, modal: true, uploader: true }); @@ -2978,15 +2985,75 @@ this.on( 'title:create:default', this.createTitle, this ); this.title.mode('default'); - this.on( 'title:render', function( view ) { - view.$el.append( '' ); - }); - // Bind default menu. this.on( 'menu:create:default', this.createMenu, this ); - }, - /** - * @returns {wp.media.view.MediaFrame} Returns itself to allow chaining + + // Set the menu ARIA tab panel attributes when the modal opens. + this.on( 'open', this.setMenuTabPanelAriaAttributes, this ); + // Set the router ARIA tab panel attributes when the modal opens. + this.on( 'open', this.setRouterTabPanelAriaAttributes, this ); + + // Update the menu ARIA tab panel attributes when the content updates. + this.on( 'content:render', this.setMenuTabPanelAriaAttributes, this ); + // Update the router ARIA tab panel attributes when the content updates. + this.on( 'content:render', this.setRouterTabPanelAriaAttributes, this ); + }, + + /** + * Sets the attributes to be used on the menu ARIA tab panel. + * + * @since 5.3.0 + * + * @return {void} + */ + setMenuTabPanelAriaAttributes: function() { + var stateId = this.state().get( 'id' ), + tabPanelEl = this.$el.find( '.media-frame-tab-panel' ), + ariaLabelledby; + + tabPanelEl.removeAttr( 'role aria-labelledby tabindex' ); + + if ( this.state().get( 'menu' ) && this.menuView && this.menuView.isVisible ) { + ariaLabelledby = 'menu-item-' + stateId; + + // Set the tab panel attributes only if the tabs are visible. + tabPanelEl + .attr( { + role: 'tabpanel', + 'aria-labelledby': ariaLabelledby, + tabIndex: '0' + } ); + } + }, + + /** + * Sets the attributes to be used on the router ARIA tab panel. + * + * @since 5.3.0 + * + * @return {void} + */ + setRouterTabPanelAriaAttributes: function() { + var tabPanelEl = this.$el.find( '.media-frame-content' ), + ariaLabelledby; + + tabPanelEl.removeAttr( 'role aria-labelledby tabindex' ); + + // Set the tab panel attributes only if the tabs are visible. + if ( this.state().get( 'router' ) && this.routerView && this.routerView.isVisible && this.content._mode ) { + ariaLabelledby = 'menu-item-' + this.content._mode; + + tabPanelEl + .attr( { + role: 'tabpanel', + 'aria-labelledby': ariaLabelledby, + tabIndex: '0' + } ); + } + }, + + /** + * @return {wp.media.view.MediaFrame} Returns itself to allow chaining. */ render: function() { // Activate the default state if no active state exists. @@ -3014,12 +3081,22 @@ */ createMenu: function( menu ) { menu.view = new wp.media.view.Menu({ - controller: this + controller: this, + + attributes: { + role: 'tablist', + 'aria-orientation': 'vertical' + } }); - }, - - toggleMenu: function() { - this.$el.find( '.media-menu' ).toggleClass( 'visible' ); + + this.menuView = menu.view; + }, + + toggleMenu: function( event ) { + var menu = this.$el.find( '.media-menu' ); + + menu.toggleClass( 'visible' ); + $( event.target ).attr( 'aria-expanded', menu.hasClass( 'visible' ) ); }, /** @@ -3037,8 +3114,15 @@ */ createRouter: function( router ) { router.view = new wp.media.view.Router({ - controller: this + controller: this, + + attributes: { + role: 'tablist', + 'aria-orientation': 'horizontal' + } }); + + this.routerView = router.view; }, /** * @param {Object} options @@ -3142,35 +3226,35 @@ * @memberOf wp.media.view.MediaFrame * @instance * - * @returns {wp.media.view.MediaFrame} Returns itself to allow chaining + * @return {wp.media.view.MediaFrame} Returns itself to allow chaining. */ /** * @function close * @memberOf wp.media.view.MediaFrame * @instance * - * @returns {wp.media.view.MediaFrame} Returns itself to allow chaining + * @return {wp.media.view.MediaFrame} Returns itself to allow chaining. */ /** * @function attach * @memberOf wp.media.view.MediaFrame * @instance * - * @returns {wp.media.view.MediaFrame} Returns itself to allow chaining + * @return {wp.media.view.MediaFrame} Returns itself to allow chaining. */ /** * @function detach * @memberOf wp.media.view.MediaFrame * @instance * - * @returns {wp.media.view.MediaFrame} Returns itself to allow chaining + * @return {wp.media.view.MediaFrame} Returns itself to allow chaining. */ /** * @function escape * @memberOf wp.media.view.MediaFrame * @instance * - * @returns {wp.media.view.MediaFrame} Returns itself to allow chaining + * @return {wp.media.view.MediaFrame} Returns itself to allow chaining. */ MediaFrame.prototype[ method ] = function() { if ( this.modal ) { @@ -3247,6 +3331,16 @@ }; }, + editImageContent: function() { + var image = this.state().get('image'), + view = new wp.media.view.EditImage( { model: image, controller: this } ).render(); + + this.content.set( view ); + + // After creating the wrapper view, load the actual editor via an Ajax call. + view.loadEditor(); + }, + /** * Create the default states on the frame. */ @@ -3265,7 +3359,8 @@ multiple: options.multiple, title: options.title, priority: 20 - }) + }), + new wp.media.controller.EditImage( { model: options.editImage } ) ]); }, @@ -3280,6 +3375,7 @@ this.on( 'content:create:browse', this.browseContent, this ); this.on( 'content:render:upload', this.uploadContent, this ); this.on( 'toolbar:create:select', this.createSelectToolbar, this ); + this.on( 'content:render:edit-image', this.editImageContent, this ); }, /** @@ -3545,7 +3641,7 @@ this.on( 'activate', this.activate, this ); - // Only bother checking media type counts if one of the counts is zero + // Only bother checking media type counts if one of the counts is zero. checkCounts = _.find( this.counts, function( type ) { return type.count === 0; } ); @@ -3600,7 +3696,7 @@ }, activate: function() { - // Hide menu items for states tied to particular media types if there are no items + // Hide menu items for states tied to particular media types if there are no items. _.each( this.counts, function( type ) { if ( type.count < 1 ) { this.menuItemVisibility( type.state, 'hide' ); @@ -3615,15 +3711,18 @@ } }, - // Menus + // Menus. /** * @param {wp.Backbone.View} view */ mainMenu: function( view ) { view.set({ 'library-separator': new wp.media.View({ - className: 'separator', - priority: 100 + className: 'separator', + priority: 100, + attributes: { + role: 'presentation' + } }) }); }, @@ -3655,8 +3754,7 @@ frame.close(); } - // Keep focus inside media modal - // after canceling a gallery + // Move focus to the modal after canceling a Gallery. this.controller.modal.focusManager.focus(); } }, @@ -3682,6 +3780,9 @@ } else { frame.close(); } + + // Move focus to the modal after canceling an Audio Playlist. + this.controller.modal.focusManager.focus(); } }, separateCancel: new wp.media.View({ @@ -3706,6 +3807,9 @@ } else { frame.close(); } + + // Move focus to the modal after canceling a Video Playlist. + this.controller.modal.focusManager.focus(); } }, separateCancel: new wp.media.View({ @@ -3715,7 +3819,7 @@ }); }, - // Content + // Content. embedContent: function() { var view = new wp.media.view.Embed({ controller: this, @@ -3723,10 +3827,6 @@ }).render(); this.content.set( view ); - - if ( ! wp.media.isTouchDevice ) { - view.url.focus(); - } }, editSelectionContent: function() { @@ -3753,13 +3853,15 @@ click: function() { this.controller.content.mode('browse'); + // Move focus to the modal when jumping back from Edit Selection to Add Media view. + this.controller.modal.focusManager.focus(); } }); // Browse our library of attachments. this.content.set( view ); - // Trigger the controller to set focus + // Trigger the controller to set focus. this.trigger( 'edit:selection', this ); }, @@ -3769,12 +3871,12 @@ this.content.set( view ); - // after creating the wrapper view, load the actual editor via an ajax call + // After creating the wrapper view, load the actual editor via an Ajax call. view.loadEditor(); }, - // Toolbars + // Toolbars. /** * @param {wp.Backbone.View} view @@ -3848,10 +3950,10 @@ multiple: true }) ); - this.controller.setState('gallery-edit'); - - // Keep focus inside media modal - // after jumping to gallery view + // Jump to Edit Gallery view. + this.controller.setState( 'gallery-edit' ); + + // Move focus to the modal after jumping to Edit Gallery view. this.controller.modal.focusManager.focus(); } }); @@ -3878,10 +3980,10 @@ multiple: true }) ); - this.controller.setState('playlist-edit'); - - // Keep focus inside media modal - // after jumping to playlist view + // Jump to Edit Audio Playlist view. + this.controller.setState( 'playlist-edit' ); + + // Move focus to the modal after jumping to Edit Audio Playlist view. this.controller.modal.focusManager.focus(); } }); @@ -3908,10 +4010,10 @@ multiple: true }) ); - this.controller.setState('video-playlist-edit'); - - // Keep focus inside media modal - // after jumping to video playlist view + // Jump to Edit Video Playlist view. + this.controller.setState( 'video-playlist-edit' ); + + // Move focus to the modal after jumping to Edit Video Playlist view. this.controller.modal.focusManager.focus(); } }); @@ -3981,6 +4083,8 @@ edit.get('library').add( state.get('selection').models ); state.trigger('reset'); controller.setState('gallery-edit'); + // Move focus to the modal when jumping back from Add to Gallery to Edit Gallery view. + this.controller.modal.focusManager.focus(); } } } @@ -4038,6 +4142,8 @@ edit.get('library').add( state.get('selection').models ); state.trigger('reset'); controller.setState('playlist-edit'); + // Move focus to the modal when jumping back from Add to Audio Playlist to Edit Audio Playlist view. + this.controller.modal.focusManager.focus(); } } } @@ -4092,6 +4198,8 @@ edit.get('library').add( state.get('selection').models ); state.trigger('reset'); controller.setState('video-playlist-edit'); + // Move focus to the modal when jumping back from Add to Video Playlist to Edit Video Playlist view. + this.controller.modal.focusManager.focus(); } } } @@ -4151,7 +4259,7 @@ this.on( 'content:create:image-details', this.imageDetailsContent, this ); this.on( 'content:render:edit-image', this.editImageContent, this ); this.on( 'toolbar:render:image-details', this.renderImageDetailsToolbar, this ); - // override the select toolbar + // Override the select toolbar. this.on( 'toolbar:render:replace', this.renderReplaceImageToolbar, this ); }, @@ -4199,7 +4307,7 @@ this.content.set( view ); - // after bringing in the frame, load the actual editor via an ajax call + // After bringing in the frame, load the actual editor via an Ajax call. view.loadEditor(); }, @@ -4219,8 +4327,8 @@ controller.close(); - // not sure if we want to use wp.media.string.image which will create a shortcode or - // perhaps wp.html.string to at least to build the + // Not sure if we want to use wp.media.string.image which will create a shortcode or + // perhaps wp.html.string to at least to build the . state.trigger( 'update', controller.image.toJSON() ); // Restore and reset the default state. @@ -4242,7 +4350,7 @@ items: { back: { text: l10n.back, - priority: 20, + priority: 80, click: function() { if ( previous ) { frame.setState( previous ); @@ -4255,7 +4363,7 @@ replace: { style: 'primary', text: l10n.replace, - priority: 80, + priority: 20, requires: { selection: true }, click: function() { @@ -4268,8 +4376,8 @@ controller.image.changeAttachment( attachment, state.display( attachment ) ); - // not sure if we want to use wp.media.string.image which will create a shortcode or - // perhaps wp.html.string to at least to build the + // Not sure if we want to use wp.media.string.image which will create a shortcode or + // perhaps wp.html.string to at least to build the . state.trigger( 'replace', controller.image.toJSON() ); // Restore and reset the default state. @@ -4329,7 +4437,7 @@ }); }, /** - * @returns {Object} + * @return {Object} */ prepare: function() { return { @@ -4339,7 +4447,7 @@ }, /** - * @returns {wp.media.view.Modal} Returns itself to allow chaining + * @return {wp.media.view.Modal} Returns itself to allow chaining. */ attach: function() { if ( this.views.attached ) { @@ -4360,7 +4468,7 @@ }, /** - * @returns {wp.media.view.Modal} Returns itself to allow chaining + * @return {wp.media.view.Modal} Returns itself to allow chaining. */ detach: function() { if ( this.$el.is(':visible') ) { @@ -4373,7 +4481,7 @@ }, /** - * @returns {wp.media.view.Modal} Returns itself to allow chaining + * @return {wp.media.view.Modal} Returns itself to allow chaining. */ open: function() { var $el = this.$el, @@ -4394,7 +4502,7 @@ $el.show(); - // Try to close the onscreen keyboard + // Try to close the onscreen keyboard. if ( 'ontouchend' in document ) { if ( ( mceEditor = window.tinymce && window.tinymce.activeEditor ) && ! mceEditor.isHidden() && mceEditor.iframeElement ) { mceEditor.iframeElement.focus(); @@ -4417,7 +4525,7 @@ /** * @param {Object} options - * @returns {wp.media.view.Modal} Returns itself to allow chaining + * @return {wp.media.view.Modal} Returns itself to allow chaining. */ close: function( options ) { if ( ! this.views.attached || ! this.$el.is(':visible') ) { @@ -4427,7 +4535,7 @@ // Enable page scrolling. $( 'body' ).removeClass( 'modal-open' ); - // Hide modal and remove restricted media modal tab focus once it's closed + // Hide modal and remove restricted media modal tab focus once it's closed. this.$el.hide().undelegate( 'keydown' ); /* @@ -4436,10 +4544,12 @@ */ this.focusManager.removeAriaHiddenFromBodyChildren(); - // Put focus back in useful location once modal is closed. + // Move focus back in useful location once modal is closed. if ( null !== this.clickedOpenerEl ) { + // Move focus back to the element that opened the modal. this.clickedOpenerEl.focus(); } else { + // Fallback to the admin page main element. $( '#wpbody-content' ) .attr( 'tabindex', '-1' ) .focus(); @@ -4454,7 +4564,7 @@ return this; }, /** - * @returns {wp.media.view.Modal} Returns itself to allow chaining + * @return {wp.media.view.Modal} Returns itself to allow chaining. */ escape: function() { return this.close({ escape: true }); @@ -4469,7 +4579,7 @@ /** * @param {Array|Object} content Views to register to '.media-modal-content' - * @returns {wp.media.view.Modal} Returns itself to allow chaining + * @return {wp.media.view.Modal} Returns itself to allow chaining. */ content: function( content ) { this.views.set( '.media-modal-content', content ); @@ -4481,7 +4591,7 @@ * forwards events to the modal's controller. * * @param {string} id - * @returns {wp.media.view.Modal} Returns itself to allow chaining + * @return {wp.media.view.Modal} Returns itself to allow chaining. */ propagate: function( id ) { this.trigger( id ); @@ -4511,6 +4621,8 @@ /* 56 */ /***/ (function(module, exports) { +var $ = jQuery; + /** * wp.media.view.FocusManager * @@ -4524,19 +4636,65 @@ var FocusManager = wp.media.View.extend(/** @lends wp.media.view.FocusManager.prototype */{ events: { - 'keydown': 'constrainTabbing' - }, - - /** - * Moves focus to the first visible menu item in the modal. + 'keydown': 'focusManagementMode' + }, + + /** + * Initializes the Focus Manager. + * + * @param {Object} options The Focus Manager options. + * + * @since 5.3.0 + * + * @return {void} + */ + initialize: function( options ) { + this.mode = options.mode || 'constrainTabbing'; + this.tabsAutomaticActivation = options.tabsAutomaticActivation || false; + }, + + /** + * Determines which focus management mode to use. + * + * @since 5.3.0 + * + * @param {Object} event jQuery event object. + * + * @return {void} + */ + focusManagementMode: function( event ) { + if ( this.mode === 'constrainTabbing' ) { + this.constrainTabbing( event ); + } + + if ( this.mode === 'tabsNavigation' ) { + this.tabsNavigation( event ); + } + }, + + /** + * Gets all the tabbable elements. + * + * @since 5.3.0 + * + * @return {Object} A jQuery collection of tabbable elements. + */ + getTabbables: function() { + // Skip the file input added by Plupload. + return this.$( ':tabbable' ).not( '.moxie-shim input[type="file"]' ); + }, + + /** + * Moves focus to the modal dialog. * * @since 3.5.0 * - * @returns {void} + * @return {void} */ focus: function() { - this.$( '.media-menu-item' ).filter( ':visible' ).first().focus(); - }, + this.$( '.media-modal' ).focus(); + }, + /** * Constrains navigation with the Tab key within the media view element. * @@ -4544,7 +4702,7 @@ * * @param {Object} event A keydown jQuery event. * - * @returns {void} + * @return {void} */ constrainTabbing: function( event ) { var tabbables; @@ -4554,10 +4712,9 @@ return; } - // Skip the file input added by Plupload. - tabbables = this.$( ':tabbable' ).not( '.moxie-shim input[type="file"]' ); - - // Keep tab focus within media modal while it's open + tabbables = this.getTabbables(); + + // Keep tab focus within media modal while it's open. if ( tabbables.last()[0] === event.target && ! event.shiftKey ) { tabbables.first().focus(); return false; @@ -4568,18 +4725,21 @@ }, /** - * Hides from assistive technologies all the body children except the - * provided element and other elements that should not be hidden. + * Hides from assistive technologies all the body children. + * + * Sets an `aria-hidden="true"` attribute on all the body children except + * the provided element and other elements that should not be hidden. * * The reason why we use `aria-hidden` is that `aria-modal="true"` is buggy - * in Safari 11.1 and support is spotty in other browsers. In the future we - * should consider to remove this helper function and only use `aria-modal="true"`. + * in Safari 11.1 and support is spotty in other browsers. Also, `aria-modal="true"` + * prevents the `wp.a11y.speak()` ARIA live regions to work as they're outside + * of the modal dialog and get hidden from assistive technologies. * * @since 5.2.3 * - * @param {object} visibleElement The jQuery object representing the element that should not be hidden. - * - * @returns {void} + * @param {Object} visibleElement The jQuery object representing the element that should not be hidden. + * + * @return {void} */ setAriaHiddenOnBodyChildren: function( visibleElement ) { var bodyChildren, @@ -4611,12 +4771,14 @@ }, /** - * Makes visible again to assistive technologies all body children + * Unhides from assistive technologies all the body children. + * + * Makes visible again to assistive technologies all the body children * previously hidden and stored in this.ariaHiddenElements. * * @since 5.2.3 * - * @returns {void} + * @return {void} */ removeAriaHiddenFromBodyChildren: function() { _.each( this.ariaHiddenElements, function( element ) { @@ -4632,9 +4794,9 @@ * * @since 5.2.3 * - * @param {object} element The DOM element that should be checked. - * - * @returns {boolean} Whether the element should not be hidden from assistive technologies. + * @param {Object} element The DOM element that should be checked. + * + * @return {boolean} Whether the element should not be hidden from assistive technologies. */ elementShouldBeHidden: function( element ) { var role = element.getAttribute( 'role' ), @@ -4665,7 +4827,158 @@ * * @since 5.2.3 */ - ariaHiddenElements: [] + ariaHiddenElements: [], + + /** + * Holds the jQuery collection of ARIA tabs. + * + * @since 5.3.0 + */ + tabs: $(), + + /** + * Sets up tabs in an ARIA tabbed interface. + * + * @since 5.3.0 + * + * @param {Object} event jQuery event object. + * + * @return {void} + */ + setupAriaTabs: function() { + this.tabs = this.$( '[role="tab"]' ); + + // Set up initial attributes. + this.tabs.attr( { + 'aria-selected': 'false', + tabIndex: '-1' + } ); + + // Set up attributes on the initially active tab. + this.tabs.filter( '.active' ) + .removeAttr( 'tabindex' ) + .attr( 'aria-selected', 'true' ); + }, + + /** + * Enables arrows navigation within the ARIA tabbed interface. + * + * @since 5.3.0 + * + * @param {Object} event jQuery event object. + * + * @return {void} + */ + tabsNavigation: function( event ) { + var orientation = 'horizontal', + keys = [ 32, 35, 36, 37, 38, 39, 40 ]; + + // Return if not Spacebar, End, Home, or Arrow keys. + if ( keys.indexOf( event.which ) === -1 ) { + return; + } + + // Determine navigation direction. + if ( this.$el.attr( 'aria-orientation' ) === 'vertical' ) { + orientation = 'vertical'; + } + + // Make Up and Down arrow keys do nothing with horizontal tabs. + if ( orientation === 'horizontal' && [ 38, 40 ].indexOf( event.which ) !== -1 ) { + return; + } + + // Make Left and Right arrow keys do nothing with vertical tabs. + if ( orientation === 'vertical' && [ 37, 39 ].indexOf( event.which ) !== -1 ) { + return; + } + + this.switchTabs( event, this.tabs ); + }, + + /** + * Switches tabs in the ARIA tabbed interface. + * + * @since 5.3.0 + * + * @param {Object} event jQuery event object. + * + * @return {void} + */ + switchTabs: function( event ) { + var key = event.which, + index = this.tabs.index( $( event.target ) ), + newIndex; + + switch ( key ) { + // Space bar: Activate current targeted tab. + case 32: { + this.activateTab( this.tabs[ index ] ); + break; + } + // End key: Activate last tab. + case 35: { + event.preventDefault(); + this.activateTab( this.tabs[ this.tabs.length - 1 ] ); + break; + } + // Home key: Activate first tab. + case 36: { + event.preventDefault(); + this.activateTab( this.tabs[ 0 ] ); + break; + } + // Left and up keys: Activate previous tab. + case 37: + case 38: { + event.preventDefault(); + newIndex = ( index - 1 ) < 0 ? this.tabs.length - 1 : index - 1; + this.activateTab( this.tabs[ newIndex ] ); + break; + } + // Right and down keys: Activate next tab. + case 39: + case 40: { + event.preventDefault(); + newIndex = ( index + 1 ) === this.tabs.length ? 0 : index + 1; + this.activateTab( this.tabs[ newIndex ] ); + break; + } + } + }, + + /** + * Sets a single tab to be focusable and semantically selected. + * + * @since 5.3.0 + * + * @param {Object} tab The tab DOM element. + * + * @return {void} + */ + activateTab: function( tab ) { + if ( ! tab ) { + return; + } + + // The tab is a DOM element: no need for jQuery methods. + tab.focus(); + + // Handle automatic activation. + if ( this.tabsAutomaticActivation ) { + tab.removeAttribute( 'tabindex' ); + tab.setAttribute( 'aria-selected', 'true' ); + tab.click(); + + return; + } + + // Handle manual activation. + $( tab ).on( 'click', function() { + tab.removeAttribute( 'tabindex' ); + tab.setAttribute( 'aria-selected', 'true' ); + } ); + } }); module.exports = FocusManager; @@ -4858,7 +5171,7 @@ /** * Check browser support for drag'n'drop. * - * @return Boolean + * @return {boolean} */ browserSupport: function() { var supports = false, div = document.createElement('div'); @@ -4923,7 +5236,7 @@ * When a file is dropped on the editor uploader, open up an editor media workflow * and upload the file immediately. * - * @param {jQuery.Event} event The 'drop' event. + * @param {jQuery.Event} event The 'drop' event. */ drop: function( event ) { var $wrap, uploadView; @@ -5085,7 +5398,7 @@ return data; }, /** - * @returns {wp.media.view.UploaderInline} Returns itself to allow chaining + * @return {wp.media.view.UploaderInline} Returns itself to allow chaining. */ dispose: function() { if ( this.disposing ) { @@ -5095,14 +5408,16 @@ return View.prototype.dispose.apply( this, arguments ); } - // Run remove on `dispose`, so we can be sure to refresh the - // uploader with a view-less DOM. Track whether we're disposing - // so we don't trigger an infinite loop. + /* + * Run remove on `dispose`, so we can be sure to refresh the + * uploader with a view-less DOM. Track whether we're disposing + * so we don't trigger an infinite loop. + */ this.disposing = true; return this.remove(); }, /** - * @returns {wp.media.view.UploaderInline} Returns itself to allow chaining + * @return {wp.media.view.UploaderInline} Returns itself to allow chaining. */ remove: function() { /** @@ -5122,7 +5437,7 @@ } }, /** - * @returns {wp.media.view.UploaderInline} + * @return {wp.media.view.UploaderInline} */ ready: function() { var $browser = this.options.$browser, @@ -5204,7 +5519,7 @@ this.errors.on( 'add', this.error, this ); }, /** - * @returns {wp.media.view.UploaderStatus} + * @return {wp.media.view.UploaderStatus} */ dispose: function() { wp.Uploader.queue.off( null, null, this ); @@ -5273,7 +5588,7 @@ }, /** * @param {string} filename - * @returns {string} + * @return {string} */ filename: function( filename ) { return _.escape( filename ); @@ -5282,10 +5597,13 @@ * @param {Backbone.Model} error */ error: function( error ) { - this.views.add( '.upload-errors', new wp.media.view.UploaderStatusError({ - filename: this.filename( error.get('file').name ), - message: error.get('message') - }), { at: 0 }); + var statusError = new wp.media.view.UploaderStatusError( { + filename: this.filename( error.get( 'file' ).name ), + message: error.get( 'message' ) + } ); + + // Can show additional info here while retrying to create image sub-sizes. + this.views.add( '.upload-errors', statusError, { at: 0 } ); }, dismiss: function() { @@ -5295,8 +5613,10 @@ _.invoke( errors, 'remove' ); } wp.Uploader.errors.reset(); - // Keep focus within the modal after the dismiss button gets removed from the DOM. - this.controller.modal.focusManager.focus(); + // Move focus to the modal after the dismiss button gets removed from the DOM. + if ( this.controller.modal ) { + this.controller.modal.focusManager.focus(); + } } }); @@ -5381,7 +5701,7 @@ } }, /** - * @returns {wp.media.view.Toolbar} Returns itsef to allow chaining + * @return {wp.media.view.Toolbar} Returns itsef to allow chaining */ dispose: function() { if ( this.selection ) { @@ -5405,7 +5725,7 @@ * @param {string} id * @param {Backbone.View|Object} view * @param {Object} [options={}] - * @returns {wp.media.view.Toolbar} Returns itself to allow chaining + * @return {wp.media.view.Toolbar} Returns itself to allow chaining. */ set: function( id, view, options ) { var list; @@ -5439,7 +5759,7 @@ }, /** * @param {string} id - * @returns {wp.media.view.Button} + * @return {wp.media.view.Button} */ get: function( id ) { return this._views[ id ]; @@ -5447,7 +5767,7 @@ /** * @param {string} id * @param {Object} options - * @returns {wp.media.view.Toolbar} Returns itself to allow chaining + * @return {wp.media.view.Toolbar} Returns itself to allow chaining. */ unset: function( id, options ) { delete this._views[ id ]; @@ -5473,7 +5793,7 @@ var requires = button.options.requires, disabled = false; - // Prevent insertion of attachments if any of them are still uploading + // Prevent insertion of attachments if any of them are still uploading. if ( selection && selection.models ) { disabled = _.some( selection.models, function( attachment ) { return attachment.get('uploading') === true; @@ -5665,7 +5985,7 @@ this.listenTo( this.model, 'change', this.render ); }, /** - * @returns {wp.media.view.Button} Returns itself to allow chaining + * @return {wp.media.view.Button} Returns itself to allow chaining. */ render: function() { var classes = [ 'button', this.className ], @@ -5745,7 +6065,7 @@ }, /** - * @returns {wp.media.view.ButtonGroup} + * @return {wp.media.view.ButtonGroup} */ render: function() { this.$el.html( $( _.pluck( this.buttons, 'el' ) ).detach() ); @@ -5787,7 +6107,7 @@ * @param {string} id * @param {wp.media.View|Object} view * @param {Object} options - * @returns {wp.media.view.PriorityList} Returns itself to allow chaining + * @return {wp.media.view.PriorityList} Returns itself to allow chaining. */ set: function( id, view, options ) { var priority, views, index; @@ -5828,14 +6148,14 @@ }, /** * @param {string} id - * @returns {wp.media.View} + * @return {wp.media.View} */ get: function( id ) { return this._views[ id ]; }, /** * @param {string} id - * @returns {wp.media.view.PriorityList} + * @return {wp.media.view.PriorityList} */ unset: function( id ) { var view = this.get( id ); @@ -5849,7 +6169,7 @@ }, /** * @param {Object} options - * @returns {wp.media.View} + * @return {wp.media.View} */ toView: function( options ) { return new wp.media.View( options ); @@ -5863,8 +6183,7 @@ /* 68 */ /***/ (function(module, exports) { -var $ = jQuery, - MenuItem; +var MenuItem; /** * wp.media.view.MenuItem @@ -5877,37 +6196,29 @@ * @augments Backbone.View */ MenuItem = wp.media.View.extend(/** @lends wp.media.view.MenuItem.prototype */{ - tagName: 'a', + tagName: 'button', className: 'media-menu-item', attributes: { - href: '#' + type: 'button', + role: 'tab' }, events: { 'click': '_click' }, - /** - * @param {Object} event - */ - _click: function( event ) { + + /** + * Allows to override the click event. + */ + _click: function() { var clickOverride = this.options.click; - if ( event ) { - event.preventDefault(); - } - if ( clickOverride ) { clickOverride.call( this ); } else { this.click(); } - - // When selecting a tab along the left side, - // focus should be transferred into the main panel - if ( ! wp.media.isTouchDevice ) { - $('.media-frame-content input').first().focus(); - } }, click: function() { @@ -5915,14 +6226,17 @@ if ( state ) { this.controller.setState( state ); - this.views.parent.$el.removeClass( 'visible' ); // TODO: or hide on any click, see below - } - }, - /** - * @returns {wp.media.view.MenuItem} returns itself to allow chaining + // Toggle the menu visibility in the responsive view. + this.views.parent.$el.removeClass( 'visible' ); // @todo Or hide on any click, see below. + } + }, + + /** + * @return {wp.media.view.MenuItem} returns itself to allow chaining. */ render: function() { - var options = this.options; + var options = this.options, + menuProperty = options.state || options.contentMode; if ( options.text ) { this.$el.text( options.text ); @@ -5930,6 +6244,9 @@ this.$el.html( options.html ); } + // Set the menu item ID based on the frame state associated to the menu item. + this.$el.attr( 'id', 'menu-item-' + menuProperty ); + return this; } }); @@ -5963,20 +6280,35 @@ ItemView: MenuItem, region: 'menu', - /* TODO: alternatively hide on any click anywhere - events: { - 'click': 'click' - }, - - click: function() { - this.$el.removeClass( 'visible' ); - }, - */ + attributes: { + role: 'tablist', + 'aria-orientation': 'horizontal' + }, + + initialize: function() { + this._views = {}; + + this.set( _.extend( {}, this._views, this.options.views ), { silent: true }); + delete this.options.views; + + if ( ! this.options.silent ) { + this.render(); + } + + // Initialize the Focus Manager. + this.focusManager = new wp.media.view.FocusManager( { + el: this.el, + mode: 'tabsNavigation' + } ); + + // The menu is always rendered and can be visible or hidden on some frames. + this.isVisible = true; + }, /** * @param {Object} options * @param {string} id - * @returns {wp.media.View} + * @return {wp.media.View} */ toView: function( options, id ) { options = options || {}; @@ -5990,6 +6322,9 @@ */ PriorityList.prototype.ready.apply( this, arguments ); this.visibility(); + + // Set up aria tabs initial attributes. + this.focusManager.setupAriaTabs(); }, set: function() { @@ -6015,6 +6350,9 @@ hide = ! views || views.length < 2; if ( this === view ) { + // Flag this menu as hidden or visible. + this.isVisible = ! hide; + // Set or remove a CSS class to hide the menu. this.controller.$el.toggleClass( 'hide-' + region, hide ); } }, @@ -6030,6 +6368,9 @@ this.deselect(); view.$el.addClass('active'); + + // Set up again the aria tabs initial attributes after the menu updates. + this.focusManager.setupAriaTabs(); }, deselect: function() { @@ -6116,6 +6457,11 @@ ItemView: wp.media.view.RouterItem, region: 'router', + attributes: { + role: 'tablist', + 'aria-orientation': 'horizontal' + }, + initialize: function() { this.controller.on( 'content:render', this.update, this ); // Call 'initialize' directly on the parent class. @@ -6227,10 +6573,10 @@ this.details( this.model, this.controller.state().get('selection') ); } - this.listenTo( this.controller, 'attachment:compat:waiting attachment:compat:ready', this.updateSave ); - }, - /** - * @returns {wp.media.view.Attachment} Returns itself to allow chaining + this.listenTo( this.controller.states, 'attachment:compat:waiting attachment:compat:ready', this.updateSave ); + }, + /** + * @return {wp.media.view.Attachment} Returns itself to allow chaining. */ dispose: function() { var selection = this.options.selection; @@ -6248,7 +6594,7 @@ return this; }, /** - * @returns {wp.media.view.Attachment} Returns itself to allow chaining + * @return {wp.media.view.Attachment} Returns itself to allow chaining. */ render: function() { var options = _.defaults( this.model.toJSON(), { @@ -6328,13 +6674,13 @@ return; } - // Catch arrow events + // Catch arrow events. if ( 37 === event.keyCode || 38 === event.keyCode || 39 === event.keyCode || 40 === event.keyCode ) { this.controller.trigger( 'attachment:keydown:arrow', event ); return; } - // Catch enter and space events + // Catch enter and space events. if ( 'keydown' === event.type && 13 !== event.keyCode && 32 !== event.keyCode ) { return; } @@ -6344,7 +6690,7 @@ // In the grid view, bubble up an edit:attachment event to the controller. if ( this.controller.isModeActive( 'grid' ) ) { if ( this.controller.isModeActive( 'edit' ) ) { - // Pass the current target to restore focus when closing + // Pass the current target to restore focus when closing. this.controller.trigger( 'edit:attachment', this.model, event.currentTarget ); return; } @@ -6416,7 +6762,7 @@ return; } - // Fixes bug that loses focus when selecting a featured image + // Fixes bug that loses focus when selecting a featured image. if ( ! method ) { method = 'add'; } @@ -6426,14 +6772,18 @@ } if ( this.selected() ) { - // If the model is the single model, remove it. - // If it is not the same as the single model, - // it now becomes the single model. + /* + * If the model is the single model, remove it. + * If it is not the same as the single model, + * it now becomes the single model. + */ selection[ single === model ? 'remove' : 'single' ]( model ); } else { - // If the model is not selected, run the `method` on the - // selection. By default, we `reset` the selection, but the - // `method` can be set to `add` the model to the selection. + /* + * If the model is not selected, run the `method` on the + * selection. By default, we `reset` the selection, but the + * `method` can be set to `add` the model to the selection. + */ selection[ method ]( model ); selection.single( model ); } @@ -6443,7 +6793,7 @@ this[ this.selected() ? 'select' : 'deselect' ](); }, /** - * @returns {unresolved|Boolean} + * @return {unresolved|boolean} */ selected: function() { var selection = this.options.selection; @@ -6459,9 +6809,11 @@ var selection = this.options.selection, controller = this.controller; - // Check if a selection exists and if it's the collection provided. - // If they're not the same collection, bail; we're in another - // selection's event loop. + /* + * Check if a selection exists and if it's the collection provided. + * If they're not the same collection, bail; we're in another + * selection's event loop. + */ if ( ! selection || ( collection && collection !== selection ) ) { return; } @@ -6485,9 +6837,11 @@ deselect: function( model, collection ) { var selection = this.options.selection; - // Check if a selection exists and if it's the collection provided. - // If they're not the same collection, bail; we're in another - // selection's event loop. + /* + * Check if a selection exists and if it's the collection provided. + * If they're not the same collection, bail; we're in another + * selection's event loop. + */ if ( ! selection || ( collection && collection !== selection ) ) { return; } @@ -6511,7 +6865,7 @@ }, /** * @param {string} size - * @returns {Object} + * @return {Object} */ imageSize: function( size ) { var sizes = this.model.get('sizes'), matched = false; @@ -6595,7 +6949,7 @@ }, /** * @param {string} status - * @returns {wp.media.view.Attachment} Returns itself to allow chaining + * @return {wp.media.view.Attachment} Returns itself to allow chaining. */ updateSave: function( status ) { var save = this._save = this._save || { status: 'ready' }; @@ -6639,7 +6993,7 @@ * @param {Object} event */ removeFromLibrary: function( event ) { - // Catch enter and space events + // Catch enter and space events. if ( 'keydown' === event.type && 13 !== event.keyCode && 32 !== event.keyCode ) { return; } @@ -6654,8 +7008,8 @@ * Add the model if it isn't in the selection, if it is in the selection, * remove it. * - * @param {[type]} event [description] - * @return {[type]} [description] + * @param {[type]} event [description] + * @return {[type]} [description] */ checkClickHandler: function ( event ) { var selection = this.options.selection; @@ -6690,7 +7044,7 @@ * * @param {Backbone.Model} model * @param {string} value - * @returns {wp.media.view.Attachment} Returns itself to allow chaining + * @return {wp.media.view.Attachment} Returns itself to allow chaining. */ /** * @function _syncTitle @@ -6699,7 +7053,7 @@ * * @param {Backbone.Model} model * @param {string} value - * @returns {wp.media.view.Attachment} Returns itself to allow chaining + * @return {wp.media.view.Attachment} Returns itself to allow chaining. */ /** * @function _syncArtist @@ -6708,7 +7062,7 @@ * * @param {Backbone.Model} model * @param {string} value - * @returns {wp.media.view.Attachment} Returns itself to allow chaining + * @return {wp.media.view.Attachment} Returns itself to allow chaining. */ /** * @function _syncAlbum @@ -6717,7 +7071,7 @@ * * @param {Backbone.Model} model * @param {string} value - * @returns {wp.media.view.Attachment} Returns itself to allow chaining + * @return {wp.media.view.Attachment} Returns itself to allow chaining. */ Attachment.prototype[ method ] = function( model, value ) { var $setting = this.$('[data-setting="' + setting + '"]'); @@ -6726,10 +7080,12 @@ return this; } - // If the updated value is in sync with the value in the DOM, there - // is no need to re-render. If we're currently editing the value, - // it will automatically be in sync, suppressing the re-render for - // the view we're editing, while updating any others. + /* + * If the updated value is in sync with the value in the DOM, there + * is no need to re-render. If we're currently editing the value, + * it will automatically be in sync, suppressing the re-render for + * the view we're editing, while updating any others. + */ if ( value === $setting.find('input, textarea, select, [value]').val() ) { return this; } @@ -6877,7 +7233,7 @@ this.collection.on( 'reset', this.render, this ); - this.listenTo( this.controller, 'library:selection:add', this.attachmentFocus ); + this.controller.on( 'library:selection:add', this.attachmentFocus, this ); // Throttle the scroll handler and bind this. this.scroll = _.chain( this.scroll ).bind( this ).throttle( this.options.refreshSensitivity ).value(); @@ -6911,7 +7267,7 @@ * * @listens window:resize * - * @returns {void} + * @return {void} */ bindEvents: function() { this.$window.off( this.resizeEvent ).on( this.resizeEvent, _.debounce( this.setColumns, 50 ) ); @@ -6922,18 +7278,34 @@ * * @since 4.0.0 * - * @returns {void} + * @return {void} */ attachmentFocus: function() { - this.$( 'li:first' ).focus(); + /* + * @todo When uploading new attachments, this tries to move focus to + * the attachments grid. Actually, a progress bar gets initially displayed + * and then updated when uploading completes, so focus is lost. + * Additionally: this view is used for both the attachments list and + * the list of selected attachments in the bottom media toolbar. Thus, when + * uploading attachments, it is called twice and returns two different `this`. + * `this.columns` is truthy within the modal. + */ + if ( this.columns ) { + // Move focus to the grid list within the modal. + this.$el.focus(); + } }, /** * Restores focus to the selected item in the collection. * + * Moves focus back to the first selected attachment in the grid. Used when + * tabbing backwards from the attachment details sidebar. + * See media.view.AttachmentsBrowser. + * * @since 4.0.0 * - * @returns {void} + * @return {void} */ restoreFocus: function() { this.$( 'li.selected:first' ).focus(); @@ -6948,7 +7320,7 @@ * * @param {KeyboardEvent} event The keyboard event that triggered this function. * - * @returns {void} + * @return {void} */ arrowEvent: function( event ) { var attachments = this.$el.children( 'li' ), @@ -6998,7 +7370,7 @@ * * @since 3.5.0 * - * @returns {void} + * @return {void} */ dispose: function() { this.collection.props.off( null, null, this ); @@ -7018,7 +7390,7 @@ * * @since 4.0.0 * - * @returns {void} + * @return {void} */ setColumns: function() { var prev = this.columns, @@ -7036,14 +7408,14 @@ /** * Initializes jQuery sortable on the attachment list. * - * Fails gracefully if jQuery sortable doesn't exist or isn't passed in the - * options. + * Fails gracefully if jQuery sortable doesn't exist or isn't passed + * in the options. * * @since 3.5.0 * * @fires collection:reset * - * @returns {void} + * @return {void} */ initSortable: function() { var collection = this.collection; @@ -7057,8 +7429,8 @@ disabled: !! collection.comparator, /* - * Change the position of the attachment as soon as the mouse pointer overlaps a - * thumbnail. + * Change the position of the attachment as soon as the mouse pointer + * overlaps a thumbnail. */ tolerance: 'pointer', @@ -7100,8 +7472,8 @@ }, this.options.sortable ) ); /* - * If the `orderby` property is changed on the `collection`, check to see if we - * have a `comparator`. If so, disable sorting. + * If the `orderby` property is changed on the `collection`, + * check to see if we have a `comparator`. If so, disable sorting. */ collection.props.on( 'change:orderby', function() { this.$el.sortable( 'option', 'disabled', !! collection.comparator ); @@ -7117,7 +7489,7 @@ * * @since 3.5.0 * - * @returns {void} + * @return {void} */ refreshSortable: function() { if ( ! this.options.sortable || ! $.fn.sortable ) { @@ -7138,7 +7510,7 @@ * * @param {wp.media.model.Attachment} attachment * - * @returns {wp.media.View} The created view. + * @return {wp.media.View} The created view. */ createAttachmentView: function( attachment ) { var view = new this.options.AttachmentView({ @@ -7159,7 +7531,7 @@ * * @since 3.5.0 * - * @returns {void} + * @return {void} */ prepare: function() { if ( this.collection.length ) { @@ -7176,7 +7548,7 @@ * * @since 3.5.0 * - * @returns {void} + * @return {void} */ ready: function() { this.scroll(); @@ -7190,7 +7562,7 @@ * * @since 3.5.0 * - * @returns {void} + * @return {void} */ scroll: function() { var view = this, @@ -7234,8 +7606,7 @@ /* 77 */ /***/ (function(module, exports) { -var l10n = wp.media.view.l10n, - Search; +var Search; /** * wp.media.view.Search @@ -7253,17 +7624,15 @@ id: 'media-search-input', attributes: { - type: 'search', - placeholder: l10n.searchMediaPlaceholder + type: 'search' }, events: { - 'input': 'search', - 'keyup': 'search' - }, - - /** - * @returns {wp.media.view.Search} Returns itself to allow chaining + 'input': 'search' + }, + + /** + * @return {wp.media.view.Search} Returns itself to allow chaining. */ render: function() { this.el.value = this.model.escape('search'); @@ -7271,12 +7640,15 @@ }, search: _.debounce( function( event ) { - if ( event.target.value ) { - this.model.set( 'search', event.target.value ); + var searchTerm = event.target.value.trim(); + + // Trigger the search only after 2 ASCII characters. + if ( searchTerm && searchTerm.length > 1 ) { + this.model.set( 'search', searchTerm ); } else { - this.model.unset('search'); - } - }, 300 ) + this.model.unset( 'search' ); + } + }, 500 ) }); module.exports = Search; @@ -7665,17 +8037,25 @@ } /* - * For accessibility reasons, place the Inline Uploader before other sections. - * This way, in the Media Library, it's right after the Add New button, see ticket #37188. + * In the grid mode (the Media Library), place the Inline Uploader before + * other sections so that the visual order and the DOM order match. This way, + * the Inline Uploader in the Media Library is right after the "Add New" + * button, see ticket #37188. */ - this.createUploader(); - - /* - * Create a multi-purpose toolbar. Used as main toolbar in the Media Library - * and also for other things, for example the "Drag and drop to reorder" and - * "Suggested dimensions" info in the media modal. - */ - this.createToolbar(); + if ( this.controller.isModeActive( 'grid' ) ) { + this.createUploader(); + + /* + * Create a multi-purpose toolbar. Used as main toolbar in the Media Library + * and also for other things, for example the "Drag and drop to reorder" and + * "Suggested dimensions" info in the media modal. + */ + this.createToolbar(); + } else { + this.createToolbar(); + this.createUploader(); + } + // Add a heading before the attachments list. this.createAttachmentsHeading(); @@ -7699,14 +8079,47 @@ } this.collection.on( 'add remove reset', this.updateContent, this ); - }, + + // The non-cached or cached attachments query has completed. + this.collection.on( 'attachments:received', this.announceSearchResults, this ); + }, + + /** + * Updates the `wp.a11y.speak()` ARIA live region with a message to communicate + * the number of search results to screen reader users. This function is + * debounced because the collection updates multiple times. + * + * @since 5.3.0 + * + * @return {void} + */ + announceSearchResults: _.debounce( function() { + var count; + + if ( this.collection.mirroring.args.s ) { + count = this.collection.length; + + if ( 0 === count ) { + wp.a11y.speak( l10n.noMediaTryNewSearch ); + return; + } + + if ( this.collection.hasMore() ) { + wp.a11y.speak( l10n.mediaFoundHasMoreResults.replace( '%d', count ) ); + return; + } + + wp.a11y.speak( l10n.mediaFound.replace( '%d', count ) ); + } + }, 200 ), editSelection: function( modal ) { + // When editing a selection, move focus to the "Return to library" button. modal.$( '.media-button-backToLibrary' ).focus(); }, /** - * @returns {wp.media.view.AttachmentsBrowser} Returns itself to allow chaining + * @return {wp.media.view.AttachmentsBrowser} Returns itself to allow chaining. */ dispose: function() { this.options.selection.off( null, null, this ); @@ -7715,7 +8128,8 @@ }, createToolbar: function() { - var LibraryViewSwitcher, Filters, toolbarOptions; + var LibraryViewSwitcher, Filters, toolbarOptions, + showFilterByType = -1 !== $.inArray( this.options.filters, [ 'uploaded', 'all' ] ); toolbarOptions = { controller: this.controller @@ -7733,12 +8147,24 @@ this.views.add( this.toolbar ); this.toolbar.set( 'spinner', new wp.media.view.Spinner({ - priority: -60 + priority: -20 }) ); - if ( -1 !== $.inArray( this.options.filters, [ 'uploaded', 'all' ] ) ) { - // "Filters" will return a , a visually hidden label element needs to be rendered before. this.toolbar.set( 'filtersLabel', new wp.media.view.Label({ value: l10n.filterByType, attributes: { @@ -7764,9 +8190,11 @@ } } - // Feels odd to bring the global media library switcher into the Attachment - // browser view. Is this a use case for doAction( 'add:toolbar-items:attachments-browser', this.toolbar ); - // which the controller can tap into and add this view? + /* + * Feels odd to bring the global media library switcher into the Attachment browser view. + * Is this a use case for doAction( 'add:toolbar-items:attachments-browser', this.toolbar ); + * which the controller can tap into and add this view? + */ if ( this.controller.isModeActive( 'grid' ) ) { LibraryViewSwitcher = View.extend({ className: 'view-switch media-grid-view-switch', @@ -7778,7 +8206,7 @@ priority: -90 }).render() ); - // DateFilter is a , a visually hidden label element needs to be rendered before. this.toolbar.set( 'dateFilterLabel', new wp.media.view.Label({ value: l10n.filterByDate, attributes: { @@ -7792,7 +8220,7 @@ priority: -75 }).render() ); - // BulkSelection is a
with subviews, including screen reader text + // BulkSelection is a
with subviews, including screen reader text. this.toolbar.set( 'selectModeToggleButton', new wp.media.view.SelectModeToggleButton({ text: l10n.bulkSelect, controller: this.controller, @@ -7900,7 +8328,7 @@ } } else if ( this.options.date ) { - // DateFilter is a , a visually hidden label element needs to be rendered before. this.toolbar.set( 'dateFilterLabel', new wp.media.view.Label({ value: l10n.filterByDate, attributes: { @@ -7916,9 +8344,10 @@ } if ( this.options.search ) { - // Search is an input, screen reader text needs to be rendered before + // Search is an input, a visually hidden label element needs to be rendered before. this.toolbar.set( 'searchLabel', new wp.media.view.Label({ - value: l10n.searchMediaLabel, + value: l10n.searchLabel, + className: 'media-search-input-label', attributes: { 'for': 'media-search-input' }, @@ -8006,7 +8435,7 @@ AttachmentView: this.options.AttachmentView }); - // Add keydown listener to the instance of the Attachments view + // Add keydown listener to the instance of the Attachments view. this.controller.on( 'attachment:keydown:arrow', _.bind( this.attachments.arrowEvent, this.attachments ) ); this.controller.on( 'attachment:details:shift-tab', _.bind( this.attachments.restoreFocus, this.attachments ) ); @@ -8085,7 +8514,7 @@ }) ); } - // Show the sidebar on mobile + // Show the sidebar on mobile. if ( this.model.id === 'insert' ) { sidebar.$el.addClass( 'visible' ); } @@ -8096,7 +8525,7 @@ sidebar.unset('details'); sidebar.unset('compat'); sidebar.unset('display'); - // Hide the sidebar on mobile + // Hide the sidebar on mobile. sidebar.$el.removeClass( 'visible' ); } }); @@ -8108,7 +8537,8 @@ /* 83 */ /***/ (function(module, exports) { -var l10n = wp.media.view.l10n, +var _n = wp.i18n._n, + sprintf = wp.i18n.sprintf, Selection; /** @@ -8170,7 +8600,10 @@ this.$el.toggleClass( 'one', 1 === collection.length ); this.$el.toggleClass( 'editing', editing ); - this.$('.count').text( l10n.selected.replace('%d', collection.length) ); + this.$( '.count' ).text( + /* translators: %s: Number of selected media attachments. */ + sprintf( _n( '%s item selected', '%s items selected', collection.length ), collection.length ) + ); }, edit: function( event ) { @@ -8184,8 +8617,7 @@ event.preventDefault(); this.collection.reset(); - // Keep focus inside media modal - // after clear link is selected + // Move focus to the modal. this.controller.modal.focusManager.focus(); } }); @@ -8319,7 +8751,7 @@ }, this.options ); }, /** - * @returns {wp.media.view.Settings} Returns itself to allow chaining + * @return {wp.media.view.Settings} Returns itself to allow chaining. */ render: function() { View.prototype.render.apply( this, arguments ); @@ -8357,8 +8789,12 @@ // Handle button groups. } else if ( $setting.hasClass('button-group') ) { - $buttons = $setting.find('button').removeClass('active'); - $buttons.filter( '[value="' + value + '"]' ).addClass('active'); + $buttons = $setting.find( 'button' ) + .removeClass( 'active' ) + .attr( 'aria-pressed', 'false' ); + $buttons.filter( '[value="' + value + '"]' ) + .addClass( 'active' ) + .attr( 'aria-pressed', 'true' ); // Handle text inputs and textareas. } else if ( $setting.is('input[type="text"], textarea') ) { @@ -8394,7 +8830,8 @@ // If the setting has a corresponding user setting, // update that as well. - if ( userSetting = $setting.data('userSetting') ) { + userSetting = $setting.data('userSetting'); + if ( userSetting ) { window.setUserSetting( userSetting, value ); } }, @@ -8457,7 +8894,7 @@ Settings.prototype.dispose.apply( this, arguments ); }, /** - * @returns {wp.media.view.AttachmentDisplay} Returns itself to allow chaining + * @return {wp.media.view.AttachmentDisplay} Returns itself to allow chaining. */ render: function() { var attachment = this.options.attachment; @@ -8481,7 +8918,7 @@ attachment = this.options.attachment; if ( 'none' === linkTo || 'embed' === linkTo || ( ! attachment && 'custom' !== linkTo ) ) { - $input.addClass( 'hidden' ); + $input.closest( '.setting' ).addClass( 'hidden' ); return; } @@ -8497,11 +8934,9 @@ $input.prop( 'readonly', 'custom' !== linkTo ); } - $input.removeClass( 'hidden' ); - - // If the input is visible, focus and select its contents. - if ( ! wp.media.isTouchDevice && $input.is(':visible') ) { - $input.focus()[0].select(); + $input.closest( '.setting' ).removeClass( 'hidden' ); + if ( $input.length ) { + $input[0].scrollIntoView(); } } }); @@ -8559,21 +8994,13 @@ /* 91 */ /***/ (function(module, exports) { +/* global ClipboardJS */ var Attachment = wp.media.view.Attachment, l10n = wp.media.view.l10n, - Details; - -/** - * wp.media.view.Attachment.Details - * - * @memberOf wp.media.view.Attachment - * - * @class - * @augments wp.media.view.Attachment - * @augments wp.media.View - * @augments wp.Backbone.View - * @augments Backbone.View - */ + $ = jQuery, + Details, + __ = wp.i18n.__; + Details = Attachment.extend(/** @lends wp.media.view.Attachment.Details.prototype */{ tagName: 'div', className: 'attachment-details', @@ -8597,6 +9024,52 @@ 'keydown': 'toggleSelectionHandler' }, + /** + * Copies the attachment URL to the clipboard. + * + * @since 5.5.0 + * + * @param {MouseEvent} event A click event. + * + * @return {void} + */ + copyAttachmentDetailsURLClipboard: function() { + var clipboard = new ClipboardJS( '.copy-attachment-url' ), + successTimeout; + + clipboard.on( 'success', function( event ) { + var triggerElement = $( event.trigger ), + successElement = $( '.success', triggerElement.closest( '.copy-to-clipboard-container' ) ); + + // Clear the selection and move focus back to the trigger. + event.clearSelection(); + // Handle ClipboardJS focus bug, see https://github.com/zenorocha/clipboard.js/issues/680 + triggerElement.focus(); + + // Show success visual feedback. + clearTimeout( successTimeout ); + successElement.removeClass( 'hidden' ); + + // Hide success visual feedback after 3 seconds since last success. + successTimeout = setTimeout( function() { + successElement.addClass( 'hidden' ); + }, 3000 ); + + // Handle success audible feedback. + wp.a11y.speak( __( 'The file URL has been copied to your clipboard' ) ); + } ); + }, + + /** + * Shows the details of an attachment. + * + * @since 3.5.0 + * + * @constructs wp.media.view.Attachment.Details + * @augments wp.media.view.Attachment + * + * @return {void} + */ initialize: function() { this.options = _.defaults( this.options, { rerenderOnModelChange: false @@ -8604,41 +9077,131 @@ // Call 'initialize' directly on the parent class. Attachment.prototype.initialize.apply( this, arguments ); - }, - - /** - * @param {Object} event + + this.copyAttachmentDetailsURLClipboard(); + }, + + /** + * Gets the focusable elements to move focus to. + * + * @since 5.3.0 + */ + getFocusableElements: function() { + var editedAttachment = $( 'li[data-id="' + this.model.id + '"]' ); + + this.previousAttachment = editedAttachment.prev(); + this.nextAttachment = editedAttachment.next(); + }, + + /** + * Moves focus to the previous or next attachment in the grid. + * Fallbacks to the upload button or media frame when there are no attachments. + * + * @since 5.3.0 + */ + moveFocus: function() { + if ( this.previousAttachment.length ) { + this.previousAttachment.focus(); + return; + } + + if ( this.nextAttachment.length ) { + this.nextAttachment.focus(); + return; + } + + // Fallback: move focus to the "Select Files" button in the media modal. + if ( this.controller.uploader && this.controller.uploader.$browser ) { + this.controller.uploader.$browser.focus(); + return; + } + + // Last fallback. + this.moveFocusToLastFallback(); + }, + + /** + * Moves focus to the media frame as last fallback. + * + * @since 5.3.0 + */ + moveFocusToLastFallback: function() { + // Last fallback: make the frame focusable and move focus to it. + $( '.media-frame' ) + .attr( 'tabindex', '-1' ) + .focus(); + }, + + /** + * Deletes an attachment. + * + * Deletes an attachment after asking for confirmation. After deletion, + * keeps focus in the modal. + * + * @since 3.5.0 + * + * @param {MouseEvent} event A click event. + * + * @return {void} */ deleteAttachment: function( event ) { event.preventDefault(); + this.getFocusableElements(); + if ( window.confirm( l10n.warnDelete ) ) { this.model.destroy(); - // Keep focus inside media modal - // after image is deleted - this.controller.modal.focusManager.focus(); - } - }, - /** - * @param {Object} event + this.moveFocus(); + } + }, + + /** + * Sets the Trash state on an attachment, or destroys the model itself. + * + * If the mediaTrash setting is set to true, trashes the attachment. + * Otherwise, the model itself is destroyed. + * + * @since 3.9.0 + * + * @param {MouseEvent} event A click event. + * + * @return {void} */ trashAttachment: function( event ) { - var library = this.controller.library; + var library = this.controller.library, + self = this; event.preventDefault(); + this.getFocusableElements(); + + // When in the Media Library and the Media Trash is enabled. if ( wp.media.view.settings.mediaTrash && 'edit-metadata' === this.controller.content.mode() ) { this.model.set( 'status', 'trash' ); this.model.save().done( function() { library._requery( true ); + /* + * @todo We need to move focus back to the previous, next, or first + * attachment but the library gets re-queried and refreshed. + * Thus, the references to the previous attachments are lost. + * We need an alternate method. + */ + self.moveFocusToLastFallback(); } ); - } else { + } else { this.model.destroy(); - } - }, - /** - * @param {Object} event + this.moveFocus(); + } + }, + /** + * Untrashes an attachment. + * + * @since 4.0.0 + * + * @param {MouseEvent} event A click event. + * + * @return {void} */ untrashAttachment: function( event ) { var library = this.controller.library; @@ -8649,8 +9212,15 @@ library._requery( true ); } ); }, - /** - * @param {Object} event + + /** + * Opens the edit page for a specific attachment. + * + * @since 3.5.0 + * + * @param {MouseEvent} event A click event. + * + * @return {void} */ editAttachment: function( event ) { var editState = this.controller.states.get( 'edit-image' ); @@ -8663,22 +9233,26 @@ this.$el.addClass('needs-refresh'); } }, - /** - * When reverse tabbing(shift+tab) out of the right details panel, deliver - * the focus to the item in the list that was being edited. - * - * @param {Object} event + + /** + * Triggers an event on the controller when reverse tabbing (shift+tab). + * + * This event can be used to make sure to move the focus correctly. + * + * @since 4.0.0 + * + * @fires wp.media.controller.MediaLibrary#attachment:details:shift-tab + * @fires wp.media.controller.MediaLibrary#attachment:keydown:arrow + * + * @param {KeyboardEvent} event A keyboard event. + * + * @return {boolean|void} Returns false or undefined. */ toggleSelectionHandler: function( event ) { if ( 'keydown' === event.type && 9 === event.keyCode && event.shiftKey && event.target === this.$( ':tabbable' ).get( 0 ) ) { this.controller.trigger( 'attachment:details:shift-tab', event ); return false; } - - if ( 37 === event.keyCode || 38 === event.keyCode || 39 === event.keyCode || 40 === event.keyCode ) { - this.controller.trigger( 'attachment:keydown:arrow', event ); - return; - } } }); @@ -8719,7 +9293,7 @@ this.listenTo( this.model, 'change:compat', this.render ); }, /** - * @returns {wp.media.view.AttachmentCompat} Returns itself to allow chaining + * @return {wp.media.view.AttachmentCompat} Returns itself to allow chaining. */ dispose: function() { if ( this.$(':focus').length ) { @@ -8731,7 +9305,7 @@ return View.prototype.dispose.apply( this, arguments ); }, /** - * @returns {wp.media.view.AttachmentCompat} Returns itself to allow chaining + * @return {wp.media.view.AttachmentCompat} Returns itself to allow chaining. */ render: function() { var compat = this.model.get('compat'); @@ -8793,7 +9367,7 @@ var Iframe = wp.media.View.extend(/** @lends wp.media.view.Iframe.prototype */{ className: 'media-iframe', /** - * @returns {wp.media.view.Iframe} Returns itself to allow chaining + * @return {wp.media.view.Iframe} Returns itself to allow chaining. */ render: function() { this.views.detach(); @@ -8914,6 +9488,7 @@ var View = wp.media.View, $ = jQuery, + l10n = wp.media.view.l10n, EmbedUrl; /** @@ -8927,17 +9502,17 @@ * @augments Backbone.View */ EmbedUrl = View.extend(/** @lends wp.media.view.EmbedUrl.prototype */{ - tagName: 'label', + tagName: 'span', className: 'embed-url', events: { - 'input': 'url', - 'keyup': 'url', - 'change': 'url' + 'input': 'url' }, initialize: function() { - this.$input = $('').val( this.model.get('url') ); + this.$input = $( '' ) + .attr( 'aria-label', l10n.insertFromUrlTitle ) + .val( this.model.get('url') ); this.input = this.$input[0]; this.spinner = $('')[0]; @@ -8952,7 +9527,7 @@ } }, /** - * @returns {wp.media.view.EmbedUrl} Returns itself to allow chaining + * @return {wp.media.view.EmbedUrl} Returns itself to allow chaining. */ render: function() { var $input = this.$input; @@ -8969,24 +9544,8 @@ return this; }, - ready: function() { - if ( ! wp.media.isTouchDevice ) { - this.focus(); - } - }, - url: function( event ) { this.model.set( 'url', $.trim( event.target.value ) ); - }, - - /** - * If the input is visible, focus and select its contents. - */ - focus: function() { - var $input = this.$input; - if ( $input.is(':visible') ) { - $input.focus()[0].select(); - } } }); @@ -9022,11 +9581,11 @@ updateoEmbed: _.debounce( function() { var url = this.model.get( 'url' ); - // clear out previous results + // Clear out previous results. this.$('.embed-container').hide().find('.embed-preview').empty(); this.$( '.setting' ).hide(); - // only proceed with embed if the field contains more than 11 characters + // Only proceed with embed if the field contains more than 11 characters. // Example: http://a.io is 11 chars if ( url && ( url.length < 11 || ! url.match(/^http(s)?:\/\//) ) ) { return; @@ -9038,7 +9597,7 @@ fetch: function() { var url = this.model.get( 'url' ), re, youTubeEmbedMatch; - // check if they haven't typed in 500 ms + // Check if they haven't typed in 500 ms. if ( $('#embed-url-field').val() !== url ) { return; } @@ -9168,7 +9727,7 @@ 'keyup [data-setting="customHeight"]': 'onCustomSize' } ), initialize: function() { - // used in AttachmentDisplay.prototype.updateLinkTo + // Used in AttachmentDisplay.prototype.updateLinkTo. this.options.attachment = this.model.attachment; this.listenTo( this.model, 'change:url', this.updateUrl ); this.listenTo( this.model, 'change:link', this.toggleLinkSettings ); @@ -9212,7 +9771,7 @@ }, postRender: function() { - setTimeout( _.bind( this.resetFocus, this ), 10 ); + setTimeout( _.bind( this.scrollToTop, this ), 10 ); this.toggleLinkSettings(); if ( window.getUserSetting( 'advImgDetails' ) === 'show' ) { this.toggleAdvanced( true ); @@ -9220,8 +9779,7 @@ this.trigger( 'post-render' ); }, - resetFocus: function() { - this.$( '.link-to-custom' ).blur(); + scrollToTop: function() { this.$( '.embed-media-settings' ).scrollTop( 0 ); }, @@ -9251,7 +9809,7 @@ num = $( event.target ).val(), value; - // Ignore bogus input + // Ignore bogus input. if ( ! /^\d+/.test( num ) || parseInt( num, 10 ) < 1 ) { event.preventDefault(); return; @@ -9553,12 +10111,7 @@ }, loadEditor: function() { - var dfd = this.editor.open( this.model.get('id'), this.model.get('nonces').edit, this ); - dfd.done( _.bind( this.focus, this ) ); - }, - - focus: function() { - this.$( '.imgedit-submit .button' ).eq( 0 ).focus(); + this.editor.open( this.model.get( 'id' ), this.model.get( 'nonces' ).edit, this ); }, back: function() {