996247f3bfb72aaf804436ee3d9071169446a973
[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.PickerWidget) {
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-parameter-panel", // mixin class from workspace to pullout
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           if (component.$.input.kind === 'XV.CheckboxWidget') {
325             if (options.value) {
326               values[label] = value;
327             } else if (value === true) {
328               values[label] = control.getValueToString ? control.getValueToString() : value;
329             }
330
331           // Special handling for toggle buttons that work opposite,
332           // meaning only show me when it's "off"
333           } else if (component.$.input.kind === 'XV.ToggleButtonWidget') {
334             if (options.value) {
335               values[label] = value;
336             } else if (value === false) {
337               values[label] = control.getValueToString ? control.getValueToString() : value;
338             }
339           } else if (value !== undefined && value !== null && value !== "") {
340             if (options.deltaDate &&
341                 component.$.input.kind === 'XV.DateWidget') {
342               today = XT.date.today();
343               date = XT.date.applyTimezoneOffset(control.getValue(), true);
344               days = XT.date.daysBetween(date, today);
345               days = days < 0 ? "" + days : "+" + days;
346               values[label] = days;
347             } else if (options.value) {
348               values[label] = value instanceof XM.Model ? value.id : value;
349             } else {
350               values[label] = control.getValueToString ?
351                 control.getValueToString() : value;
352             }
353           }
354         }
355       }
356       return values;
357     },
358     // implementation is up to subkinds or monkeypatches
359     parameterChanged: function (inSender, inEvent) {
360     },
361     /**
362       Receives the event from the item form with the user-
363       entered values and combines these with the other item values
364       for a save.
365       If the item name already exists for this type, it performs an edit
366       instead of a new insert.
367     */
368     saveItem: function (inSender, inEvent) {
369       var Klass = XT.getObjectByName("XM.Filter"),
370         model = inEvent.model,
371         options = {}, attrs = {},
372         params = {},
373         success,
374         kind = this.kind;
375
376       if (inSender.itemType === "filter") {
377         params = JSON.stringify(this.getSelectedValues({
378           name: true,
379           value: true,
380           deltaDate: true
381         }));
382       } else if (inSender.itemType === "layout") {
383         params = JSON.stringify(this.$.layoutForm.getColumnValues());
384       }
385
386       if (!model) {
387         // there is not already a model
388         model = new Klass(null, {isNew: true});
389       }
390       // set the values from the save event and save
391       model.set("name", inEvent.itemName);
392       model.set("shared", inEvent.isShared);
393       model.set("params", params);
394       model.set("kind", kind);
395       model.set("type", inSender.itemType);
396
397       options.success = function (model, resp, options) {
398         // dear item form, we're all done here!
399         inEvent.callback(model);
400       };
401       if (model.isDirty()) {
402         model.save(null, options);
403       } else {
404         options.success(model);
405       }
406     },
407     /**
408       Gets the model from the change
409       event and populates the fields with parameter
410       values from the model.
411     */
412     loadItem: function (inSender, inEvent) {
413       var model = inEvent.model,
414         params = model ? model.get("params") : null,
415         type = model ? model.get("type") : "filter";
416
417       if (type === "filter") {
418         this.setCurrentFilter(model);
419         // clear out the existing filters before populating this one
420         this.clearParameters();
421         // if there are no parameters, take the defaults
422         params = params ? params : this.getDefaultFilter();
423         // populate the parameters with filter or defaults
424         this.populateParameters(params);
425       } else if (type === "layout") {
426         this.setCurrentLayout(model);
427         this.populateLayout(params);
428       }
429       this.saveToUserPref();
430     },
431     /**
432       Clear all of the currently loaded parameters and bubble
433       parameter change events.
434     */
435     clearParameters: function () {
436       _.each(this.$, function (item) {
437         if (item instanceof XV.ParameterItem && item.showing) {
438           item.$.input.clear({silent: true});
439         }
440       });
441       if (this._init) { this.doParameterChange(); }
442     },
443     /**
444       Loops through the values in the params object
445       and sets the value on attribute fields in the
446       layout tree. Bubbles up a change event after
447       all of the fields are set.
448
449       @param {Object|String} params
450     */
451     populateLayout: function (params) {
452       params = _.isObject(params) ? params : JSON.parse(params);
453       this.$.layoutForm.loadColumns(params);
454     },
455     /**
456       Loops through the values in the params object
457       and sets the value on the parameter fields and
458       bubbles up the change events.
459
460       @param {String} params
461     */
462     populateParameters: function (params) {
463       params = _.isObject(params) ? params : JSON.parse(params);
464       _.each(params, function (value, key) {
465         var param = this.$[key];
466         if (param) {
467           param.setValue(value, {silent: true});
468         }
469       }, this);
470
471       if (this._init) { this.doParameterChange(); }
472     },
473     /**
474       Reads the last filter value from the user preferences
475       and populates the filter picker with this value.
476      */
477     populateFromUserPref: function () {
478       var lastPref,
479         kind = this.kind,
480         lastFilter = {},
481         lastLayout = {},
482         lastSort;
483
484       // we have a cache of preferences, so find the last
485       // selected preferences for this kind
486       lastPref = XT.DataSource.getUserPreference(kind);
487
488       // if there is a last preference for this kind,
489       // and it isn't null, set the pickers.
490       if (lastPref && lastPref !== "null") {
491         lastPref = JSON.parse(lastPref);
492         lastFilter = lastPref.filter || {};
493         lastLayout = lastPref.layout || {};
494         if (this.getShowSaveFilter()) {
495           this.$.filterForm.$.itemPicker.setValue(lastFilter.uuid);
496         }
497         if (this.getShowLayout()) {
498           this.$.layoutForm.$.itemPicker.setValue(lastLayout.uuid);
499         }
500       }
501     },
502     /**
503       Saves the last selected filter and layout to the user
504       preference table.
505     */
506     saveToUserPref: function () {
507       var payload,
508         operation,
509         kind = this.kind,
510         params = {},
511         filterModel = this.getCurrentFilter(),
512         layoutModel = this.getCurrentLayout();
513
514       // setup the parms object with the filter and layout
515       params.filter = filterModel || {};
516       params.layout = layoutModel || {};
517       payload = JSON.stringify(params);
518
519       // save the last selected filter and layout to the user preference
520       operation = XT.DataSource.getUserPreference(kind) ? "replace" : "add";
521       XT.DataSource.saveUserPreference(kind, payload, operation);
522       // save this item to the preference cache
523       XT.session.preferences.set(kind, payload);
524     },
525     /**
526       Accepts an array of items to push into the parameter items. Matches item name
527       to component under the assumption that there will be a component by the
528       name of the incoming name (which itself is the name of the model attribute).
529       Fails silently if it cannot find the name.
530
531       @param {Array} items
532       @param {Array|String} [item.name] A string *or an array of strings* with the name or attr of
533         the attribute to be updated. In the case of an array, this function will set
534         any component that matches any of the names, and ignore the rest.
535       @param {Object|String|Number} [item.value] The payload of the setValue to the ParameterItem.
536       @param {Boolean} [item.showing]  Set to false to completely hide and disable the parameter.
537      */
538     setParameterItemValues: function (items) {
539       var that = this,
540         setValueOnMatch = function (name, item) {
541           var control = that.$[name] || _.find(that.$, function (ctl) {
542             // look up controls by name or by the model attribute they map to
543             return ctl.attr === name;
544           });
545           if (control) {
546             if (item.showing !== false) {
547               control.setValue(item.value, {silent: true});
548             }
549             control.setShowing(item.showing !== false);
550           }
551         };
552
553       _.each(items, function (item) {
554         if (typeof item.name === 'string') {
555           // string case. set the component by this name
556           setValueOnMatch(item.name, item);
557         } else if (typeof item.name === 'object') {
558           // array case. loop through item.name and set any matches
559           _.each(item.name, function (subname) {
560             setValueOnMatch(subname, item);
561           });
562         }
563       });
564     }
565   }, XV.ExtensionsMixin));
566
567   /**
568     Generalized form for supporting saved filters, layouts, and sorts.
569     This component expects a picker for selecting
570     saved items, a save drawer for saving the current item,
571     and a manage drawer for sharing and deleting items.
572
573     TODO: Move common components here from Filter and Layout forms
574   */
575   enyo.kind(/** @lends XV.UserItemForm# */{
576     name: "XV.UserItemForm",
577     classes: "xv-filter-form",
578     events: {
579       onItemSave: "",
580       onItemChange: ""
581     },
582     handlers: {
583       onValueChange: "valueChanged",
584       onListChange: "listChanged"
585     },
586     itemType: "",
587     components: [],
588     /**
589       Opens the save drawer and switches the drawer icons.
590     */
591     activateSave: function (inSender, inEvent) {
592       this.$.saveDrawer.setOpen(!this.$.saveDrawer.open);
593       this.$.saveTopDrawer.changeIcon(this.$.saveDrawer.open);
594     },
595     /**
596       Opens the manage filter drawer and switches the
597       drawer icons.
598     */
599     activateManage: function (inSender, inEvent) {
600       this.$.filterList.reset();
601       this.$.manageDrawer.setOpen(!this.$.manageDrawer.open);
602       this.$.manageTopDrawer.changeIcon(this.$.manageDrawer.open);
603     },
604     /**
605       This is called by the save/apply button in the
606       save drawer. It checks for an existing filter and
607       shows the warning popup or continues to the save.
608     */
609     checkExisting: function (inSender, inEvent) {
610       var name = this.$.itemName.getValue() ?
611         this.$.itemName.getValue().trim() : "",
612         exists = this.itemExists(name);
613       if (exists) {
614         this.$.existingPopup.show();
615       } else {
616         this.saveItem(inSender, inEvent);
617       }
618     },
619     /**
620       There was a change to the list and a model
621       was added or removed. Refresh list and picker.
622     */
623     listChanged: function (inSender, inEvent) {
624       inEvent = inEvent || {};
625       this.$.itemPicker.collectionChanged();
626       // a model was deleted
627       if (inEvent.delete && inEvent.model === this.$.itemPicker.getValue()) {
628         this.$.itemPicker.setValue(null);
629       }
630       this.$.filterList.fetched();
631     },
632     /**
633       Sets the default items for the picker and the
634       list of items that can be managed.
635     */
636     create: function () {
637       this.inherited(arguments);
638       this.$.apply.setDisabled(!this.validate());
639
640       // filter the picker
641       var filter = function (models, options) {
642         var filtered = _.filter(models, function (model) {
643           var kind = this.parent.parent.kind === model.get("kind"),
644           permission = model.get("shared") ||
645             model.get("createdBy") === XM.currentUser.get("username"),
646           type = this.parent.itemType === model.get("type") ||
647             (this.parent.itemType === "filter" && !model.get("type"));
648           if (kind && permission && type) {
649             return true;
650           }
651         }, this);
652         return filtered;
653       };
654       this.$.itemPicker.filter = filter;
655       this.$.itemPicker.collectionChanged();
656
657       // filters the manage list
658       var query =  this.$.filterList.getQuery() || {};
659
660       query.parameters = [];
661       query.parameters = [{
662         attribute: "createdBy",
663         operator: "=",
664         value: XT.session.details.username
665       },
666       {
667         attribute: "kind",
668         operator: "=",
669         value: this.parent.kind
670       },
671       {
672         attribute: "type",
673         operator: "=",
674         value: this.itemType,
675         includeNull: this.itemType === "filter" // handles filters added before type was created
676       }];
677       this.$.filterList.setQuery(query);
678     },
679     /**
680       Looks at the list of items for the current user and determines
681       an item exists by name.
682
683       @param {String} name
684       @returns {Boolean}
685     */
686     itemExists: function (name) {
687       var exists = _.find(this.$.filterList.getValue().models,
688         function (model) {
689           var value = model.get("name");
690           return value.toLowerCase() === name.toLowerCase();
691         });
692       return exists;
693     },
694     hidePopup: function (inSender, inEvent) {
695       this.$.existingPopup.hide();
696     },
697     /**
698       This is called by the save/apply button in the
699       save drawer. It bubbles up the event to the parameter
700       widget with a callback so it knows when the save was
701       successful. It then closes the drawer and refreshes
702       the collections.
703     */
704     saveItem: function (inSender, inEvent) {
705       var name = this.$.itemName.getValue() ?
706         this.$.itemName.getValue().trim() : "",
707         shared = this.$.isShared.getValue(),
708         exists = this.itemExists(name),
709         that = this;
710
711       inEvent = {model: exists, itemName: name, isShared: shared,
712         callback: function (model) {
713           if (!exists) {
714             that.$.filterList.getValue().add(model);
715           }
716           that.activateSave(); // close the save drawer
717           that.$.existingPopup.hide(); // hide the popup if it was showing
718           that.$.itemPicker.setValue(model);
719           that.$.filterList.doListChange();
720         }};
721       this.doItemSave(inEvent);
722     },
723     /**
724       Checks that all of the form fields have
725       been completed.
726     */
727     validate: function () {
728       if (this.$.itemName.getValue()) {
729         return true;
730       }
731       return false;
732     },
733     /**
734       When an item is selected from the picker, the
735       form fields are populated and a changed
736       event is bubbled up to the parameter widget.
737     */
738     valueChanged: function (inSender, inEvent) {
739       this.$.apply.setDisabled(!this.validate());
740       if (inSender.kind === "XV.FilterPicker") {
741         // get values from inEvent for form load
742         var value = inEvent.originator.value,
743           name = value ? value.get("name") : null,
744           shared = value ? value.get("shared") : false;
745         this.$.itemName.setValue(name);
746         this.$.isShared.setValue(shared);
747         inEvent = {model: value};
748         this.doItemChange(inEvent);
749       }
750       return true;
751     }
752   });
753
754   /**
755     User Item Form for selecting saved filters, a save drawer for saving
756     the current filter, and a manage drawer for sharing and deleting filters.
757   */
758   enyo.kind(
759     /** @lends XV.FilterForm# */{
760     name: "XV.FilterForm",
761     kind: "XV.UserItemForm",
762     itemType: "filter",
763     components: [
764       {kind: "onyx.GroupboxHeader", content: "_filters".loc()},
765       {kind: "XV.FilterPicker", name: "itemPicker", label: "_filter".loc()},
766       {kind: "XV.TopDrawer", name: "saveTopDrawer", ontap: "activateSave", content: "_saveFilter".loc()},
767       {kind: "onyx.Drawer", name: "saveDrawer", open: false, animated: true, components: [
768         {kind: "onyx.GroupboxHeader", content: "_saveFilter".loc()},
769         {kind: "XV.InputWidget", name: "itemName", label: "_filterName".loc()},
770         {kind: "XV.CheckboxWidget", name: "isShared", attr: "isShared", label: "_isShared".loc()},
771         {kind: "FittableColumns", classes: "xv-buttons", components: [
772           {kind: "onyx.Button", name: "apply", classes: "icon-ok", disabled: true, ontap: "checkExisting"},
773           {kind: "onyx.Button", name: "cancel", classes: "icon-remove", ontap: "activateSave"}
774         ]}
775       ]},
776       {kind: "XV.TopDrawer", name: "manageTopDrawer", ontap: "activateManage", content: "_manageFilters".loc()},
777       {kind: "onyx.Drawer", name: "manageDrawer", open: false, animated: true, components: [
778         {kind: "onyx.GroupboxHeader", content: "_manageFilters".loc()},
779         {kind: "XV.FilterList", allowFilter: false},
780         {kind: "FittableColumns", classes: "xv-buttons", components: [
781           {kind: "onyx.Button", name: "done", classes: "icon-ok", ontap: "activateManage"}
782         ]}
783       ]},
784       {kind: "onyx.Popup", name: "existingPopup", centered: true,
785         modal: true, floating: true, scrim: true, components: [
786         {content: "_filterExists".loc()},
787         {content: "_editFilter?".loc()},
788         {classes: "xv-buttons", components: [
789           {kind: "onyx.Button", ontap: "saveItem", classes: "selected icon-ok"},
790           {kind: "onyx.Button", ontap: "hidePopup", classes: "icon-remove"}
791         ]}
792       ]}
793     ]
794   });
795
796   /**
797     User Item Form for selecting saved layouts, a save drawer for saving
798     the current layout, and a manage drawer for sharing and deleting layouts.
799   */
800   enyo.kind(/** @lends XV.LayoutForm# */{
801     name: "XV.LayoutForm",
802     kind: "XV.UserItemForm",
803     events: {
804       onColumnsChange: "",
805       onItemChange: "",
806       onItemSave: ""
807     },
808     itemType: "layout",
809     components: [
810       {kind: "onyx.GroupboxHeader", content: "_layout".loc()},
811       {kind: "XV.FilterPicker", name: "itemPicker", label: "_layout".loc()},
812       {kind: "XV.TopDrawer", name: "layoutTopDrawer", ontap: "activateLayout", content: "_changeLayout".loc()},
813       {kind: "onyx.Drawer", name: "columnsDrawer", open: false, animated: true, components: [
814         {kind: "XV.LayoutTree", name: "layoutTree"}
815       ]},
816       {kind: "XV.TopDrawer", name: "saveTopDrawer", ontap: "activateSave", content: "_saveLayout".loc()},
817       {kind: "onyx.Drawer", name: "saveDrawer", open: false, animated: true, components: [
818         {kind: "onyx.GroupboxHeader", content: "_saveLayout".loc()},
819         {kind: "XV.InputWidget", name: "itemName", label: "_layoutName".loc()},
820         {kind: "XV.CheckboxWidget", name: "isShared", attr: "isShared", label: "_isShared".loc()},
821         {kind: "FittableColumns", classes: "button-row", components: [
822           {kind: "onyx.Button", name: "apply", disabled: true, content: "_apply".loc(), ontap: "checkExisting"},
823           {kind: "onyx.Button", content: "_cancel".loc(), ontap: "activateSave"}
824         ]}
825       ]},
826       {kind: "XV.TopDrawer", name: "manageTopDrawer", ontap: "activateManage", content: "_manageLayouts".loc()},
827       {kind: "onyx.Drawer", name: "manageDrawer", open: false, animated: true, components: [
828         {kind: "onyx.GroupboxHeader", content: "_manageLayouts".loc()},
829         {kind: "XV.FilterList", allowFilter: false},
830         {kind: "FittableColumns", classes: "button-row", components: [
831           {kind: "onyx.Button", content: "_done".loc(), ontap: "activateManage"}
832         ]}
833       ]},
834       {kind: "onyx.Popup", name: "existingPopup", centered: true,
835         modal: true, floating: true, scrim: true, components: [
836         {content: "_layoutExists".loc()},
837         {content: "_editLayout?".loc()},
838         {tag: "br"},
839         {kind: "onyx.Button", content: "_edit".loc(), ontap: "saveItem",
840           classes: "onyx-blue xv-popup-button"},
841         {kind: "onyx.Button", content: "_cancel".loc(), ontap: "hidePopup",
842           classes: "xv-popup-button"}
843       ]}
844     ],
845     /**
846       Opens the change layout drawer and switches the drawer icons.
847     */
848     activateLayout: function (inSender, inEvent) {
849       this.$.columnsDrawer.setOpen(!this.$.columnsDrawer.open);
850       this.$.layoutTopDrawer.changeIcon(this.$.columnsDrawer.open);
851     },
852     /**
853       Builds a tree of nodes based on the current list layout. For each current
854       list attribute, a picker is shown with the set of available attributes. If
855       a column is already shown, it is selected, otherwise it will show as "none."
856
857       @param {Object} currentLayout
858     */
859     addColumns: function (currentLayout) {
860       var i, attributes,
861         matchSelections = function (sel) { return sel.attr === attributes[i]; },
862         value,
863         disabled,
864         label,
865         component;
866       // set the layout tree with the list of possible display attributes
867       this.$.layoutTree.setListAttrs(currentLayout.getDisplayAttributes());
868       // clear out the tree before building it
869       this.$.layoutTree.$.tree.destroyClientControls();
870       this.$.layoutTree.createTree(currentLayout);
871       this.$.columnsDrawer.render();
872     },
873     getColumnValues: function () {
874       var params = {};
875       _.each(this.$.layoutTree.$, function (leaf) {
876         if (leaf.order !== undefined && leaf.order !== null) {
877           params[leaf.order] = leaf.getAttr();
878         }
879       });
880       return params;
881     },
882     loadColumns: function (params) {
883       _.each(this.$.layoutTree.$, function (leaf) {
884         if (leaf.order !== undefined && leaf.order !== null &&
885           leaf.getAttr() !== params[leaf.order]) {
886           leaf.setAttr(params[leaf.order]);
887           var inEvent = {
888             order: leaf.order,
889             value: params[leaf.order]
890           };
891           this.doColumnsChange(inEvent);
892         }
893       }, this);
894     },
895     /**
896       When an attribute is selected from the picker,
897       a change event is bubbled up to the parameter widget.
898     */
899     valueChanged: function (inSender, inEvent) {
900       this.inherited(arguments);
901       if (inEvent.originator.kind === "XV.AttributePicker") {
902         inEvent = {
903           order: inEvent.originator.owner.order,
904           value: inEvent.value
905         };
906         this.doColumnsChange(inEvent);
907       }
908     }
909   });
910
911   /**
912     Simple FittableColumns that contains the open/close icon
913     and some text. When the ontap event is fired, this control
914     is responsible for switching icons and opening a drawer.
915   */
916   enyo.kind(
917     /** @lends XV.TopDrawer */{
918     name: "XV.TopDrawer",
919     kind: "FittableColumns",
920     classes: "onyx-groupbox-header",
921     published: {
922       content: ""
923     },
924     components: [
925       {tag: "i", classes: "icon-plus-sign-alt", name: "drawerIcon"},
926       {name: "label"}
927     ],
928     /**
929       Switch icons depending on open state of a drawer
930
931       @param {Boolean} drawerOpen
932     */
933     changeIcon: function (drawerOpen) {
934       if (drawerOpen) {
935         this.$.drawerIcon.setClasses("icon-minus-sign-alt");
936       } else {
937         this.$.drawerIcon.setClasses("icon-plus-sign-alt");
938       }
939     },
940     contentChanged: function () {
941       this.$.label.setContent(this.getContent());
942     },
943     create: function () {
944       this.inherited(arguments);
945       this.contentChanged();
946     }
947   });
948
949 }());