/*jshint bitwise:true, indent:2, curly:true, eqeqeq:true, immed:true,
latedef:true, newcap:true, noarg:true, regexp:true, undef:true,
trailing:true*/
/*global XT:true, XV:true, XM:true, _:true, enyo:true, window:true */
(function () {
var MODULE_MENU = 0;
var PANEL_MENU = 1;
/**
@name XV.Navigator
@class Contains a set of panels for navigating the app and modules
within the app.<br/>
Navigation within the app is accomplished by elements within the menu
tool bar, such as history, search, the back button or logout.<br/>
Navigation within modules in the app is accomplished with a list within
the panel menu which displays the menu items for each context.<br/>
The root menu (module menu) contains the list of modules and the logout.<br/>
Only three menus are cached at one time.<br />
Layout: Collapsing Arranger.<br />
Use to implement the high-level container of all business object lists.<br/>
Derived from <a href="http://enyojs.com/api/#enyo.Panels">enyo.Panels</a>.
@extends enyo.Panels
@extends XV.ListMenuManager
*/
var navigator = /** @lends XV.Navigator# */{
name: "XV.Navigator",
kind: "XV.ContainerPanels",
classes: 'xv-navigator',
/**
Published fields
@type {Object}
@property {Array} modules A DOM-free representation of all of the modules
contained in the navigator. The details of these module objects will
inform the creation of the panel components.
@property {Object} panelCache A hashmap of cached panels where the key is
the global ID of the panel and the value is the enyo panel component.
*/
published: {
modules: [],
panelCache: {},
actions: [
{name: "newTabItem", label: "_openNewTab".loc(), method: "newTab", alwaysShowing: true},
{name: "preferencesItem", label: "_preferences".loc(), method: "openPreferencesWorkspace" },
{name: "myAccountItem", label: "_changePassword".loc(), method: "showMyAccount"},
{name: "aboutItem", label: "_about".loc(), method: "showAbout"}
],
},
events: {
onListAdded: "",
onNavigatorEvent: "",
onNotify: "",
onWorkspace: ""
},
handlers: {
onDeleteTap: "showDeletePopup",
onListItemMenuTap: "showListItemMenu",
onMessage: "setMessageContent",
onParameterChange: "requery",
onColumnsChange: "changeLayout",
onItemTap: "itemTap",
onExportList: "exportList",
onHotKey: "handleHotKey",
onPrintList: "printList",
onPrintSelectList: "printSelectList",
onReportList: "reportList",
},
showPullout: true,
components: [
{kind: "FittableRows", name: "navigationMenu", classes: "xv-menu-container", components: [
{kind: "onyx.Toolbar", classes: "onyx-menu-toolbar", components: [
{kind: "font.TextIcon", name: "backButton",
content: "_logout".loc(), ontap: "backTapped", icon: "chevron-left"},
{kind: "Group", name: "iconButtonGroup", tag: null, components: [
{kind: "font.TextIcon", name: "historyIconButton",
icon: "time",
ontap: "showHistory", content: "_history".loc()},
{kind: "font.TextIcon", name: "searchIconButton",
icon: "search", ontap: "showParameters",
content: "_search".loc(), showing: false},
{kind: "font.TextIcon", name: "helpIconButton",
icon: "question",
ontap: "showHelp", content: "_help".loc()}
]},
{kind: "onyx.MenuDecorator", onSelect: "actionSelected", components: [
{kind: "font.TextIcon", icon: "cog",
content: "_actions".loc(), name: "actionButton"},
{kind: "onyx.Menu", name: "actionMenu", floating: true}
]},
{kind: "onyx.Popup", name: "aboutPopup", centered: true,
modal: true, floating: true, scrim: true, components: [
{content: "Copyright xTuple 2014" }, // translation?
{name: "aboutVersion", allowHtml: true },
{kind: "onyx.Button", content: "_ok".loc(), ontap: "closeAboutPopup"}
]}
]},
{name: "loginInfo", classes: "xv-header"},
{name: "menuPanels", kind: "Panels", classes: 'xv-panels', draggable: false,
fit: true, margin: 0, components: [
{name: "moduleMenu", kind: "List", touch: true, classes: 'xv-navigator-menu',
onSetupItem: "setupModuleMenuItem", ontap: "menuTap",
components: [
{name: "moduleItem", classes: "item enyo-border-box xv-list-item"}
]},
{name: "panelMenu", kind: "List", touch: true, classes: 'xv-navigator-menu',
onSetupItem: "setupPanelMenuItem", ontap: "panelTap", components: [
{name: "listItem", classes: "item enyo-border-box xv-list-item"}
]},
{} // Why do panels only work when there are 3+ objects?
]}
]},
{kind: "FittableRows", name: "list-view", components: [
// the onyx-menu-toolbar class keeps the popups from being hidden
{kind: "onyx.MoreToolbar", name: "contentToolbar",
classes: "onyx-menu-toolbar", movedClass: "xv-toolbar-moved", components: [
{kind: "onyx.Grabber", classes: "spacer", unmoveable: true,},
{name: "rightLabel", classes: "xv-toolbar-label", unmoveable: true,},
// The MoreToolbar is a FittableColumnsLayout, so this spacer takes up all available space
{name: "spacer", classes: "spacer", fit: true},
{kind: "font.TextIcon", name: "backPanelButton", unmoveable: true,
content: "_back".loc(), ontap: "backPanelTapped", icon: "chevron-left"},
{kind: "font.TextIcon", name: "refreshButton",
icon: "rotate-right", content: "_refresh".loc(),
ontap: "requery", showing: false},
// Selectable "New" menu which is hidden by default
{kind: "onyx.MenuDecorator", onSelect: "newRecord", showing: false, name: "newMenuButton", components: [
{kind: "font.TextIcon", icon: "plus", content: "_new".loc()},
{kind: "onyx.Menu", name: "newMenu", floating: true}
]},
{kind: "font.TextIcon", icon: "plus",
content: "_new".loc(), name: "newButton", ontap: "newRecord", showing: false},
{kind: "font.TextIcon", name: "sortButton", content: "_sort".loc(),
icon: "sort-by-alphabet", ontap: "showSortPopup", showing: false},
{kind: "onyx.MenuDecorator", onSelect: "exportSelected", name: "exportButton", components: [
{kind: "font.TextIcon", icon: "share", content: "_export".loc()},
{kind: "onyx.Menu", name: "exportMenu", floating: true}
]},
{kind: "XV.SortPopup", name: "sortPopup", showing: false},
{name: "search", kind: "onyx.InputDecorator",
showing: false, classes: "xv-search", components: [
{name: 'searchInput', kind: "onyx.Input",
placeholder: "_search".loc(), onchange: "inputChanged"},
{tag: "i", classes: "icon-search", name: "searchJump", ontap: "jump"}
]}
]},
{name: "messageHeader"},
{name: "header", classes: "xv-header"},
{name: "contentHeader"},
{name: "contentPanels", kind: "Panels", margin: 0, fit: true,
draggable: false, panelCount: 0, classes: "scroll-ios xv-content-panel"},
{name: "myAccountPopup", kind: "XV.MyAccountPopup"},
{name: "listItemMenu", kind: "onyx.Menu", floating: true, onSelect: "listActionSelected",
maxHeight: 500, components: []
}
]}
],
/**
Keeps track of whether any list has already been fetched, to avoid unnecessary
refetching.
*/
fetched: {},
actionSelected: function (inSender, inEvent) {
var index = this.$.menuPanels.getIndex(),
context = this,
action = inEvent.originator.action,
method = action.method || action.name;
// Determine if action is coming from a more specific context
if (action.context) {
context = this.$.contentPanels.getActive();
} else if (index && !action.alwaysShowing) {
context = this.getSelectedModule();
}
context[method](inSender, inEvent);
},
activate: function () {
this.setMenuPanel(MODULE_MENU);
},
backPanelTapped: function () {
this.setIndex(0);
},
/**
The back button is a logout button if you're at the root menu. Otherwise it's a
back button that takes you to the root menu.
*/
backTapped: function () {
var index = this.$.menuPanels.getIndex();
if (index === MODULE_MENU) {
var inEvent = {
originator: this,
type: XM.Model.QUESTION,
callback: function (response) {
if (response.answer) {
XT.logout();
}
},
message: "_logoutConfirmation".loc()
};
this.doNotify(inEvent);
} else {
this.setHeaderContent("");
this.setMenuPanel(MODULE_MENU);
}
},
/**
If we're on the main menu, then use the navigator actions defined
in its `actions` property. If we're in a module, then use the module's
actions.
*/
buildMenus: function () {
var actionMenu = this.$.actionMenu,
exportMenu = this.$.exportMenu,
newMenu = this.$.newMenu,
// This is the index of the active panel. All panels
// that have been selected from the menus have an index of
// greater than 0. The welcome page has an index of 0.
idx = this.$.menuPanels.getIndex(),
activePanel = this.$.contentPanels.getActive(),
ary = idx ? (this.getSelectedModule().actions || []).concat(this.getActions(true)) : this.getActions(),
actions = ary.slice(0),
privileges = XT.session.privileges;
// HANDLE ACTIONS
// reset the menu
actionMenu.destroyClientControls();
if (idx && activePanel.getNavigatorActions) {
_.each(activePanel.getNavigatorActions(), function (action) {
action.context = activePanel;
actions.push(action);
});
}
// then add whatever actions are applicable to the current context
_.each(actions, function (action) {
var name = action.name,
actPrivs = action.privilege ? action.privilege.split(" ") : [],
isDisabled = false;
if (actPrivs.length) {
isDisabled = !_.some(actPrivs, function (privilege) {
return privileges.get(privilege);
});
}
actionMenu.createComponent({
name: name,
kind: XV.MenuItem,
content: action.label || ("_" + name).loc(),
action: action,
disabled: isDisabled
});
});
actionMenu.render();
this.$.actionButton.setShowing(actions.length);
// HANDLE EXPORTS
actions = [];
// reset the menu
exportMenu.destroyClientControls();
if (idx && activePanel.getExportActions) {
_.each(activePanel.getExportActions(), function (action) {
action.context = activePanel;
actions.push(action);
});
}
// then add whatever actions are applicable to the current context
_.each(actions, function (action) {
var name = action.name,
actPrivs = action.privilege ? action.privilege.split(" ") : [],
isDisabled = false;
if (actPrivs.length) {
isDisabled = !_.some(actPrivs, function (privilege) {
return privileges.get(privilege);
});
}
exportMenu.createComponent({
name: name,
kind: XV.MenuItem,
content: action.label || ("_" + name).loc(),
action: action,
disabled: isDisabled
});
});
exportMenu.render();
this.$.exportButton.setShowing(actions.length && !enyo.Panels.isScreenNarrow());
// HANDLE SORT BUTTON
// if the activepanel is a list of some kind, show the button
if (activePanel.kindClasses) {
this.$.sortButton.setShowing(activePanel.kindClasses.indexOf("list") !== -1);
} else {
this.$.sortButton.setShowing(false);
}
// HANDLE NEW
actions = [];
// reset the menu
newMenu.destroyClientControls();
// XXX not sure about possible falsy condition of idx here
if ((idx || idx === 0) && activePanel.getNewActions) {
_.each(activePanel.getNewActions(), function (action) {
action.context = activePanel;
actions.push(action);
});
}
// then add whatever actions are applicable to the current context
_.each(actions, function (action) {
newMenu.createComponent({
name: action.name,
kind: XV.MenuItem,
content: action.label || ("_" + action.name).loc(),
// this item is the payload from the menu item
// that can be handled differently depending on the list.
item: action.item,
defaults: action.defaults,
allowNew: action.allowNew
});
});
newMenu.render();
},
/**
If there is a parameter widget, send the current list to the
layout form to build the list of columns.
*/
buildLayout: function () {
var list = this.$.contentPanels.getActive(),
parameterWidget = XT.app ? XT.app.$.pullout.getParameterWidget(list.name) : null;
if (parameterWidget && parameterWidget.showLayout) {
parameterWidget.buildColumnList(list);
}
},
/**
The navigator only keeps three panels in the DOM at a time. Anything extra panels
will be periodically cached into the panelCache published field and removed from the DOM.
*/
cachePanels: function () {
var contentPanels = this.$.contentPanels,
panelToCache,
globalIndex,
pertinentModule,
panelReference,
findPanel = function (panel) {
return panel.index === globalIndex;
},
findModule = function (module) {
var panel = _.find(module.panels, findPanel);
return panel !== undefined;
};
while (contentPanels.children.length > 3) {
panelToCache = contentPanels.children[0];
globalIndex = panelToCache.index;
// Panels are abstractly referenced in this.getModules().
// Find the abstract panel of the panelToCache
// XXX this would be cleaner if we kept a backwards reference
// from the panel to its containing module (and index therein)
pertinentModule = _.find(this.getModules(), findModule);
panelReference = _.find(pertinentModule.panels, findPanel);
contentPanels.removeChild(panelToCache);
// only render the most recent (i.e. active) child
contentPanels.children[2].render();
panelReference.status = "cached";
this.getPanelCache()[globalIndex] = panelToCache;
}
},
/**
When a new column value is selected in the layout panel,
this value replaces the old attribute value in the list
field.
*/
changeLayout: function (inSender, inEvent) {
var newValue = inEvent.value ? inEvent.value : "",
order = inEvent.order,
list = this.$.contentPanels.getActive(),
// get the current list of attribute kinds
currentColumns = _.filter(list.$, function (item) {
return item.kind === "XV.ListAttr";
});
// When we get to the current selected list attribute,
// set the new value in place of the old attribute
for (var i = 0; i < currentColumns.length; i++) {
var col = currentColumns[i];
if (order === (i + 1)) {
col.setAttr(newValue);
}
}
// requery this list with the new attribute values
this.requery();
// rebuild the tree of columns
this.buildLayout();
return true; // stop right here
},
clearMessage: function () {
this.$.messageHeader.setContent("");
this.$.messageHeader.setClasses("");
},
closeAboutPopup: function () {
this.$.aboutPopup.hide();
},
create: function () {
this.inherited(arguments);
var that = this,
callback = function () {
that.buildMenus();
};
// If not everything is loaded yet, come back to it later
if (!XT.session || !XT.session.privileges) {
XT.getStartupManager().registerCallback(callback);
} else {
callback();
}
},
exportSelected: function (inSender, inEvent) {
var index = this.$.menuPanels.getIndex(),
context = this,
action = inEvent.originator.action,
method = action.method || action.name;
action.context[method](inSender, inEvent);
},
getActions: function (alwaysShowingOnly) {
var actions = this.actions;
if (alwaysShowingOnly) {
actions = _.filter(actions, function (action) {
return action.alwaysShowing === true;
});
}
return actions;
},
getSelectedModule: function () {
return this._selectedModule;
},
/**
Exports the contents of a list to CSV. Note that it will export the entire
list, not just the part that's been lazy-loaded. Of course, it will apply
the filter criteria as selected. Goes to the server for this.
Avoids websockets or AJAX because the server will prompt the browser to download
the file by setting the Content-Type of the response, which is not possible with
those technologies.
*/
exportList: function (inSender, inEvent) {
this.openExportTab('export');
return true;
},
newTab: function () {
window.open(XT.getOrganizationPath() + '/app', '_newtab');
},
openPreferencesWorkspace: function () {
this.doWorkspace({workspace: "XV.UserPreferenceWorkspace", id: false});
},
reportList: function (inSender, inEvent) {
this.openExportTab('report');
return true;
},
printList: function (inSender, inEvent) {
var list = this.$.contentPanels.getActive(),
recordType = list.value.model.prototype.recordType,
query = JSON.parse(JSON.stringify(list.getQuery())); // clone
XT.DataSource.callRoute(
'report?details={"nameSpace":"%@","type":"%@","query":%@,"culture":%@,"print":%@}'
.f(recordType.prefix(), recordType.suffix(), JSON.stringify(query), JSON.stringify(XT.locale.culture), "true"),
{},
{success: function (result) {
//
// Here's where we could do a popup for print status
//
if (XT.session.config.debugging) {
XT.log("Print route success, type: ", recordType.suffix());
}
},
error: function (result) {
if (XT.session.config.debugging) {
XT.log("Print route error, type: ", recordType.suffix());
}
}
}
);
return true;
},
printSelectList: function (inSender, inEvent) {
var list = this.$.contentPanels.getActive(),
selected = list.getSelection().getSelected();
_.each(selected, function (value, index) {
var model = list.getModel(index),
modelName = model.editableModel || model.recordType,
reportName = modelName.suffix(),
details = {
nameSpace: modelName.prefix(),
type: modelName.suffix(),
id: model.id,
name: reportName,
culture: XT.locale.culture,
print: true
};
XT.DataSource.callRoute(
"report?details=%@".f(JSON.stringify(details)),
{},
{success: function (result) {
//
// Here's where we could do a popup for print status
//
if (XT.session.config.debugging) {
XT.log("Print route success, type: ", modelName.suffix());
}
},
error: function (result) {
if (XT.session.config.debugging) {
XT.log("Print route error, type: ", modelName.suffix());
}
}
}
);
});
return true;
},
/**
* If a reportSelectList is needed, here it is. This will open a browser
* tab for the first report and new windows for the other reports.
*
reportSelectList: function (inSender, inEvent) {
var list = this.$.contentPanels.getActive(),
selected = list.getSelection().getSelected();
_.each(selected, function (value, index) {
var model = list.getModel(index),
modelName = model.editableModel || model.recordType,
reportName = modelName.suffix(),
details = {
nameSpace: modelName.prefix(),
type: modelName.suffix(),
id: model.id,
name: reportName,
culture: XT.locale.culture
};
// sending the locale information back over the wire saves a call to the db
console.log("printing id: " + model.id);
window.open(XT.getOrganizationPath() +
"/report?details=%@".f(JSON.stringify(details)), "Print: " + model.id);
});
return true;
},
*/
openExportTab: function (routeName) {
var list = this.$.contentPanels.getActive(),
recordType = list.value.model.prototype.recordType,
query = JSON.parse(JSON.stringify(list.getQuery())); // clone
delete query.rowLimit;
delete query.rowOffset;
// sending the locale information back over the wire saves a call to the db
window.open(XT.getOrganizationPath() +
'/%@?details={"nameSpace":"%@","type":"%@","query":%@,"culture":%@,"print":%@}'
.f(routeName,
recordType.prefix(),
recordType.suffix(),
JSON.stringify(query),
JSON.stringify(XT.locale.culture),
"false"),
'_newtab');
},
showSortPopup: function (inSender, inEvent) {
this.$.sortPopup.setList(this.$.contentPanels.getActive());
this.$.sortPopup.setNav(this);
this.$.sortPopup.setPickerStrings();
this.$.sortPopup.show();
},
/**
Fetch a list.
*/
fetch: function (options) {
options = options ? _.clone(options) : {};
var list = this.$.contentPanels.getActive(),
name = list ? list.name : "",
query,
input,
parameterWidget,
parameters,
filterDescription;
// in order to continue to the fetch, this needs to be a list
// or a dashboard
if (!(list instanceof XV.List) && !(list instanceof XV.Dashboard) && !(list instanceof XV.Listboard)) { return; }
query = list.getQuery() || {};
input = this.$.searchInput.getValue();
// if the "list" doesn't allow dynamic searching, skip this
// Dashboards have an allowFilter of false
if (list.allowFilter) {
parameterWidget = XT.app ? XT.app.$.pullout.getParameterWidget(name) : null;
parameters = parameterWidget ? parameterWidget.getParameters() : [];
options.showMore = _.isBoolean(options.showMore) ?
options.showMore : false;
// Get information from filters and set description
filterDescription = this.formatQuery(parameterWidget ? parameterWidget.getSelectedValues() : null, input);
list.setFilterDescription(filterDescription);
this.setHeaderContent(filterDescription);
delete query.parameters;
// Build parameters
if (input || parameters.length) {
query.parameters = [];
// Input search parameters
if (input) {
query.parameters.push({
attribute: list.getSearchableAttributes(),
operator: 'MATCHES',
value: this.$.searchInput.getValue()
});
}
// Advanced parameters
if (parameters) {
query.parameters = query.parameters.concat(parameters);
}
}
// if there is a parameter widget for this list, build the columns
if (parameterWidget && parameterWidget.showLayout) {
this.buildLayout();
}
}
list.setQuery(query);
list.fetch(options);
},
formatQuery: function (advancedSearch, simpleSearch) {
var key,
formattedQuery = "";
for (key in advancedSearch) {
formattedQuery += (key + ": " + advancedSearch[key] + ", ");
}
if (simpleSearch && formattedQuery) {
formattedQuery += "_match".loc() + ": " + simpleSearch;
} else if (simpleSearch) {
formattedQuery += simpleSearch;
}
if (formattedQuery) {
formattedQuery = "_filterBy".loc() + ": " + formattedQuery;
}
if (formattedQuery.lastIndexOf(", ") + 2 === formattedQuery.length) {
// chop off trailing comma
formattedQuery = formattedQuery.substring(0, formattedQuery.length - 2);
}
return formattedQuery;
},
handleHotKey: function (inSender, inEvent) {
var destinationIndex,
isWelcome = this.getSelectedModule().name === 'welcome',
currentIndex,
keyCode = inEvent.keyCode;
// numbers navigate to the nth menu option
if (keyCode >= 49 && keyCode <= 57) {
destinationIndex = keyCode - 49;
if (isWelcome) {
this.setModule(Math.min(destinationIndex, this.getModules().length - 1));
} else {
this.setContentPanel(Math.min(destinationIndex, this.getSelectedModule().panels.length - 1));
}
return;
} else if (!isWelcome) {
currentIndex = this.$.panelMenu.getSelection().lastSelected;
if (keyCode === 38) {
this.setContentPanel(Math.max(currentIndex - 1, 0));
return;
} else if (keyCode === 40) {
this.setContentPanel(Math.min(currentIndex + 1, this.getSelectedModule().panels.length - 1));
return;
}
}
switch(String.fromCharCode(keyCode)) {
case 'A':
this.showParameters();
break;
case 'B':
this.backTapped();
break;
case 'H':
this.showHelp();
break;
case 'N':
this.newRecord({}, {originator: {}});
break;
case 'R':
this.requery();
break;
}
},
inputChanged: function (inSender, inEvent) {
this.fetched = {};
this.fetch();
},
/**
Drills down into a workspace if a user clicks a list item.
*/
itemTap: function (inSender, inEvent) {
var workspace = inEvent.originator.getWorkspace(),
model = inEvent.model,
canNotRead = model.couldRead ? !model.couldRead() : !model.getClass().canRead(),
id = model && model.id ? model.id : false;
// Check privileges first
if (canNotRead) {
this.showError("_insufficientViewPrivileges".loc());
return true;
}
// Bubble requset for workspace view, including the model id payload
if (workspace) {
this.doWorkspace({workspace: workspace, id: id});
}
return true;
},
jump: function () {
var list = this.$.contentPanels.getActive(),
workspace = list ? list.getWorkspace() : null,
Klass = list.getValue().model,
upper = this._getModelProperty(Klass, 'enforceUpperKey'),
input = this.$.searchInput.getValue(),
that = this,
options = {},
key = this._getModelProperty(Klass, 'documentKey'),
model,
attrs = {};
if (this._busy || !input || !key) { return; }
this._busy = true;
// First find a matching id
options.success = function (id) {
var options = {};
if (id) {
// Next fetch the model, see if we have privs
options.success = function () {
var canNotRead = model.couldRead ?
!model.couldRead() : !model.getClass().canRead();
// Check privileges first
if (canNotRead) {
this.showError("_insufficientViewPrivileges".loc());
} else {
// Bubble requset for workspace view, including the model id payload
if (workspace) { that.doWorkspace({workspace: workspace, id: id}); }
}
that._busy = false;
};
attrs[Klass.prototype.idAttribute] = id;
model = Klass.findOrCreate(attrs);
model.fetch(options);
} else {
that.showError("_noDocumentFound".loc());
that.$.searchInput.clear();
that._busy = false;
}
};
input = upper ? input.toUpperCase() : input;
Klass.findExisting(key, input, options);
},
loginInfo: function () {
return this.$.loginInfo;
},
/**
Handles additive changes only
*/
modulesChanged: function () {
var modules = this.getModules() || [],
existingModules = this._modules || [],
existingModule,
existingPanel,
panels,
panel,
i,
n,
findExistingModule = function (name) {
return _.find(existingModules, function (module) {
return module.name === name;
});
},
findExistingPanel = function (panels, name) {
return _.find(panels, function (panel) {
return panel.name === name;
});
};
// Build panels
for (i = 0; i < modules.length; i++) {
panels = modules[i].panels || [];
existingModule = findExistingModule(modules[i].name);
for (n = 0; n < panels.length; n++) {
// If the panel already exists, move on
if (existingModule) {
existingPanel = findExistingPanel(existingModule.panels, panels[n].name);
if (existingPanel) { continue; }
}
// Keep track of where this panel is being placed for later reference
panels[n].index = this.$.contentPanels.panelCount++;
// XXX try this: only create the first three
if (panels[n].index < 3) {
panels[n].status = "active";
// Default behavior for Lists is toggle selections
// So we can perform actions on rows. If not a List
// this property shouldn't hurt anything
if (panels[n].toggleSelected === undefined) {
panels[n].toggleSelected = true;
}
panel = this.$.contentPanels.createComponent(panels[n]);
if (panel instanceof XV.List) {
// Bubble parameter widget up to pullout
this.doListAdded(panel);
}
} else {
panels[n].status = "unborn";
}
}
}
this.$.moduleMenu.setCount(modules.length);
// Cache a deep copy
this._modules = JSON.parse(JSON.stringify(modules));
this.render();
},
/**
Fired when the user clicks the "New" button. Takes the user to a workspace
backed by an empty object of the type displayed in the current list.
*/
newRecord: function (inSender, inEvent) {
var list = this.$.contentPanels.getActive(),
workspace = list instanceof XV.List ? list.getWorkspace() : null,
item = inEvent.originator.item,
defaults = inEvent.originator.defaults,
allowNew = inEvent.originator.allowNew,
Model,
canCreate,
callback;
// This list is actually a dashboard so the item
// will be a type of chart added to the dashboard
if (list instanceof XV.Dashboard && item) {
list.newRecord(item);
return true;
}
if (list instanceof XV.Listboard && item) {
list.newRecord(item);
return true;
}
if (!list instanceof XV.List) {
return true;
}
// Check privileges
Model = list.getValue().model;
canCreate = Model.couldCreate ? Model.couldCreate() : Model.canCreate();
if (!canCreate) {
this.showError("_insufficientCreatePrivileges".loc());
return true;
}
if (workspace) {
var params = {};
this.doWorkspace({
workspace: workspace,
attributes: defaults,
allowNew: allowNew === false ? false : true
});
}
// In addition to preventing Enyo event propagation,
// we need to prevent propagation of DOM events to support
// mobile browsers and long button clicks
// check to make sure this is a button click before calling inEvent function
if (inEvent && inEvent.preventDefault) {
inEvent.preventDefault();
}
return true;
},
popupHidden: function (inSender, inEvent) {
if (!this._popupDone) {
inEvent.originator.show();
}
},
requery: function (inSender, inEvent) {
this.fetch();
},
/**
Determines whether the advanced search or the history icon (or neither) is
lit.
*/
setActiveIconButton: function (buttonName) {
var activeIconButton = null;
// Null deactivates both
if (buttonName === 'search') {
activeIconButton = this.$.searchIconButton;
} else if (buttonName === 'history') {
activeIconButton = this.$.historyIconButton;
}
this.$.iconButtonGroup.setActive(activeIconButton);
},
/**
Renders a list and performs all the necessary auxilliary work such as hiding/showing
the advanced search icon if appropriate. Called when a user chooses a menu item.
*/
setContentPanel: function (index) {
var contentPanels = this.$.contentPanels,
module = this.getSelectedModule(),
panelIndex = module && module.panels ? module.panels[index].index : -1,
panelStatus = module && module.panels ? module.panels[index].status : 'unknown',
contentHeader = this.$.contentHeader,
panel,
label,
collection,
model,
canNotCreate = true;
this.clearMessage();
if (panelStatus === 'active') {
panel = _.find(contentPanels.children, function (child) {
return child.index === panelIndex;
});
} else if (panelStatus === 'unborn') {
// panel exists but has not been rendered. Render it.
module.panels[index].status = 'active';
// Default behavior for Lists is toggle selections
// So we can perform actions on rows. If not a List
// this property shouldn't hurt anything
if (module.panels[index].toggleSelected === undefined) {
module.panels[index].toggleSelected = true;
}
panel = contentPanels.createComponent(module.panels[index]);
panel.render();
if (panel instanceof XV.List) {
// Bubble parameter widget up to pullout
this.doListAdded(panel);
}
} else if (panelStatus === 'cached') {
module.panels[index].status = 'active';
panel = this.panelCache[panelIndex];
contentPanels.addChild(panel);
panel.node = undefined; // new to enyo2.2! wipe out the node so that it can get re-rendered fresh
panel.render();
} else {
XT.error("Don't know what to do with this panel status");
}
// cache any extraneous content panels
this.cachePanels();
label = panel && panel.label ? panel.label : "";
collection = panel && panel.getCollection ? XT.getObjectByName(panel.getCollection()) : false;
if (!panel) { return; }
// Make sure the advanced search icon is visible iff there is an advanced
// search for this list
this.$.searchIconButton.setShowing(!!panel.parameterWidget);
this.doNavigatorEvent({name: panel.name, show: false});
// Handle new button
this.$.newButton.setShowing(panel.canAddNew && !panel.newActions);
this.$.newMenuButton.setShowing(panel.canAddNew && panel.newActions);
if (panel.canAddNew && collection) {
// Check 'couldCreate' first in case it's an info model.
model = collection.prototype.model;
canNotCreate = model.prototype.couldCreate ? !model.prototype.couldCreate() : !model.canCreate();
}
this.$.newButton.setDisabled(canNotCreate);
// Select panelMenu
if (!this.$.panelMenu.isSelected(index)) {
this.$.panelMenu.select(index);
}
// Select list
contentPanels.setIndex(this.$.contentPanels.indexOfChild(panel));
this.$.rightLabel.setContent(label);
if (panel.getFilterDescription) {
this.setHeaderContent(panel.getFilterDescription());
}
if (panel.alwaysRefetch || (panel.fetch && !this.fetched[panelIndex])) {
this.fetch();
this.fetched[panelIndex] = true;
}
this.buildMenus();
this.$.contentToolbar.resized();
contentHeader.destroyClientControls();
if (panel.headerComponents) {
contentHeader.createComponents(panel.headerComponents);
contentHeader.render();
}
// If this is a mobile device and we're not selecting
// the welcome screen.
if (enyo.Panels.isScreenNarrow()) {
this.next();
}
},
/**
The header content typically describes to the user the particular query filter in effect.
*/
setHeaderContent: function (content) {
this.$.header.setShowing(!!content);
this.$.header.setContent(content);
},
setMenuPanel: function (index) {
var label = index ? "_back".loc() : "_logout".loc();
this.$.menuPanels.setIndex(index);
// If this is a small device, don't automatically select a menu
if (!enyo.Panels.isScreenNarrow()) {
this.$.menuPanels.getActive().select(0);
this.setContentPanel(0);
}
// this causes the gear menu to reload when the module
// is chosen, rather than just when a content panel is selected
this.buildMenus();
this.$.backButton.setContent(label);
// Only show this back button on small devices
this.$.backPanelButton.setContent(label);
this.$.backPanelButton.setShowing(enyo.Panels.isScreenNarrow());
this.$.refreshButton.setShowing(!!index);
// remove unnecessary buttons on small devices
this.$.search.setShowing(!!index && !enyo.Panels.isScreenNarrow());
this.$.contentToolbar.resized();
},
setMessageContent: function (inSender, inEvent) {
var content = inEvent.message;
this.$.messageHeader.setContent(content);
},
setModule: function (index) {
var module = this.getModules()[index],
panels = module.panels || [],
hasSubmenu = module.hasSubmenu !== false && panels.length;
if (module !== this._selectedModule || enyo.Panels.isScreenNarrow()) {
this._selectedModule = module;
if (hasSubmenu) {
this.$.panelMenu.setCount(panels.length);
this.$.panelMenu.render();
this.setMenuPanel(PANEL_MENU);
} else {
// if no submenus, treat lke a panel
this.setContentPanel(0);
}
}
},
setModules: function (modules) {
this.modules = modules;
this.modulesChanged();
},
/**
Renders a list of modules from the root menu.
*/
setupModuleMenuItem: function (inSender, inEvent) {
var index = inEvent.index,
label = this.modules[index].label,
isSelected = inSender.isSelected(index);
this.$.moduleItem.setContent(label);
this.$.moduleItem.addRemoveClass("onyx-selected", isSelected);
this.$.moduleItem.setAttribute("menubar_name", this.modules[index].name);
if (isSelected) { this.setModule(index); }
},
/**
Renders the leftbar list of objects within a given module. This function
is also called when a leftbar item is tapped, per enyo's List conventions.
*/
setupPanelMenuItem: function (inSender, inEvent) {
var module = this.getSelectedModule(),
index = inEvent.index,
isSelected = inSender.isSelected(index),
panel = module.panels[index],
name = panel && panel.name ? module.panels[index].name : "",
// peek inside the kind to see what the label should be
kind = panel && panel.kind ? XT.getObjectByName(panel.kind) : null,
label = kind && kind.prototype.label ? kind.prototype.label : "",
shortKindName;
if (!label && kind && kind.prototype.determineLabel) {
// some of these lists have labels that are dynamically computed,
// so we can't rely on their being statically defined. We have to
// compute them in the same way that their create() method would.
shortKindName = panel.kind.substring(0, panel.kind.length - 4).substring(3);
label = kind.prototype.determineLabel(shortKindName);
} else if (!label) {
label = panel ? panel.label || name : name;
}
this.$.listItem.setContent(label);
this.$.listItem.setAttribute("menubar_name", name);
this.$.listItem.addRemoveClass("onyx-selected", isSelected);
},
/**
This function is called when a panel is selected. If the selection is valid,
then the content panel is set for that panel selection.
*/
panelTap: function (inSender, inEvent) {
var index = inEvent.index, validIndex = index || index === 0;
if (validIndex) { // make sure an item in the list was clicked
this.setContentPanel(index);
}
},
/**
This function is called when a module is selected. If the selection is valid,
then the list of panels is shown for that module.
*/
menuTap: function (inSender, inEvent) {
var validIndex = inEvent.index || inEvent.index === 0;
if (validIndex) { // make sure an item in the list was clicked
this.setupModuleMenuItem(inSender, inEvent);
}
},
showAbout: function () {
this.$.aboutPopup.show();
},
/**
Error notification, using XV.ModuleContainer notify mechanism
*/
showError: function (message) {
var inEvent = {
originator: this,
type: XM.Model.CRITICAL,
message: message
};
this.doNotify(inEvent);
},
showHelp: function () {
var listName,
culture,
objectName,
pageName,
url,
panel;
listName = this.$.contentPanels.getActive().name;
culture = XT.locale.culture;
objectName = (listName.indexOf("List") >= 0 || listName.indexOf("Page") >= 0) ?
listName.substring(0, listName.length - 4) : // get rid of the word "List" or "Page"
listName;
objectName = objectName.indexOf("_") >= 0 ?
objectName.substring(1 + objectName.indexOf("_")) : // strip out underscore-terminated-prefixes
objectName;
pageName = objectName.decamelize().replace(/_/g, "-");
url = XT.HELP_URL_ROOT + pageName + "?culture=" + culture;
panel = {name: 'help', show: true, url: url};
this.doNavigatorEvent(panel);
},
/**
Displays the history panel.
*/
showHistory: function (inSender, inEvent) {
var panel = {name: 'history', show: true};
this.doNavigatorEvent(panel);
},
/**
Displays the advanced search panel.
*/
showParameters: function (inSender, inEvent) {
var list = this.$.contentPanels.getActive();
this.doNavigatorEvent({name: list.name, show: true});
},
/**
Displays the My Account popup.
*/
showMyAccount: function (inSender, inEvent) {
this.$.myAccountPopup.show();
},
/** @private */
_getModelProperty: function (Klass, prop) {
var ret = false;
// Get the key if it's a document model
if (Klass.prototype[prop]) {
ret = Klass.prototype[prop];
// Hopefully it's an info model
} else if (Klass.prototype.editableModel) {
Klass = XT.getObjectByName(Klass.prototype.editableModel);
ret = Klass.prototype[prop];
}
return ret;
}
};
enyo.mixin(navigator, XV.ListMenuManagerMixin);
enyo.kind(navigator);
}());