PageRenderTime 64ms CodeModel.GetById 2ms app.highlight 51ms RepoModel.GetById 1ms app.codeStats 0ms

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

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