PageRenderTime 44ms CodeModel.GetById 14ms RepoModel.GetById 1ms app.codeStats 0ms

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

https://bitbucket.org/mozillapancake/pancake
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
  1. define([
  2. '$',
  3. 'underscore',
  4. 'backbone',
  5. 'views/singleview',
  6. 'views/listview',
  7. 'models/stackcollection',
  8. 'models/stackmodel',
  9. 'models/sitemodel',
  10. 'lib/template',
  11. 'lib/bridge',
  12. 'lib/promiseplusplus',
  13. 'lib/lazily'
  14. ], function (
  15. $,
  16. util,
  17. Backbone,
  18. SingleView,
  19. ListView,
  20. StackCollection,
  21. StackModel,
  22. SiteModel,
  23. template,
  24. Bridge,
  25. Promise,
  26. lazily
  27. ) {
  28. // Define shorthands for prototypes. This allows easy calls to `super` methods.
  29. var __SingleView = SingleView.prototype;
  30. var __ListView = ListView.prototype;
  31. var bridge = new Bridge();
  32. function isDescendant(node, ancestor){
  33. // summary:
  34. // Returns true if node is a descendant of ancestor
  35. // node: DOMNode
  36. // node reference to test
  37. // ancestor: DOMNode
  38. // node reference of potential parent to test against
  39. try{
  40. while(node && node.nodeType === 1){
  41. if(node == ancestor){
  42. return true; // Boolean
  43. }
  44. node = node.parentNode;
  45. }
  46. }catch(e){ /* squelch, return false */ }
  47. return false; // Boolean
  48. }
  49. // Shared static property.
  50. var animateMoveMixin = {
  51. initialize: function (options) {
  52. var self = this;
  53. // Create closure-based functions for shuffling and un-postioning views.
  54. this.shuffleViews = function () {
  55. return util.reduce(
  56. self.views(),
  57. self.calculateViewYPosition,
  58. 0
  59. );
  60. };
  61. this.unpositionViews = function (view) {
  62. self.$('list')
  63. .css('height', 'auto')
  64. .removeClass('js-has-moving')
  65. ;
  66. // Remove z-index used for animation.
  67. $(view.el).css('z-index', '');
  68. return self;
  69. };
  70. },
  71. // Use for a reduce operation on the sub-views, recalulating where they
  72. // should be placed within this stack. `memo` is the sum total height of the
  73. // views before the current view.
  74. calculateViewYPosition: function (memo, view) {
  75. return memo + $(view.el).css('top', memo + 'px').height();
  76. },
  77. moveView: function (model, options) {
  78. var view = this.lookup(model);
  79. var views = this.views();
  80. return this.animateViewMove(view, views);
  81. },
  82. animateViewMove: function (view, views) {
  83. var model = view.model;
  84. // Set this view as the highest index (one higher than all other places).
  85. // This will ensure it animates *over* other places.
  86. $(view.el).css('z-index', 2);
  87. // The first pass-through makes sure all the views have correct initial
  88. // top positions.
  89. var height = this.shuffleViews();
  90. this.$('list')
  91. .css('height', height + 'px')
  92. .addClass('js-has-moving')
  93. ;
  94. view = this.relocateView(view, views);
  95. // Wait for the browser to finish calculations for the elements.
  96. setTimeout(this.shuffleViews, 1);
  97. // Wait for the animation to complete, then remove all positioning code.
  98. return Promise.wait(501, view).then(this.unpositionViews);
  99. },
  100. relocateView: function (view, views) {
  101. var model = view.model;
  102. // Remove view from the DOM and deregister it.
  103. this.removeView(model);
  104. // Re-register the view.
  105. this.register(view);
  106. // And append it again.
  107. this.appendView(model);
  108. return view;
  109. }
  110. };
  111. // Incremental updates for DOM elements.
  112. // `updates` object should be in format `{ '.selector': value }`.
  113. var updates = function (updates) {
  114. var model = this.model;
  115. var attr;
  116. var value;
  117. var $el;
  118. for (var key in updates) {
  119. attr = updates[key];
  120. $el = this.$(key);
  121. // If attr is a computed value, invoke it and allow it to update.
  122. if (typeof attr === 'function') {
  123. attr.call(this, this, $el);
  124. continue;
  125. }
  126. if (model.hasChanged(attr)) {
  127. // If it's a string, get the attribute via the model and set it.
  128. $el.html(model.get(attr));
  129. }
  130. }
  131. };
  132. // Place
  133. // ---------------------------------------------------------------------------
  134. var PlaceView = SingleView.extend({
  135. model: SiteModel,
  136. // Make `PlaceView` a list item. It lives in an EraView list view.
  137. tagName: 'li',
  138. className: 'accordion--item js-movable',
  139. 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>'),
  140. events: {
  141. 'click .link': 'onClick'
  142. },
  143. onClick: function (e) {
  144. e.preventDefault();
  145. e.stopPropagation();
  146. var place = this.model;
  147. var stack = place.collection.stack();
  148. // Routes that are already active will rightly not trigger.
  149. // `showViewer` must happen here, because it should happen regardless of
  150. // whether the route below is triggered.
  151. bridge
  152. .showViewer()
  153. .navigate('stack/' + stack.id + '/' + place.id, true)
  154. ;
  155. },
  156. // Render the innerHTML of the element the first time.
  157. $populateEl: lazily(function () {
  158. var model = this.model;
  159. var place_url = model.get('place_url');
  160. var a = document.createElement("a");
  161. a.href = place_url;
  162. var favicon_url = a.protocol + "//" + a.hostname + "/favicon.ico";
  163. var html = this.template({
  164. favicon_url: favicon_url,
  165. place_title: model.get('place_title'),
  166. place_url: place_url
  167. });
  168. return $(this.el)
  169. .toggleClass('activated', this.isSelected(model))
  170. .html(html);
  171. }),
  172. updates: updates,
  173. _updatePlace: function (view, $place) {
  174. var model = view.model;
  175. $place
  176. .attr('href', model.get('place_url'))
  177. ;
  178. },
  179. render: function () {
  180. var model = this.model;
  181. var $el = this.$populateEl();
  182. $el.toggleClass('activated', this.isSelected(model));
  183. this.updates({
  184. '.title': 'place_title',
  185. '.place': this._updatePlace
  186. });
  187. return this;
  188. },
  189. // Just a short-cut for getting a boolean selected value from a model.
  190. isSelected: function (model) {
  191. return !!model.get('selected');
  192. }
  193. });
  194. // Stack
  195. // ---------------------------------------------------------------------------
  196. //
  197. // Handles rendering the styles for a stack containing eras.
  198. // StackView is a listview, but itself lives inside of a listview.
  199. // Define a prototype object for StackView. Mix in animation object.
  200. var __StackView = util.extend({}, animateMoveMixin, {
  201. view: PlaceView,
  202. tagName: 'li',
  203. className: 'accordion--group js-movable',
  204. attachPoints: {
  205. 'list': '.accordion--items'
  206. },
  207. events: {
  208. 'click .link': 'onClick'
  209. },
  210. 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>'),
  211. initialize: function (options) {
  212. var model = this.model;
  213. // Requires a stackModel, or another model that implements `eras`.
  214. this.collection = model.places();
  215. __ListView.initialize.call(this, options);
  216. animateMoveMixin.initialize.call(this, options);
  217. this.closePlaces();
  218. },
  219. bindEvents: function () {
  220. // Subscribe to default handlers for events.
  221. // Note that `render` is called for every event. Be sure to queue it last
  222. // for the events, so it renders based on the most up-to-date state.
  223. this.collection
  224. .bind('add', this.addView, this)
  225. .bind('add', this.render, this)
  226. .bind('remove', this.removeView, this)
  227. .bind('remove', this.render, this)
  228. .bind('move', this.render, this)
  229. .bind('move', this.moveView, this)
  230. .bind('reset', this.resetViews, this)
  231. .bind('reset', this.render, this)
  232. ;
  233. this.model
  234. .bind('change', this.render, this)
  235. .bind('change:selected', this.renderSelected, this)
  236. .bind('change:open', function (model) {
  237. this[this.model.get('open') ? 'openPlaces' : 'closePlaces']();
  238. }, this)
  239. ;
  240. return this;
  241. },
  242. onClick: function (e) {
  243. e.preventDefault();
  244. // further event-delegation to branch off for action-toolbar clicks
  245. var touchTarget = e.target;
  246. if(touchTarget && $(touchTarget.parentNode).hasClass("edit-toolbar")){
  247. return this.onToolbarClick(e);
  248. }
  249. var stack = this.model;
  250. // If the stack is already selected, close the stack by navigating
  251. // to root.
  252. if (stack.get('open') === true) {
  253. bridge.navigate('index', true);
  254. }
  255. else {
  256. // If there are any places, hit the router immediately. If not,
  257. // fetch the places. If there is 1 place, we can assume it is the
  258. // selected place, since this is returned with the `sessions` API.
  259. var places = stack.places().length ?
  260. stack.places() : stack.places().promisePlaces();
  261. // Otherwise, navigate to the stack and place.
  262. Promise.when(places, function(places) {
  263. // If there is a selected place, go there. If not, go for the
  264. // first place.
  265. var place = places.selected() || places.at(0);
  266. // Routes that are already active will rightly not trigger.
  267. // `showViewer` must happen here, because it should happen regardless of
  268. // whether the route below is triggered.
  269. bridge
  270. .showViewer()
  271. .navigate('stack/' + stack.id + '/' + place.id, true)
  272. ;
  273. });
  274. }
  275. },
  276. onToolbarClick: function(e){
  277. e.preventDefault();
  278. e.stopPropagation();
  279. var target = e.target,
  280. action = target.getAttribute("data-action");
  281. this.trigger(action, this, this.model, e);
  282. return false; // block further event handli
  283. },
  284. openPlaces: function () {
  285. var height = '' + this.shuffleViews();
  286. var $list = this.$('list');
  287. $(this.el).addClass('js-open');
  288. $list
  289. .addClass('js-animating')
  290. .css('height', height + 'px')
  291. ;
  292. // Remove the fixed height and class after the animation is complete.
  293. setTimeout(function () {
  294. $list
  295. .removeClass('js-animating')
  296. .css('height', 'auto')
  297. ;
  298. }, 300);
  299. return this;
  300. },
  301. isOpen: function () {
  302. return !!this.model.get('open');
  303. },
  304. closePlaces: function () {
  305. var $list = this.$('list');
  306. // Fix height, since CSS animation can't handle animating from "auto".
  307. $list.css('height', $list.height() + 'px');
  308. $(this.el).removeClass('js-open');
  309. // Set a timeout, allowing the browser to re-flow the list.
  310. setTimeout(function () {
  311. $list
  312. // Add the animation class
  313. .addClass('js-animating')
  314. // Set height to 0.
  315. .css('height', '0')
  316. ;
  317. }, 1);
  318. },
  319. getSubtypeClass: function (subtype) {
  320. if (subtype === 'social') return 'media--ico-person';
  321. if (subtype === 'direct') return 'media--ico-url';
  322. return 'media--ico-search';
  323. },
  324. renderSelected: function () {
  325. // Update stack title.
  326. $(this.el).toggleClass('js-selected', !!this.model.get('selected'));
  327. },
  328. // A specialized version of moveView that skips animation if closed.
  329. moveView: function (model, options) {
  330. var view = this.lookup(model);
  331. var views = this.views();
  332. var method = this.isOpen() ? 'animateViewMove' : 'relocateView';
  333. return this[method].call(this, view, views);
  334. },
  335. render: function () {
  336. var stack_title = this.model.get('stack_title');
  337. var hasStackTitle = !!stack_title;
  338. var model = this.model;
  339. var subtypeClass = this.getSubtypeClass(this.model.get('subtype'));
  340. this.$('.stack .title').text(this.model.get('stack_title'));
  341. this.$('.media--ico-stack').addClass(subtypeClass);
  342. return this;
  343. }
  344. });
  345. var StackView = ListView.extend(__StackView);
  346. var __StackListView = util.extend({}, animateMoveMixin, {
  347. view: StackView,
  348. tagName: 'ul',
  349. className: 'accordion js-invisible',
  350. initialize: function (options) {
  351. animateMoveMixin.initialize.call(this, options);
  352. return __ListView.initialize.call(this, options);
  353. },
  354. bindEvents: function () {
  355. __ListView.bindEvents.call(this);
  356. this.collection
  357. // Wait a moment for the browser to finish reflows, then shuffle views.
  358. // Since we haven't positioned these elements, the result is that
  359. // initial top positions are set based on the heights of elements.
  360. // This preps them for any move animations to come.
  361. .bind('reset', util.bind(setTimeout, null, this.shuffleViews, 1))
  362. ;
  363. return this;
  364. },
  365. // Change visibility via class, rather than using a visibility toggle.
  366. // This is compatible with the render code below.
  367. onStateChange: function (state) {
  368. $(this.el).toggleClass('js-invisible', state === 'loading' && !this.collection.length);
  369. return this;
  370. },
  371. render: function () {
  372. // Transition opacity to/from zero depending on whether we have items.
  373. $(this.el).toggleClass('js-invisible', !this.collection.length);
  374. return this;
  375. }
  376. });
  377. var StackListView = ListView.extend(__StackListView);
  378. return {
  379. PlaceView: PlaceView,
  380. StackView: StackView,
  381. StackListView: StackListView
  382. };
  383. });