/* global _wpMediaViewsL10n, confirm, getUserSetting, setUserSetting */
-(function($, _){
- var media = wp.media, l10n;
+( function( $, _ ) {
+ var l10n,
+ media = wp.media,
+ isTouchDevice = ( 'ontouchend' in document );
// Link any localized strings.
l10n = media.view.l10n = typeof _wpMediaViewsL10n === 'undefined' ? {} : _wpMediaViewsL10n;
_.extend( media.controller.Region.prototype, {
/**
- * Switch modes
+ * Activate a mode.
*
* @param {string} mode
*
- * @fires wp.media.controller.Region#{id}:activate:{mode}
- * @fires wp.media.controller.Region#{id}:deactivate:{mode}
+ * @fires this.view#{this.id}:activate:{this._mode}
+ * @fires this.view#{this.id}:activate
+ * @fires this.view#{this.id}:deactivate:{this._mode}
+ * @fires this.view#{this.id}:deactivate
*
- * @returns {wp.media.controller.Region} Returns itself to allow chaining
+ * @returns {wp.media.controller.Region} Returns itself to allow chaining.
*/
mode: function( mode ) {
if ( ! mode ) {
return this;
}
+ /**
+ * Region mode deactivation event.
+ *
+ * @event this.view#{this.id}:deactivate:{this._mode}
+ * @event this.view#{this.id}:deactivate
+ */
this.trigger('deactivate');
+
this._mode = mode;
this.render( mode );
+
+ /**
+ * Region mode activation event.
+ *
+ * @event this.view#{this.id}:activate:{this._mode}
+ * @event this.view#{this.id}:activate
+ */
this.trigger('activate');
return this;
},
/**
- * Render a new mode, the view is set in the `create` callback method
- * of the extending class
- *
- * If no mode is provided, just re-render the current mode.
- * If the provided mode isn't active, perform a full switch.
+ * Render a mode.
*
* @param {string} mode
*
- * @fires wp.media.controller.Region#{id}:create:{mode}
- * @fires wp.media.controller.Region#{id}:render:{mode}
+ * @fires this.view#{this.id}:create:{this._mode}
+ * @fires this.view#{this.id}:create
+ * @fires this.view#{this.id}:render:{this._mode}
+ * @fires this.view#{this.id}:render
*
* @returns {wp.media.controller.Region} Returns itself to allow chaining
*/
render: function( mode ) {
+ // If the mode isn't active, activate it.
if ( mode && mode !== this._mode ) {
return this.mode( mode );
}
var set = { view: null },
view;
+ /**
+ * Create region view event.
+ *
+ * Region view creation takes place in an event callback on the frame.
+ *
+ * @event this.view#{this.id}:create:{this._mode}
+ * @event this.view#{this.id}:create
+ */
this.trigger( 'create', set );
view = set.view;
+
+ /**
+ * Render region view event.
+ *
+ * Region view creation takes place in an event callback on the frame.
+ *
+ * @event this.view#{this.id}:create:{this._mode}
+ * @event this.view#{this.id}:create
+ */
this.trigger( 'render', view );
if ( view ) {
this.set( view );
},
/**
- * @returns {wp.media.View} Returns the selector's first subview
+ * Get the region's view.
+ *
+ * @returns {wp.media.View}
*/
get: function() {
return this.view.views.first( this.selector );
},
/**
+ * Set the region's view as a subview of the frame.
+ *
* @param {Array|Object} views
* @param {Object} [options={}]
* @returns {wp.Backbone.Subviews} Subviews is returned to allow chaining
},
/**
- * Helper function to trigger view events based on {id}:{event}:{mode}
+ * Trigger regional view events on the frame.
*
* @param {string} event
- * @returns {undefined|wp.media.controller.Region} Returns itself to allow chaining
+ * @returns {undefined|wp.media.controller.Region} Returns itself to allow chaining.
*/
trigger: function( event ) {
var base, args;
args = _.toArray( arguments );
base = this.id + ':' + event;
- // Trigger `region:action:mode` event.
+ // Trigger `{this.id}:{event}:{this._mode}` event on the frame.
args[0] = base + ':' + this._mode;
this.view.trigger.apply( this.view, args );
- // Trigger `region:action` event.
+ // Trigger `{this.id}:{event}` event on the frame.
args[0] = base;
this.view.trigger.apply( this.view, args );
return this;
// Use Backbone's self-propagating `extend` inheritance method.
media.controller.StateMachine.extend = Backbone.Model.extend;
- // Add events to the `StateMachine`.
_.extend( media.controller.StateMachine.prototype, Backbone.Events, {
/**
* Fetch a state.
}
});
- // Map methods from the `states` collection to the `StateMachine` itself.
+ // Map all event binding and triggering on a StateMachine to its `states` collection.
_.each([ 'on', 'off', 'trigger' ], function( method ) {
/**
- * @returns {wp.media.controller.StateMachine} Returns itself to allow chaining
+ * @returns {wp.media.controller.StateMachine} Returns itself to allow chaining.
*/
media.controller.StateMachine.prototype[ method ] = function() {
// Ensure that the `states` collection exists so the `StateMachine`
/**
* wp.media.controller.State
*
- * A state is a step in a workflow that when set will trigger
- * the controllers for the regions to be updated as specified. This
- * class is the base class that the various states used in the media
- * modals extend.
+ * A state is a step in a workflow that when set will trigger the controllers
+ * for the regions to be updated as specified in the frame. This is the base
+ * class that the various states used in wp.media extend.
*
* @constructor
* @augments Backbone.Model
Backbone.Model.apply( this, arguments );
this.on( 'change:menu', this._updateMenu, this );
},
-
/**
* @abstract
*/
},
/**
* @access private
- */
+ */
_preActivate: function() {
this.active = true;
},
mode = this.get('menu'),
view;
+ this.frame.$el.toggleClass( 'hide-menu', ! mode );
if ( ! mode ) {
return;
}
};
/**
- * wp.media.controller.Library
+ * A state for choosing an attachment from the media library.
*
* @constructor
* @augments wp.media.controller.State
*/
media.controller.Library = media.controller.State.extend({
defaults: {
- id: 'library',
- multiple: false, // false, 'add', 'reset'
- describe: false,
- toolbar: 'select',
- sidebar: 'settings',
- content: 'upload',
- router: 'browse',
- menu: 'default',
- searchable: true,
- filterable: false,
- sortable: true,
- title: l10n.mediaLibraryTitle,
-
+ id: 'library',
+ title: l10n.mediaLibraryTitle,
+ // Selection defaults. @see media.model.Selection
+ multiple: false,
+ // Initial region modes.
+ content: 'upload',
+ menu: 'default',
+ router: 'browse',
+ toolbar: 'select',
+ // Attachments browser defaults. @see media.view.AttachmentsBrowser
+ searchable: true,
+ filterable: false,
+ sortable: true,
+
+ autoSelect: true,
+ describe: false,
// Uses a user setting to override the content mode.
contentUserSetting: true,
-
// Sync the selection from the last state when 'multiple' matches.
- syncSelection: true
+ syncSelection: true
},
/**
}) );
}
- if ( ! this.get('edge') ) {
- this.set( 'edge', 120 );
- }
-
- if ( ! this.get('gutter') ) {
- this.set( 'gutter', 8 );
- }
-
this.resetDisplays();
},
this.get('selection').on( 'add remove reset', this.refreshContent, this );
- if ( this.get('contentUserSetting') ) {
+ if ( this.get( 'router' ) && this.get('contentUserSetting') ) {
this.frame.on( 'content:activate', this.saveContentMode, this );
this.set( 'content', getUserSetting( 'libraryContent', this.get('content') ) );
}
if ( 'upload' === content.mode() ) {
this.frame.content.mode('browse');
}
- this.get('selection').add( attachment );
+
+ if ( this.get( 'autoSelect' ) ) {
+ this.get('selection').add( attachment );
+ this.frame.trigger( 'library:selection:add' );
+ }
},
/**
_.extend( media.controller.Library.prototype, media.selectionSync );
/**
- * wp.media.controller.ImageDetails
+ * A state for editing the settings of an image within a content editor.
*
* @constructor
* @augments wp.media.controller.State
*/
media.controller.ImageDetails = media.controller.State.extend({
defaults: _.defaults({
- id: 'image-details',
- toolbar: 'image-details',
- title: l10n.imageDetailsTitle,
- content: 'image-details',
- menu: 'image-details',
- router: false,
- attachment: false,
- priority: 60,
- editing: false
+ id: 'image-details',
+ title: l10n.imageDetailsTitle,
+ // Initial region modes.
+ content: 'image-details',
+ menu: false,
+ router: false,
+ toolbar: 'image-details',
+
+ editing: false,
+ priority: 60
}, media.controller.Library.prototype.defaults ),
initialize: function( options ) {
});
/**
- * wp.media.controller.GalleryEdit
+ * A state for editing a gallery's images and settings.
*
* @constructor
* @augments wp.media.controller.Library
*/
media.controller.GalleryEdit = media.controller.Library.extend({
defaults: {
- id: 'gallery-edit',
- multiple: false,
- describe: true,
- edge: 199,
- editing: false,
- sortable: true,
- searchable: false,
- toolbar: 'gallery-edit',
- content: 'browse',
- title: l10n.editGalleryTitle,
- priority: 60,
- dragInfo: true,
+ id: 'gallery-edit',
+ title: l10n.editGalleryTitle,
+ // Selection defaults. @see media.model.Selection
+ multiple: false,
+ // Attachments browser defaults. @see media.view.AttachmentsBrowser
+ searchable: false,
+ sortable: true,
+ display: false,
+ // Initial region modes.
+ content: 'browse',
+ toolbar: 'gallery-edit',
+
+ describe: true,
+ displaySettings: true,
+ dragInfo: true,
+ idealColumnWidth: 170,
+ editing: false,
+ priority: 60,
// Don't sync the selection, as the Edit Gallery library
// *is* the selection.
},
gallerySettings: function( browser ) {
+ if ( ! this.get('displaySettings') ) {
+ return;
+ }
+
var library = this.get('library');
- if ( ! library || ! browser )
+ if ( ! library || ! browser ) {
return;
+ }
library.gallery = library.gallery || new Backbone.Model();
});
/**
- * wp.media.controller.GalleryAdd
+ * A state for adding an image to a gallery.
*
* @constructor
* @augments wp.media.controller.Library
*/
media.controller.GalleryAdd = media.controller.Library.extend({
defaults: _.defaults({
- id: 'gallery-library',
- filterable: 'uploaded',
- multiple: 'add',
- menu: 'gallery',
- toolbar: 'gallery-add',
- title: l10n.addToGalleryTitle,
- priority: 100,
+ id: 'gallery-library',
+ title: l10n.addToGalleryTitle,
+ // Selection defaults. @see media.model.Selection
+ multiple: 'add',
+ // Attachments browser defaults. @see media.view.AttachmentsBrowser
+ filterable: 'uploaded',
+ // Initial region modes.
+ menu: 'gallery',
+ toolbar: 'gallery-add',
+ priority: 100,
// Don't sync the selection, as the Edit Gallery library
// *is* the selection.
syncSelection: false
*/
media.controller.CollectionEdit = media.controller.Library.extend({
defaults: {
+ // Selection defaults. @see media.model.Selection
multiple: false,
- describe: true,
- edge: 199,
- editing: false,
+ // Attachments browser defaults. @see media.view.AttachmentsBrowser
sortable: true,
searchable: false,
+ // Region mode defaults.
content: 'browse',
- priority: 60,
- dragInfo: true,
- SettingsView: false,
+
+ describe: true,
+ dragInfo: true,
+ idealColumnWidth: 170,
+ editing: false,
+ priority: 60,
+ SettingsView: false,
// Don't sync the selection, as the Edit {Collection} library
// *is* the selection.
*/
media.controller.CollectionAdd = media.controller.Library.extend({
defaults: _.defaults( {
- filterable: 'uploaded',
+ // Selection defaults. @see media.model.Selection
multiple: 'add',
+ // Attachments browser defaults. @see media.view.AttachmentsBrowser
+ filterable: 'uploaded',
+
priority: 100,
syncSelection: false
}, media.controller.Library.prototype.defaults ),
});
/**
- * wp.media.controller.FeaturedImage
+ * A state for selecting a featured image for a post.
*
* @constructor
* @augments wp.media.controller.Library
*/
media.controller.FeaturedImage = media.controller.Library.extend({
defaults: _.defaults({
- id: 'featured-image',
- filterable: 'uploaded',
- multiple: false,
- toolbar: 'featured-image',
- title: l10n.setFeaturedImageTitle,
- priority: 60,
+ id: 'featured-image',
+ title: l10n.setFeaturedImageTitle,
+ // Selection defaults. @see media.model.Selection
+ multiple: false,
+ // Attachments browser defaults. @see media.view.AttachmentsBrowser
+ filterable: 'uploaded',
+ // Region mode defaults.
+ toolbar: 'featured-image',
+
+ priority: 60,
syncSelection: true
}, media.controller.Library.prototype.defaults ),
});
/**
- * wp.media.controller.ReplaceImage
- *
- * Replace a selected single image
+ * A state for replacing an image.
*
* @constructor
* @augments wp.media.controller.Library
*/
media.controller.ReplaceImage = media.controller.Library.extend({
defaults: _.defaults({
- id: 'replace-image',
- filterable: 'uploaded',
- multiple: false,
- toolbar: 'replace',
- title: l10n.replaceImageTitle,
- priority: 60,
+ id: 'replace-image',
+ title: l10n.replaceImageTitle,
+ // Selection defaults. @see media.model.Selection
+ multiple: false,
+ // Attachments browser defaults. @see media.view.AttachmentsBrowser
+ filterable: 'uploaded',
+ // Region mode defaults.
+ toolbar: 'replace',
+ menu: false,
+
+ priority: 60,
syncSelection: true
}, media.controller.Library.prototype.defaults ),
});
/**
- * wp.media.controller.EditImage
+ * A state for editing (cropping, etc.) an image.
*
* @constructor
* @augments wp.media.controller.State
*/
media.controller.EditImage = media.controller.State.extend({
defaults: {
- id: 'edit-image',
- url: '',
- menu: false,
+ id: 'edit-image',
+ title: l10n.editImage,
+ // Region mode defaults.
+ menu: false,
toolbar: 'edit-image',
- title: l10n.editImage,
- content: 'edit-image'
+ content: 'edit-image',
+
+ url: ''
},
activate: function() {
*/
media.controller.MediaLibrary = media.controller.Library.extend({
defaults: _.defaults({
- filterable: 'uploaded',
- priority: 80,
- syncSelection: false,
- displaySettings: false
+ // Attachments browser defaults. @see media.view.AttachmentsBrowser
+ filterable: 'uploaded',
+
+ displaySettings: false,
+ priority: 80,
+ syncSelection: false
}, media.controller.Library.prototype.defaults ),
initialize: function( options ) {
*/
media.controller.Embed = media.controller.State.extend({
defaults: {
- id: 'embed',
- url: '',
- menu: 'default',
- content: 'embed',
- toolbar: 'main-embed',
- type: 'link',
-
+ id: 'embed',
title: l10n.insertFromUrlTitle,
- priority: 120
+ // Region mode defaults.
+ content: 'embed',
+ menu: 'default',
+ toolbar: 'main-embed',
+
+ priority: 120,
+ type: 'link',
+ url: '',
+ metadata: {}
},
// The amount of time used when debouncing the scan.
sensitivity: 200,
- initialize: function() {
+ initialize: function(options) {
+ this.metadata = options.metadata;
this.debouncedScan = _.debounce( _.bind( this.scan, this ), this.sensitivity );
- this.props = new Backbone.Model({ url: '' });
+ this.props = new Backbone.Model( this.metadata || { url: '' });
this.props.on( 'change:url', this.debouncedScan, this );
this.props.on( 'change:url', this.refresh, this );
this.on( 'scan', this.scanImage, this );
*/
media.controller.Cropper = media.controller.State.extend({
defaults: {
- id: 'cropper',
- title: l10n.cropImage,
- toolbar: 'crop',
- content: 'crop',
- router: false,
+ id: 'cropper',
+ title: l10n.cropImage,
+ // Region mode defaults.
+ toolbar: 'crop',
+ content: 'crop',
+ router: false,
+
canSkipCrop: false
},
*/
media.view.Frame = media.View.extend({
initialize: function() {
+ _.defaults( this.options, {
+ mode: [ 'select' ]
+ });
this._createRegions();
this._createStates();
+ this._createModes();
},
_createRegions: function() {
this.states.add( this.options.states );
}
},
+ _createModes: function() {
+ // Store active "modes" that the frame is in. Unrelated to region modes.
+ this.activeModes = new Backbone.Collection();
+ this.activeModes.on( 'add remove reset', _.bind( this.triggerModeEvents, this ) );
+
+ _.each( this.options.mode, function( mode ) {
+ this.activateMode( mode );
+ }, this );
+ },
/**
* @returns {wp.media.view.Frame} Returns itself to allow chaining
*/
reset: function() {
this.states.invoke( 'trigger', 'reset' );
return this;
+ },
+ /**
+ * Map activeMode collection events to the frame.
+ */
+ triggerModeEvents: function( model, collection, options ) {
+ var collectionEvent,
+ modeEventMap = {
+ add: 'activate',
+ remove: 'deactivate'
+ },
+ eventToTrigger;
+ // Probably a better way to do this.
+ _.each( options, function( value, key ) {
+ if ( value ) {
+ collectionEvent = key;
+ }
+ } );
+
+ if ( ! _.has( modeEventMap, collectionEvent ) ) {
+ return;
+ }
+
+ eventToTrigger = model.get('id') + ':' + modeEventMap[collectionEvent];
+ this.trigger( eventToTrigger );
+ },
+ /**
+ * Activate a mode on the frame.
+ *
+ * @param string mode Mode ID.
+ * @returns {this} Returns itself to allow chaining.
+ */
+ activateMode: function( mode ) {
+ // Bail if the mode is already active.
+ if ( this.isModeActive( mode ) ) {
+ return;
+ }
+ this.activeModes.add( [ { id: mode } ] );
+ // Add a CSS class to the frame so elements can be styled for the mode.
+ this.$el.addClass( 'mode-' + mode );
+
+ return this;
+ },
+ /**
+ * Deactivate a mode on the frame.
+ *
+ * @param string mode Mode ID.
+ * @returns {this} Returns itself to allow chaining.
+ */
+ deactivateMode: function( mode ) {
+ // Bail if the mode isn't active.
+ if ( ! this.isModeActive( mode ) ) {
+ return this;
+ }
+ this.activeModes.remove( this.activeModes.where( { id: mode } ) );
+ this.$el.removeClass( 'mode-' + mode );
+ /**
+ * Frame mode deactivation event.
+ *
+ * @event this#{mode}:deactivate
+ */
+ this.trigger( mode + ':deactivate' );
+
+ return this;
+ },
+ /**
+ * Check if a mode is enabled on the frame.
+ *
+ * @param string mode Mode ID.
+ * @return bool
+ */
+ isModeActive: function( mode ) {
+ return Boolean( this.activeModes.where( { id: mode } ).length );
}
});
template: media.template('media-frame'),
regions: ['menu','title','content','toolbar','router'],
+ events: {
+ 'click div.media-frame-title h1': 'toggleMenu'
+ },
+
/**
* @global wp.Uploader
*/
initialize: function() {
-
media.view.Frame.prototype.initialize.apply( this, arguments );
_.defaults( this.options, {
this.on( 'title:create:default', this.createTitle, this );
this.title.mode('default');
+ this.on( 'title:render', function( view ) {
+ view.$el.append( '<span class="dashicons dashicons-arrow-down"></span>' );
+ });
+
// Bind default menu.
this.on( 'menu:create:default', this.createMenu, this );
},
controller: this
});
},
+
+ toggleMenu: function() {
+ this.$el.find( '.media-menu' ).toggleClass( 'visible' );
+ },
+
/**
* @param {Object} toolbar
* @this wp.media.controller.Region
this.bindHandlers();
},
+ /**
+ * Attach a selection collection to the frame.
+ *
+ * A selection is a collection of attachments used for a specific purpose
+ * by a media frame. e.g. Selecting an attachment (or many) to insert into
+ * post content.
+ *
+ * @see media.model.Selection
+ */
createSelection: function() {
var selection = this.options.selection;
};
},
+ /**
+ * Create the default states on the frame.
+ */
createStates: function() {
var options = this.options;
]);
},
+ /**
+ * Bind region mode event callbacks.
+ *
+ * @see media.controller.Region.render
+ */
bindHandlers: function() {
this.on( 'router:create:browse', this.createRouter, this );
this.on( 'router:render:browse', this.browseRouter, this );
this.on( 'toolbar:create:select', this.createSelectToolbar, this );
},
- // Routers
- browseRouter: function( view ) {
- view.set({
+ /**
+ * Render callback for the router region in the `browse` mode.
+ *
+ * @param {wp.media.view.Router} routerView
+ */
+ browseRouter: function( routerView ) {
+ routerView.set({
upload: {
text: l10n.uploadFilesTitle,
priority: 20
},
/**
- * Content
+ * Render callback for the content region in the `browse` mode.
*
- * @param {Object} content
- * @this wp.media.controller.Region
+ * @param {wp.media.controller.Region} contentRegion
*/
- browseContent: function( content ) {
+ browseContent: function( contentRegion ) {
var state = this.state();
this.$el.removeClass('hide-toolbar');
// Browse our library of attachments.
- content.view = new media.view.AttachmentsBrowser({
+ contentRegion.view = new media.view.AttachmentsBrowser({
controller: this,
collection: state.get('library'),
selection: state.get('selection'),
sortable: state.get('sortable'),
search: state.get('searchable'),
filters: state.get('filterable'),
- display: state.get('displaySettings'),
+ display: state.has('display') ? state.get('display') : state.get('displaySettings'),
dragInfo: state.get('dragInfo'),
- suggestedWidth: state.get('suggestedWidth'),
- suggestedHeight: state.get('suggestedHeight'),
+ idealColumnWidth: state.get('idealColumnWidth'),
+ suggestedWidth: state.get('suggestedWidth'),
+ suggestedHeight: state.get('suggestedHeight'),
AttachmentView: state.get('AttachmentView')
});
},
/**
- *
- * @this wp.media.controller.Region
+ * Render callback for the content region in the `upload` mode.
*/
uploadContent: function() {
- this.$el.removeClass('hide-toolbar');
+ this.$el.removeClass( 'hide-toolbar' );
this.content.set( new media.view.UploaderInline({
controller: this
}) );
_.defaults( this.options, {
multiple: true,
editing: false,
- state: 'insert'
+ state: 'insert',
+ metadata: {}
});
/**
* call 'initialize' directly on the parent class
}),
// Embed states.
- new media.controller.Embed(),
+ new media.controller.Embed( { metadata: options.metadata } ),
new media.controller.EditImage( { model: options.editImage } ),
} else {
frame.close();
}
+
+ // Keep focus inside media modal
+ // after canceling a gallery
+ this.controller.modal.focusManager.focus();
}
},
separateCancel: new media.View({
}).render();
this.content.set( view );
- view.url.focus();
+
+ if ( ! isTouchDevice ) {
+ view.url.focus();
+ }
},
editSelectionContent: function() {
}) );
this.controller.setState('gallery-edit');
+
+ // Keep focus inside media modal
+ // after jumping to gallery view
+ this.controller.modal.focusManager.focus();
}
});
},
}) );
this.controller.setState('playlist-edit');
+
+ // Keep focus inside media modal
+ // after jumping to playlist view
+ this.controller.modal.focusManager.focus();
}
});
},
}) );
this.controller.setState('video-playlist-edit');
+
+ // Keep focus inside media modal
+ // after jumping to video playlist view
+ this.controller.modal.focusManager.focus();
}
});
},
this.on( 'menu:create:image-details', this.createMenu, this );
this.on( 'content:create:image-details', this.imageDetailsContent, this );
this.on( 'content:render:edit-image', this.editImageContent, this );
- this.on( 'menu:render:image-details', this.renderMenu, this );
this.on( 'toolbar:render:image-details', this.renderImageDetailsToolbar, this );
// override the select toolbar
this.on( 'toolbar:render:replace', this.renderReplaceImageToolbar, this );
this.states.add([
new media.controller.ImageDetails({
image: this.image,
- editable: false,
- menu: 'image-details'
+ editable: false
}),
new media.controller.ReplaceImage({
id: 'replace-image',
image: this.image,
multiple: false,
title: l10n.imageReplaceTitle,
- menu: 'image-details',
toolbar: 'replace',
priority: 80,
displaySettings: true
},
- renderMenu: function( view ) {
- var lastState = this.lastState(),
- previous = lastState && lastState.id,
- frame = this;
-
- view.set({
- cancel: {
- text: l10n.imageDetailsCancel,
- priority: 20,
- click: function() {
- if ( previous ) {
- frame.setState( previous );
- } else {
- frame.close();
- }
- }
- },
- separateCancel: new media.View({
- className: 'separator',
- priority: 40
- })
- });
-
- },
-
renderImageDetailsToolbar: function() {
this.toolbar.set( new media.view.Toolbar({
controller: this,
propagate: true,
freeze: true
});
+
+ this.focusManager = new media.view.FocusManager({
+ el: this.el
+ });
},
/**
* @returns {Object}
*/
open: function() {
var $el = this.$el,
- options = this.options;
+ options = this.options,
+ mceEditor;
if ( $el.is(':visible') ) {
return this;
};
}
- $el.show().focus();
+ // Disable page scrolling.
+ $( 'body' ).addClass( 'modal-open' );
+
+ $el.show();
+
+ // Try to close the onscreen keyboard
+ if ( 'ontouchend' in document ) {
+ if ( ( mceEditor = window.tinymce && window.tinymce.activeEditor ) && ! mceEditor.isHidden() && mceEditor.iframeElement ) {
+ mceEditor.iframeElement.focus();
+ mceEditor.iframeElement.blur();
+
+ setTimeout( function() {
+ mceEditor.iframeElement.blur();
+ }, 100 );
+ }
+ }
+
+ this.$el.focus();
+
return this.propagate('open');
},
return this;
}
- this.$el.hide();
+ // Enable page scrolling.
+ $( 'body' ).removeClass( 'modal-open' );
+
+ // Hide modal and remove restricted media modal tab focus once it's closed
+ this.$el.hide().undelegate( 'keydown' );
+
+ // Put focus back in useful location once modal is closed
+ $('#wpbody-content').focus();
+
this.propagate('close');
// If the `freeze` option is set, restore the container's scroll position.
* @augments Backbone.View
*/
media.view.FocusManager = media.View.extend({
+
events: {
- keydown: 'recordTab',
- focusin: 'updateIndex'
+ 'keydown': 'constrainTabbing'
},
- focus: function() {
- if ( _.isUndefined( this.index ) ) {
- return;
- }
-
- // Update our collection of `$tabbables`.
- this.$tabbables = this.$(':tabbable');
-
- // If tab is saved, focus it.
- this.$tabbables.eq( this.index ).focus();
+ focus: function() { // Reset focus on first left menu item
+ this.$('.media-menu-item').first().focus();
},
/**
* @param {Object} event
*/
- recordTab: function( event ) {
+ constrainTabbing: function( event ) {
+ var tabbables;
+
// Look for the tab key.
if ( 9 !== event.keyCode ) {
return;
}
- // First try to update the index.
- if ( _.isUndefined( this.index ) ) {
- this.updateIndex( event );
- }
-
- // If we still don't have an index, bail.
- if ( _.isUndefined( this.index ) ) {
- return;
- }
-
- var index = this.index + ( event.shiftKey ? -1 : 1 );
-
- if ( index >= 0 && index < this.$tabbables.length ) {
- this.index = index;
- } else {
- delete this.index;
- }
- },
- /**
- * @param {Object} event
- */
- updateIndex: function( event ) {
- this.$tabbables = this.$(':tabbable');
-
- var index = this.$tabbables.index( event.target );
+ tabbables = this.$( ':tabbable' );
- if ( -1 === index ) {
- delete this.index;
- } else {
- this.index = index;
+ // Keep tab focus within media modal while it's open
+ if ( tabbables.last()[0] === event.target && ! event.shiftKey ) {
+ tabbables.first().focus();
+ return false;
+ } else if ( tabbables.first()[0] === event.target && event.shiftKey ) {
+ tabbables.last().focus();
+ return false;
}
}
+
});
/**
$el.hide();
}
});
+
+ // https://core.trac.wordpress.org/ticket/27341
+ _.delay( function() {
+ if ( '0' === $el.css('opacity') && $el.is(':visible') ) {
+ $el.hide();
+ }
+ }, 500 );
}
});
className: 'uploader-inline',
template: media.template('uploader-inline'),
+ events: {
+ 'click .close': 'hide'
+ },
+
initialize: function() {
_.defaults( this.options, {
message: '',
- status: true
+ status: true,
+ canClose: false
});
if ( ! this.options.$browser && this.controller.uploader ) {
prepare: function() {
var suggestedWidth = this.controller.state().get('suggestedWidth'),
- suggestedHeight = this.controller.state().get('suggestedHeight');
+ suggestedHeight = this.controller.state().get('suggestedHeight'),
+ data = {};
+
+ data.message = this.options.message;
+ data.canClose = this.options.canClose;
if ( suggestedWidth && suggestedHeight ) {
- return {
- suggestedWidth: suggestedWidth,
- suggestedHeight: suggestedHeight
- };
+ data.suggestedWidth = suggestedWidth;
+ data.suggestedHeight = suggestedHeight;
}
+
+ return data;
},
/**
* @returns {wp.media.view.UploaderInline} Returns itself to allow chaining
this.refresh();
return this;
+ },
+ show: function() {
+ this.$el.removeClass( 'hidden' );
+ },
+ hide: function() {
+ this.$el.addClass( 'hidden' );
}
+
});
/**
// The toolbar is composed of two `PriorityList` views.
this.primary = new media.view.PriorityList();
this.secondary = new media.view.PriorityList();
- this.primary.$el.addClass('media-toolbar-primary');
+ this.primary.$el.addClass('media-toolbar-primary search-form');
this.secondary.$el.addClass('media-toolbar-secondary');
this.views.set([ this.secondary, this.primary ]);
} else {
this.click();
}
+
+ // When selecting a tab along the left side,
+ // focus should be transferred into the main panel
+ if ( ! isTouchDevice ) {
+ $('.media-frame-content input').first().focus();
+ }
},
click: function() {
var state = this.options.state;
+
if ( state ) {
this.controller.setState( state );
+ this.views.parent.$el.removeClass( 'visible' ); // TODO: or hide on any click, see below
}
},
/**
property: 'state',
ItemView: media.view.MenuItem,
region: 'menu',
+
+ /* TODO: alternatively hide on any click anywhere
+ events: {
+ 'click': 'click'
+ },
+
+ click: function() {
+ this.$el.removeClass( 'visible' );
+ },
+ */
+
/**
* @param {Object} options
* @param {string} id
* @augments Backbone.View
*/
media.view.RouterItem = media.view.MenuItem.extend({
+ /**
+ * On click handler to activate the content region's corresponding mode.
+ */
click: function() {
var contentMode = this.options.contentMode;
if ( contentMode ) {
className: 'attachment',
template: media.template('attachment'),
+ attributes: function() {
+ return {
+ 'tabIndex': 0,
+ 'role': 'checkbox',
+ 'aria-label': this.model.get( 'title' ),
+ 'aria-checked': false,
+ 'data-id': this.model.get( 'id' )
+ };
+ },
+
events: {
- 'click .attachment-preview': 'toggleSelectionHandler',
+ 'click .js--select-attachment': 'toggleSelectionHandler',
'change [data-setting]': 'updateSetting',
'change [data-setting] input': 'updateSetting',
'change [data-setting] select': 'updateSetting',
'change [data-setting] textarea': 'updateSetting',
'click .close': 'removeFromLibrary',
- 'click .check': 'removeFromSelection',
- 'click a': 'preventDefault'
+ 'click .check': 'checkClickHandler',
+ 'click a': 'preventDefault',
+ 'keydown': 'toggleSelectionHandler'
},
buttons: {},
initialize: function() {
- var selection = this.options.selection;
+ var selection = this.options.selection,
+ options = _.defaults( this.options, {
+ rerenderOnModelChange: true
+ } );
- this.model.on( 'change:sizes change:uploading', this.render, this );
+ if ( options.rerenderOnModelChange ) {
+ this.model.on( 'change', this.render, this );
+ } else {
+ this.model.on( 'change:percent', this.progress, this );
+ }
this.model.on( 'change:title', this._syncTitle, this );
this.model.on( 'change:caption', this._syncCaption, this );
- this.model.on( 'change:percent', this.progress, this );
+ this.model.on( 'change:artist', this._syncArtist, this );
+ this.model.on( 'change:album', this._syncAlbum, this );
// Update the selection.
this.model.on( 'add', this.select, this );
this.model.on( 'remove', this.deselect, this );
if ( selection ) {
selection.on( 'reset', this.updateSelect, this );
+ // Update the model's details view.
+ this.model.on( 'selection:single selection:unsingle', this.details, this );
+ this.details( this.model, this.controller.state().get('selection') );
}
-
- // Update the model's details view.
- this.model.on( 'selection:single selection:unsingle', this.details, this );
- this.details( this.model, this.controller.state().get('selection') );
},
/**
* @returns {wp.media.view.Attachment} Returns itself to allow chaining
compat: false,
alt: '',
description: ''
- });
+ }, this.options );
options.buttons = this.buttons;
options.describe = this.controller.state().get('describe');
options.allowLocalEdits = true;
}
+ if ( options.uploading && ! options.percent ) {
+ options.percent = 0;
+ }
+
this.views.detach();
this.$el.html( this.template( options ) );
this.$el.toggleClass( 'uploading', options.uploading );
+
if ( options.uploading ) {
this.$bar = this.$('.media-progress-bar div');
} else {
this.$bar.width( this.model.get('percent') + '%' );
}
},
+
/**
* @param {Object} event
*/
toggleSelectionHandler: function( event ) {
var method;
+ // Don't do anything inside inputs.
+ if ( 'INPUT' === event.target.nodeName ) {
+ return;
+ }
+
+ // 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
+ if ( 'keydown' === event.type && 13 !== event.keyCode && 32 !== event.keyCode ) {
+ return;
+ }
+
+ // 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
+ this.controller.trigger( 'edit:attachment', this.model, event.currentTarget );
+
+ // Don't scroll the view and don't attempt to submit anything.
+ event.stopPropagation();
+ return;
+ }
+
+ if ( this.controller.isModeActive( 'select' ) ) {
+ method = 'toggle';
+ }
+ }
+
if ( event.shiftKey ) {
method = 'between';
} else if ( event.ctrlKey || event.metaKey ) {
this.toggleSelection({
method: method
});
- },
+
+ this.controller.trigger( 'selection:toggle' );
+
+ // Don't scroll the view and don't attempt to submit anything.
+ event.stopPropagation();
+ },
/**
* @param {Object} options
*/
selection[ this.selected() ? 'remove' : 'add' ]( model );
selection.single( model );
return;
+ } else if ( 'add' === method ) {
+ selection.add( model );
+ selection.single( model );
+ return;
+ }
+
+ // Fixes bug that loses focus when selecting a featured image
+ if ( ! method ) {
+ method = 'add';
}
if ( method !== 'add' ) {
* @param {Backbone.Collection} collection
*/
select: function( model, collection ) {
- var selection = this.options.selection;
+ 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
return;
}
- this.$el.addClass('selected');
+ // Bail if the model is already selected.
+ if ( this.$el.hasClass( 'selected' ) ) {
+ return;
+ }
+
+ // Add 'selected' class to model, set aria-checked to true.
+ this.$el.addClass( 'selected' ).attr( 'aria-checked', true );
+ // Make the checkbox tabable, except in media grid (bulk select mode).
+ if ( ! ( controller.isModeActive( 'grid' ) && controller.isModeActive( 'select' ) ) ) {
+ this.$( '.check' ).attr( 'tabindex', '0' );
+ }
},
/**
* @param {Backbone.Model} model
if ( ! selection || ( collection && collection !== selection ) ) {
return;
}
- this.$el.removeClass('selected');
+ this.$el.removeClass( 'selected' ).attr( 'aria-checked', false )
+ .find( '.check' ).attr( 'tabindex', '-1' );
},
/**
* @param {Backbone.Model} model
this.collection.remove( this.model );
},
+
/**
- * @param {Object} event
+ * 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]
*/
- removeFromSelection: function( event ) {
+ checkClickHandler: function ( event ) {
var selection = this.options.selection;
if ( ! selection ) {
return;
}
-
- // Stop propagation so the model isn't selected.
event.stopPropagation();
-
- selection.remove( this.model );
+ if ( selection.where( { id: this.model.get( 'id' ) } ).length ) {
+ selection.remove( this.model );
+ // Move focus back to the attachment tile (from the check).
+ this.$el.focus();
+ } else {
+ selection.add( this.model );
+ }
}
});
// Ensure settings remain in sync between attachment views.
_.each({
caption: '_syncCaption',
- title: '_syncTitle'
+ title: '_syncTitle',
+ artist: '_syncArtist',
+ album: '_syncAlbum'
}, function( method, setting ) {
/**
* @param {Backbone.Model} model
tagName: 'ul',
className: 'attachments',
- cssTemplate: media.template('attachments-css'),
-
- events: {
- 'scroll': 'scroll'
+ attributes: {
+ tabIndex: -1
},
initialize: function() {
this.el.id = _.uniqueId('__attachments-view-');
_.defaults( this.options, {
- refreshSensitivity: 200,
+ refreshSensitivity: isTouchDevice ? 300 : 200,
refreshThreshold: 3,
AttachmentView: media.view.Attachment,
sortable: false,
- resize: true
+ resize: true,
+ idealColumnWidth: $( window ).width() < 640 ? 135 : 150
});
this._viewsByCid = {};
+ this.$window = $( window );
+ this.resizeEvent = 'resize.media-modal-columns';
this.collection.on( 'add', function( attachment ) {
this.views.add( this.createAttachmentView( attachment ), {
this.collection.on( 'reset', this.render, this );
- // Throttle the scroll handler.
+ this.listenTo( this.controller, 'library:selection:add', this.attachmentFocus );
+
+ // Throttle the scroll handler and bind this.
this.scroll = _.chain( this.scroll ).bind( this ).throttle( this.options.refreshSensitivity ).value();
+ this.options.scrollElement = this.options.scrollElement || this.el;
+ $( this.options.scrollElement ).on( 'scroll', this.scroll );
+
this.initSortable();
- _.bindAll( this, 'css' );
- this.model.on( 'change:edge change:gutter', this.css, this );
- this._resizeCss = _.debounce( _.bind( this.css, this ), this.refreshSensitivity );
+ _.bindAll( this, 'setColumns' );
+
if ( this.options.resize ) {
- $(window).on( 'resize.attachments', this._resizeCss );
+ this.on( 'ready', this.bindEvents );
+ this.controller.on( 'open', this.setColumns );
+
+ // Call this.setColumns() after this view has been rendered in the DOM so
+ // attachments get proper width applied.
+ _.defer( this.setColumns, this );
+ }
+ },
+
+ bindEvents: function() {
+ this.$window.off( this.resizeEvent ).on( this.resizeEvent, _.debounce( this.setColumns, 50 ) );
+ },
+
+ attachmentFocus: function() {
+ this.$( 'li:first' ).focus();
+ },
+
+ restoreFocus: function() {
+ this.$( 'li.selected:first' ).focus();
+ },
+
+ arrowEvent: function( event ) {
+ var attachments = this.$el.children( 'li' ),
+ perRow = this.columns,
+ index = attachments.filter( ':focus' ).index(),
+ row = ( index + 1 ) <= perRow ? 1 : Math.ceil( ( index + 1 ) / perRow );
+
+ if ( index === -1 ) {
+ return;
+ }
+
+ // Left arrow
+ if ( 37 === event.keyCode ) {
+ if ( 0 === index ) {
+ return;
+ }
+ attachments.eq( index - 1 ).focus();
+ }
+
+ // Up arrow
+ if ( 38 === event.keyCode ) {
+ if ( 1 === row ) {
+ return;
+ }
+ attachments.eq( index - perRow ).focus();
+ }
+
+ // Right arrow
+ if ( 39 === event.keyCode ) {
+ if ( attachments.length === index ) {
+ return;
+ }
+ attachments.eq( index + 1 ).focus();
+ }
+
+ // Down arrow
+ if ( 40 === event.keyCode ) {
+ if ( Math.ceil( attachments.length / perRow ) === row ) {
+ return;
+ }
+ attachments.eq( index + perRow ).focus();
}
- this.css();
},
dispose: function() {
this.collection.props.off( null, null, this );
- $(window).off( 'resize.attachments', this._resizeCss );
+ if ( this.options.resize ) {
+ this.$window.off( this.resizeEvent );
+ }
+
/**
* call 'dispose' directly on the parent class
*/
media.View.prototype.dispose.apply( this, arguments );
},
- css: function() {
- var $css = $( '#' + this.el.id + '-css' );
+ setColumns: function() {
+ var prev = this.columns,
+ width = this.$el.width();
- if ( $css.length ) {
- $css.remove();
- }
+ if ( width ) {
+ this.columns = Math.min( Math.round( width / this.options.idealColumnWidth ), 12 ) || 1;
- media.view.Attachments.$head().append( this.cssTemplate({
- id: this.el.id,
- edge: this.edge(),
- gutter: this.model.get('gutter')
- }) );
- },
- /**
- * @returns {Number}
- */
- edge: function() {
- var edge = this.model.get('edge'),
- gutter, width, columns;
-
- if ( ! this.$el.is(':visible') ) {
- return edge;
+ if ( ! prev || prev !== this.columns ) {
+ this.$el.closest( '.media-frame-content' ).attr( 'data-columns', this.columns );
+ }
}
-
- gutter = this.model.get('gutter') * 2;
- width = this.$el.width() - gutter;
- columns = Math.ceil( width / ( edge + gutter ) );
- edge = Math.floor( ( width - ( columns * gutter ) ) / columns );
- return edge;
},
initSortable: function() {
var collection = this.collection;
- if ( ! this.options.sortable || ! $.fn.sortable ) {
+ if ( isTouchDevice || ! this.options.sortable || ! $.fn.sortable ) {
return;
}
},
refreshSortable: function() {
- if ( ! this.options.sortable || ! $.fn.sortable ) {
+ if ( isTouchDevice || ! this.options.sortable || ! $.fn.sortable ) {
return;
}
*/
createAttachmentView: function( attachment ) {
var view = new this.options.AttachmentView({
- controller: this.controller,
- model: attachment,
- collection: this.collection,
- selection: this.options.selection
+ controller: this.controller,
+ model: attachment,
+ collection: this.collection,
+ selection: this.options.selection
});
return this._viewsByCid[ attachment.cid ] = view;
scroll: function() {
var view = this,
+ el = this.options.scrollElement,
+ scrollTop = el.scrollTop,
toolbar;
- if ( ! this.$el.is(':visible') || ! this.collection.hasMore() ) {
+ // The scroll event occurs on the document, but the element
+ // that should be checked is the document body.
+ if ( el == document ) {
+ el = document.body;
+ scrollTop = $(document).scrollTop();
+ }
+
+ if ( ! $(el).is(':visible') || ! this.collection.hasMore() ) {
return;
}
toolbar = this.views.parent.toolbar;
// Show the spinner only if we are close to the bottom.
- if ( this.el.scrollHeight - ( this.el.scrollTop + this.el.clientHeight ) < this.el.clientHeight / 3 ) {
+ if ( el.scrollHeight - ( scrollTop + el.clientHeight ) < el.clientHeight / 3 ) {
toolbar.get('spinner').show();
}
- if ( this.el.scrollHeight < this.el.scrollTop + ( this.el.clientHeight * this.options.refreshThreshold ) ) {
+ if ( el.scrollHeight < scrollTop + ( el.clientHeight * this.options.refreshThreshold ) ) {
this.collection.more().done(function() {
view.scroll();
toolbar.get('spinner').hide();
});
}
}
- }, {
- $head: (function() {
- var $head;
- return function() {
- return $head = $head || $('head');
- };
- }())
});
/**
media.view.Search = media.View.extend({
tagName: 'input',
className: 'search',
+ id: 'media-search-input',
attributes: {
type: 'search',
media.view.AttachmentFilters = media.View.extend({
tagName: 'select',
className: 'attachment-filters',
+ id: 'media-attachment-filters',
events: {
change: 'change'
this.select();
},
+ /**
+ * @abstract
+ */
createFilters: function() {
this.filters = {};
},
+ /**
+ * When the selection changes, set the Query properties
+ * accordingly for the selected filter.
+ */
change: function() {
var filter = this.filters[ this.el.value ];
-
if ( filter ) {
this.model.set( filter.props );
}
filters[ key ] = {
text: text,
props: {
+ status: null,
type: key,
uploadedTo: null,
orderby: 'date',
filters.all = {
text: l10n.allMediaItems,
props: {
+ status: null,
type: null,
uploadedTo: null,
orderby: 'date',
priority: 10
};
- filters.uploaded = {
- text: l10n.uploadedToThisPost,
+ if ( media.view.settings.post.id ) {
+ filters.uploaded = {
+ text: l10n.uploadedToThisPost,
+ props: {
+ status: null,
+ type: null,
+ uploadedTo: media.view.settings.post.id,
+ orderby: 'menuOrder',
+ order: 'ASC'
+ },
+ priority: 20
+ };
+ }
+
+ filters.unattached = {
+ text: l10n.unattached,
props: {
- type: null,
- uploadedTo: media.view.settings.post.id,
- orderby: 'menuOrder',
- order: 'ASC'
+ status: null,
+ uploadedTo: 0,
+ type: null,
+ orderby: 'menuOrder',
+ order: 'ASC'
},
- priority: 20
+ priority: 50
};
+ if ( media.view.settings.mediaTrash &&
+ this.controller.isModeActive( 'grid' ) ) {
+
+ filters.trash = {
+ text: l10n.trash,
+ props: {
+ uploadedTo: null,
+ status: 'trash',
+ type: null,
+ orderby: 'date',
+ order: 'DESC'
+ },
+ priority: 50
+ };
+ }
+
this.filters = filters;
}
});
-
/**
* wp.media.view.AttachmentsBrowser
*
filters: false,
search: true,
display: false,
-
+ sidebar: true,
AttachmentView: media.view.Attachment.Library
});
+ this.listenTo( this.controller, 'toggle:upload:attachment', _.bind( this.toggleUploader, this ) );
+
this.createToolbar();
+ if ( this.options.sidebar ) {
+ this.createSidebar();
+ }
+ this.createUploader();
+ this.createAttachments();
this.updateContent();
- this.createSidebar();
+
+ if ( ! this.options.sidebar || 'errors' === this.options.sidebar ) {
+ this.$el.addClass( 'hide-sidebar' );
+
+ if ( 'errors' === this.options.sidebar ) {
+ this.$el.addClass( 'sidebar-for-errors' );
+ }
+ }
this.collection.on( 'add remove reset', this.updateContent, this );
},
},
createToolbar: function() {
- var filters, FiltersConstructor;
+ var LibraryViewSwitcher, Filters, toolbarOptions;
- /**
- * @member {wp.media.view.Toolbar}
- */
- this.toolbar = new media.view.Toolbar({
+ toolbarOptions = {
controller: this.controller
- });
+ };
+
+ if ( this.controller.isModeActive( 'grid' ) ) {
+ toolbarOptions.className = 'media-toolbar wp-filter';
+ }
+
+ /**
+ * @member {wp.media.view.Toolbar}
+ */
+ this.toolbar = new media.view.Toolbar( toolbarOptions );
this.views.add( this.toolbar );
- filters = this.options.filters;
- if ( 'uploaded' === filters ) {
- FiltersConstructor = media.view.AttachmentFilters.Uploaded;
- } else if ( 'all' === filters ) {
- FiltersConstructor = media.view.AttachmentFilters.All;
+ this.toolbar.set( 'spinner', new media.view.Spinner({
+ priority: -60
+ }) );
+
+ if ( -1 !== $.inArray( this.options.filters, [ 'uploaded', 'all' ] ) ) {
+ // "Filters" will return a <select>, need to render
+ // screen reader text before
+ this.toolbar.set( 'filtersLabel', new media.view.Label({
+ value: l10n.filterByType,
+ attributes: {
+ 'for': 'media-attachment-filters'
+ },
+ priority: -80
+ }).render() );
+
+ if ( 'uploaded' === this.options.filters ) {
+ this.toolbar.set( 'filters', new media.view.AttachmentFilters.Uploaded({
+ controller: this.controller,
+ model: this.collection.props,
+ priority: -80
+ }).render() );
+ } else {
+ Filters = new media.view.AttachmentFilters.All({
+ controller: this.controller,
+ model: this.collection.props,
+ priority: -80
+ });
+
+ this.toolbar.set( 'filters', Filters.render() );
+ }
}
- if ( FiltersConstructor ) {
- this.toolbar.set( 'filters', new FiltersConstructor({
+ // 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 = media.View.extend({
+ className: 'view-switch media-grid-view-switch',
+ template: media.template( 'media-library-view-switcher')
+ });
+
+ this.toolbar.set( 'libraryViewSwitcher', new LibraryViewSwitcher({
+ controller: this.controller,
+ priority: -90
+ }).render() );
+
+ // DateFilter is a <select>, screen reader text needs to be rendered before
+ this.toolbar.set( 'dateFilterLabel', new media.view.Label({
+ value: l10n.filterByDate,
+ attributes: {
+ 'for': 'media-attachment-date-filters'
+ },
+ priority: -75
+ }).render() );
+ this.toolbar.set( 'dateFilter', new media.view.DateFilter({
controller: this.controller,
model: this.collection.props,
- priority: -80
+ priority: -75
}).render() );
- }
- this.toolbar.set( 'spinner', new media.view.Spinner({
- priority: -70
- }) );
+ // BulkSelection is a <div> with subviews, including screen reader text
+ this.toolbar.set( 'selectModeToggleButton', new media.view.SelectModeToggleButton({
+ text: l10n.bulkSelect,
+ controller: this.controller,
+ priority: -70
+ }).render() );
+
+ this.toolbar.set( 'deleteSelectedButton', new media.view.DeleteSelectedButton({
+ filters: Filters,
+ style: 'primary',
+ disabled: true,
+ text: media.view.settings.mediaTrash ? l10n.trashSelected : l10n.deleteSelected,
+ controller: this.controller,
+ priority: -60,
+ click: function() {
+ var model, changed = [], self = this,
+ selection = this.controller.state().get( 'selection' ),
+ library = this.controller.state().get( 'library' );
+
+ if ( ! selection.length ) {
+ return;
+ }
+
+ if ( ! media.view.settings.mediaTrash && ! confirm( l10n.warnBulkDelete ) ) {
+ return;
+ }
+
+ if ( media.view.settings.mediaTrash &&
+ 'trash' !== selection.at( 0 ).get( 'status' ) &&
+ ! confirm( l10n.warnBulkTrash ) ) {
+
+ return;
+ }
+
+ while ( selection.length > 0 ) {
+ model = selection.at( 0 );
+ if ( media.view.settings.mediaTrash && 'trash' === model.get( 'status' ) ) {
+ model.set( 'status', 'inherit' );
+ changed.push( model.save() );
+ selection.remove( model );
+ } else if ( media.view.settings.mediaTrash ) {
+ model.set( 'status', 'trash' );
+ changed.push( model.save() );
+ selection.remove( model );
+ } else {
+ model.destroy();
+ }
+ }
+
+ if ( changed.length ) {
+ $.when.apply( null, changed ).then( function() {
+ library._requery( true );
+ self.controller.trigger( 'selection:action:done' );
+ } );
+ } else {
+ this.controller.trigger( 'selection:action:done' );
+ }
+ }
+ }).render() );
+ }
if ( this.options.search ) {
+ // Search is an input, screen reader text needs to be rendered before
+ this.toolbar.set( 'searchLabel', new media.view.Label({
+ value: l10n.searchMediaLabel,
+ attributes: {
+ 'for': 'media-search-input'
+ },
+ priority: 60
+ }).render() );
this.toolbar.set( 'search', new media.view.Search({
controller: this.controller,
model: this.collection.props,
},
updateContent: function() {
- var view = this;
+ var view = this,
+ noItemsView;
- if( ! this.attachments ) {
- this.createAttachments();
+ if ( this.controller.isModeActive( 'grid' ) ) {
+ noItemsView = view.attachmentsNoResults;
+ } else {
+ noItemsView = view.uploader;
}
if ( ! this.collection.length ) {
this.toolbar.get( 'spinner' ).show();
- this.collection.more().done(function() {
+ this.dfd = this.collection.more().done( function() {
if ( ! view.collection.length ) {
- view.createUploader();
+ noItemsView.$el.removeClass( 'hidden' );
+ } else {
+ noItemsView.$el.addClass( 'hidden' );
}
view.toolbar.get( 'spinner' ).hide();
- });
+ } );
} else {
+ noItemsView.$el.addClass( 'hidden' );
view.toolbar.get( 'spinner' ).hide();
}
},
- removeContent: function() {
- _.each(['attachments','uploader'], function( key ) {
- if ( this[ key ] ) {
- this[ key ].remove();
- delete this[ key ];
- }
- }, this );
- },
-
createUploader: function() {
- this.removeContent();
-
this.uploader = new media.view.UploaderInline({
controller: this.controller,
status: false,
- message: l10n.noItemsFound
+ message: this.controller.isModeActive( 'grid' ) ? '' : l10n.noItemsFound,
+ canClose: this.controller.isModeActive( 'grid' )
});
+ this.uploader.hide();
this.views.add( this.uploader );
},
- createAttachments: function() {
- this.removeContent();
+ toggleUploader: function() {
+ if ( this.uploader.$el.hasClass( 'hidden' ) ) {
+ this.uploader.show();
+ } else {
+ this.uploader.hide();
+ }
+ },
+ createAttachments: function() {
this.attachments = new media.view.Attachments({
- controller: this.controller,
- collection: this.collection,
- selection: this.options.selection,
- model: this.model,
- sortable: this.options.sortable,
+ controller: this.controller,
+ collection: this.collection,
+ selection: this.options.selection,
+ model: this.model,
+ sortable: this.options.sortable,
+ scrollElement: this.options.scrollElement,
+ idealColumnWidth: this.options.idealColumnWidth,
// The single `Attachment` view to be used in the `Attachments` view.
AttachmentView: this.options.AttachmentView
});
+ // Add keydown listener to the instance of the Attachments view
+ this.attachments.listenTo( this.controller, 'attachment:keydown:arrow', this.attachments.arrowEvent );
+ this.attachments.listenTo( this.controller, 'attachment:details:shift-tab', this.attachments.restoreFocus );
+
this.views.add( this.attachments );
+
+
+ if ( this.controller.isModeActive( 'grid' ) ) {
+ this.attachmentsNoResults = new media.View({
+ controller: this.controller,
+ tagName: 'p'
+ });
+
+ this.attachmentsNoResults.$el.addClass( 'hidden no-media' );
+ this.attachmentsNoResults.$el.html( l10n.noMedia );
+
+ this.views.add( this.attachmentsNoResults );
+ }
},
createSidebar: function() {
userSettings: this.model.get('displayUserSettings')
}) );
}
+
+ // Show the sidebar on mobile
+ if ( this.model.id === 'insert' ) {
+ sidebar.$el.addClass( 'visible' );
+ }
},
disposeSingle: function() {
sidebar.unset('details');
sidebar.unset('compat');
sidebar.unset('display');
+ // Hide the sidebar on mobile
+ sidebar.$el.removeClass( 'visible' );
}
});
controller: this.controller,
collection: this.collection,
selection: this.collection,
- model: new Backbone.Model({
- edge: 40,
- gutter: 5
- })
+ model: new Backbone.Model()
});
this.views.set( '.selection-view', this.attachments );
clear: function( event ) {
event.preventDefault();
this.collection.reset();
+
+ // Keep focus inside media modal
+ // after clear link is selected
+ this.controller.modal.focusManager.focus();
}
});
}
// Handle checkboxes.
} else if ( $setting.is('input[type="checkbox"]') ) {
- $setting.prop( 'checked', !! value );
+ $setting.prop( 'checked', !! value && 'false' !== value );
}
},
/**
$input.removeClass( 'hidden' );
// If the input is visible, focus and select its contents.
- if ( $input.is(':visible') ) {
+ if ( ! isTouchDevice && $input.is(':visible') ) {
$input.focus()[0].select();
}
}
'change [data-setting] textarea': 'updateSetting',
'click .delete-attachment': 'deleteAttachment',
'click .trash-attachment': 'trashAttachment',
+ 'click .untrash-attachment': 'untrashAttachment',
'click .edit-attachment': 'editAttachment',
- 'click .refresh-attachment': 'refreshAttachment'
+ 'click .refresh-attachment': 'refreshAttachment',
+ 'keydown': 'toggleSelectionHandler'
},
initialize: function() {
- /**
- * @member {wp.media.view.FocusManager}
- */
- this.focusManager = new media.view.FocusManager({
- el: this.el
+ this.options = _.defaults( this.options, {
+ rerenderOnModelChange: false
});
+
+ this.on( 'ready', this.initialFocus );
/**
* call 'initialize' directly on the parent class
*/
media.view.Attachment.prototype.initialize.apply( this, arguments );
},
- /**
- * @returns {wp.media.view..Attachment.Details} Returns itself to allow chaining
- */
- render: function() {
- /**
- * call 'render' directly on the parent class
- */
- media.view.Attachment.prototype.render.apply( this, arguments );
- this.focusManager.focus();
- return this;
+
+ initialFocus: function() {
+ if ( ! isTouchDevice ) {
+ this.$( ':input' ).eq( 0 ).focus();
+ }
},
/**
* @param {Object} event
if ( confirm( l10n.warnDelete ) ) {
this.model.destroy();
+ // Keep focus inside media modal
+ // after image is deleted
+ this.controller.modal.focusManager.focus();
}
},
/**
* @param {Object} event
*/
trashAttachment: function( event ) {
+ var library = this.controller.library;
event.preventDefault();
- this.model.destroy();
+ if ( media.view.settings.mediaTrash &&
+ 'edit-metadata' === this.controller.content.mode() ) {
+
+ this.model.set( 'status', 'trash' );
+ this.model.save().done( function() {
+ library._requery( true );
+ } );
+ } else {
+ this.model.destroy();
+ }
+ },
+ /**
+ * @param {Object} event
+ */
+ untrashAttachment: function( event ) {
+ var library = this.controller.library;
+ event.preventDefault();
+
+ this.model.set( 'status', 'inherit' );
+ this.model.save().done( function() {
+ library._requery( true );
+ } );
},
/**
* @param {Object} event
this.$el.removeClass('needs-refresh');
event.preventDefault();
this.model.fetch();
- }
+ },
+ /**
+ * 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
+ */
+ 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;
+ }
+ }
});
/**
* wp.media.view.AttachmentCompat
*
+ * A view to display fields added via the `attachment_fields_to_edit` filter.
+ *
* @constructor
* @augments wp.media.View
* @augments wp.Backbone.View
},
initialize: function() {
- /**
- * @member {wp.media.view.FocusManager}
- */
- this.focusManager = new media.view.FocusManager({
- el: this.el
- });
-
this.model.on( 'change:compat', this.render, this );
},
/**
this.views.detach();
this.$el.html( compat.item );
this.views.render();
-
- this.focusManager.focus();
return this;
},
/**
}
});
+ /**
+ * @constructor
+ * @augments wp.media.View
+ * @augments wp.Backbone.View
+ * @augments Backbone.View
+ */
+ media.view.Label = media.View.extend({
+ tagName: 'label',
+ className: 'screen-reader-text',
+
+ initialize: function() {
+ this.value = this.options.value;
+ },
+
+ render: function() {
+ this.$el.html( this.value );
+
+ return this;
+ }
+ });
+
/**
* wp.media.view.EmbedUrl
*
},
initialize: function() {
- this.$input = $('<input/>').attr( 'type', 'text' ).val( this.model.get('url') );
+ var self = this;
+
+ this.$input = $('<input id="embed-url-field" type="url" />').val( this.model.get('url') );
this.input = this.$input[0];
this.spinner = $('<span class="spinner" />')[0];
this.$el.append([ this.input, this.spinner ]);
this.model.on( 'change:url', this.render, this );
+
+ if ( this.model.get( 'url' ) ) {
+ _.delay( function () {
+ self.model.trigger( 'change:url' );
+ }, 500 );
+ }
},
/**
* @returns {wp.media.view.EmbedUrl} Returns itself to allow chaining
},
ready: function() {
- this.focus();
+ if ( ! isTouchDevice ) {
+ this.focus();
+ }
},
url: function( event ) {
*/
media.view.EmbedLink = media.view.Settings.extend({
className: 'embed-link-settings',
- template: media.template('embed-link-settings')
+ template: media.template('embed-link-settings'),
+
+ initialize: function() {
+ this.spinner = $('<span class="spinner" />');
+ this.$el.append( this.spinner[0] );
+ this.listenTo( this.model, 'change:url', this.updateoEmbed );
+ },
+
+ updateoEmbed: function() {
+ var url = this.model.get( 'url' );
+
+ this.$('.setting.title').show();
+ // clear out previous results
+ this.$('.embed-container').hide().find('.embed-preview').html('');
+
+ // only proceed with embed if the field contains more than 6 characters
+ if ( url && url.length < 6 ) {
+ return;
+ }
+
+ this.spinner.show();
+
+ setTimeout( _.bind( this.fetch, this ), 500 );
+ },
+
+ fetch: function() {
+ // check if they haven't typed in 500 ms
+ if ( $('#embed-url-field').val() !== this.model.get('url') ) {
+ return;
+ }
+
+ wp.ajax.send( 'parse-embed', {
+ data : {
+ post_ID: media.view.settings.post.id,
+ shortcode: '[embed]' + this.model.get('url') + '[/embed]'
+ }
+ } ).done( _.bind( this.renderoEmbed, this ) );
+ },
+
+ renderoEmbed: function( response ) {
+ var html = ( response && response.body ) || '';
+
+ this.spinner.hide();
+
+ this.$('.setting.title').hide();
+ this.$('.embed-container').show().find('.embed-preview').html( html );
+ }
});
/**
* wp.media.view.EmbedImage
*
- * @contructor
+ * @constructor
* @augments wp.media.view.Settings.AttachmentDisplay
* @augments wp.media.view.Settings
* @augments wp.media.View
/**
* wp.media.view.ImageDetails
*
- * @contructor
+ * @constructor
* @augments wp.media.view.Settings.AttachmentDisplay
* @augments wp.media.view.Settings
* @augments wp.media.View
value = Math.round( this.model.get( 'aspectRatio' ) * num );
this.model.set( 'customWidth', value, { silent: true } );
this.$( '[data-setting="customWidth"]' ).val( value );
-
}
},
},
loadEditor: function() {
- this.editor.open( this.model.get('id'), this.model.get('nonces').edit, this );
+ 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();
},
back: function() {