X-Git-Url: https://scripts.mit.edu/gitweb/autoinstalls/wordpress.git/blobdiff_plain/48ab98cb1779cf2088c1351ac3dd3d0da6fb31d3..5e031ad59895b5682d1509675cafe9f2c5081c12:/wp-includes/js/media-models.js diff --git a/wp-includes/js/media-models.js b/wp-includes/js/media-models.js index ac7ee938..4bc7f61c 100644 --- a/wp-includes/js/media-models.js +++ b/wp-includes/js/media-models.js @@ -1,1354 +1,1503 @@ -/* global _wpMediaModelsL10n:false */ +(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o bc ? -1 : 1); + } else { + return a > b ? -1 : 1; + } +}; + +_.extend( media, { /** - * Create and return a media frame. + * media.template( id ) * - * Handles the default media experience. Automatically creates - * and opens a media frame, and returns the result. - * Does nothing if the controllers do not exist. + * Fetch a JavaScript template for an id, and return a templating function for it. * - * @param {object} attributes The properties passed to the main media controller. - * @return {wp.media.view.MediaFrame} A media workflow. + * See wp.template() in `wp-includes/js/wp-util.js`. + * + * @borrows wp.template as template */ - media = wp.media = function( attributes ) { - var MediaFrame = media.view.MediaFrame, - frame; - - if ( ! MediaFrame ) { - return; - } - - attributes = _.defaults( attributes || {}, { - frame: 'select' - }); - - if ( 'select' === attributes.frame && MediaFrame.Select ) { - frame = new MediaFrame.Select( attributes ); - } 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 ); - } + template: wp.template, - delete attributes.frame; - - media.frame = frame; - - return frame; - }; - - _.extend( media, { model: {}, view: {}, controller: {}, frames: {} }); - - // Link any localized strings. - l10n = media.model.l10n = typeof _wpMediaModelsL10n === 'undefined' ? {} : _wpMediaModelsL10n; - - // Link any settings. - media.model.settings = l10n.settings || {}; - delete l10n.settings; + /** + * media.post( [action], [data] ) + * + * 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, /** - * ======================================================================== - * UTILITIES - * ======================================================================== + * media.ajax( [action], [options] ) + * + * 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, /** - * A basic comparator. + * Scales a set of dimensions to fit within bounding dimensions. * - * @param {mixed} a The primary parameter to compare. - * @param {mixed} b The primary parameter to compare. - * @param {string} ac The fallback parameter to compare, a's cid. - * @param {string} bc The fallback parameter to compare, b's cid. - * @return {number} -1: a should come before b. - * 0: a and b are of the same rank. - * 1: b should come before a. + * @param {Object} dimensions + * @returns {Object} */ - compare = function( a, b, ac, bc ) { - if ( _.isEqual( a, b ) ) { - return ac === bc ? 0 : (ac > bc ? -1 : 1); - } else { - return a > b ? -1 : 1; + fit: function( dimensions ) { + var width = dimensions.width, + height = dimensions.height, + maxWidth = dimensions.maxWidth, + maxHeight = dimensions.maxHeight, + constraint; + + // Compare ratios between the two values to determine which + // max to constrain by. If a max value doesn't exist, then the + // opposite side is the constraint. + if ( ! _.isUndefined( maxWidth ) && ! _.isUndefined( maxHeight ) ) { + constraint = ( width / height > maxWidth / maxHeight ) ? 'width' : 'height'; + } else if ( _.isUndefined( maxHeight ) ) { + constraint = 'width'; + } else if ( _.isUndefined( maxWidth ) && height > maxHeight ) { + constraint = 'height'; } - }; - - _.extend( media, { - /** - * media.template( id ) - * - * Fetches a template by id. - * See wp.template() in `wp-includes/js/wp-util.js`. - * - * @borrows wp.template as template - */ - template: wp.template, - - /** - * media.post( [action], [data] ) - * - * 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, - - /** - * media.ajax( [action], [options] ) - * - * 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. - * - * @param {Object} dimensions - * @returns {Object} - */ - fit: function( dimensions ) { - var width = dimensions.width, - height = dimensions.height, - maxWidth = dimensions.maxWidth, - maxHeight = dimensions.maxHeight, - constraint; - - // Compare ratios between the two values to determine which - // max to constrain by. If a max value doesn't exist, then the - // opposite side is the constraint. - if ( ! _.isUndefined( maxWidth ) && ! _.isUndefined( maxHeight ) ) { - constraint = ( width / height > maxWidth / maxHeight ) ? 'width' : 'height'; - } else if ( _.isUndefined( maxHeight ) ) { - constraint = 'width'; - } else if ( _.isUndefined( maxWidth ) && height > maxHeight ) { - constraint = 'height'; - } - // If the value of the constrained side is larger than the max, - // then scale the values. Otherwise return the originals; they fit. - if ( 'width' === constraint && width > maxWidth ) { - return { - width : maxWidth, - height: Math.round( maxWidth * height / width ) - }; - } else if ( 'height' === constraint && height > maxHeight ) { - return { - width : Math.round( maxHeight * width / height ), - height: maxHeight - }; - } else { - return { - width : width, - height: height - }; - } - }, - /** - * 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 ) { - return string; - } - - return string.substr( 0, length / 2 ) + replacement + string.substr( -1 * length / 2 ); + // If the value of the constrained side is larger than the max, + // then scale the values. Otherwise return the originals; they fit. + if ( 'width' === constraint && width > maxWidth ) { + return { + width : maxWidth, + height: Math.round( maxWidth * height / width ) + }; + } else if ( 'height' === constraint && height > maxHeight ) { + return { + width : Math.round( maxHeight * width / height ), + height: maxHeight + }; + } else { + return { + width : width, + height: height + }; } - }); - + }, /** - * ======================================================================== - * MODELS - * ======================================================================== - */ - /** - * wp.media.attachment + * Truncates a string by injecting an ellipsis into the middle. + * Useful for filenames. * - * @static - * @param {String} id A string used to identify a model. - * @returns {wp.media.model.Attachment} + * @param {String} string + * @param {Number} [length=30] + * @param {String} [replacement=…] + * @returns {String} The string, unless length is greater than string.length. */ - media.attachment = function( id ) { - return Attachment.get( id ); - }; + truncate: function( string, length, replacement ) { + length = length || 30; + replacement = replacement || '…'; + + if ( string.length <= length ) { + return string; + } + return string.substr( 0, length / 2 ) + replacement + string.substr( -1 * length / 2 ); + } +}); + +/** + * ======================================================================== + * MODELS + * ======================================================================== + */ +/** + * 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 ); +}; + +/** + * 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, { + props: _.extend( _.defaults( props || {}, { orderby: 'date' } ), { query: true } ) + }); +}; + +// Clean up. Prevents mobile browsers caching +$(window).on('unload', function(){ + window.wp = null; +}); + +},{"./models/attachment.js":2,"./models/attachments.js":3,"./models/post-image.js":4,"./models/query.js":5,"./models/selection.js":6}],2:[function(require,module,exports){ +/*globals wp, _, Backbone */ + +/** + * wp.media.model.Attachment + * + * @class + * @augments Backbone.Model + */ +var $ = Backbone.$, + Attachment; + +Attachment = Backbone.Model.extend({ /** - * wp.media.model.Attachment + * Triggered when attachment details change + * Overrides Backbone.Model.sync + * + * @param {string} method + * @param {wp.media.model.Attachment} model + * @param {Object} [options={}] * - * @constructor - * @augments Backbone.Model + * @returns {Promise} */ - 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 ) ) { - return $.Deferred().rejectWith( this ).promise(); - } - - // Overload the `read` request so Attachment.fetch() functions correctly. - if ( 'read' === method ) { - options = options || {}; - options.context = this; - options.data = _.extend( options.data || {}, { - action: 'get-attachment', - id: this.id - }); - return media.ajax( options ); + 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 ) ) { + return $.Deferred().rejectWith( this ).promise(); + } - // 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 ) { - return $.Deferred().rejectWith( this ).promise(); - } + // Overload the `read` request so Attachment.fetch() functions correctly. + if ( 'read' === method ) { + options = options || {}; + options.context = this; + options.data = _.extend( options.data || {}, { + action: 'get-attachment', + id: this.id + }); + return wp.media.ajax( options ); - options = options || {}; - options.context = this; + // 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 ) { + return $.Deferred().rejectWith( this ).promise(); + } - // Set the action and ID. - options.data = _.extend( options.data || {}, { - action: 'save-attachment', - id: this.id, - nonce: this.get('nonces').update, - post_id: media.model.settings.post.id - }); + options = options || {}; + options.context = this; - // Record the values of the changed attributes. - if ( model.hasChanged() ) { - options.data.changes = {}; + // Set the action and ID. + options.data = _.extend( options.data || {}, { + action: 'save-attachment', + id: this.id, + nonce: this.get('nonces').update, + post_id: wp.media.model.settings.post.id + }); - _.each( model.changed, function( value, key ) { - options.data.changes[ key ] = this.get( key ); - }, this ); - } + // Record the values of the changed attributes. + if ( model.hasChanged() ) { + options.data.changes = {}; - return media.ajax( options ); + _.each( model.changed, function( value, key ) { + options.data.changes[ key ] = this.get( key ); + }, this ); + } - // Overload the `delete` request so attachments can be removed. - // This will permanently delete an attachment. - } else if ( 'delete' === method ) { - options = options || {}; + return wp.media.ajax( options ); - if ( ! options.wait ) { - this.destroyed = true; - } + // Overload the `delete` request so attachments can be removed. + // This will permanently delete an attachment. + } else if ( 'delete' === method ) { + options = options || {}; - options.context = this; - options.data = _.extend( options.data || {}, { - action: 'delete-post', - id: this.id, - _wpnonce: this.get('nonces')['delete'] - }); + if ( ! options.wait ) { + this.destroyed = true; + } - return media.ajax( options ).done( function() { - this.destroyed = true; - }).fail( function() { - this.destroyed = false; - }); + options.context = this; + options.data = _.extend( options.data || {}, { + action: 'delete-post', + id: this.id, + _wpnonce: this.get('nonces')['delete'] + }); - // Otherwise, fall back to `Backbone.sync()`. - } else { - /** - * Call `sync` directly on Backbone.Model - */ - return Backbone.Model.prototype.sync.apply( this, arguments ); - } - }, - /** - * 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; - } + return wp.media.ajax( options ).done( function() { + this.destroyed = true; + }).fail( function() { + this.destroyed = false; + }); - resp.date = new Date( resp.date ); - resp.modified = new Date( resp.modified ); + // Otherwise, fall back to `Backbone.sync()`. + } else { + /** + * Call `sync` directly on Backbone.Model + */ + return Backbone.Model.prototype.sync.apply( this, arguments ); + } + }, + /** + * 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; - }, - /** - * @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 ) { - return $.Deferred().rejectWith( this ).promise(); - } + 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; - return media.post( 'save-attachment-compat', _.defaults({ - id: this.id, - nonce: this.get('nonces').update, - post_id: media.model.settings.post.id - }, data ) ).done( function( resp, status, xhr ) { - model.set( model.parse( resp, xhr ), options ); - }); + // If we do not have the necessary nonce, fail immeditately. + if ( ! this.get('nonces') || ! this.get('nonces').update ) { + return $.Deferred().rejectWith( this ).promise(); } - }, { - /** - * Add a model to the end of the static 'all' collection and return it. - * - * @static - * @param {Object} attrs - * @returns {wp.media.model.Attachment} - */ - create: function( attrs ) { - return Attachments.all.push( attrs ); - }, - /** - * Retrieve a model, or add it to the end of the static 'all' collection before returning it. - * - * @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 } ); - }) - }); + return wp.media.post( 'save-attachment-compat', _.defaults({ + id: this.id, + nonce: this.get('nonces').update, + post_id: wp.media.model.settings.post.id + }, data ) ).done( function( resp, status, xhr ) { + model.set( model.parse( resp, xhr ), options ); + }); + } +}, { /** - * wp.media.model.PostImage + * Create a new model on the static 'all' attachments collection and return it. * - * @constructor - * @augments Backbone.Model - **/ - 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(); - } + * @static + * @param {Object} attrs + * @returns {wp.media.model.Attachment} + */ + create: function( attrs ) { + var Attachments = wp.media.model.Attachments; + 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 ) { + var Attachments = wp.media.model.Attachments; + return Attachments.all.push( attachment || { id: id } ); + }) +}); + +module.exports = Attachment; + +},{}],3:[function(require,module,exports){ +/*globals wp, _, Backbone */ + +/** + * 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] + * + */ +var Attachments = Backbone.Collection.extend({ + /** + * @type {wp.media.model.Attachment} + */ + model: wp.media.model.Attachment, + /** + * @param {Array} [models=[]] Array of models used to populate the collection. + * @param {Object} [options={}] + */ + initialize: function( models, options ) { + options = options || {}; - // 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.props = new Backbone.Model(); + this.filters = options.filters || {}; - this.setLinkTypeFromUrl(); - this.setAspectRatio(); + // Bind default `change` events to the `props` model. + this.props.on( 'change', this._changeFilteredProps, this ); - this.set( 'originalUrl', attributes.url ); - }, + this.props.on( 'change:order', this._changeOrder, this ); + this.props.on( 'change:orderby', this._changeOrderby, this ); + this.props.on( 'change:query', this._changeQuery, this ); - bindAttachmentListeners: function() { - this.listenTo( this.attachment, 'sync', this.setLinkTypeFromUrl ); - this.listenTo( this.attachment, 'sync', this.setAspectRatio ); - this.listenTo( this.attachment, 'change', this.updateSize ); - }, + this.props.set( _.defaults( options.props || {} ) ); - changeAttachment: function( attachment, props ) { - this.stopListening( this.attachment ); - this.attachment = attachment; - this.bindAttachmentListeners(); + if ( options.observe ) { + this.observe( options.observe ); + } + }, + /** + * 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. + * + * @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 ) { + return; + } - 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(); - }, + if ( orderby && 'post__in' !== orderby ) { + this.comparator = Attachments.comparator; + } 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. + * + * @access private + * + * @param {Backbone.Model} model + * @param {Boolean} query + */ + _changeQuery: function( model, query ) { + if ( query ) { + this.props.on( 'change', this._requery, this ); + this._requery(); + } else { + this.props.off( 'change', this._requery, this ); + } + }, + /** + * @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') ) { + return; + } - setLinkTypeFromUrl: function() { - var linkUrl = this.get( 'linkUrl' ), - type; + var changed = _.chain( model.changed ).map( function( t, prop ) { + var filter = Attachments.filters[ prop ], + term = model.get( prop ); - if ( ! linkUrl ) { - this.set( 'link', 'none' ); + if ( ! filter ) { 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'; - } + if ( term && ! this.filters[ prop ] ) { + this.filters[ prop ] = filter; + } else if ( ! term && this.filters[ prop ] === filter ) { + delete this.filters[ prop ]; } 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; + return; } - }, - updateSize: function() { - var size; + // Record the change. + return true; + }, this ).any().value(); - if ( ! this.attachment ) { - return; - } + if ( ! changed ) { + 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; - } + // If no `Attachments` model is provided to source the searches + // from, then automatically generate a source from the existing + // models. + if ( ! this._source ) { + this._source = new Attachments( this.models ); + } - size = this.attachment.get( 'sizes' )[ this.get( 'size' ) ]; + this.reset( this._source.filter( this.validator, this ) ); + }, - if ( ! size ) { - return; - } + validateDestroyed: false, + /** + * Checks whether an attachment is valid. + * + * @param {wp.media.model.Attachment} attachment + * @returns {Boolean} + */ + validator: function( attachment ) { + if ( ! this.validateDestroyed && attachment.destroyed ) { + return false; + } + 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 ) { + this.remove( attachment, options ); + } else if ( valid && ! hasAttachment ) { + this.add( attachment, options ); + } - this.set( 'url', size.url ); - this.set( 'width', size.width ); - this.set( 'height', size.height ); - }, + return this; + }, - setAspectRatio: function() { - var full; + /** + * 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 || {}; - if ( this.attachment && this.attachment.get( 'sizes' ) ) { - full = this.attachment.get( 'sizes' ).full; + _.each( attachments.models, function( attachment ) { + this.validate( attachment, { silent: true }); + }, this ); - if ( full ) { - this.set( 'aspectRatio', full.width / full.height ); - return; - } - } + 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 ); + + attachments.on( 'add change remove', this._validateHandler, this ); + attachments.on( 'reset', this._validateAllHandler, this ); + 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 ); + this.observers = _.without( this.observers, attachments ); - this.set( 'aspectRatio', this.get( 'customWidth' ) / this.get( 'customHeight' ) ); + } else { + _.each( this.observers, function( attachments ) { + attachments.off( null, null, this ); + }, this ); + delete this.observers; } - }); + 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. + options = attachments === this.mirroring ? options : { + silent: options && options.silent + }; + + return this.validate( attachment, options ); + }, /** - * wp.media.model.Attachments + * @access private * - * @constructor - * @augments Backbone.Collection + * @param {wp.media.model.Attachments} attachments + * @param {Object} options + * @returns {wp.media.model.Attachments} Returns itself to allow chaining */ - 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 || {}; - - this.props = new Backbone.Model(); - this.filters = options.filters || {}; + _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 ) { + return this; + } - // Bind default `change` events to the `props` model. - this.props.on( 'change', this._changeFilteredProps, this ); + this.unmirror(); + this.mirroring = attachments; - this.props.on( 'change:order', this._changeOrder, this ); - this.props.on( 'change:orderby', this._changeOrderby, this ); - this.props.on( 'change:query', this._changeQuery, this ); + // Clear the collection silently. A `reset` event will be fired + // when `observe()` calls `validateAll()`. + this.reset( [], { silent: true } ); + this.observe( attachments ); - // Set the `props` model and fill the default property values. - this.props.set( _.defaults( options.props || {} ) ); + return this; + }, + /** + * Stop mirroring another attachments collection. + */ + unmirror: function() { + if ( ! this.mirroring ) { + return; + } - // Observe another `Attachments` collection if one is provided. - if ( options.observe ) { - this.observe( options.observe ); - } - }, - /** - * Automatically sort the collection when the order changes. - * - * @access private - */ - _changeOrder: function() { - if ( this.comparator ) { - this.sort(); - } - }, - /** - * 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 ) { - 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 = jQuery.Deferred(), + mirroring = this.mirroring, + attachments = this; - if ( orderby && 'post__in' !== orderby ) { - this.comparator = Attachments.comparator; - } 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. - * - * @access private - * - * @param {Backbone.Model} model - * @param {Boolean} query - */ - _changeQuery: function( model, query ) { - if ( query ) { - this.props.on( 'change', this._requery, this ); - this._requery(); - } else { - this.props.off( 'change', this._requery, this ); - } - }, - /** - * @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') ) { - return; + 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 + // the request resolves. + mirroring.more( options ).done( function() { + if ( this === attachments.mirroring ) { + deferred.resolveWith( this ); } + }); - var changed = _.chain( model.changed ).map( function( t, prop ) { - var filter = Attachments.filters[ prop ], - term = model.get( prop ); + 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 ) ) { + resp = [resp]; + } - if ( ! filter ) { - return; - } + return _.map( resp, function( attrs ) { + var id, attachment, newAttributes; - if ( term && ! this.filters[ prop ] ) { - this.filters[ prop ] = filter; - } else if ( ! term && this.filters[ prop ] === filter ) { - delete this.filters[ prop ]; - } else { - return; - } + if ( attrs instanceof Backbone.Model ) { + id = attrs.get( 'id' ); + attrs = attrs.attributes; + } else { + id = attrs.id; + } - // Record the change. - return true; - }, this ).any().value(); + attachment = wp.media.model.Attachment.get( id ); + newAttributes = attachment.parse( attrs, xhr ); - if ( ! changed ) { - return; + if ( ! _.isEqual( attachment.attributes, newAttributes ) ) { + attachment.set( newAttributes ); } - // If no `Attachments` model is provided to source the searches - // from, then automatically generate a source from the existing - // models. - if ( ! this._source ) { - this._source = new Attachments( this.models ); - } + return attachment; + }); + }, + /** + * 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( wp.media.model.Query.get( props ) ); + } + }, + /** + * 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') ) { + return; + } - this.reset( this._source.filter( this.validator, this ) ); - }, + // Removes any uploading attachments, updates each attachment's + // menu order, and returns an object with an { id: menuOrder } + // mapping to pass to the request. + var attachments = this.chain().filter( function( attachment ) { + return ! _.isUndefined( attachment.id ); + }).map( function( attachment, index ) { + // Indices start at 1. + index = index + 1; + attachment.set( 'menuOrder', index ); + return [ attachment.id, index ]; + }).object().value(); + + if ( _.isEmpty( attachments ) ) { + return; + } - validateDestroyed: false, - /** - * @param {wp.media.model.Attachment} attachment - * @returns {Boolean} - */ - validator: function( attachment ) { - if ( ! this.validateDestroyed && attachment.destroyed ) { - return false; - } - return _.all( this.filters, function( filter ) { - return !! filter.call( this, attachment ); - }, this ); - }, - /** - * @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 ) { - this.remove( attachment, options ); - } else if ( valid && ! hasAttachment ) { - this.add( attachment, options ); - } + return wp.media.post( 'save-attachment-order', { + nonce: wp.media.model.settings.post.nonce, + post_id: wp.media.model.settings.post.id, + attachments: attachments + }); + } +}, { + /** + * 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', + ac = a.cid, + bc = b.cid; + + a = a.get( key ); + b = b.get( key ); + + if ( 'date' === key || 'modified' === key ) { + a = a || new Date(); + b = b || new Date(); + } - return this; - }, + // If `options.ties` is set, don't enforce the `cid` tiebreaker. + if ( options && options.ties ) { + ac = bc = null; + } + return ( 'DESC' === order ) ? wp.media.compare( a, b, ac, bc ) : wp.media.compare( b, a, bc, ac ); + }, + /** + * @namespace + */ + filters: { /** - * @param {wp.media.model.Attachments} attachments - * @param {object} [options={}] + * @static + * Note that this client-side searching is *not* equivalent + * to our server-side searching. * - * @fires wp.media.model.Attachments#reset + * @param {wp.media.model.Attachment} attachment + * + * @this wp.media.model.Attachments * - * @returns {wp.media.model.Attachments} Returns itself to allow chaining + * @returns {Boolean} */ - validateAll: function( attachments, options ) { - options = options || {}; + search: function( attachment ) { + if ( ! this.props.get('search') ) { + return true; + } - _.each( attachments.models, function( attachment ) { - this.validate( attachment, { silent: 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 ); - - if ( ! options.silent ) { - this.trigger( 'reset', this, options ); - } - return this; }, /** - * @param {wp.media.model.Attachments} attachments - * @returns {wp.media.model.Attachments} Returns itself to allow chaining + * @static + * @param {wp.media.model.Attachment} attachment + * + * @this wp.media.model.Attachments + * + * @returns {Boolean} */ - observe: function( attachments ) { - this.observers = this.observers || []; - this.observers.push( attachments ); - - attachments.on( 'add change remove', this._validateHandler, this ); - attachments.on( 'reset', this._validateAllHandler, this ); - this.validateAll( attachments ); - return this; + type: function( attachment ) { + var type = this.props.get('type'); + return ! type || -1 !== type.indexOf( attachment.get('type') ); }, /** - * @param {wp.media.model.Attachments} attachments - * @returns {wp.media.model.Attachments} Returns itself to allow chaining + * @static + * @param {wp.media.model.Attachment} attachment + * + * @this wp.media.model.Attachments + * + * @returns {Boolean} */ - unobserve: function( attachments ) { - if ( attachments ) { - attachments.off( null, null, this ); - this.observers = _.without( this.observers, attachments ); - - } else { - _.each( this.observers, function( attachments ) { - attachments.off( null, null, this ); - }, this ); - delete this.observers; + uploadedTo: function( attachment ) { + var uploadedTo = this.props.get('uploadedTo'); + if ( _.isUndefined( uploadedTo ) ) { + return true; } - return this; + return uploadedTo === attachment.get('uploadedTo'); }, /** - * @access private + * @static + * @param {wp.media.model.Attachment} attachment * - * @param {wp.media.model.Attachments} attachment - * @param {wp.media.model.Attachments} attachments - * @param {Object} options + * @this wp.media.model.Attachments * - * @returns {wp.media.model.Attachments} Returns itself to allow chaining + * @returns {Boolean} */ - _validateHandler: function( attachment, attachments, options ) { - // If we're not mirroring this `attachments` collection, - // only retain the `silent` option. - options = attachments === this.mirroring ? options : { - silent: options && options.silent - }; + status: function( attachment ) { + var status = this.props.get('status'); + if ( _.isUndefined( status ) ) { + return true; + } - 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 ); - }, - /** - * @param {wp.media.model.Attachments} attachments - * @returns {wp.media.model.Attachments} Returns itself to allow chaining - */ - mirror: function( attachments ) { - if ( this.mirroring && this.mirroring === attachments ) { - return this; + return status === attachment.get('status'); + } + } +}); + +module.exports = Attachments; + +},{}],4:[function(require,module,exports){ +/*globals Backbone */ + +/** + * 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. + **/ +var PostImage = Backbone.Model.extend({ + + initialize: function( attributes ) { + var Attachment = wp.media.model.Attachment; + this.attachment = false; + + if ( attributes.attachment_id ) { + this.attachment = Attachment.get( attributes.attachment_id ); + if ( this.attachment.get( 'url' ) ) { + this.dfd = jQuery.Deferred(); + this.dfd.resolve(); + } else { + this.dfd = this.attachment.fetch(); } + this.bindAttachmentListeners(); + } - this.unmirror(); - this.mirroring = attachments; + // 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; + } - // Clear the collection silently. A `reset` event will be fired - // when `observe()` calls `validateAll()`. - this.reset( [], { silent: true } ); - this.observe( attachments ); + // default to custom if there is a linkUrl + type = 'custom'; - return this; - }, - unmirror: function() { - if ( ! this.mirroring ) { - return; + if ( this.attachment ) { + if ( this.attachment.get( 'url' ) === linkUrl ) { + type = 'file'; + } else if ( this.attachment.get( 'link' ) === linkUrl ) { + type = 'post'; } - - this.unobserve( this.mirroring ); - delete this.mirroring; - }, - /** - * @param {Object} options - * @returns {Promise} - */ - more: function( options ) { - var deferred = $.Deferred(), - mirroring = this.mirroring, - attachments = this; - - if ( ! mirroring || ! mirroring.more ) { - return deferred.resolveWith( this ).promise(); + } else { + if ( this.get( 'url' ) === linkUrl ) { + type = 'file'; } - // 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 - // the request resolves. - mirroring.more( options ).done( function() { - if ( this === attachments.mirroring ) - deferred.resolveWith( this ); - }); + } - return deferred.promise(); - }, - /** - * @returns {Boolean} - */ - hasMore: function() { - return this.mirroring ? this.mirroring.hasMore() : false; - }, - /** - * Overrides Backbone.Collection.parse - * - * @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 ) ) { - resp = [resp]; - } + this.set( 'link', type ); + }, - return _.map( resp, function( attrs ) { - var id, attachment, newAttributes; + updateLinkUrl: function() { + var link = this.get( 'link' ), + url; - if ( attrs instanceof Backbone.Model ) { - id = attrs.get( 'id' ); - attrs = attrs.attributes; + switch( link ) { + case 'file': + if ( this.attachment ) { + url = this.attachment.get( 'url' ); } else { - id = attrs.id; + 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; + } + }, - attachment = Attachment.get( id ); - newAttributes = attachment.parse( attrs, xhr ); + updateSize: function() { + var size; - if ( ! _.isEqual( attachment.attributes, newAttributes ) ) { - attachment.set( newAttributes ); - } + if ( ! this.attachment ) { + return; + } - return attachment; - }); - }, - /** - * @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. - * - * @returns {undefined|Promise} - */ - saveMenuOrder: function() { - if ( 'menuOrder' !== this.props.get('orderby') ) { - 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; + } - // Removes any uploading attachments, updates each attachment's - // menu order, and returns an object with an { id: menuOrder } - // mapping to pass to the request. - var attachments = this.chain().filter( function( attachment ) { - return ! _.isUndefined( attachment.id ); - }).map( function( attachment, index ) { - // Indices start at 1. - index = index + 1; - attachment.set( 'menuOrder', index ); - return [ attachment.id, index ]; - }).object().value(); - - if ( _.isEmpty( attachments ) ) { - return; - } + size = this.attachment.get( 'sizes' )[ this.get( 'size' ) ]; - return media.post( 'save-attachment-order', { - nonce: media.model.settings.post.nonce, - post_id: media.model.settings.post.id, - attachments: attachments - }); + if ( ! size ) { + return; } - }, { - /** - * @static - * Overrides Backbone.Collection.comparator - * - * @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', - ac = a.cid, - bc = b.cid; - - a = a.get( key ); - b = b.get( key ); - - if ( 'date' === key || 'modified' === key ) { - a = a || new Date(); - b = b || new Date(); - } - // If `options.ties` is set, don't enforce the `cid` tiebreaker. - if ( options && options.ties ) { - ac = bc = null; - } - - return ( 'DESC' === order ) ? compare( a, b, ac, bc ) : compare( b, a, bc, ac ); - }, - /** - * @namespace - */ - filters: { - /** - * @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') ) { - return true; - } + this.set( 'url', size.url ); + this.set( 'width', size.width ); + this.set( 'height', size.height ); + }, - 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 ) ) { - return true; - } + setAspectRatio: function() { + var full; - 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; - } + if ( this.attachment && this.attachment.get( 'sizes' ) ) { + full = this.attachment.get( 'sizes' ).full; - return status === attachment.get('status'); + if ( full ) { + this.set( 'aspectRatio', full.width / full.height ); + return; } } - }); - - /** - * @static - * @member {wp.media.model.Attachments} - */ - Attachments.all = new Attachments(); + this.set( 'aspectRatio', this.get( 'customWidth' ) / this.get( 'customHeight' ) ); + } +}); + +module.exports = PostImage; + +},{}],5:[function(require,module,exports){ +/*globals wp, _ */ + +/** + * wp.media.model.Query + * + * 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] + */ +var Attachments = wp.media.model.Attachments, + Query; + +Query = Attachments.extend({ /** - * wp.media.query + * @global wp.Uploader * - * @static - * @returns {wp.media.model.Attachments} + * @param {array} [models=[]] Array of initial models to populate the collection. + * @param {object} [options={}] */ - media.query = function( props ) { - return new Attachments( null, { - props: _.extend( _.defaults( props || {}, { orderby: 'date' } ), { query: true } ) - }); - }; + initialize: function( models, options ) { + var allowed; - /** - * wp.media.model.Query - * - * A set of attachments that corresponds to a set of consecutively paged - * queries on the server. - * - * Note: Do NOT change this.args after the query has been initialized. - * Things will break. - * - * @constructor - * @augments wp.media.model.Attachments - * @augments Backbone.Collection - */ - Query = media.model.Query = Attachments.extend({ - /** - * @global wp.Uploader - * - * @param {Array} [models=[]] Array of models used to populate the collection. - * @param {Object} [options={}] - */ - initialize: function( models, options ) { - var allowed; + options = options || {}; + Attachments.prototype.initialize.apply( this, arguments ); - options = options || {}; - Attachments.prototype.initialize.apply( this, arguments ); + this.args = options.args; + this._hasMore = true; + this.created = new Date(); - this.args = options.args; - this._hasMore = true; - this.created = new Date(); + this.filters.order = function( attachment ) { + var orderby = this.props.get('orderby'), + order = this.props.get('order'); - this.filters.order = function( attachment ) { - var orderby = this.props.get('orderby'), - order = this.props.get('order'); + if ( ! this.comparator ) { + return true; + } - 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 + // item, then we can't guarantee the set is complete. + if ( this.length ) { + return 1 !== this.comparator( attachment, this.last(), { ties: true }); + + // Handle the case where there are no items yet and + // we're sorting for recent items. In that case, we want + // changes that occurred after we created the query. + } else if ( 'DESC' === order && ( 'date' === orderby || 'modified' === orderby ) ) { + return attachment.get( orderby ) >= this.created; + + // If we're sorting by menu order and we have no items, + // accept any items that have the default menu order (0). + } else if ( 'ASC' === order && 'menuOrder' === orderby ) { + return attachment.get( orderby ) === 0; + } - // We want any items that can be placed before the last - // item in the set. If we add any items after the last - // item, then we can't guarantee the set is complete. - if ( this.length ) { - return 1 !== this.comparator( attachment, this.last(), { ties: true }); - - // Handle the case where there are no items yet and - // we're sorting for recent items. In that case, we want - // changes that occurred after we created the query. - } else if ( 'DESC' === order && ( 'date' === orderby || 'modified' === orderby ) ) { - return attachment.get( orderby ) >= this.created; - - // If we're sorting by menu order and we have no items, - // accept any items that have the default menu order (0). - } else if ( 'ASC' === order && 'menuOrder' === orderby ) { - return attachment.get( orderby ) === 0; - } + // Otherwise, we don't want any items yet. + return false; + }; + + // Observe the central `wp.Uploader.queue` collection to watch for + // new matches for the query. + // + // Only observe when a limited number of query args are set. There + // 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() ) { + 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; - // Otherwise, we don't want any items yet. - return false; - }; + // If there is already a request pending, return early with the Deferred object. + if ( this._more && 'pending' === this._more.state() ) { + return this._more; + } - // Observe the central `wp.Uploader.queue` collection to watch for - // new matches for the query. - // - // Only observe when a limited number of query args are set. There - // 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() ) { - this.observe( wp.Uploader.queue ); - } - }, - /** - * @returns {Boolean} - */ - hasMore: function() { - return this._hasMore; - }, - /** - * @param {Object} [options={}] - * @returns {Promise} - */ - more: function( options ) { - var query = this; + if ( ! this.hasMore() ) { + return jQuery.Deferred().resolveWith( this ).promise(); + } - if ( this._more && 'pending' === this._more.state() ) { - return this._more; - } + options = options || {}; + options.remove = false; - if ( ! this.hasMore() ) { - return $.Deferred().resolveWith( this ).promise(); + 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 ) { + 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 args, fallback; + // Overload the read method so Attachment.fetch() functions correctly. + if ( 'read' === method ) { 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 ) { - query._hasMore = false; - } + options.context = this; + options.data = _.extend( options.data || {}, { + action: 'query-attachments', + post_id: wp.media.model.settings.post.id }); - }, - /** - * 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 args, fallback; - - // Overload the read method so Attachment.fetch() functions correctly. - if ( 'read' === method ) { - options = options || {}; - options.context = this; - options.data = _.extend( options.data || {}, { - action: 'query-attachments', - post_id: media.model.settings.post.id - }); - // Clone the args so manipulation is non-destructive. - args = _.clone( this.args ); + // Clone the args so manipulation is non-destructive. + args = _.clone( this.args ); - // Determine which page to query. - if ( -1 !== args.posts_per_page ) { - args.paged = Math.floor( this.length / args.posts_per_page ) + 1; - } + // Determine which page to query. + if ( -1 !== args.posts_per_page ) { + args.paged = Math.round( this.length / args.posts_per_page ) + 1; + } - options.data.query = args; - return media.ajax( options ); + options.data.query = args; + return wp.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 ); - } + // 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' ], /** - * @readonly + * A map of JavaScript orderby values to their WP_Query equivalents. + * @type {Object} */ - defaultProps: { - orderby: 'date', - order: 'DESC' - }, + 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', + 'status': 'post_status', + 'include': 'post__in', + 'exclude': 'post__not_in' + }, + /** + * 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(){ /** - * @readonly + * @static + * @type Array */ - defaultArgs: { - posts_per_page: 40 - }, + var queries = []; + /** - * @readonly + * @returns {Query} */ - orderby: { - allowed: [ 'name', 'author', 'date', 'title', 'modified', 'uploadedTo', 'id', 'post__in', 'menuOrder' ], - valuemap: { - 'id': 'ID', - 'uploadedTo': 'parent', - 'menuOrder': 'menu_order ID' + return function( props, options ) { + var args = {}, + orderby = Query.orderby, + defaults = Query.defaultProps, + 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 ) { + props.order = defaults.order.toUpperCase(); } - }, - /** - * @readonly - */ - propmap: { - 'search': 's', - 'type': 'post_mime_type', - 'perPage': 'posts_per_page', - 'menuOrder': 'menu_order', - 'uploadedTo': 'post_parent', - 'status': 'post_status' - }, - /** - * @static - * @method - * - * @returns {wp.media.model.Query} A new query. - */ - // Caches query objects so queries can be easily reused. - get: (function(){ - /** - * @static - * @type Array - */ - var queries = []; - /** - * @param {Object} props - * @param {Object} options - * @returns {Query} - */ - return function( props, options ) { - var args = {}, - orderby = Query.orderby, - defaults = Query.defaultProps, - 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 ) { - props.order = defaults.order.toUpperCase(); - } + // Ensure we have a valid orderby value. + if ( ! _.contains( orderby.allowed, props.orderby ) ) { + props.orderby = defaults.orderby; + } - // Ensure we have a valid orderby value. - 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 ) ) { - return; - } - - args[ Query.propmap[ prop ] || prop ] = value; - }); + // Generate the query `args` object. + // Correct any differing property names. + _.each( props, function( value, prop ) { + if ( _.isNull( value ) ) { + return; + } - // Fill any other default query args. - _.defaults( args, Query.defaultArgs ); + args[ Query.propmap[ prop ] || prop ] = value; + }); - // `props.orderby` does not always map directly to `args.orderby`. - // Substitute exceptions specified in orderby.keymap. - args.orderby = orderby.valuemap[ props.orderby ] || props.orderby; + // Fill any other default query args. + _.defaults( args, Query.defaultArgs ); - // Search the query cache for matches. - if ( cache ) { - query = _.find( queries, function( query ) { - return _.isEqual( query.args, args ); - }); - } else { - queries = []; - } + // `props.orderby` does not always map directly to `args.orderby`. + // Substitute exceptions specified in orderby.keymap. + args.orderby = orderby.valuemap[ props.orderby ] || props.orderby; - // Otherwise, create a new query and add it to the cache. - if ( ! query ) { - query = new Query( [], _.extend( options || {}, { - props: props, - args: args - } ) ); - queries.push( query ); - } + // Search the query cache for a matching query. + if ( cache ) { + query = _.find( queries, function( query ) { + return _.isEqual( query.args, args ); + }); + } else { + queries = []; + } - return query; - }; - }()) - }); + // Otherwise, create a new query and add it to the cache. + if ( ! query ) { + query = new Query( [], _.extend( options || {}, { + props: props, + args: args + } ) ); + queries.push( query ); + } + return query; + }; + }()) +}); + +module.exports = Query; + +},{}],6:[function(require,module,exports){ +/*globals wp, _ */ + +/** + * wp.media.model.Selection + * + * A selection of attachments. + * + * @class + * @augments wp.media.model.Attachments + * @augments Backbone.Collection + */ +var Attachments = wp.media.model.Attachments, + Selection; + +Selection = Attachments.extend({ /** - * wp.media.model.Selection - * - * Used to manage a selection of attachments in the views. + * Refresh the `single` model whenever the selection changes. + * Binds `single` instead of using the context argument to ensure + * it receives no parameters. * - * @constructor - * @augments wp.media.model.Attachments - * @augments Backbone.Collection + * @param {Array} [models=[]] Array of models used to populate the collection. + * @param {Object} [options={}] */ - media.model.Selection = Attachments.extend({ + initialize: function( models, options ) { /** - * 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={}] + * call 'initialize' directly on the parent class */ - initialize: function( models, options ) { - /** - * call 'initialize' directly on the parent class - */ - Attachments.prototype.initialize.apply( this, arguments ); - this.multiple = options && options.multiple; + Attachments.prototype.initialize.apply( this, arguments ); + this.multiple = options && options.multiple; - this.on( 'add remove reset', _.bind( this.single, this, false ) ); - }, + this.on( 'add remove reset', _.bind( this.single, this, false ) ); + }, + /** + * 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 ) { + this.remove( this.models ); + } /** - * Override the selection's add method. - * If the workflow does not support multiple - * selected attachments, reset the selection. - * - * Overrides Backbone.Collection.add - * Overrides wp.media.model.Attachments.add - * - * @param {Array} models - * @param {Object} options - * @returns {wp.media.model.Attachment[]} + * call 'add' directly on the parent class */ - add: function( models, options ) { - if ( ! this.multiple ) { - this.remove( this.models ); - } - /** - * call 'add' directly on the parent class - */ - return Attachments.prototype.add.call( this, models, options ); - }, + return Attachments.prototype.add.call( this, models, options ); + }, - /** - * Triggered 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; + /** + * 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 ) { - this._single = model; - } - // If the single model isn't in the selection, remove it. - if ( this._single && ! this.get( this._single.cid ) ) { - delete this._single; - } + // If a `model` is provided, use it as the single 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 ) ) { + delete this._single; + } - this._single = this._single || this.last(); + this._single = this._single || this.last(); - // If single has changed, fire an event. - if ( this._single !== previous ) { - if ( previous ) { - previous.trigger( 'selection:unsingle', previous, this ); + // If single has changed, fire an event. + if ( this._single !== previous ) { + if ( previous ) { + previous.trigger( 'selection:unsingle', previous, this ); - // If the model was already removed, trigger the collection - // event manually. - if ( ! this.get( previous.cid ) ) { - this.trigger( 'selection:unsingle', previous, this ); - } - } - if ( this._single ) { - this._single.trigger( 'selection:single', this._single, this ); + // If the model was already removed, trigger the collection + // event manually. + if ( ! this.get( previous.cid ) ) { + this.trigger( 'selection:unsingle', previous, this ); } } - - // Return the single model, or the last model as a fallback. - return this._single; + if ( this._single ) { + this._single.trigger( 'selection:single', this._single, this ); + } } - }); - // Clean up. Prevents mobile browsers caching - $(window).on('unload', function(){ - window.wp = null; - }); + // Return the single model, or the last model as a fallback. + return this._single; + } +}); + +module.exports = Selection; -}(jQuery)); +},{}]},{},[1]);