/* 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;
return deferred.promise();
};
- /**
- * ========================================================================
- * CONTROLLERS
- * ========================================================================
- */
-
/**
* wp.media.controller.Region
*
- * @constructor
- * @augments Backbone.Model
+ * A region is a persistent application layout area.
*
- * @param {Object} [options={}]
+ * A region assumes one mode at any time, and can be switched to another.
+ *
+ * When mode changes, events are triggered on the region's parent view.
+ * The parent view will listen to specific events and fill the region with an
+ * appropriate view depending on mode. For example, a frame listens for the
+ * 'browse' mode t be activated on the 'content' view and then fills the region
+ * with an AttachmentsBrowser view.
+ *
+ * @class
+ *
+ * @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.
*/
media.controller.Region = function( options ) {
_.extend( this, _.pick( options || {}, 'id', 'view', 'selector' ) );
_.extend( media.controller.Region.prototype, {
/**
- * Switch modes
+ * Activate a mode.
+ *
+ * @since 3.5.0
*
* @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
+ * Render a mode.
*
- * If no mode is provided, just re-render the current mode.
- * If the provided mode isn't active, perform a full switch.
+ * @since 3.5.0
*
* @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.
+ *
+ * @since 3.5.0
+ *
+ * @returns {wp.media.View}
*/
get: function() {
return this.view.views.first( this.selector );
},
/**
+ * Set the region's view as a subview of the frame.
+ *
+ * @since 3.5.0
+ *
* @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.
+ *
+ * @since 3.5.0
*
* @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;
/**
* wp.media.controller.StateMachine
*
- * @constructor
+ * A state machine keeps track of state. It is in one state at a time,
+ * and can change from one state to another.
+ *
+ * States are stored as models in a Backbone collection.
+ *
+ * @since 3.5.0
+ *
+ * @class
* @augments Backbone.Model
* @mixin
* @mixes Backbone.Events
* @param {Array} states
*/
media.controller.StateMachine = function( states ) {
+ // @todo This is dead code. The states collection gets created in media.view.Frame._createStates.
this.states = new Backbone.Collection( states );
};
// 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.
* Ensure that the `states` collection exists so the `StateMachine`
* 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
* created the `states` collection, or are trying to select a state
* that does not exist.
*
+ * @since 3.5.0
+ *
* @param {string} id
*
* @fires wp.media.controller.State#deactivate
* Call the `state()` method with no parameters to retrieve the current
* active state.
*
+ * @since 3.5.0
+ *
* @returns {wp.media.controller.State} Returns a State model
* from the StateMachine collection
*/
}
});
- // 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.
+ *
+ * A state has an event-driven lifecycle:
*
- * @constructor
+ * 'ready' triggers when a state is added to a state machine's collection.
+ * 'activate' triggers when a state is activated by a state machine.
+ * 'deactivate' triggers when a state is deactivated by a state machine.
+ * 'reset' is not triggered automatically. It should be invoked by the
+ * proper controller to reset the state to its default.
+ *
+ * @class
* @augments Backbone.Model
*/
media.controller.State = Backbone.Model.extend({
+ /**
+ * Constructor.
+ *
+ * @since 3.5.0
+ */
constructor: function() {
this.on( 'activate', this._preActivate, this );
this.on( 'activate', this.activate, this );
Backbone.Model.apply( this, arguments );
this.on( 'change:menu', this._updateMenu, this );
},
-
/**
+ * Ready event callback.
+ *
* @abstract
+ * @since 3.5.0
*/
ready: function() {},
+
/**
+ * Activate event callback.
+ *
* @abstract
+ * @since 3.5.0
*/
activate: function() {},
+
/**
+ * Deactivate event callback.
+ *
* @abstract
+ * @since 3.5.0
*/
deactivate: function() {},
+
/**
+ * Reset event callback.
+ *
* @abstract
+ * @since 3.5.0
*/
reset: function() {},
+
/**
* @access private
+ * @since 3.5.0
*/
_ready: function() {
this._updateMenu();
},
+
/**
* @access private
- */
+ * @since 3.5.0
+ */
_preActivate: function() {
this.active = true;
},
+
/**
* @access private
+ * @since 3.5.0
*/
_postActivate: function() {
this.on( 'change:menu', this._menu, this );
this._content();
this._router();
},
+
/**
* @access private
+ * @since 3.5.0
*/
_deactivate: function() {
this.active = false;
this.off( 'change:content', this._content, this );
this.off( 'change:toolbar', this._toolbar, this );
},
+
/**
* @access private
+ * @since 3.5.0
*/
_title: function() {
this.frame.title.render( this.get('titleMode') || 'default' );
},
+
/**
* @access private
+ * @since 3.5.0
*/
_renderTitle: function( view ) {
view.$el.text( this.get('title') || '' );
},
+
/**
* @access private
+ * @since 3.5.0
*/
_router: function() {
var router = this.frame.router,
view.select( this.frame.content.mode() );
}
},
+
/**
* @access private
+ * @since 3.5.0
*/
_menu: function() {
var menu = this.frame.menu,
mode = this.get('menu'),
view;
+ this.frame.$el.toggleClass( 'hide-menu', ! mode );
if ( ! mode ) {
return;
}
view.select( this.id );
}
},
+
/**
* @access private
+ * @since 3.5.0
*/
_updateMenu: function() {
var previous = this.previous('menu'),
this.frame.on( 'menu:render:' + menu, this._renderMenu, this );
}
},
+
/**
+ * Create a view in the media menu for the state.
+ *
* @access private
+ * @since 3.5.0
+ *
+ * @param {media.view.Menu} view The menu view.
*/
_renderMenu: function( view ) {
var menuItem = this.get('menuItem'),
};
});
+ /**
+ * wp.media.selectionSync
+ *
+ * Sync an attachments selection in a state with another state.
+ *
+ * Allows for selecting multiple images in the Insert Media workflow, and then
+ * switching to the Insert Gallery workflow while preserving the attachments selection.
+ *
+ * @mixin
+ */
media.selectionSync = {
+ /**
+ * @since 3.5.0
+ */
syncSelection: function() {
var selection = this.get('selection'),
manager = this.frame._selection;
* of the selection's attachments and the set of selected
* attachments that this specific selection considered invalid.
* Reset the difference and record the single attachment.
+ *
+ * @since 3.5.0
*/
recordSelection: function() {
var selection = this.get('selection'),
/**
* wp.media.controller.Library
*
- * @constructor
+ * A state for choosing an attachment or group of attachments from the media library.
+ *
+ * @class
* @augments wp.media.controller.State
* @augments Backbone.Model
+ * @mixes media.selectionSync
+ *
+ * @param {object} [attributes] The attributes hash passed to the state.
+ * @param {string} [attributes.id=library] Unique identifier.
+ * @param {string} [attributes.title=Media library] Title for the state. Displays in the media menu and the frame's title region.
+ * @param {wp.media.model.Attachments} [attributes.library] The attachments collection to browse.
+ * If one is not supplied, a collection of all attachments will be created.
+ * @param {wp.media.model.Selection|object} [attributes.selection] A collection to contain attachment selections within the state.
+ * If the 'selection' attribute is a plain JS object,
+ * a Selection will be created using its values as the selection instance's `props` model.
+ * Otherwise, it will copy the library's `props` model.
+ * @param {boolean} [attributes.multiple=false] Whether multi-select is enabled.
+ * @param {string} [attributes.content=upload] Initial mode for the content region.
+ * Overridden by persistent user setting if 'contentUserSetting' is true.
+ * @param {string} [attributes.menu=default] Initial mode for the menu region.
+ * @param {string} [attributes.router=browse] Initial mode for the router region.
+ * @param {string} [attributes.toolbar=select] Initial mode for the toolbar region.
+ * @param {boolean} [attributes.searchable=true] Whether the library is searchable.
+ * @param {boolean|string} [attributes.filterable=false] Whether the library is filterable, and if so what filters should be shown.
+ * Accepts 'all', 'uploaded', or 'unattached'.
+ * @param {boolean} [attributes.sortable=true] Whether the Attachments should be sortable. Depends on the orderby property being set to menuOrder on the attachments collection.
+ * @param {boolean} [attributes.autoSelect=true] Whether an uploaded attachment should be automatically added to the selection.
+ * @param {boolean} [attributes.describe=false] Whether to offer UI to describe attachments - e.g. captioning images in a gallery.
+ * @param {boolean} [attributes.contentUserSetting=true] Whether the content region's mode should be set and persisted per user.
+ * @param {boolean} [attributes.syncSelection=true] Whether the Attachments selection should be persisted from the last 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,
-
- // Uses a user setting to override the content mode.
+ id: 'library',
+ title: l10n.mediaLibraryTitle,
+ multiple: false,
+ content: 'upload',
+ menu: 'default',
+ router: 'browse',
+ toolbar: 'select',
+ searchable: true,
+ filterable: false,
+ sortable: true,
+ autoSelect: true,
+ describe: false,
contentUserSetting: true,
-
- // Sync the selection from the last state when 'multiple' matches.
- syncSelection: true
+ syncSelection: true
},
/**
* If a library isn't provided, query all media items.
* If a selection instance isn't provided, create one.
+ *
+ * @since 3.5.0
*/
initialize: function() {
var selection = this.get('selection'),
props = _.omit( props, 'orderby', 'query' );
}
- // If the `selection` attribute is set to an object,
- // it will use those values as the selection instance's
- // `props` model. Otherwise, it will copy the library's
- // `props` model.
this.set( 'selection', new media.model.Selection( null, {
multiple: this.get('multiple'),
props: props
}) );
}
- if ( ! this.get('edge') ) {
- this.set( 'edge', 120 );
- }
-
- if ( ! this.get('gutter') ) {
- this.set( 'gutter', 8 );
- }
-
this.resetDisplays();
},
+ /**
+ * @since 3.5.0
+ */
activate: function() {
this.syncSelection();
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') ) );
}
},
+ /**
+ * @since 3.5.0
+ */
deactivate: function() {
this.recordSelection();
wp.Uploader.queue.off( null, null, this );
},
+ /**
+ * Reset the library to its initial state.
+ *
+ * @since 3.5.0
+ */
reset: function() {
this.get('selection').reset();
this.resetDisplays();
this.refreshContent();
},
+ /**
+ * Reset the attachment display settings defaults to the site options.
+ *
+ * If site options don't define them, fall back to a persistent user setting.
+ *
+ * @since 3.5.0
+ */
resetDisplays: function() {
var defaultProps = media.view.settings.defaultProps;
this._displays = [];
},
/**
+ * Create a model to represent display settings (alignment, etc.) for an attachment.
+ *
+ * @since 3.5.0
+ *
* @param {wp.media.model.Attachment} attachment
* @returns {Backbone.Model}
*/
},
/**
+ * Given an attachment, create attachment display settings properties.
+ *
+ * @since 3.6.0
+ *
* @param {wp.media.model.Attachment} attachment
* @returns {Object}
*/
},
/**
+ * Whether an attachment can be embedded (audio or video).
+ *
+ * @since 3.6.0
+ *
* @param {wp.media.model.Attachment} attachment
* @returns {Boolean}
*/
* If the state is active, no items are selected, and the current
* content mode is not an option in the state's router (provided
* the state has a router), reset the content mode to the default.
+ *
+ * @since 3.5.0
*/
refreshContent: function() {
var selection = this.get('selection'),
},
/**
- * If the uploader was selected, navigate to the browser.
+ * Callback handler when an attachment is uploaded.
+ *
+ * Switch to the Media Library if uploaded from the 'Upload Files' tab.
*
- * Automatically select any uploading attachments.
+ * Adds any uploading attachments to the selection.
*
- * Selections that don't support multiple attachments automatically
- * limit themselves to one attachment (in this case, the last
- * attachment in the upload queue).
+ * If the state only supports one attachment to be selected and multiple
+ * attachments are uploaded, the last attachment in the upload queue will
+ * be selected.
+ *
+ * @since 3.5.0
*
* @param {wp.media.model.Attachment} attachment
*/
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' );
+ }
},
/**
- * Only track the browse router on library states.
+ * Persist the mode of the content region as a user setting.
+ *
+ * @since 3.5.0
*/
saveContentMode: function() {
if ( 'browse' !== this.get('router') ) {
}
});
+ // Make selectionSync available on any Media Library state.
_.extend( media.controller.Library.prototype, media.selectionSync );
/**
* wp.media.controller.ImageDetails
*
- * @constructor
+ * A state for editing the attachment display settings of an image that's been
+ * inserted into the editor.
+ *
+ * @class
* @augments wp.media.controller.State
* @augments Backbone.Model
+ *
+ * @param {object} [attributes] The attributes hash passed to the state.
+ * @param {string} [attributes.id=image-details] Unique identifier.
+ * @param {string} [attributes.title=Image Details] Title for the state. Displays in the frame's title region.
+ * @param {wp.media.model.Attachment} attributes.image The image's model.
+ * @param {string|false} [attributes.content=image-details] Initial mode for the content region.
+ * @param {string|false} [attributes.menu=false] Initial mode for the menu region.
+ * @param {string|false} [attributes.router=false] Initial mode for the router region.
+ * @param {string|false} [attributes.toolbar=image-details] Initial mode for the toolbar region.
+ * @param {boolean} [attributes.editing=false] Unused.
+ * @param {int} [attributes.priority=60] Unused.
+ *
+ * @todo This state inherits some defaults from media.controller.Library.prototype.defaults,
+ * however this may not do anything.
*/
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,
+ content: 'image-details',
+ menu: false,
+ router: false,
+ toolbar: 'image-details',
+ editing: false,
+ priority: 60
}, media.controller.Library.prototype.defaults ),
+ /**
+ * @since 3.9.0
+ *
+ * @param options Attributes
+ */
initialize: function( options ) {
this.image = options.image;
media.controller.State.prototype.initialize.apply( this, arguments );
},
+ /**
+ * @since 3.9.0
+ */
activate: function() {
this.frame.modal.$el.addClass('image-details');
}
/**
* wp.media.controller.GalleryEdit
*
- * @constructor
+ * A state for editing a gallery's images and settings.
+ *
+ * @class
* @augments wp.media.controller.Library
* @augments wp.media.controller.State
* @augments Backbone.Model
+ *
+ * @param {object} [attributes] The attributes hash passed to the state.
+ * @param {string} [attributes.id=gallery-edit] Unique identifier.
+ * @param {string} [attributes.title=Edit Gallery] Title for the state. Displays in the frame's title region.
+ * @param {wp.media.model.Attachments} [attributes.library] The collection of attachments in the gallery.
+ * If one is not supplied, an empty media.model.Selection collection is created.
+ * @param {boolean} [attributes.multiple=false] Whether multi-select is enabled.
+ * @param {boolean} [attributes.searchable=false] Whether the library is searchable.
+ * @param {boolean} [attributes.sortable=true] Whether the Attachments should be sortable. Depends on the orderby property being set to menuOrder on the attachments collection.
+ * @param {string|false} [attributes.content=browse] Initial mode for the content region.
+ * @param {string|false} [attributes.toolbar=image-details] Initial mode for the toolbar region.
+ * @param {boolean} [attributes.describe=true] Whether to offer UI to describe attachments - e.g. captioning images in a gallery.
+ * @param {boolean} [attributes.displaySettings=true] Whether to show the attachment display settings interface.
+ * @param {boolean} [attributes.dragInfo=true] Whether to show instructional text about the attachments being sortable.
+ * @param {int} [attributes.idealColumnWidth=170] The ideal column width in pixels for attachments.
+ * @param {boolean} [attributes.editing=false] Whether the gallery is being created, or editing an existing instance.
+ * @param {int} [attributes.priority=60] 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 for this state, because the library passed in *is* the selection.
+ * @param {view} [attributes.AttachmentView] The single `Attachment` view to be used in the `Attachments`.
+ * If none supplied, defaults to wp.media.view.Attachment.EditLibrary.
*/
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,
-
- // Don't sync the selection, as the Edit Gallery library
- // *is* the selection.
- syncSelection: false
+ id: 'gallery-edit',
+ title: l10n.editGalleryTitle,
+ multiple: false,
+ searchable: false,
+ sortable: true,
+ display: false,
+ content: 'browse',
+ toolbar: 'gallery-edit',
+ describe: true,
+ displaySettings: true,
+ dragInfo: true,
+ idealColumnWidth: 170,
+ editing: false,
+ priority: 60,
+ syncSelection: false
},
+ /**
+ * @since 3.5.0
+ */
initialize: function() {
// If we haven't been provided a `library`, create a `Selection`.
if ( ! this.get('library') )
media.controller.Library.prototype.initialize.apply( this, arguments );
},
+ /**
+ * @since 3.5.0
+ */
activate: function() {
var library = this.get('library');
media.controller.Library.prototype.activate.apply( this, arguments );
},
+ /**
+ * @since 3.5.0
+ */
deactivate: function() {
// Stop watching for uploaded attachments.
this.get('library').unobserve( wp.Uploader.queue );
media.controller.Library.prototype.deactivate.apply( this, arguments );
},
+ /**
+ * @since 3.5.0
+ *
+ * @param browser
+ */
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 selecting more images to add to a gallery.
*
- * @constructor
+ * @class
* @augments wp.media.controller.Library
* @augments wp.media.controller.State
* @augments Backbone.Model
+ *
+ * @param {object} [attributes] The attributes hash passed to the state.
+ * @param {string} [attributes.id=gallery-library] Unique identifier.
+ * @param {string} [attributes.title=Add to Gallery] 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 all images will be created.
+ * @param {boolean|string} [attributes.filterable=uploaded] Whether the library is filterable, and if so what filters should be shown.
+ * Accepts 'all', 'uploaded', or 'unattached'.
+ * @param {string} [attributes.menu=gallery] Initial mode for the menu region.
+ * @param {string} [attributes.content=upload] Initial mode for the content region.
+ * Overridden by persistent user setting if 'contentUserSetting' is true.
+ * @param {string} [attributes.router=browse] Initial mode for the router region.
+ * @param {string} [attributes.toolbar=gallery-add] Initial mode for the toolbar region.
+ * @param {boolean} [attributes.searchable=true] Whether the library is searchable.
+ * @param {boolean} [attributes.sortable=true] Whether the Attachments should be sortable. Depends on the orderby property being set to menuOrder on the attachments collection.
+ * @param {boolean} [attributes.autoSelect=true] Whether an uploaded attachment should be automatically added to the selection.
+ * @param {boolean} [attributes.contentUserSetting=true] Whether the content region's mode should be set and persisted per user.
+ * @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.
*/
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,
-
- // Don't sync the selection, as the Edit Gallery library
- // *is* the selection.
+ id: 'gallery-library',
+ title: l10n.addToGalleryTitle,
+ multiple: 'add',
+ filterable: 'uploaded',
+ menu: 'gallery',
+ toolbar: 'gallery-add',
+ priority: 100,
syncSelection: false
}, media.controller.Library.prototype.defaults ),
+ /**
+ * @since 3.5.0
+ */
initialize: function() {
- // If we haven't been provided a `library`, create a `Selection`.
+ // If a library wasn't supplied, create a library of images.
if ( ! this.get('library') )
this.set( 'library', media.query({ type: 'image' }) );
media.controller.Library.prototype.initialize.apply( this, arguments );
},
+ /**
+ * @since 3.5.0
+ */
activate: function() {
var library = this.get('library'),
edit = this.frame.state('gallery-edit').get('library');
/**
* wp.media.controller.CollectionEdit
*
- * @constructor
+ * A state for editing a collection, which is used by audio and video playlists,
+ * and can be used for other collections.
+ *
+ * @class
* @augments wp.media.controller.Library
* @augments wp.media.controller.State
* @augments Backbone.Model
+ *
+ * @param {object} [attributes] The attributes hash passed to the state.
+ * @param {string} attributes.title Title for the state. Displays in the media menu and the frame's title region.
+ * @param {wp.media.model.Attachments} [attributes.library] The attachments collection to edit.
+ * If one is not supplied, an empty media.model.Selection collection is created.
+ * @param {boolean} [attributes.multiple=false] Whether multi-select is enabled.
+ * @param {string} [attributes.content=browse] Initial mode for the content region.
+ * @param {string} attributes.menu Initial mode for the menu region. @todo this needs a better explanation.
+ * @param {boolean} [attributes.searchable=false] Whether the library is searchable.
+ * @param {boolean} [attributes.sortable=true] Whether the Attachments should be sortable. Depends on the orderby property being set to menuOrder on the attachments collection.
+ * @param {boolean} [attributes.describe=true] Whether to offer UI to describe the attachments - e.g. captioning images in a gallery.
+ * @param {boolean} [attributes.dragInfo=true] Whether to show instructional text about the attachments being sortable.
+ * @param {boolean} [attributes.dragInfoText] Instructional text about the attachments being sortable.
+ * @param {int} [attributes.idealColumnWidth=170] The ideal column width in pixels for attachments.
+ * @param {boolean} [attributes.editing=false] Whether the gallery is being created, or editing an existing instance.
+ * @param {int} [attributes.priority=60] 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 for this state, because the library passed in *is* the selection.
+ * @param {view} [attributes.SettingsView] The view to edit the collection instance settings (e.g. Playlist settings with "Show tracklist" checkbox).
+ * @param {view} [attributes.AttachmentView] The single `Attachment` view to be used in the `Attachments`.
+ * If none supplied, defaults to wp.media.view.Attachment.EditLibrary.
+ * @param {string} attributes.type The collection's media type. (e.g. 'video').
+ * @param {string} attributes.collectionType The collection type. (e.g. 'playlist').
*/
media.controller.CollectionEdit = media.controller.Library.extend({
defaults: {
- multiple: false,
- describe: true,
- edge: 199,
- editing: false,
- sortable: true,
- searchable: false,
- content: 'browse',
- priority: 60,
- dragInfo: true,
- SettingsView: false,
-
- // Don't sync the selection, as the Edit {Collection} library
- // *is* the selection.
- syncSelection: false
+ multiple: false,
+ sortable: true,
+ searchable: false,
+ content: 'browse',
+ describe: true,
+ dragInfo: true,
+ idealColumnWidth: 170,
+ editing: false,
+ priority: 60,
+ SettingsView: false,
+ syncSelection: false
},
+ /**
+ * @since 3.9.0
+ */
initialize: function() {
var collectionType = this.get('collectionType');
media.controller.Library.prototype.initialize.apply( this, arguments );
},
+ /**
+ * @since 3.9.0
+ */
activate: function() {
var library = this.get('library');
media.controller.Library.prototype.activate.apply( this, arguments );
},
+ /**
+ * @since 3.9.0
+ */
deactivate: function() {
// Stop watching for uploaded attachments.
this.get('library').unobserve( wp.Uploader.queue );
media.controller.Library.prototype.deactivate.apply( this, arguments );
},
- renderSettings: function( browser ) {
+ /**
+ * Render the collection embed settings view in the browser sidebar.
+ *
+ * @todo This is against the pattern elsewhere in media. Typically the frame
+ * is responsible for adding region mode callbacks. Explain.
+ *
+ * @since 3.9.0
+ *
+ * @param {wp.media.view.attachmentsBrowser} The attachments browser view.
+ */
+ renderSettings: function( attachmentsBrowserView ) {
var library = this.get('library'),
collectionType = this.get('collectionType'),
dragInfoText = this.get('dragInfoText'),
SettingsView = this.get('SettingsView'),
obj = {};
- if ( ! library || ! browser ) {
+ if ( ! library || ! attachmentsBrowserView ) {
return;
}
priority: 40
});
- browser.sidebar.set( obj );
+ attachmentsBrowserView.sidebar.set( obj );
if ( dragInfoText ) {
- browser.toolbar.set( 'dragInfo', new media.View({
+ attachmentsBrowserView.toolbar.set( 'dragInfo', new media.View({
el: $( '<div class="instructions">' + dragInfoText + '</div>' )[0],
priority: -40
}) );
}
- browser.toolbar.set( 'reverse', {
+ // Add the 'Reverse order' button to the toolbar.
+ attachmentsBrowserView.toolbar.set( 'reverse', {
text: l10n.reverseOrder,
priority: 80,
/**
* wp.media.controller.CollectionAdd
*
- * @constructor
+ * A state for adding attachments to a collection (e.g. video playlist).
+ *
+ * @class
* @augments wp.media.controller.Library
* @augments wp.media.controller.State
* @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 {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.
+ * @param {boolean|string} [attributes.filterable=uploaded] Whether the library is filterable, and if so what filters should be shown.
+ * Accepts 'all', 'uploaded', or 'unattached'.
+ * @param {string} [attributes.menu=gallery] Initial mode for the menu region.
+ * @param {string} [attributes.content=upload] Initial mode for the content region.
+ * Overridden by persistent user setting if 'contentUserSetting' is true.
+ * @param {string} [attributes.router=browse] Initial mode for the router region.
+ * @param {string} [attributes.toolbar=gallery-add] Initial mode for the toolbar region.
+ * @param {boolean} [attributes.searchable=true] Whether the library is searchable.
+ * @param {boolean} [attributes.sortable=true] Whether the Attachments should be sortable. Depends on the orderby property being set to menuOrder on the attachments collection.
+ * @param {boolean} [attributes.autoSelect=true] Whether an uploaded attachment should be automatically added to the selection.
+ * @param {boolean} [attributes.contentUserSetting=true] Whether the content region's mode should be set and persisted per user.
+ * @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').
*/
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 ),
+ /**
+ * @since 3.9.0
+ */
initialize: function() {
var collectionType = this.get('collectionType');
media.controller.Library.prototype.initialize.apply( this, arguments );
},
+ /**
+ * @since 3.9.0
+ */
activate: function() {
var library = this.get('library'),
editLibrary = this.get('editLibrary'),
/**
* wp.media.controller.FeaturedImage
*
- * @constructor
+ * A state for selecting a featured image for a post.
+ *
+ * @class
* @augments wp.media.controller.Library
* @augments wp.media.controller.State
* @augments Backbone.Model
+ *
+ * @param {object} [attributes] The attributes hash passed to the state.
+ * @param {string} [attributes.id=featured-image] Unique identifier.
+ * @param {string} [attributes.title=Set Featured Image] Title for the state. Displays in the media menu and the frame's title region.
+ * @param {wp.media.model.Attachments} [attributes.library] The attachments collection to browse.
+ * If one is not supplied, a collection of all images will be created.
+ * @param {boolean} [attributes.multiple=false] Whether multi-select is enabled.
+ * @param {string} [attributes.content=upload] Initial mode for the content region.
+ * Overridden by persistent user setting if 'contentUserSetting' is true.
+ * @param {string} [attributes.menu=default] Initial mode for the menu region.
+ * @param {string} [attributes.router=browse] Initial mode for the router region.
+ * @param {string} [attributes.toolbar=featured-image] Initial mode for the toolbar region.
+ * @param {int} [attributes.priority=60] The priority for the state link in the media menu.
+ * @param {boolean} [attributes.searchable=true] Whether the library is searchable.
+ * @param {boolean|string} [attributes.filterable=false] Whether the library is filterable, and if so what filters should be shown.
+ * Accepts 'all', 'uploaded', or 'unattached'.
+ * @param {boolean} [attributes.sortable=true] Whether the Attachments should be sortable. Depends on the orderby property being set to menuOrder on the attachments collection.
+ * @param {boolean} [attributes.autoSelect=true] Whether an uploaded attachment should be automatically added to the selection.
+ * @param {boolean} [attributes.describe=false] Whether to offer UI to describe attachments - e.g. captioning images in a gallery.
+ * @param {boolean} [attributes.contentUserSetting=true] Whether the content region's mode should be set and persisted per user.
+ * @param {boolean} [attributes.syncSelection=true] Whether the Attachments selection should be persisted from the last state.
*/
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,
+ multiple: false,
+ filterable: 'uploaded',
+ toolbar: 'featured-image',
+ priority: 60,
syncSelection: true
}, media.controller.Library.prototype.defaults ),
+ /**
+ * @since 3.5.0
+ */
initialize: function() {
var library, comparator;
library.observe( this.get('selection') );
},
+ /**
+ * @since 3.5.0
+ */
activate: function() {
this.updateSelection();
this.frame.on( 'open', this.updateSelection, this );
media.controller.Library.prototype.activate.apply( this, arguments );
},
+ /**
+ * @since 3.5.0
+ */
deactivate: function() {
this.frame.off( 'open', this.updateSelection, this );
media.controller.Library.prototype.deactivate.apply( this, arguments );
},
+ /**
+ * @since 3.5.0
+ */
updateSelection: function() {
var selection = this.get('selection'),
id = media.view.settings.post.featuredImageId,
/**
* wp.media.controller.ReplaceImage
*
- * Replace a selected single image
+ * A state for replacing an image.
*
- * @constructor
+ * @class
* @augments wp.media.controller.Library
* @augments wp.media.controller.State
* @augments Backbone.Model
+ *
+ * @param {object} [attributes] The attributes hash passed to the state.
+ * @param {string} [attributes.id=replace-image] Unique identifier.
+ * @param {string} [attributes.title=Replace Image] Title for the state. Displays in the media menu and the frame's title region.
+ * @param {wp.media.model.Attachments} [attributes.library] The attachments collection to browse.
+ * If one is not supplied, a collection of all images will be created.
+ * @param {boolean} [attributes.multiple=false] Whether multi-select is enabled.
+ * @param {string} [attributes.content=upload] Initial mode for the content region.
+ * Overridden by persistent user setting if 'contentUserSetting' is true.
+ * @param {string} [attributes.menu=default] Initial mode for the menu region.
+ * @param {string} [attributes.router=browse] Initial mode for the router region.
+ * @param {string} [attributes.toolbar=replace] Initial mode for the toolbar region.
+ * @param {int} [attributes.priority=60] The priority for the state link in the media menu.
+ * @param {boolean} [attributes.searchable=true] Whether the library is searchable.
+ * @param {boolean|string} [attributes.filterable=uploaded] Whether the library is filterable, and if so what filters should be shown.
+ * Accepts 'all', 'uploaded', or 'unattached'.
+ * @param {boolean} [attributes.sortable=true] Whether the Attachments should be sortable. Depends on the orderby property being set to menuOrder on the attachments collection.
+ * @param {boolean} [attributes.autoSelect=true] Whether an uploaded attachment should be automatically added to the selection.
+ * @param {boolean} [attributes.describe=false] Whether to offer UI to describe attachments - e.g. captioning images in a gallery.
+ * @param {boolean} [attributes.contentUserSetting=true] Whether the content region's mode should be set and persisted per user.
+ * @param {boolean} [attributes.syncSelection=true] Whether the Attachments selection should be persisted from the last state.
*/
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,
+ multiple: false,
+ filterable: 'uploaded',
+ toolbar: 'replace',
+ menu: false,
+ priority: 60,
syncSelection: true
}, media.controller.Library.prototype.defaults ),
+ /**
+ * @since 3.9.0
+ *
+ * @param options
+ */
initialize: function( options ) {
var library, comparator;
library.observe( this.get('selection') );
},
+ /**
+ * @since 3.9.0
+ */
activate: function() {
this.updateSelection();
media.controller.Library.prototype.activate.apply( this, arguments );
},
+ /**
+ * @since 3.9.0
+ */
updateSelection: function() {
var selection = this.get('selection'),
attachment = this.image.attachment;
/**
* wp.media.controller.EditImage
*
- * @constructor
+ * A state for editing (cropping, etc.) an image.
+ *
+ * @class
* @augments wp.media.controller.State
* @augments Backbone.Model
+ *
+ * @param {object} attributes The attributes hash passed to the state.
+ * @param {wp.media.model.Attachment} attributes.model The attachment.
+ * @param {string} [attributes.id=edit-image] Unique identifier.
+ * @param {string} [attributes.title=Edit Image] Title for the state. Displays in the media menu and the frame's title region.
+ * @param {string} [attributes.content=edit-image] Initial mode for the content region.
+ * @param {string} [attributes.toolbar=edit-image] Initial mode for the toolbar region.
+ * @param {string} [attributes.menu=false] Initial mode for the menu region.
+ * @param {string} [attributes.url] Unused. @todo Consider removal.
*/
media.controller.EditImage = media.controller.State.extend({
defaults: {
- id: 'edit-image',
- url: '',
- menu: false,
+ id: 'edit-image',
+ title: l10n.editImage,
+ menu: false,
toolbar: 'edit-image',
- title: l10n.editImage,
- content: 'edit-image'
+ content: 'edit-image',
+ url: ''
},
+ /**
+ * @since 3.9.0
+ */
activate: function() {
this.listenTo( this.frame, 'toolbar:render:edit-image', this.toolbar );
},
+ /**
+ * @since 3.9.0
+ */
deactivate: function() {
this.stopListening( this.frame );
},
+ /**
+ * @since 3.9.0
+ */
toolbar: function() {
var frame = this.frame,
lastState = frame.lastState(),
/**
* wp.media.controller.MediaLibrary
*
- * @constructor
+ * @class
* @augments wp.media.controller.Library
* @augments wp.media.controller.State
* @augments Backbone.Model
*/
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 ),
+ /**
+ * @since 3.9.0
+ *
+ * @param options
+ */
initialize: function( options ) {
this.media = options.media;
this.type = options.type;
media.controller.Library.prototype.initialize.apply( this, arguments );
},
+ /**
+ * @since 3.9.0
+ */
activate: function() {
+ // @todo this should use this.frame.
if ( media.frame.lastMime ) {
this.set( 'library', media.query({ type: media.frame.lastMime }) );
delete media.frame.lastMime;
/**
* wp.media.controller.Embed
*
- * @constructor
+ * A state for embedding media from a URL.
+ *
+ * @class
* @augments wp.media.controller.State
* @augments Backbone.Model
+ *
+ * @param {object} attributes The attributes hash passed to the state.
+ * @param {string} [attributes.id=embed] Unique identifier.
+ * @param {string} [attributes.title=Insert From URL] Title for the state. Displays in the media menu and the frame's title region.
+ * @param {string} [attributes.content=embed] Initial mode for the content region.
+ * @param {string} [attributes.menu=default] Initial mode for the menu region.
+ * @param {string} [attributes.toolbar=main-embed] Initial mode for the toolbar region.
+ * @param {string} [attributes.menu=false] Initial mode for the menu region.
+ * @param {int} [attributes.priority=120] The priority for the state link in the media menu.
+ * @param {string} [attributes.type=link] The type of embed. Currently only link is supported.
+ * @param {string} [attributes.url] The embed URL.
+ * @param {object} [attributes.metadata={}] Properties of the embed, which will override attributes.url if set.
*/
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
+ 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 );
},
/**
+ * Trigger a scan of the embedded URL's content for metadata required to embed.
+ *
* @fires wp.media.controller.Embed#scan
*/
scan: function() {
this.set( attributes );
},
/**
+ * Try scanning the embed as an image to discover its dimensions.
+ *
* @param {Object} attributes
*/
scanImage: function( attributes ) {
/**
* wp.media.controller.Cropper
*
- * Allows for a cropping step.
+ * A state for cropping an image.
*
- * @constructor
+ * @class
* @augments wp.media.controller.State
* @augments Backbone.Model
*/
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
},
}
});
- /**
- * ========================================================================
- * VIEWS
- * ========================================================================
- */
-
/**
* wp.media.View
- * -------------
*
- * The base view class.
+ * The base view class for media.
*
* Undelegating events, removing events from the model, and
* removing events from the controller mirror the code for
* This behavior has since been removed, and should not be used
* outside of the media manager.
*
- * @constructor
+ * @class
* @augments wp.Backbone.View
* @augments Backbone.View
*/
wp.Backbone.View.apply( this, arguments );
},
/**
+ * @todo The internal comment mentions this might have been a stop-gap
+ * 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
*/
dispose: function() {
* wp.media.view.Frame
*
* A frame is a composite view consisting of one or more regions and one or more
- * states. Only one state can be active at any given moment.
+ * states.
*
- * @constructor
+ * @see wp.media.controller.State
+ * @see wp.media.controller.Region
+ *
+ * @class
* @augments wp.media.View
* @augments wp.Backbone.View
* @augments Backbone.View
*/
media.view.Frame = media.View.extend({
initialize: function() {
+ _.defaults( this.options, {
+ mode: [ 'select' ]
+ });
this._createRegions();
this._createStates();
+ this._createModes();
},
_createRegions: function() {
}, this );
},
/**
+ * Create the frame's states.
+ *
+ * @see wp.media.controller.State
+ * @see wp.media.controller.StateMachine
+ *
* @fires wp.media.controller.State#ready
*/
_createStates: function() {
this.states.add( this.options.states );
}
},
+
+ /**
+ * A frame can be in a mode or multiple modes at one time.
+ *
+ * For example, the manage media frame can be in the `Bulk Select` or `Edit` mode.
+ */
+ _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 );
+ },
/**
+ * Reset all states on the frame to their defaults.
+ *
* @returns {wp.media.view.Frame} Returns itself to allow chaining
*/
reset: function() {
this.states.invoke( 'trigger', 'reset' );
return this;
- }
- });
-
- // Make the `Frame` a `StateMachine`.
- _.extend( media.view.Frame.prototype, media.controller.StateMachine.prototype );
-
- /**
- * wp.media.view.MediaFrame
- *
- * Type of frame used to create the media modal.
- *
- * @constructor
- * @augments wp.media.view.Frame
- * @augments wp.media.View
- * @augments wp.Backbone.View
- * @augments Backbone.View
- * @mixes wp.media.controller.StateMachine
- */
- media.view.MediaFrame = media.view.Frame.extend({
- className: 'media-frame',
- template: media.template('media-frame'),
- regions: ['menu','title','content','toolbar','router'],
-
+ },
/**
- * @global wp.Uploader
+ * Map activeMode collection events to the frame.
*/
- initialize: function() {
+ 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 );
+ }
+ });
+
+ // Make the `Frame` a `StateMachine`.
+ _.extend( media.view.Frame.prototype, media.controller.StateMachine.prototype );
+
+ /**
+ * wp.media.view.MediaFrame
+ *
+ * The frame used to create the media modal.
+ *
+ * @class
+ * @augments wp.media.view.Frame
+ * @augments wp.media.View
+ * @augments wp.Backbone.View
+ * @augments Backbone.View
+ * @mixes wp.media.controller.StateMachine
+ */
+ media.view.MediaFrame = media.view.Frame.extend({
+ className: 'media-frame',
+ 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 );
this.on( 'content:create:iframe', this.iframeContent, this );
+ this.on( 'content:deactivate:iframe', this.iframeContentCleanup, this );
this.on( 'menu:render:default', this.iframeMenu, this );
this.on( 'open', this.hijackThickbox, this );
this.on( 'close', this.restoreThickbox, this );
});
},
+ iframeContentCleanup: function() {
+ this.$el.removeClass('hide-toolbar');
+ },
+
iframeMenu: function( view ) {
var views = {};
/**
* wp.media.view.MediaFrame.Select
*
- * Type of media frame that is used to select an item or items from the media library
+ * A frame for selecting an item or items from the media library.
*
- * @constructor
+ * @class
* @augments wp.media.view.MediaFrame
* @augments wp.media.view.Frame
* @augments wp.media.View
*/
media.view.MediaFrame.Select = media.view.MediaFrame.extend({
initialize: function() {
- /**
- * call 'initialize' directly on the parent class
- */
+ // Call 'initialize' directly on the parent class.
media.view.MediaFrame.prototype.initialize.apply( this, arguments );
_.defaults( this.options, {
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'),
+ date: state.get('date'),
+ 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
}) );
/**
* wp.media.view.MediaFrame.Post
*
- * @constructor
+ * The frame for manipulating media on the Edit Post page.
+ *
+ * @class
* @augments wp.media.view.MediaFrame.Select
* @augments wp.media.view.MediaFrame
* @augments wp.media.view.Frame
_.defaults( this.options, {
multiple: true,
editing: false,
- state: 'insert'
+ state: 'insert',
+ metadata: {}
});
- /**
- * call 'initialize' directly on the parent class
- */
+
+ // Call 'initialize' directly on the parent class.
media.view.MediaFrame.Select.prototype.initialize.apply( this, arguments );
this.createIframeStates();
},
+ /**
+ * Create the default states.
+ */
createStates: function() {
var options = this.options;
- // Add the default states.
this.states.add([
// Main states.
new media.controller.Library({
}),
// 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() {
// Browse our library of attachments.
this.content.set( view );
+
+ // Trigger the controller to set focus
+ this.trigger( 'edit:selection', this );
},
editImageContent: 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();
}
});
},
/**
* wp.media.view.MediaFrame.ImageDetails
*
- * @constructor
+ * A media frame for manipulating an image that's already been inserted
+ * into a post.
+ *
+ * @class
* @augments wp.media.view.MediaFrame.Select
* @augments wp.media.view.MediaFrame
* @augments wp.media.view.Frame
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,
/**
* wp.media.view.Modal
*
- * @constructor
+ * A modal view, which the media modal uses as its default container.
+ *
+ * @class
* @augments wp.media.View
* @augments wp.Backbone.View
* @augments Backbone.View
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.
/**
* wp.media.view.FocusManager
*
- * @constructor
+ * @class
* @augments wp.media.View
* @augments wp.Backbone.View
* @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;
}
}
+
});
/**
* wp.media.view.UploaderWindow
*
- * @constructor
+ * An uploader window that allows for dragging and dropping media.
+ *
+ * @class
* @augments wp.media.View
* @augments wp.Backbone.View
* @augments Backbone.View
+ *
+ * @param {object} [options] Options hash passed to the view.
+ * @param {object} [options.uploader] Uploader properties.
+ * @param {jQuery} [options.uploader.browser]
+ * @param {jQuery} [options.uploader.dropzone] jQuery collection of the dropzone.
+ * @param {object} [options.uploader.params]
*/
media.view.UploaderWindow = media.View.extend({
tagName: 'div',
$el.hide();
}
});
+
+ // https://core.trac.wordpress.org/ticket/27341
+ _.delay( function() {
+ if ( '0' === $el.css('opacity') && $el.is(':visible') ) {
+ $el.hide();
+ }
+ }, 500 );
}
});
/**
+ * Creates a dropzone on WP editor instances (elements with .wp-editor-wrap
+ * or #wp-fullscreen-body) and relays drag'n'dropped files to a media workflow.
+ *
* wp.media.view.EditorUploader
*
- * @constructor
+ * @class
* @augments wp.media.View
* @augments wp.Backbone.View
* @augments Backbone.View
overDropzone: false,
draggingFile: null,
+ /**
+ * Bind drag'n'drop events to callbacks.
+ */
initialize: function() {
var self = this;
return this;
},
+ /**
+ * Check browser support for drag'n'drop.
+ *
+ * @return Boolean
+ */
browserSupport: function() {
var supports = false, div = document.createElement('div');
return this;
},
+ /**
+ * 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.
+ */
drop: function( event ) {
- var $wrap = null;
+ var $wrap = null, uploadView;
this.containerDragleave( event );
this.dropzoneDragleave( event );
title: wp.media.view.l10n.addMedia,
multiple: true
});
- this.workflow.on( 'uploader:ready', this.addFiles, this );
+ uploadView = this.workflow.uploader;
+ if ( uploadView.uploader && uploadView.uploader.ready ) {
+ this.addFiles.apply( this );
+ } else {
+ this.workflow.on( 'uploader:ready', this.addFiles, this );
+ }
} else {
this.workflow.state().reset();
this.addFiles.apply( this );
return false;
},
+ /**
+ * Add the files to the uploader.
+ */
addFiles: function() {
if ( this.files.length ) {
this.workflow.uploader.uploader.uploader.addFile( _.toArray( this.files ) );
/**
* wp.media.view.UploaderInline
*
- * @constructor
+ * The inline uploader that shows up in the 'Upload Files' tab.
+ *
+ * @class
* @augments wp.media.View
* @augments wp.Backbone.View
* @augments Backbone.View
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' );
}
+
});
/**
* wp.media.view.UploaderStatus
*
- * @constructor
+ * An uploader status for on-going uploads.
+ *
+ * @class
* @augments wp.media.View
* @augments wp.Backbone.View
* @augments Backbone.View
/**
* wp.media.view.UploaderStatusError
*
- * @constructor
+ * @class
* @augments wp.media.View
* @augments wp.Backbone.View
* @augments Backbone.View
/**
* wp.media.view.Toolbar
*
- * @constructor
+ * A toolbar which consists of a primary and a secondary section. Each sections
+ * can be filled with views.
+ *
+ * @class
* @augments wp.media.View
* @augments wp.Backbone.View
* @augments Backbone.View
// 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 ]);
/**
* wp.media.view.Toolbar.Select
*
- * @constructor
+ * @class
* @augments wp.media.view.Toolbar
* @augments wp.media.View
* @augments wp.Backbone.View
requires: options.requires
}
});
- /**
- * call 'initialize' directly on the parent class
- */
+ // Call 'initialize' directly on the parent class.
media.view.Toolbar.prototype.initialize.apply( this, arguments );
},
/**
* wp.media.view.Toolbar.Embed
*
- * @constructor
+ * @class
* @augments wp.media.view.Toolbar.Select
* @augments wp.media.view.Toolbar
* @augments wp.media.View
text: l10n.insertIntoPost,
requires: false
});
- /**
- * call 'initialize' directly on the parent class
- */
+ // Call 'initialize' directly on the parent class.
media.view.Toolbar.Select.prototype.initialize.apply( this, arguments );
},
/**
* wp.media.view.Button
*
- * @constructor
+ * @class
* @augments wp.media.View
* @augments wp.Backbone.View
* @augments Backbone.View
/**
* wp.media.view.ButtonGroup
*
- * @constructor
+ * @class
* @augments wp.media.View
* @augments wp.Backbone.View
* @augments Backbone.View
/**
* wp.media.view.PriorityList
*
- * @constructor
+ * @class
* @augments wp.media.View
* @augments wp.Backbone.View
* @augments Backbone.View
/**
* wp.media.view.MenuItem
*
- * @constructor
+ * @class
* @augments wp.media.View
* @augments wp.Backbone.View
* @augments Backbone.View
} 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
}
},
/**
/**
* wp.media.view.Menu
*
- * @constructor
+ * @class
* @augments wp.media.view.PriorityList
* @augments wp.media.View
* @augments wp.Backbone.View
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
/**
* wp.media.view.RouterItem
*
- * @constructor
+ * @class
* @augments wp.media.view.MenuItem
* @augments wp.media.View
* @augments wp.Backbone.View
* @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 ) {
/**
* wp.media.view.Router
*
- * @constructor
+ * @class
* @augments wp.media.view.Menu
* @augments wp.media.view.PriorityList
* @augments wp.media.View
initialize: function() {
this.controller.on( 'content:render', this.update, this );
- /**
- * call 'initialize' directly on the parent class
- */
+ // Call 'initialize' directly on the parent class.
media.view.Menu.prototype.initialize.apply( this, arguments );
},
/**
* wp.media.view.Sidebar
*
- * @constructor
+ * @class
* @augments wp.media.view.PriorityList
* @augments wp.media.View
* @augments wp.Backbone.View
/**
* wp.media.view.Attachment
*
- * @constructor
+ * @class
* @augments wp.media.View
* @augments wp.Backbone.View
* @augments Backbone.View
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 .close': 'removeFromLibrary',
+ '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') );
+ this.listenTo( this.controller, 'attachment:compat:waiting attachment:compat:ready', this.updateSave );
},
/**
* @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;
+ }
+
+ event.preventDefault();
+
+ // 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 );
+ 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' );
},
/**
* @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
* @param {Object} event
*/
removeFromLibrary: function( event ) {
- // Stop propagation so the model isn't selected.
+ // Catch enter and space events
+ if ( 'keydown' === event.type && 13 !== event.keyCode && 32 !== event.keyCode ) {
+ return;
+ }
+
+ // Stop propagation so the model isn't selected.
event.stopPropagation();
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
/**
* wp.media.view.Attachment.Library
*
- * @constructor
+ * @class
* @augments wp.media.view.Attachment
* @augments wp.media.View
* @augments wp.Backbone.View
/**
* wp.media.view.Attachment.EditLibrary
*
- * @constructor
+ * @class
* @augments wp.media.view.Attachment
* @augments wp.media.View
* @augments wp.Backbone.View
/**
* wp.media.view.Attachments
*
- * @constructor
+ * @class
* @augments wp.media.View
* @augments wp.Backbone.View
* @augments Backbone.View
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;
}
// If the `collection` has a `comparator`, disable sorting.
disabled: !! collection.comparator,
- // Prevent attachments from being dragged outside the bounding
- // box of the list.
- containment: this.$el,
-
// Change the position of the attachment as soon as the
// mouse pointer overlaps a thumbnail.
tolerance: 'pointer',
},
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');
- };
- }())
});
/**
* wp.media.view.Search
*
- * @constructor
+ * @class
* @augments wp.media.View
* @augments wp.Backbone.View
* @augments Backbone.View
media.view.Search = media.View.extend({
tagName: 'input',
className: 'search',
+ id: 'media-search-input',
attributes: {
type: 'search',
/**
* wp.media.view.AttachmentFilters
*
- * @constructor
+ * @class
* @augments wp.media.View
* @augments wp.Backbone.View
* @augments Backbone.View
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 selected filter changes, update the Attachment Query properties to match.
+ */
change: function() {
var filter = this.filters[ this.el.value ];
-
if ( filter ) {
this.model.set( filter.props );
}
}
});
+ /**
+ * A filter dropdown for month/dates.
+ *
+ * @class
+ * @augments wp.media.view.AttachmentFilters
+ * @augments wp.media.View
+ * @augments wp.Backbone.View
+ * @augments Backbone.View
+ */
+ media.view.DateFilter = media.view.AttachmentFilters.extend({
+ id: 'media-attachment-date-filters',
+
+ createFilters: function() {
+ var filters = {};
+ _.each( media.view.settings.months || {}, function( value, index ) {
+ filters[ index ] = {
+ text: value.text,
+ props: {
+ year: value.year,
+ monthnum: value.month
+ }
+ };
+ });
+ filters.all = {
+ text: l10n.allDates,
+ props: {
+ monthnum: false,
+ year: false
+ },
+ priority: 10
+ };
+ this.filters = filters;
+ }
+ });
+
/**
* wp.media.view.AttachmentFilters.Uploaded
*
- * @constructor
+ * @class
* @augments wp.media.view.AttachmentFilters
* @augments wp.media.View
* @augments wp.Backbone.View
order: 'ASC'
},
priority: 20
+ },
+
+ unattached: {
+ text: l10n.unattached,
+ props: {
+ uploadedTo: 0,
+ orderby: 'menuOrder',
+ order: 'ASC'
+ },
+ priority: 50
}
};
}
/**
* wp.media.view.AttachmentFilters.All
*
- * @constructor
+ * @class
* @augments wp.media.view.AttachmentFilters
* @augments wp.media.View
* @augments wp.Backbone.View
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
*
- * @constructor
+ * @class
* @augments wp.media.View
* @augments wp.Backbone.View
* @augments Backbone.View
+ *
+ * @param {object} options
+ * @param {object} [options.filters=false] Which filters to show in the browser's toolbar.
+ * Accepts 'uploaded' and 'all'.
+ * @param {object} [options.search=true] Whether to show the search interface in the
+ * browser's toolbar.
+ * @param {object} [options.date=true] Whether to show the date filter in the
+ * browser's toolbar.
+ * @param {object} [options.display=false] Whether to show the attachments display settings
+ * view in the sidebar.
+ * @param {bool|string} [options.sidebar=true] Whether to create a sidebar for the browser.
+ * Accepts true, false, and 'errors'.
*/
media.view.AttachmentsBrowser = media.View.extend({
tagName: 'div',
_.defaults( this.options, {
filters: false,
search: true,
+ date: true,
display: false,
-
+ sidebar: true,
AttachmentView: media.view.Attachment.Library
});
+ this.listenTo( this.controller, 'toggle:upload:attachment', _.bind( this.toggleUploader, this ) );
+ this.controller.on( 'edit:selection', this.editSelection );
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 );
},
+
+ editSelection: function( modal ) {
+ modal.$( '.media-button-backToLibrary' ).focus();
+ },
+
/**
* @returns {wp.media.view.AttachmentsBrowser} Returns itself to allow chaining
*/
},
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 changed = [], removed = [], 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;
+ }
+
+ selection.each( function( model ) {
+ if ( ! model.get( 'nonces' )['delete'] ) {
+ removed.push( model );
+ return;
+ }
+
+ if ( media.view.settings.mediaTrash && 'trash' === model.get( 'status' ) ) {
+ model.set( 'status', 'inherit' );
+ changed.push( model.save() );
+ removed.push( model );
+ } else if ( media.view.settings.mediaTrash ) {
+ model.set( 'status', 'trash' );
+ changed.push( model.save() );
+ removed.push( model );
+ } else {
+ model.destroy({wait: true});
+ }
+ } );
+
+ if ( changed.length ) {
+ selection.remove( removed );
+
+ $.when.apply( null, changed ).then( function() {
+ library._requery( true );
+ self.controller.trigger( 'selection:action:done' );
+ } );
+ } else {
+ this.controller.trigger( 'selection:action:done' );
+ }
+ }
+ }).render() );
+
+ if ( media.view.settings.mediaTrash ) {
+ this.toolbar.set( 'deleteSelectedPermanentlyButton', new media.view.DeleteSelectedPermanentlyButton({
+ filters: Filters,
+ style: 'primary',
+ disabled: true,
+ text: l10n.deleteSelected,
+ controller: this.controller,
+ priority: -55,
+ click: function() {
+ var removed = [], selection = this.controller.state().get( 'selection' );
+
+ if ( ! selection.length || ! confirm( l10n.warnBulkDelete ) ) {
+ return;
+ }
+
+ selection.each( function( model ) {
+ if ( ! model.get( 'nonces' )['delete'] ) {
+ removed.push( model );
+ return;
+ }
+
+ model.destroy();
+ } );
+
+ selection.remove( removed );
+ this.controller.trigger( 'selection:action:done' );
+ }
+ }).render() );
+ }
+
+ } else if ( this.options.date ) {
+ // 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: -75
+ }).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' );
}
});
/**
* wp.media.view.Selection
*
- * @constructor
+ * @class
* @augments wp.media.View
* @augments wp.Backbone.View
* @augments Backbone.View
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();
}
});
/**
* wp.media.view.Attachment.Selection
*
- * @constructor
+ * @class
* @augments wp.media.view.Attachment
* @augments wp.media.View
* @augments wp.Backbone.View
/**
* wp.media.view.Attachments.Selection
*
- * @constructor
+ * @class
* @augments wp.media.view.Attachments
* @augments wp.media.View
* @augments wp.Backbone.View
events: {},
initialize: function() {
_.defaults( this.options, {
- sortable: true,
+ sortable: false,
resize: false,
// The single `Attachment` view to be used in the `Attachments` view.
AttachmentView: media.view.Attachment.Selection
});
- /**
- * call 'initialize' directly on the parent class
- */
+ // Call 'initialize' directly on the parent class.
return media.view.Attachments.prototype.initialize.apply( this, arguments );
}
});
/**
* wp.media.view.Attachments.EditSelection
*
- * @constructor
+ * @class
* @augments wp.media.view.Attachment.Selection
* @augments wp.media.view.Attachment
* @augments wp.media.View
/**
* wp.media.view.Settings
*
- * @constructor
+ * @class
* @augments wp.media.View
* @augments wp.Backbone.View
* @augments Backbone.View
}
// Handle checkboxes.
} else if ( $setting.is('input[type="checkbox"]') ) {
- $setting.prop( 'checked', !! value );
+ $setting.prop( 'checked', !! value && 'false' !== value );
}
},
/**
/**
* wp.media.view.Settings.AttachmentDisplay
*
- * @constructor
+ * @class
* @augments wp.media.view.Settings
* @augments wp.media.View
* @augments wp.Backbone.View
_.defaults( this.options, {
userSettings: false
});
- /**
- * call 'initialize' directly on the parent class
- */
+ // Call 'initialize' directly on the parent class.
media.view.Settings.prototype.initialize.apply( this, arguments );
this.model.on( 'change:link', this.updateLinkTo, this );
$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();
}
}
/**
* wp.media.view.Settings.Gallery
*
- * @constructor
+ * @class
* @augments wp.media.view.Settings
* @augments wp.media.View
* @augments wp.Backbone.View
/**
* wp.media.view.Settings.Playlist
*
- * @constructor
+ * @class
* @augments wp.media.view.Settings
* @augments wp.media.View
* @augments wp.Backbone.View
/**
* wp.media.view.Attachment.Details
*
- * @constructor
+ * @class
* @augments wp.media.view.Attachment
* @augments wp.media.View
* @augments wp.Backbone.View
className: 'attachment-details',
template: media.template('attachment-details'),
+ attributes: function() {
+ return {
+ 'tabIndex': 0,
+ 'data-id': this.model.get( 'id' )
+ };
+ },
+
events: {
'change [data-setting]': 'updateSetting',
'change [data-setting] input': 'updateSetting',
'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
});
- /**
- * call 'initialize' directly on the parent class
- */
+
+ 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
*
- * @constructor
+ * A view to display fields added via the `attachment_fields_to_edit` filter.
+ *
+ * @class
* @augments wp.media.View
* @augments wp.Backbone.View
* @augments 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;
},
/**
data[ pair.name ] = pair.value;
});
- this.model.saveCompat( data );
+ this.controller.trigger( 'attachment:compat:waiting', ['waiting'] );
+ this.model.saveCompat( data ).always( _.bind( this.postSave, this ) );
+ },
+
+ postSave: function() {
+ this.controller.trigger( 'attachment:compat:ready', ['ready'] );
}
});
/**
* wp.media.view.Iframe
*
- * @constructor
+ * @class
* @augments wp.media.View
* @augments wp.Backbone.View
* @augments Backbone.View
/**
* wp.media.view.Embed
*
- * @constructor
+ * @class
* @augments wp.media.View
* @augments wp.Backbone.View
* @augments Backbone.View
}
});
+ /**
+ * @class
+ * @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
*
- * @constructor
+ * @class
* @augments wp.media.View
* @augments wp.Backbone.View
* @augments Backbone.View
},
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 ) {
/**
* wp.media.view.EmbedLink
*
- * @constructor
+ * @class
* @augments wp.media.view.Settings
* @augments wp.media.View
* @augments wp.Backbone.View
*/
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
+ * @class
* @augments wp.media.view.Settings.AttachmentDisplay
* @augments wp.media.view.Settings
* @augments wp.media.View
/**
* wp.media.view.ImageDetails
*
- * @contructor
+ * @class
* @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 );
-
}
},
* wp.customize.HeaderControl.calculateImageSelectOptions via
* wp.customize.HeaderControl.openMM.
*
- * @constructor
+ * @class
* @augments wp.media.View
* @augments wp.Backbone.View
* @augments Backbone.View
},
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() {
/**
* wp.media.view.Spinner
*
- * @constructor
+ * @class
* @augments wp.media.View
* @augments wp.Backbone.View
* @augments Backbone.View