update copyright to 2014
[xtuple] / lib / enyo-x / source / views / navigator.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*/
4 /*global XT:true, XV:true, XM:true, _:true, enyo:true, window:true */
5
6 (function () {
7   var MODULE_MENU = 0;
8   var PANEL_MENU = 1;
9
10   /**
11     @name XV.Navigator
12     @class Contains a set of panels for navigating the app and modules
13     within the app.<br/>
14     Navigation within the app is accomplished by elements within the menu
15     tool bar, such as history, search, the back button or logout.<br/>
16     Navigation within modules in the app is accomplished with a list within
17     the panel menu which displays the menu items for each context.<br/>
18     The root menu (module menu) contains the list of modules and the logout.<br/>
19     Only three menus are cached at one time.<br />
20     Layout: Collapsing Arranger.<br />
21     Use to implement the high-level container of all business object lists.<br/>
22     Derived from <a href="http://enyojs.com/api/#enyo.Panels">enyo.Panels</a>.
23     @extends enyo.Panels
24     @extends XV.ListMenuManager
25    */
26   var navigator = /** @lends XV.Navigator# */{
27     name: "XV.Navigator",
28     kind: "XV.GridPanels",
29     /**
30       Published fields
31       @type {Object}
32
33       @property {Array} modules A DOM-free representation of all of the modules
34          contained in the navigator. The details of these module objects will
35          inform the creation of the panel components.
36
37       @property {Object} panelCache A hashmap of cached panels where the key is
38          the global ID of the panel and the value is the enyo panel component.
39     */
40     published: {
41       modules: [],
42       panelCache: {},
43       actions: [
44         {name: "newTabItem", label: "_openNewTab".loc(), method: "newTab", alwaysShowing: true},
45         {name: "preferencesItem", label: "_preferences".loc(), method: "openPreferencesWorkspace" },
46         {name: "myAccountItem", label: "_changePassword".loc(), method: "showMyAccount"},
47         {name: "helpItem", label: "_help".loc(), method: "showHelp", alwaysShowing: true},
48         {name: "aboutItem", label: "_about".loc(), method: "showAbout"}
49       ],
50     },
51     events: {
52       onListAdded: "",
53       onNavigatorEvent: "",
54       onNotify: "",
55       onWorkspace: ""
56     },
57     handlers: {
58       onDeleteTap: "showDeletePopup",
59       onListItemMenuTap: "showListItemMenu",
60       onMessage: "setMessageContent",
61       onParameterChange: "requery",
62       onColumnsChange: "changeLayout",
63       onItemTap: "itemTap",
64       onExportList: "exportList",
65       onHotKey: "handleHotKey",
66       onPrintList: "printList",
67       onPrintSelectList: "printSelectList",
68       onReportList: "reportList",
69     },
70     showPullout: true,
71     components: [
72       {kind: "FittableRows", name: "nav-menu", classes: "left", components: [
73         {kind: "onyx.Toolbar", classes: "onyx-menu-toolbar", components: [
74           {kind: "onyx.Button", name: "backButton", content: "_logout".loc(),
75               ontap: "backTapped"},
76           {kind: "Group", name: "iconButtonGroup", tag: null, components: [
77             {kind: "XV.IconButton", name: "historyIconButton",
78                src: "/assets/menu-icon-bookmark.png",
79                ontap: "showHistory", content: "_history".loc()},
80             {kind: "XV.IconButton", name: "searchIconButton",
81                src: "/assets/menu-icon-search.png",
82                ontap: "showParameters", content: "_advancedSearch".loc(), showing: false}
83           ]},
84           {kind: "onyx.MenuDecorator", style: "margin: 0;", onSelect: "actionSelected", components: [
85             {kind: "XV.IconButton", src: "/assets/menu-icon-gear.png",
86                                                         content: "_actions".loc(), name: "actionButton",
87               classes: "xv-action-icon"},
88             {kind: "onyx.Menu", name: "actionMenu"}
89           ]},
90           {kind: "onyx.Popup", name: "aboutPopup", centered: true,
91             modal: true, floating: true, scrim: true, components: [
92             {content: "Copyright xTuple 2014" },
93             {name: "aboutVersion", allowHtml: true },
94             {kind: "onyx.Button", content: "_ok".loc(), ontap: "closeAboutPopup"}
95           ]}
96         ]},
97         {name: "loginInfo", content: "", classes: "xv-navigator-header"},
98         {name: "menuPanels", kind: "Panels", draggable: false, fit: true,
99           margin: 0, components: [
100           {name: "moduleMenu", kind: "List", touch: true,
101               onSetupItem: "setupModuleMenuItem", ontap: "menuTap",
102               components: [
103             {name: "moduleItem", classes: "item enyo-border-box"}
104           ]},
105           {name: "panelMenu", kind: "List", touch: true,
106              onSetupItem: "setupPanelMenuItem", ontap: "panelTap", components: [
107             {name: "listItem", classes: "item enyo-border-box"}
108           ]},
109           {} // Why do panels only work when there are 3+ objects?
110         ]}
111       ]},
112       {kind: "FittableRows", name: "list-view", components: [
113                                 // the onyx-menu-toolbar class keeps the popups from being hidden
114         {kind: "onyx.MoreToolbar", name: "contentToolbar",
115           classes: "onyx-menu-toolbar", movedClass: "xv-toolbar-moved", components: [
116           {name: "rightLabel", classes: "xv-toolbar-label"},
117           // The MoreToolbar is a FittableColumnsLayout, so this spacer takes up all available space
118           {name: "spacer", content: "", fit: true},
119           // Selectable "New" menu which is hidden by default
120           {kind: "onyx.MenuDecorator", style: "margin: 0;", onSelect: "newRecord", components: [
121             {kind: "XV.IconButton", src: "/assets/menu-icon-new.png",
122               content: "_new".loc(), name: "newMenuButton", showing: false},
123             {kind: "onyx.Menu", name: "newMenu"}
124           ]},
125           // Button to initiate new workspace
126           {kind: "XV.IconButton", src: "/assets/menu-icon-new.png",
127             content: "_new".loc(), name: "newButton", ontap: "newRecord", showing: false},
128           {kind: "onyx.MenuDecorator", style: "margin: 0;", onSelect: "exportSelected", components: [
129             {kind: "XV.IconButton", src: "/assets/menu-icon-export.png",
130               content: "_export".loc(), name: "exportButton"},
131             {kind: "onyx.Menu", name: "exportMenu", showing: false}
132           ]},
133           {kind: "XV.IconButton", name: "sortButton", content: "_sort".loc(),
134             src: "/assets/sort-icon.png", ontap: "showSortPopup", showing: false,
135             classes: "xv-sort-icon"},
136           {name: "refreshButton", kind: "XV.IconButton",
137             src: "/assets/menu-icon-refresh.png", content: "_refresh".loc(),
138             ontap: "requery", showing: false},
139           {name: "search", kind: "onyx.InputDecorator",
140             showing: false, components: [
141             {name: 'searchInput', kind: "onyx.Input", style: "width: 200px;",
142               placeholder: "_search".loc(), onchange: "inputChanged"},
143             {kind: "Image", src: "/assets/search-input-search.png",
144               name: "searchJump", ontap: "jump"},
145             {kind: "XV.SortPopup", name: "sortPopup", showing: false}
146           ]}
147         ]},
148         {name: "messageHeader", content: "", classes: ""},
149         {name: "header", content: "", classes: ""},
150         {name: "contentPanels", kind: "Panels", margin: 0, fit: true,
151           draggable: false, panelCount: 0, classes: "scroll-ios"},
152         {name: "myAccountPopup", kind: "XV.MyAccountPopup"},
153         {name: "listItemMenu", kind: "onyx.Menu", floating: true, onSelect: "listActionSelected",
154           maxHeight: 500, components: []
155         }
156       ]}
157     ],
158     /**
159       Keeps track of whether any list has already been fetched, to avoid unnecessary
160       refetching.
161      */
162     fetched: {},
163     actionSelected: function (inSender, inEvent) {
164       var index = this.$.menuPanels.getIndex(),
165         context = this,
166         action = inEvent.originator.action,
167         method = action.method || action.name;
168
169       // Determine if action is coming from a more specific context
170       if (action.context) {
171         context = this.$.contentPanels.getActive();
172       } else if (index && !action.alwaysShowing) {
173         context = this.getSelectedModule();
174       }
175       context[method](inSender, inEvent);
176     },
177     activate: function () {
178       this.setMenuPanel(MODULE_MENU);
179     },
180     /**
181       The back button is a logout button if you're at the root menu. Otherwise it's a
182       back button that takes you to the root menu.
183      */
184     backTapped: function () {
185       var index = this.$.menuPanels.getIndex();
186       if (index === MODULE_MENU) {
187         var inEvent = {
188           originator: this,
189           type: XM.Model.QUESTION,
190           callback: function (response) {
191             if (response.answer) {
192               XT.logout();
193             }
194           },
195           message: "_logoutConfirmation".loc()
196         };
197         this.doNotify(inEvent);
198       } else {
199         this.setHeaderContent("");
200         this.setMenuPanel(MODULE_MENU);
201       }
202     },
203     /**
204       If we're on the main menu, then use the navigator actions defined
205       in its `actions` property. If we're in a module, then use the module's
206       actions.
207     */
208     buildMenus: function () {
209       var actionMenu = this.$.actionMenu,
210         exportMenu = this.$.exportMenu,
211         newMenu = this.$.newMenu,
212         // This is the index of the active panel. All panels
213         // that have been selected from the menus have an index of
214         // greater than 0. The welcome page has an index of 0.
215         idx = this.$.menuPanels.getIndex(),
216         activePanel = this.$.contentPanels.getActive(),
217         ary = idx ? (this.getSelectedModule().actions || []).concat(this.getActions(true)) : this.getActions(),
218         actions = ary.slice(0),
219         privileges = XT.session.privileges;
220
221       // HANDLE ACTIONS
222       // reset the menu
223       actionMenu.destroyClientControls();
224
225       if (idx && activePanel.getNavigatorActions) {
226         _.each(activePanel.getNavigatorActions(), function (action) {
227           action.context = activePanel;
228           actions.push(action);
229         });
230       }
231
232       // then add whatever actions are applicable to the current context
233       _.each(actions, function (action) {
234         var name = action.name,
235           privilege = action.privilege,
236           isDisabled = privilege ? privileges.get(privilege) : false;
237         actionMenu.createComponent({
238           name: name,
239           kind: XV.MenuItem,
240           content: action.label || ("_" + name).loc(),
241           action: action,
242           disabled: isDisabled
243         });
244
245       });
246       actionMenu.render();
247       this.$.actionButton.setShowing(actions.length);
248
249       // HANDLE EXPORTS
250       actions = [];
251
252       // reset the menu
253       exportMenu.destroyClientControls();
254
255       if (idx && activePanel.getExportActions) {
256         _.each(activePanel.getExportActions(), function (action) {
257           action.context = activePanel;
258           actions.push(action);
259         });
260       }
261
262       // then add whatever actions are applicable to the current context
263       _.each(actions, function (action) {
264         var name = action.name,
265           privilege = action.privilege,
266           isDisabled = privilege ? privileges.get(privilege) : false;
267         exportMenu.createComponent({
268           name: name,
269           kind: XV.MenuItem,
270           content: action.label || ("_" + name).loc(),
271           action: action,
272           disabled: isDisabled
273         });
274       });
275       exportMenu.render();
276       this.$.exportButton.setShowing(actions.length);
277
278       // HANDLE SORT BUTTON
279       // if the activepanel is a list of some kind, show the button
280       if (activePanel.kindClasses) {
281         this.$.sortButton.setShowing(activePanel.kindClasses.indexOf("list") !== -1);
282       } else {
283         this.$.sortButton.setShowing(false);
284       }
285
286       // HANDLE NEW
287       actions = [];
288
289       // reset the menu
290       newMenu.destroyClientControls();
291
292       // XXX not sure about possible falsy condition of idx here
293       if ((idx || idx === 0) && activePanel.getNewActions) {
294         _.each(activePanel.getNewActions(), function (action) {
295           action.context = activePanel;
296           actions.push(action);
297         });
298       }
299
300       // then add whatever actions are applicable to the current context
301       _.each(actions, function (action) {
302         newMenu.createComponent({
303           name: action.name,
304           kind: XV.MenuItem,
305           content: action.label || ("_" + action.name).loc(),
306           // this item is the payload from the menu item
307           // that can be handled differently depending on the list.
308           item: action.item,
309           defaults: action.defaults,
310           allowNew: action.allowNew
311         });
312       });
313       newMenu.render();
314     },
315     /**
316       If there is a parameter widget, send the current list to the
317         layout form to build the list of columns.
318     */
319     buildLayout: function () {
320       var list = this.$.contentPanels.getActive(),
321         parameterWidget = XT.app ? XT.app.$.pullout.getParameterWidget(list.name) : null;
322       if (parameterWidget && parameterWidget.showLayout) {
323         parameterWidget.buildColumnList(list);
324       }
325     },
326     /**
327       The navigator only keeps three panels in the DOM at a time. Anything extra panels
328       will be periodically cached into the panelCache published field and removed from the DOM.
329     */
330     cachePanels: function () {
331       var contentPanels = this.$.contentPanels,
332         panelToCache,
333         globalIndex,
334         pertinentModule,
335         panelReference,
336         findPanel = function (panel) {
337           return panel.index === globalIndex;
338         },
339         findModule = function (module) {
340           var panel = _.find(module.panels, findPanel);
341           return panel !== undefined;
342         };
343
344       while (contentPanels.children.length > 3) {
345         panelToCache = contentPanels.children[0];
346         globalIndex = panelToCache.index;
347
348         // Panels are abstractly referenced in this.getModules().
349         // Find the abstract panel of the panelToCache
350         // XXX this would be cleaner if we kept a backwards reference
351         // from the panel to its containing module (and index therein)
352         pertinentModule = _.find(this.getModules(), findModule);
353         panelReference = _.find(pertinentModule.panels, findPanel);
354
355         contentPanels.removeChild(panelToCache);
356         // only render the most recent (i.e. active) child
357         contentPanels.children[2].render();
358         panelReference.status = "cached";
359         this.getPanelCache()[globalIndex] = panelToCache;
360       }
361     },
362     /**
363       When a new column value is selected in the layout panel,
364       this value replaces the old attribute value in the list
365       field.
366     */
367     changeLayout: function (inSender, inEvent) {
368       var newValue = inEvent.value ? inEvent.value : "",
369         order = inEvent.order,
370         list = this.$.contentPanels.getActive(),
371         // get the current list of attribute kinds
372         currentColumns = _.filter(list.$, function (item) {
373           return item.kind === "XV.ListAttr";
374         });
375
376       // When we get to the current selected list attribute,
377       // set the new value in place of the old attribute
378       for (var i = 0; i < currentColumns.length; i++) {
379         var col = currentColumns[i];
380         if (order === (i + 1)) {
381           col.setAttr(newValue);
382         }
383       }
384
385       // requery this list with the new attribute values
386       this.requery();
387
388       // rebuild the tree of columns
389       this.buildLayout();
390
391       return true; // stop right here
392     },
393     clearMessage: function () {
394       this.$.messageHeader.setContent("");
395       this.$.messageHeader.setClasses("");
396     },
397     closeAboutPopup: function () {
398       this.$.aboutPopup.hide();
399     },
400     create: function () {
401       this.inherited(arguments);
402       var that = this,
403         callback = function () {
404           that.buildMenus();
405         };
406
407       // If not everything is loaded yet, come back to it later
408       if (!XT.session || !XT.session.privileges) {
409         XT.getStartupManager().registerCallback(callback);
410       } else {
411         callback();
412       }
413     },
414     exportSelected: function (inSender, inEvent) {
415       var index = this.$.menuPanels.getIndex(),
416         context = this,
417         action = inEvent.originator.action,
418         method = action.method || action.name;
419
420       action.context[method](inSender, inEvent);
421     },
422     getActions: function (alwaysShowingOnly) {
423       var actions = this.actions;
424       if (alwaysShowingOnly) {
425         actions = _.filter(actions, function (action) {
426           return action.alwaysShowing === true;
427         });
428       }
429       return actions;
430     },
431     getSelectedModule: function () {
432       return this._selectedModule;
433     },
434     /**
435       Exports the contents of a list to CSV. Note that it will export the entire
436       list, not just the part that's been lazy-loaded. Of course, it will apply
437       the filter criteria as selected. Goes to the server for this.
438       Avoids websockets or AJAX because the server will prompt the browser to download
439       the file by setting the Content-Type of the response, which is not possible with
440       those technologies.
441      */
442     exportList: function (inSender, inEvent) {
443       this.openExportTab('export');
444       return true;
445     },
446     newTab: function () {
447       window.open(XT.getOrganizationPath() + '/app', '_newtab');
448     },
449     openPreferencesWorkspace: function () {
450       this.doWorkspace({workspace: "XV.UserPreferenceWorkspace", id: false});
451     },
452     reportList: function (inSender, inEvent) {
453       this.openExportTab('report');
454       return true;
455     },
456     printList: function (inSender, inEvent) {
457       var list = this.$.contentPanels.getActive(),
458         recordType = list.value.model.prototype.recordType,
459         query = JSON.parse(JSON.stringify(list.getQuery())); // clone
460   
461       XT.DataSource.callRoute(
462         'report?details={"nameSpace":"%@","type":"%@","query":%@,"culture":%@,"print":%@}'
463         .f(recordType.prefix(), recordType.suffix(), JSON.stringify(query), JSON.stringify(XT.locale.culture), "true"),
464         {},
465         {success: function (result) {
466           //
467           // Here's where we could do a popup for print status
468           //
469           if (XT.session.config.debugging) {
470             XT.log("Print route success, type: ", recordType.suffix());
471           }
472         },
473         error: function (result) {
474           if (XT.session.config.debugging) {
475             XT.log("Print route error, type: ", recordType.suffix());
476           }
477         }
478         }
479       );
480       return true;
481     },
482     
483     
484     printSelectList: function (inSender, inEvent) {
485       var list = this.$.contentPanels.getActive(),
486         selected = list.getSelection().getSelected();
487       
488       _.each(selected, function (value, index) {
489         var model = list.getModel(index),
490           modelName = model.editableModel || model.recordType,
491           reportName = modelName.suffix(),
492           details = {
493             nameSpace: modelName.prefix(),
494             type: modelName.suffix(),
495             id: model.id,
496             name: reportName,
497             culture: XT.locale.culture,
498             print: true
499           };
500         
501         XT.DataSource.callRoute(
502           "report?details=%@".f(JSON.stringify(details)),
503           {},
504           {success: function (result) {
505             //
506             // Here's where we could do a popup for print status
507             //
508             if (XT.session.config.debugging) {
509               XT.log("Print route success, type: ", modelName.suffix());
510             }
511           },
512           error: function (result) {
513             if (XT.session.config.debugging) {
514               XT.log("Print route error, type: ", modelName.suffix());
515             }
516           }
517         }
518         );
519       });
520       return true;
521     },
522     
523     
524     
525     /**
526      * If a reportSelectList is needed, here it is.  This will open a browser
527      * tab for the first report and new windows for the other reports.
528      *
529     reportSelectList: function (inSender, inEvent) {
530       var list = this.$.contentPanels.getActive(),
531         selected = list.getSelection().getSelected();
532       
533       _.each(selected, function (value, index) {
534         var model = list.getModel(index),
535           modelName = model.editableModel || model.recordType,
536           reportName = modelName.suffix(),
537           details = {
538             nameSpace: modelName.prefix(),
539             type: modelName.suffix(),
540             id: model.id,
541             name: reportName,
542             culture: XT.locale.culture
543           };
544         // sending the locale information back over the wire saves a call to the db
545         console.log("printing id: " + model.id);
546         window.open(XT.getOrganizationPath() +
547            "/report?details=%@".f(JSON.stringify(details)), "Print: " + model.id);
548       });
549        
550       return true;
551     },
552     */
553     openExportTab: function (routeName) {
554       var list = this.$.contentPanels.getActive(),
555         recordType = list.value.model.prototype.recordType,
556         query = JSON.parse(JSON.stringify(list.getQuery())); // clone
557
558       delete query.rowLimit;
559       delete query.rowOffset;
560
561       // sending the locale information back over the wire saves a call to the db
562       window.open(XT.getOrganizationPath() +
563         '/%@?details={"nameSpace":"%@","type":"%@","query":%@,"culture":%@,"print":%@}'
564         .f(routeName,
565           recordType.prefix(),
566           recordType.suffix(),
567           JSON.stringify(query),
568           JSON.stringify(XT.locale.culture),
569           "false"),
570         '_newtab');
571     },
572     showSortPopup: function (inSender, inEvent) {
573       this.$.sortPopup.setList(this.$.contentPanels.getActive());
574       this.$.sortPopup.setNav(this);
575       this.$.sortPopup.setPickerStrings();
576       this.$.sortPopup.show();
577     },
578
579     /**
580       Fetch a list.
581      */
582     fetch: function (options) {
583       options = options ? _.clone(options) : {};
584       var list = this.$.contentPanels.getActive(),
585         name = list ? list.name : "",
586         query,
587         input,
588         parameterWidget,
589         parameters,
590         filterDescription;
591
592       // in order to continue to the fetch, this needs to be a list
593       // or a dashboard
594
595       if (!(list instanceof XV.List) && !(list instanceof XV.Dashboard) && !(list instanceof XV.Listboard)) { return; }
596
597       query = list.getQuery() || {};
598       input = this.$.searchInput.getValue();
599
600       // if the "list" doesn't allow dynamic searching, skip this
601       // Dashboards have an allowFilter of false
602       if (list.allowFilter) {
603         parameterWidget = XT.app ? XT.app.$.pullout.getParameterWidget(name) : null;
604         parameters = parameterWidget ? parameterWidget.getParameters() : [];
605         options.showMore = _.isBoolean(options.showMore) ?
606           options.showMore : false;
607
608         // Get information from filters and set description
609         filterDescription = this.formatQuery(parameterWidget ? parameterWidget.getSelectedValues() : null, input);
610         list.setFilterDescription(filterDescription);
611         this.setHeaderContent(filterDescription);
612
613         delete query.parameters;
614         // Build parameters
615         if (input || parameters.length) {
616           query.parameters = [];
617
618           // Input search parameters
619           if (input) {
620             query.parameters.push({
621               attribute: list.getSearchableAttributes(),
622               operator: 'MATCHES',
623               value: this.$.searchInput.getValue()
624             });
625           }
626
627           // Advanced parameters
628           if (parameters) {
629             query.parameters = query.parameters.concat(parameters);
630           }
631         }
632
633         // if there is a parameter widget for this list, build the columns
634         if (parameterWidget && parameterWidget.showLayout) {
635           this.buildLayout();
636         }
637       }
638
639       list.setQuery(query);
640       list.fetch(options);
641     },
642     formatQuery: function (advancedSearch, simpleSearch) {
643       var key,
644         formattedQuery = "";
645
646       for (key in advancedSearch) {
647         formattedQuery += (key + ": " + advancedSearch[key] + ", ");
648       }
649
650       if (simpleSearch && formattedQuery) {
651         formattedQuery += "_match".loc() + ": " + simpleSearch;
652       } else if (simpleSearch) {
653         formattedQuery += simpleSearch;
654       }
655
656       if (formattedQuery) {
657         formattedQuery = "_filterBy".loc() + ": " + formattedQuery;
658       }
659
660       if (formattedQuery.lastIndexOf(", ") + 2 === formattedQuery.length) {
661         // chop off trailing comma
662         formattedQuery = formattedQuery.substring(0, formattedQuery.length - 2);
663       }
664       return formattedQuery;
665     },
666     handleHotKey: function (inSender, inEvent) {
667       var destinationIndex,
668         isWelcome = this.getSelectedModule().name === 'welcome',
669         currentIndex,
670         keyCode = inEvent.keyCode;
671
672       // numbers navigate to the nth menu option
673       if (keyCode >= 49 && keyCode <= 57) {
674         destinationIndex = keyCode - 49;
675         if (isWelcome) {
676           this.setModule(Math.min(destinationIndex, this.getModules().length - 1));
677         } else {
678           this.setContentPanel(Math.min(destinationIndex, this.getSelectedModule().panels.length - 1));
679         }
680         return;
681
682       } else if (!isWelcome) {
683         currentIndex = this.$.panelMenu.getSelection().lastSelected;
684         if (keyCode === 38) {
685           this.setContentPanel(Math.max(currentIndex - 1, 0));
686           return;
687         } else if (keyCode === 40) {
688           this.setContentPanel(Math.min(currentIndex + 1, this.getSelectedModule().panels.length - 1));
689           return;
690         }
691       }
692
693       switch(String.fromCharCode(keyCode)) {
694       case 'A':
695         this.showParameters();
696         break;
697       case 'B':
698         this.backTapped();
699         break;
700       case 'H':
701         this.showHelp();
702         break;
703       case 'N':
704         this.newRecord({}, {originator: {}});
705         break;
706       case 'R':
707         this.requery();
708         break;
709       }
710     },
711     inputChanged: function (inSender, inEvent) {
712       this.fetched = {};
713       this.fetch();
714     },
715     /**
716       Drills down into a workspace if a user clicks a list item.
717      */
718     itemTap: function (inSender, inEvent) {
719       var workspace = inEvent.originator.getWorkspace(),
720         model = inEvent.model,
721         canNotRead = model.couldRead ? !model.couldRead() : !model.getClass().canRead(),
722         id = model && model.id ? model.id : false;
723
724       // Check privileges first
725       if (canNotRead) {
726         this.showError("_insufficientViewPrivileges".loc());
727         return true;
728       }
729
730       // Bubble requset for workspace view, including the model id payload
731       if (workspace) {
732         this.doWorkspace({workspace: workspace, id: id});
733       }
734       return true;
735     },
736     jump: function () {
737       var list = this.$.contentPanels.getActive(),
738          workspace = list ? list.getWorkspace() : null,
739          Klass = list.getValue().model,
740          upper = this._getModelProperty(Klass, 'enforceUpperKey'),
741          input = this.$.searchInput.getValue(),
742          that = this,
743          options = {},
744          key = this._getModelProperty(Klass, 'documentKey'),
745          model,
746          attrs = {};
747       if (this._busy  || !input || !key) { return; }
748       this._busy = true;
749
750       // First find a matching id
751       options.success = function (id) {
752         var options = {};
753         if (id) {
754
755           // Next fetch the model, see if we have privs
756           options.success = function () {
757             var canNotRead = model.couldRead ?
758               !model.couldRead() : !model.getClass().canRead();
759
760             // Check privileges first
761             if (canNotRead) {
762               this.showError("_insufficientViewPrivileges".loc());
763             } else {
764
765               // Bubble requset for workspace view, including the model id payload
766               if (workspace) { that.doWorkspace({workspace: workspace, id: id}); }
767             }
768             that._busy = false;
769           };
770           attrs[Klass.prototype.idAttribute] = id;
771           model = Klass.findOrCreate(attrs);
772           model.fetch(options);
773         } else {
774           that.showError("_noDocumentFound".loc());
775           that.$.searchInput.clear();
776           that._busy = false;
777         }
778       };
779       input = upper ? input.toUpperCase() : input;
780       Klass.findExisting(key, input, options);
781     },
782     loginInfo: function () {
783       return this.$.loginInfo;
784     },
785     /**
786       Handles additive changes only
787     */
788     modulesChanged: function () {
789       var modules = this.getModules() || [],
790         existingModules = this._modules || [],
791         existingModule,
792         existingPanel,
793         panels,
794         panel,
795         i,
796         n,
797         findExistingModule = function (name) {
798           return _.find(existingModules, function (module) {
799             return module.name === name;
800           });
801         },
802         findExistingPanel = function (panels, name) {
803           return _.find(panels, function (panel) {
804             return panel.name === name;
805           });
806         };
807
808       // Build panels
809       for (i = 0; i < modules.length; i++) {
810         panels = modules[i].panels || [];
811         existingModule = findExistingModule(modules[i].name);
812         for (n = 0; n < panels.length; n++) {
813
814           // If the panel already exists, move on
815           if (existingModule) {
816             existingPanel = findExistingPanel(existingModule.panels, panels[n].name);
817             if (existingPanel) { continue; }
818           }
819
820           // Keep track of where this panel is being placed for later reference
821           panels[n].index = this.$.contentPanels.panelCount++;
822
823           // XXX try this: only create the first three
824           if (panels[n].index < 3) {
825             panels[n].status = "active";
826
827             // Default behavior for Lists is toggle selections
828             // So we can perform actions on rows. If not a List
829             // this property shouldn't hurt anything
830             if (panels[n].toggleSelected === undefined) {
831               panels[n].toggleSelected = true;
832             }
833             panel = this.$.contentPanels.createComponent(panels[n]);
834             if (panel instanceof XV.List) {
835
836               // Bubble parameter widget up to pullout
837               this.doListAdded(panel);
838             }
839           } else {
840             panels[n].status = "unborn";
841           }
842         }
843       }
844       this.$.moduleMenu.setCount(modules.length);
845       // Cache a deep copy
846       this._modules = JSON.parse(JSON.stringify(modules));
847       this.render();
848     },
849     /**
850       Fired when the user clicks the "New" button. Takes the user to a workspace
851       backed by an empty object of the type displayed in the current list.
852      */
853     newRecord: function (inSender, inEvent) {
854       var list = this.$.contentPanels.getActive(),
855         workspace = list instanceof XV.List ? list.getWorkspace() : null,
856         item = inEvent.originator.item,
857         defaults = inEvent.originator.defaults,
858         allowNew = inEvent.originator.allowNew,
859         Model,
860         canCreate,
861         callback;
862
863       // This list is actually a dashboard so the item
864       // will be a type of chart added to the dashboard
865       if (list instanceof XV.Dashboard && item) {
866         list.newRecord(item);
867         return true;
868       }
869
870       if (list instanceof XV.Listboard && item) {
871         list.newRecord(item);
872         return true;
873       }
874
875       if (!list instanceof XV.List) {
876         return true;
877       }
878
879       // Check privileges
880       Model = list.getValue().model;
881       canCreate = Model.couldCreate ? Model.couldCreate() : Model.canCreate();
882       if (!canCreate) {
883         this.showError("_insufficientCreatePrivileges".loc());
884         return true;
885       }
886
887       if (workspace) {
888         var params = {};
889         this.doWorkspace({
890           workspace: workspace,
891           attributes: defaults,
892           allowNew: allowNew === false ? false : true
893         });
894       }
895
896       // In addition to preventing Enyo event propagation,
897       // we need to prevent propagation of DOM events to support
898       // mobile browsers and long button clicks
899       // check to make sure this is a button click before calling inEvent function
900       if (inEvent && inEvent.preventDefault) {
901         inEvent.preventDefault();
902       }
903       return true;
904     },
905     popupHidden: function (inSender, inEvent) {
906       if (!this._popupDone) {
907         inEvent.originator.show();
908       }
909     },
910     requery: function (inSender, inEvent) {
911       this.fetch();
912     },
913     /**
914       Determines whether the advanced search or the history icon (or neither) is
915       lit.
916      */
917     setActiveIconButton: function (buttonName) {
918       var activeIconButton = null;
919       // Null deactivates both
920       if (buttonName === 'search') {
921         activeIconButton = this.$.searchIconButton;
922       } else if (buttonName === 'history') {
923         activeIconButton = this.$.historyIconButton;
924       }
925       this.$.iconButtonGroup.setActive(activeIconButton);
926     },
927     /**
928       Renders a list and performs all the necessary auxilliary work such as hiding/showing
929       the advanced search icon if appropriate. Called when a user chooses a menu item.
930      */
931     setContentPanel: function (index) {
932       var contentPanels = this.$.contentPanels,
933         module = this.getSelectedModule(),
934         panelIndex = module && module.panels ? module.panels[index].index : -1,
935         panelStatus = module && module.panels ? module.panels[index].status : 'unknown',
936         panel,
937         label,
938         collection,
939         model,
940         canNotCreate = true;
941
942       this.clearMessage();
943       if (panelStatus === 'active') {
944         panel = _.find(contentPanels.children, function (child) {
945           return child.index === panelIndex;
946         });
947       } else if (panelStatus === 'unborn') {
948         // panel exists but has not been rendered. Render it.
949         module.panels[index].status = 'active';
950
951         // Default behavior for Lists is toggle selections
952         // So we can perform actions on rows. If not a List
953         // this property shouldn't hurt anything
954         if (module.panels[index].toggleSelected === undefined) {
955           module.panels[index].toggleSelected = true;
956         }
957         panel = contentPanels.createComponent(module.panels[index]);
958         panel.render();
959         if (panel instanceof XV.List) {
960
961           // Bubble parameter widget up to pullout
962           this.doListAdded(panel);
963         }
964
965       } else if (panelStatus === 'cached') {
966         module.panels[index].status = 'active';
967         panel = this.panelCache[panelIndex];
968         contentPanels.addChild(panel);
969         panel.node = undefined; // new to enyo2.2! wipe out the node so that it can get re-rendered fresh
970         panel.render();
971
972       } else {
973         XT.error("Don't know what to do with this panel status");
974       }
975
976       // If we're already here, bail
977       if (contentPanels.index === this.$.contentPanels.indexOfChild(panel)) {
978         return;
979       }
980
981       // cache any extraneous content panels
982       this.cachePanels();
983
984       label = panel && panel.label ? panel.label : "";
985       collection = panel && panel.getCollection ? XT.getObjectByName(panel.getCollection()) : false;
986
987       if (!panel) { return; }
988
989       // Make sure the advanced search icon is visible iff there is an advanced
990       // search for this list
991       if (panel.parameterWidget) {
992         this.$.searchIconButton.setShowing(true);
993       } else {
994         this.$.searchIconButton.setShowing(false);
995       }
996       this.doNavigatorEvent({name: panel.name, show: false});
997
998       // Handle new button
999       this.$.newButton.setShowing(panel.canAddNew && !panel.newActions);
1000       this.$.newMenuButton.setShowing(panel.canAddNew && panel.newActions);
1001
1002       if (panel.canAddNew && collection) {
1003         // Check 'couldCreate' first in case it's an info model.
1004         model = collection.prototype.model;
1005         canNotCreate = model.prototype.couldCreate ? !model.prototype.couldCreate() : !model.canCreate();
1006       }
1007       this.$.newButton.setDisabled(canNotCreate);
1008
1009       // Select panelMenu
1010       if (!this.$.panelMenu.isSelected(index)) {
1011         this.$.panelMenu.select(index);
1012       }
1013
1014       // Select list
1015       contentPanels.setIndex(this.$.contentPanels.indexOfChild(panel));
1016
1017       this.$.rightLabel.setContent(label);
1018       if (panel.getFilterDescription) {
1019         this.setHeaderContent(panel.getFilterDescription());
1020       }
1021       if (panel.fetch && !this.fetched[panelIndex]) {
1022         this.fetch();
1023         this.fetched[panelIndex] = true;
1024       }
1025
1026       this.buildMenus();
1027       this.$.contentToolbar.resized();
1028     },
1029
1030     /**
1031       The header content typically describes to the user the particular query filter in effect.
1032      */
1033     setHeaderContent: function (content) {
1034       this.$.header.setContent(content);
1035       if (content !== "") {
1036         this.$.header.setClasses("xv-navigator-header");
1037       } else {
1038         this.$.header.setClasses("");
1039       }
1040     },
1041     setMenuPanel: function (index) {
1042       var label = index ? "_back".loc() : "_logout".loc();
1043       this.$.menuPanels.setIndex(index);
1044       this.$.menuPanels.getActive().select(0);
1045       this.setContentPanel(0);
1046       // this causes the gear menu to reload when the module
1047       // is chosen, rather than just when a content panel is selected
1048       this.buildMenus();
1049
1050       this.$.backButton.setContent(label);
1051       this.$.refreshButton.setShowing(index);
1052       this.$.search.setShowing(index);
1053       this.$.contentToolbar.resized();
1054     },
1055     setMessageContent: function (inSender, inEvent) {
1056       var content = inEvent.message;
1057
1058       this.$.messageHeader.setContent(content);
1059       if (content !== "") {
1060         this.$.messageHeader.setClasses("xv-navigator-header");
1061       } else {
1062         this.$.messageHeader.setClasses("");
1063       }
1064     },
1065     setModule: function (index) {
1066       var module = this.getModules()[index],
1067         panels = module.panels || [],
1068         hasSubmenu = module.hasSubmenu !== false && panels.length;
1069       if (module !== this._selectedModule || enyo.Panels.isScreenNarrow()) {
1070         this._selectedModule = module;
1071         if (hasSubmenu) {
1072           this.$.panelMenu.setCount(panels.length);
1073           this.$.panelMenu.render();
1074           this.setMenuPanel(PANEL_MENU);
1075         } else {
1076           // if no submenus, treat lke a panel
1077           this.setContentPanel(0);
1078         }
1079       }
1080     },
1081     setModules: function (modules) {
1082       this.modules = modules;
1083       this.modulesChanged();
1084     },
1085     /**
1086       Renders a list of modules from the root menu.
1087      */
1088     setupModuleMenuItem: function (inSender, inEvent) {
1089       var index = inEvent.index,
1090         label = this.modules[index].label,
1091         isSelected = inSender.isSelected(index);
1092       this.$.moduleItem.setContent(label);
1093       this.$.moduleItem.addRemoveClass("onyx-selected", isSelected);
1094       if (isSelected) { this.setModule(index); }
1095     },
1096     /**
1097       Renders the leftbar list of objects within a given module. This function
1098       is also called when a leftbar item is tapped, per enyo's List conventions.
1099      */
1100     setupPanelMenuItem: function (inSender, inEvent) {
1101       var module = this.getSelectedModule(),
1102         index = inEvent.index,
1103         isSelected = inSender.isSelected(index),
1104         panel = module.panels[index],
1105         name = panel && panel.name ? module.panels[index].name : "",
1106         // peek inside the kind to see what the label should be
1107         kind = panel && panel.kind ? XT.getObjectByName(panel.kind) : null,
1108         label = kind && kind.prototype.label ? kind.prototype.label : "",
1109         shortKindName;
1110
1111       if (!label && kind && kind.prototype.determineLabel) {
1112         // some of these lists have labels that are dynamically computed,
1113         // so we can't rely on their being statically defined. We have to
1114         // compute them in the same way that their create() method would.
1115         shortKindName = panel.kind.substring(0, panel.kind.length - 4).substring(3);
1116         label = kind.prototype.determineLabel(shortKindName);
1117
1118       } else if (!label) {
1119         label = panel ? panel.label || name : name;
1120       }
1121
1122       this.$.listItem.setContent(label);
1123       this.$.listItem.addRemoveClass("onyx-selected", isSelected);
1124     },
1125
1126     /**
1127       This function is called when a panel is selected. If the selection is valid,
1128       then the content panel is set for that panel selection.
1129     */
1130     panelTap: function (inSender, inEvent) {
1131       var index = inEvent.index, validIndex = index || index === 0;
1132       if (validIndex && inSender.isSelected(index)) { // make sure an item in the list was clicked and is selected
1133         this.setContentPanel(index);
1134       }
1135     },
1136
1137     /**
1138       This function is called when a module is selected. If the selection is valid,
1139       then the list of panels is shown for that module.
1140     */
1141     menuTap: function (inSender, inEvent) {
1142       var validIndex = inEvent.index || inEvent.index === 0;
1143       if (validIndex) { // make sure an item in the list was clicked
1144         this.setupModuleMenuItem(inSender, inEvent);
1145       }
1146     },
1147     showAbout: function () {
1148       this.$.aboutPopup.show();
1149     },
1150     /**
1151       Error notification, using XV.ModuleContainer notify mechanism
1152      */
1153     showError: function (message) {
1154       var inEvent = {
1155         originator: this,
1156         type: XM.Model.CRITICAL,
1157         message: message
1158       };
1159       this.doNotify(inEvent);
1160     },
1161     showHelp: function () {
1162       var listName,
1163         culture,
1164         objectName,
1165         pageName,
1166         url,
1167         panel;
1168
1169       listName = this.$.contentPanels.getActive().name;
1170       culture = XT.locale.culture;
1171       objectName = (listName.indexOf("List") >= 0 || listName.indexOf("Page") >= 0) ?
1172         listName.substring(0, listName.length - 4) : // get rid of the word "List" or "Page"
1173         listName;
1174       objectName = objectName.indexOf("_") >= 0 ?
1175         objectName.substring(1 + objectName.indexOf("_")) : // strip out underscore-terminated-prefixes
1176         objectName;
1177       pageName = objectName.decamelize().replace(/_/g, "-");
1178       url = XT.HELP_URL_ROOT + pageName + "?culture=" + culture;
1179       panel = {name: 'help', show: true, url: url};
1180
1181       this.doNavigatorEvent(panel);
1182     },
1183     /**
1184       Displays the history panel.
1185      */
1186     showHistory: function (inSender, inEvent) {
1187       var panel = {name: 'history', show: true};
1188       this.doNavigatorEvent(panel);
1189     },
1190     /**
1191       Displays the advanced search panel.
1192      */
1193     showParameters: function (inSender, inEvent) {
1194       var list = this.$.contentPanels.getActive();
1195       this.doNavigatorEvent({name: list.name, show: true});
1196     },
1197     /**
1198       Displays the My Account popup.
1199      */
1200     showMyAccount: function (inSender, inEvent) {
1201       this.$.myAccountPopup.show();
1202     },
1203     /** @private */
1204     _getModelProperty: function (Klass, prop) {
1205       var ret = false;
1206       // Get the key if it's a document model
1207       if (Klass.prototype[prop]) {
1208         ret = Klass.prototype[prop];
1209
1210       // Hopefully it's an info model
1211       } else if (Klass.prototype.editableModel) {
1212         Klass = XT.getObjectByName(Klass.prototype.editableModel);
1213         ret = Klass.prototype[prop];
1214       }
1215       return ret;
1216     }
1217   };
1218
1219   enyo.mixin(navigator, XV.ListMenuManagerMixin);
1220   enyo.kind(navigator);
1221
1222 }());