/pancake-web/pancake/web/static/js/views/listview.js
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
- /* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
- define([
- '$',
- 'underscore',
- 'backbone',
- 'views/singleview',
- 'lib/promiseplusplus',
- 'logger'
- ], function (
- $,
- util,
- Backbone,
- SingleView,
- Promise,
- logger
- ) {
- var slice = Array.prototype.slice;
- // Used to republish events.
- var republishAs = function () {
- this.trigger.apply(this, arguments);
- };
- // 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({
- declaredClass: 'ListView',
- template: function () {},
- view: SingleView,
- attachPoints: {
- 'list': '',
- 'fallback': '',
- 'loading': ''
- },
- // A specialized form of `this.$` that allows querying by attachPoint name.
- $: function (selector) {
- return Backbone.View.prototype.$.call(
- this, this.attachPoints[selector] || selector
- );
- },
- 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;
- // **Note** chaining these 2 function calls deliberately. If we do chain
- // them and some specialized class does not return `this` from
- // `bindEvents`, you get a very mysterious error. -GB.
- this.bindEvents();
- this.reset();
- },
- bindPlaceholder: function(){
- var self = this;
-
- var toggleOff = util.bind(function(){
- setTimeout(function(){
- var listEl = self.$('list')[0],
- placeholderEl = self.$('placeholder')[0];
- var contentHeight = Math.max(listEl.offsetHeight, listEl.scrollHeight);
- Promise.animate(
- placeholderEl,
- { height: contentHeight }, 600, 'ease-out'
- ).then(function(){
- $(self.el).removeClass("list--loading");
- $(placeholderEl).css({ height: ''}).addClass('hidden');
- });
- }, 0);
- }, this);
-
- // only hookup the loading placeholder if the view was explicitly configured to do so
- this.collection.bind('fetch', function(){
- $(self.el).addClass("list--loading");
- self.$('placeholder').removeClass("hidden")
- .css({
- opacity: 0.8,
- overflow: 'hidden',
- height: Math.max(50, self.el.offsetHeight), width: Math.max(624, self.el.offsetWidth),
- zIndex: 2
- });
- }, this);
- this.collection.bind('add', toggleOff);
- this.collection.bind('reset', toggleOff);
- },
- // Handles binding all collection and model events to the model.
- // Putting everything here allows specialized views to easily replace
- // event bindings.
- bindEvents: function () {
- // 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.
- if( this.options.showLoadingPlaceholder && this.attachPoints.placeholder){
- this.bindPlaceholder();
- } else {
- // console.log(this.declaredClass + ": skipping bindPlaceholder, as: ", this.className, this.options.showLoadingPlaceholder, this.attachPoints.placeholder);
- }
-
- this.collection
- .bind('state', this.onStateChange, this)
- .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)
- ;
- return this;
- },
- // Return a JSON representation of the model for this view.
- // Will provide empty object fallback for the model.
- // Decouples the data from the model, allowing subclasses to filter the
- // data, for the template, mix together multiple models, etc.
- getModelData: function () {
- return this.model ? this.model.toJSON(): {};
- },
- // 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));
- },
- isAttached: function (key) {
- return !!this.attachPoints[key];
- },
- // Return a shallow clone of the `_views` array cache. Safe to manipulate
- // without negative rendering effects.
- views: function () {
- return util.clone(this._views);
- },
- $views: function () {
- var els = util.map(this._views, function (view) {
- return view.el;
- });
- return $(els);
- },
- // 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);
- // If the view's model does not appear in the collection, this sub-view
- // should not be allowed to be registered. Throw an exception.
- if (i === -1) throw new Error("View's model does not appear in ListView's collection.");
- // 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);
- },
- reset: function () {
- // !!!!!! 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.getModelData()));
- this.resetViews();
- return this;
- },
- resetViews: function () {
- this.deregisterAll();
- this.$('list').html('');
- // 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 () {
- return this;
- },
- onStateChange: function (state) {
- if(this.options.showLoadingPlaceholder){
- // override, no toggling
- return this;
- } else {
- $(this.el).toggle(state !== 'loading' && !!this.collection.length);
- return this;
- }
- }
- });
-
- return ListView;
- });