wp-backbone.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567
  1. /**
  2. * @output wp-includes/js/wp-backbone.js
  3. */
  4. /** @namespace wp */
  5. window.wp = window.wp || {};
  6. (function ($) {
  7. /**
  8. * Create the WordPress Backbone namespace.
  9. *
  10. * @namespace wp.Backbone
  11. */
  12. wp.Backbone = {};
  13. /**
  14. * A backbone subview manager.
  15. *
  16. * @since 3.5.0
  17. * @since 3.6.0 Moved wp.media.Views to wp.Backbone.Subviews.
  18. *
  19. * @memberOf wp.Backbone
  20. *
  21. * @class
  22. *
  23. * @param {wp.Backbone.View} view The main view.
  24. * @param {Array|Object} views The subviews for the main view.
  25. */
  26. wp.Backbone.Subviews = function( view, views ) {
  27. this.view = view;
  28. this._views = _.isArray( views ) ? { '': views } : views || {};
  29. };
  30. wp.Backbone.Subviews.extend = Backbone.Model.extend;
  31. _.extend( wp.Backbone.Subviews.prototype, {
  32. /**
  33. * Fetches all of the subviews.
  34. *
  35. * @since 3.5.0
  36. *
  37. * @return {Array} All the subviews.
  38. */
  39. all: function() {
  40. return _.flatten( _.values( this._views ) );
  41. },
  42. /**
  43. * Fetches all subviews that match a given `selector`.
  44. *
  45. * If no `selector` is provided, it will grab all subviews attached
  46. * to the view's root.
  47. *
  48. * @since 3.5.0
  49. *
  50. * @param {string} selector A jQuery selector.
  51. *
  52. * @return {Array} All the subviews that match the selector.
  53. */
  54. get: function( selector ) {
  55. selector = selector || '';
  56. return this._views[ selector ];
  57. },
  58. /**
  59. * Fetches the first subview that matches a given `selector`.
  60. *
  61. * If no `selector` is provided, it will grab the first subview attached to the
  62. * view's root.
  63. *
  64. * Useful when a selector only has one subview at a time.
  65. *
  66. * @since 3.5.0
  67. *
  68. * @param {string} selector A jQuery selector.
  69. *
  70. * @return {Backbone.View} The view.
  71. */
  72. first: function( selector ) {
  73. var views = this.get( selector );
  74. return views && views.length ? views[0] : null;
  75. },
  76. /**
  77. * Registers subview(s).
  78. *
  79. * Registers any number of `views` to a `selector`.
  80. *
  81. * When no `selector` is provided, the root selector (the empty string)
  82. * is used. `views` accepts a `Backbone.View` instance or an array of
  83. * `Backbone.View` instances.
  84. *
  85. * ---
  86. *
  87. * Accepts an `options` object, which has a significant effect on the
  88. * resulting behavior.
  89. *
  90. * `options.silent` - *boolean, `false`*
  91. * If `options.silent` is true, no DOM modifications will be made.
  92. *
  93. * `options.add` - *boolean, `false`*
  94. * Use `Views.add()` as a shortcut for setting `options.add` to true.
  95. *
  96. * By default, the provided `views` will replace any existing views
  97. * associated with the selector. If `options.add` is true, the provided
  98. * `views` will be added to the existing views.
  99. *
  100. * `options.at` - *integer, `undefined`*
  101. * When adding, to insert `views` at a specific index, use `options.at`.
  102. * By default, `views` are added to the end of the array.
  103. *
  104. * @since 3.5.0
  105. *
  106. * @param {string} selector A jQuery selector.
  107. * @param {Array|Object} views The subviews for the main view.
  108. * @param {Object} options Options for call. If `options.silent` is true,
  109. * no DOM modifications will be made. Use
  110. * `Views.add()` as a shortcut for setting
  111. * `options.add` to true. If `options.add` is
  112. * true, the provided `views` will be added to
  113. * the existing views. When adding, to insert
  114. * `views` at a specific index, use `options.at`.
  115. *
  116. * @return {wp.Backbone.Subviews} The current Subviews instance.
  117. */
  118. set: function( selector, views, options ) {
  119. var existing, next;
  120. if ( ! _.isString( selector ) ) {
  121. options = views;
  122. views = selector;
  123. selector = '';
  124. }
  125. options = options || {};
  126. views = _.isArray( views ) ? views : [ views ];
  127. existing = this.get( selector );
  128. next = views;
  129. if ( existing ) {
  130. if ( options.add ) {
  131. if ( _.isUndefined( options.at ) ) {
  132. next = existing.concat( views );
  133. } else {
  134. next = existing;
  135. next.splice.apply( next, [ options.at, 0 ].concat( views ) );
  136. }
  137. } else {
  138. _.each( next, function( view ) {
  139. view.__detach = true;
  140. });
  141. _.each( existing, function( view ) {
  142. if ( view.__detach )
  143. view.$el.detach();
  144. else
  145. view.remove();
  146. });
  147. _.each( next, function( view ) {
  148. delete view.__detach;
  149. });
  150. }
  151. }
  152. this._views[ selector ] = next;
  153. _.each( views, function( subview ) {
  154. var constructor = subview.Views || wp.Backbone.Subviews,
  155. subviews = subview.views = subview.views || new constructor( subview );
  156. subviews.parent = this.view;
  157. subviews.selector = selector;
  158. }, this );
  159. if ( ! options.silent )
  160. this._attach( selector, views, _.extend({ ready: this._isReady() }, options ) );
  161. return this;
  162. },
  163. /**
  164. * Add subview(s) to existing subviews.
  165. *
  166. * An alias to `Views.set()`, which defaults `options.add` to true.
  167. *
  168. * Adds any number of `views` to a `selector`.
  169. *
  170. * When no `selector` is provided, the root selector (the empty string)
  171. * is used. `views` accepts a `Backbone.View` instance or an array of
  172. * `Backbone.View` instances.
  173. *
  174. * Uses `Views.set()` when setting `options.add` to `false`.
  175. *
  176. * Accepts an `options` object. By default, provided `views` will be
  177. * inserted at the end of the array of existing views. To insert
  178. * `views` at a specific index, use `options.at`. If `options.silent`
  179. * is true, no DOM modifications will be made.
  180. *
  181. * For more information on the `options` object, see `Views.set()`.
  182. *
  183. * @since 3.5.0
  184. *
  185. * @param {string} selector A jQuery selector.
  186. * @param {Array|Object} views The subviews for the main view.
  187. * @param {Object} options Options for call. To insert `views` at a
  188. * specific index, use `options.at`. If
  189. * `options.silent` is true, no DOM modifications
  190. * will be made.
  191. *
  192. * @return {wp.Backbone.Subviews} The current subviews instance.
  193. */
  194. add: function( selector, views, options ) {
  195. if ( ! _.isString( selector ) ) {
  196. options = views;
  197. views = selector;
  198. selector = '';
  199. }
  200. return this.set( selector, views, _.extend({ add: true }, options ) );
  201. },
  202. /**
  203. * Removes an added subview.
  204. *
  205. * Stops tracking `views` registered to a `selector`. If no `views` are
  206. * set, then all of the `selector`'s subviews will be unregistered and
  207. * removed.
  208. *
  209. * Accepts an `options` object. If `options.silent` is set, `remove`
  210. * will *not* be triggered on the unregistered views.
  211. *
  212. * @since 3.5.0
  213. *
  214. * @param {string} selector A jQuery selector.
  215. * @param {Array|Object} views The subviews for the main view.
  216. * @param {Object} options Options for call. If `options.silent` is set,
  217. * `remove` will *not* be triggered on the
  218. * unregistered views.
  219. *
  220. * @return {wp.Backbone.Subviews} The current Subviews instance.
  221. */
  222. unset: function( selector, views, options ) {
  223. var existing;
  224. if ( ! _.isString( selector ) ) {
  225. options = views;
  226. views = selector;
  227. selector = '';
  228. }
  229. views = views || [];
  230. if ( existing = this.get( selector ) ) {
  231. views = _.isArray( views ) ? views : [ views ];
  232. this._views[ selector ] = views.length ? _.difference( existing, views ) : [];
  233. }
  234. if ( ! options || ! options.silent )
  235. _.invoke( views, 'remove' );
  236. return this;
  237. },
  238. /**
  239. * Detaches all subviews.
  240. *
  241. * Helps to preserve all subview events when re-rendering the master
  242. * view. Used in conjunction with `Views.render()`.
  243. *
  244. * @since 3.5.0
  245. *
  246. * @return {wp.Backbone.Subviews} The current Subviews instance.
  247. */
  248. detach: function() {
  249. $( _.pluck( this.all(), 'el' ) ).detach();
  250. return this;
  251. },
  252. /**
  253. * Renders all subviews.
  254. *
  255. * Used in conjunction with `Views.detach()`.
  256. *
  257. * @since 3.5.0
  258. *
  259. * @return {wp.Backbone.Subviews} The current Subviews instance.
  260. */
  261. render: function() {
  262. var options = {
  263. ready: this._isReady()
  264. };
  265. _.each( this._views, function( views, selector ) {
  266. this._attach( selector, views, options );
  267. }, this );
  268. this.rendered = true;
  269. return this;
  270. },
  271. /**
  272. * Removes all subviews.
  273. *
  274. * Triggers the `remove()` method on all subviews. Detaches the master
  275. * view from its parent. Resets the internals of the views manager.
  276. *
  277. * Accepts an `options` object. If `options.silent` is set, `unset`
  278. * will *not* be triggered on the master view's parent.
  279. *
  280. * @since 3.6.0
  281. *
  282. * @param {Object} options Options for call.
  283. * @param {boolean} options.silent If true, `unset` wil *not* be triggered on
  284. * the master views' parent.
  285. *
  286. * @return {wp.Backbone.Subviews} The current Subviews instance.
  287. */
  288. remove: function( options ) {
  289. if ( ! options || ! options.silent ) {
  290. if ( this.parent && this.parent.views )
  291. this.parent.views.unset( this.selector, this.view, { silent: true });
  292. delete this.parent;
  293. delete this.selector;
  294. }
  295. _.invoke( this.all(), 'remove' );
  296. this._views = [];
  297. return this;
  298. },
  299. /**
  300. * Replaces a selector's subviews
  301. *
  302. * By default, sets the `$target` selector's html to the subview `els`.
  303. *
  304. * Can be overridden in subclasses.
  305. *
  306. * @since 3.5.0
  307. *
  308. * @param {string} $target Selector where to put the elements.
  309. * @param {*} els HTML or elements to put into the selector's HTML.
  310. *
  311. * @return {wp.Backbone.Subviews} The current Subviews instance.
  312. */
  313. replace: function( $target, els ) {
  314. $target.html( els );
  315. return this;
  316. },
  317. /**
  318. * Insert subviews into a selector.
  319. *
  320. * By default, appends the subview `els` to the end of the `$target`
  321. * selector. If `options.at` is set, inserts the subview `els` at the
  322. * provided index.
  323. *
  324. * Can be overridden in subclasses.
  325. *
  326. * @since 3.5.0
  327. *
  328. * @param {string} $target Selector where to put the elements.
  329. * @param {*} els HTML or elements to put at the end of the
  330. * $target.
  331. * @param {?Object} options Options for call.
  332. * @param {?number} options.at At which index to put the elements.
  333. *
  334. * @return {wp.Backbone.Subviews} The current Subviews instance.
  335. */
  336. insert: function( $target, els, options ) {
  337. var at = options && options.at,
  338. $children;
  339. if ( _.isNumber( at ) && ($children = $target.children()).length > at )
  340. $children.eq( at ).before( els );
  341. else
  342. $target.append( els );
  343. return this;
  344. },
  345. /**
  346. * Triggers the ready event.
  347. *
  348. * Only use this method if you know what you're doing. For performance reasons,
  349. * this method does not check if the view is actually attached to the DOM. It's
  350. * taking your word for it.
  351. *
  352. * Fires the ready event on the current view and all attached subviews.
  353. *
  354. * @since 3.5.0
  355. */
  356. ready: function() {
  357. this.view.trigger('ready');
  358. // Find all attached subviews, and call ready on them.
  359. _.chain( this.all() ).map( function( view ) {
  360. return view.views;
  361. }).flatten().where({ attached: true }).invoke('ready');
  362. },
  363. /**
  364. * Attaches a series of views to a selector. Internal.
  365. *
  366. * Checks to see if a matching selector exists, renders the views,
  367. * performs the proper DOM operation, and then checks if the view is
  368. * attached to the document.
  369. *
  370. * @since 3.5.0
  371. *
  372. * @private
  373. *
  374. * @param {string} selector A jQuery selector.
  375. * @param {Array|Object} views The subviews for the main view.
  376. * @param {Object} options Options for call.
  377. * @param {boolean} options.add If true the provided views will be added.
  378. *
  379. * @return {wp.Backbone.Subviews} The current Subviews instance.
  380. */
  381. _attach: function( selector, views, options ) {
  382. var $selector = selector ? this.view.$( selector ) : this.view.$el,
  383. managers;
  384. // Check if we found a location to attach the views.
  385. if ( ! $selector.length )
  386. return this;
  387. managers = _.chain( views ).pluck('views').flatten().value();
  388. // Render the views if necessary.
  389. _.each( managers, function( manager ) {
  390. if ( manager.rendered )
  391. return;
  392. manager.view.render();
  393. manager.rendered = true;
  394. }, this );
  395. // Insert or replace the views.
  396. this[ options.add ? 'insert' : 'replace' ]( $selector, _.pluck( views, 'el' ), options );
  397. /*
  398. * Set attached and trigger ready if the current view is already
  399. * attached to the DOM.
  400. */
  401. _.each( managers, function( manager ) {
  402. manager.attached = true;
  403. if ( options.ready )
  404. manager.ready();
  405. }, this );
  406. return this;
  407. },
  408. /**
  409. * Determines whether or not the current view is in the DOM.
  410. *
  411. * @since 3.5.0
  412. *
  413. * @private
  414. *
  415. * @return {boolean} Whether or not the current view is in the DOM.
  416. */
  417. _isReady: function() {
  418. var node = this.view.el;
  419. while ( node ) {
  420. if ( node === document.body )
  421. return true;
  422. node = node.parentNode;
  423. }
  424. return false;
  425. }
  426. });
  427. wp.Backbone.View = Backbone.View.extend({
  428. // The constructor for the `Views` manager.
  429. Subviews: wp.Backbone.Subviews,
  430. /**
  431. * The base view class.
  432. *
  433. * This extends the backbone view to have a build-in way to use subviews. This
  434. * makes it easier to have nested views.
  435. *
  436. * @since 3.5.0
  437. * @since 3.6.0 Moved wp.media.View to wp.Backbone.View
  438. *
  439. * @constructs
  440. * @augments Backbone.View
  441. *
  442. * @memberOf wp.Backbone
  443. *
  444. *
  445. * @param {Object} options The options for this view.
  446. */
  447. constructor: function( options ) {
  448. this.views = new this.Subviews( this, this.views );
  449. this.on( 'ready', this.ready, this );
  450. this.options = options || {};
  451. Backbone.View.apply( this, arguments );
  452. },
  453. /**
  454. * Removes this view and all subviews.
  455. *
  456. * @since 3.5.0
  457. *
  458. * @return {wp.Backbone.Subviews} The current Subviews instance.
  459. */
  460. remove: function() {
  461. var result = Backbone.View.prototype.remove.apply( this, arguments );
  462. // Recursively remove child views.
  463. if ( this.views )
  464. this.views.remove();
  465. return result;
  466. },
  467. /**
  468. * Renders this view and all subviews.
  469. *
  470. * @since 3.5.0
  471. *
  472. * @return {wp.Backbone.View} The current instance of the view.
  473. */
  474. render: function() {
  475. var options;
  476. if ( this.prepare )
  477. options = this.prepare();
  478. this.views.detach();
  479. if ( this.template ) {
  480. options = options || {};
  481. this.trigger( 'prepare', options );
  482. this.$el.html( this.template( options ) );
  483. }
  484. this.views.render();
  485. return this;
  486. },
  487. /**
  488. * Returns the options for this view.
  489. *
  490. * @since 3.5.0
  491. *
  492. * @return {Object} The options for this view.
  493. */
  494. prepare: function() {
  495. return this.options;
  496. },
  497. /**
  498. * Method that is called when the ready event is triggered.
  499. *
  500. * @since 3.5.0
  501. */
  502. ready: function() {}
  503. });
  504. }(jQuery));