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