PageRenderTime 60ms CodeModel.GetById 29ms RepoModel.GetById 0ms app.codeStats 0ms

/static/js/views/listview.js

https://bitbucket.org/gordonbrander/accordion-drawer-prototypes
JavaScript | 316 lines | 170 code | 60 blank | 86 comment | 25 complexity | dbe7c5aabc13e42bb303d6a913567fc5 MD5 | raw file
Possible License(s): Apache-2.0
  1. define([
  2. '$',
  3. 'underscore',
  4. 'backbone',
  5. 'views/singleview',
  6. 'logger'
  7. ], function (
  8. $,
  9. util,
  10. Backbone,
  11. SingleView,
  12. logger
  13. ) {
  14. var slice = Array.prototype.slice;
  15. // Used to republish events.
  16. var republishAs = function () {
  17. this.trigger.apply(this, arguments);
  18. };
  19. // Used as a fallback for template rendering.
  20. var emptyObject = {};
  21. // Used as a fallback for template function (to prevent exceptions).
  22. var noop = function () {};
  23. // A specialized form of `this.$` that allows querying by attachPoint name.
  24. // Attach to a view prototype.
  25. var $$ = function (selector) {
  26. return Backbone.View.prototype.$.call(
  27. this, this.attachPoints[selector] || selector
  28. );
  29. };
  30. // An ordered set of sub-views.
  31. // Conceptually corresponds to an array.
  32. // Usage:
  33. //
  34. // var view = new ListView({
  35. // view: MyViewCtor
  36. // });
  37. // view.collection.add(new Backbone.Model());
  38. var ListView = Backbone.View.extend({
  39. template: noop,
  40. view: SingleView,
  41. attachPoints: {
  42. 'list': ''
  43. },
  44. $: $$,
  45. initialize: function (options) {
  46. options = options || {};
  47. // Create a hash for storing view lookups.
  48. this._viewsByModelCid = {};
  49. this._views = [];
  50. this.collection = this.collection || new Backbone.Collection();
  51. // Set target element for subviews. Use scoped selector if element
  52. // isn't `this.el`.
  53. // Target is a legacy name for this attach point. We should change it
  54. // to "list" -GB.
  55. var target = options.target && this.attaches({ list: target });
  56. // If we still don't have a list attach point, set `this.el` as the
  57. // attach point.
  58. if (!this.attachPoints['list']) this.attaches({ list: this.el });
  59. // Set subview constructor.
  60. if(options.view) this.view = options.view;
  61. if (options.bridge) this.bridge = options.bridge;
  62. // Subscribe to default handlers for events.
  63. // Note that `render` is called for every event. Be sure to queue it last
  64. // for the events, so it renders based on the most up-to-date state.
  65. this.collection
  66. .bind('add', this.addView, this)
  67. .bind('add', this.render, this)
  68. .bind('remove', this.removeView, this)
  69. .bind('remove', this.render, this)
  70. .bind('move', this.render, this)
  71. .bind('move', this.moveView, this)
  72. .bind('reset', this.resetViews, this)
  73. .bind('reset', this.render, this)
  74. ;
  75. this.resetViews();
  76. },
  77. // Register attachPoints.
  78. attaches: function (obj) {
  79. // Create a shallow copy of `this.attachPoints` so we don't accidentally
  80. // modify the prototype.
  81. return this.attachPoints = util.extend({}, this.attachPoints, obj);
  82. },
  83. // Return a shallow clone of the `_views` array cache. Safe to manipulate
  84. // without negative rendering effects.
  85. views: function () {
  86. return util.clone(this._views);
  87. },
  88. // Look up a view reference by its model.
  89. // Profile: `[Model model] -> [View view]`
  90. lookup: function (model) {
  91. return this._viewsByModelCid[model.cid];
  92. },
  93. // Store a reference to a view by model.
  94. // Profile: `[Model model], [View view] -> [View view]`
  95. register: function (view) {
  96. var model = view.model;
  97. var i = this.collection.indexOf(model);
  98. // Update by-cid lookup hash.
  99. this._viewsByModelCid[model.cid] = view;
  100. // Update ordered views array, adding view at index of model.
  101. this._views.splice(i, 0, view);
  102. // Republish all events on the sub view in the parent view.
  103. view.bind('all', republishAs, this);
  104. return view;
  105. },
  106. // Delete a reference to a view by model.
  107. // Profile: `[Model model] -> [View view]`
  108. deregister: function (view) {
  109. var model = view.model;
  110. var i = util.indexOf(this._views, view);
  111. // Delete view from lookup hash.
  112. delete this._viewsByModelCid[model.cid];
  113. // Remove model from ordered views array.
  114. this._views.splice(i, 1);
  115. // Remove event republishing for this view.
  116. view.unbind('all', republishAs);
  117. return view;
  118. },
  119. // Deletes ALL view references and event republishing.
  120. deregisterAll: function () {
  121. this.collection.each(function (model) {
  122. // Check for view -- we want this to be safe to run, even if no views
  123. // have been registered for models yet. This precise scenario happens
  124. // the first time that a collection is populated and `reset` is called.
  125. var view = this.lookup(model);
  126. if (view) this.deregister(view);
  127. }, this);
  128. return this;
  129. },
  130. // General factory for subviews.
  131. createView: function (options) {
  132. var View = this.view,
  133. // Create view and store it in views lookup hash.
  134. view = this.register(new View(options));
  135. // could set data-itemid in an attributes property in the options
  136. // but we'll do it here
  137. // TODO: use cid instead of id, which might not *always* be set?
  138. view.el.setAttribute('data-itemid', view.model.id || view.model.cid);
  139. return view;
  140. },
  141. // Add a view to the DOM for a model. Creates the view and appends it to
  142. // the DOM.
  143. addView: function (model) {
  144. this.createView({ model: model, bridge: this.bridge });
  145. this.appendView(model);
  146. return this;
  147. },
  148. // Appends a view to the DOM for a given model in the collection.
  149. // Note that you will have to register the view before you can append it.
  150. appendView: function (model) {
  151. var view = this.lookup(model);
  152. if (!view) throw new Error('No view registered for this model.');
  153. var views = this._views;
  154. var domEls = $('list').children();
  155. // Get the index of the view reference from the ordered views.
  156. var i = util.indexOf(views, view);
  157. // Render the view and capture the element.
  158. var el = view.render().el;
  159. if (i > 0 && domEls.length > i) {
  160. this.$(views[i].el).before(el);
  161. }
  162. else if (i === 0) {
  163. this.$('list').prepend(el);
  164. }
  165. else {
  166. this.$('list').append(el);
  167. }
  168. return this;
  169. },
  170. // Remove a view (referenced by it's model) from this listview's records
  171. // and from the DOM.
  172. removeView: function (model) {
  173. // Remove view from references and from DOM.
  174. // Calls view's `remove` method, allowing sub-views to customize how
  175. // they are removed.
  176. this.deregister(this.lookup(model)).remove();
  177. return this;
  178. },
  179. // Special-case rendering for views that have been moved in a collection.
  180. // By default, this does a remove followed by an immediate add, but re-uses
  181. // the original view object, rather than creating a new one.
  182. moveView: function (model, options) {
  183. // Look up the view.
  184. var view = this.lookup(model);
  185. // Remove view from the DOM and deregister it.
  186. this.removeView(model);
  187. // Re-register the view.
  188. this.register(view);
  189. // And append it again.
  190. this.appendView(model);
  191. },
  192. resetViews: function (collection) {
  193. // Remove all view references and event bindings.
  194. this.deregisterAll();
  195. // !!!!!! HACKY FIX !!!!!!
  196. // If we don't empty the contents of this HTML element, elements in the
  197. // target will not get cleared by the code below. You end up with a
  198. // duplicated set of list items. True story. I don't
  199. // know why, but I theorize that Zepto may be doing something with
  200. // the string below -- caching, perhaps? -GB
  201. $(this.el).html('');
  202. // Render the template function on the prototype, passing any model data
  203. // we have, or an empty object as a fallback.
  204. $(this.el).html(
  205. this.template(this.model ? this.model.toJSON() : emptyObject)
  206. );
  207. // Call `addView` factory, to re-create elements in DOM.
  208. // Pass model and collection to addView to mimic the arguments it
  209. // receives from model events.
  210. this.collection.each(this.addView, this);
  211. return this;
  212. },
  213. render: function () {
  214. // Hide this view if the collection is empty, otherwise, show it.
  215. $(this.el).toggle(this.collection.length);
  216. return this;
  217. },
  218. scrollIntoView: function(selectedNode) {
  219. // find the scrollable parent node (Y-axis only)
  220. var scrollParent = null,
  221. reScrollValue = /scroll|auto/,
  222. isScrollable = function (node){
  223. var cssScrollValue = $(node).css('overflow-y');
  224. return reScrollValue.test(cssScrollValue);
  225. };
  226. for(var node = selectedNode.parentNode; node && node.nodeType; node = node.parentNode){
  227. if(isScrollable(node)){
  228. scrollParent = node;
  229. break;
  230. }
  231. }
  232. if(!scrollParent){
  233. // no scroll parent, so can't scroll into view
  234. logger.log("StackPlacesView .scrollIntoView: no scrollParent found for node: " + selectedNode.tagName + '.' + selectedNode.className);
  235. return;
  236. }
  237. var top = selectedNode.offsetTop,
  238. bottom = top + selectedNode.offsetHeight;
  239. if( top >= scrollParent.scrollTop && bottom <= scrollParent.offsetHeight + scrollParent.scrollTop ){
  240. logger.log("StackPlacesView .scrollIntoView: no scrollTop change needed");
  241. // already within the viewport, do nothing
  242. return;
  243. }
  244. if(selectedNode.offsetTop < scrollParent.scrollTop){
  245. // currently above the viewport
  246. logger.log("StackPlacesView .scrollIntoView: currently above the viewport");
  247. scrollParent.scrollTop = selectedNode.offsetTop + selectedNode.offsetHeight > scrollParent.offsetHeight ?
  248. selectedNode.offsetTop : 0;
  249. } else if(selectedNode.offsetTop + selectedNode.offsetHeight > scrollParent.offsetHeight) {
  250. // currently below the viewport
  251. scrollParent.scrollTop = selectedNode.offsetTop + selectedNode.offsetHeight - scrollParent.offsetHeight;
  252. }
  253. }
  254. });
  255. return ListView;
  256. });