PageRenderTime 948ms CodeModel.GetById 20ms RepoModel.GetById 0ms app.codeStats 0ms

/src/js/media/models/attachments.js

https://gitlab.com/morganestes/wordpress-develop
JavaScript | 552 lines | 270 code | 51 blank | 231 comment | 68 complexity | a67d306bb321e4f5c65c5acece12de1c MD5 | raw file
  1. /**
  2. * wp.media.model.Attachments
  3. *
  4. * A collection of attachments.
  5. *
  6. * This collection has no persistence with the server without supplying
  7. * 'options.props.query = true', which will mirror the collection
  8. * to an Attachments Query collection - @see wp.media.model.Attachments.mirror().
  9. *
  10. * @memberOf wp.media.model
  11. *
  12. * @class
  13. * @augments Backbone.Collection
  14. *
  15. * @param {array} [models] Models to initialize with the collection.
  16. * @param {object} [options] Options hash for the collection.
  17. * @param {string} [options.props] Options hash for the initial query properties.
  18. * @param {string} [options.props.order] Initial order (ASC or DESC) for the collection.
  19. * @param {string} [options.props.orderby] Initial attribute key to order the collection by.
  20. * @param {string} [options.props.query] Whether the collection is linked to an attachments query.
  21. * @param {string} [options.observe]
  22. * @param {string} [options.filters]
  23. *
  24. */
  25. var Attachments = Backbone.Collection.extend(/** @lends wp.media.model.Attachments.prototype */{
  26. /**
  27. * @type {wp.media.model.Attachment}
  28. */
  29. model: wp.media.model.Attachment,
  30. /**
  31. * @param {Array} [models=[]] Array of models used to populate the collection.
  32. * @param {Object} [options={}]
  33. */
  34. initialize: function( models, options ) {
  35. options = options || {};
  36. this.props = new Backbone.Model();
  37. this.filters = options.filters || {};
  38. // Bind default `change` events to the `props` model.
  39. this.props.on( 'change', this._changeFilteredProps, this );
  40. this.props.on( 'change:order', this._changeOrder, this );
  41. this.props.on( 'change:orderby', this._changeOrderby, this );
  42. this.props.on( 'change:query', this._changeQuery, this );
  43. this.props.set( _.defaults( options.props || {} ) );
  44. if ( options.observe ) {
  45. this.observe( options.observe );
  46. }
  47. },
  48. /**
  49. * Sort the collection when the order attribute changes.
  50. *
  51. * @access private
  52. */
  53. _changeOrder: function() {
  54. if ( this.comparator ) {
  55. this.sort();
  56. }
  57. },
  58. /**
  59. * Set the default comparator only when the `orderby` property is set.
  60. *
  61. * @access private
  62. *
  63. * @param {Backbone.Model} model
  64. * @param {string} orderby
  65. */
  66. _changeOrderby: function( model, orderby ) {
  67. // If a different comparator is defined, bail.
  68. if ( this.comparator && this.comparator !== Attachments.comparator ) {
  69. return;
  70. }
  71. if ( orderby && 'post__in' !== orderby ) {
  72. this.comparator = Attachments.comparator;
  73. } else {
  74. delete this.comparator;
  75. }
  76. },
  77. /**
  78. * If the `query` property is set to true, query the server using
  79. * the `props` values, and sync the results to this collection.
  80. *
  81. * @access private
  82. *
  83. * @param {Backbone.Model} model
  84. * @param {Boolean} query
  85. */
  86. _changeQuery: function( model, query ) {
  87. if ( query ) {
  88. this.props.on( 'change', this._requery, this );
  89. this._requery();
  90. } else {
  91. this.props.off( 'change', this._requery, this );
  92. }
  93. },
  94. /**
  95. * @access private
  96. *
  97. * @param {Backbone.Model} model
  98. */
  99. _changeFilteredProps: function( model ) {
  100. // If this is a query, updating the collection will be handled by
  101. // `this._requery()`.
  102. if ( this.props.get('query') ) {
  103. return;
  104. }
  105. var changed = _.chain( model.changed ).map( function( t, prop ) {
  106. var filter = Attachments.filters[ prop ],
  107. term = model.get( prop );
  108. if ( ! filter ) {
  109. return;
  110. }
  111. if ( term && ! this.filters[ prop ] ) {
  112. this.filters[ prop ] = filter;
  113. } else if ( ! term && this.filters[ prop ] === filter ) {
  114. delete this.filters[ prop ];
  115. } else {
  116. return;
  117. }
  118. // Record the change.
  119. return true;
  120. }, this ).any().value();
  121. if ( ! changed ) {
  122. return;
  123. }
  124. // If no `Attachments` model is provided to source the searches
  125. // from, then automatically generate a source from the existing
  126. // models.
  127. if ( ! this._source ) {
  128. this._source = new Attachments( this.models );
  129. }
  130. this.reset( this._source.filter( this.validator, this ) );
  131. },
  132. validateDestroyed: false,
  133. /**
  134. * Checks whether an attachment is valid.
  135. *
  136. * @param {wp.media.model.Attachment} attachment
  137. * @returns {Boolean}
  138. */
  139. validator: function( attachment ) {
  140. // Filter out contextually created attachments (e.g. headers, logos, etc.).
  141. if (
  142. ! _.isUndefined( attachment.attributes.context ) &&
  143. '' !== attachment.attributes.context
  144. ) {
  145. return false;
  146. }
  147. if ( ! this.validateDestroyed && attachment.destroyed ) {
  148. return false;
  149. }
  150. return _.all( this.filters, function( filter ) {
  151. return !! filter.call( this, attachment );
  152. }, this );
  153. },
  154. /**
  155. * Add or remove an attachment to the collection depending on its validity.
  156. *
  157. * @param {wp.media.model.Attachment} attachment
  158. * @param {Object} options
  159. * @returns {wp.media.model.Attachments} Returns itself to allow chaining
  160. */
  161. validate: function( attachment, options ) {
  162. var valid = this.validator( attachment ),
  163. hasAttachment = !! this.get( attachment.cid );
  164. if ( ! valid && hasAttachment ) {
  165. this.remove( attachment, options );
  166. } else if ( valid && ! hasAttachment ) {
  167. this.add( attachment, options );
  168. }
  169. return this;
  170. },
  171. /**
  172. * Add or remove all attachments from another collection depending on each one's validity.
  173. *
  174. * @param {wp.media.model.Attachments} attachments
  175. * @param {object} [options={}]
  176. *
  177. * @fires wp.media.model.Attachments#reset
  178. *
  179. * @returns {wp.media.model.Attachments} Returns itself to allow chaining
  180. */
  181. validateAll: function( attachments, options ) {
  182. options = options || {};
  183. _.each( attachments.models, function( attachment ) {
  184. this.validate( attachment, { silent: true });
  185. }, this );
  186. if ( ! options.silent ) {
  187. this.trigger( 'reset', this, options );
  188. }
  189. return this;
  190. },
  191. /**
  192. * Start observing another attachments collection change events
  193. * and replicate them on this collection.
  194. *
  195. * @param {wp.media.model.Attachments} The attachments collection to observe.
  196. * @returns {wp.media.model.Attachments} Returns itself to allow chaining.
  197. */
  198. observe: function( attachments ) {
  199. this.observers = this.observers || [];
  200. this.observers.push( attachments );
  201. attachments.on( 'add change remove', this._validateHandler, this );
  202. attachments.on( 'reset', this._validateAllHandler, this );
  203. this.validateAll( attachments );
  204. return this;
  205. },
  206. /**
  207. * Stop replicating collection change events from another attachments collection.
  208. *
  209. * @param {wp.media.model.Attachments} The attachments collection to stop observing.
  210. * @returns {wp.media.model.Attachments} Returns itself to allow chaining
  211. */
  212. unobserve: function( attachments ) {
  213. if ( attachments ) {
  214. attachments.off( null, null, this );
  215. this.observers = _.without( this.observers, attachments );
  216. } else {
  217. _.each( this.observers, function( attachments ) {
  218. attachments.off( null, null, this );
  219. }, this );
  220. delete this.observers;
  221. }
  222. return this;
  223. },
  224. /**
  225. * @access private
  226. *
  227. * @param {wp.media.model.Attachments} attachment
  228. * @param {wp.media.model.Attachments} attachments
  229. * @param {Object} options
  230. *
  231. * @returns {wp.media.model.Attachments} Returns itself to allow chaining
  232. */
  233. _validateHandler: function( attachment, attachments, options ) {
  234. // If we're not mirroring this `attachments` collection,
  235. // only retain the `silent` option.
  236. options = attachments === this.mirroring ? options : {
  237. silent: options && options.silent
  238. };
  239. return this.validate( attachment, options );
  240. },
  241. /**
  242. * @access private
  243. *
  244. * @param {wp.media.model.Attachments} attachments
  245. * @param {Object} options
  246. * @returns {wp.media.model.Attachments} Returns itself to allow chaining
  247. */
  248. _validateAllHandler: function( attachments, options ) {
  249. return this.validateAll( attachments, options );
  250. },
  251. /**
  252. * Start mirroring another attachments collection, clearing out any models already
  253. * in the collection.
  254. *
  255. * @param {wp.media.model.Attachments} The attachments collection to mirror.
  256. * @returns {wp.media.model.Attachments} Returns itself to allow chaining
  257. */
  258. mirror: function( attachments ) {
  259. if ( this.mirroring && this.mirroring === attachments ) {
  260. return this;
  261. }
  262. this.unmirror();
  263. this.mirroring = attachments;
  264. // Clear the collection silently. A `reset` event will be fired
  265. // when `observe()` calls `validateAll()`.
  266. this.reset( [], { silent: true } );
  267. this.observe( attachments );
  268. return this;
  269. },
  270. /**
  271. * Stop mirroring another attachments collection.
  272. */
  273. unmirror: function() {
  274. if ( ! this.mirroring ) {
  275. return;
  276. }
  277. this.unobserve( this.mirroring );
  278. delete this.mirroring;
  279. },
  280. /**
  281. * Retrieve more attachments from the server for the collection.
  282. *
  283. * Only works if the collection is mirroring a Query Attachments collection,
  284. * and forwards to its `more` method. This collection class doesn't have
  285. * server persistence by itself.
  286. *
  287. * @param {object} options
  288. * @returns {Promise}
  289. */
  290. more: function( options ) {
  291. var deferred = jQuery.Deferred(),
  292. mirroring = this.mirroring,
  293. attachments = this;
  294. if ( ! mirroring || ! mirroring.more ) {
  295. return deferred.resolveWith( this ).promise();
  296. }
  297. // If we're mirroring another collection, forward `more` to
  298. // the mirrored collection. Account for a race condition by
  299. // checking if we're still mirroring that collection when
  300. // the request resolves.
  301. mirroring.more( options ).done( function() {
  302. if ( this === attachments.mirroring ) {
  303. deferred.resolveWith( this );
  304. }
  305. });
  306. return deferred.promise();
  307. },
  308. /**
  309. * Whether there are more attachments that haven't been sync'd from the server
  310. * that match the collection's query.
  311. *
  312. * Only works if the collection is mirroring a Query Attachments collection,
  313. * and forwards to its `hasMore` method. This collection class doesn't have
  314. * server persistence by itself.
  315. *
  316. * @returns {boolean}
  317. */
  318. hasMore: function() {
  319. return this.mirroring ? this.mirroring.hasMore() : false;
  320. },
  321. /**
  322. * A custom AJAX-response parser.
  323. *
  324. * See trac ticket #24753
  325. *
  326. * @param {Object|Array} resp The raw response Object/Array.
  327. * @param {Object} xhr
  328. * @returns {Array} The array of model attributes to be added to the collection
  329. */
  330. parse: function( resp, xhr ) {
  331. if ( ! _.isArray( resp ) ) {
  332. resp = [resp];
  333. }
  334. return _.map( resp, function( attrs ) {
  335. var id, attachment, newAttributes;
  336. if ( attrs instanceof Backbone.Model ) {
  337. id = attrs.get( 'id' );
  338. attrs = attrs.attributes;
  339. } else {
  340. id = attrs.id;
  341. }
  342. attachment = wp.media.model.Attachment.get( id );
  343. newAttributes = attachment.parse( attrs, xhr );
  344. if ( ! _.isEqual( attachment.attributes, newAttributes ) ) {
  345. attachment.set( newAttributes );
  346. }
  347. return attachment;
  348. });
  349. },
  350. /**
  351. * If the collection is a query, create and mirror an Attachments Query collection.
  352. *
  353. * @access private
  354. */
  355. _requery: function( refresh ) {
  356. var props;
  357. if ( this.props.get('query') ) {
  358. props = this.props.toJSON();
  359. props.cache = ( true !== refresh );
  360. this.mirror( wp.media.model.Query.get( props ) );
  361. }
  362. },
  363. /**
  364. * If this collection is sorted by `menuOrder`, recalculates and saves
  365. * the menu order to the database.
  366. *
  367. * @returns {undefined|Promise}
  368. */
  369. saveMenuOrder: function() {
  370. if ( 'menuOrder' !== this.props.get('orderby') ) {
  371. return;
  372. }
  373. // Removes any uploading attachments, updates each attachment's
  374. // menu order, and returns an object with an { id: menuOrder }
  375. // mapping to pass to the request.
  376. var attachments = this.chain().filter( function( attachment ) {
  377. return ! _.isUndefined( attachment.id );
  378. }).map( function( attachment, index ) {
  379. // Indices start at 1.
  380. index = index + 1;
  381. attachment.set( 'menuOrder', index );
  382. return [ attachment.id, index ];
  383. }).object().value();
  384. if ( _.isEmpty( attachments ) ) {
  385. return;
  386. }
  387. return wp.media.post( 'save-attachment-order', {
  388. nonce: wp.media.model.settings.post.nonce,
  389. post_id: wp.media.model.settings.post.id,
  390. attachments: attachments
  391. });
  392. }
  393. },/** @lends wp.media.model.Attachments */{
  394. /**
  395. * A function to compare two attachment models in an attachments collection.
  396. *
  397. * Used as the default comparator for instances of wp.media.model.Attachments
  398. * and its subclasses. @see wp.media.model.Attachments._changeOrderby().
  399. *
  400. * @param {Backbone.Model} a
  401. * @param {Backbone.Model} b
  402. * @param {Object} options
  403. * @returns {Number} -1 if the first model should come before the second,
  404. * 0 if they are of the same rank and
  405. * 1 if the first model should come after.
  406. */
  407. comparator: function( a, b, options ) {
  408. var key = this.props.get('orderby'),
  409. order = this.props.get('order') || 'DESC',
  410. ac = a.cid,
  411. bc = b.cid;
  412. a = a.get( key );
  413. b = b.get( key );
  414. if ( 'date' === key || 'modified' === key ) {
  415. a = a || new Date();
  416. b = b || new Date();
  417. }
  418. // If `options.ties` is set, don't enforce the `cid` tiebreaker.
  419. if ( options && options.ties ) {
  420. ac = bc = null;
  421. }
  422. return ( 'DESC' === order ) ? wp.media.compare( a, b, ac, bc ) : wp.media.compare( b, a, bc, ac );
  423. },
  424. /** @namespace wp.media.model.Attachments.filters */
  425. filters: {
  426. /**
  427. * @static
  428. * Note that this client-side searching is *not* equivalent
  429. * to our server-side searching.
  430. *
  431. * @param {wp.media.model.Attachment} attachment
  432. *
  433. * @this wp.media.model.Attachments
  434. *
  435. * @returns {Boolean}
  436. */
  437. search: function( attachment ) {
  438. if ( ! this.props.get('search') ) {
  439. return true;
  440. }
  441. return _.any(['title','filename','description','caption','name'], function( key ) {
  442. var value = attachment.get( key );
  443. return value && -1 !== value.search( this.props.get('search') );
  444. }, this );
  445. },
  446. /**
  447. * @static
  448. * @param {wp.media.model.Attachment} attachment
  449. *
  450. * @this wp.media.model.Attachments
  451. *
  452. * @returns {Boolean}
  453. */
  454. type: function( attachment ) {
  455. var type = this.props.get('type'), atts = attachment.toJSON(), mime, found;
  456. if ( ! type || ( _.isArray( type ) && ! type.length ) ) {
  457. return true;
  458. }
  459. mime = atts.mime || ( atts.file && atts.file.type ) || '';
  460. if ( _.isArray( type ) ) {
  461. found = _.find( type, function (t) {
  462. return -1 !== mime.indexOf( t );
  463. } );
  464. } else {
  465. found = -1 !== mime.indexOf( type );
  466. }
  467. return found;
  468. },
  469. /**
  470. * @static
  471. * @param {wp.media.model.Attachment} attachment
  472. *
  473. * @this wp.media.model.Attachments
  474. *
  475. * @returns {Boolean}
  476. */
  477. uploadedTo: function( attachment ) {
  478. var uploadedTo = this.props.get('uploadedTo');
  479. if ( _.isUndefined( uploadedTo ) ) {
  480. return true;
  481. }
  482. return uploadedTo === attachment.get('uploadedTo');
  483. },
  484. /**
  485. * @static
  486. * @param {wp.media.model.Attachment} attachment
  487. *
  488. * @this wp.media.model.Attachments
  489. *
  490. * @returns {Boolean}
  491. */
  492. status: function( attachment ) {
  493. var status = this.props.get('status');
  494. if ( _.isUndefined( status ) ) {
  495. return true;
  496. }
  497. return status === attachment.get('status');
  498. }
  499. }
  500. });
  501. module.exports = Attachments;