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