/pancake-web/pancake/web/static/js/drawer.app.js
JavaScript | 527 lines | 345 code | 75 blank | 107 comment | 22 complexity | 2f259f78dc56b93f99db489d525bd7fc 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([
- 'config',
- '$',
- 'backbone',
- 'underscore',
- 'views/drawerviews',
- 'views/widgetview',
- 'models/stackcollection',
- 'models/stackmodel',
- 'models/eras',
- 'lib/promiseplusplus',
- 'lib/page',
- 'lib/confirm',
- 'logger',
- 'lib/bridge',
- 'lib/lazily',
- 'lib/assert',
- 'lib/errors',
- 'xmessage'
- ], function (
- config,
- $,
- Backbone,
- util,
- drawer,
- WidgetView,
- StackCollection,
- StackModel,
- eras,
- Promise,
- Page,
- confirmDialog,
- logger,
- Bridge,
- lazily,
- assert,
- errors,
- xmessage
- ) {
-
- // Page router configuration
- // ---------------------------------------------------------------------------
- var bridge = new Bridge();
- var wait = Promise.wait;
- // A composable approach to showing the viewer. Passes through the result
- // while calling a side-effect.
- var showViewer = function (x) {
- bridge.showViewer();
- return x;
- };
- var __Page = Page.prototype;
- var DrawerAppRouter = Page.extend({
- events: {
- 'readyAcknowledged': 'onTopReady',
- 'navigate': 'onNavigate',
- 'stack:change': 'onStackChange',
- 'stack:create': 'onStackCreate',
- 'social:create': 'onSocialCreate',
- 'direct:create': 'onDirectCreate',
- 'connection:change': 'onConnectionChange',
- 'editmode:change': 'onEditModeChange'
- },
- routes: {
- '': 'toRoot',
- // Route these requests to root as well. This is an alternate route that
- // will not "jump to top" when hit. Preferred.
- 'index': 'toRoot',
- // This is a shortcut for finding the active place in the stack. Routes
- // through `toStackAndPlace` after getting active place.
- 'stack/:stack_id': 'toStack',
- 'stack/:stack_id/:place_id': 'toStackAndPlace'
- },
- initialize: function (options) {
- var self = this;
- __Page.initialize.call(this, options);
- this.stacks = window.stacks = new StackCollection(
- [],
- { urlTokens: { service: 'session', method: 'active' } }
- );
- // Limit the number of stacks returned
- this.stacks.url(null, { l: 20 });
- this.stackListView = new drawer.StackListView({
- el: $('#drawer'),
- collection: this.stacks
- });
-
- },
- onTopReady: function(){
- logger.log("Great, top knows we're ready");
- topReady = true;
- clearInterval(topReadyInterval);
- // set editmode to true by default - in lieu of UI in the native app
- this.setEditMode(true);
- },
- onNavigate: util.compose(showViewer, function (msg) {
- var self = this;
- // Get currently active stack.
- var stack_id = this.get('stack_id');
- // Make sure we have the stack in question. Request from the server if
- // necessary.
- this.stacks.fetchStack(stack_id)
- .then(function (stack) {
- // Format place object and issue a `link` request to Lattice.
- return stack.link({
- place_url: msg.place_url,
- place_title: msg.place_title,
- status: Number(msg.status) >= 400 ? 'error' : 'ok'
- });
- })
- .then(function (place) {
- // When the stack link request is finished, defer to navigation
- // route handler.
- self.navigate('stack/' + stack_id + '/' + place.id, true);
- })
- ;
- return msg;
- }),
- onStackChange: util.compose(showViewer, function (msg) {
- var place_id = msg.place_id;
- var route = 'stack/' + msg.stack_id + (place_id ? ('/' + place_id) : '');
- this.navigate(route, true);
- return msg;
- }),
- onStackCreate: util.compose(showViewer, function (msg) {
- // Create a fresh `StackModel` for this new search item.
- // Prepare it with information with have from message.
- var stack = new StackModel({
- stack_title: msg.search_terms,
- place_title: msg.place_title,
- place_url: msg.place_url,
- // We're currently handed `search_url` via the msg, which I think is
- // the right thing. We'll let the originator of the message tell us
- // what the `search_url` is. It should know.
- search_url: msg.search_url,
- search_provider: msg.search_provider,
- search_terms: msg.search_terms
- });
- // Issue a search request to the server.
- stack.search().then(util.bind(this.augmentAndRouteToStack, this));
- return msg;
- }),
- onSocialCreate: util.compose(showViewer, function (msg) {
- // Create a fresh `StackModel` for this new social item.
- // Prepare it with information with have from message.
- var newStackModel = new StackModel({
- // Stack titles are defined by friend name
- stack_title: msg.friend_name,
- friend_name: msg.friend_name,
- friend_url: msg.friend_url,
- service_url: msg.service_url,
- place_title: msg.place_title,
- place_url: msg.place_url
- });
- // get/create this stack
- // if this was a get, just mo
- // Issue a social request to the server.
- var self = this;
- newStackModel.social().then(function(){
- return self.augmentAndRouteToStack.apply(self, arguments);
- });
- }),
- onDirectCreate: util.compose(showViewer, function (msg) {
- // Create a fresh `StackModel` for this new direct-url stack item.
- // Prepare it with information with have from message.
- var stack = new StackModel({
- // We don't have a title yet, just the url and the term which we
- // matched as a url.
- title: msg.title,
- url: msg.url
- });
- // Issue a stack/direct request to the server.
- stack.direct().then(util.bind(this.augmentAndRouteToStack, this));
- }),
-
- onEditModeChange: function(msg){
- logger.log("onEditModeChange:", msg);
- this.setEditMode(msg.value);
- },
- setEditMode: function(editOn){
-
- function onCancelRemove(){
- logger.log("User cancelled account wipe");
- }
-
- var onStackRemoveAction = function(view, model, e){
- if(model){
- var title = model.get('stack_title');
- confirmDialog("Are you sure you want to remove '" + title + "' and all the places it contains?")
- .then(function(){
- $(view.el).addClass('delete-pending');
- model.remove().then(function(){
- logger.log("Removed stack: " + title);
- }, function(){
- logger.log("Failed to remove " + title);
- $(view.el).removeClass('delete-pending');
- });
- }, onCancelRemove);
- } else {
- logger.log("No model associated with remove toolbar");
- }
- };
-
- this.stackListView.bind('remove', onStackRemoveAction);
- },
- toRoot: function () {
- var rejected = this.logRejection;
- var stacks = this.stacks;
- // Make sure we load stacks, and that no stack is selected.
- stacks.promiseStacks()
- .then(this._closeAllStacks, rejected)
- .then(function (stacks) {
- stacks.selectStack(null);
- });
- },
- toStack: function (stack_id) {
- var self = this;
- var stacks = this.stacks;
- stacks.promiseStacks()
- // Then insure we have the stack that was requested
- .then(util.bind(stacks.fetchStack, stacks, stack_id))
- .then(this.promisePlacesForStack)
- .then(function (places) {
- var stack = places.stack();
- var place = places.selected() || places.at(0);
- self.toStackAndPlace(stack.id, place.id);
- })
- ;
- },
- toStackAndPlace: function (stack_id, place_id) {
- var stacks = this.stacks;
- var self = this;
- var page = this;
- var rejected = this.logRejection;
- // Load all stacks at least once.
- return stacks.promiseStacks()
- // Then make sure we have the stack that was requested.
- .then(
- // If stacks come back from server-side, promise we have the
- // exact stack requeste.
- function (promisedStacks) {
- return promisedStacks.fetchStack(stack_id);
- },
- // If stacks don't come back (404 or 500), recover by using
- // the in-memory `StackCollection`. Promise the specific stack
- // we need. This case happens when a user has no stacks at all
- // (brand new account).
- function (err) {
- return stacks.fetchStack(stack_id);
- }
- )
- .then(this._selectStack)
- // At this point we have the stack and it's active place.
- // If the place being requested is the active place, `fetchPlace`
- // will simply return it. If not, it will load all the places.
- //
- // The most common case, clicking on a stack, will request the active
- // place, so no I/O delay will happen in that case.
- .then(function (stack) {
- return stack.fetchPlace(place_id);
- }, rejected)
- .then(this._makeSurePlacesAreLoaded, rejected)
- .then(function (places) {
- return places.stack();
- })
- .then(this._closeOtherStacks, rejected)
- .then(this._scrollToTop, rejected)
- .then(this._moveStack)
- // Ok, all places are loaded. Move the place in the places list.
- // If the stack is already open, there is an animation that happens for
- // the move.
- // If not, continue immediately.
- .then(function (stack) {
- // Not sure if we actually need to do this, but it's a hold-over
- // from the previous drawer. -GB
- self.set({ stack_id: stack.id });
- var places = stack.places();
- var place = places.get(place_id);
- // ...update it's accessed time...
- place.set({ accessed: Date.now() }, { silent: true });
- // ...and select it...
- places.selectPlace(place.id);
- // If the stack is open, the place move will be animated. Wait for it to
- // finish.
- //
- // If it's closed, we can continue immediately: there is no animation.
- return wait((!stack.get('open') ? 300 : 0), stack);
- }, rejected)
- // get the stack for the place (calls stack())
- .then(this._openStack, rejected)
- .then(function (stack) {
- return stack.places().get(place_id);
- })
- .then(this._notifyPlaceChange, rejected)
- ;
- },
- // Profile: `stacks -> stacks`.
- _closeAllStacks: function (stacks) {
- var openLength = stacks.reduce(function (memo, stack) {
- if (stack.get('open')) memo++;
- stack.set({ open: false });
- return memo;
- }, 0);
- return !stacks.selected() ? stacks : wait(300, stacks);
- },
- _closeOtherStacks: function (stack) {
- logger.log("_closeOtherStacks got " + stack.get('stack_title'));
- var id = stack.id;
- var collection = stack.collection;
- if(!collection) {
- logger.error( new Error("Missing collection in stack: " + id) );
- }
- var openLength = collection ? collection.reduce(function (memo, siblingStack) {
- if (siblingStack.get('open')) memo++;
- if (siblingStack.id !== id) siblingStack.set({ open: false });
- return memo;
- }, 0) : 0;
- return openLength === 0 ? stack : wait(300, stack);
- },
- _scrollToTop: function (stack) {
- var body = document.getElementsByTagName('body')[0];
- if (body.scrollTop !== 0) body.scrollTop = 0;
- return wait(1, stack);
- },
- _openStack: function (stack) {
- var stacks = stack.collection;
- var isOpenAlready = !!stack.get('open');
- if (!isOpenAlready) stack.set({ open: true });
- // If stack is already selected, return immediately. Otherwise,
- // wait until animation is finished before calling next.
- return isOpenAlready ? stack : wait(300, stack);
- },
- _selectStack: function (stack) {
- var stacks = stack.collection;
- var isSelectedAlready = stacks.isSelected(stack);
- // Kicks off open animation.
- if (!isSelectedAlready) stacks.selectStack(stack.id);
- return stack;
- },
- _moveStack: function (stack) {
- var stacks = stack.collection;
- // humour me.
- // Sometimes we're given a stack matches but is not === a stack already in the collection
- // FIXME: find out why this is
- stack = stacks.get(stack.id) || stack;
-
- // If the stack is already at position 0, return immediately.
- var alreadyAt0 = (stacks.indexOf(stack) === 0);
- if (!alreadyAt0) stacks.move(stack, { at: 0 });
- // Wait until stack move animation is finished, then move the
- // stack. Doing these as waterfalled animations makes animations
- // much more performant.
- return alreadyAt0 ? stack : wait(300, stack);
- },
- _placeToStack: function (place) {
- return place.collection.stack();
- },
- // Kick off stack I/O (if neccessary)
- // Make sure it takes at least 300 ms, to give the viewer some time to
- // render/reflow.
- _makeSurePlacesAreLoaded: function (place) {
- return place.collection.loaded ? place.collection
- : place.collection.fetch({ diff: true });
- },
- _notifyPlaceChange: function (place) {
- // In order to send this message we have to have a place object in
- // the collection. The message requires:
- //
- // * `stack_id`
- // * `place_id`
- // * `session_id`
- // * `place_url`
- //
- // `stack_id` and `session_id` come back with the stack. `place_url` is
- // not available unless the place object is in memory.
- place.collection.changePlace(place.id);
- return place;
- },
- // Helper used in `stackAndPlace` route handler.
- promisePlacesForStack: function (stack) {
- return stack.places().promisePlaces();
- },
- // The server-side makes sure that people are de-duped, but the client side
- // can't really know whether a person has been created or not.
- // Anyway, the takeaway is that we check for the stackmodel first.
- // If it exists in the collection already, we don't add it.
- augmentStacksWithStack: function (newStack) {
- var stacks = this.stacks;
- var stack = stacks.get(newStack.id);
- var newPlaces = newStack.places();
- var place_id = newStack.get('place_id');
- logger.log("augmentStacksWithStack, stack is in stackcollection?", !!stack);
- if(!stack) {
- // we dont have this stack yet
- newStack.addTo(stacks);
- logger.log("augmentStacksWithStack, stack added, has .collection", newStack.collection);
- // now stack and newStack are one and the same
- stack = newStack;
- }
-
- var places = stack.places();
- // If the place does not already exist in stack, add it.
- if(newPlaces !== places){
- newPlaces.each(function(place){
- if(!places.get(place.id)){
- places.add(place);
- }
- });
- }
- return stack;
- },
- augmentAndRouteToStack: function (tmpStack) {
- // normalize the stack, sometimes we get an orphaned stack here? Go back to the stackcollection for the "real" one
- var place_id = tmpStack.get('place_id');
- var stack = this.augmentStacksWithStack(this.stacks.get(tmpStack.id) || tmpStack);
- if(stack !== stacks.get(stack.id)) {
- logger.log("augmentStacksWithStack returned a different model object to the one in app.stacks");
- }
- this.navigate('stack/' + stack.id + '/' + place_id, true);
- },
- logRejection: function (err) {
- logger.warn('Promise was rejected', err);
- }
- });
- // Scratch setup
- // ---------------------------------------------------------------------------
- var global = window;
- var exports = {};
- // Make sure uncaught exceptions are send along to the iOS app.
- if (config.platform === 'ios' && logger.listenForUncaughtExceptions) {
- logger.listenForUncaughtExceptions();
- }
- var app = global.app = exports.app = new DrawerAppRouter();
- // work around a race condition where this window checks in before the 'top' dispatcher is ready
- var topReady;
- var topReadyInterval = setInterval(notifyReady, 500);
- var topReadyTries = 0;
- function notifyReady(){
- if(topReady || topReadyTries > 20) {
- clearInterval(topReadyInterval);
- } else {
- topReadyTries++;
- logger.log(config.appName + " sending ready message to top");
- xmessage.sendMessage("top", "ready", [{
- origin: config.appName, type: "ready", acknowledge: 'readyAcknowledged'
- }]);
- }
- }
- notifyReady();
- // ---------
-
- // Set a "static" reference to the router instance on bridge.
- // Used by bridge.navigate (a facade for Router.prototype.navigate).
- Bridge.setActiveRouter(app);
-
- // Now that all of our routes are configured, start router listener.
- Backbone.history.start({ pushState: false });
- // Expose exports to Require (used by drawer spec)
- return exports;
- });