~canonical-sysadmins/wordpress/4.7.2

« back to all changes in this revision

Viewing changes to wp-includes/js/media-models.js

  • Committer: Jacek Nykis
  • Date: 2015-01-05 16:17:05 UTC
  • Revision ID: jacek.nykis@canonical.com-20150105161705-w544l1h5mcg7u4w9
Initial commit

Show diffs side-by-side

added added

removed removed

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