PageRenderTime 48ms CodeModel.GetById 19ms RepoModel.GetById 1ms app.codeStats 0ms

/static/js/drawer.app.js

https://bitbucket.org/gordonbrander/accordion-drawer-prototypes
JavaScript | 415 lines | 262 code | 62 blank | 91 comment | 20 complexity | 2c3356cb5cd5438ff5072ed28ba2ad8f MD5 | raw file
Possible License(s): Apache-2.0
  1. define([
  2. 'config',
  3. '$',
  4. 'underscore',
  5. 'backbone',
  6. 'lib/page',
  7. 'lib/bridge',
  8. 'views/widgetview',
  9. 'models/stackcollection',
  10. 'lib/template',
  11. 'xmessage',
  12. 'lib/errors',
  13. 'lib/logger/noisy',
  14. // preload controlers
  15. 'controllers/placescontroller',
  16. 'controllers/stacklistcontroller'
  17. ], function (
  18. config,
  19. $,
  20. util,
  21. Backbone,
  22. Page,
  23. Bridge,
  24. WidgetView,
  25. StackCollection,
  26. template,
  27. xmessage,
  28. errors,
  29. logger
  30. ) {
  31. var global = window,
  32. exports = {},
  33. // Create a local reference to StackModel, using the reference to it
  34. // that lives on StackCollection.
  35. StackModel = StackCollection.prototype.model;
  36. // Make sure uncaught exceptions are send along to the iOS app.
  37. if (config.platform === 'ios' && logger.listenForUncaughtExceptions)
  38. logger.listenForUncaughtExceptions();
  39. // DrawerAppRouter acts as Page-level model, and front-controller for the drawer 'application'
  40. // it provides entry points via Backbone's routing mechanism
  41. // and also by handling xmessage events
  42. // Each route uses one of 2 controllers: stacklistcontroller or placescontroller
  43. // these are lazily loaded via require. Routes map to controller actions - methods on the controller
  44. var __Page = Page.prototype;
  45. var DrawerAppRouter = Page.extend({
  46. // handled events: register for and handle these events.
  47. // events/routes
  48. events: {
  49. 'navigate': 'onNavigate',
  50. 'stack:change': 'onStackChange',
  51. // New code path: requests a stack be created by drawer.
  52. 'stack:create': 'onStackCreate',
  53. 'social:create': 'onSocialCreate',
  54. // Old code path: creates stack in `top` and sends details to drawer,
  55. 'stack:new': 'onNewStack',
  56. 'stacks:view': 'onStacksView',
  57. //'place:new': 'onNewPlace',
  58. 'readyAcknowledged': 'onTopReady'
  59. },
  60. initialize: function () {
  61. // Call superconstructor.
  62. __Page.initialize.apply(this, arguments);
  63. // The collection of active stacks. This is a property of the Page
  64. // so it can be shared across controllers
  65. var stacksCollection = this.stacksCollection = new StackCollection(
  66. [],
  67. { urlTokens: { service: 'session', method: 'active' } }
  68. );
  69. // Limit the number of stacks returned
  70. stacksCollection.url(null, { l: 20 });
  71. var self = this;
  72. this.bind('change:selected', function (stackModel) {
  73. // TODO: clearly these disect* methods need to move to a util library or something?
  74. var stateProperties = Bridge.prototype.dissectStackModel(stackModel);
  75. self.set(stateProperties, { silent: true });
  76. });
  77. // Create a top-level view for app.
  78. var __WidgetView = WidgetView.prototype;
  79. var DrawerView = WidgetView.extend({
  80. // `.pane` is part of the widgetview, not the widgets.
  81. $panes: function () {
  82. return this.$('.pane');
  83. },
  84. // Fix the height of the wrapping element to be at least
  85. // the height of the given pane.
  86. // This prevents empty space from appearing at the bottom
  87. // of a pane when the inactive pane is longer than the
  88. // active one.
  89. //
  90. // If the currently active pane is shorter than the window,
  91. // use the window dimensions instead (this keeps items from
  92. // appearing visually cropped during the CSS transition).
  93. fitMaskTo: function (viewId) {
  94. var winHeight = $(window).height();
  95. var paneHeight = this.$panes().filter(viewId).height();
  96. // `this.el` is the masking element for the drawer.
  97. $(this.el).height(paneHeight > winHeight ? paneHeight : winHeight);
  98. return this;
  99. },
  100. // only one widget child can be 'active' at a time
  101. activateChild: function(viewId) {
  102. var $el = $(this.el);
  103. this.activePane = viewId;
  104. this.fitMaskTo(viewId).$panes().each(function (index) {
  105. var $pane = $(this),
  106. active = ('#' + $pane.attr('id')) === viewId;
  107. $el.toggleClass('panes--depth' + index, active);
  108. });
  109. }
  110. });
  111. var appView = this.appView = new DrawerView({
  112. el: $('#drawer')
  113. });
  114. this.appView.render();
  115. },
  116. onTopReady: function(){
  117. logger.log("Great, top knows we're ready");
  118. topReady = true;
  119. clearInterval(topReadyInterval);
  120. },
  121. onLoadRouteMessage: function(msg){
  122. var route = msg.route;
  123. if(msg.data) {
  124. this.set(msg.data, { silent: true });
  125. }
  126. this.navigate(route, true);
  127. },
  128. _updateStateFromMessage: function(msg){
  129. var stateData = {};
  130. // pull the properties we need to keep current from the message
  131. util.each(['session_id', 'stack_id', 'place_id'], function(key){
  132. if(key in msg){
  133. stateData[key] = msg[key];
  134. }
  135. });
  136. return this.set(stateData, { silent: true });
  137. },
  138. onNavigate: function (msg) {
  139. require(['controllers/placescontroller'], function (controller) {
  140. var ret = activateController(controller, {
  141. appView: app.appView,
  142. pageState: app,
  143. collection: app.stacksCollection
  144. });
  145. var placeData = {
  146. place_url: msg.place_url,
  147. place_title: msg.place_title,
  148. status: Number(msg.status) >= 400 ? 'error' : 'ok'
  149. };
  150. logger.log('onNavigate placeData: ', placeData);
  151. // FIXME: navigate may not always indicate a new place?
  152. // its simply a notification that a page was loaded in the viewer
  153. controller.createPlaceInActiveStack(placeData);
  154. });
  155. },
  156. onStackChange: function(msg){
  157. this._updateStateFromMessage(msg);
  158. // when we've added this stack, navigate to it.
  159. if(msg.place_id){
  160. this.navigate('stack/'+msg.stack_id+'/'+msg.place_id, true);
  161. }
  162. // TODO: I don't think this clause is ever hit. `onStackChange` handles
  163. // `stack:change`, which are recieved from `main.app.js` via `top.app.js`
  164. // through `Bridge.prototype.changeStack`. `changeStack` requres
  165. // a `place_id`. -GB
  166. else {
  167. this.navigate('stack/'+msg.stack_id, true);
  168. }
  169. },
  170. onStackCreate: function (msg) {
  171. var stackCollection = this.stacksCollection;
  172. // Taking the details in the message, create a `stackModel`, complete with
  173. // `places()` placeCollection containing the first `siteModel`.
  174. // We trust that `msg` contains the relevant details for creating a stack.
  175. var stackModel = new StackModel({
  176. stack_title: msg.search_terms,
  177. place_title: msg.place_title,
  178. place_url: msg.place_url,
  179. // We're currently handed `search_url` via the msg, which I think is
  180. // the right thing. We'll let the originator of the message tell us
  181. // what the `search_url` is. It should know.
  182. search_url: msg.search_url,
  183. search_provider: msg.search_provider,
  184. search_terms: msg.search_terms
  185. });
  186. // Add stack to drawer immediately.
  187. stackCollection.add(stackModel, { at: 0 });
  188. stackModel.search().then(util.bind(function (stackModel) {
  189. this.navigate(
  190. 'stack/'+stackModel.get('stack_id')+'/'+stackModel.get('place_id'),
  191. true
  192. );
  193. }, this));
  194. },
  195. onSocialCreate: function (msg) {
  196. var stackCollection = this.stacksCollection;
  197. // Just a shared config object.
  198. var at0 = { at: 0 };
  199. // Create a fresh `StackModel` for this new social item.
  200. // Prepare it with information with have from message.
  201. var stackModel = new StackModel({
  202. // Stack titles are defined by friend name
  203. stack_title: msg.friend_name,
  204. friend_name: msg.friend_name,
  205. friend_url: msg.friend_url,
  206. service_url: msg.service_url,
  207. place_title: msg.place_title,
  208. place_url: msg.place_url
  209. });
  210. // Issue a social request to the server.
  211. stackModel.social().then(util.bind(function (stackModel) {
  212. // The way social stacks are handled is a little wonkey. The server-
  213. // side makes sure that people are de-duped, but the client side
  214. // can't really know whether a person has been created or not.
  215. // Anyway, the takeaway is that we check for the stackmodel first.
  216. // If it exists in the collection already, we don't add it.
  217. var preExistingStackModel = stackCollection.find(function (model) {
  218. return stackModel.id === model.id;
  219. });
  220. // If the model does not exist in the collection already, add it.
  221. if (!preExistingStackModel) {
  222. stackCollection.add(stackModel, at0);
  223. }
  224. // If the stack does exist, but does not exist in the collection at
  225. // index 0, move it there.
  226. else if (stackCollection.indexOf(preExistingStackModel) !== 0) {
  227. stackCollection.put(preExistingStackModel, at0);
  228. }
  229. // Pick a canonical model. This will be the model object we modify.
  230. // Use the existing `StackModel`, if there is one, or fall back to
  231. // our new stack model.
  232. // Capture the places from the canonical model. This is the model
  233. // we will modify if necessary.
  234. var places = (preExistingStackModel || stackModel).places();
  235. // Capture the current `place_id` from the StackModel we just created.
  236. var place_id = stackModel.get('place_id');
  237. // If the stack already exists in the collection, and its places have
  238. // been populated, piece together a `SiteModel` from the data we
  239. // have on hand, and add it. If there are no places, skip this step,
  240. // since navigating to `:stack_id/:place_id` will trigger a fetch
  241. // for places when the place is not found in the collection -- and
  242. // that's exactly what we want for new models.
  243. //
  244. // Verified to be hit for new places in stack.
  245. if (places.length > 0 && !places.get(place_id)) {
  246. places.add({
  247. place_id: stackModel.get('place_id'),
  248. place_title: stackModel.get('place_title'),
  249. place_url: stackModel.get('place_url')
  250. });
  251. }
  252. this.navigate(
  253. 'stack/'+stackModel.get('stack_id')+'/'+place_id,
  254. true
  255. );
  256. }, this));
  257. },
  258. onNewStack: function(msg){
  259. logger.log(config.appName +": onNewStack: " + msg.stack_title);
  260. var stackData = msg.stack;
  261. delete msg.stack;
  262. this._updateStateFromMessage(msg);
  263. // when we've added this stack, navigate to it.
  264. this.navigate('stack/'+stackData.id, true);
  265. },
  266. onStacksView: function (msg) {
  267. this.navigate('stacks/active', true);
  268. },
  269. onNewPlace: function(msg){
  270. logger.log(config.appName +": onNewPlace: ", msg);
  271. var placeData = msg.place;
  272. delete msg.place;
  273. this._updateStateFromMessage(msg);
  274. // add the new place into the stack, in our stacksCollection
  275. var stackModel = this.stacksCollection.get(msg.stack_id),
  276. collection = stackModel && stackModel.places();
  277. if (!stackModel) throw new errors.IdMissingError("No stack model found in stack collection for this ID: " + msg.stack_id);
  278. var insertIndex = -1,
  279. placeRef = null;
  280. if(msg.insert_after_place) {
  281. placeRef = collection.get(msg.insert_after_place);
  282. if(placeRef){
  283. // insert right after the insert_after_place model id
  284. insertIndex = 1 + collection.indexOf( placeRef ); // sortedIndex was returning wrong value?
  285. }
  286. }
  287. if(-1 === insertIndex) {
  288. insertIndex = collection.length-1;
  289. }
  290. stackModel.addPlace( placeData, {
  291. at: insertIndex,
  292. thumbnails_job: msg.thumbnails_job
  293. });
  294. collection.selectPlace(placeData.place_id);
  295. // we're effectively in the stack/:stackid/:placeid, so mark as such in history
  296. // GB: what is this doing? Adding true as the second argument (to do a real
  297. // navigate) creates some strange issues.
  298. this.navigate('stack/'+msg.stack_id+'/'+placeData.id);
  299. }
  300. });
  301. // Initialize app router.
  302. var app = global.app = exports.app = new DrawerAppRouter();
  303. // Create a bound activation method
  304. var activateController = util.bind(app.activateController, app);
  305. // Stack Contents
  306. app.route('stack/:id', 'contents', function(stackId){
  307. require(['controllers/placescontroller'], function(controller){
  308. logger.log("stack selection route, stack:" + stackId);
  309. var ret = activateController(controller, { appView: app.appView, pageState: app, collection: app.stacksCollection });
  310. controller.viewPlaces(stackId);
  311. });
  312. });
  313. // Stack Contents & Place selected
  314. app.route('stack/:id/:placeid', 'place', function(stackId, placeId){
  315. require(['controllers/placescontroller'], function(controller){
  316. logger.log("place selection route, stack: %s, place: %s: ", stackId, placeId);
  317. var ret = activateController(controller, { appView: app.appView, pageState: app, collection: app.stacksCollection });
  318. controller.viewPlaces(stackId, placeId);
  319. });
  320. });
  321. var activeController = function(){
  322. require(['controllers/stacklistcontroller'], function(controller){
  323. var ret = activateController(controller, { appView: app.appView, pageState: app, collection: app.stacksCollection });
  324. });
  325. };
  326. // Active Stacks
  327. app.route('stacks/active', 'list', activeController);
  328. app.route('', 'list', activeController);
  329. // work around a race condition where this window checks in before the 'top' dispatcher is ready
  330. var topReady,
  331. topReadyInterval = setInterval(notifyReady, 500),
  332. topReadyTries = 0;
  333. function notifyReady(){
  334. if(topReady || topReadyTries > 20) {
  335. clearInterval(topReadyInterval);
  336. } else {
  337. topReadyTries++;
  338. logger.log(config.appName + " sending ready message to top");
  339. xmessage.sendMessage("top", "ready", [{
  340. origin: config.appName, type: "ready", acknowledge: 'readyAcknowledged'
  341. }]);
  342. }
  343. }
  344. notifyReady();
  345. // ---------
  346. // Set a "static" reference to the router instance on bridge.
  347. // Used by bridge.navigate (a facade for Router.prototype.navigate).
  348. Bridge.setActiveRouter(app);
  349. // Now that all of our routes are configured, start router listener.
  350. Backbone.history.start({ pushState: false });
  351. // Expose exports to Require (used by drawer spec)
  352. return exports;
  353. });