PageRenderTime 57ms CodeModel.GetById 2ms app.highlight 44ms RepoModel.GetById 1ms app.codeStats 0ms

/wp-includes/js/media-models.js

https://gitlab.com/SiryjVyiko/qwerty
JavaScript | 1362 lines | 716 code | 154 blank | 492 comment | 189 complexity | 9f1cc3edfd9f471f51f6d5177fe646a8 MD5 | raw file
   1/* global _wpMediaModelsL10n:false */
   2window.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			'include':    'post__in',
1169			'exclude':    'post__not_in'
1170		},
1171		/**
1172		 * @static
1173		 * @method
1174		 *
1175		 * @returns {wp.media.model.Query} A new query.
1176		 */
1177		// Caches query objects so queries can be easily reused.
1178		get: (function(){
1179			/**
1180			 * @static
1181			 * @type Array
1182			 */
1183			var queries = [];
1184
1185			/**
1186			 * @param {Object} props
1187			 * @param {Object} options
1188			 * @returns {Query}
1189			 */
1190			return function( props, options ) {
1191				var args     = {},
1192					orderby  = Query.orderby,
1193					defaults = Query.defaultProps,
1194					query,
1195					cache    = !! props.cache || _.isUndefined( props.cache );
1196
1197				// Remove the `query` property. This isn't linked to a query,
1198				// this *is* the query.
1199				delete props.query;
1200				delete props.cache;
1201
1202				// Fill default args.
1203				_.defaults( props, defaults );
1204
1205				// Normalize the order.
1206				props.order = props.order.toUpperCase();
1207				if ( 'DESC' !== props.order && 'ASC' !== props.order ) {
1208					props.order = defaults.order.toUpperCase();
1209				}
1210
1211				// Ensure we have a valid orderby value.
1212				if ( ! _.contains( orderby.allowed, props.orderby ) ) {
1213					props.orderby = defaults.orderby;
1214				}
1215
1216				_.each( [ 'include', 'exclude' ], function( prop ) {
1217					if ( props[ prop ] && ! _.isArray( props[ prop ] ) ) {
1218						props[ prop ] = [ props[ prop ] ];
1219					}
1220				} );
1221
1222				// Generate the query `args` object.
1223				// Correct any differing property names.
1224				_.each( props, function( value, prop ) {
1225					if ( _.isNull( value ) ) {
1226						return;
1227					}
1228
1229					args[ Query.propmap[ prop ] || prop ] = value;
1230				});
1231
1232				// Fill any other default query args.
1233				_.defaults( args, Query.defaultArgs );
1234
1235				// `props.orderby` does not always map directly to `args.orderby`.
1236				// Substitute exceptions specified in orderby.keymap.
1237				args.orderby = orderby.valuemap[ props.orderby ] || props.orderby;
1238
1239				// Search the query cache for matches.
1240				if ( cache ) {
1241					query = _.find( queries, function( query ) {
1242						return _.isEqual( query.args, args );
1243					});
1244				} else {
1245					queries = [];
1246				}
1247
1248				// Otherwise, create a new query and add it to the cache.
1249				if ( ! query ) {
1250					query = new Query( [], _.extend( options || {}, {
1251						props: props,
1252						args:  args
1253					} ) );
1254					queries.push( query );
1255				}
1256
1257				return query;
1258			};
1259		}())
1260	});
1261
1262	/**
1263	 * wp.media.model.Selection
1264	 *
1265	 * Used to manage a selection of attachments in the views.
1266	 *
1267	 * @constructor
1268	 * @augments wp.media.model.Attachments
1269	 * @augments Backbone.Collection
1270	 */
1271	media.model.Selection = Attachments.extend({
1272		/**
1273		 * Refresh the `single` model whenever the selection changes.
1274		 * Binds `single` instead of using the context argument to ensure
1275		 * it receives no parameters.
1276		 *
1277		 * @param {Array} [models=[]] Array of models used to populate the collection.
1278		 * @param {Object} [options={}]
1279		 */
1280		initialize: function( models, options ) {
1281			/**
1282			 * call 'initialize' directly on the parent class
1283			 */
1284			Attachments.prototype.initialize.apply( this, arguments );
1285			this.multiple = options && options.multiple;
1286
1287			this.on( 'add remove reset', _.bind( this.single, this, false ) );
1288		},
1289
1290		/**
1291		 * Override the selection's add method.
1292		 * If the workflow does not support multiple
1293		 * selected attachments, reset the selection.
1294		 *
1295		 * Overrides Backbone.Collection.add
1296		 * Overrides wp.media.model.Attachments.add
1297		 *
1298		 * @param {Array} models
1299		 * @param {Object} options
1300		 * @returns {wp.media.model.Attachment[]}
1301		 */
1302		add: function( models, options ) {
1303			if ( ! this.multiple ) {
1304				this.remove( this.models );
1305			}
1306			/**
1307			 * call 'add' directly on the parent class
1308			 */
1309			return Attachments.prototype.add.call( this, models, options );
1310		},
1311
1312		/**
1313		 * Triggered when toggling (clicking on) an attachment in the modal
1314		 *
1315		 * @param {undefined|boolean|wp.media.model.Attachment} model
1316		 *
1317		 * @fires wp.media.model.Selection#selection:single
1318		 * @fires wp.media.model.Selection#selection:unsingle
1319		 *
1320		 * @returns {Backbone.Model}
1321		 */
1322		single: function( model ) {
1323			var previous = this._single;
1324
1325			// If a `model` is provided, use it as the single model.
1326			if ( model ) {
1327				this._single = model;
1328			}
1329			// If the single model isn't in the selection, remove it.
1330			if ( this._single && ! this.get( this._single.cid ) ) {
1331				delete this._single;
1332			}
1333
1334			this._single = this._single || this.last();
1335
1336			// If single has changed, fire an event.
1337			if ( this._single !== previous ) {
1338				if ( previous ) {
1339					previous.trigger( 'selection:unsingle', previous, this );
1340
1341					// If the model was already removed, trigger the collection
1342					// event manually.
1343					if ( ! this.get( previous.cid ) ) {
1344						this.trigger( 'selection:unsingle', previous, this );
1345					}
1346				}
1347				if ( this._single ) {
1348					this._single.trigger( 'selection:single', this._single, this );
1349				}
1350			}
1351
1352			// Return the single model, or the last model as a fallback.
1353			return this._single;
1354		}
1355	});
1356
1357	// Clean up. Prevents mobile browsers caching
1358	$(window).on('unload', function(){
1359		window.wp = null;
1360	});
1361
1362}(jQuery));