X-Git-Url: https://scripts.mit.edu/gitweb/autoinstalls/wordpress.git/blobdiff_plain/11be15bd505d66a91e2c80062190b13e315a04a9..88550bc3400cc7c035ff590ecb007c7938041ded:/wp-includes/js/media-models.js diff --git a/wp-includes/js/media-models.js b/wp-includes/js/media-models.js index f501b69f..84bb02fa 100644 --- a/wp-includes/js/media-models.js +++ b/wp-includes/js/media-models.js @@ -1,36 +1,49 @@ +/* global _wpMediaModelsL10n:false */ window.wp = window.wp || {}; (function($){ - var Attachment, Attachments, Query, compare, l10n, media; + var Attachment, Attachments, Query, PostImage, compare, l10n, media; /** - * wp.media( attributes ) + * Create and return a media frame. * - * Handles the default media experience. Automatically creates - * and opens a media frame, and returns the result. - * Does nothing if the controllers do not exist. + * Handles the default media experience. * * @param {object} attributes The properties passed to the main media controller. - * @return {object} A media workflow. + * @return {wp.media.view.MediaFrame} A media workflow. */ media = wp.media = function( attributes ) { var MediaFrame = media.view.MediaFrame, frame; - if ( ! MediaFrame ) + if ( ! MediaFrame ) { return; + } attributes = _.defaults( attributes || {}, { frame: 'select' }); - if ( 'select' === attributes.frame && MediaFrame.Select ) + if ( 'select' === attributes.frame && MediaFrame.Select ) { frame = new MediaFrame.Select( attributes ); - else if ( 'post' === attributes.frame && MediaFrame.Post ) + } else if ( 'post' === attributes.frame && MediaFrame.Post ) { frame = new MediaFrame.Post( attributes ); + } else if ( 'manage' === attributes.frame && MediaFrame.Manage ) { + frame = new MediaFrame.Manage( attributes ); + } else if ( 'image' === attributes.frame && MediaFrame.ImageDetails ) { + frame = new MediaFrame.ImageDetails( attributes ); + } else if ( 'audio' === attributes.frame && MediaFrame.AudioDetails ) { + frame = new MediaFrame.AudioDetails( attributes ); + } else if ( 'video' === attributes.frame && MediaFrame.VideoDetails ) { + frame = new MediaFrame.VideoDetails( attributes ); + } else if ( 'edit-attachments' === attributes.frame && MediaFrame.EditAttachments ) { + frame = new MediaFrame.EditAttachments( attributes ); + } delete attributes.frame; + media.frame = frame; + return frame; }; @@ -50,7 +63,9 @@ window.wp = window.wp || {}; */ /** - * A basic comparator. + * A basic equality comparator for Backbone models. + * + * Used to order models within a collection - @see wp.media.model.Attachments.comparator(). * * @param {mixed} a The primary parameter to compare. * @param {mixed} b The primary parameter to compare. @@ -61,18 +76,22 @@ window.wp = window.wp || {}; * 1: b should come before a. */ compare = function( a, b, ac, bc ) { - if ( _.isEqual( a, b ) ) + if ( _.isEqual( a, b ) ) { return ac === bc ? 0 : (ac > bc ? -1 : 1); - else + } else { return a > b ? -1 : 1; + } }; _.extend( media, { /** * media.template( id ) * - * Fetches a template by id. + * Fetch a JavaScript template for an id, and return a templating function for it. + * * See wp.template() in `wp-includes/js/wp-util.js`. + * + * @borrows wp.template as template */ template: wp.template, @@ -81,6 +100,8 @@ window.wp = window.wp || {}; * * Sends a POST request to WordPress. * See wp.ajax.post() in `wp-includes/js/wp-util.js`. + * + * @borrows wp.ajax.post as post */ post: wp.ajax.post, @@ -89,10 +110,17 @@ window.wp = window.wp || {}; * * Sends an XHR request to WordPress. * See wp.ajax.send() in `wp-includes/js/wp-util.js`. + * + * @borrows wp.ajax.send as ajax */ ajax: wp.ajax.send, - // Scales a set of dimensions to fit within bounding dimensions. + /** + * Scales a set of dimensions to fit within bounding dimensions. + * + * @param {Object} dimensions + * @returns {Object} + */ fit: function( dimensions ) { var width = dimensions.width, height = dimensions.height, @@ -130,43 +158,66 @@ window.wp = window.wp || {}; }; } }, - - // Truncates a string by injecting an ellipsis into the middle. - // Useful for filenames. + /** + * Truncates a string by injecting an ellipsis into the middle. + * Useful for filenames. + * + * @param {String} string + * @param {Number} [length=30] + * @param {String} [replacement=…] + * @returns {String} The string, unless length is greater than string.length. + */ truncate: function( string, length, replacement ) { length = length || 30; replacement = replacement || '…'; - if ( string.length <= length ) + if ( string.length <= length ) { return string; + } return string.substr( 0, length / 2 ) + replacement + string.substr( -1 * length / 2 ); } }); - /** * ======================================================================== * MODELS * ======================================================================== */ - - /** - * wp.media.attachment - */ - media.attachment = function( id ) { + /** + * wp.media.attachment + * + * @static + * @param {String} id A string used to identify a model. + * @returns {wp.media.model.Attachment} + */ + media.attachment = function( id ) { return Attachment.get( id ); - }; + }; /** * wp.media.model.Attachment + * + * @class + * @augments Backbone.Model */ Attachment = media.model.Attachment = Backbone.Model.extend({ + /** + * Triggered when attachment details change + * Overrides Backbone.Model.sync + * + * @param {string} method + * @param {wp.media.model.Attachment} model + * @param {Object} [options={}] + * + * @returns {Promise} + */ sync: function( method, model, options ) { // If the attachment does not yet have an `id`, return an instantly // rejected promise. Otherwise, all of our requests will fail. - if ( _.isUndefined( this.id ) ) + if ( _.isUndefined( this.id ) ) { return $.Deferred().rejectWith( this ).promise(); + } // Overload the `read` request so Attachment.fetch() functions correctly. if ( 'read' === method ) { @@ -181,8 +232,9 @@ window.wp = window.wp || {}; // Overload the `update` request so properties can be saved. } else if ( 'update' === method ) { // If we do not have the necessary nonce, fail immeditately. - if ( ! this.get('nonces') || ! this.get('nonces').update ) + if ( ! this.get('nonces') || ! this.get('nonces').update ) { return $.Deferred().rejectWith( this ).promise(); + } options = options || {}; options.context = this; @@ -211,8 +263,9 @@ window.wp = window.wp || {}; } else if ( 'delete' === method ) { options = options || {}; - if ( ! options.wait ) + if ( ! options.wait ) { this.destroyed = true; + } options.context = this; options.data = _.extend( options.data || {}, { @@ -229,26 +282,43 @@ window.wp = window.wp || {}; // Otherwise, fall back to `Backbone.sync()`. } else { + /** + * Call `sync` directly on Backbone.Model + */ return Backbone.Model.prototype.sync.apply( this, arguments ); } }, - - parse: function( resp, xhr ) { - if ( ! resp ) + /** + * Convert date strings into Date objects. + * + * @param {Object} resp The raw response object, typically returned by fetch() + * @returns {Object} The modified response object, which is the attributes hash + * to be set on the model. + */ + parse: function( resp ) { + if ( ! resp ) { return resp; + } - // Convert date strings into Date objects. resp.date = new Date( resp.date ); resp.modified = new Date( resp.modified ); return resp; }, - + /** + * @param {Object} data The properties to be saved. + * @param {Object} options Sync options. e.g. patch, wait, success, error. + * + * @this Backbone.Model + * + * @returns {Promise} + */ saveCompat: function( data, options ) { var model = this; // If we do not have the necessary nonce, fail immeditately. - if ( ! this.get('nonces') || ! this.get('nonces').update ) + if ( ! this.get('nonces') || ! this.get('nonces').update ) { return $.Deferred().rejectWith( this ).promise(); + } return media.post( 'save-attachment-compat', _.defaults({ id: this.id, @@ -259,21 +329,213 @@ window.wp = window.wp || {}; }); } }, { + /** + * Create a new model on the static 'all' attachments collection and return it. + * + * @static + * @param {Object} attrs + * @returns {wp.media.model.Attachment} + */ create: function( attrs ) { return Attachments.all.push( attrs ); }, - + /** + * Create a new model on the static 'all' attachments collection and return it. + * + * If this function has already been called for the id, + * it returns the specified attachment. + * + * @static + * @param {string} id A string used to identify a model. + * @param {Backbone.Model|undefined} attachment + * @returns {wp.media.model.Attachment} + */ get: _.memoize( function( id, attachment ) { return Attachments.all.push( attachment || { id: id } ); }) }); + /** + * wp.media.model.PostImage + * + * An instance of an image that's been embedded into a post. + * + * Used in the embedded image attachment display settings modal - @see wp.media.view.MediaFrame.ImageDetails. + * + * @class + * @augments Backbone.Model + * + * @param {int} [attributes] Initial model attributes. + * @param {int} [attributes.attachment_id] ID of the attachment. + **/ + PostImage = media.model.PostImage = Backbone.Model.extend({ + + initialize: function( attributes ) { + this.attachment = false; + + if ( attributes.attachment_id ) { + this.attachment = Attachment.get( attributes.attachment_id ); + if ( this.attachment.get( 'url' ) ) { + this.dfd = $.Deferred(); + this.dfd.resolve(); + } else { + this.dfd = this.attachment.fetch(); + } + this.bindAttachmentListeners(); + } + + // keep url in sync with changes to the type of link + this.on( 'change:link', this.updateLinkUrl, this ); + this.on( 'change:size', this.updateSize, this ); + + this.setLinkTypeFromUrl(); + this.setAspectRatio(); + + this.set( 'originalUrl', attributes.url ); + }, + + bindAttachmentListeners: function() { + this.listenTo( this.attachment, 'sync', this.setLinkTypeFromUrl ); + this.listenTo( this.attachment, 'sync', this.setAspectRatio ); + this.listenTo( this.attachment, 'change', this.updateSize ); + }, + + changeAttachment: function( attachment, props ) { + this.stopListening( this.attachment ); + this.attachment = attachment; + this.bindAttachmentListeners(); + + this.set( 'attachment_id', this.attachment.get( 'id' ) ); + this.set( 'caption', this.attachment.get( 'caption' ) ); + this.set( 'alt', this.attachment.get( 'alt' ) ); + this.set( 'size', props.get( 'size' ) ); + this.set( 'align', props.get( 'align' ) ); + this.set( 'link', props.get( 'link' ) ); + this.updateLinkUrl(); + this.updateSize(); + }, + + setLinkTypeFromUrl: function() { + var linkUrl = this.get( 'linkUrl' ), + type; + + if ( ! linkUrl ) { + this.set( 'link', 'none' ); + return; + } + + // default to custom if there is a linkUrl + type = 'custom'; + + if ( this.attachment ) { + if ( this.attachment.get( 'url' ) === linkUrl ) { + type = 'file'; + } else if ( this.attachment.get( 'link' ) === linkUrl ) { + type = 'post'; + } + } else { + if ( this.get( 'url' ) === linkUrl ) { + type = 'file'; + } + } + + this.set( 'link', type ); + }, + + updateLinkUrl: function() { + var link = this.get( 'link' ), + url; + + switch( link ) { + case 'file': + if ( this.attachment ) { + url = this.attachment.get( 'url' ); + } else { + url = this.get( 'url' ); + } + this.set( 'linkUrl', url ); + break; + case 'post': + this.set( 'linkUrl', this.attachment.get( 'link' ) ); + break; + case 'none': + this.set( 'linkUrl', '' ); + break; + } + }, + + updateSize: function() { + var size; + + if ( ! this.attachment ) { + return; + } + + if ( this.get( 'size' ) === 'custom' ) { + this.set( 'width', this.get( 'customWidth' ) ); + this.set( 'height', this.get( 'customHeight' ) ); + this.set( 'url', this.get( 'originalUrl' ) ); + return; + } + + size = this.attachment.get( 'sizes' )[ this.get( 'size' ) ]; + + if ( ! size ) { + return; + } + + this.set( 'url', size.url ); + this.set( 'width', size.width ); + this.set( 'height', size.height ); + }, + + setAspectRatio: function() { + var full; + + if ( this.attachment && this.attachment.get( 'sizes' ) ) { + full = this.attachment.get( 'sizes' ).full; + + if ( full ) { + this.set( 'aspectRatio', full.width / full.height ); + return; + } + } + + this.set( 'aspectRatio', this.get( 'customWidth' ) / this.get( 'customHeight' ) ); + } + }); + /** * wp.media.model.Attachments + * + * A collection of attachments. + * + * This collection has no persistence with the server without supplying + * 'options.props.query = true', which will mirror the collection + * to an Attachments Query collection - @see wp.media.model.Attachments.mirror(). + * + * @class + * @augments Backbone.Collection + * + * @param {array} [models] Models to initialize with the collection. + * @param {object} [options] Options hash for the collection. + * @param {string} [options.props] Options hash for the initial query properties. + * @param {string} [options.props.order] Initial order (ASC or DESC) for the collection. + * @param {string} [options.props.orderby] Initial attribute key to order the collection by. + * @param {string} [options.props.query] Whether the collection is linked to an attachments query. + * @param {string} [options.observe] + * @param {string} [options.filters] + * */ Attachments = media.model.Attachments = Backbone.Collection.extend({ + /** + * @type {wp.media.model.Attachment} + */ model: Attachment, - + /** + * @param {Array} [models=[]] Array of models used to populate the collection. + * @param {Object} [options={}] + */ initialize: function( models, options ) { options = options || {}; @@ -287,34 +549,51 @@ window.wp = window.wp || {}; this.props.on( 'change:orderby', this._changeOrderby, this ); this.props.on( 'change:query', this._changeQuery, this ); - // Set the `props` model and fill the default property values. this.props.set( _.defaults( options.props || {} ) ); - // Observe another `Attachments` collection if one is provided. - if ( options.observe ) + if ( options.observe ) { this.observe( options.observe ); + } }, - - // Automatically sort the collection when the order changes. - _changeOrder: function( model, order ) { - if ( this.comparator ) + /** + * Sort the collection when the order attribute changes. + * + * @access private + */ + _changeOrder: function() { + if ( this.comparator ) { this.sort(); + } }, - - // Set the default comparator only when the `orderby` property is set. + /** + * Set the default comparator only when the `orderby` property is set. + * + * @access private + * + * @param {Backbone.Model} model + * @param {string} orderby + */ _changeOrderby: function( model, orderby ) { // If a different comparator is defined, bail. - if ( this.comparator && this.comparator !== Attachments.comparator ) + if ( this.comparator && this.comparator !== Attachments.comparator ) { return; + } - if ( orderby && 'post__in' !== orderby ) + if ( orderby && 'post__in' !== orderby ) { this.comparator = Attachments.comparator; - else + } else { delete this.comparator; + } }, - - // If the `query` property is set to true, query the server using - // the `props` values, and sync the results to this collection. + /** + * If the `query` property is set to true, query the server using + * the `props` values, and sync the results to this collection. + * + * @access private + * + * @param {Backbone.Model} model + * @param {Boolean} query + */ _changeQuery: function( model, query ) { if ( query ) { this.props.on( 'change', this._requery, this ); @@ -323,65 +602,97 @@ window.wp = window.wp || {}; this.props.off( 'change', this._requery, this ); } }, - - _changeFilteredProps: function( model, options ) { + /** + * @access private + * + * @param {Backbone.Model} model + */ + _changeFilteredProps: function( model ) { // If this is a query, updating the collection will be handled by // `this._requery()`. - if ( this.props.get('query') ) + if ( this.props.get('query') ) { return; + } var changed = _.chain( model.changed ).map( function( t, prop ) { var filter = Attachments.filters[ prop ], term = model.get( prop ); - if ( ! filter ) + if ( ! filter ) { return; + } - if ( term && ! this.filters[ prop ] ) + if ( term && ! this.filters[ prop ] ) { this.filters[ prop ] = filter; - else if ( ! term && this.filters[ prop ] === filter ) + } else if ( ! term && this.filters[ prop ] === filter ) { delete this.filters[ prop ]; - else + } else { return; + } // Record the change. return true; }, this ).any().value(); - if ( ! changed ) + if ( ! changed ) { return; + } // If no `Attachments` model is provided to source the searches // from, then automatically generate a source from the existing // models. - if ( ! this._source ) + if ( ! this._source ) { this._source = new Attachments( this.models ); + } this.reset( this._source.filter( this.validator, this ) ); }, validateDestroyed: false, - + /** + * Checks whether an attachment is valid. + * + * @param {wp.media.model.Attachment} attachment + * @returns {Boolean} + */ validator: function( attachment ) { - if ( ! this.validateDestroyed && attachment.destroyed ) + if ( ! this.validateDestroyed && attachment.destroyed ) { return false; - return _.all( this.filters, function( filter, key ) { + } + return _.all( this.filters, function( filter ) { return !! filter.call( this, attachment ); }, this ); }, - + /** + * Add or remove an attachment to the collection depending on its validity. + * + * @param {wp.media.model.Attachment} attachment + * @param {Object} options + * @returns {wp.media.model.Attachments} Returns itself to allow chaining + */ validate: function( attachment, options ) { var valid = this.validator( attachment ), hasAttachment = !! this.get( attachment.cid ); - if ( ! valid && hasAttachment ) + if ( ! valid && hasAttachment ) { this.remove( attachment, options ); - else if ( valid && ! hasAttachment ) + } else if ( valid && ! hasAttachment ) { this.add( attachment, options ); + } return this; }, + /** + * Add or remove all attachments from another collection depending on each one's validity. + * + * @param {wp.media.model.Attachments} attachments + * @param {object} [options={}] + * + * @fires wp.media.model.Attachments#reset + * + * @returns {wp.media.model.Attachments} Returns itself to allow chaining + */ validateAll: function( attachments, options ) { options = options || {}; @@ -389,12 +700,18 @@ window.wp = window.wp || {}; this.validate( attachment, { silent: true }); }, this ); - if ( ! options.silent ) + if ( ! options.silent ) { this.trigger( 'reset', this, options ); - + } return this; }, - + /** + * Start observing another attachments collection change events + * and replicate them on this collection. + * + * @param {wp.media.model.Attachments} The attachments collection to observe. + * @returns {wp.media.model.Attachments} Returns itself to allow chaining. + */ observe: function( attachments ) { this.observers = this.observers || []; this.observers.push( attachments ); @@ -404,7 +721,12 @@ window.wp = window.wp || {}; this.validateAll( attachments ); return this; }, - + /** + * Stop replicating collection change events from another attachments collection. + * + * @param {wp.media.model.Attachments} The attachments collection to stop observing. + * @returns {wp.media.model.Attachments} Returns itself to allow chaining + */ unobserve: function( attachments ) { if ( attachments ) { attachments.off( null, null, this ); @@ -419,7 +741,15 @@ window.wp = window.wp || {}; return this; }, - + /** + * @access private + * + * @param {wp.media.model.Attachments} attachment + * @param {wp.media.model.Attachments} attachments + * @param {Object} options + * + * @returns {wp.media.model.Attachments} Returns itself to allow chaining + */ _validateHandler: function( attachment, attachments, options ) { // If we're not mirroring this `attachments` collection, // only retain the `silent` option. @@ -429,14 +759,27 @@ window.wp = window.wp || {}; return this.validate( attachment, options ); }, - + /** + * @access private + * + * @param {wp.media.model.Attachments} attachments + * @param {Object} options + * @returns {wp.media.model.Attachments} Returns itself to allow chaining + */ _validateAllHandler: function( attachments, options ) { return this.validateAll( attachments, options ); }, - + /** + * Start mirroring another attachments collection, clearing out any models already + * in the collection. + * + * @param {wp.media.model.Attachments} The attachments collection to mirror. + * @returns {wp.media.model.Attachments} Returns itself to allow chaining + */ mirror: function( attachments ) { - if ( this.mirroring && this.mirroring === attachments ) + if ( this.mirroring && this.mirroring === attachments ) { return this; + } this.unmirror(); this.mirroring = attachments; @@ -448,23 +791,35 @@ window.wp = window.wp || {}; return this; }, - + /** + * Stop mirroring another attachments collection. + */ unmirror: function() { - if ( ! this.mirroring ) + if ( ! this.mirroring ) { return; + } this.unobserve( this.mirroring ); delete this.mirroring; }, - + /** + * Retrive more attachments from the server for the collection. + * + * Only works if the collection is mirroring a Query Attachments collection, + * and forwards to its `more` method. This collection class doesn't have + * server persistence by itself. + * + * @param {object} options + * @returns {Promise} + */ more: function( options ) { var deferred = $.Deferred(), mirroring = this.mirroring, attachments = this; - if ( ! mirroring || ! mirroring.more ) + if ( ! mirroring || ! mirroring.more ) { return deferred.resolveWith( this ).promise(); - + } // If we're mirroring another collection, forward `more` to // the mirrored collection. Account for a race condition by // checking if we're still mirroring that collection when @@ -476,14 +831,32 @@ window.wp = window.wp || {}; return deferred.promise(); }, - + /** + * Whether there are more attachments that haven't been sync'd from the server + * that match the collection's query. + * + * Only works if the collection is mirroring a Query Attachments collection, + * and forwards to its `hasMore` method. This collection class doesn't have + * server persistence by itself. + * + * @returns {boolean} + */ hasMore: function() { return this.mirroring ? this.mirroring.hasMore() : false; }, - + /** + * A custom AJAX-response parser. + * + * See trac ticket #24753 + * + * @param {Object|Array} resp The raw response Object/Array. + * @param {Object} xhr + * @returns {Array} The array of model attributes to be added to the collection + */ parse: function( resp, xhr ) { - if ( ! _.isArray( resp ) ) + if ( ! _.isArray( resp ) ) { resp = [resp]; + } return _.map( resp, function( attrs ) { var id, attachment, newAttributes; @@ -498,23 +871,36 @@ window.wp = window.wp || {}; attachment = Attachment.get( id ); newAttributes = attachment.parse( attrs, xhr ); - if ( ! _.isEqual( attachment.attributes, newAttributes ) ) + if ( ! _.isEqual( attachment.attributes, newAttributes ) ) { attachment.set( newAttributes ); + } return attachment; }); }, - - _requery: function() { - if ( this.props.get('query') ) - this.mirror( Query.get( this.props.toJSON() ) ); + /** + * If the collection is a query, create and mirror an Attachments Query collection. + * + * @access private + */ + _requery: function( refresh ) { + var props; + if ( this.props.get('query') ) { + props = this.props.toJSON(); + props.cache = ( true !== refresh ); + this.mirror( Query.get( props ) ); + } }, - - // If this collection is sorted by `menuOrder`, recalculates and saves - // the menu order to the database. + /** + * If this collection is sorted by `menuOrder`, recalculates and saves + * the menu order to the database. + * + * @returns {undefined|Promise} + */ saveMenuOrder: function() { - if ( 'menuOrder' !== this.props.get('orderby') ) + if ( 'menuOrder' !== this.props.get('orderby') ) { return; + } // Removes any uploading attachments, updates each attachment's // menu order, and returns an object with an { id: menuOrder } @@ -528,8 +914,9 @@ window.wp = window.wp || {}; return [ attachment.id, index ]; }).object().value(); - if ( _.isEmpty( attachments ) ) + if ( _.isEmpty( attachments ) ) { return; + } return media.post( 'save-attachment-order', { nonce: media.model.settings.post.nonce, @@ -538,6 +925,21 @@ window.wp = window.wp || {}; }); } }, { + /** + * A function to compare two attachment models in an attachments collection. + * + * Used as the default comparator for instances of wp.media.model.Attachments + * and its subclasses. @see wp.media.model.Attachments._changeOrderby(). + * + * @static + * + * @param {Backbone.Model} a + * @param {Backbone.Model} b + * @param {Object} options + * @returns {Number} -1 if the first model should come before the second, + * 0 if they are of the same rank and + * 1 if the first model should come after. + */ comparator: function( a, b, options ) { var key = this.props.get('orderby'), order = this.props.get('order') || 'DESC', @@ -553,44 +955,99 @@ window.wp = window.wp || {}; } // If `options.ties` is set, don't enforce the `cid` tiebreaker. - if ( options && options.ties ) + if ( options && options.ties ) { ac = bc = null; + } return ( 'DESC' === order ) ? compare( a, b, ac, bc ) : compare( b, a, bc, ac ); }, - + /** + * @namespace + */ filters: { - // Note that this client-side searching is *not* equivalent - // to our server-side searching. + /** + * @static + * Note that this client-side searching is *not* equivalent + * to our server-side searching. + * + * @param {wp.media.model.Attachment} attachment + * + * @this wp.media.model.Attachments + * + * @returns {Boolean} + */ search: function( attachment ) { - if ( ! this.props.get('search') ) + if ( ! this.props.get('search') ) { return true; + } return _.any(['title','filename','description','caption','name'], function( key ) { var value = attachment.get( key ); return value && -1 !== value.search( this.props.get('search') ); }, this ); }, - + /** + * @static + * @param {wp.media.model.Attachment} attachment + * + * @this wp.media.model.Attachments + * + * @returns {Boolean} + */ type: function( attachment ) { var type = this.props.get('type'); return ! type || -1 !== type.indexOf( attachment.get('type') ); }, - + /** + * @static + * @param {wp.media.model.Attachment} attachment + * + * @this wp.media.model.Attachments + * + * @returns {Boolean} + */ uploadedTo: function( attachment ) { var uploadedTo = this.props.get('uploadedTo'); - if ( _.isUndefined( uploadedTo ) ) + if ( _.isUndefined( uploadedTo ) ) { return true; + } return uploadedTo === attachment.get('uploadedTo'); + }, + /** + * @static + * @param {wp.media.model.Attachment} attachment + * + * @this wp.media.model.Attachments + * + * @returns {Boolean} + */ + status: function( attachment ) { + var status = this.props.get('status'); + if ( _.isUndefined( status ) ) { + return true; + } + + return status === attachment.get('status'); } } }); + /** + * A collection of all attachments that have been fetched from the server. + * + * @static + * @member {wp.media.model.Attachments} + */ Attachments.all = new Attachments(); /** * wp.media.query + * + * Shorthand for creating a new Attachments Query. + * + * @param {object} [props] + * @returns {wp.media.model.Attachments} */ media.query = function( props ) { return new Attachments( null, { @@ -601,13 +1058,27 @@ window.wp = window.wp || {}; /** * wp.media.model.Query * - * A set of attachments that corresponds to a set of consecutively paged - * queries on the server. + * A collection of attachments that match the supplied query arguments. * * Note: Do NOT change this.args after the query has been initialized. * Things will break. + * + * @class + * @augments wp.media.model.Attachments + * @augments Backbone.Collection + * + * @param {array} [models] Models to initialize with the collection. + * @param {object} [options] Options hash. + * @param {object} [options.args] Attachments query arguments. + * @param {object} [options.args.posts_per_page] */ Query = media.model.Query = Attachments.extend({ + /** + * @global wp.Uploader + * + * @param {array} [models=[]] Array of initial models to populate the collection. + * @param {object} [options={}] + */ initialize: function( models, options ) { var allowed; @@ -622,8 +1093,9 @@ window.wp = window.wp || {}; var orderby = this.props.get('orderby'), order = this.props.get('order'); - if ( ! this.comparator ) + if ( ! this.comparator ) { return true; + } // We want any items that can be placed before the last // item in the set. If we add any items after the last @@ -654,34 +1126,57 @@ window.wp = window.wp || {}; // are no filters for other properties, so observing will result in // false positives in those queries. allowed = [ 's', 'order', 'orderby', 'posts_per_page', 'post_mime_type', 'post_parent' ]; - if ( wp.Uploader && _( this.args ).chain().keys().difference( allowed ).isEmpty().value() ) + if ( wp.Uploader && _( this.args ).chain().keys().difference( allowed ).isEmpty().value() ) { this.observe( wp.Uploader.queue ); + } }, - + /** + * Whether there are more attachments that haven't been sync'd from the server + * that match the collection's query. + * + * @returns {boolean} + */ hasMore: function() { return this._hasMore; }, - + /** + * Fetch more attachments from the server for the collection. + * + * @param {object} [options={}] + * @returns {Promise} + */ more: function( options ) { var query = this; - if ( this._more && 'pending' === this._more.state() ) + // If there is already a request pending, return early with the Deferred object. + if ( this._more && 'pending' === this._more.state() ) { return this._more; + } - if ( ! this.hasMore() ) + if ( ! this.hasMore() ) { return $.Deferred().resolveWith( this ).promise(); + } options = options || {}; options.remove = false; return this._more = this.fetch( options ).done( function( resp ) { - if ( _.isEmpty( resp ) || -1 === this.args.posts_per_page || resp.length < this.args.posts_per_page ) + if ( _.isEmpty( resp ) || -1 === this.args.posts_per_page || resp.length < this.args.posts_per_page ) { query._hasMore = false; + } }); }, - + /** + * Overrides Backbone.Collection.sync + * Overrides wp.media.model.Attachments.sync + * + * @param {String} method + * @param {Backbone.Model} model + * @param {Object} [options={}] + * @returns {Promise} + */ sync: function( method, model, options ) { - var fallback; + var args, fallback; // Overload the read method so Attachment.fetch() functions correctly. if ( 'read' === method ) { @@ -696,76 +1191,138 @@ window.wp = window.wp || {}; args = _.clone( this.args ); // Determine which page to query. - if ( -1 !== args.posts_per_page ) + if ( -1 !== args.posts_per_page ) { args.paged = Math.floor( this.length / args.posts_per_page ) + 1; + } options.data.query = args; return media.ajax( options ); // Otherwise, fall back to Backbone.sync() } else { + /** + * Call wp.media.model.Attachments.sync or Backbone.sync + */ fallback = Attachments.prototype.sync ? Attachments.prototype : Backbone; return fallback.sync.apply( this, arguments ); } } }, { + /** + * @readonly + */ defaultProps: { orderby: 'date', order: 'DESC' }, - + /** + * @readonly + */ defaultArgs: { posts_per_page: 40 }, - + /** + * @readonly + */ orderby: { allowed: [ 'name', 'author', 'date', 'title', 'modified', 'uploadedTo', 'id', 'post__in', 'menuOrder' ], + /** + * A map of JavaScript orderby values to their WP_Query equivalents. + * @type {Object} + */ valuemap: { 'id': 'ID', 'uploadedTo': 'parent', 'menuOrder': 'menu_order ID' } }, - + /** + * A map of JavaScript query properties to their WP_Query equivalents. + * + * @readonly + */ propmap: { 'search': 's', 'type': 'post_mime_type', 'perPage': 'posts_per_page', 'menuOrder': 'menu_order', - 'uploadedTo': 'post_parent' + 'uploadedTo': 'post_parent', + 'status': 'post_status', + 'include': 'post__in', + 'exclude': 'post__not_in' }, - - // Caches query objects so queries can be easily reused. + /** + * Creates and returns an Attachments Query collection given the properties. + * + * Caches query objects and reuses where possible. + * + * @static + * @method + * + * @param {object} [props] + * @param {Object} [props.cache=true] Whether to use the query cache or not. + * @param {Object} [props.order] + * @param {Object} [props.orderby] + * @param {Object} [props.include] + * @param {Object} [props.exclude] + * @param {Object} [props.s] + * @param {Object} [props.post_mime_type] + * @param {Object} [props.posts_per_page] + * @param {Object} [props.menu_order] + * @param {Object} [props.post_parent] + * @param {Object} [props.post_status] + * @param {Object} [options] + * + * @returns {wp.media.model.Query} A new Attachments Query collection. + */ get: (function(){ + /** + * @static + * @type Array + */ var queries = []; + /** + * @returns {Query} + */ return function( props, options ) { var args = {}, orderby = Query.orderby, defaults = Query.defaultProps, - query; + query, + cache = !! props.cache || _.isUndefined( props.cache ); // Remove the `query` property. This isn't linked to a query, // this *is* the query. delete props.query; + delete props.cache; // Fill default args. _.defaults( props, defaults ); // Normalize the order. props.order = props.order.toUpperCase(); - if ( 'DESC' !== props.order && 'ASC' !== props.order ) + if ( 'DESC' !== props.order && 'ASC' !== props.order ) { props.order = defaults.order.toUpperCase(); + } // Ensure we have a valid orderby value. - if ( ! _.contains( orderby.allowed, props.orderby ) ) + if ( ! _.contains( orderby.allowed, props.orderby ) ) { props.orderby = defaults.orderby; + } + + _.each( [ 'include', 'exclude' ], function( prop ) { + if ( props[ prop ] && ! _.isArray( props[ prop ] ) ) { + props[ prop ] = [ props[ prop ] ]; + } + } ); // Generate the query `args` object. // Correct any differing property names. _.each( props, function( value, prop ) { - if ( _.isNull( value ) ) + if ( _.isNull( value ) ) { return; + } args[ Query.propmap[ prop ] || prop ] = value; }); @@ -777,10 +1334,14 @@ window.wp = window.wp || {}; // Substitute exceptions specified in orderby.keymap. args.orderby = orderby.valuemap[ props.orderby ] || props.orderby; - // Search the query cache for matches. - query = _.find( queries, function( query ) { - return _.isEqual( query.args, args ); - }); + // Search the query cache for a matching query. + if ( cache ) { + query = _.find( queries, function( query ) { + return _.isEqual( query.args, args ); + }); + } else { + queries = []; + } // Otherwise, create a new query and add it to the cache. if ( ! query ) { @@ -799,39 +1360,70 @@ window.wp = window.wp || {}; /** * wp.media.model.Selection * - * Used to manage a selection of attachments in the views. + * A selection of attachments. + * + * @class + * @augments wp.media.model.Attachments + * @augments Backbone.Collection */ media.model.Selection = Attachments.extend({ + /** + * Refresh the `single` model whenever the selection changes. + * Binds `single` instead of using the context argument to ensure + * it receives no parameters. + * + * @param {Array} [models=[]] Array of models used to populate the collection. + * @param {Object} [options={}] + */ initialize: function( models, options ) { + /** + * call 'initialize' directly on the parent class + */ Attachments.prototype.initialize.apply( this, arguments ); this.multiple = options && options.multiple; - // Refresh the `single` model whenever the selection changes. - // Binds `single` instead of using the context argument to ensure - // it receives no parameters. this.on( 'add remove reset', _.bind( this.single, this, false ) ); }, - // Override the selection's add method. - // If the workflow does not support multiple - // selected attachments, reset the selection. + /** + * If the workflow does not support multi-select, clear out the selection + * before adding a new attachment to it. + * + * @param {Array} models + * @param {Object} options + * @returns {wp.media.model.Attachment[]} + */ add: function( models, options ) { - if ( ! this.multiple ) + if ( ! this.multiple ) { this.remove( this.models ); - + } + /** + * call 'add' directly on the parent class + */ return Attachments.prototype.add.call( this, models, options ); }, + /** + * Fired when toggling (clicking on) an attachment in the modal. + * + * @param {undefined|boolean|wp.media.model.Attachment} model + * + * @fires wp.media.model.Selection#selection:single + * @fires wp.media.model.Selection#selection:unsingle + * + * @returns {Backbone.Model} + */ single: function( model ) { var previous = this._single; // If a `model` is provided, use it as the single model. - if ( model ) + if ( model ) { this._single = model; - + } // If the single model isn't in the selection, remove it. - if ( this._single && ! this.get( this._single.cid ) ) + if ( this._single && ! this.get( this._single.cid ) ) { delete this._single; + } this._single = this._single || this.last(); @@ -842,11 +1434,13 @@ window.wp = window.wp || {}; // If the model was already removed, trigger the collection // event manually. - if ( ! this.get( previous.cid ) ) + if ( ! this.get( previous.cid ) ) { this.trigger( 'selection:unsingle', previous, this ); + } } - if ( this._single ) + if ( this._single ) { this._single.trigger( 'selection:single', this._single, this ); + } } // Return the single model, or the last model as a fallback. @@ -859,4 +1453,4 @@ window.wp = window.wp || {}; window.wp = null; }); -}(jQuery)); \ No newline at end of file +}(jQuery));