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