Merge pull request #1220 from shackbarth/22390-email
[xtuple] / lib / enyo-x / source / views / list.js
1 /*jshint bitwise:true, indent:2, curly:true, eqeqeq:true, immed:true,
2 latedef:true, newcap:true, noarg:true, regexp:true, undef:true,
3 trailing:true, white:true, strict:false*/
4 /*global XT:true, XM:true, XV:true, _:true, enyo:true, window: true*/
5
6 (function () {
7
8   var ROWS_PER_FETCH = 50;
9   var FETCH_TRIGGER = 100;
10
11   /**
12     @name XV.List
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.
21     @extends enyo.List
22    */
23   var list = /** @lends XV.List# */{
24     name: "XV.List",
25     kind: "List",
26     classes: "xv-list",
27     fixedHeight: true,
28     /**
29      * Published fields
30      * @type {Object}
31      *
32      * @property {Boolean} canAddNew
33      *
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
41         verify if the user
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
49      */
50     published: {
51       label: "",
52       collection: null,
53       filterDescription: "",
54       exportActions: null,
55       navigatorActions: null,
56       isMore: true,
57       parameterWidget: null,
58       canAddNew: true,
59       allowPrint: false,
60       // this property signifies that the list allows
61       // searches via the input or parameter widget
62       allowFilter: true,
63       showDeleteAction: true,
64       newActions: null,
65
66       /**
67        * @see XM.View#list.actions
68        */
69       actions: null,
70
71
72       /**
73        * @see XM.View#list.query
74        */
75       query: null,
76
77       /**
78        * The backing model for this component.
79        * @see XM.EnyoView#model
80        */
81       value: null,
82     },
83     events: {
84       onExportList: "",
85       onItemTap: "",
86       onPrintList: "",
87       onPrintSelectList: "",
88       onReportList: "",
89       onSelectionChanged: "",
90       onWorkspace: "",
91       onNotify: ""
92     },
93     handlers: {
94       onActionSelected: "actionSelected",
95       onModelChange: "modelChanged",
96       onListItemMenuTap: "transformListAction",
97       onSetupItem: "setupItem",
98       onChange: "selectionChanged",
99     },
100     /**
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.
103     */
104     actionSelected: function (inSender, inEvent) {
105       this[inEvent.action.method](inEvent);
106     },
107     /**
108      @todo Document the collectionChanged method.
109      */
110     collectionChanged: function () {
111       var collection = this.getCollection(),
112         Klass = collection ? XT.getObjectByName(collection) : false;
113
114       if (Klass) {
115         this.setValue(new Klass());
116       } else {
117         this.setValue(null);
118       }
119     },
120     /**
121      @todo Document the create method.
122      */
123     create: function () {
124       var actions = this.getActions() || [],
125         deleteAction = _.findWhere(actions, {"name": "delete"}),
126         collection,
127         Klass,
128         method;
129       this._actionPermissions = [];
130       this._haveAllAnswers = [];
131
132       this.inherited(arguments);
133       XM.View.setPresenter(this, "list");
134
135       this.collectionChanged();
136       this.queryChanged();
137
138       collection = this.getValue();
139       Klass = collection ? collection.model : false;
140       method = Klass && Klass.prototype.couldDestroy ? "couldDestroy" : "canDestroy";
141
142       // add the delete action if it's not already there
143       if (!deleteAction && this.getShowDeleteAction()) {
144         actions.push({
145           name: "delete",
146           prerequisite: method,
147           notifyMessage: "_confirmDelete".loc() + " " + "_confirmAction".loc(),
148           method: "deleteItem",
149           isViewMethod: true
150         });
151       }
152       this.setActions(actions);
153
154       this._selectedIndexes = [];
155       this.createOverflowRow();
156     },
157
158     /**
159       Creates an additional hidden row which
160       can be used if more space is needed for
161       columns added during layout changes.
162
163
164       XXX yes, this is incompatible with View templates. so ListItems
165       that want to use overflow rows cannot yet use View templates.
166     */
167     createOverflowRow: function () {
168       var columns = [],
169         row = this.createComponent({
170           kind: "XV.ListItem",
171           name: "overflow",
172           owner: this
173         });
174
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") {
179           columns.push(
180             {kind: "XV.ListColumn", classes: col.classes, components: [
181               {kind: "XV.ListAttr", allowLayout: true, overflow: true,
182                 showPlaceholder: true}
183             ]}
184           );
185         }
186       });
187       row.createComponents(columns, {owner: this});
188     },
189
190     /**
191      @todo Document the getModel method.
192      */
193     getModel: function (index) {
194       return this.getValue().models[index];
195     },
196
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;
203
204       // Handle default navigator actions
205       if (!exportAction) {
206         actions.push({name: "export"});
207       }
208       if (!printAction) {
209         actions.push({name: "printList"});
210         actions.push({name: "printSelect"});
211         actions.push({name: "reportList"});
212       }
213       this.exportActions = actions;
214       return actions;
215     },
216     getSortActions: function () {
217       var actions = this.sortActions || [],
218         sortAction =  _.findWhere(actions, {"name": "sort"});
219
220       // Handle default navigator actions
221       if (!sortAction) {
222         actions.push({name: "sort"});
223       }
224       this.sortActions = actions;
225       return actions;
226     },
227     deleteItem: function (inEvent) {
228       var collection = this.getValue(),
229         imodel = inEvent.model,
230         model = imodel,
231         fetchOptions = {},
232         that = this,
233         attrs = {},
234         Klass;
235
236       if (imodel instanceof XM.Info) {
237         Klass = XT.getObjectByName(model.editableModel);
238         attrs[Klass.prototype.idAttribute] = model.id;
239         model = Klass.findOrCreate(attrs);
240       }
241
242       fetchOptions.success = function () {
243         var destroyOptions = {};
244         destroyOptions.success = function () {
245           collection.remove(imodel);
246           that.fetched();
247           if (inEvent.done) {
248             inEvent.done();
249           }
250         };
251         destroyOptions.error = function (model, err) {
252           XT.log("error destroying model from list", JSON.stringify(err));
253         };
254         model.destroy(destroyOptions);
255       };
256       model.fetch(fetchOptions);
257     },
258     /**
259      Returns an array of attributes from the model that can be used
260       in a text-based search.
261      */
262     getSearchableAttributes: function () {
263       var model = this.getValue().model;
264       return model.getSearchableAttributes ? model.getSearchableAttributes() : [];
265     },
266     /**
267       Returns a list of List Attribute kinds that are currently displayed
268         in the list.
269     */
270     getCurrentListAttributes: function () {
271       return _.map(_.filter(this.$, function (item) {
272           return item.kind === "XV.ListAttr";
273         }), function (item) {
274           return item.attr;
275         });
276     },
277     /**
278       Returns a list of available attributes that can be used for sorting
279         the list or adding as columns.
280     */
281     getDisplayAttributes: function () {
282       var attributes,
283         relations,
284         model,
285         relationModels;
286
287       model = this.getValue().model;
288       // strip out the ids
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";
294       });
295
296       // Return an array of the model names of the relation models
297       relationModels =
298         _.map(_.filter(relations, function (rel) {
299           // Filter attributes for just relations
300           return _.contains(attributes, rel.key);
301         }), function (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;
307           });
308           return mapping;
309         });
310
311       attributes = _.difference(attributes, _.map(relations, function (rel) {
312         return rel.key;
313       }));
314
315       // Loop through the relation models and concatenate the arrays of
316       // attribute names.
317       _.each(relationModels, function () {
318         // combine the relation arrays
319         attributes = attributes.concat(_.flatten(relationModels));
320       });
321
322       return _.uniq(attributes);
323     },
324    /**
325     @todo Document the getWorkspace method.
326     */
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);
332     },
333     export: function () {
334       this.doExportList();
335     },
336     /**
337      @todo Document the fetch method.
338      */
339     fetch: function (options) {
340       var that = this,
341         query = this.getQuery() || {},
342         success;
343
344       options = options ? _.clone(options) : {};
345       options.showMore = _.isBoolean(options.showMore) ?
346         options.showMore : false;
347       success = options.success;
348
349       // Lazy Loading
350       if (options.showMore) {
351         query.rowOffset += ROWS_PER_FETCH;
352         options.update = true;
353         options.add = true;
354         options.remove = false;
355       } else {
356         query.rowOffset = 0;
357         query.rowLimit = ROWS_PER_FETCH;
358       }
359
360       _.extend(options, {
361         success: function (resp, status, xhr) {
362           that.fetched();
363           if (success) { success(resp, status, xhr); }
364         },
365         query: query
366       });
367       this.getValue().fetch(options);
368     },
369     /**
370      @todo Document the fetched method.
371      */
372     fetched: function () {
373       var query = this.getQuery() || {},
374         offset = query.rowOffset || 0,
375         limit = query.rowLimit || 0,
376         count = this.getValue().length,
377         isMore = limit ?
378           (offset + limit <= count) && (this.getCount() !== count) : false,
379         rowsPerPage;
380       this.isMore = isMore;
381       this.fetching = false;
382
383       // Reset the size of the list
384       this.setCount(count);
385
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);
391       }
392       if (offset) {
393         this.refresh();
394       } else {
395         this.reset();
396       }
397       this._maxTop = this.getScrollBounds().maxTop;
398     },
399     /**
400       If the attribute is equal to or exists as a child
401       attribute, then it is returned.
402     */
403     findNameInAttr: function (attr, name) {
404       return _.find(attr.split("."), function (s) {
405         return s === name;
406       });
407     },
408     /**
409       Returns a placeholder translation string to be
410       used as a placeholder if one is not specified.
411
412       @param {String} attr
413       @returns {String}
414     */
415     getPlaceholderForAttr: function (attr) {
416       var attrs, str;
417       if (attr.indexOf(".") !== -1) {
418         attrs = attr.split(".");
419         str = ("_" + attrs[0]).loc() + " " + ("_" + attrs[1]).loc();
420       } else {
421         str = ("_" + attr).loc();
422       }
423       return "_no".loc() + " " + str;
424     },
425     /**
426       Returns whether all actions on the list have been determined
427       to be available or not.
428
429       @param {Number} index
430       @returns {Boolean}
431     */
432     haveAllAnswers: function () {
433       if (this._haveAllAnswers) { return true; }
434       var that = this,
435         permissions = that._actionPermissions,
436         ret;
437       if (_.isEmpty(permissions)) { return false; }
438       ret = _.reduce(this.getActions(), function (memo, action) {
439         return memo && _.isBoolean(permissions[action.name]);
440       }, true);
441       if (ret) { this._haveAllAnswers = true; }
442       return ret;
443     },
444
445     /**
446      * @listens onItemTap
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
451      */
452     itemTap: function (inSender, inEvent) {
453       if (!this.getToggleSelected() || inEvent.originator.isKey) {
454         this.doItemTap({ model: this.getModel(inEvent.index) });
455         return true;
456       }
457     },
458     /**
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.
462      */
463     modelChanged: function (inSender, inEvent) {
464       var that = this,
465         value = this.getValue(),
466         workspace = this.getWorkspace(),
467         model,
468         Klass = XT.getObjectByName(this.getCollection()),
469         checkStatusCollection,
470         checkStatusParameter,
471         checkStatusQuery;
472
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);
479
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
483         // means it doesn't.
484
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);
490         } else {
491           checkStatusQuery.parameters = [checkStatusParameter];
492         }
493
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
499             if (model) {
500               value.remove(model);
501             }
502
503             if (collection.size() > 0) {
504               // this model should still be in the collection. Refresh it.
505               value.add(collection.at(0), {silent: true});
506             }
507             if (value.comparator) { value.sort(); }
508             if (that.getCount() !== value.length) {
509               that.setCount(value.length);
510             }
511             that.refresh();
512             if (inEvent.done) {
513               inEvent.done();
514             }
515           },
516           error: function () {
517             XT.log("Error checking model status in list");
518           }
519         });
520       }
521     },
522     reportList: function () {
523       this.doReportList();
524     },
525     printSelect: function () {
526       this.doPrintSelectList();
527     },
528     printList: function () {
529       this.doPrintList();
530     },
531     /**
532       Makes sure the collection can handle the sort order
533       defined in the query.
534     */
535     queryChanged: function () {
536       var query = this.getQuery(),
537         value = this.getValue();
538       if (value && query && query.orderBy) {
539         value.comparator = function (a, b) {
540           var aval,
541             bval,
542             attr,
543             numeric,
544             i,
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;
553             if (aval !== bval) {
554               return aval > bval ? 1 : -1;
555             }
556           }
557           return 0;
558         };
559       }
560     },
561     /**
562       Reset actions permission checks will be regenerated.
563
564       @param {Number} Index
565     */
566     resetActions: function () {
567       this._actionPermissions = {};
568       this._haveAllAnswers = undefined;
569     },
570      /**
571       Manages lazy loading of items in the list.
572       */
573     scroll: function () {
574       var r = this.inherited(arguments),
575         options = {},
576         max;
577       if (!this._maxTop) { return r; }
578
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;
584         this.fetch(options);
585       }
586
587       return r;
588     },
589     /**
590       Helper fuction that returns an array of indexes based on
591       the current selection.
592
593       @returns {Array}
594     */
595     selectedIndexes: function () {
596       return _.keys(this.getSelection().selected);
597     },
598     /**
599      * Re-evaluates actions menu.
600      */
601     selectionChanged: function (inSender, inEvent) {
602       var keys = this.selectedIndexes(),
603         index = inEvent.index,
604         collection = this.value,
605         actions = this.actions,
606         that = this;
607
608       this.resetActions();
609
610       // Loop through each action
611       _.each(actions, function (action) {
612         var prerequisite = action.prerequisite,
613           permissions = that._actionPermissions,
614           name = action.name,
615           len = keys.length,
616           counter = 0,
617           model,
618           idx,
619           callback,
620           i;
621
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) {
625
626           // If some other model failed, forget about it
627           if (permissions[name] === false) { return; }
628
629           // If even one selected model fails, then we can't do the action
630           if (response) {
631             counter++;
632
633             // If we haven't heard back from all requests yet, wait for the next
634             if (counter < len) {
635               return;
636             }
637           }
638           permissions[name] = response;
639
640           // Handle asynchronous result re-rendering
641           if (that.haveAllAnswers()) {
642             that.waterfallDown("onListMenuReady");
643             that.renderRow(index);
644           }
645         };
646
647         if (prerequisite) {
648           // Loop through each selection model and check pre-requisite
649           for (i = 0; i < keys.length; i++) {
650             idx = keys[i] - 0;
651             model = collection.at(idx);
652             if (model instanceof XM.Info && !model[prerequisite]) {
653               XT.getObjectByName(model.editableModel)[prerequisite](model, callback);
654             } else {
655               model[prerequisite](callback);
656             }
657           }
658         } else {
659           callback(true);
660         }
661
662       });
663     },
664      /**
665       @todo Document the etupItem method.
666       */
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(),
675         isActive,
676         isNotActive,
677         isNothing,
678         isItalic,
679         isHyperlink,
680         isBold,
681         isPlaceholder,
682         prev,
683         next,
684         prop,
685         obj,
686         view,
687         value,
688         formatter,
689         attr,
690         showd,
691         ary,
692         type;
693
694       // It is possible in some cases where setupItem might
695       // be called, but the inEvent index is not a model
696       if (!model) {
697         return true;
698       }
699
700       // set the overflow row to be hidden by default
701       this.$.overflow.setShowing(false);
702
703       // ======
704       // New ListItem Logic. I want to remove everything outside of this block
705       // ASAP.
706       // {
707
708       this.$.listItem.setSelected(inEvent.selected && this.toggleSelected);
709       if (this.$.listItem.decorated) {
710         this.$.listItem.setValue(model);
711         return true;
712       }
713
714       // }
715       // END new ListItem Logic
716       // ======
717
718       isActive = model.getValue ? model.getValue("isActive") : true;
719       isNotActive = _.isBoolean(isActive) ? !isActive : false;
720
721       // Loop through all attribute container children and set content
722       for (prop in this.$) {
723         if (this.$.hasOwnProperty(prop)) {
724           obj = this.$[prop];
725           isItalic = obj.classes === "italic";
726           isHyperlink = false;
727           isBold = false;
728           isPlaceholder = false;
729
730           if (obj.isKey) {
731             isBold = true;
732             isHyperlink = toggleSelected;
733           }
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);
741           }
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;
747           }
748           if (obj.overflow) {
749             if (!_.isEmpty(obj.attr)) {
750               this.$.overflow.setShowing(true);
751             }
752           }
753           if (obj.getAttr) {
754             view = obj;
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;
761
762             if (!value) {
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;
767               }
768             } else if (type === "Email") {
769               isHyperlink = true;
770               obj.ontap = "sendMail";
771             } else if (type === "Phone") {
772               isHyperlink = true;
773               obj.ontap = "callPhone";
774             } else if (type === "Url") {
775               isHyperlink = true;
776               obj.ontap = "sendUrl";
777             }
778
779             // Add or remove classes as needed for formatting
780             view.addRemoveClass("placeholder", isPlaceholder);
781             view.addRemoveClass("hyperlink", isHyperlink);
782             view.addRemoveClass("bold", isBold);
783
784             // If this column has a formatter specified - use it
785             if (formatter) {
786               value = this[formatter](value, view, model);
787
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);
792             }
793
794             view.setContent(value);
795           }
796         }
797       }
798
799       // Inactive
800       this.$.listItem.addRemoveClass("inactive", isNotActive);
801
802       return true;
803     },
804     /**
805       If the device has phone capability, this will dial
806       the phone number value.
807     */
808     callPhone: function (inSender, inEvent) {
809       this.sendUrl(inSender, inEvent, "tel://");
810       return true;
811     },
812     /**
813       If the device has email capability, this bring up the
814       default email client with the current address as the "to:"
815       value.
816     */
817     sendMail: function (inSender, inEvent) {
818       this.sendUrl(inSender, inEvent, "mailto:");
819       return true;
820     },
821     /**
822       Opens a new window with the url provided, appended
823       with a prefix if provided.
824     */
825     sendUrl: function (inSender, inEvent, prefix) {
826       var model = this.getModel(inEvent.index),
827         url = model ? model.getValue(inSender.attr) : null,
828         win;
829       if (url) {
830         if (prefix) {
831           win = window.open(prefix + url);
832           win.close();
833         } else {
834           win = window.open(url, "_blank");
835         }
836       }
837     },
838    /**
839     @todo Document the setQuery method.
840     */
841     /*
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)) {
847         this.queryChanged();
848       }
849     },
850     */
851     /**
852       Add information onto the inEvent object of the list item menu
853       as it flies by
854      */
855     transformListAction: function (inSender, inEvent) {
856       var index = inEvent.index,
857         model = this.getValue().models[index];
858
859       if (!this.haveAllAnswers()) {
860         return true;
861       }
862
863       inEvent.model = model;
864       inEvent.actions = this.actions;
865       inEvent.actionPermissions = this._actionPermissions;
866     }
867   };
868
869   enyo.mixin(list, XV.FormattingMixin);
870   enyo.kind(list);
871
872 }());