bd18801f6a7e121b79e6306d8e8b14ca393ef7c4
[xtuple] / lib / enyo-x / source / app.js
1 /*jshint indent:2, curly:true, eqeqeq:true, immed:true, latedef:true,
2 newcap:true, noarg:true, regexp:true, undef:true, trailing:true,
3 white:true*/
4 /*global Backbone:true, enyo:true, XT:true, _:true, document:true, window:true, XM:true */
5
6 (function () {
7
8   var UNINITIALIZED = 0;
9   var LOADING_SESSION = 1;
10   var LOADING_EXTENSIONS = 3;
11   var LOADING_SCHEMA = 4;
12   var LOADING_APP_DATA = 5;
13   var RUNNING = 6;
14
15   /**
16     Application kind. Contains two components, "postbooks", which is a module container,
17     and the pullout.
18    */
19   enyo.kind(
20     /** @lends XV.App# */{
21     name: "XV.App",
22     classes: "enyo-fit enyo-unselectable",
23     published: {
24       isStarted: false,
25       debug: false,
26       keyCapturePatterns: []
27     },
28     handlers: {
29       onListAdded: "addPulloutItem",
30       onModelChange: "modelChanged",
31       onParameterChange: "parameterDidChange",
32       onNavigatorEvent: "togglePullout",
33       onHistoryChange: "refreshHistoryPanel",
34       onHistoryItemSelected: "selectHistoryItem",
35       onSearch: "waterfallSearch",
36       onWorkspace: "waterfallWorkspace",
37       onColumnsChange: "columnsDidChange"
38     },
39     /*
40       Three use cases:
41       hotkey mode (triggered by alt)
42       notify popup mode (if the notify popup is showing)
43       pattern-match mode
44     */
45     handleKeyDown: function (inSender, inEvent) {
46       var that = this,
47         keyCode = inEvent.keyCode,
48         exoticTransforms = {},
49         numberShiftCharacters = ")!@#$%^&*(";
50
51       // exotic transforms. sorta hackish, but the inEvent codes coming
52       // through are not always the ones we want
53
54       // http://stackoverflow.com/questions/1898129/javascript-subtract-keycode
55       exoticTransforms[XT.ASCII.VULGAR_HALF] = XT.ASCII.N_DASH;
56       if (_.contains(Object.keys(exoticTransforms), "" + keyCode)) {
57         // account for inexplicably wrongly mapped keys
58         keyCode = exoticTransforms[keyCode];
59       } else if (inEvent.shiftKey && keyCode >= XT.ASCII.ZERO && keyCode <= XT.ASCII.NINE) {
60         // shift-number should be transformed into the appropriate character
61         // XXX jingoistic implementation
62         keyCode = numberShiftCharacters.charCodeAt(keyCode - XT.ASCII.ZERO);
63       }
64
65       // remember the last 10 key presses
66       this._keyBufferArray.push(keyCode);
67       this._keyBufferArray = this._keyBufferArray.slice(-10);
68
69       // not working
70       if (this.$.postbooks.isNotifyPopupShowing()) {
71         this.$.postbooks.notifyKey(keyCode, inEvent.shiftKey);
72         return;
73       }
74
75       if (inEvent.altKey) {
76         inEvent.cancelBubble = true;
77         inEvent.returnValue = false;
78         this.processHotKey(keyCode);
79         return true;
80       }
81       if (this._keyBufferEndPattern) {
82         // we're in record mode, so record.
83         this._keyBuffer = this._keyBuffer + String.fromCharCode(keyCode);
84       }
85
86       if (this._keyBufferEndPattern &&
87           _.isEqual(this._keyBufferArray.slice(-1 * this._keyBufferEndPattern.length), this._keyBufferEndPattern) &&
88           this._falsePositives) {
89         this._falsePositives--;
90
91       } else if (this._keyBufferEndPattern &&
92           _.isEqual(this._keyBufferArray.slice(-1 * this._keyBufferEndPattern.length), this._keyBufferEndPattern)) {
93
94         // first slice the end pattern off the payload
95         this._keyBuffer = this._keyBuffer.substring(0, this._keyBuffer.length - this._keyBufferEndPattern.length);
96
97         // we've matched an end pattern. Send the recorded buffer to the appropriate method
98         this[this._keyBufferMethod](this._keyBuffer);
99         this._keyBuffer = "";
100         this._keyBufferEndPattern = undefined;
101         this._falsePositives = undefined;
102       }
103
104       // specification of the key capture patterns themselves are up to the subkind
105       _.each(this.getKeyCapturePatterns(), function (pattern) {
106         if (_.isEqual(that._keyBufferArray.slice(-1 * pattern.start.length), pattern.start)) {
107           // we've matched a start pattern. Now we're in record mode, waiting for a match to the end pattern
108           that._keyBuffer = that._keyBuffer || "";
109           that._keyBufferEndPattern = pattern.end;
110           that._keyBufferMethod = pattern.method;
111           that._falsePositives = pattern.falsePositives;
112         }
113       });
114
115     },
116     // the components array is overriden by the subkind
117     components: [
118       {name: "postbooks", kind: "XV.ModuleContainer",  onTransitionStart: "handlePullout"},
119       {name: "pullout", kind: "XV.Pullout", onAnimateFinish: "pulloutAnimateFinish"},
120       {name: "signals", kind: "enyo.Signals", onkeydown: "handleKeyDown"},
121     ],
122     state: UNINITIALIZED,
123     /**
124       Passes the pullout payload straight from the sender (presumably the list
125       containing the pullout parameter) to the pullout, who will deal with
126       adding it.
127      */
128     addPulloutItem: function (inSender, inEvent) {
129       if (!this.$.pullout) {
130         this._cachePullouts.push(inEvent);
131         return;
132       }
133       this.$.pullout.addPulloutItem(inSender, inEvent);
134     },
135     columnsDidChange: function (inSender, inEvent) {
136       this.$.postbooks.getNavigator().waterfall("onColumnsChange", inEvent);
137     },
138     create: function () {
139       this._cachePullouts = [];
140       this._keyBufferArray = [];
141       this.inherited(arguments);
142       XT.app = this;
143       // make even modal popups hear keydown (necessary for keyboarding the notify popup)
144       enyo.dispatcher.autoForwardEvents.keydown = 1;
145       window.onbeforeunload = function () {
146         return "_exitPageWarning".loc();
147       };
148     },
149     handlePullout: function (inSender, inEvent) {
150       var showing = inSender.getActive().showPullout || false;
151       this.$.pullout.setShowing(showing);
152     },
153     /*
154       When a model is changed, we want to reflect the new state across
155       the app, so we bubble all the way up there and waterfall down.
156       Currently, the only things that are updated by this process are the lists.
157     */
158     modelChanged: function (inSender, inEvent) {
159       //
160       // Waterfall down to all lists to tell them to update themselves
161       //
162       this.$.postbooks.getNavigator().waterfall("onModelChange", inEvent);
163     },
164     parameterDidChange: function (inSender, inEvent) {
165       if (this.getIsStarted()) {
166         this.$.postbooks.getNavigator().waterfall("onParameterChange", inEvent);
167       }
168     },
169     /**
170       Implemented by the subkind
171     */
172     processHotKey: function (keyCode) {
173     },
174     /**
175      * Manages the "lit-up-ness" of the icon buttons based on the pullout.
176      * If the pull-out is put away, we want all buttons to dim. If the pull-out
177      * is activated, we want the button related to the active pullout pane
178      * to light up. The presentation of these buttons take care of themselves
179      * if the user actually clicks on the buttons.
180      */
181     pulloutAnimateFinish: function (inSender, inEvent) {
182       var activeIconButton;
183
184       if (inSender.value === inSender.max) {
185         // pullout is active
186         if (this.$.pullout.getSelectedPanel() === 'history') {
187           activeIconButton = 'history';
188         } else {
189           activeIconButton = 'search';
190         }
191       } else if (inSender.value === inSender.min) {
192         // pullout is inactive
193         activeIconButton = null;
194       }
195       this.$.postbooks.getNavigator().setActiveIconButton(activeIconButton);
196     },
197     refreshHistoryPanel: function (inSender, inEvent) {
198       this.$.pullout.refreshHistoryList();
199     },
200     /**
201       When a history item is selected we bubble an event way up the application.
202       Note that we create a sort of ersatz model to mimic the way the handler
203       expects to have a model with the event to know what to drill down into.
204     */
205     selectHistoryItem: function (inSender, inEvent) {
206       inEvent.eventName = "onWorkspace";
207       this.waterfall("onWorkspace", inEvent);
208     },
209     startupProcess: function () {
210       var startupManager = XT.getStartupManager(),
211         progressBar = XT.app.$.postbooks.getStartupProgressBar(),
212         that = this,
213         prop,
214         ext,
215         extprop,
216         i = 0,
217         task,
218         startupTaskCount,
219         text,
220         inEvent,
221         ajax,
222         extensionSuccess,
223         extensionError,
224         extensionLocation,
225         extensionName,
226         extensionPrivilegeName,
227         extensionCount = 0,
228         extensionsDownloaded = 0,
229         extensionPayloads = [],
230         loginInfo,
231         details,
232         eachCallback = function () {
233           var completed = startupManager.get('completed').length;
234           progressBar.animateProgressTo(completed);
235           if (completed === startupTaskCount) {
236             that.startupProcess();
237           }
238         };
239
240       // 1: Load session data
241       if (this.state === UNINITIALIZED) {
242         this.state = LOADING_SESSION;
243         startupManager.registerCallback(eachCallback, true);
244         XT.dataSource.connect();
245         startupTaskCount = startupManager.get('queue').length + startupManager.get('completed').length;
246         progressBar.setMax(startupTaskCount);
247
248       // #2 is off high-wire walking the Grand Canyon
249
250       // 3: Initialize extensions
251       } else if (this.state === LOADING_SESSION) {
252
253         this.state = LOADING_EXTENSIONS;
254         text = "_loadingExtensions".loc() + "...";
255         XT.app.$.postbooks.getStartupText().setContent(text);
256         for (prop in XT.extensions) {
257           if (XT.extensions.hasOwnProperty(prop)) {
258             ext = XT.extensions[prop];
259             for (extprop in ext) {
260               if (ext.hasOwnProperty(extprop) &&
261                   typeof ext[extprop] === "function") {
262                 //XT.log('Installing ' + prop + ' ' + extprop);
263                 ext[extprop]();
264               }
265             }
266           }
267           i++;
268         }
269         this.startupProcess();
270
271       // 4. Load Schema
272       } else if (this.state === LOADING_EXTENSIONS) {
273         this.state = LOADING_SCHEMA;
274         text = "_loadingSchema".loc() + "...";
275         XT.app.$.postbooks.getStartupText().setContent(text);
276         XT.StartupTask.create({
277           taskName: "loadSessionSchema",
278           task: function () {
279             var task = this,
280               options = {
281                 success: function () {
282                   task.didComplete();
283                   that.startupProcess();
284                 }
285               };
286             XT.session.loadSessionObjects(XT.session.SCHEMA, options);
287           }
288         });
289
290       // 5 Load Application Data
291       } else if (this.state === LOADING_SCHEMA) {
292         // Run startup tasks
293         this.state = LOADING_APP_DATA;
294         text = "_loadingApplicationData".loc() + "...";
295         XT.app.$.postbooks.getStartupText().setContent(text);
296         progressBar.setMax(XT.StartupTasks.length);
297         progressBar.setProgress(0);
298
299         // there's a new startup task count now that
300         // the second stage of tasks are being loaded.
301         startupTaskCount = startupManager.get('queue').length +
302           startupManager.get('completed').length +
303           XT.StartupTasks.length;
304
305         // create a new each callback to manage the completion of this step
306         // the previously registered callback isn't doing any harm. It would be
307         // best to unregister the previous eachCallback and replicate the code
308         // here that advances the progress bar. This would make the animation look
309         // better. There's not a great way to unregister a startup callback, though.
310         eachCallback = function () {
311           var completed = startupManager.get('completed').length;
312           if (completed === startupTaskCount) {
313             that.startupProcess();
314           }
315         };
316
317         startupManager.registerCallback(eachCallback, true);
318         for (i = 0; i < XT.StartupTasks.length; i++) {
319           task = XT.StartupTasks[i];
320           XT.StartupTask.create(task);
321         }
322
323       // 6. Finish up
324       } else if (this.state === LOADING_APP_DATA) {
325         // Go to the navigator
326         for (i = 0; i < this._cachePullouts.length; i++) {
327           inEvent = this._cachePullouts[i];
328           this.$.pullout.addPulloutItem(null, inEvent);
329         }
330         loginInfo = XT.app.$.postbooks.getNavigator().loginInfo();
331         details = XT.session.details;
332         loginInfo.setContent(details.username + " ยท " + details.organization);
333         this.state = RUNNING;
334         that.startupProcess();
335
336       // TODO - 7. Initiate Empty Database Startup "checklist" process for empty/quick start databases
337       } else if (this.state === RUNNING) {
338         // No process in place so proceed to activate
339         XM.Tuplespace.trigger("activate");
340         XT.app.$.postbooks.activate();
341
342         // Send no Base Currency event
343         if (!XT.baseCurrency()) {
344           this.waterfallNoBaseCurr();
345         }
346         Backbone.history.start();
347       }
348     },
349     start: function (debug) {
350       if (this.getIsStarted()) {return; }
351       XT.app = this;
352       this.setDebug(debug);
353
354       // Run through the multi-step start process
355       this.startupProcess();
356
357       // lets not allow this to happen again
358       this.setIsStarted(true);
359     },
360     show: function () {
361       if (this.getShowing() && this.getIsStarted()) {
362         this.renderInto(document.body);
363       } else {
364         this.inherited(arguments);
365       }
366     },
367     togglePullout: function (inSender, inEvent) {
368       this.$.pullout.togglePullout(inSender, inEvent);
369     },
370     waterfallNoBaseCurr: function (inSender, inEvent) {
371       this.$.postbooks.waterfall("onNoBaseCurr", inEvent);
372       return true;
373     },
374     waterfallSearch: function (inSender, inEvent) {
375       this.$.postbooks.waterfall("onSearch", inEvent);
376       return true; // don't want to double up
377     },
378     waterfallWorkspace: function (inSender, inEvent) {
379       this.$.postbooks.waterfall("onWorkspace", inEvent);
380       return true; // don't want to double up
381     }
382   });
383 }());