0622eec49dea457a2dc446e9f23a16ddadaef768
[xtuple] / lib / enyo-x / source / views / module_container.js
1 /*jshint bitwise:true, indent:2, curly:true, eqeqeq:true, immed:true,
2 latedef:true, newcap:true, noarg:true, regexp:true, undef:true,
3 trailing:true, white:true*/
4 /*global XT:true, XM:true, _:true, enyo:true*/
5
6 (function () {
7
8   /**
9     @class
10     @name XV.ModuleContainer
11     @extends enyo.Panels
12     */
13   enyo.kind(
14     /** @lends XV.ModuleContainer# */{
15     name: "XV.ModuleContainer",
16     kind: "Panels",
17     arrangerKind: "CardArranger",
18     draggable: false,
19     classes: "enyo-fit",
20     handlers: {
21       onPrevious: "previous",
22       onSearch: "addSearch",
23       onTransactionList: "addTransactionList",
24       onWorkspace: "addWorkspace",
25       onChildWorkspace: "addChildWorkspacePanel",
26       onPopupWorkspace: "popupWorkspace",
27       onNotify: "notify",
28       onTransitionFinish: "handleNotify"
29     },
30     published: {
31       modules: null
32     },
33     components: [
34       {name: "startup", classes: "xv-startup-panel",
35         components: [
36         {name: "startupText", classes: "xv-startup-divider",
37           content: "_loadingSessionData".loc() + "..."},
38         {name: "startupProgressBar", kind: "onyx.ProgressBar",
39           classes: "xv-startup-progress  onyx-progress-button", progress: 0}
40       ]},
41       {kind: "onyx.Popup", name: "notifyPopup", classes: "xv-popup", centered: true,
42         onHide: "notifyHidden",
43         modal: true, floating: true, scrim: true, components: [
44         {name: "notifyMessage", classes: "message"},
45         {classes: "xv-buttons", name: "notifyButtons", components: [
46           {kind: "onyx.Button", content: "_ok".loc(), name: "notifyOk", ontap: "notifyTap",
47             showing: false, classes: "text"},
48           {kind: "onyx.Button", content: "_yes".loc(), name: "notifyYes", ontap: "notifyTap",
49             showing: false, classes: "text"},
50           {kind: "onyx.Button", content: "_no".loc(), name: "notifyNo", ontap: "notifyTap",
51             showing: false, classes: "text"},
52           {kind: "onyx.Button", content: "_cancel".loc(), name: "notifyCancel", ontap: "notifyTap",
53             showing: false, classes: "text"}
54         ]}
55       ]},
56       {kind: "onyx.Popup", name: "popupWorkspace", classes: "xv-popup xv-groupbox-popup", centered: true,
57         autoDismiss: false, modal: true, floating: true, scrim: true},
58       {name: "navigator", kind: "XV.Navigator"}
59     ],
60     activate: function () {
61       this.goToNavigator();
62       this.$.navigator.activate();
63     },
64     addSearch: function (inSender, inEvent) {
65       var panel;
66       if (inEvent.list) {
67         panel = this.createComponent({kind: "XV.SearchContainer"});
68         panel.render();
69         this.reflow();
70         panel.setList(inEvent);
71         panel.fetch();
72         this.next();
73       } else {
74         XT.log("No list associated with this model for searching. Are you sure you've registered it?");
75       }
76       return true;
77     },
78     /**
79       Do the exact same thing as addWorkspace, just use a ChildWorkspaceContainer
80       instead of a WorkspaceContainer.
81      */
82     addChildWorkspacePanel: function (inSender, inEvent) {
83       this.addWorkspace(inSender, inEvent, "XV.ChildWorkspaceContainer");
84     },
85     addTransactionList: function (inSender, inEvent) {
86       var panel = this.createComponent({kind: inEvent.kind}),
87         order = panel.$.parameterWidget.$.order;
88
89       panel.setCallback(inEvent.callback);
90       panel.render();
91       this.reflow();
92       if (inEvent.key) {
93         order.setValue(inEvent.key);
94         order.setDisabled(true);
95       }
96       this.setIndex(this.getPanels().length - 1);
97
98       return true;
99     },
100     /**
101       Create and drill down into a new workspace as defined by the inEvent object
102      */
103     addWorkspace: function (inSender, inEvent, workspaceContainerKind) {
104       var panel;
105
106       workspaceContainerKind = workspaceContainerKind || "XV.WorkspaceContainer"; // default
107
108       if (inEvent.workspace) {
109         panel = this.createComponent({kind: workspaceContainerKind});
110         panel.render();
111         this.reflow();
112         panel.setWorkspace(inEvent);
113         // this workspace is now the last panel. Go to it.
114         this.setIndex(this.getPanels().length - 1);
115       } else if (inEvent.kind) {
116         if (inEvent.model) {
117           panel = this.createComponent({kind: inEvent.kind, model: inEvent.model});
118         } else {
119           panel = this.createComponent({kind: inEvent.kind});
120         }
121         panel.render();
122         this.reflow();
123         // this workspace is now the last panel. Go to it.
124         this.setIndex(this.getPanels().length - 1);
125       }
126       return true;
127     },
128     /**
129       Add panels to a module. If any are found to already
130       exist by the same name they will be ignored.
131
132       @param {String} Module name
133       @param {Array} Panels
134       @param {Boolean} Append panels first
135     */
136     appendPanels: function (moduleName, panels, first) {
137       var modules = this.getModules(),
138         module = _.find(modules, function (mod) {
139           return mod.name === moduleName;
140         }),
141         existing,
142         i;
143
144       if (!module) {
145         // crash coming soon!
146         XT.log("Error: trying to insert panel into nonexistent module", moduleName);
147       }
148       existing = _.pluck(module.panels, "name");
149
150       for (i = 0; i < panels.length; i++) {
151         if (!_.contains(existing, panels[i].name)) {
152           if (first) {
153             module.panels.unshift(panels[i]);
154           }
155           else {
156             module.panels.push(panels[i]);
157           }
158         }
159       }
160       if (module.sortAlpha) {
161         // keep these alphabetically sorted
162         module.panels = _.sortBy(module.panels, function (panel) {
163           return panel.name;
164         });
165       }
166       this._setModules();
167     },
168     create: function () {
169       this.inherited(arguments);
170       this._setModules();
171     },
172     getNavigator: function () {
173       return this.$.navigator;
174     },
175     getNotifyButtons: function () {
176       return this.$.notifyButtons.controls;
177     },
178     getStartupProgressBar: function () {
179       return this.$.startupProgressBar;
180     },
181     getStartupText: function () {
182       return this.$.startupText;
183     },
184     goToNavigator: function () {
185       var that = this;
186
187       _.each(this.getPanels(), function (panel, index) {
188         if (panel.name === 'navigator') {
189           that.setIndex(index);
190         }
191       });
192     },
193     handleNotify: function () {
194       if (this._pendingNotify) {
195         this.notify(
196           this._pendingNotify.inSender,
197           this._pendingNotify.inEvent
198         );
199         delete this._pendingNotify;
200       }
201     },
202     /**
203       Insert a new `module`. `Index` is currently ignored,
204       but may be used in the future. The modules are appended to the
205       end of the menu (before setup) based on extension load order.
206
207       @param {Object} Module
208       @param {Number} Index
209     */
210     insertModule: function (module, index) {
211       var modules = this.getModules(),
212         count = modules.length;
213       index = count;
214       modules.splice(index - 1, 0, module);
215       this._setModules();
216     },
217     isNotifyPopupShowing: function () {
218       return this.$.notifyPopup.showing;
219     },
220     /**
221       The model wants to ask the user something.
222      */
223     notify: function (inSender, inEvent) {
224       var that = this,
225         customComponentControls,
226         typeToButtonMap = {};
227
228       // If we're still animating, then we'll do this when
229       // that's done via `handleNotify`. Otherwise the popup
230       // will get tangled up in events and lost.
231       if (this.transitionPoints.length) {
232         this._pendingNotify = {
233           inSender: inSender,
234           inEvent: inEvent
235         };
236         return;
237       }
238
239       typeToButtonMap[String(XM.Model.NOTICE)] = ["notifyOk"];
240       typeToButtonMap[String(XM.Model.WARNING)] = ["notifyOk"];
241       typeToButtonMap[String(XM.Model.CRITICAL)] = ["notifyOk"];
242       typeToButtonMap[String(XM.Model.QUESTION)] = ["notifyYes", "notifyNo"];
243       typeToButtonMap[String(XM.Model.OK_CANCEL)] = ["notifyOk", "notifyCancel"];
244       typeToButtonMap[String(XM.Model.YES_NO_CANCEL)] = ["notifyYes", "notifyNo", "notifyCancel"];
245
246       this.$.notifyMessage.setContent(inEvent.message);
247       this._notifyCallback = inEvent.callback;
248       this._notifyOptions = inEvent.options;
249
250       // events are of type NOTICE by default
251       inEvent.type = inEvent.type || XM.Model.NOTICE;
252
253       // show the appropriate buttons
254       _.each(this.getNotifyButtons(), function (component) {
255         component.setShowing(_.indexOf(typeToButtonMap[String(inEvent.type)], component.name) >= 0);
256       });
257
258       // allow custom button text
259       this.$.notifyYes.setContent(inEvent.yesLabel || "_yes".loc());
260       this.$.notifyNo.setContent(inEvent.noLabel || "_no".loc());
261       this.$.notifyOk.setContent(inEvent.okLabel || "_ok".loc());
262       this.$.notifyCancel.setContent(inEvent.cancelLabel || "_cancel".loc());
263
264       // highlight the index active button
265       // it's the OK button unless it's a 2- or 3- way question, in which case it's YES
266       this._activeNotify = inEvent.type === XM.Model.QUESTION || inEvent.type === XM.Model.YES_NO_CANCEL ? 1 : 0;
267       _.each(this.getNotifyButtons(), function (button, index) {
268         button.addRemoveClass("selected", index === that._activeNotify);
269       });
270
271       // delete out any previously added customComponents/customComponentControls
272       if (this.$.notifyPopup.$.customComponent) {
273         this.$.notifyPopup.removeComponent(this.$.notifyPopup.$.customComponent);
274
275         customComponentControls = _.filter(that.$.notifyPopup.controls, function (control) {
276           return control.name === "customComponent";
277         });
278
279         if (customComponentControls) {
280           _.each(customComponentControls, function (control) {
281             that.$.notifyPopup.removeControl(control);
282           });
283         }
284       }
285
286       // Add the custom component
287       if (inEvent.component) {
288         inEvent.component.name = "customComponent";
289         inEvent.component.addBefore = this.$.notifyOk;
290         this.$.notifyPopup.createComponent(inEvent.component);
291         // TODO: this inline style needs to go away
292         this.$.notifyPopup.$.customComponent.addStyles("color:black;");
293         if (inEvent.componentModel) {
294           this.$.notifyPopup.$.customComponent.setValue(inEvent.componentModel);
295         }
296       }
297
298       this._notifyDone = false;
299       this.$.notifyPopup.render();
300       this.$.notifyPopup.show();
301       this.$.notifyPopup.applyStyle("opacity", 1); // XXX not sure why this hack is necessary.
302     },
303     notifyHidden: function () {
304       if (!this._notifyDone) {
305         this.$.notifyPopup.show();
306       }
307     },
308     notifyKey: function (keyCode, isShift) {
309       var activeIndex = this._activeNotify,
310         notifyButtons = this.getNotifyButtons(),
311         nextShowing;
312
313       if (keyCode === 13) {
314         //enter
315         this.notifyTap(null, {originator: notifyButtons[activeIndex]});
316
317       } else if (keyCode === 37 || (keyCode === 9 && isShift)) {
318         // left or shift-tab
319         notifyButtons[activeIndex].removeClass("onyx-blue");
320         for (nextShowing = activeIndex - 1; nextShowing >= 0; nextShowing--) {
321           if (nextShowing === 0 && !notifyButtons[nextShowing].showing) {
322             // there are no showing buttons to the left
323             nextShowing = undefined;
324             break;
325           }
326           if (notifyButtons[nextShowing].showing) {
327             break;
328           }
329         }
330         if (nextShowing && nextShowing >= 0) {
331           activeIndex = nextShowing;
332         }
333         this._activeNotify = activeIndex;
334         notifyButtons[activeIndex].addClass("onyx-blue");
335
336       } else if (keyCode === 39 || keyCode === 9) {
337         // right or tab
338         notifyButtons[activeIndex].removeClass("onyx-blue");
339         for (nextShowing = activeIndex + 1; nextShowing < notifyButtons.length; nextShowing++) {
340           if (nextShowing + 1 === notifyButtons.length && !notifyButtons[nextShowing].showing) {
341             // there are no showing buttons to the right
342             nextShowing = undefined;
343             break;
344           }
345           if (notifyButtons[nextShowing].showing) {
346             break;
347           }
348         }
349         if (nextShowing && nextShowing < notifyButtons.length) {
350           activeIndex = nextShowing;
351         }
352         this._activeNotify = activeIndex;
353
354         notifyButtons[activeIndex].addClass("onyx-blue");
355       }
356     },
357     /**
358       The OK button has been clicked from the notification popup. Close the popup and call
359       the callback with the appropriate parameter if the callback exists.
360      */
361     notifyTap: function (inSender, inEvent) {
362       var notifyParameter,
363         callbackObj = {},
364         that = this,
365         optionsObj = this._notifyOptions || {};
366
367       this._notifyDone = true;
368       if (typeof this._notifyCallback === 'function') {
369         switch (inEvent.originator.name) {
370         case 'notifyOk':
371           notifyParameter = undefined;
372           break;
373         case 'notifyYes':
374           notifyParameter = true;
375           break;
376         case 'notifyNo':
377           notifyParameter = false;
378           break;
379         case 'notifyCancel':
380           notifyParameter = null;
381           break;
382         }
383         // the callback might make its own popup, which we do not want to hide.
384         this.$.notifyPopup.hide();
385         callbackObj.answer = notifyParameter;
386         if (this.$.notifyPopup.$.customComponent) {
387           if (this.$.notifyPopup.$.customComponent.getValueAsync) {
388             this.$.notifyPopup.$.customComponent.getValueAsync(function (result) {
389               callbackObj.componentValue = result;
390               that._notifyCallback(callbackObj, optionsObj);
391             });
392             return;
393           }
394           callbackObj.componentValue = this.$.notifyPopup.$.customComponent.getValue();
395         }
396         this._notifyCallback(callbackObj, optionsObj);
397       } else {
398         this.$.notifyPopup.hide();
399       }
400     },
401     /**
402       Go back to the previous panel. Note the implementation
403       here means that we can't just put components anywhere
404       we want in the component array. The navigator has to
405       be the last one, so we can simply go "back" to it coming
406       back out of a workspace.
407      */
408     previous: function () {
409       // Stock implementation is screwy, do our own
410       var last = this.getActive(),
411         previous = this.index - 1,
412         active;
413       this.setIndex(previous);
414
415       // Provide a way to let panels or their children know they have been activated
416       active = this.getActive();
417       active.waterfallDown("onActivatePanel", {activated: active});
418
419       last.destroy();
420     },
421     popupWorkspaceNotify: function (inSender, inEvent) {
422       this.notify(inSender, inEvent);
423     },
424     popupWorkspace: function (inSender, inEvent) {
425       this._popupWorkspaceCallback = inEvent.callback;
426
427       this.$.popupWorkspace.destroyClientControls();
428       this.$.popupWorkspace.createComponent({content: inEvent.message});
429       this.$.popupWorkspace.createComponent({
430         kind: "enyo.Scroller",
431         name: "popupScroller",
432         maxHeight: "400px",
433         horizontal: "hidden"
434       }, {owner: this});
435       this.$.popupWorkspace.createComponent({name: "workspace", kind: inEvent.workspace, container: this.$.popupScroller});
436       this.$.popupWorkspace.$.workspace.addStyles("color:black;");
437       this.$.popupWorkspace.$.workspace.setValue(inEvent.model);
438       this.$.popupWorkspace.createComponent({
439         kind: "onyx.Button",
440         content: "_save".loc(),
441         name: "popupWorkspaceSave",
442         ontap: "popupWorkspaceTap",
443         classes: "onyx-blue xv-popup-button"
444       }, {owner: this});
445       this.$.popupWorkspace.createComponent({
446         kind: "onyx.Button",
447         content: "_cancel".loc(),
448         name: "popupWorkspaceCancel",
449         ontap: "popupWorkspaceTap",
450         classes: "xv-popup-button"
451       }, {owner: this});
452
453       this.$.popupWorkspace.render();
454       this.$.popupWorkspace.show();
455       this.$.popupWorkspace.applyStyle("opacity", 1); // XXX not sure why this hack is necessary.
456     },
457     popupWorkspaceTap: function (inSender, inEvent) {
458       var model = this.$.popupWorkspace.$.workspace.value,
459         response = inEvent.originator.name === 'popupWorkspaceCancel' ? false : model,
460         validationError,
461         errorMessage;
462
463       if (response) {
464         validationError = model.validate(model.attributes);
465         if (validationError) {
466           errorMessage = validationError.message ? validationError.message() : "Error";
467           this.notify(null, {message: validationError.message(), type: XM.Model.CRITICAL});
468           return;
469         }
470       }
471       this.$.popupWorkspace.hide();
472       this._popupWorkspaceCallback(response);
473     },
474     _setModules: function () {
475       var modules = this.getModules();
476       this.$.navigator.setModules(modules);
477     }
478
479   });
480
481 }());