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