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, strict:false*/
4 /*global XT:true, XM:true, XV:true, _:true, enyo:true, window: true*/
8 var ROWS_PER_FETCH = 50;
9 var FETCH_TRIGGER = 100;
13 @class Displays a scrolling list of rows.</br >
14 Handles lazy loading. Passes in the first 50 items, and as one scrolls, passes more.<br />
15 Use to display large lists, typically a collection of records retrieved from the database,
16 for example a list of accounts, addresses, contacts, incidents, projects, and so forth.
17 But can also be used to display lists stored elsewhere such as state or country abbreviations.<br />
18 Related: list, XV.List; row, {@link XV.ListItem}; cell, {@link XV.ListColumn}; data, {@link XV.ListAttr}<br />
19 Derived from <a href="http://enyojs.com/api/#enyo.List">enyo.List</a>.<br />
20 Note: enyo.List includes a scroller; therefore, XV.List should not be placed inside a scroller.
23 var list = /** @lends XV.List# */{
32 * @property {Boolean} canAddNew
34 * @property {Array} actions. Array of objects. What actions
35 * that we allow on the list item? Currently supported fields of the object are
36 name {String} The name of the action
37 label {String} Menu item label. Default ("_" + name).loc()
38 notify {Boolean} Do we want to verify with the user? Default true.
39 notifyMessage {String} Overrides default notify message. optional.
40 prerequisite {String} the name of an editeable-model class method that will
42 can perform this action on this item. Asyncronous. First param is a callback
43 function that must be called with true or false, even if syncronous. Leave
44 this out if the user can always take the action.
45 method {String} the name of an enyo method in XV.FooList, or a class method
46 on the editable model.
47 isViewMethod {Boolean}
48 * @property {String} label
53 filterDescription: "",
55 navigatorActions: null,
57 parameterWidget: null,
60 // this property signifies that the list allows
61 // searches via the input or parameter widget
63 showDeleteAction: true,
67 * @see XM.View#list.actions
73 * @see XM.View#list.query
78 * The backing model for this component.
79 * @see XM.EnyoView#model
87 onPrintSelectList: "",
89 onSelectionChanged: "",
94 onActionSelected: "actionSelected",
95 onModelChange: "modelChanged",
96 onListItemMenuTap: "transformListAction",
97 onSetupItem: "setupItem",
98 onChange: "selectionChanged",
101 A list item has been selected. Delegate to the method cited
102 in the action, and pass it an object with the relevant attributes.
104 actionSelected: function (inSender, inEvent) {
105 this[inEvent.action.method](inEvent);
108 @todo Document the collectionChanged method.
110 collectionChanged: function () {
111 var collection = this.getCollection(),
112 Klass = collection ? XT.getObjectByName(collection) : false;
115 this.setValue(new Klass());
121 @todo Document the create method.
123 create: function () {
124 var actions = this.getActions() || [],
125 deleteAction = _.findWhere(actions, {"name": "delete"}),
129 this._actionPermissions = [];
130 this._haveAllAnswers = [];
132 this.inherited(arguments);
133 XM.View.setPresenter(this, "list");
135 this.collectionChanged();
138 collection = this.getValue();
139 Klass = collection ? collection.model : false;
140 method = Klass && Klass.prototype.couldDestroy ? "couldDestroy" : "canDestroy";
142 // add the delete action if it's not already there
143 if (!deleteAction && this.getShowDeleteAction()) {
146 prerequisite: method,
147 notifyMessage: "_confirmDelete".loc() + " " + "_confirmAction".loc(),
148 method: "deleteItem",
152 this.setActions(actions);
154 this._selectedIndexes = [];
155 this.createOverflowRow();
159 Creates an additional hidden row which
160 can be used if more space is needed for
161 columns added during layout changes.
164 XXX yes, this is incompatible with View templates. so ListItems
165 that want to use overflow rows cannot yet use View templates.
167 createOverflowRow: function () {
169 row = this.createComponent({
175 // see how many columns are in the existing row
176 // and create that number here
177 _.each(this.$, function (col) {
178 if (col.kind === "XV.ListColumn") {
180 {kind: "XV.ListColumn", classes: col.classes, components: [
181 {kind: "XV.ListAttr", allowLayout: true, overflow: true,
182 showPlaceholder: true}
187 row.createComponents(columns, {owner: this});
191 @todo Document the getModel method.
193 getModel: function (index) {
194 return this.getValue().models[index];
197 getExportActions: function () {
198 var actions = this.exportActions || [],
199 canExport = !XM.currentUser.get("disableExport"),
200 exportAction = canExport ? _.findWhere(actions, {"name": "export"}) : true,
201 printAction = XT.session.config.biAvailable && this.getAllowPrint() && canExport ?
202 _.findWhere(actions, {"name": "printList"}) : true;
204 // Handle default navigator actions
206 actions.push({name: "export"});
209 actions.push({name: "printList"});
210 actions.push({name: "printSelect"});
211 actions.push({name: "reportList"});
213 this.exportActions = actions;
216 getSortActions: function () {
217 var actions = this.sortActions || [],
218 sortAction = _.findWhere(actions, {"name": "sort"});
220 // Handle default navigator actions
222 actions.push({name: "sort"});
224 this.sortActions = actions;
227 deleteItem: function (inEvent) {
228 var collection = this.getValue(),
229 imodel = inEvent.model,
236 if (imodel instanceof XM.Info) {
237 Klass = XT.getObjectByName(model.editableModel);
238 attrs[Klass.prototype.idAttribute] = model.id;
239 model = Klass.findOrCreate(attrs);
242 fetchOptions.success = function () {
243 var destroyOptions = {};
244 destroyOptions.success = function () {
245 collection.remove(imodel);
251 destroyOptions.error = function (model, err) {
252 XT.log("error destroying model from list", JSON.stringify(err));
254 model.destroy(destroyOptions);
256 model.fetch(fetchOptions);
259 Returns an array of attributes from the model that can be used
260 in a text-based search.
262 getSearchableAttributes: function () {
263 var model = this.getValue().model;
264 return model.getSearchableAttributes ? model.getSearchableAttributes() : [];
267 Returns a list of List Attribute kinds that are currently displayed
270 getCurrentListAttributes: function () {
271 return _.map(_.filter(this.$, function (item) {
272 return item.kind === "XV.ListAttr";
273 }), function (item) {
278 Returns a list of available attributes that can be used for sorting
279 the list or adding as columns.
281 getDisplayAttributes: function () {
287 model = this.getValue().model;
289 attributes = _.without(model.getAttributeNames(), "id", "uuid") || [];
290 // filter out characteristics and address because they have special
291 // formatting concerns
292 relations = _.filter(model.prototype.relations, function (relation) {
293 return relation.key !== "characteristics" && relation.key !== "address";
296 // Return an array of the model names of the relation models
298 _.map(_.filter(relations, function (rel) {
299 // Filter attributes for just relations
300 return _.contains(attributes, rel.key);
302 var model = XT.getObjectByName(key.relatedModel);
303 // For each related model, this removes the ids and formats the
304 // attribute names in the relation.name format
305 var mapping = _.map(_.without(model.getAttributeNames(), "id", "uuid"), function (name) {
306 return key.key + "." + name;
311 attributes = _.difference(attributes, _.map(relations, function (rel) {
315 // Loop through the relation models and concatenate the arrays of
317 _.each(relationModels, function () {
318 // combine the relation arrays
319 attributes = attributes.concat(_.flatten(relationModels));
322 return _.uniq(attributes);
325 @todo Document the getWorkspace method.
327 getWorkspace: function () {
328 var collection = this.getCollection(),
329 Klass = collection ? XT.getObjectByName(collection) : null,
330 recordType = Klass ? Klass.prototype.model.prototype.recordType : null;
331 return XV.getWorkspace(recordType);
333 export: function () {
337 @todo Document the fetch method.
339 fetch: function (options) {
341 query = this.getQuery() || {},
344 options = options ? _.clone(options) : {};
345 options.showMore = _.isBoolean(options.showMore) ?
346 options.showMore : false;
347 success = options.success;
350 if (options.showMore) {
351 query.rowOffset += ROWS_PER_FETCH;
352 options.update = true;
354 options.remove = false;
357 query.rowLimit = ROWS_PER_FETCH;
361 success: function (resp, status, xhr) {
363 if (success) { success(resp, status, xhr); }
367 this.getValue().fetch(options);
370 @todo Document the fetched method.
372 fetched: function () {
373 var query = this.getQuery() || {},
374 offset = query.rowOffset || 0,
375 limit = query.rowLimit || 0,
376 count = this.getValue().length,
378 (offset + limit <= count) && (this.getCount() !== count) : false,
380 this.isMore = isMore;
381 this.fetching = false;
383 // Reset the size of the list
384 this.setCount(count);
386 // Hack: Solves scroll problem for small number of rows
387 // but doesn't seem quite right
388 rowsPerPage = count && 50 > count ? count : 50;
389 if (rowsPerPage !== this.rowsPerPage) {
390 this.setRowsPerPage(rowsPerPage);
397 this._maxTop = this.getScrollBounds().maxTop;
400 If the attribute is equal to or exists as a child
401 attribute, then it is returned.
403 findNameInAttr: function (attr, name) {
404 return _.find(attr.split("."), function (s) {
409 Returns a placeholder translation string to be
410 used as a placeholder if one is not specified.
415 getPlaceholderForAttr: function (attr) {
417 if (attr.indexOf(".") !== -1) {
418 attrs = attr.split(".");
419 str = ("_" + attrs[0]).loc() + " " + ("_" + attrs[1]).loc();
421 str = ("_" + attr).loc();
423 return "_no".loc() + " " + str;
426 Returns whether all actions on the list have been determined
427 to be available or not.
429 @param {Number} index
432 haveAllAnswers: function () {
433 if (this._haveAllAnswers) { return true; }
435 permissions = that._actionPermissions,
437 if (_.isEmpty(permissions)) { return false; }
438 ret = _.reduce(this.getActions(), function (memo, action) {
439 return memo && _.isBoolean(permissions[action.name]);
441 if (ret) { this._haveAllAnswers = true; }
447 * Open up a workspace if the key is tapped, or if 'toggleSelected' is off.
448 * Propagate this event only if no workspace is opened.
449 * @see XV.SearchContainer#itemTap
450 * @see XV.Navigator#itemTap
452 itemTap: function (inSender, inEvent) {
453 if (!this.getToggleSelected() || inEvent.originator.isKey) {
454 this.doItemTap({ model: this.getModel(inEvent.index) });
459 When a model changes, we are notified. We check the list to see if the
460 model is of the same recordType. If so, we check to see if the newly
461 changed model should still be on the list, and refresh appropriately.
463 modelChanged: function (inSender, inEvent) {
465 value = this.getValue(),
466 workspace = this.getWorkspace(),
468 Klass = XT.getObjectByName(this.getCollection()),
469 checkStatusCollection,
470 checkStatusParameter,
473 // If the model that changed was related to and exists on this list
474 // refresh the item. Remove the item if appropriate
475 workspace = workspace ? XT.getObjectByName(workspace) : null;
476 if (workspace && inEvent && workspace.prototype.model === inEvent.model &&
477 value && typeof Klass === "function") {
478 model = this.getValue().get(inEvent.id);
480 // cleverness: we're going to see if the model still belongs in the collection by
481 // creating a new query that's the same as the current filter but with the addition
482 // of filtering on the id. Any result means it still belongs. An empty result
485 // clone the query so as not to change the real thing with this check.
486 checkStatusQuery = JSON.parse(JSON.stringify(this.getQuery()));
487 checkStatusParameter = { attribute: this.getValue().model.prototype.idAttribute, operator: "=", value: inEvent.id};
488 if (checkStatusQuery.parameters) {
489 checkStatusQuery.parameters.push(checkStatusParameter);
491 checkStatusQuery.parameters = [checkStatusParameter];
494 checkStatusCollection = new Klass();
495 checkStatusCollection.fetch({
496 query: checkStatusQuery,
497 success: function (collection, response) {
498 // remove the old model no matter the query result
503 if (collection.size() > 0) {
504 // this model should still be in the collection. Refresh it.
505 value.add(collection.at(0), {silent: true});
507 if (value.comparator) { value.sort(); }
508 if (that.getCount() !== value.length) {
509 that.setCount(value.length);
517 XT.log("Error checking model status in list");
522 reportList: function () {
525 printSelect: function () {
526 this.doPrintSelectList();
528 printList: function () {
532 Makes sure the collection can handle the sort order
533 defined in the query.
535 queryChanged: function () {
536 var query = this.getQuery(),
537 value = this.getValue();
538 if (value && query && query.orderBy) {
539 value.comparator = function (a, b) {
545 get = a.getValue ? "getValue" : "get";
546 for (i = 0; i < query.orderBy.length; i++) {
547 attr = query.orderBy[i].attribute;
548 numeric = query.orderBy[i].numeric;
549 aval = query.orderBy[i].descending ? b[get](attr) : a[get](attr);
550 bval = query.orderBy[i].descending ? a[get](attr) : b[get](attr);
551 aval = numeric ? aval - 0 : aval;
552 bval = numeric ? bval - 0 : bval;
554 return aval > bval ? 1 : -1;
562 Reset actions permission checks will be regenerated.
564 @param {Number} Index
566 resetActions: function () {
567 this._actionPermissions = {};
568 this._haveAllAnswers = undefined;
571 Manages lazy loading of items in the list.
573 scroll: function () {
574 var r = this.inherited(arguments),
577 if (!this._maxTop) { return r; }
579 // Manage lazy loading
580 max = this._maxTop - this.rowHeight * FETCH_TRIGGER;
581 if (this.isMore && !this.fetching && this.getScrollPosition() > max) {
582 this.fetching = true;
583 options.showMore = true;
590 Helper fuction that returns an array of indexes based on
591 the current selection.
595 selectedIndexes: function () {
596 return _.keys(this.getSelection().selected);
599 * Re-evaluates actions menu.
601 selectionChanged: function (inSender, inEvent) {
602 var keys = this.selectedIndexes(),
603 index = inEvent.index,
604 collection = this.value,
605 actions = this.actions,
610 // Loop through each action
611 _.each(actions, function (action) {
612 var prerequisite = action.prerequisite,
613 permissions = that._actionPermissions,
622 // Callback to let us know if we can do an action. If we have
623 // all the answers, enable the action icon.
624 callback = function (response) {
626 // If some other model failed, forget about it
627 if (permissions[name] === false) { return; }
629 // If even one selected model fails, then we can't do the action
633 // If we haven't heard back from all requests yet, wait for the next
638 permissions[name] = response;
640 // Handle asynchronous result re-rendering
641 if (that.haveAllAnswers()) {
642 that.waterfallDown("onListMenuReady");
643 that.renderRow(index);
648 // Loop through each selection model and check pre-requisite
649 for (i = 0; i < keys.length; i++) {
651 model = collection.at(idx);
652 if (model instanceof XM.Info && !model[prerequisite]) {
653 XT.getObjectByName(model.editableModel)[prerequisite](model, callback);
655 model[prerequisite](callback);
665 @todo Document the etupItem method.
667 setupItem: function (inSender, inEvent) {
668 var index = inEvent.index,
669 isSelected = inEvent.originator.isSelected(index),
670 collection = this.value,
671 model = collection.at(index),
672 actionIconButton = this.$.listItem.getActionIconButton(),
673 toggleSelected = this.getToggleSelected(),
674 actions = this.getActions(),
694 // It is possible in some cases where setupItem might
695 // be called, but the inEvent index is not a model
700 // set the overflow row to be hidden by default
701 this.$.overflow.setShowing(false);
704 // New ListItem Logic. I want to remove everything outside of this block
708 this.$.listItem.setSelected(inEvent.selected && this.toggleSelected);
709 if (this.$.listItem.decorated) {
710 this.$.listItem.setValue(model);
715 // END new ListItem Logic
718 isActive = model.getValue ? model.getValue("isActive") : true;
719 isNotActive = _.isBoolean(isActive) ? !isActive : false;
721 // Loop through all attribute container children and set content
722 for (prop in this.$) {
723 if (this.$.hasOwnProperty(prop)) {
725 isItalic = obj.classes === "italic";
728 isPlaceholder = false;
732 isHyperlink = toggleSelected;
734 if (obj.headerAttr) {
735 attr = obj.headerAttr;
736 prev = index ? this.getValue().models[index - 1] : false;
737 showd = !prev || model.getValue(attr) !== prev.getValue(attr);
738 this.$.header.canGenerate = showd;
739 // Still need to do some work here to get the list lines and header to display correctly
740 // this.$.header.applyStyle("border-top", showd ? "none" : null);
742 if (obj.footerAttr) {
743 attr = obj.footerAttr;
744 next = index ? this.getValue().models[index + 1] : false;
745 showd = !next || model.getValue(attr) !== next.getValue(attr);
746 this.$.footer.canGenerate = showd;
749 if (!_.isEmpty(obj.attr)) {
750 this.$.overflow.setShowing(true);
755 isPlaceholder = false;
756 attr = obj.getAttr();
757 value = model.getValue ? model.getValue(attr) : model.get(attr);
758 isNothing = _.isNull(value) || _.isUndefined(value) || value === "";
759 type = model.getType ? model.getType(attr) : "";
760 formatter = view.formatter;
763 // If the value is empty, and a placeholder is needed - show it
764 if (attr && (view.placeholder || view.showPlaceholder)) {
765 value = view.placeholder || this.getPlaceholderForAttr(attr);
766 isPlaceholder = true;
768 } else if (type === "Email") {
770 obj.ontap = "sendMail";
771 } else if (type === "Phone") {
773 obj.ontap = "callPhone";
774 } else if (type === "Url") {
776 obj.ontap = "sendUrl";
779 // Add or remove classes as needed for formatting
780 view.addRemoveClass("placeholder", isPlaceholder);
781 view.addRemoveClass("hyperlink", isHyperlink);
782 view.addRemoveClass("bold", isBold);
784 // If this column has a formatter specified - use it
786 value = this[formatter](value, view, model);
788 // Use type based formatter if applicable
789 } else if (!isPlaceholder && !isNothing &&
790 _.contains(this.formatted, type)) {
791 value = this["format" + type](value, view, model);
794 view.setContent(value);
800 this.$.listItem.addRemoveClass("inactive", isNotActive);
805 If the device has phone capability, this will dial
806 the phone number value.
808 callPhone: function (inSender, inEvent) {
809 this.sendUrl(inSender, inEvent, "tel://");
813 If the device has email capability, this bring up the
814 default email client with the current address as the "to:"
817 sendMail: function (inSender, inEvent) {
818 this.sendUrl(inSender, inEvent, "mailto:");
822 Opens a new window with the url provided, appended
823 with a prefix if provided.
825 sendUrl: function (inSender, inEvent, prefix) {
826 var model = this.getModel(inEvent.index),
827 url = model ? model.getValue(inSender.attr) : null,
831 win = window.open(prefix + url);
834 win = window.open(url, "_blank");
839 @todo Document the setQuery method.
842 setQuery: function () {
843 var old = _.clone(this.query);
844 this.inherited(arguments);
845 // Standard call doesn't do deep comparison
846 if (_.isEqual(old, this.query)) {
852 Add information onto the inEvent object of the list item menu
855 transformListAction: function (inSender, inEvent) {
856 var index = inEvent.index,
857 model = this.getValue().models[index];
859 if (!this.haveAllAnswers()) {
863 inEvent.model = model;
864 inEvent.actions = this.actions;
865 inEvent.actionPermissions = this._actionPermissions;
869 enyo.mixin(list, XV.FormattingMixin);