/pancake-web/pancake/web/static/js/views/drawerviews.js
JavaScript | 479 lines | 313 code | 96 blank | 70 comment | 14 complexity | 3d4fd1483030638bd2bc5091235b2cdd MD5 | raw file
Possible License(s): MPL-2.0-no-copyleft-exception, LGPL-2.1, MIT, Apache-2.0
- define([
- '$',
- 'underscore',
- 'backbone',
- 'views/singleview',
- 'views/listview',
- 'models/stackcollection',
- 'models/stackmodel',
- 'models/sitemodel',
- 'lib/template',
- 'lib/bridge',
- 'lib/promiseplusplus',
- 'lib/lazily'
- ], function (
- $,
- util,
- Backbone,
- SingleView,
- ListView,
- StackCollection,
- StackModel,
- SiteModel,
- template,
- Bridge,
- Promise,
- lazily
- ) {
- // Define shorthands for prototypes. This allows easy calls to `super` methods.
- var __SingleView = SingleView.prototype;
- var __ListView = ListView.prototype;
- var bridge = new Bridge();
- function isDescendant(node, ancestor){
- // summary:
- // Returns true if node is a descendant of ancestor
- // node: DOMNode
- // node reference to test
- // ancestor: DOMNode
- // node reference of potential parent to test against
- try{
- while(node && node.nodeType === 1){
- if(node == ancestor){
- return true; // Boolean
- }
- node = node.parentNode;
- }
- }catch(e){ /* squelch, return false */ }
- return false; // Boolean
- }
- // Shared static property.
- var animateMoveMixin = {
- initialize: function (options) {
- var self = this;
- // Create closure-based functions for shuffling and un-postioning views.
- this.shuffleViews = function () {
- return util.reduce(
- self.views(),
- self.calculateViewYPosition,
- 0
- );
- };
- this.unpositionViews = function (view) {
- self.$('list')
- .css('height', 'auto')
- .removeClass('js-has-moving')
- ;
- // Remove z-index used for animation.
- $(view.el).css('z-index', '');
- return self;
- };
- },
- // Use for a reduce operation on the sub-views, recalulating where they
- // should be placed within this stack. `memo` is the sum total height of the
- // views before the current view.
- calculateViewYPosition: function (memo, view) {
- return memo + $(view.el).css('top', memo + 'px').height();
- },
- moveView: function (model, options) {
- var view = this.lookup(model);
- var views = this.views();
- return this.animateViewMove(view, views);
- },
- animateViewMove: function (view, views) {
- var model = view.model;
- // Set this view as the highest index (one higher than all other places).
- // This will ensure it animates *over* other places.
- $(view.el).css('z-index', 2);
- // The first pass-through makes sure all the views have correct initial
- // top positions.
- var height = this.shuffleViews();
- this.$('list')
- .css('height', height + 'px')
- .addClass('js-has-moving')
- ;
- view = this.relocateView(view, views);
- // Wait for the browser to finish calculations for the elements.
- setTimeout(this.shuffleViews, 1);
- // Wait for the animation to complete, then remove all positioning code.
- return Promise.wait(501, view).then(this.unpositionViews);
- },
- relocateView: function (view, views) {
- var model = view.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);
- return view;
- }
- };
- // Incremental updates for DOM elements.
- // `updates` object should be in format `{ '.selector': value }`.
- var updates = function (updates) {
- var model = this.model;
- var attr;
- var value;
- var $el;
- for (var key in updates) {
- attr = updates[key];
- $el = this.$(key);
- // If attr is a computed value, invoke it and allow it to update.
- if (typeof attr === 'function') {
- attr.call(this, this, $el);
- continue;
- }
- if (model.hasChanged(attr)) {
- // If it's a string, get the attribute via the model and set it.
- $el.html(model.get(attr));
- }
- }
- };
- // Place
- // ---------------------------------------------------------------------------
- var PlaceView = SingleView.extend({
- model: SiteModel,
- // Make `PlaceView` a list item. It lives in an EraView list view.
- tagName: 'li',
- className: 'accordion--item js-movable',
- template: template('<a class="place link" href="{place_url}"><span class="media media--ico"><img src="{ favicon_url }"></span><span class="title">{place_title}</span></a>'),
- events: {
- 'click .link': 'onClick'
- },
- onClick: function (e) {
- e.preventDefault();
- e.stopPropagation();
- var place = this.model;
- var stack = place.collection.stack();
- // Routes that are already active will rightly not trigger.
- // `showViewer` must happen here, because it should happen regardless of
- // whether the route below is triggered.
- bridge
- .showViewer()
- .navigate('stack/' + stack.id + '/' + place.id, true)
- ;
- },
- // Render the innerHTML of the element the first time.
- $populateEl: lazily(function () {
- var model = this.model;
- var place_url = model.get('place_url');
- var a = document.createElement("a");
- a.href = place_url;
- var favicon_url = a.protocol + "//" + a.hostname + "/favicon.ico";
- var html = this.template({
- favicon_url: favicon_url,
- place_title: model.get('place_title'),
- place_url: place_url
- });
- return $(this.el)
- .toggleClass('activated', this.isSelected(model))
- .html(html);
- }),
- updates: updates,
- _updatePlace: function (view, $place) {
- var model = view.model;
- $place
- .attr('href', model.get('place_url'))
- ;
- },
- render: function () {
- var model = this.model;
- var $el = this.$populateEl();
- $el.toggleClass('activated', this.isSelected(model));
- this.updates({
- '.title': 'place_title',
- '.place': this._updatePlace
- });
- return this;
- },
- // Just a short-cut for getting a boolean selected value from a model.
- isSelected: function (model) {
- return !!model.get('selected');
- }
- });
- // Stack
- // ---------------------------------------------------------------------------
- //
- // Handles rendering the styles for a stack containing eras.
- // StackView is a listview, but itself lives inside of a listview.
- // Define a prototype object for StackView. Mix in animation object.
- var __StackView = util.extend({}, animateMoveMixin, {
- view: PlaceView,
- tagName: 'li',
- className: 'accordion--group js-movable',
- attachPoints: {
- 'list': '.accordion--items'
- },
- events: {
- 'click .link': 'onClick'
- },
- template: template('<a class="stack link"><span class="media media--ico media--ico-stack"></span><span class="title">{stack_title}</span><span class="edit-toolbar"><span title="Remove" class="button button--cell button--cell-remove" data-action="remove"></span></span></a><ul class="accordion--items"></ul>'),
- initialize: function (options) {
- var model = this.model;
- // Requires a stackModel, or another model that implements `eras`.
- this.collection = model.places();
- __ListView.initialize.call(this, options);
- animateMoveMixin.initialize.call(this, options);
- this.closePlaces();
- },
- 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.
- 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.model
- .bind('change', this.render, this)
- .bind('change:selected', this.renderSelected, this)
- .bind('change:open', function (model) {
- this[this.model.get('open') ? 'openPlaces' : 'closePlaces']();
- }, this)
- ;
- return this;
- },
- onClick: function (e) {
- e.preventDefault();
-
- // further event-delegation to branch off for action-toolbar clicks
- var touchTarget = e.target;
- if(touchTarget && $(touchTarget.parentNode).hasClass("edit-toolbar")){
- return this.onToolbarClick(e);
- }
-
- var stack = this.model;
- // If the stack is already selected, close the stack by navigating
- // to root.
- if (stack.get('open') === true) {
- bridge.navigate('index', true);
- }
- else {
- // If there are any places, hit the router immediately. If not,
- // fetch the places. If there is 1 place, we can assume it is the
- // selected place, since this is returned with the `sessions` API.
- var places = stack.places().length ?
- stack.places() : stack.places().promisePlaces();
- // Otherwise, navigate to the stack and place.
- Promise.when(places, function(places) {
- // If there is a selected place, go there. If not, go for the
- // first place.
- var place = places.selected() || places.at(0);
- // Routes that are already active will rightly not trigger.
- // `showViewer` must happen here, because it should happen regardless of
- // whether the route below is triggered.
- bridge
- .showViewer()
- .navigate('stack/' + stack.id + '/' + place.id, true)
- ;
- });
- }
- },
- onToolbarClick: function(e){
- e.preventDefault();
- e.stopPropagation();
- var target = e.target,
- action = target.getAttribute("data-action");
- this.trigger(action, this, this.model, e);
- return false; // block further event handli
- },
- openPlaces: function () {
- var height = '' + this.shuffleViews();
- var $list = this.$('list');
- $(this.el).addClass('js-open');
- $list
- .addClass('js-animating')
- .css('height', height + 'px')
- ;
- // Remove the fixed height and class after the animation is complete.
- setTimeout(function () {
- $list
- .removeClass('js-animating')
- .css('height', 'auto')
- ;
- }, 300);
- return this;
- },
- isOpen: function () {
- return !!this.model.get('open');
- },
- closePlaces: function () {
- var $list = this.$('list');
- // Fix height, since CSS animation can't handle animating from "auto".
- $list.css('height', $list.height() + 'px');
- $(this.el).removeClass('js-open');
- // Set a timeout, allowing the browser to re-flow the list.
- setTimeout(function () {
- $list
- // Add the animation class
- .addClass('js-animating')
- // Set height to 0.
- .css('height', '0')
- ;
- }, 1);
- },
- getSubtypeClass: function (subtype) {
- if (subtype === 'social') return 'media--ico-person';
- if (subtype === 'direct') return 'media--ico-url';
- return 'media--ico-search';
- },
- renderSelected: function () {
- // Update stack title.
- $(this.el).toggleClass('js-selected', !!this.model.get('selected'));
- },
- // A specialized version of moveView that skips animation if closed.
- moveView: function (model, options) {
- var view = this.lookup(model);
- var views = this.views();
- var method = this.isOpen() ? 'animateViewMove' : 'relocateView';
- return this[method].call(this, view, views);
- },
- render: function () {
- var stack_title = this.model.get('stack_title');
- var hasStackTitle = !!stack_title;
- var model = this.model;
-
- var subtypeClass = this.getSubtypeClass(this.model.get('subtype'));
- this.$('.stack .title').text(this.model.get('stack_title'));
- this.$('.media--ico-stack').addClass(subtypeClass);
- return this;
- }
- });
- var StackView = ListView.extend(__StackView);
- var __StackListView = util.extend({}, animateMoveMixin, {
- view: StackView,
- tagName: 'ul',
- className: 'accordion js-invisible',
- initialize: function (options) {
- animateMoveMixin.initialize.call(this, options);
- return __ListView.initialize.call(this, options);
- },
- bindEvents: function () {
- __ListView.bindEvents.call(this);
- this.collection
- // Wait a moment for the browser to finish reflows, then shuffle views.
- // Since we haven't positioned these elements, the result is that
- // initial top positions are set based on the heights of elements.
- // This preps them for any move animations to come.
- .bind('reset', util.bind(setTimeout, null, this.shuffleViews, 1))
- ;
- return this;
- },
- // Change visibility via class, rather than using a visibility toggle.
- // This is compatible with the render code below.
- onStateChange: function (state) {
- $(this.el).toggleClass('js-invisible', state === 'loading' && !this.collection.length);
- return this;
- },
- render: function () {
- // Transition opacity to/from zero depending on whether we have items.
- $(this.el).toggleClass('js-invisible', !this.collection.length);
- return this;
- }
- });
- var StackListView = ListView.extend(__StackListView);
- return {
- PlaceView: PlaceView,
- StackView: StackView,
- StackListView: StackListView
- };
- });