/static/js/drawer.app.js
JavaScript | 415 lines | 262 code | 62 blank | 91 comment | 20 complexity | 2c3356cb5cd5438ff5072ed28ba2ad8f MD5 | raw file
Possible License(s): Apache-2.0
- define([
- 'config',
- '$',
- 'underscore',
- 'backbone',
- 'lib/page',
- 'lib/bridge',
-
- 'views/widgetview',
- 'models/stackcollection',
-
- 'lib/template',
- 'xmessage',
- 'lib/errors',
- 'lib/logger/noisy',
-
- // preload controlers
- 'controllers/placescontroller',
- 'controllers/stacklistcontroller'
- ], function (
- config,
- $,
- util,
- Backbone,
- Page,
- Bridge,
-
- WidgetView,
- StackCollection,
-
- template,
- xmessage,
- errors,
- logger
- ) {
- var global = window,
- exports = {},
- // Create a local reference to StackModel, using the reference to it
- // that lives on StackCollection.
- StackModel = StackCollection.prototype.model;
-
- // Make sure uncaught exceptions are send along to the iOS app.
- if (config.platform === 'ios' && logger.listenForUncaughtExceptions)
- logger.listenForUncaughtExceptions();
- // DrawerAppRouter acts as Page-level model, and front-controller for the drawer 'application'
- // it provides entry points via Backbone's routing mechanism
- // and also by handling xmessage events
-
- // Each route uses one of 2 controllers: stacklistcontroller or placescontroller
- // these are lazily loaded via require. Routes map to controller actions - methods on the controller
-
- var __Page = Page.prototype;
- var DrawerAppRouter = Page.extend({
- // handled events: register for and handle these events.
- // events/routes
- events: {
- 'navigate': 'onNavigate',
- 'stack:change': 'onStackChange',
- // New code path: requests a stack be created by drawer.
- 'stack:create': 'onStackCreate',
- 'social:create': 'onSocialCreate',
- // Old code path: creates stack in `top` and sends details to drawer,
- 'stack:new': 'onNewStack',
- 'stacks:view': 'onStacksView',
- //'place:new': 'onNewPlace',
- 'readyAcknowledged': 'onTopReady'
- },
- initialize: function () {
- // Call superconstructor.
- __Page.initialize.apply(this, arguments);
-
- // The collection of active stacks. This is a property of the Page
- // so it can be shared across controllers
- var stacksCollection = this.stacksCollection = new StackCollection(
- [],
- { urlTokens: { service: 'session', method: 'active' } }
- );
- // Limit the number of stacks returned
- stacksCollection.url(null, { l: 20 });
- var self = this;
- this.bind('change:selected', function (stackModel) {
- // TODO: clearly these disect* methods need to move to a util library or something?
- var stateProperties = Bridge.prototype.dissectStackModel(stackModel);
- self.set(stateProperties, { silent: true });
- });
- // Create a top-level view for app.
- var __WidgetView = WidgetView.prototype;
- var DrawerView = WidgetView.extend({
- // `.pane` is part of the widgetview, not the widgets.
- $panes: function () {
- return this.$('.pane');
- },
- // Fix the height of the wrapping element to be at least
- // the height of the given pane.
- // This prevents empty space from appearing at the bottom
- // of a pane when the inactive pane is longer than the
- // active one.
- //
- // If the currently active pane is shorter than the window,
- // use the window dimensions instead (this keeps items from
- // appearing visually cropped during the CSS transition).
- fitMaskTo: function (viewId) {
- var winHeight = $(window).height();
- var paneHeight = this.$panes().filter(viewId).height();
- // `this.el` is the masking element for the drawer.
- $(this.el).height(paneHeight > winHeight ? paneHeight : winHeight);
- return this;
- },
- // only one widget child can be 'active' at a time
- activateChild: function(viewId) {
- var $el = $(this.el);
- this.activePane = viewId;
- this.fitMaskTo(viewId).$panes().each(function (index) {
- var $pane = $(this),
- active = ('#' + $pane.attr('id')) === viewId;
- $el.toggleClass('panes--depth' + index, active);
- });
- }
- });
- var appView = this.appView = new DrawerView({
- el: $('#drawer')
- });
- this.appView.render();
- },
- onTopReady: function(){
- logger.log("Great, top knows we're ready");
- topReady = true;
- clearInterval(topReadyInterval);
- },
- onLoadRouteMessage: function(msg){
- var route = msg.route;
- if(msg.data) {
- this.set(msg.data, { silent: true });
- }
- this.navigate(route, true);
- },
- _updateStateFromMessage: function(msg){
-
- var stateData = {};
- // pull the properties we need to keep current from the message
- util.each(['session_id', 'stack_id', 'place_id'], function(key){
- if(key in msg){
- stateData[key] = msg[key];
- }
- });
- return this.set(stateData, { silent: true });
- },
- onNavigate: function (msg) {
- require(['controllers/placescontroller'], function (controller) {
- var ret = activateController(controller, {
- appView: app.appView,
- pageState: app,
- collection: app.stacksCollection
- });
- var placeData = {
- place_url: msg.place_url,
- place_title: msg.place_title,
- status: Number(msg.status) >= 400 ? 'error' : 'ok'
- };
- logger.log('onNavigate placeData: ', placeData);
- // FIXME: navigate may not always indicate a new place?
- // its simply a notification that a page was loaded in the viewer
- controller.createPlaceInActiveStack(placeData);
- });
- },
- onStackChange: function(msg){
- this._updateStateFromMessage(msg);
- // when we've added this stack, navigate to it.
- if(msg.place_id){
- this.navigate('stack/'+msg.stack_id+'/'+msg.place_id, true);
- }
- // TODO: I don't think this clause is ever hit. `onStackChange` handles
- // `stack:change`, which are recieved from `main.app.js` via `top.app.js`
- // through `Bridge.prototype.changeStack`. `changeStack` requres
- // a `place_id`. -GB
- else {
- this.navigate('stack/'+msg.stack_id, true);
- }
- },
- onStackCreate: function (msg) {
- var stackCollection = this.stacksCollection;
- // Taking the details in the message, create a `stackModel`, complete with
- // `places()` placeCollection containing the first `siteModel`.
- // We trust that `msg` contains the relevant details for creating a stack.
- var stackModel = 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
- });
- // Add stack to drawer immediately.
- stackCollection.add(stackModel, { at: 0 });
- stackModel.search().then(util.bind(function (stackModel) {
- this.navigate(
- 'stack/'+stackModel.get('stack_id')+'/'+stackModel.get('place_id'),
- true
- );
- }, this));
- },
- onSocialCreate: function (msg) {
- var stackCollection = this.stacksCollection;
- // Just a shared config object.
- var at0 = { at: 0 };
- // Create a fresh `StackModel` for this new social item.
- // Prepare it with information with have from message.
- var stackModel = 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
- });
- // Issue a social request to the server.
- stackModel.social().then(util.bind(function (stackModel) {
- // The way social stacks are handled is a little wonkey. 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.
- var preExistingStackModel = stackCollection.find(function (model) {
- return stackModel.id === model.id;
- });
- // If the model does not exist in the collection already, add it.
- if (!preExistingStackModel) {
- stackCollection.add(stackModel, at0);
- }
- // If the stack does exist, but does not exist in the collection at
- // index 0, move it there.
- else if (stackCollection.indexOf(preExistingStackModel) !== 0) {
- stackCollection.put(preExistingStackModel, at0);
- }
- // Pick a canonical model. This will be the model object we modify.
- // Use the existing `StackModel`, if there is one, or fall back to
- // our new stack model.
- // Capture the places from the canonical model. This is the model
- // we will modify if necessary.
- var places = (preExistingStackModel || stackModel).places();
- // Capture the current `place_id` from the StackModel we just created.
- var place_id = stackModel.get('place_id');
- // If the stack already exists in the collection, and its places have
- // been populated, piece together a `SiteModel` from the data we
- // have on hand, and add it. If there are no places, skip this step,
- // since navigating to `:stack_id/:place_id` will trigger a fetch
- // for places when the place is not found in the collection -- and
- // that's exactly what we want for new models.
- //
- // Verified to be hit for new places in stack.
- if (places.length > 0 && !places.get(place_id)) {
- places.add({
- place_id: stackModel.get('place_id'),
- place_title: stackModel.get('place_title'),
- place_url: stackModel.get('place_url')
- });
- }
- this.navigate(
- 'stack/'+stackModel.get('stack_id')+'/'+place_id,
- true
- );
- }, this));
- },
- onNewStack: function(msg){
- logger.log(config.appName +": onNewStack: " + msg.stack_title);
- var stackData = msg.stack;
- delete msg.stack;
-
- this._updateStateFromMessage(msg);
-
- // when we've added this stack, navigate to it.
- this.navigate('stack/'+stackData.id, true);
- },
- onStacksView: function (msg) {
- this.navigate('stacks/active', true);
- },
- onNewPlace: function(msg){
- logger.log(config.appName +": onNewPlace: ", msg);
- var placeData = msg.place;
- delete msg.place;
- this._updateStateFromMessage(msg);
- // add the new place into the stack, in our stacksCollection
- var stackModel = this.stacksCollection.get(msg.stack_id),
- collection = stackModel && stackModel.places();
- if (!stackModel) throw new errors.IdMissingError("No stack model found in stack collection for this ID: " + msg.stack_id);
- var insertIndex = -1,
- placeRef = null;
- if(msg.insert_after_place) {
- placeRef = collection.get(msg.insert_after_place);
- if(placeRef){
- // insert right after the insert_after_place model id
- insertIndex = 1 + collection.indexOf( placeRef ); // sortedIndex was returning wrong value?
- }
- }
- if(-1 === insertIndex) {
- insertIndex = collection.length-1;
- }
- stackModel.addPlace( placeData, {
- at: insertIndex,
- thumbnails_job: msg.thumbnails_job
- });
- collection.selectPlace(placeData.place_id);
- // we're effectively in the stack/:stackid/:placeid, so mark as such in history
- // GB: what is this doing? Adding true as the second argument (to do a real
- // navigate) creates some strange issues.
- this.navigate('stack/'+msg.stack_id+'/'+placeData.id);
- }
- });
- // Initialize app router.
- var app = global.app = exports.app = new DrawerAppRouter();
-
- // Create a bound activation method
- var activateController = util.bind(app.activateController, app);
-
- // Stack Contents
- app.route('stack/:id', 'contents', function(stackId){
- require(['controllers/placescontroller'], function(controller){
- logger.log("stack selection route, stack:" + stackId);
-
- var ret = activateController(controller, { appView: app.appView, pageState: app, collection: app.stacksCollection });
- controller.viewPlaces(stackId);
- });
- });
- // Stack Contents & Place selected
- app.route('stack/:id/:placeid', 'place', function(stackId, placeId){
- require(['controllers/placescontroller'], function(controller){
- logger.log("place selection route, stack: %s, place: %s: ", stackId, placeId);
-
- var ret = activateController(controller, { appView: app.appView, pageState: app, collection: app.stacksCollection });
- controller.viewPlaces(stackId, placeId);
- });
- });
- var activeController = function(){
- require(['controllers/stacklistcontroller'], function(controller){
- var ret = activateController(controller, { appView: app.appView, pageState: app, collection: app.stacksCollection });
- });
- };
- // Active Stacks
- app.route('stacks/active', 'list', activeController);
- app.route('', 'list', activeController);
- // work around a race condition where this window checks in before the 'top' dispatcher is ready
- var topReady,
- topReadyInterval = setInterval(notifyReady, 500),
- 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;
- });