PageRenderTime 40ms CodeModel.GetById 15ms RepoModel.GetById 0ms app.codeStats 0ms

/pancake-web/pancake/web/static/js/views/listview.js

https://bitbucket.org/mozillapancake/pancake
JavaScript | 363 lines | 202 code | 67 blank | 94 comment | 23 complexity | 8c82f82b7482f41c52c963ac0d860980 MD5 | raw file
Possible License(s): MPL-2.0-no-copyleft-exception, LGPL-2.1, MIT, Apache-2.0
  1. /* This Source Code Form is subject to the terms of the Mozilla Public
  2. * License, v. 2.0. If a copy of the MPL was not distributed with this
  3. * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
  4. define([
  5. '$',
  6. 'underscore',
  7. 'backbone',
  8. 'views/singleview',
  9. 'lib/promiseplusplus',
  10. 'logger'
  11. ], function (
  12. $,
  13. util,
  14. Backbone,
  15. SingleView,
  16. Promise,
  17. logger
  18. ) {
  19. var slice = Array.prototype.slice;
  20. // Used to republish events.
  21. var republishAs = function () {
  22. this.trigger.apply(this, arguments);
  23. };
  24. // An ordered set of sub-views.
  25. // Conceptually corresponds to an array.
  26. // Usage:
  27. //
  28. // var view = new ListView({
  29. // view: MyViewCtor
  30. // });
  31. // view.collection.add(new Backbone.Model());
  32. var ListView = Backbone.View.extend({
  33. declaredClass: 'ListView',
  34. template: function () {},
  35. view: SingleView,
  36. attachPoints: {
  37. 'list': '',
  38. 'fallback': '',
  39. 'loading': ''
  40. },
  41. // A specialized form of `this.$` that allows querying by attachPoint name.
  42. $: function (selector) {
  43. return Backbone.View.prototype.$.call(
  44. this, this.attachPoints[selector] || selector
  45. );
  46. },
  47. initialize: function (options) {
  48. options = options || {};
  49. // Create a hash for storing view lookups.
  50. this._viewsByModelCid = {};
  51. this._views = [];
  52. this.collection = this.collection || new Backbone.Collection();
  53. // Set target element for subviews. Use scoped selector if element
  54. // isn't `this.el`.
  55. // Target is a legacy name for this attach point. We should change it
  56. // to "list" -GB.
  57. var target = options.target && this.attaches({ list: target });
  58. // If we still don't have a list attach point, set `this.el` as the
  59. // attach point.
  60. if (!this.attachPoints['list']) this.attaches({ list: this.el });
  61. // Set subview constructor.
  62. if(options.view) this.view = options.view;
  63. if (options.bridge) this.bridge = options.bridge;
  64. // **Note** chaining these 2 function calls deliberately. If we do chain
  65. // them and some specialized class does not return `this` from
  66. // `bindEvents`, you get a very mysterious error. -GB.
  67. this.bindEvents();
  68. this.reset();
  69. },
  70. bindPlaceholder: function(){
  71. var self = this;
  72. var toggleOff = util.bind(function(){
  73. setTimeout(function(){
  74. var listEl = self.$('list')[0],
  75. placeholderEl = self.$('placeholder')[0];
  76. var contentHeight = Math.max(listEl.offsetHeight, listEl.scrollHeight);
  77. Promise.animate(
  78. placeholderEl,
  79. { height: contentHeight }, 600, 'ease-out'
  80. ).then(function(){
  81. $(self.el).removeClass("list--loading");
  82. $(placeholderEl).css({ height: ''}).addClass('hidden');
  83. });
  84. }, 0);
  85. }, this);
  86. // only hookup the loading placeholder if the view was explicitly configured to do so
  87. this.collection.bind('fetch', function(){
  88. $(self.el).addClass("list--loading");
  89. self.$('placeholder').removeClass("hidden")
  90. .css({
  91. opacity: 0.8,
  92. overflow: 'hidden',
  93. height: Math.max(50, self.el.offsetHeight), width: Math.max(624, self.el.offsetWidth),
  94. zIndex: 2
  95. });
  96. }, this);
  97. this.collection.bind('add', toggleOff);
  98. this.collection.bind('reset', toggleOff);
  99. },
  100. // Handles binding all collection and model events to the model.
  101. // Putting everything here allows specialized views to easily replace
  102. // event bindings.
  103. bindEvents: function () {
  104. // Subscribe to default handlers for events.
  105. // Note that `render` is called for every event. Be sure to queue it last
  106. // for the events, so it renders based on the most up-to-date state.
  107. if( this.options.showLoadingPlaceholder && this.attachPoints.placeholder){
  108. this.bindPlaceholder();
  109. } else {
  110. // console.log(this.declaredClass + ": skipping bindPlaceholder, as: ", this.className, this.options.showLoadingPlaceholder, this.attachPoints.placeholder);
  111. }
  112. this.collection
  113. .bind('state', this.onStateChange, this)
  114. .bind('add', this.addView, this)
  115. .bind('add', this.render, this)
  116. .bind('remove', this.removeView, this)
  117. .bind('remove', this.render, this)
  118. .bind('move', this.render, this)
  119. .bind('move', this.moveView, this)
  120. .bind('reset', this.resetViews, this)
  121. .bind('reset', this.render, this)
  122. ;
  123. return this;
  124. },
  125. // Return a JSON representation of the model for this view.
  126. // Will provide empty object fallback for the model.
  127. // Decouples the data from the model, allowing subclasses to filter the
  128. // data, for the template, mix together multiple models, etc.
  129. getModelData: function () {
  130. return this.model ? this.model.toJSON(): {};
  131. },
  132. // Register attachPoints.
  133. attaches: function (obj) {
  134. // Create a shallow copy of `this.attachPoints` so we don't accidentally
  135. // modify the prototype.
  136. return (this.attachPoints = util.extend({}, this.attachPoints, obj));
  137. },
  138. isAttached: function (key) {
  139. return !!this.attachPoints[key];
  140. },
  141. // Return a shallow clone of the `_views` array cache. Safe to manipulate
  142. // without negative rendering effects.
  143. views: function () {
  144. return util.clone(this._views);
  145. },
  146. $views: function () {
  147. var els = util.map(this._views, function (view) {
  148. return view.el;
  149. });
  150. return $(els);
  151. },
  152. // Look up a view reference by its model.
  153. // Profile: `[Model model] -> [View view]`
  154. lookup: function (model) {
  155. return this._viewsByModelCid[model.cid];
  156. },
  157. // Store a reference to a view by model.
  158. // Profile: `[Model model], [View view] -> [View view]`
  159. register: function (view) {
  160. var model = view.model;
  161. var i = this.collection.indexOf(model);
  162. // If the view's model does not appear in the collection, this sub-view
  163. // should not be allowed to be registered. Throw an exception.
  164. if (i === -1) throw new Error("View's model does not appear in ListView's collection.");
  165. // Update by-cid lookup hash.
  166. this._viewsByModelCid[model.cid] = view;
  167. // Update ordered views array, adding view at index of model.
  168. this._views.splice(i, 0, view);
  169. // Republish all events on the sub view in the parent view.
  170. view.bind('all', republishAs, this);
  171. return view;
  172. },
  173. // Delete a reference to a view by model.
  174. // Profile: `[Model model] -> [View view]`
  175. deregister: function (view) {
  176. var model = view.model;
  177. var i = util.indexOf(this._views, view);
  178. // Delete view from lookup hash.
  179. delete this._viewsByModelCid[model.cid];
  180. // Remove model from ordered views array.
  181. this._views.splice(i, 1);
  182. // Remove event republishing for this view.
  183. view.unbind('all', republishAs);
  184. return view;
  185. },
  186. // Deletes ALL view references and event republishing.
  187. deregisterAll: function () {
  188. this.collection.each(function (model) {
  189. // Check for view -- we want this to be safe to run, even if no views
  190. // have been registered for models yet. This precise scenario happens
  191. // the first time that a collection is populated and `reset` is called.
  192. var view = this.lookup(model);
  193. if (view) this.deregister(view);
  194. }, this);
  195. return this;
  196. },
  197. // General factory for subviews.
  198. createView: function (options) {
  199. var View = this.view,
  200. // Create view and store it in views lookup hash.
  201. view = this.register(new View(options));
  202. // could set data-itemid in an attributes property in the options
  203. // but we'll do it here
  204. // TODO: use cid instead of id, which might not *always* be set?
  205. view.el.setAttribute('data-itemid', view.model.id || view.model.cid);
  206. return view;
  207. },
  208. // Add a view to the DOM for a model. Creates the view and appends it to
  209. // the DOM.
  210. addView: function (model) {
  211. this.createView({ model: model, bridge: this.bridge });
  212. this.appendView(model);
  213. return this;
  214. },
  215. // Appends a view to the DOM for a given model in the collection.
  216. // Note that you will have to register the view before you can append it.
  217. appendView: function (model) {
  218. var view = this.lookup(model);
  219. if (!view) throw new Error('No view registered for this model.');
  220. var views = this._views;
  221. var domEls = $('list').children();
  222. // Get the index of the view reference from the ordered views.
  223. var i = util.indexOf(views, view);
  224. // Render the view and capture the element.
  225. var el = view.render().el;
  226. if (i > 0 && domEls.length > i) {
  227. this.$(views[i].el).before(el);
  228. }
  229. else if (i === 0) {
  230. this.$('list').prepend(el);
  231. }
  232. else {
  233. this.$('list').append(el);
  234. }
  235. return this;
  236. },
  237. // Remove a view (referenced by it's model) from this listview's records
  238. // and from the DOM.
  239. removeView: function (model) {
  240. // Remove view from references and from DOM.
  241. // Calls view's `remove` method, allowing sub-views to customize how
  242. // they are removed.
  243. this.deregister(this.lookup(model)).remove();
  244. return this;
  245. },
  246. // Special-case rendering for views that have been moved in a collection.
  247. // By default, this does a remove followed by an immediate add, but re-uses
  248. // the original view object, rather than creating a new one.
  249. moveView: function (model, options) {
  250. // Look up the view.
  251. var view = this.lookup(model);
  252. // Remove view from the DOM and deregister it.
  253. this.removeView(model);
  254. // Re-register the view.
  255. this.register(view);
  256. // And append it again.
  257. this.appendView(model);
  258. },
  259. reset: function () {
  260. // !!!!!! HACKY FIX !!!!!!
  261. // If we don't empty the contents of this HTML element, elements in the
  262. // target will not get cleared by the code below. You end up with a
  263. // duplicated set of list items. True story. I don't
  264. // know why, but I theorize that Zepto may be doing something with
  265. // the string below -- caching, perhaps? -GB
  266. $(this.el).html('');
  267. // Render the template function on the prototype, passing any model data
  268. // we have, or an empty object as a fallback.
  269. $(this.el).html(this.template(this.getModelData()));
  270. this.resetViews();
  271. return this;
  272. },
  273. resetViews: function () {
  274. this.deregisterAll();
  275. this.$('list').html('');
  276. // Call `addView` factory, to re-create elements in DOM.
  277. // Pass model and collection to addView to mimic the arguments it
  278. // receives from model events.
  279. this.collection.each(this.addView, this);
  280. return this;
  281. },
  282. render: function () {
  283. return this;
  284. },
  285. onStateChange: function (state) {
  286. if(this.options.showLoadingPlaceholder){
  287. // override, no toggling
  288. return this;
  289. } else {
  290. $(this.el).toggle(state !== 'loading' && !!this.collection.length);
  291. return this;
  292. }
  293. }
  294. });
  295. return ListView;
  296. });