Merge pull request #1228 from shackbarth/jsdoc
[xtuple] / lib / enyo-x / source / widgets / parameter.js
1 /*jshint indent:2, curly:true, eqeqeq:true, immed:true, latedef:true,
2 newcap:true, noarg:true, regexp:true, undef:true, trailing:true,
3 white:true*/
4 /*global enyo:true, XM:true, XT:true, XV:true, _:true, console:true */
5
6 (function () {
7
8   /**
9     @name XV.ParameterItem
10     @class An input control for the Advanced Search feature
11     in which the user specifies one or more search parameters.<br />
12     Represents one search parameter.
13     A component of {@link XV.ParameterWidget}.
14    */
15   enyo.kind(
16     /** @lends XV.ParameterItem# */{
17     name: "XV.ParameterItem",
18     classes: "xv-parameter-item",
19     published: {
20       value: null,
21       label: "",
22       filterLabel: "",
23       attr: "",
24       operator: "",
25       isCharacteristic: false
26     },
27     events: {
28       onParameterChange: ""
29     },
30     handlers: {
31       onValueChange: "parameterChanged"
32     },
33     components: [
34       {name: "input", classes: "xv-parameter-item-input"}
35     ],
36     defaultKind: "XV.InputWidget",
37     /**
38      Sets up widget in parameter item.
39      */
40     create: function () {
41       this.inherited(arguments);
42       this.labelChanged();
43       if (!this.getOperator() && this.defaultKind === "XV.InputWidget") {
44         this.setOperator("MATCHES");
45       } else if (this.$.input instanceof XV.Picker) {
46         this.$.input.setNoneText("_any".loc());
47       }
48     },
49     /**
50      Sets the label value of the parameter item to that
51      specified in the kind definition.
52      */
53     labelChanged: function () {
54       this.$.input.setLabel(this.label);
55     },
56     /**
57      Returns the search parameter object.
58      */
59     getParameter: function () {
60       var param,
61         attr = this.getAttr(),
62         value = this.getValue();
63       if (attr && value !== undefined && value !== null && value !== "") {
64         param = {
65           attribute: attr,
66           operator: this.getOperator(),
67           isCharacteristic: this.getIsCharacteristic(),
68           value: value
69         };
70       }
71       return param;
72     },
73     /**
74      Returns the value of the parameter.
75      */
76     getValue: function () {
77       var value = this.$.input.getValue();
78       if (value && this.$.input.valueAttribute) {
79         value = value.get(this.$.input.valueAttribute);
80       }
81       return value;
82     },
83     /**
84      This stores the originating parameter item and it's value
85      in an event and bubbles up a parameter change to the
86      parent (Navigator).
87      */
88     parameterChanged: function () {
89       var inEvent = { value: this.getValue, originator: this };
90       this.doParameterChange(inEvent);
91       return true; // stop right here
92     },
93     setDisabled: function (disabled) {
94       if (this.$.input.setDisabled) {
95         return this.$.input.setDisabled(disabled);
96       }
97       return false;
98     },
99     /**
100      Sets the value of the parameter item.
101      */
102     setValue: function (value, options) {
103       this.$.input.setValue(value, options);
104     }
105
106   });
107
108   /**
109     @name XV.ParameterWidget
110     @class Contains a set of fittable rows to implement the Advanced Search feature.<br />
111     Each row is a {@link XV.ParameterItem} and represents a parameter on which
112     the user can narrow the search results.<br />
113     Derived from <a href="http://enyojs.com/api/#enyo.FittableRows">enyo.FittableRows</a>.
114     @extends enyo.FittableRows
115   */
116   enyo.kind(enyo.mixin(/** @lends XV.ParameterWidget# */{
117     name: "XV.ParameterWidget",
118     kind: "FittableRows",
119     classes: "xv-groupbox xv-parameter",
120     handlers: {
121       onItemSave: "saveItem",
122       onItemChange: "loadItem",
123       onParameterChange: "parameterChanged"
124     },
125     events: {
126       onParameterChange: ""
127     },
128     published: {
129       characteristicsRole: undefined,
130       showSaveFilter: true,
131       showLayout: false,
132       defaultFilter: null,
133       currentFilter: null,
134       currentLayout: null
135     },
136     defaultParameters: null,
137     defaultKind: "XV.ParameterItem",
138     isAllSetUp: false,
139     /**
140      Setup function for the parameter widget which adds the
141      item management forms to the top of the kind and the
142      characteristics to the bottom of the kind. Also loads
143      the last filter choice and sets the parameter values for
144      this choice.
145      */
146     create: function () {
147       var role = this.getCharacteristicsRole(),
148         K = XM.Characteristic,
149         that = this,
150         chars,
151         hash = {},
152         compArray = [];
153       this.inherited(arguments);
154       this.processExtensions();
155       this.isAllSetUp = true;
156
157       if (role) {
158         hash[role] = true;
159         if (XM.characteristics.where(hash).length) {
160           // Header
161           this.createComponent({
162             kind: "onyx.GroupboxHeader",
163             content: "_characteristics".loc()
164           });
165
166           // Process text and list
167           chars = XM.characteristics.filter(function (char) {
168             var type = char.get('characteristicType');
169             return char.get(role) &&
170               char.get('isSearchable') &&
171               (type === K.TEXT || type === K.LIST);
172           });
173
174           _.each(chars, function (char) {
175             var kind;
176             hash = {
177               name: char.get('name') + "Characteristic",
178               label: char.get('name'),
179               isCharacteristic: true,
180               attr:  char.get('name')
181             };
182             if (char.get('characteristicType') === K.LIST) {
183               kind = enyo.kind({
184                 kind: "XV.PickerWidget",
185                 idAttribute: "value",
186                 nameAttribute: "value",
187                 create: function () {
188                   this.inherited(arguments);
189                   this.buildList();
190                 },
191                 filteredList: function () {
192                   return char.get('options').models;
193                 },
194                 getValue: function () {
195                   return this.value ? this.value.get('value') : null;
196                 }
197               });
198               hash.defaultKind = kind;
199             }
200             that.createComponent(hash);
201           });
202
203           // Process dates
204           chars = XM.characteristics.filter(function (char) {
205             var type = char.get('characteristicType');
206             return char.get(role) &&
207               char.get('isSearchable') &&
208               (type === K.DATE);
209           });
210
211           _.each(chars, function (char) {
212             that.createComponent({
213               kind: "onyx.GroupboxHeader",
214               content: char.get('name').loc()
215             });
216
217             hash = {
218               name: char.get('name') + "FromCharacteristic",
219               label: "_from".loc(),
220               filterLabel: char.get('name') + " " + "_from".loc(),
221               operator: ">=",
222               attr:  char.get('name'),
223               isCharacteristic: true,
224               defaultKind: "XV.DateWidget"
225             };
226             that.createComponent(hash);
227
228             hash = {
229               name: char.get('name') + "ToCharacteristic",
230               label: "_to".loc(),
231               filterLabel: char.get('name') + " " + "_to".loc(),
232               operator: "<=",
233               attr:  char.get('name'),
234               isCharacteristic: true,
235               defaultKind: "XV.DateWidget"
236             };
237             that.createComponent(hash);
238           });
239         }
240       }
241       // Sets published defaultFilter with the params object
242       // from the defaultParameters in the parameter widget (if exists)
243       this.setDefaultFilter(_.result(this, 'defaultParameters'));
244
245       // The "null" value of addBefore will add this
246       // component to the beginning of the array.
247       if (this.getShowSaveFilter()) {
248         compArray.push({kind: "XV.FilterForm", name: "filterForm", addBefore: null});
249       }
250
251       if (this.getShowLayout()) {
252         compArray.push({kind: "XV.LayoutForm", name: "layoutForm", addBefore: null});
253       }
254
255       this.createComponents(compArray, {owner: this});
256       this.populateFromUserPref();
257       this._init = true;
258     },
259     /**
260       Sends list object from the parent kind to the layout form
261
262       @param {Object} index
263     */
264     buildColumnList: function (list) {
265       this.$.layoutForm.addColumns(list);
266     },
267     /**
268       When the default filter is set, populate the
269       params with the values.
270     */
271     defaultFilterChanged: function () {
272       // if defaults are null, this does nothing
273       this.populateParameters(this.getDefaultFilter());
274     },
275     /**
276      Returns an array of the parameter search objects.
277      */
278     getParameters: function () {
279       var i,
280         param,
281         params = [],
282         child;
283       for (i = 0; i < this.children.length; i++) {
284         child = this.children[i];
285         param = child && child.showing && child.getParameter ?
286           child.getParameter() : null;
287         if (param) {
288           // using union instead of push here in case param is an array of params
289           params = _.union(params, param);
290         }
291       }
292       return params;
293     },
294     /**
295       Retrieves parameter values. By default returns values as human readable
296       strings. Boolean options are:<br />
297         &#42; name - If true returns the parameter item control name, otherwise returns the label.<br />
298         &#42; value - If true returns the control value, other wise returns a human readable string.<br />
299         &#42; deltaDate - If true returns as string for the date difference for date widgets. (i.e. "+5").
300       @param {Object} options
301      */
302     getSelectedValues: function (options) {
303       options = options || {};
304       var values = {},
305         componentName,
306         component,
307         value,
308         label,
309         control,
310         date,
311         today,
312         days;
313
314       for (componentName in this.$) {
315         if (this.$[componentName] instanceof XV.ParameterItem &&
316             this.$[componentName].showing &&
317             this.$.hasOwnProperty(componentName)) {
318           component = this.$[componentName];
319           value = component.getValue();
320           label = options.name ?
321             component.getName() :
322             component.getFilterLabel() || component.getLabel();
323           control = component.$.input;
324           // Special handling for toggle buttons that work opposite,
325           // meaning only show me when it's "off"
326           if (component.$.input.kind === 'XV.ToggleButtonWidget') {
327             if (options.value) {
328               values[label] = value;
329             } else if (value === false) {
330               values[label] = control.getValueToString ? control.getValueToString() : value;
331             }
332           } else if (value !== undefined && value !== null && value !== "") {
333             if (options.deltaDate &&
334                 component.$.input.kind === 'XV.DateWidget') {
335               today = XT.date.today();
336               date = XT.date.applyTimezoneOffset(control.getValue(), true);
337               days = XT.date.daysBetween(date, today);
338               days = days < 0 ? "" + days : "+" + days;
339               values[label] = days;
340             } else if (options.value) {
341               values[label] = value instanceof XM.Model ? value.id : value;
342             } else {
343               values[label] = control.getValueToString ?
344                 control.getValueToString() : value;
345             }
346           }
347         }
348       }
349       return values;
350     },
351     // implementation is up to subkinds or monkeypatches
352     parameterChanged: function (inSender, inEvent) {
353     },
354     /**
355       Receives the event from the item form with the user-
356       entered values and combines these with the other item values
357       for a save.
358       If the item name already exists for this type, it performs an edit
359       instead of a new insert.
360     */
361     saveItem: function (inSender, inEvent) {
362       var Klass = XT.getObjectByName("XM.Filter"),
363         model = inEvent.model,
364         options = {}, attrs = {},
365         params = {},
366         success,
367         kind = this.kind;
368
369       if (inSender.itemType === "filter") {
370         params = JSON.stringify(this.getSelectedValues({
371           name: true,
372           value: true,
373           deltaDate: true
374         }));
375       } else if (inSender.itemType === "layout") {
376         params = JSON.stringify(this.$.layoutForm.getColumnValues());
377       }
378
379       if (!model) {
380         // there is not already a model
381         model = new Klass(null, {isNew: true});
382       }
383       // set the values from the save event and save
384       model.set("name", inEvent.itemName);
385       model.set("shared", inEvent.isShared);
386       model.set("params", params);
387       model.set("kind", kind);
388       model.set("type", inSender.itemType);
389
390       options.success = function (model, resp, options) {
391         // dear item form, we're all done here!
392         inEvent.callback(model);
393       };
394       if (model.isDirty()) {
395         model.save(null, options);
396       } else {
397         options.success(model);
398       }
399     },
400     /**
401       Gets the model from the change
402       event and populates the fields with parameter
403       values from the model.
404     */
405     loadItem: function (inSender, inEvent) {
406       var model = inEvent.model,
407         params = model ? model.get("params") : null,
408         type = model ? model.get("type") : "filter";
409
410       if (type === "filter") {
411         this.setCurrentFilter(model);
412         // clear out the existing filters before populating this one
413         this.clearParameters();
414         // if there are no parameters, take the defaults
415         params = params ? params : this.getDefaultFilter();
416         // populate the parameters with filter or defaults
417         this.populateParameters(params);
418       } else if (type === "layout") {
419         this.setCurrentLayout(model);
420         this.populateLayout(params);
421       }
422       this.saveToUserPref();
423     },
424     /**
425       Clear all of the currently loaded parameters and bubble
426       parameter change events.
427     */
428     clearParameters: function () {
429       _.each(this.$, function (item) {
430         if (item instanceof XV.ParameterItem && item.showing) {
431           item.$.input.clear({silent: true});
432         }
433       });
434       if (this._init) { this.doParameterChange(); }
435     },
436     /**
437       Loops through the values in the params object
438       and sets the value on attribute fields in the
439       layout tree. Bubbles up a change event after
440       all of the fields are set.
441
442       @param {Object|String} params
443     */
444     populateLayout: function (params) {
445       params = _.isObject(params) ? params : JSON.parse(params);
446       this.$.layoutForm.loadColumns(params);
447     },
448     /**
449       Loops through the values in the params object
450       and sets the value on the parameter fields and
451       bubbles up the change events.
452
453       @param {String} params
454     */
455     populateParameters: function (params) {
456       params = _.isObject(params) ? params : JSON.parse(params);
457       _.each(params, function (value, key) {
458         var param = this.$[key];
459         if (param) {
460           param.setValue(value, {silent: true});
461         }
462       }, this);
463
464       if (this._init) { this.doParameterChange(); }
465     },
466     /**
467       Reads the last filter value from the user preferences
468       and populates the filter picker with this value.
469      */
470     populateFromUserPref: function () {
471       var lastPref,
472         kind = this.kind,
473         lastFilter = {},
474         lastLayout = {},
475         lastSort;
476
477       // we have a cache of preferences, so find the last
478       // selected preferences for this kind
479       lastPref = XT.DataSource.getUserPreference(kind);
480
481       // if there is a last preference for this kind,
482       // and it isn't null, set the pickers.
483       if (lastPref && lastPref !== "null") {
484         lastPref = JSON.parse(lastPref);
485         lastFilter = lastPref.filter || {};
486         lastLayout = lastPref.layout || {};
487         if (this.getShowSaveFilter()) {
488           this.$.filterForm.$.itemPicker.setValue(lastFilter.uuid);
489         }
490         if (this.getShowLayout()) {
491           this.$.layoutForm.$.itemPicker.setValue(lastLayout.uuid);
492         }
493       }
494     },
495     /**
496       Saves the last selected filter and layout to the user
497       preference table.
498     */
499     saveToUserPref: function () {
500       var payload,
501         operation,
502         kind = this.kind,
503         params = {},
504         filterModel = this.getCurrentFilter(),
505         layoutModel = this.getCurrentLayout();
506
507       // setup the parms object with the filter and layout
508       params.filter = filterModel || {};
509       params.layout = layoutModel || {};
510       payload = JSON.stringify(params);
511
512       // save the last selected filter and layout to the user preference
513       operation = XT.DataSource.getUserPreference(kind) ? "replace" : "add";
514       XT.DataSource.saveUserPreference(kind, payload, operation);
515       // save this item to the preference cache
516       XT.session.preferences.set(kind, payload);
517     },
518     /**
519       Accepts an array of items to push into the parameter items. Matches item name
520       to component under the assumption that there will be a component by the
521       name of the incoming name (which itself is the name of the model attribute).
522       Fails silently if it cannot find the name.
523
524       @param {Array} items
525       @param {Array|String} [item.name] A string *or an array of strings* with the name or attr of
526         the attribute to be updated. In the case of an array, this function will set
527         any component that matches any of the names, and ignore the rest.
528       @param {Object|String|Number} [item.value] The payload of the setValue to the ParameterItem.
529       @param {Boolean} [item.showing]  Set to false to completely hide and disable the parameter.
530      */
531     setParameterItemValues: function (items) {
532       var that = this,
533         setValueOnMatch = function (name, item) {
534           var control = that.$[name] || _.find(that.$, function (ctl) {
535             // look up controls by name or by the model attribute they map to
536             return ctl.attr === name;
537           });
538           if (control) {
539             if (item.showing !== false) {
540               control.setValue(item.value, {silent: true});
541             }
542             control.setShowing(item.showing !== false);
543           }
544         };
545
546       _.each(items, function (item) {
547         if (typeof item.name === 'string') {
548           // string case. set the component by this name
549           setValueOnMatch(item.name, item);
550         } else if (typeof item.name === 'object') {
551           // array case. loop through item.name and set any matches
552           _.each(item.name, function (subname) {
553             setValueOnMatch(subname, item);
554           });
555         }
556       });
557     }
558   }, XV.ExtensionsMixin));
559
560   /**
561     Generalized form for supporting saved filters, layouts, and sorts.
562     This component expects a picker for selecting
563     saved items, a save drawer for saving the current item,
564     and a manage drawer for sharing and deleting items.
565
566     TODO: Move common components here from Filter and Layout forms
567   */
568   enyo.kind(/** @lends XV.UserItemForm# */{
569     name: "XV.UserItemForm",
570     classes: "xv-filter-form",
571     events: {
572       onItemSave: "",
573       onItemChange: ""
574     },
575     handlers: {
576       onValueChange: "valueChanged",
577       onListChange: "listChanged"
578     },
579     itemType: "",
580     components: [],
581     /**
582       Opens the save drawer and switches the drawer icons.
583     */
584     activateSave: function (inSender, inEvent) {
585       this.$.saveDrawer.setOpen(!this.$.saveDrawer.open);
586       this.$.saveTopDrawer.changeIcon(this.$.saveDrawer.open);
587     },
588     /**
589       Opens the manage filter drawer and switches the
590       drawer icons.
591     */
592     activateManage: function (inSender, inEvent) {
593       this.$.filterList.reset();
594       this.$.manageDrawer.setOpen(!this.$.manageDrawer.open);
595       this.$.manageTopDrawer.changeIcon(this.$.manageDrawer.open);
596     },
597     /**
598       This is called by the save/apply button in the
599       save drawer. It checks for an existing filter and
600       shows the warning popup or continues to the save.
601     */
602     checkExisting: function (inSender, inEvent) {
603       var name = this.$.itemName.getValue() ?
604         this.$.itemName.getValue().trim() : "",
605         exists = this.itemExists(name);
606       if (exists) {
607         this.$.existingPopup.show();
608       } else {
609         this.saveItem(inSender, inEvent);
610       }
611     },
612     /**
613       There was a change to the list and a model
614       was added or removed. Refresh list and picker.
615     */
616     listChanged: function (inSender, inEvent) {
617       inEvent = inEvent || {};
618       this.$.itemPicker.collectionChanged();
619       // a model was deleted
620       if (inEvent.delete && inEvent.model === this.$.itemPicker.getValue()) {
621         this.$.itemPicker.setValue(null);
622       }
623       this.$.filterList.fetched();
624     },
625     /**
626       Sets the default items for the picker and the
627       list of items that can be managed.
628     */
629     create: function () {
630       this.inherited(arguments);
631       this.$.apply.setDisabled(!this.validate());
632
633       // filter the picker
634       var filter = function (models, options) {
635         var filtered = _.filter(models, function (model) {
636           var kind = this.parent.parent.kind === model.get("kind"),
637           permission = model.get("shared") ||
638             model.get("createdBy") === XM.currentUser.get("username"),
639           type = this.parent.itemType === model.get("type") ||
640             (this.parent.itemType === "filter" && !model.get("type"));
641           if (kind && permission && type) {
642             return true;
643           }
644         }, this);
645         return filtered;
646       };
647       this.$.itemPicker.filter = filter;
648       this.$.itemPicker.collectionChanged();
649
650       // filters the manage list
651       var query =  this.$.filterList.getQuery() || {};
652
653       query.parameters = [];
654       query.parameters = [{
655         attribute: "createdBy",
656         operator: "=",
657         value: XT.session.details.username
658       },
659       {
660         attribute: "kind",
661         operator: "=",
662         value: this.parent.kind
663       },
664       {
665         attribute: "type",
666         operator: "=",
667         value: this.itemType,
668         includeNull: this.itemType === "filter" // handles filters added before type was created
669       }];
670       this.$.filterList.setQuery(query);
671     },
672     /**
673       Looks at the list of items for the current user and determines
674       an item exists by name.
675
676       @param {String} name
677       @returns {Boolean}
678     */
679     itemExists: function (name) {
680       var exists = _.find(this.$.filterList.getValue().models,
681         function (model) {
682           var value = model.get("name");
683           return value.toLowerCase() === name.toLowerCase();
684         });
685       return exists;
686     },
687     hidePopup: function (inSender, inEvent) {
688       this.$.existingPopup.hide();
689     },
690     /**
691       This is called by the save/apply button in the
692       save drawer. It bubbles up the event to the parameter
693       widget with a callback so it knows when the save was
694       successful. It then closes the drawer and refreshes
695       the collections.
696     */
697     saveItem: function (inSender, inEvent) {
698       var name = this.$.itemName.getValue() ?
699         this.$.itemName.getValue().trim() : "",
700         shared = this.$.isShared.getValue(),
701         exists = this.itemExists(name),
702         that = this;
703
704       inEvent = {model: exists, itemName: name, isShared: shared,
705         callback: function (model) {
706           if (!exists) {
707             that.$.filterList.getValue().add(model);
708           }
709           that.activateSave(); // close the save drawer
710           that.$.existingPopup.hide(); // hide the popup if it was showing
711           that.$.itemPicker.setValue(model);
712           that.$.filterList.doListChange();
713         }};
714       this.doItemSave(inEvent);
715     },
716     /**
717       Checks that all of the form fields have
718       been completed.
719     */
720     validate: function () {
721       if (this.$.itemName.getValue()) {
722         return true;
723       }
724       return false;
725     },
726     /**
727       When an item is selected from the picker, the
728       form fields are populated and a changed
729       event is bubbled up to the parameter widget.
730     */
731     valueChanged: function (inSender, inEvent) {
732       this.$.apply.setDisabled(!this.validate());
733       if (inSender.kind === "XV.FilterPicker") {
734         // get values from inEvent for form load
735         var value = inEvent.originator.value,
736           name = value ? value.get("name") : null,
737           shared = value ? value.get("shared") : false;
738         this.$.itemName.setValue(name);
739         this.$.isShared.setValue(shared);
740         inEvent = {model: value};
741         this.doItemChange(inEvent);
742       }
743       return true;
744     }
745   });
746
747   /**
748     User Item Form for selecting saved filters, a save drawer for saving
749     the current filter, and a manage drawer for sharing and deleting filters.
750   */
751   enyo.kind(
752     /** @lends XV.FilterForm# */{
753     name: "XV.FilterForm",
754     kind: "XV.UserItemForm",
755     itemType: "filter",
756     components: [
757       {kind: "onyx.GroupboxHeader", content: "_filters".loc()},
758       {kind: "XV.FilterPicker", name: "itemPicker", label: "_filter".loc()},
759       {kind: "XV.TopDrawer", name: "saveTopDrawer", ontap: "activateSave", content: "_saveFilter".loc()},
760       {kind: "onyx.Drawer", name: "saveDrawer", open: false, animated: true, components: [
761         {kind: "onyx.GroupboxHeader", content: "_saveFilter".loc()},
762         {kind: "XV.InputWidget", name: "itemName", label: "_filterName".loc()},
763         {kind: "XV.CheckboxWidget", name: "isShared", attr: "isShared", label: "_isShared".loc()},
764         {kind: "FittableColumns", classes: "button-row", components: [
765           {kind: "onyx.Button", name: "apply", disabled: true, content: "_apply".loc(), ontap: "checkExisting"},
766           {kind: "onyx.Button", content: "_cancel".loc(), ontap: "activateSave"}
767         ]}
768       ]},
769       {kind: "XV.TopDrawer", name: "manageTopDrawer", ontap: "activateManage", content: "_manageFilters".loc()},
770       {kind: "onyx.Drawer", name: "manageDrawer", open: false, animated: true, components: [
771         {kind: "onyx.GroupboxHeader", content: "_manageFilters".loc()},
772         {kind: "XV.FilterList", allowFilter: false},
773         {kind: "FittableColumns", classes: "button-row", components: [
774           {kind: "onyx.Button", content: "_done".loc(), ontap: "activateManage"}
775         ]}
776       ]},
777       {kind: "onyx.Popup", name: "existingPopup", centered: true,
778         modal: true, floating: true, scrim: true, components: [
779         {content: "_filterExists".loc()},
780         {content: "_editFilter?".loc()},
781         {tag: "br"},
782         {kind: "onyx.Button", content: "_edit".loc(), ontap: "saveItem",
783           classes: "onyx-blue xv-popup-button"},
784         {kind: "onyx.Button", content: "_cancel".loc(), ontap: "hidePopup",
785           classes: "xv-popup-button"}
786       ]}
787     ]
788   });
789
790   /**
791     User Item Form for selecting saved layouts, a save drawer for saving
792     the current layout, and a manage drawer for sharing and deleting layouts.
793   */
794   enyo.kind(/** @lends XV.LayoutForm# */{
795     name: "XV.LayoutForm",
796     kind: "XV.UserItemForm",
797     events: {
798       onColumnsChange: "",
799       onItemChange: "",
800       onItemSave: ""
801     },
802     itemType: "layout",
803     components: [
804       {kind: "onyx.GroupboxHeader", content: "_layout".loc()},
805       {kind: "XV.FilterPicker", name: "itemPicker", label: "_layout".loc()},
806       {kind: "XV.TopDrawer", name: "layoutTopDrawer", ontap: "activateLayout", content: "_changeLayout".loc()},
807       {kind: "onyx.Drawer", name: "columnsDrawer", open: false, animated: true, components: [
808         {kind: "XV.LayoutTree", name: "layoutTree"}
809       ]},
810       {kind: "XV.TopDrawer", name: "saveTopDrawer", ontap: "activateSave", content: "_saveLayout".loc()},
811       {kind: "onyx.Drawer", name: "saveDrawer", open: false, animated: true, components: [
812         {kind: "onyx.GroupboxHeader", content: "_saveLayout".loc()},
813         {kind: "XV.InputWidget", name: "itemName", label: "_layoutName".loc()},
814         {kind: "XV.CheckboxWidget", name: "isShared", attr: "isShared", label: "_isShared".loc()},
815         {kind: "FittableColumns", classes: "button-row", components: [
816           {kind: "onyx.Button", name: "apply", disabled: true, content: "_apply".loc(), ontap: "checkExisting"},
817           {kind: "onyx.Button", content: "_cancel".loc(), ontap: "activateSave"}
818         ]}
819       ]},
820       {kind: "XV.TopDrawer", name: "manageTopDrawer", ontap: "activateManage", content: "_manageLayouts".loc()},
821       {kind: "onyx.Drawer", name: "manageDrawer", open: false, animated: true, components: [
822         {kind: "onyx.GroupboxHeader", content: "_manageLayouts".loc()},
823         {kind: "XV.FilterList", allowFilter: false},
824         {kind: "FittableColumns", classes: "button-row", components: [
825           {kind: "onyx.Button", content: "_done".loc(), ontap: "activateManage"}
826         ]}
827       ]},
828       {kind: "onyx.Popup", name: "existingPopup", centered: true,
829         modal: true, floating: true, scrim: true, components: [
830         {content: "_layoutExists".loc()},
831         {content: "_editLayout?".loc()},
832         {tag: "br"},
833         {kind: "onyx.Button", content: "_edit".loc(), ontap: "saveItem",
834           classes: "onyx-blue xv-popup-button"},
835         {kind: "onyx.Button", content: "_cancel".loc(), ontap: "hidePopup",
836           classes: "xv-popup-button"}
837       ]}
838     ],
839     /**
840       Opens the change layout drawer and switches the drawer icons.
841     */
842     activateLayout: function (inSender, inEvent) {
843       this.$.columnsDrawer.setOpen(!this.$.columnsDrawer.open);
844       this.$.layoutTopDrawer.changeIcon(this.$.columnsDrawer.open);
845     },
846     /**
847       Builds a tree of nodes based on the current list layout. For each current
848       list attribute, a picker is shown with the set of available attributes. If
849       a column is already shown, it is selected, otherwise it will show as "none."
850
851       @param {Object} currentLayout
852     */
853     addColumns: function (currentLayout) {
854       var i, attributes,
855         matchSelections = function (sel) { return sel.attr === attributes[i]; },
856         value,
857         disabled,
858         label,
859         component;
860       // set the layout tree with the list of possible display attributes
861       this.$.layoutTree.setListAttrs(currentLayout.getDisplayAttributes());
862       // clear out the tree before building it
863       this.$.layoutTree.$.tree.destroyClientControls();
864       this.$.layoutTree.createTree(currentLayout);
865       this.$.columnsDrawer.render();
866     },
867     getColumnValues: function () {
868       var params = {};
869       _.each(this.$.layoutTree.$, function (leaf) {
870         if (leaf.order !== undefined && leaf.order !== null) {
871           params[leaf.order] = leaf.getAttr();
872         }
873       });
874       return params;
875     },
876     loadColumns: function (params) {
877       _.each(this.$.layoutTree.$, function (leaf) {
878         if (leaf.order !== undefined && leaf.order !== null &&
879           leaf.getAttr() !== params[leaf.order]) {
880           leaf.setAttr(params[leaf.order]);
881           var inEvent = {
882             order: leaf.order,
883             value: params[leaf.order]
884           };
885           this.doColumnsChange(inEvent);
886         }
887       }, this);
888     },
889     /**
890       When an attribute is selected from the picker,
891       a change event is bubbled up to the parameter widget.
892     */
893     valueChanged: function (inSender, inEvent) {
894       this.inherited(arguments);
895       if (inEvent.originator.kind === "XV.AttributePicker") {
896         inEvent = {
897           order: inEvent.originator.owner.order,
898           value: inEvent.value
899         };
900         this.doColumnsChange(inEvent);
901       }
902     }
903   });
904
905   /**
906     Simple FittableColumns that contains the open/close icon
907     and some text. When the ontap event is fired, this control
908     is responsible for switching icons and opening a drawer.
909   */
910   enyo.kind(
911     /** @lends XV.TopDrawer */{
912     name: "XV.TopDrawer",
913     kind: "FittableColumns",
914     classes: "top-drawer",
915     published: {
916       content: ""
917     },
918     components: [
919       {tag: "i", classes: "icon-plus-sign-alt", name: "drawerIcon"},
920       {name: "label"}
921     ],
922     /**
923       Switch icons depending on open state of a drawer
924
925       @param {Boolean} drawerOpen
926     */
927     changeIcon: function (drawerOpen) {
928       if (drawerOpen) {
929         this.$.drawerIcon.setClasses("icon-minus-sign-alt");
930       } else {
931         this.$.drawerIcon.setClasses("icon-plus-sign-alt");
932       }
933     },
934     contentChanged: function () {
935       this.$.label.setContent(this.getContent());
936     },
937     create: function () {
938       this.inherited(arguments);
939       this.contentChanged();
940     }
941   });
942
943 }());