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