]> scripts.mit.edu Git - autoinstalls/wordpress.git/blob - wp-includes/js/media-models.js
WordPress 4.1-scripts
[autoinstalls/wordpress.git] / wp-includes / js / media-models.js
1 /* global _wpMediaModelsL10n:false */
2 window.wp = window.wp || {};
3
4 (function($){
5         var Attachment, Attachments, Query, PostImage, compare, l10n, media;
6
7         /**
8          * Create and return a media frame.
9          *
10          * Handles the default media experience.
11          *
12          * @param  {object} attributes The properties passed to the main media controller.
13          * @return {wp.media.view.MediaFrame} A media workflow.
14          */
15         media = wp.media = function( attributes ) {
16                 var MediaFrame = media.view.MediaFrame,
17                         frame;
18
19                 if ( ! MediaFrame ) {
20                         return;
21                 }
22
23                 attributes = _.defaults( attributes || {}, {
24                         frame: 'select'
25                 });
26
27                 if ( 'select' === attributes.frame && MediaFrame.Select ) {
28                         frame = new MediaFrame.Select( attributes );
29                 } else if ( 'post' === attributes.frame && MediaFrame.Post ) {
30                         frame = new MediaFrame.Post( attributes );
31                 } else if ( 'manage' === attributes.frame && MediaFrame.Manage ) {
32                         frame = new MediaFrame.Manage( attributes );
33                 } else if ( 'image' === attributes.frame && MediaFrame.ImageDetails ) {
34                         frame = new MediaFrame.ImageDetails( attributes );
35                 } else if ( 'audio' === attributes.frame && MediaFrame.AudioDetails ) {
36                         frame = new MediaFrame.AudioDetails( attributes );
37                 } else if ( 'video' === attributes.frame && MediaFrame.VideoDetails ) {
38                         frame = new MediaFrame.VideoDetails( attributes );
39                 } else if ( 'edit-attachments' === attributes.frame && MediaFrame.EditAttachments ) {
40                         frame = new MediaFrame.EditAttachments( attributes );
41                 }
42
43                 delete attributes.frame;
44
45                 media.frame = frame;
46
47                 return frame;
48         };
49
50         _.extend( media, { model: {}, view: {}, controller: {}, frames: {} });
51
52         // Link any localized strings.
53         l10n = media.model.l10n = typeof _wpMediaModelsL10n === 'undefined' ? {} : _wpMediaModelsL10n;
54
55         // Link any settings.
56         media.model.settings = l10n.settings || {};
57         delete l10n.settings;
58
59         /**
60          * ========================================================================
61          * UTILITIES
62          * ========================================================================
63          */
64
65         /**
66          * A basic equality comparator for Backbone models.
67          *
68          * Used to order models within a collection - @see wp.media.model.Attachments.comparator().
69          *
70          * @param  {mixed}  a  The primary parameter to compare.
71          * @param  {mixed}  b  The primary parameter to compare.
72          * @param  {string} ac The fallback parameter to compare, a's cid.
73          * @param  {string} bc The fallback parameter to compare, b's cid.
74          * @return {number}    -1: a should come before b.
75          *                      0: a and b are of the same rank.
76          *                      1: b should come before a.
77          */
78         compare = function( a, b, ac, bc ) {
79                 if ( _.isEqual( a, b ) ) {
80                         return ac === bc ? 0 : (ac > bc ? -1 : 1);
81                 } else {
82                         return a > b ? -1 : 1;
83                 }
84         };
85
86         _.extend( media, {
87                 /**
88                  * media.template( id )
89                  *
90                  * Fetch a JavaScript template for an id, and return a templating function for it.
91                  *
92                  * See wp.template() in `wp-includes/js/wp-util.js`.
93                  *
94                  * @borrows wp.template as template
95                  */
96                 template: wp.template,
97
98                 /**
99                  * media.post( [action], [data] )
100                  *
101                  * Sends a POST request to WordPress.
102                  * See wp.ajax.post() in `wp-includes/js/wp-util.js`.
103                  *
104                  * @borrows wp.ajax.post as post
105                  */
106                 post: wp.ajax.post,
107
108                 /**
109                  * media.ajax( [action], [options] )
110                  *
111                  * Sends an XHR request to WordPress.
112                  * See wp.ajax.send() in `wp-includes/js/wp-util.js`.
113                  *
114                  * @borrows wp.ajax.send as ajax
115                  */
116                 ajax: wp.ajax.send,
117
118                 /**
119                  * Scales a set of dimensions to fit within bounding dimensions.
120                  *
121                  * @param {Object} dimensions
122                  * @returns {Object}
123                  */
124                 fit: function( dimensions ) {
125                         var width     = dimensions.width,
126                                 height    = dimensions.height,
127                                 maxWidth  = dimensions.maxWidth,
128                                 maxHeight = dimensions.maxHeight,
129                                 constraint;
130
131                         // Compare ratios between the two values to determine which
132                         // max to constrain by. If a max value doesn't exist, then the
133                         // opposite side is the constraint.
134                         if ( ! _.isUndefined( maxWidth ) && ! _.isUndefined( maxHeight ) ) {
135                                 constraint = ( width / height > maxWidth / maxHeight ) ? 'width' : 'height';
136                         } else if ( _.isUndefined( maxHeight ) ) {
137                                 constraint = 'width';
138                         } else if (  _.isUndefined( maxWidth ) && height > maxHeight ) {
139                                 constraint = 'height';
140                         }
141
142                         // If the value of the constrained side is larger than the max,
143                         // then scale the values. Otherwise return the originals; they fit.
144                         if ( 'width' === constraint && width > maxWidth ) {
145                                 return {
146                                         width : maxWidth,
147                                         height: Math.round( maxWidth * height / width )
148                                 };
149                         } else if ( 'height' === constraint && height > maxHeight ) {
150                                 return {
151                                         width : Math.round( maxHeight * width / height ),
152                                         height: maxHeight
153                                 };
154                         } else {
155                                 return {
156                                         width : width,
157                                         height: height
158                                 };
159                         }
160                 },
161                 /**
162                  * Truncates a string by injecting an ellipsis into the middle.
163                  * Useful for filenames.
164                  *
165                  * @param {String} string
166                  * @param {Number} [length=30]
167                  * @param {String} [replacement=…]
168                  * @returns {String} The string, unless length is greater than string.length.
169                  */
170                 truncate: function( string, length, replacement ) {
171                         length = length || 30;
172                         replacement = replacement || '…';
173
174                         if ( string.length <= length ) {
175                                 return string;
176                         }
177
178                         return string.substr( 0, length / 2 ) + replacement + string.substr( -1 * length / 2 );
179                 }
180         });
181
182         /**
183          * ========================================================================
184          * MODELS
185          * ========================================================================
186          */
187         /**
188          * wp.media.attachment
189          *
190          * @static
191          * @param {String} id A string used to identify a model.
192          * @returns {wp.media.model.Attachment}
193          */
194         media.attachment = function( id ) {
195                 return Attachment.get( id );
196         };
197
198         /**
199          * wp.media.model.Attachment
200          *
201          * @class
202          * @augments Backbone.Model
203          */
204         Attachment = media.model.Attachment = Backbone.Model.extend({
205                 /**
206                  * Triggered when attachment details change
207                  * Overrides Backbone.Model.sync
208                  *
209                  * @param {string} method
210                  * @param {wp.media.model.Attachment} model
211                  * @param {Object} [options={}]
212                  *
213                  * @returns {Promise}
214                  */
215                 sync: function( method, model, options ) {
216                         // If the attachment does not yet have an `id`, return an instantly
217                         // rejected promise. Otherwise, all of our requests will fail.
218                         if ( _.isUndefined( this.id ) ) {
219                                 return $.Deferred().rejectWith( this ).promise();
220                         }
221
222                         // Overload the `read` request so Attachment.fetch() functions correctly.
223                         if ( 'read' === method ) {
224                                 options = options || {};
225                                 options.context = this;
226                                 options.data = _.extend( options.data || {}, {
227                                         action: 'get-attachment',
228                                         id: this.id
229                                 });
230                                 return media.ajax( options );
231
232                         // Overload the `update` request so properties can be saved.
233                         } else if ( 'update' === method ) {
234                                 // If we do not have the necessary nonce, fail immeditately.
235                                 if ( ! this.get('nonces') || ! this.get('nonces').update ) {
236                                         return $.Deferred().rejectWith( this ).promise();
237                                 }
238
239                                 options = options || {};
240                                 options.context = this;
241
242                                 // Set the action and ID.
243                                 options.data = _.extend( options.data || {}, {
244                                         action:  'save-attachment',
245                                         id:      this.id,
246                                         nonce:   this.get('nonces').update,
247                                         post_id: media.model.settings.post.id
248                                 });
249
250                                 // Record the values of the changed attributes.
251                                 if ( model.hasChanged() ) {
252                                         options.data.changes = {};
253
254                                         _.each( model.changed, function( value, key ) {
255                                                 options.data.changes[ key ] = this.get( key );
256                                         }, this );
257                                 }
258
259                                 return media.ajax( options );
260
261                         // Overload the `delete` request so attachments can be removed.
262                         // This will permanently delete an attachment.
263                         } else if ( 'delete' === method ) {
264                                 options = options || {};
265
266                                 if ( ! options.wait ) {
267                                         this.destroyed = true;
268                                 }
269
270                                 options.context = this;
271                                 options.data = _.extend( options.data || {}, {
272                                         action:   'delete-post',
273                                         id:       this.id,
274                                         _wpnonce: this.get('nonces')['delete']
275                                 });
276
277                                 return media.ajax( options ).done( function() {
278                                         this.destroyed = true;
279                                 }).fail( function() {
280                                         this.destroyed = false;
281                                 });
282
283                         // Otherwise, fall back to `Backbone.sync()`.
284                         } else {
285                                 /**
286                                  * Call `sync` directly on Backbone.Model
287                                  */
288                                 return Backbone.Model.prototype.sync.apply( this, arguments );
289                         }
290                 },
291                 /**
292                  * Convert date strings into Date objects.
293                  *
294                  * @param {Object} resp The raw response object, typically returned by fetch()
295                  * @returns {Object} The modified response object, which is the attributes hash
296                  *    to be set on the model.
297                  */
298                 parse: function( resp ) {
299                         if ( ! resp ) {
300                                 return resp;
301                         }
302
303                         resp.date = new Date( resp.date );
304                         resp.modified = new Date( resp.modified );
305                         return resp;
306                 },
307                 /**
308                  * @param {Object} data The properties to be saved.
309                  * @param {Object} options Sync options. e.g. patch, wait, success, error.
310                  *
311                  * @this Backbone.Model
312                  *
313                  * @returns {Promise}
314                  */
315                 saveCompat: function( data, options ) {
316                         var model = this;
317
318                         // If we do not have the necessary nonce, fail immeditately.
319                         if ( ! this.get('nonces') || ! this.get('nonces').update ) {
320                                 return $.Deferred().rejectWith( this ).promise();
321                         }
322
323                         return media.post( 'save-attachment-compat', _.defaults({
324                                 id:      this.id,
325                                 nonce:   this.get('nonces').update,
326                                 post_id: media.model.settings.post.id
327                         }, data ) ).done( function( resp, status, xhr ) {
328                                 model.set( model.parse( resp, xhr ), options );
329                         });
330                 }
331         }, {
332                 /**
333                  * Create a new model on the static 'all' attachments collection and return it.
334                  *
335                  * @static
336                  * @param {Object} attrs
337                  * @returns {wp.media.model.Attachment}
338                  */
339                 create: function( attrs ) {
340                         return Attachments.all.push( attrs );
341                 },
342                 /**
343                  * Create a new model on the static 'all' attachments collection and return it.
344                  *
345                  * If this function has already been called for the id,
346                  * it returns the specified attachment.
347                  *
348                  * @static
349                  * @param {string} id A string used to identify a model.
350                  * @param {Backbone.Model|undefined} attachment
351                  * @returns {wp.media.model.Attachment}
352                  */
353                 get: _.memoize( function( id, attachment ) {
354                         return Attachments.all.push( attachment || { id: id } );
355                 })
356         });
357
358         /**
359          * wp.media.model.PostImage
360          *
361          * An instance of an image that's been embedded into a post.
362          *
363          * Used in the embedded image attachment display settings modal - @see wp.media.view.MediaFrame.ImageDetails.
364          *
365          * @class
366          * @augments Backbone.Model
367          *
368          * @param {int} [attributes]               Initial model attributes.
369          * @param {int} [attributes.attachment_id] ID of the attachment.
370          **/
371         PostImage = media.model.PostImage = Backbone.Model.extend({
372
373                 initialize: function( attributes ) {
374                         this.attachment = false;
375
376                         if ( attributes.attachment_id ) {
377                                 this.attachment = Attachment.get( attributes.attachment_id );
378                                 if ( this.attachment.get( 'url' ) ) {
379                                         this.dfd = $.Deferred();
380                                         this.dfd.resolve();
381                                 } else {
382                                         this.dfd = this.attachment.fetch();
383                                 }
384                                 this.bindAttachmentListeners();
385                         }
386
387                         // keep url in sync with changes to the type of link
388                         this.on( 'change:link', this.updateLinkUrl, this );
389                         this.on( 'change:size', this.updateSize, this );
390
391                         this.setLinkTypeFromUrl();
392                         this.setAspectRatio();
393
394                         this.set( 'originalUrl', attributes.url );
395                 },
396
397                 bindAttachmentListeners: function() {
398                         this.listenTo( this.attachment, 'sync', this.setLinkTypeFromUrl );
399                         this.listenTo( this.attachment, 'sync', this.setAspectRatio );
400                         this.listenTo( this.attachment, 'change', this.updateSize );
401                 },
402
403                 changeAttachment: function( attachment, props ) {
404                         this.stopListening( this.attachment );
405                         this.attachment = attachment;
406                         this.bindAttachmentListeners();
407
408                         this.set( 'attachment_id', this.attachment.get( 'id' ) );
409                         this.set( 'caption', this.attachment.get( 'caption' ) );
410                         this.set( 'alt', this.attachment.get( 'alt' ) );
411                         this.set( 'size', props.get( 'size' ) );
412                         this.set( 'align', props.get( 'align' ) );
413                         this.set( 'link', props.get( 'link' ) );
414                         this.updateLinkUrl();
415                         this.updateSize();
416                 },
417
418                 setLinkTypeFromUrl: function() {
419                         var linkUrl = this.get( 'linkUrl' ),
420                                 type;
421
422                         if ( ! linkUrl ) {
423                                 this.set( 'link', 'none' );
424                                 return;
425                         }
426
427                         // default to custom if there is a linkUrl
428                         type = 'custom';
429
430                         if ( this.attachment ) {
431                                 if ( this.attachment.get( 'url' ) === linkUrl ) {
432                                         type = 'file';
433                                 } else if ( this.attachment.get( 'link' ) === linkUrl ) {
434                                         type = 'post';
435                                 }
436                         } else {
437                                 if ( this.get( 'url' ) === linkUrl ) {
438                                         type = 'file';
439                                 }
440                         }
441
442                         this.set( 'link', type );
443                 },
444
445                 updateLinkUrl: function() {
446                         var link = this.get( 'link' ),
447                                 url;
448
449                         switch( link ) {
450                                 case 'file':
451                                         if ( this.attachment ) {
452                                                 url = this.attachment.get( 'url' );
453                                         } else {
454                                                 url = this.get( 'url' );
455                                         }
456                                         this.set( 'linkUrl', url );
457                                         break;
458                                 case 'post':
459                                         this.set( 'linkUrl', this.attachment.get( 'link' ) );
460                                         break;
461                                 case 'none':
462                                         this.set( 'linkUrl', '' );
463                                         break;
464                         }
465                 },
466
467                 updateSize: function() {
468                         var size;
469
470                         if ( ! this.attachment ) {
471                                 return;
472                         }
473
474                         if ( this.get( 'size' ) === 'custom' ) {
475                                 this.set( 'width', this.get( 'customWidth' ) );
476                                 this.set( 'height', this.get( 'customHeight' ) );
477                                 this.set( 'url', this.get( 'originalUrl' ) );
478                                 return;
479                         }
480
481                         size = this.attachment.get( 'sizes' )[ this.get( 'size' ) ];
482
483                         if ( ! size ) {
484                                 return;
485                         }
486
487                         this.set( 'url', size.url );
488                         this.set( 'width', size.width );
489                         this.set( 'height', size.height );
490                 },
491
492                 setAspectRatio: function() {
493                         var full;
494
495                         if ( this.attachment && this.attachment.get( 'sizes' ) ) {
496                                 full = this.attachment.get( 'sizes' ).full;
497
498                                 if ( full ) {
499                                         this.set( 'aspectRatio', full.width / full.height );
500                                         return;
501                                 }
502                         }
503
504                         this.set( 'aspectRatio', this.get( 'customWidth' ) / this.get( 'customHeight' ) );
505                 }
506         });
507
508         /**
509          * wp.media.model.Attachments
510          *
511          * A collection of attachments.
512          *
513          * This collection has no persistence with the server without supplying
514          * 'options.props.query = true', which will mirror the collection
515          * to an Attachments Query collection - @see wp.media.model.Attachments.mirror().
516          *
517          * @class
518          * @augments Backbone.Collection
519          *
520          * @param {array}  [models]                Models to initialize with the collection.
521          * @param {object} [options]               Options hash for the collection.
522          * @param {string} [options.props]         Options hash for the initial query properties.
523          * @param {string} [options.props.order]   Initial order (ASC or DESC) for the collection.
524          * @param {string} [options.props.orderby] Initial attribute key to order the collection by.
525          * @param {string} [options.props.query]   Whether the collection is linked to an attachments query.
526          * @param {string} [options.observe]
527          * @param {string} [options.filters]
528          *
529          */
530         Attachments = media.model.Attachments = Backbone.Collection.extend({
531                 /**
532                  * @type {wp.media.model.Attachment}
533                  */
534                 model: Attachment,
535                 /**
536                  * @param {Array} [models=[]] Array of models used to populate the collection.
537                  * @param {Object} [options={}]
538                  */
539                 initialize: function( models, options ) {
540                         options = options || {};
541
542                         this.props   = new Backbone.Model();
543                         this.filters = options.filters || {};
544
545                         // Bind default `change` events to the `props` model.
546                         this.props.on( 'change', this._changeFilteredProps, this );
547
548                         this.props.on( 'change:order',   this._changeOrder,   this );
549                         this.props.on( 'change:orderby', this._changeOrderby, this );
550                         this.props.on( 'change:query',   this._changeQuery,   this );
551
552                         this.props.set( _.defaults( options.props || {} ) );
553
554                         if ( options.observe ) {
555                                 this.observe( options.observe );
556                         }
557                 },
558                 /**
559                  * Sort the collection when the order attribute changes.
560                  *
561                  * @access private
562                  */
563                 _changeOrder: function() {
564                         if ( this.comparator ) {
565                                 this.sort();
566                         }
567                 },
568                 /**
569                  * Set the default comparator only when the `orderby` property is set.
570                  *
571                  * @access private
572                  *
573                  * @param {Backbone.Model} model
574                  * @param {string} orderby
575                  */
576                 _changeOrderby: function( model, orderby ) {
577                         // If a different comparator is defined, bail.
578                         if ( this.comparator && this.comparator !== Attachments.comparator ) {
579                                 return;
580                         }
581
582                         if ( orderby && 'post__in' !== orderby ) {
583                                 this.comparator = Attachments.comparator;
584                         } else {
585                                 delete this.comparator;
586                         }
587                 },
588                 /**
589                  * If the `query` property is set to true, query the server using
590                  * the `props` values, and sync the results to this collection.
591                  *
592                  * @access private
593                  *
594                  * @param {Backbone.Model} model
595                  * @param {Boolean} query
596                  */
597                 _changeQuery: function( model, query ) {
598                         if ( query ) {
599                                 this.props.on( 'change', this._requery, this );
600                                 this._requery();
601                         } else {
602                                 this.props.off( 'change', this._requery, this );
603                         }
604                 },
605                 /**
606                  * @access private
607                  *
608                  * @param {Backbone.Model} model
609                  */
610                 _changeFilteredProps: function( model ) {
611                         // If this is a query, updating the collection will be handled by
612                         // `this._requery()`.
613                         if ( this.props.get('query') ) {
614                                 return;
615                         }
616
617                         var changed = _.chain( model.changed ).map( function( t, prop ) {
618                                 var filter = Attachments.filters[ prop ],
619                                         term = model.get( prop );
620
621                                 if ( ! filter ) {
622                                         return;
623                                 }
624
625                                 if ( term && ! this.filters[ prop ] ) {
626                                         this.filters[ prop ] = filter;
627                                 } else if ( ! term && this.filters[ prop ] === filter ) {
628                                         delete this.filters[ prop ];
629                                 } else {
630                                         return;
631                                 }
632
633                                 // Record the change.
634                                 return true;
635                         }, this ).any().value();
636
637                         if ( ! changed ) {
638                                 return;
639                         }
640
641                         // If no `Attachments` model is provided to source the searches
642                         // from, then automatically generate a source from the existing
643                         // models.
644                         if ( ! this._source ) {
645                                 this._source = new Attachments( this.models );
646                         }
647
648                         this.reset( this._source.filter( this.validator, this ) );
649                 },
650
651                 validateDestroyed: false,
652                 /**
653                  * Checks whether an attachment is valid.
654                  *
655                  * @param {wp.media.model.Attachment} attachment
656                  * @returns {Boolean}
657                  */
658                 validator: function( attachment ) {
659                         if ( ! this.validateDestroyed && attachment.destroyed ) {
660                                 return false;
661                         }
662                         return _.all( this.filters, function( filter ) {
663                                 return !! filter.call( this, attachment );
664                         }, this );
665                 },
666                 /**
667                  * Add or remove an attachment to the collection depending on its validity.
668                  *
669                  * @param {wp.media.model.Attachment} attachment
670                  * @param {Object} options
671                  * @returns {wp.media.model.Attachments} Returns itself to allow chaining
672                  */
673                 validate: function( attachment, options ) {
674                         var valid = this.validator( attachment ),
675                                 hasAttachment = !! this.get( attachment.cid );
676
677                         if ( ! valid && hasAttachment ) {
678                                 this.remove( attachment, options );
679                         } else if ( valid && ! hasAttachment ) {
680                                 this.add( attachment, options );
681                         }
682
683                         return this;
684                 },
685
686                 /**
687                  * Add or remove all attachments from another collection depending on each one's validity.
688                  *
689                  * @param {wp.media.model.Attachments} attachments
690                  * @param {object} [options={}]
691                  *
692                  * @fires wp.media.model.Attachments#reset
693                  *
694                  * @returns {wp.media.model.Attachments} Returns itself to allow chaining
695                  */
696                 validateAll: function( attachments, options ) {
697                         options = options || {};
698
699                         _.each( attachments.models, function( attachment ) {
700                                 this.validate( attachment, { silent: true });
701                         }, this );
702
703                         if ( ! options.silent ) {
704                                 this.trigger( 'reset', this, options );
705                         }
706                         return this;
707                 },
708                 /**
709                  * Start observing another attachments collection change events
710                  * and replicate them on this collection.
711                  *
712                  * @param {wp.media.model.Attachments} The attachments collection to observe.
713                  * @returns {wp.media.model.Attachments} Returns itself to allow chaining.
714                  */
715                 observe: function( attachments ) {
716                         this.observers = this.observers || [];
717                         this.observers.push( attachments );
718
719                         attachments.on( 'add change remove', this._validateHandler, this );
720                         attachments.on( 'reset', this._validateAllHandler, this );
721                         this.validateAll( attachments );
722                         return this;
723                 },
724                 /**
725                  * Stop replicating collection change events from another attachments collection.
726                  *
727                  * @param {wp.media.model.Attachments} The attachments collection to stop observing.
728                  * @returns {wp.media.model.Attachments} Returns itself to allow chaining
729                  */
730                 unobserve: function( attachments ) {
731                         if ( attachments ) {
732                                 attachments.off( null, null, this );
733                                 this.observers = _.without( this.observers, attachments );
734
735                         } else {
736                                 _.each( this.observers, function( attachments ) {
737                                         attachments.off( null, null, this );
738                                 }, this );
739                                 delete this.observers;
740                         }
741
742                         return this;
743                 },
744                 /**
745                  * @access private
746                  *
747                  * @param {wp.media.model.Attachments} attachment
748                  * @param {wp.media.model.Attachments} attachments
749                  * @param {Object} options
750                  *
751                  * @returns {wp.media.model.Attachments} Returns itself to allow chaining
752                  */
753                 _validateHandler: function( attachment, attachments, options ) {
754                         // If we're not mirroring this `attachments` collection,
755                         // only retain the `silent` option.
756                         options = attachments === this.mirroring ? options : {
757                                 silent: options && options.silent
758                         };
759
760                         return this.validate( attachment, options );
761                 },
762                 /**
763                  * @access private
764                  *
765                  * @param {wp.media.model.Attachments} attachments
766                  * @param {Object} options
767                  * @returns {wp.media.model.Attachments} Returns itself to allow chaining
768                  */
769                 _validateAllHandler: function( attachments, options ) {
770                         return this.validateAll( attachments, options );
771                 },
772                 /**
773                  * Start mirroring another attachments collection, clearing out any models already
774                  * in the collection.
775                  *
776                  * @param {wp.media.model.Attachments} The attachments collection to mirror.
777                  * @returns {wp.media.model.Attachments} Returns itself to allow chaining
778                  */
779                 mirror: function( attachments ) {
780                         if ( this.mirroring && this.mirroring === attachments ) {
781                                 return this;
782                         }
783
784                         this.unmirror();
785                         this.mirroring = attachments;
786
787                         // Clear the collection silently. A `reset` event will be fired
788                         // when `observe()` calls `validateAll()`.
789                         this.reset( [], { silent: true } );
790                         this.observe( attachments );
791
792                         return this;
793                 },
794                 /**
795                  * Stop mirroring another attachments collection.
796                  */
797                 unmirror: function() {
798                         if ( ! this.mirroring ) {
799                                 return;
800                         }
801
802                         this.unobserve( this.mirroring );
803                         delete this.mirroring;
804                 },
805                 /**
806                  * Retrive more attachments from the server for the collection.
807                  *
808                  * Only works if the collection is mirroring a Query Attachments collection,
809                  * and forwards to its `more` method. This collection class doesn't have
810                  * server persistence by itself.
811                  *
812                  * @param {object} options
813                  * @returns {Promise}
814                  */
815                 more: function( options ) {
816                         var deferred = $.Deferred(),
817                                 mirroring = this.mirroring,
818                                 attachments = this;
819
820                         if ( ! mirroring || ! mirroring.more ) {
821                                 return deferred.resolveWith( this ).promise();
822                         }
823                         // If we're mirroring another collection, forward `more` to
824                         // the mirrored collection. Account for a race condition by
825                         // checking if we're still mirroring that collection when
826                         // the request resolves.
827                         mirroring.more( options ).done( function() {
828                                 if ( this === attachments.mirroring )
829                                         deferred.resolveWith( this );
830                         });
831
832                         return deferred.promise();
833                 },
834                 /**
835                  * Whether there are more attachments that haven't been sync'd from the server
836                  * that match the collection's query.
837                  *
838                  * Only works if the collection is mirroring a Query Attachments collection,
839                  * and forwards to its `hasMore` method. This collection class doesn't have
840                  * server persistence by itself.
841                  *
842                  * @returns {boolean}
843                  */
844                 hasMore: function() {
845                         return this.mirroring ? this.mirroring.hasMore() : false;
846                 },
847                 /**
848                  * A custom AJAX-response parser.
849                  *
850                  * See trac ticket #24753
851                  *
852                  * @param {Object|Array} resp The raw response Object/Array.
853                  * @param {Object} xhr
854                  * @returns {Array} The array of model attributes to be added to the collection
855                  */
856                 parse: function( resp, xhr ) {
857                         if ( ! _.isArray( resp ) ) {
858                                 resp = [resp];
859                         }
860
861                         return _.map( resp, function( attrs ) {
862                                 var id, attachment, newAttributes;
863
864                                 if ( attrs instanceof Backbone.Model ) {
865                                         id = attrs.get( 'id' );
866                                         attrs = attrs.attributes;
867                                 } else {
868                                         id = attrs.id;
869                                 }
870
871                                 attachment = Attachment.get( id );
872                                 newAttributes = attachment.parse( attrs, xhr );
873
874                                 if ( ! _.isEqual( attachment.attributes, newAttributes ) ) {
875                                         attachment.set( newAttributes );
876                                 }
877
878                                 return attachment;
879                         });
880                 },
881                 /**
882                  * If the collection is a query, create and mirror an Attachments Query collection.
883                  *
884                  * @access private
885                  */
886                 _requery: function( refresh ) {
887                         var props;
888                         if ( this.props.get('query') ) {
889                                 props = this.props.toJSON();
890                                 props.cache = ( true !== refresh );
891                                 this.mirror( Query.get( props ) );
892                         }
893                 },
894                 /**
895                  * If this collection is sorted by `menuOrder`, recalculates and saves
896                  * the menu order to the database.
897                  *
898                  * @returns {undefined|Promise}
899                  */
900                 saveMenuOrder: function() {
901                         if ( 'menuOrder' !== this.props.get('orderby') ) {
902                                 return;
903                         }
904
905                         // Removes any uploading attachments, updates each attachment's
906                         // menu order, and returns an object with an { id: menuOrder }
907                         // mapping to pass to the request.
908                         var attachments = this.chain().filter( function( attachment ) {
909                                 return ! _.isUndefined( attachment.id );
910                         }).map( function( attachment, index ) {
911                                 // Indices start at 1.
912                                 index = index + 1;
913                                 attachment.set( 'menuOrder', index );
914                                 return [ attachment.id, index ];
915                         }).object().value();
916
917                         if ( _.isEmpty( attachments ) ) {
918                                 return;
919                         }
920
921                         return media.post( 'save-attachment-order', {
922                                 nonce:       media.model.settings.post.nonce,
923                                 post_id:     media.model.settings.post.id,
924                                 attachments: attachments
925                         });
926                 }
927         }, {
928                 /**
929                  * A function to compare two attachment models in an attachments collection.
930                  *
931                  * Used as the default comparator for instances of wp.media.model.Attachments
932                  * and its subclasses. @see wp.media.model.Attachments._changeOrderby().
933                  *
934                  * @static
935                  *
936                  * @param {Backbone.Model} a
937                  * @param {Backbone.Model} b
938                  * @param {Object} options
939                  * @returns {Number} -1 if the first model should come before the second,
940                  *    0 if they are of the same rank and
941                  *    1 if the first model should come after.
942                  */
943                 comparator: function( a, b, options ) {
944                         var key   = this.props.get('orderby'),
945                                 order = this.props.get('order') || 'DESC',
946                                 ac    = a.cid,
947                                 bc    = b.cid;
948
949                         a = a.get( key );
950                         b = b.get( key );
951
952                         if ( 'date' === key || 'modified' === key ) {
953                                 a = a || new Date();
954                                 b = b || new Date();
955                         }
956
957                         // If `options.ties` is set, don't enforce the `cid` tiebreaker.
958                         if ( options && options.ties ) {
959                                 ac = bc = null;
960                         }
961
962                         return ( 'DESC' === order ) ? compare( a, b, ac, bc ) : compare( b, a, bc, ac );
963                 },
964                 /**
965                  * @namespace
966                  */
967                 filters: {
968                         /**
969                          * @static
970                          * Note that this client-side searching is *not* equivalent
971                          * to our server-side searching.
972                          *
973                          * @param {wp.media.model.Attachment} attachment
974                          *
975                          * @this wp.media.model.Attachments
976                          *
977                          * @returns {Boolean}
978                          */
979                         search: function( attachment ) {
980                                 if ( ! this.props.get('search') ) {
981                                         return true;
982                                 }
983
984                                 return _.any(['title','filename','description','caption','name'], function( key ) {
985                                         var value = attachment.get( key );
986                                         return value && -1 !== value.search( this.props.get('search') );
987                                 }, this );
988                         },
989                         /**
990                          * @static
991                          * @param {wp.media.model.Attachment} attachment
992                          *
993                          * @this wp.media.model.Attachments
994                          *
995                          * @returns {Boolean}
996                          */
997                         type: function( attachment ) {
998                                 var type = this.props.get('type');
999                                 return ! type || -1 !== type.indexOf( attachment.get('type') );
1000                         },
1001                         /**
1002                          * @static
1003                          * @param {wp.media.model.Attachment} attachment
1004                          *
1005                          * @this wp.media.model.Attachments
1006                          *
1007                          * @returns {Boolean}
1008                          */
1009                         uploadedTo: function( attachment ) {
1010                                 var uploadedTo = this.props.get('uploadedTo');
1011                                 if ( _.isUndefined( uploadedTo ) ) {
1012                                         return true;
1013                                 }
1014
1015                                 return uploadedTo === attachment.get('uploadedTo');
1016                         },
1017                         /**
1018                          * @static
1019                          * @param {wp.media.model.Attachment} attachment
1020                          *
1021                          * @this wp.media.model.Attachments
1022                          *
1023                          * @returns {Boolean}
1024                          */
1025                         status: function( attachment ) {
1026                                 var status = this.props.get('status');
1027                                 if ( _.isUndefined( status ) ) {
1028                                         return true;
1029                                 }
1030
1031                                 return status === attachment.get('status');
1032                         }
1033                 }
1034         });
1035
1036         /**
1037          * A collection of all attachments that have been fetched from the server.
1038          *
1039          * @static
1040          * @member {wp.media.model.Attachments}
1041          */
1042         Attachments.all = new Attachments();
1043
1044         /**
1045          * wp.media.query
1046          *
1047          * Shorthand for creating a new Attachments Query.
1048          *
1049          * @param {object} [props]
1050          * @returns {wp.media.model.Attachments}
1051          */
1052         media.query = function( props ) {
1053                 return new Attachments( null, {
1054                         props: _.extend( _.defaults( props || {}, { orderby: 'date' } ), { query: true } )
1055                 });
1056         };
1057
1058         /**
1059          * wp.media.model.Query
1060          *
1061          * A collection of attachments that match the supplied query arguments.
1062          *
1063          * Note: Do NOT change this.args after the query has been initialized.
1064          *       Things will break.
1065          *
1066          * @class
1067          * @augments wp.media.model.Attachments
1068          * @augments Backbone.Collection
1069          *
1070          * @param {array}  [models]                      Models to initialize with the collection.
1071          * @param {object} [options]                     Options hash.
1072          * @param {object} [options.args]                Attachments query arguments.
1073          * @param {object} [options.args.posts_per_page]
1074          */
1075         Query = media.model.Query = Attachments.extend({
1076                 /**
1077                  * @global wp.Uploader
1078                  *
1079                  * @param {array}  [models=[]]  Array of initial models to populate the collection.
1080                  * @param {object} [options={}]
1081                  */
1082                 initialize: function( models, options ) {
1083                         var allowed;
1084
1085                         options = options || {};
1086                         Attachments.prototype.initialize.apply( this, arguments );
1087
1088                         this.args     = options.args;
1089                         this._hasMore = true;
1090                         this.created  = new Date();
1091
1092                         this.filters.order = function( attachment ) {
1093                                 var orderby = this.props.get('orderby'),
1094                                         order = this.props.get('order');
1095
1096                                 if ( ! this.comparator ) {
1097                                         return true;
1098                                 }
1099
1100                                 // We want any items that can be placed before the last
1101                                 // item in the set. If we add any items after the last
1102                                 // item, then we can't guarantee the set is complete.
1103                                 if ( this.length ) {
1104                                         return 1 !== this.comparator( attachment, this.last(), { ties: true });
1105
1106                                 // Handle the case where there are no items yet and
1107                                 // we're sorting for recent items. In that case, we want
1108                                 // changes that occurred after we created the query.
1109                                 } else if ( 'DESC' === order && ( 'date' === orderby || 'modified' === orderby ) ) {
1110                                         return attachment.get( orderby ) >= this.created;
1111
1112                                 // If we're sorting by menu order and we have no items,
1113                                 // accept any items that have the default menu order (0).
1114                                 } else if ( 'ASC' === order && 'menuOrder' === orderby ) {
1115                                         return attachment.get( orderby ) === 0;
1116                                 }
1117
1118                                 // Otherwise, we don't want any items yet.
1119                                 return false;
1120                         };
1121
1122                         // Observe the central `wp.Uploader.queue` collection to watch for
1123                         // new matches for the query.
1124                         //
1125                         // Only observe when a limited number of query args are set. There
1126                         // are no filters for other properties, so observing will result in
1127                         // false positives in those queries.
1128                         allowed = [ 's', 'order', 'orderby', 'posts_per_page', 'post_mime_type', 'post_parent' ];
1129                         if ( wp.Uploader && _( this.args ).chain().keys().difference( allowed ).isEmpty().value() ) {
1130                                 this.observe( wp.Uploader.queue );
1131                         }
1132                 },
1133                 /**
1134                  * Whether there are more attachments that haven't been sync'd from the server
1135                  * that match the collection's query.
1136                  *
1137                  * @returns {boolean}
1138                  */
1139                 hasMore: function() {
1140                         return this._hasMore;
1141                 },
1142                 /**
1143                  * Fetch more attachments from the server for the collection.
1144                  *
1145                  * @param   {object}  [options={}]
1146                  * @returns {Promise}
1147                  */
1148                 more: function( options ) {
1149                         var query = this;
1150
1151                         // If there is already a request pending, return early with the Deferred object.
1152                         if ( this._more && 'pending' === this._more.state() ) {
1153                                 return this._more;
1154                         }
1155
1156                         if ( ! this.hasMore() ) {
1157                                 return $.Deferred().resolveWith( this ).promise();
1158                         }
1159
1160                         options = options || {};
1161                         options.remove = false;
1162
1163                         return this._more = this.fetch( options ).done( function( resp ) {
1164                                 if ( _.isEmpty( resp ) || -1 === this.args.posts_per_page || resp.length < this.args.posts_per_page ) {
1165                                         query._hasMore = false;
1166                                 }
1167                         });
1168                 },
1169                 /**
1170                  * Overrides Backbone.Collection.sync
1171                  * Overrides wp.media.model.Attachments.sync
1172                  *
1173                  * @param {String} method
1174                  * @param {Backbone.Model} model
1175                  * @param {Object} [options={}]
1176                  * @returns {Promise}
1177                  */
1178                 sync: function( method, model, options ) {
1179                         var args, fallback;
1180
1181                         // Overload the read method so Attachment.fetch() functions correctly.
1182                         if ( 'read' === method ) {
1183                                 options = options || {};
1184                                 options.context = this;
1185                                 options.data = _.extend( options.data || {}, {
1186                                         action:  'query-attachments',
1187                                         post_id: media.model.settings.post.id
1188                                 });
1189
1190                                 // Clone the args so manipulation is non-destructive.
1191                                 args = _.clone( this.args );
1192
1193                                 // Determine which page to query.
1194                                 if ( -1 !== args.posts_per_page ) {
1195                                         args.paged = Math.floor( this.length / args.posts_per_page ) + 1;
1196                                 }
1197
1198                                 options.data.query = args;
1199                                 return media.ajax( options );
1200
1201                         // Otherwise, fall back to Backbone.sync()
1202                         } else {
1203                                 /**
1204                                  * Call wp.media.model.Attachments.sync or Backbone.sync
1205                                  */
1206                                 fallback = Attachments.prototype.sync ? Attachments.prototype : Backbone;
1207                                 return fallback.sync.apply( this, arguments );
1208                         }
1209                 }
1210         }, {
1211                 /**
1212                  * @readonly
1213                  */
1214                 defaultProps: {
1215                         orderby: 'date',
1216                         order:   'DESC'
1217                 },
1218                 /**
1219                  * @readonly
1220                  */
1221                 defaultArgs: {
1222                         posts_per_page: 40
1223                 },
1224                 /**
1225                  * @readonly
1226                  */
1227                 orderby: {
1228                         allowed:  [ 'name', 'author', 'date', 'title', 'modified', 'uploadedTo', 'id', 'post__in', 'menuOrder' ],
1229                         /**
1230                          * A map of JavaScript orderby values to their WP_Query equivalents.
1231                          * @type {Object}
1232                          */
1233                         valuemap: {
1234                                 'id':         'ID',
1235                                 'uploadedTo': 'parent',
1236                                 'menuOrder':  'menu_order ID'
1237                         }
1238                 },
1239                 /**
1240                  * A map of JavaScript query properties to their WP_Query equivalents.
1241                  *
1242                  * @readonly
1243                  */
1244                 propmap: {
1245                         'search':    's',
1246                         'type':      'post_mime_type',
1247                         'perPage':   'posts_per_page',
1248                         'menuOrder': 'menu_order',
1249                         'uploadedTo': 'post_parent',
1250                         'status':     'post_status',
1251                         'include':    'post__in',
1252                         'exclude':    'post__not_in'
1253                 },
1254                 /**
1255                  * Creates and returns an Attachments Query collection given the properties.
1256                  *
1257                  * Caches query objects and reuses where possible.
1258                  *
1259                  * @static
1260                  * @method
1261                  *
1262                  * @param {object} [props]
1263                  * @param {Object} [props.cache=true]   Whether to use the query cache or not.
1264                  * @param {Object} [props.order]
1265                  * @param {Object} [props.orderby]
1266                  * @param {Object} [props.include]
1267                  * @param {Object} [props.exclude]
1268                  * @param {Object} [props.s]
1269                  * @param {Object} [props.post_mime_type]
1270                  * @param {Object} [props.posts_per_page]
1271                  * @param {Object} [props.menu_order]
1272                  * @param {Object} [props.post_parent]
1273                  * @param {Object} [props.post_status]
1274                  * @param {Object} [options]
1275                  *
1276                  * @returns {wp.media.model.Query} A new Attachments Query collection.
1277                  */
1278                 get: (function(){
1279                         /**
1280                          * @static
1281                          * @type Array
1282                          */
1283                         var queries = [];
1284
1285                         /**
1286                          * @returns {Query}
1287                          */
1288                         return function( props, options ) {
1289                                 var args     = {},
1290                                         orderby  = Query.orderby,
1291                                         defaults = Query.defaultProps,
1292                                         query,
1293                                         cache    = !! props.cache || _.isUndefined( props.cache );
1294
1295                                 // Remove the `query` property. This isn't linked to a query,
1296                                 // this *is* the query.
1297                                 delete props.query;
1298                                 delete props.cache;
1299
1300                                 // Fill default args.
1301                                 _.defaults( props, defaults );
1302
1303                                 // Normalize the order.
1304                                 props.order = props.order.toUpperCase();
1305                                 if ( 'DESC' !== props.order && 'ASC' !== props.order ) {
1306                                         props.order = defaults.order.toUpperCase();
1307                                 }
1308
1309                                 // Ensure we have a valid orderby value.
1310                                 if ( ! _.contains( orderby.allowed, props.orderby ) ) {
1311                                         props.orderby = defaults.orderby;
1312                                 }
1313
1314                                 _.each( [ 'include', 'exclude' ], function( prop ) {
1315                                         if ( props[ prop ] && ! _.isArray( props[ prop ] ) ) {
1316                                                 props[ prop ] = [ props[ prop ] ];
1317                                         }
1318                                 } );
1319
1320                                 // Generate the query `args` object.
1321                                 // Correct any differing property names.
1322                                 _.each( props, function( value, prop ) {
1323                                         if ( _.isNull( value ) ) {
1324                                                 return;
1325                                         }
1326
1327                                         args[ Query.propmap[ prop ] || prop ] = value;
1328                                 });
1329
1330                                 // Fill any other default query args.
1331                                 _.defaults( args, Query.defaultArgs );
1332
1333                                 // `props.orderby` does not always map directly to `args.orderby`.
1334                                 // Substitute exceptions specified in orderby.keymap.
1335                                 args.orderby = orderby.valuemap[ props.orderby ] || props.orderby;
1336
1337                                 // Search the query cache for a matching query.
1338                                 if ( cache ) {
1339                                         query = _.find( queries, function( query ) {
1340                                                 return _.isEqual( query.args, args );
1341                                         });
1342                                 } else {
1343                                         queries = [];
1344                                 }
1345
1346                                 // Otherwise, create a new query and add it to the cache.
1347                                 if ( ! query ) {
1348                                         query = new Query( [], _.extend( options || {}, {
1349                                                 props: props,
1350                                                 args:  args
1351                                         } ) );
1352                                         queries.push( query );
1353                                 }
1354
1355                                 return query;
1356                         };
1357                 }())
1358         });
1359
1360         /**
1361          * wp.media.model.Selection
1362          *
1363          * A selection of attachments.
1364          *
1365          * @class
1366          * @augments wp.media.model.Attachments
1367          * @augments Backbone.Collection
1368          */
1369         media.model.Selection = Attachments.extend({
1370                 /**
1371                  * Refresh the `single` model whenever the selection changes.
1372                  * Binds `single` instead of using the context argument to ensure
1373                  * it receives no parameters.
1374                  *
1375                  * @param {Array} [models=[]] Array of models used to populate the collection.
1376                  * @param {Object} [options={}]
1377                  */
1378                 initialize: function( models, options ) {
1379                         /**
1380                          * call 'initialize' directly on the parent class
1381                          */
1382                         Attachments.prototype.initialize.apply( this, arguments );
1383                         this.multiple = options && options.multiple;
1384
1385                         this.on( 'add remove reset', _.bind( this.single, this, false ) );
1386                 },
1387
1388                 /**
1389                  * If the workflow does not support multi-select, clear out the selection
1390                  * before adding a new attachment to it.
1391                  *
1392                  * @param {Array} models
1393                  * @param {Object} options
1394                  * @returns {wp.media.model.Attachment[]}
1395                  */
1396                 add: function( models, options ) {
1397                         if ( ! this.multiple ) {
1398                                 this.remove( this.models );
1399                         }
1400                         /**
1401                          * call 'add' directly on the parent class
1402                          */
1403                         return Attachments.prototype.add.call( this, models, options );
1404                 },
1405
1406                 /**
1407                  * Fired when toggling (clicking on) an attachment in the modal.
1408                  *
1409                  * @param {undefined|boolean|wp.media.model.Attachment} model
1410                  *
1411                  * @fires wp.media.model.Selection#selection:single
1412                  * @fires wp.media.model.Selection#selection:unsingle
1413                  *
1414                  * @returns {Backbone.Model}
1415                  */
1416                 single: function( model ) {
1417                         var previous = this._single;
1418
1419                         // If a `model` is provided, use it as the single model.
1420                         if ( model ) {
1421                                 this._single = model;
1422                         }
1423                         // If the single model isn't in the selection, remove it.
1424                         if ( this._single && ! this.get( this._single.cid ) ) {
1425                                 delete this._single;
1426                         }
1427
1428                         this._single = this._single || this.last();
1429
1430                         // If single has changed, fire an event.
1431                         if ( this._single !== previous ) {
1432                                 if ( previous ) {
1433                                         previous.trigger( 'selection:unsingle', previous, this );
1434
1435                                         // If the model was already removed, trigger the collection
1436                                         // event manually.
1437                                         if ( ! this.get( previous.cid ) ) {
1438                                                 this.trigger( 'selection:unsingle', previous, this );
1439                                         }
1440                                 }
1441                                 if ( this._single ) {
1442                                         this._single.trigger( 'selection:single', this._single, this );
1443                                 }
1444                         }
1445
1446                         // Return the single model, or the last model as a fallback.
1447                         return this._single;
1448                 }
1449         });
1450
1451         // Clean up. Prevents mobile browsers caching
1452         $(window).on('unload', function(){
1453                 window.wp = null;
1454         });
1455
1456 }(jQuery));