Merge pull request #1609 from xtuple/4_5_x
[xtuple] / lib / enyo-x / source / widgets / characteristics.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, XT:true, XM:true, XV:true, _:true */
5
6 (function () {
7
8   var TEXT = 0;
9   var LIST = 1;
10   var DATE = 2;
11
12   /**
13     @name XV.CharacteristicPicker
14     @class For the {@link XV.CharacteristicsWidget} displays the list of characteristics
15     that the user can select from to classify a {@link XV.CharacteristicItem}.
16     @extends XV.PickerWidget
17    */
18   enyo.kind(
19     /** @lends XV.CharacteristicPicker# */{
20     name: "XV.CharacteristicPicker",
21     kind: "XV.PickerWidget",
22     classes: "xv-characteristic-picker",
23     collection: "XM.characteristics",
24     noneText: "_delete".loc(),
25     noneClasses: "xv-negative",
26     orderBy: [
27       {attribute: 'order'},
28       {attribute: 'name'}
29     ]
30   });
31
32   /**
33     @name XV.OptionsPicker
34     @class Displays a list of items that can be selected.
35    */
36   enyo.kind(/** @lends XV.OptionsPicker# */{
37     name: "XV.OptionsPicker",
38     published: {
39       attr: null,
40       collection: null,
41       value: null,
42       disabled: false
43     },
44     events: {
45       onValueChange: ""
46     },
47     handlers: {
48       onSelect: "itemSelected"
49     },
50     components: [
51       {kind: "onyx.InputDecorator", classes: "xv-input-decorator",
52         components: [
53         {kind: "onyx.PickerDecorator",
54           components: [
55           {classes: "xv-picker"},
56           {name: "picker", kind: "onyx.Picker"}
57         ]}
58       ]}
59     ],
60     disabledChanged: function () {
61       // XXX Implementation appears slightly crazy because I can't find a disable
62       // setter for onyx.InputDecorator:
63       // http://enyojs.com/api/#onyx.InputDecorator::disabledChange
64       //
65       this.$.inputDecorator.disabled = this.disabled;
66       this.$.inputDecorator.bubble("onDisabledChange");
67     },
68     /**
69      @todo Document the itemSelected method.
70      */
71     itemSelected: function (inSender, inEvent) {
72       var value = this.$.picker.getSelected().content;
73       this.setValue(value);
74       return true;
75     },
76     /**
77      @todo Document the setCollection method.
78      */
79     setCollection: function (collection) {
80       var model,
81         value,
82         i,
83         c;
84       this.collection = collection;
85       collection.comparator = this.sort;
86       collection.sort();
87       this.$.picker.destroyClientControls();
88       for (i = 0; i < collection.length; i++) {
89         model = collection.at(i);
90         value = model.get('value');
91         c = this.$.picker.createComponent({ content: value });
92         // Autormatically select first
93         if (i === 0) { this.$.picker.setSelected(c); }
94       }
95       this.render();
96     },
97     /**
98      @todo Document the setValue method.
99      */
100     setValue: function (value, options) {
101       options = options || {};
102       var oldValue = this.getValue(),
103         inEvent,
104         components = this.$.picker.getComponents(),
105         component = _.find(components, function (c) {
106           if (c.kind === "onyx.MenuItem") {
107             return c.content === value;
108           }
109         });
110       if (!component) { value = null; }
111       this.$.picker.setSelected(component);
112
113       if (value !== oldValue) {
114         this.value = value;
115         if (!options.silent) {
116           inEvent = { originator: this, value: value };
117           this.doValueChange(inEvent);
118         }
119       }
120       return value;
121     },
122     /**
123      @todo Document the sort method.
124      */
125     sort: function (a, b) {
126       var aord = a.get('order'),
127         bord = b.get('order'),
128         aname,
129         bname;
130       if (aord === bord) {
131         aname = a.get('value');
132         bname = b.get('value');
133         return aname < bname ? -1 : 1;
134       }
135       return aord < bord ? -1 : 1;
136     }
137   });
138
139   /**
140     @name XV.CharacteristicItem
141     @class Contains a set of fittable columns which make up a single row in a list of characteristics
142       for the {@link XV.CharacteristicsWidget}.<br />
143     Components: {@link XV.CharacteristicPicker}, {@link XV.InputWidget},
144       {@link XV.DateWidget}, {@link XV.OptionsPicker}.<br />
145     Derived from <a href="http://enyojs.com/api/#enyo.FittableColumns">enyo.FittableColumns</a>.
146     @extends enyo.FittableColumns
147    */
148   enyo.kind(/** @lends XV.CharacteristicItem# */{
149     name: "XV.CharacteristicItem",
150     kind: "FittableColumns",
151     classes: "xv-characteristic-item",
152     published: {
153       value: null,
154       disabled: false
155     },
156     handlers: {
157       onValueChange: "controlValueChanged"
158     },
159     components: [
160       {kind: "XV.CharacteristicPicker", attr: "characteristic",
161         showLabel: false},
162       {kind: "XV.InputWidget", attr: "value", showLabel: false},
163       {kind: "XV.DateWidget", attr: "value", showLabel: false,
164         showing: false},
165       {kind: "XV.OptionsPicker", attr: "value", showLabel: false,
166         showing: false}
167     ],
168     disabledChanged: function (oldValue) {
169       this.$.characteristicPicker.setDisabled(this.disabled);
170       this.$.inputWidget.setDisabled(this.disabled);
171       this.$.dateWidget.setDisabled(this.disabled);
172       this.$.optionsPicker.setDisabled(this.disabled);
173     },
174     /**
175      @todo Document the controlValueChanged method.
176      */
177     controlValueChanged: function (inSender, inEvent) {
178       var attr = inSender.getAttr(),
179         value = inSender.getValue(),
180         attributes = {},
181         model = this.getValue(),
182         characteristic,
183         defaultVal;
184       attributes[attr] = _.isDate(value) ? value.toJSON() : value;
185       model.set(attributes);
186       if (attr === 'characteristic') {
187         if (value) {
188           characteristic = model.get('characteristic');
189           switch (characteristic.get('characteristicType'))
190           {
191           case DATE:
192             defaultVal = null;
193             break;
194           case TEXT:
195             defaultVal = "";
196             break;
197           case LIST:
198             defaultVal = characteristic.get('options').models[0].get('value');
199           }
200           model.set('value', defaultVal);
201           this.valueChanged();
202         } else {
203           model.destroy();
204         }
205       }
206       return true;
207     },
208     /**
209      @todo Document the getCharacteristicPicker method.
210      */
211     getCharacteristicPicker: function () {
212       return this.$.characteristicPicker;
213     },
214     /**
215      @todo Document the valueChanged method.
216      */
217     valueChanged: function () {
218       var model = this.getValue(),
219         characteristic = model.get('characteristic'),
220         type = characteristic ?
221           characteristic.get('characteristicType') : TEXT,
222         value = model.get('value'),
223         valueWidget,
224         options;
225       switch (type)
226       {
227       case TEXT:
228         this.$.dateWidget.hide();
229         this.$.optionsPicker.hide();
230         this.$.inputWidget.show();
231         valueWidget = this.$.inputWidget;
232         break;
233       case DATE:
234         this.$.optionsPicker.hide();
235         this.$.inputWidget.hide();
236         this.$.dateWidget.show();
237         valueWidget = this.$.dateWidget;
238         break;
239       case LIST:
240         this.$.dateWidget.hide();
241         this.$.inputWidget.hide();
242         this.$.optionsPicker.show();
243         valueWidget = this.$.optionsPicker;
244         options = characteristic.get('options');
245         this.$.optionsPicker.setCollection(options);
246       }
247       this.$.characteristicPicker.setValue(characteristic, {silent: true});
248       valueWidget.setValue(value, {silent: true});
249     }
250   });
251
252   /**
253     @name XV.CharacteristicsWidget
254     @class Use to implement a box for entering and viewing characteristics.
255     Made up of a header, a repeater (the control for making the list of characteristic items),
256       and fittable columns for the navigation buttons.<br />
257     Components: {@link XV.CharacteristicItem}.
258    */
259   enyo.kind(/** @lends XV.CharacteristicsWidget# */{
260     name: "XV.CharacteristicsWidget",
261     classes: "xv-characteristics-widget xv-input",
262     published: {
263       attr: null,
264       model: null,
265       value: null,
266       // note: which is now being kept track of in the model, and maybe should
267       // only be kept track of in the model.
268       which: null,
269       disabled: false
270     },
271     components: [
272       {kind: "onyx.GroupboxHeader", content: "_characteristics".loc()},
273       {kind: "Repeater", count: 0, onSetupItem: "setupItem", components: [
274         {kind: "XV.CharacteristicItem"}
275       ]},
276       {kind: "FittableColumns", classes: "xv-characteristic-buttons",
277         components: [
278         {kind: "onyx.Button", name: "newButton",
279           classes: "icon-plus xv-characteristic-button",
280           onclick: "newItem"}
281       ]}
282     ],
283     disabledChanged: function () {
284       this.$.newButton.setDisabled(this.disabled);
285     },
286     /**
287       Remove bindings
288      */
289     destroy: function () {
290       if (this.value) {
291         this.value.off('add', this.lengthChanged, this);
292         this.value.off('statusChange', this.lengthChanged, this);
293       }
294       this.inherited(arguments);
295     },
296     /**
297      Kick off the repeater.
298      */
299     lengthChanged: function () {
300       this.$.repeater.setCount(this.readyModels().length);
301     },
302     /**
303      Add a new model to the collection.
304      */
305     newItem: function () {
306       var Klass = XT.getObjectByName(this.getModel()),
307         model = new Klass(null, { isNew: true });
308       this.value.add(model);
309     },
310     /**
311       Returns an array of models in the collection whose status is ready.
312
313       @return {Array} models
314      */
315     readyModels: function () {
316       return _.filter(this.value.models, function (model) {
317         var status = model.getStatus(),
318           K = XM.Model;
319         // Avoiding bitwise because performance matters here
320         return (status === K.READY_CLEAN ||
321                 status === K.READY_DIRTY ||
322                 status === K.READY_NEW);
323       });
324     },
325     /**
326       Render the repeaterlist.
327      */
328     setupItem: function (inSender, inEvent) {
329       var item = inEvent.item.$.characteristicItem,
330         model = this.readyModels()[inEvent.index],
331         which = this.getWhich(),
332         picker = item.getCharacteristicPicker(),
333         filter = function (models) {
334           return _.filter(models, function (m) {
335             return m.get(which);
336           });
337         };
338       item.setValue(model);
339       item.setDisabled(this.disabled);
340       if (picker) {
341         // quote characteristics, notably, have no picker
342         picker.filter = filter;
343         picker.buildList();
344         if (!model.get('characteristic')) {
345           picker.select(1);
346         }
347       }
348       return true;
349     },
350     /**
351       Sort by status, then order, then name
352      */
353     sort: function (a, b) {
354       var astatus = a.isNew(),
355         bstatus = b.isNew(),
356         achar = a.get('characteristic'),
357         bchar = b.get('characteristic'),
358         aord = achar ? achar.get('order') : null,
359         bord = bchar ? bchar.get('order') : null,
360         aname,
361         bname;
362       if (astatus === bstatus) {
363         if (aord === bord) {
364           aname = achar ? achar.get('name') : null;
365           bname = bchar ? bchar.get('name') : null;
366           return aname < bname ? -1 : 1;
367         }
368         return aord < bord ? -1 : 1;
369       }
370       return astatus < bstatus ? -1 : 1;
371     },
372     /**
373       @param {XM.Collection} value
374      */
375     setValue: function (value) {
376       if (this.value) {
377         this.value.off('add', this.lengthChanged, this);
378         this.value.off('statusChange', this.lengthChanged, this);
379       }
380       this.value = value;
381       this.value.on('add', this.lengthChanged, this);
382       this.value.on('statusChange', this.lengthChanged, this);
383       this.value.comparator = this.sort;
384       this.value.sort();
385       this.lengthChanged();
386     }
387   });
388
389 }());