/static/js/views/listview.js
JavaScript | 316 lines | 170 code | 60 blank | 86 comment | 25 complexity | dbe7c5aabc13e42bb303d6a913567fc5 MD5 | raw file
Possible License(s): Apache-2.0
- define([
- '$',
- 'underscore',
- 'backbone',
- 'views/singleview',
- 'logger'
- ], function (
- $,
- util,
- Backbone,
- SingleView,
- logger
- ) {
- var slice = Array.prototype.slice;
- // Used to republish events.
- var republishAs = function () {
- this.trigger.apply(this, arguments);
- };
- // Used as a fallback for template rendering.
- var emptyObject = {};
- // Used as a fallback for template function (to prevent exceptions).
- var noop = function () {};
- // A specialized form of `this.$` that allows querying by attachPoint name.
- // Attach to a view prototype.
- var $$ = function (selector) {
- return Backbone.View.prototype.$.call(
- this, this.attachPoints[selector] || selector
- );
- };
- // An ordered set of sub-views.
- // Conceptually corresponds to an array.
- // Usage:
- //
- // var view = new ListView({
- // view: MyViewCtor
- // });
- // view.collection.add(new Backbone.Model());
- var ListView = Backbone.View.extend({
- template: noop,
- view: SingleView,
- attachPoints: {
- 'list': ''
- },
- $: $$,
- initialize: function (options) {
- options = options || {};
- // Create a hash for storing view lookups.
- this._viewsByModelCid = {};
- this._views = [];
- this.collection = this.collection || new Backbone.Collection();
- // Set target element for subviews. Use scoped selector if element
- // isn't `this.el`.
- // Target is a legacy name for this attach point. We should change it
- // to "list" -GB.
- var target = options.target && this.attaches({ list: target });
- // If we still don't have a list attach point, set `this.el` as the
- // attach point.
- if (!this.attachPoints['list']) this.attaches({ list: this.el });
- // Set subview constructor.
- if(options.view) this.view = options.view;
-
- if (options.bridge) this.bridge = options.bridge;
- // Subscribe to default handlers for events.
- // Note that `render` is called for every event. Be sure to queue it last
- // for the events, so it renders based on the most up-to-date state.
- this.collection
- .bind('add', this.addView, this)
- .bind('add', this.render, this)
- .bind('remove', this.removeView, this)
- .bind('remove', this.render, this)
- .bind('move', this.render, this)
- .bind('move', this.moveView, this)
- .bind('reset', this.resetViews, this)
- .bind('reset', this.render, this)
- ;
- this.resetViews();
- },
- // Register attachPoints.
- attaches: function (obj) {
- // Create a shallow copy of `this.attachPoints` so we don't accidentally
- // modify the prototype.
- return this.attachPoints = util.extend({}, this.attachPoints, obj);
- },
- // Return a shallow clone of the `_views` array cache. Safe to manipulate
- // without negative rendering effects.
- views: function () {
- return util.clone(this._views);
- },
- // Look up a view reference by its model.
- // Profile: `[Model model] -> [View view]`
- lookup: function (model) {
- return this._viewsByModelCid[model.cid];
- },
- // Store a reference to a view by model.
- // Profile: `[Model model], [View view] -> [View view]`
- register: function (view) {
- var model = view.model;
- var i = this.collection.indexOf(model);
- // Update by-cid lookup hash.
- this._viewsByModelCid[model.cid] = view;
- // Update ordered views array, adding view at index of model.
- this._views.splice(i, 0, view);
- // Republish all events on the sub view in the parent view.
- view.bind('all', republishAs, this);
- return view;
- },
- // Delete a reference to a view by model.
- // Profile: `[Model model] -> [View view]`
- deregister: function (view) {
- var model = view.model;
- var i = util.indexOf(this._views, view);
- // Delete view from lookup hash.
- delete this._viewsByModelCid[model.cid];
- // Remove model from ordered views array.
- this._views.splice(i, 1);
- // Remove event republishing for this view.
- view.unbind('all', republishAs);
- return view;
- },
- // Deletes ALL view references and event republishing.
- deregisterAll: function () {
- this.collection.each(function (model) {
- // Check for view -- we want this to be safe to run, even if no views
- // have been registered for models yet. This precise scenario happens
- // the first time that a collection is populated and `reset` is called.
- var view = this.lookup(model);
- if (view) this.deregister(view);
- }, this);
- return this;
- },
- // General factory for subviews.
- createView: function (options) {
- var View = this.view,
- // Create view and store it in views lookup hash.
- view = this.register(new View(options));
- // could set data-itemid in an attributes property in the options
- // but we'll do it here
- // TODO: use cid instead of id, which might not *always* be set?
- view.el.setAttribute('data-itemid', view.model.id || view.model.cid);
- return view;
- },
- // Add a view to the DOM for a model. Creates the view and appends it to
- // the DOM.
- addView: function (model) {
- this.createView({ model: model, bridge: this.bridge });
- this.appendView(model);
- return this;
- },
- // Appends a view to the DOM for a given model in the collection.
- // Note that you will have to register the view before you can append it.
- appendView: function (model) {
- var view = this.lookup(model);
- if (!view) throw new Error('No view registered for this model.');
- var views = this._views;
- var domEls = $('list').children();
- // Get the index of the view reference from the ordered views.
- var i = util.indexOf(views, view);
- // Render the view and capture the element.
- var el = view.render().el;
- if (i > 0 && domEls.length > i) {
- this.$(views[i].el).before(el);
- }
- else if (i === 0) {
- this.$('list').prepend(el);
- }
- else {
- this.$('list').append(el);
- }
- return this;
- },
- // Remove a view (referenced by it's model) from this listview's records
- // and from the DOM.
- removeView: function (model) {
- // Remove view from references and from DOM.
- // Calls view's `remove` method, allowing sub-views to customize how
- // they are removed.
- this.deregister(this.lookup(model)).remove();
- return this;
- },
- // Special-case rendering for views that have been moved in a collection.
- // By default, this does a remove followed by an immediate add, but re-uses
- // the original view object, rather than creating a new one.
- moveView: function (model, options) {
- // Look up the view.
- var view = this.lookup(model);
- // Remove view from the DOM and deregister it.
- this.removeView(model);
- // Re-register the view.
- this.register(view);
- // And append it again.
- this.appendView(model);
- },
- resetViews: function (collection) {
- // Remove all view references and event bindings.
- this.deregisterAll();
- // !!!!!! HACKY FIX !!!!!!
- // If we don't empty the contents of this HTML element, elements in the
- // target will not get cleared by the code below. You end up with a
- // duplicated set of list items. True story. I don't
- // know why, but I theorize that Zepto may be doing something with
- // the string below -- caching, perhaps? -GB
- $(this.el).html('');
- // Render the template function on the prototype, passing any model data
- // we have, or an empty object as a fallback.
- $(this.el).html(
- this.template(this.model ? this.model.toJSON() : emptyObject)
- );
- // Call `addView` factory, to re-create elements in DOM.
- // Pass model and collection to addView to mimic the arguments it
- // receives from model events.
- this.collection.each(this.addView, this);
- return this;
- },
- render: function () {
- // Hide this view if the collection is empty, otherwise, show it.
- $(this.el).toggle(this.collection.length);
- return this;
- },
- scrollIntoView: function(selectedNode) {
- // find the scrollable parent node (Y-axis only)
- var scrollParent = null,
- reScrollValue = /scroll|auto/,
- isScrollable = function (node){
- var cssScrollValue = $(node).css('overflow-y');
- return reScrollValue.test(cssScrollValue);
- };
-
- for(var node = selectedNode.parentNode; node && node.nodeType; node = node.parentNode){
- if(isScrollable(node)){
- scrollParent = node;
- break;
- }
- }
- if(!scrollParent){
- // no scroll parent, so can't scroll into view
- logger.log("StackPlacesView .scrollIntoView: no scrollParent found for node: " + selectedNode.tagName + '.' + selectedNode.className);
- return;
- }
-
- var top = selectedNode.offsetTop,
- bottom = top + selectedNode.offsetHeight;
-
- if( top >= scrollParent.scrollTop && bottom <= scrollParent.offsetHeight + scrollParent.scrollTop ){
- logger.log("StackPlacesView .scrollIntoView: no scrollTop change needed");
- // already within the viewport, do nothing
- return;
- }
- if(selectedNode.offsetTop < scrollParent.scrollTop){
- // currently above the viewport
- logger.log("StackPlacesView .scrollIntoView: currently above the viewport");
- scrollParent.scrollTop = selectedNode.offsetTop + selectedNode.offsetHeight > scrollParent.offsetHeight ?
- selectedNode.offsetTop : 0;
- } else if(selectedNode.offsetTop + selectedNode.offsetHeight > scrollParent.offsetHeight) {
- // currently below the viewport
- scrollParent.scrollTop = selectedNode.offsetTop + selectedNode.offsetHeight - scrollParent.offsetHeight;
- }
- }
- });
-
- return ListView;
- });