wp-backbone.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391
  1. /** @namespace wp */
  2. window.wp = window.wp || {};
  3. (function ($) {
  4. /**
  5. * Create the WordPress Backbone namespace.
  6. *
  7. * @namespace wp.Backbone
  8. */
  9. wp.Backbone = {};
  10. // wp.Backbone.Subviews
  11. // --------------------
  12. //
  13. // A subview manager.
  14. wp.Backbone.Subviews = function( view, views ) {
  15. this.view = view;
  16. this._views = _.isArray( views ) ? { '': views } : views || {};
  17. };
  18. wp.Backbone.Subviews.extend = Backbone.Model.extend;
  19. _.extend( wp.Backbone.Subviews.prototype, {
  20. // ### Fetch all of the subviews
  21. //
  22. // Returns an array of all subviews.
  23. all: function() {
  24. return _.flatten( _.values( this._views ) );
  25. },
  26. // ### Get a selector's subviews
  27. //
  28. // Fetches all subviews that match a given `selector`.
  29. //
  30. // If no `selector` is provided, it will grab all subviews attached
  31. // to the view's root.
  32. get: function( selector ) {
  33. selector = selector || '';
  34. return this._views[ selector ];
  35. },
  36. // ### Get a selector's first subview
  37. //
  38. // Fetches the first subview that matches a given `selector`.
  39. //
  40. // If no `selector` is provided, it will grab the first subview
  41. // attached to the view's root.
  42. //
  43. // Useful when a selector only has one subview at a time.
  44. first: function( selector ) {
  45. var views = this.get( selector );
  46. return views && views.length ? views[0] : null;
  47. },
  48. // ### Register subview(s)
  49. //
  50. // Registers any number of `views` to a `selector`.
  51. //
  52. // When no `selector` is provided, the root selector (the empty string)
  53. // is used. `views` accepts a `Backbone.View` instance or an array of
  54. // `Backbone.View` instances.
  55. //
  56. // ---
  57. //
  58. // Accepts an `options` object, which has a significant effect on the
  59. // resulting behavior.
  60. //
  61. // `options.silent` – *boolean, `false`*
  62. // > If `options.silent` is true, no DOM modifications will be made.
  63. //
  64. // `options.add` – *boolean, `false`*
  65. // > Use `Views.add()` as a shortcut for setting `options.add` to true.
  66. //
  67. // > By default, the provided `views` will replace
  68. // any existing views associated with the selector. If `options.add`
  69. // is true, the provided `views` will be added to the existing views.
  70. //
  71. // `options.at` – *integer, `undefined`*
  72. // > When adding, to insert `views` at a specific index, use
  73. // `options.at`. By default, `views` are added to the end of the array.
  74. set: function( selector, views, options ) {
  75. var existing, next;
  76. if ( ! _.isString( selector ) ) {
  77. options = views;
  78. views = selector;
  79. selector = '';
  80. }
  81. options = options || {};
  82. views = _.isArray( views ) ? views : [ views ];
  83. existing = this.get( selector );
  84. next = views;
  85. if ( existing ) {
  86. if ( options.add ) {
  87. if ( _.isUndefined( options.at ) ) {
  88. next = existing.concat( views );
  89. } else {
  90. next = existing;
  91. next.splice.apply( next, [ options.at, 0 ].concat( views ) );
  92. }
  93. } else {
  94. _.each( next, function( view ) {
  95. view.__detach = true;
  96. });
  97. _.each( existing, function( view ) {
  98. if ( view.__detach )
  99. view.$el.detach();
  100. else
  101. view.remove();
  102. });
  103. _.each( next, function( view ) {
  104. delete view.__detach;
  105. });
  106. }
  107. }
  108. this._views[ selector ] = next;
  109. _.each( views, function( subview ) {
  110. var constructor = subview.Views || wp.Backbone.Subviews,
  111. subviews = subview.views = subview.views || new constructor( subview );
  112. subviews.parent = this.view;
  113. subviews.selector = selector;
  114. }, this );
  115. if ( ! options.silent )
  116. this._attach( selector, views, _.extend({ ready: this._isReady() }, options ) );
  117. return this;
  118. },
  119. // ### Add subview(s) to existing subviews
  120. //
  121. // An alias to `Views.set()`, which defaults `options.add` to true.
  122. //
  123. // Adds any number of `views` to a `selector`.
  124. //
  125. // When no `selector` is provided, the root selector (the empty string)
  126. // is used. `views` accepts a `Backbone.View` instance or an array of
  127. // `Backbone.View` instances.
  128. //
  129. // Use `Views.set()` when setting `options.add` to `false`.
  130. //
  131. // Accepts an `options` object. By default, provided `views` will be
  132. // inserted at the end of the array of existing views. To insert
  133. // `views` at a specific index, use `options.at`. If `options.silent`
  134. // is true, no DOM modifications will be made.
  135. //
  136. // For more information on the `options` object, see `Views.set()`.
  137. add: function( selector, views, options ) {
  138. if ( ! _.isString( selector ) ) {
  139. options = views;
  140. views = selector;
  141. selector = '';
  142. }
  143. return this.set( selector, views, _.extend({ add: true }, options ) );
  144. },
  145. // ### Stop tracking subviews
  146. //
  147. // Stops tracking `views` registered to a `selector`. If no `views` are
  148. // set, then all of the `selector`'s subviews will be unregistered and
  149. // removed.
  150. //
  151. // Accepts an `options` object. If `options.silent` is set, `remove`
  152. // will *not* be triggered on the unregistered views.
  153. unset: function( selector, views, options ) {
  154. var existing;
  155. if ( ! _.isString( selector ) ) {
  156. options = views;
  157. views = selector;
  158. selector = '';
  159. }
  160. views = views || [];
  161. if ( existing = this.get( selector ) ) {
  162. views = _.isArray( views ) ? views : [ views ];
  163. this._views[ selector ] = views.length ? _.difference( existing, views ) : [];
  164. }
  165. if ( ! options || ! options.silent )
  166. _.invoke( views, 'remove' );
  167. return this;
  168. },
  169. // ### Detach all subviews
  170. //
  171. // Detaches all subviews from the DOM.
  172. //
  173. // Helps to preserve all subview events when re-rendering the master
  174. // view. Used in conjunction with `Views.render()`.
  175. detach: function() {
  176. $( _.pluck( this.all(), 'el' ) ).detach();
  177. return this;
  178. },
  179. // ### Render all subviews
  180. //
  181. // Renders all subviews. Used in conjunction with `Views.detach()`.
  182. render: function() {
  183. var options = {
  184. ready: this._isReady()
  185. };
  186. _.each( this._views, function( views, selector ) {
  187. this._attach( selector, views, options );
  188. }, this );
  189. this.rendered = true;
  190. return this;
  191. },
  192. // ### Remove all subviews
  193. //
  194. // Triggers the `remove()` method on all subviews. Detaches the master
  195. // view from its parent. Resets the internals of the views manager.
  196. //
  197. // Accepts an `options` object. If `options.silent` is set, `unset`
  198. // will *not* be triggered on the master view's parent.
  199. remove: function( options ) {
  200. if ( ! options || ! options.silent ) {
  201. if ( this.parent && this.parent.views )
  202. this.parent.views.unset( this.selector, this.view, { silent: true });
  203. delete this.parent;
  204. delete this.selector;
  205. }
  206. _.invoke( this.all(), 'remove' );
  207. this._views = [];
  208. return this;
  209. },
  210. // ### Replace a selector's subviews
  211. //
  212. // By default, sets the `$target` selector's html to the subview `els`.
  213. //
  214. // Can be overridden in subclasses.
  215. replace: function( $target, els ) {
  216. $target.html( els );
  217. return this;
  218. },
  219. // ### Insert subviews into a selector
  220. //
  221. // By default, appends the subview `els` to the end of the `$target`
  222. // selector. If `options.at` is set, inserts the subview `els` at the
  223. // provided index.
  224. //
  225. // Can be overridden in subclasses.
  226. insert: function( $target, els, options ) {
  227. var at = options && options.at,
  228. $children;
  229. if ( _.isNumber( at ) && ($children = $target.children()).length > at )
  230. $children.eq( at ).before( els );
  231. else
  232. $target.append( els );
  233. return this;
  234. },
  235. // ### Trigger the ready event
  236. //
  237. // **Only use this method if you know what you're doing.**
  238. // For performance reasons, this method does not check if the view is
  239. // actually attached to the DOM. It's taking your word for it.
  240. //
  241. // Fires the ready event on the current view and all attached subviews.
  242. ready: function() {
  243. this.view.trigger('ready');
  244. // Find all attached subviews, and call ready on them.
  245. _.chain( this.all() ).map( function( view ) {
  246. return view.views;
  247. }).flatten().where({ attached: true }).invoke('ready');
  248. },
  249. // #### Internal. Attaches a series of views to a selector.
  250. //
  251. // Checks to see if a matching selector exists, renders the views,
  252. // performs the proper DOM operation, and then checks if the view is
  253. // attached to the document.
  254. _attach: function( selector, views, options ) {
  255. var $selector = selector ? this.view.$( selector ) : this.view.$el,
  256. managers;
  257. // Check if we found a location to attach the views.
  258. if ( ! $selector.length )
  259. return this;
  260. managers = _.chain( views ).pluck('views').flatten().value();
  261. // Render the views if necessary.
  262. _.each( managers, function( manager ) {
  263. if ( manager.rendered )
  264. return;
  265. manager.view.render();
  266. manager.rendered = true;
  267. }, this );
  268. // Insert or replace the views.
  269. this[ options.add ? 'insert' : 'replace' ]( $selector, _.pluck( views, 'el' ), options );
  270. // Set attached and trigger ready if the current view is already
  271. // attached to the DOM.
  272. _.each( managers, function( manager ) {
  273. manager.attached = true;
  274. if ( options.ready )
  275. manager.ready();
  276. }, this );
  277. return this;
  278. },
  279. // #### Internal. Checks if the current view is in the DOM.
  280. _isReady: function() {
  281. var node = this.view.el;
  282. while ( node ) {
  283. if ( node === document.body )
  284. return true;
  285. node = node.parentNode;
  286. }
  287. return false;
  288. }
  289. });
  290. // wp.Backbone.View
  291. // ----------------
  292. //
  293. // The base view class.
  294. wp.Backbone.View = Backbone.View.extend({
  295. // The constructor for the `Views` manager.
  296. Subviews: wp.Backbone.Subviews,
  297. constructor: function( options ) {
  298. this.views = new this.Subviews( this, this.views );
  299. this.on( 'ready', this.ready, this );
  300. this.options = options || {};
  301. Backbone.View.apply( this, arguments );
  302. },
  303. remove: function() {
  304. var result = Backbone.View.prototype.remove.apply( this, arguments );
  305. // Recursively remove child views.
  306. if ( this.views )
  307. this.views.remove();
  308. return result;
  309. },
  310. render: function() {
  311. var options;
  312. if ( this.prepare )
  313. options = this.prepare();
  314. this.views.detach();
  315. if ( this.template ) {
  316. options = options || {};
  317. this.trigger( 'prepare', options );
  318. this.$el.html( this.template( options ) );
  319. }
  320. this.views.render();
  321. return this;
  322. },
  323. prepare: function() {
  324. return this.options;
  325. },
  326. ready: function() {}
  327. });
  328. }(jQuery));