1 /*jshint indent:2, curly:true, eqeqeq:true, immed:true, latedef:true, newcap:true, noarg:true,
2 regexp:true, undef:true, trailing:true, white:true, strict:false */
3 /*global XT:true, Backbone:true, enyo:true, _:true */
9 @class A picker control that implements a dropdown list of items which can be selected.<br />
10 Unlike the {@link XV.RelationWidget}, the collection is stored local to the widget.<br />
11 The superkind of {@link XV.CharacteristicPicker}.<br />
13 Accepts a single attribute mapping or an object attribute mapping where the object requires
14 two properties "colleciton" and "value." This technique can be used to bind the picker
15 to a collection on a local model in addition to the selected value.
17 Derived from <a href="http://enyojs.com/api/#enyo.Control">enyo.Control</a>.
21 /** @lends XV.PickerWidget# */{
22 name: "XV.PickerWidget",
24 classes: "xv-pickerwidget",
25 events: /** @lends XV.PickerWidget# */{
27 @property {Object} inEvent The payload that's attached to bubbled-up events
28 @property {XV.PickerWidget} inEvent.originator This
29 @property inEvent.value The value passed up is the key of the object and not the object itself
38 nameAttribute: "name",
40 noneText: "_none".loc(),
49 onSelect: "itemSelected"
52 {controlClasses: 'enyo-inline', components: [
53 {name: "label", classes: "xv-label"},
54 {kind: "onyx.InputDecorator", name: "inputWrapper", components: [
55 {kind: "onyx.PickerDecorator", components: [
56 {kind: "XV.PickerButton", name: "pickerButton", content: "_none".loc(), onkeyup: "keyUp"},
57 {name: "picker", kind: "onyx.Picker"}
63 @todo Document the buildList method.
65 buildList: function (options) {
66 var nameAttribute = this.getNameAttribute(),
67 models = this.filteredList(options),
68 none = this.getNoneText(),
69 classes = this.getNoneClasses(),
70 picker = this.$.picker,
71 iconClass = this.iconClass,
72 iconVisible = this.iconVisible,
76 picker.destroyClientControls();
78 picker.createComponent({
79 kind: "XV.PickerMenuItem",
86 _.each(models, function (model) {
87 var name = model.getValue ? model.getValue(nameAttribute) : model.get(nameAttribute),
88 isActive = model && model.getValue ? model.getValue("isActive") !== false :
89 (model && _.isBoolean(model.get("isActive")) ? model.get("isActive") : true);
91 picker.createComponent({
92 kind: "XV.PickerMenuItem",
97 iconVisible: iconVisible
102 // this is for an Enyo Bug relating
103 // to pickers inside of a popup
104 if (this.prerender) {
105 this.$.picker.render();
109 @todo Document the clear method.
111 clear: function (options) {
112 this.setValue(null, options);
115 Collection can either be a pointer to a real collection, or a string
116 that will be resolved to a real collection.
118 collectionChanged: function () {
119 var collection = _.isObject(this.collection) ? this.collection :
120 XT.getObjectByName(this.collection),
125 // Remove any old bindings
126 if (this._collection) {
127 this._collection.off("add remove reset", this.buildList, this);
130 // If we don't have data yet, try again after start up tasks complete
133 XT.log('Could not find collection ' + this.getCollection());
136 callback = function () {
138 that.collectionChanged();
140 XT.getStartupManager().registerCallback(callback);
144 this._collection = collection;
145 this._collection.on("add remove reset", this.buildList, this);
146 this.orderByChanged();
147 if (this._collection.comparator) { this._collection.sort(); }
151 @todo Document the create method.
153 create: function () {
154 var defaultValue = this.getDefaultValue();
156 this.inherited(arguments);
157 this.noneTextChanged();
158 if (this.getCollection()) {
159 this.collectionChanged();
162 this.setValue(defaultValue, {silent: true});
166 this.showLabelChanged();
168 destroy: function () {
169 if (this._collection && this._collection.off) {
170 this._collection.off("add remove reset", this.buildList, this);
172 this.inherited(arguments);
175 @todo Document the disabledChanged method.
177 disabledChanged: function (inSender, inEvent) {
178 var disabled = this.getDisabled();
179 this.$.pickerButton.setDisabled(disabled);
180 this.$.label.addRemoveClass("disabled", disabled);
183 @todo Document the getValueToString method.
185 getValueToString: function () {
186 return this.$.pickerButton.getContent();
189 @todo Document the itemSelected method.
191 itemSelected: function (inSender, inEvent) {
192 var value = this.$.picker.getSelected().value;
193 this.setValue(value);
196 Implement your own filter function here. By default
197 simply returns the array of models passed.
202 filter: function (models, options) {
206 Returns array of models for current collection instance with `filter`
209 filteredList: function (options) {
210 return this._collection ? this.filter(this._collection.models, options) : [];
212 keyUp: function (inSender, inEvent) {
213 var keyCode = inEvent.keyCode,
214 currentSelection = this.$.picker.getSelected(),
215 controlsContents = _.map(this.$.picker.controls, function (control) {
216 return control.content;
218 currentIndex = _.indexOf(controlsContents, currentSelection && currentSelection.content),
222 if (keyCode === 40) {
223 // down key: go down one
224 newIndex = Math.min(currentIndex + 1, this.$.picker.controls.length - 1);
225 this.$.picker.setSelected(this.$.picker.controls[newIndex]);
226 this.itemSelected(); // TODO: only select item on blur
227 } else if (keyCode === 38) {
229 // looks like the minimum picker option we want to allow is at index 1 ("none"), and not
230 // the undefined-value backed index 0
231 newIndex = Math.max(currentIndex - 1, 1);
232 this.$.picker.setSelected(this.$.picker.controls[newIndex]);
234 } else if (keyCode >= 65 && keyCode <= 90) {
235 // alpha keycode: find the first option that starts with that letter
236 newSelection = _.find(this.$.picker.$, function (control) {
237 return control.content.charCodeAt(0) === keyCode;
240 this.$.picker.setSelected(newSelection);
247 @todo Document the noneTextChanged method.
249 noneTextChanged: function () {
250 var noneText = this.getNoneText(),
251 button = this.$.pickerButton;
253 button.setContent(noneText);
258 @todo Document the noneClassesChanged method.
260 noneClassesChanged: function () {
264 @todo Document the orderByChanged method.
266 orderByChanged: function () {
267 var orderBy = this.getOrderBy();
268 if (this._collection && orderBy) {
269 this._collection.comparator = function (a, b) {
276 for (i = 0; i < orderBy.length; i++) {
277 attr = orderBy[i].attribute;
278 // Add support for Backbone.Models in static.js
279 aValue = a.getValue ? a.getValue(attr) : a.get(attr);
280 bValue = b.getValue ? b.getValue(attr) : b.get(attr);
281 aval = orderBy[i].descending ? bValue : aValue;
282 bval = orderBy[i].descending ? aValue : bValue;
283 // Bad hack for null 'order' values
284 if (attr === "order" && !_.isNumber(aval)) { aval = 9999; }
285 if (attr === "order" && !_.isNumber(bval)) { bval = 9999; }
286 aval = !isNaN(aval) ? aval - 0 : aval;
287 bval = !isNaN(aval) ? bval - 0 : bval;
289 return aval > bval ? 1 : -1;
298 @todo Document the select method.
300 select: function (index) {
302 component = _.find(this.$.picker.getComponents(), function (c) {
303 if (c.kind === "onyx.MenuItem") { i++; }
307 this.setValue(component.value);
311 selectValue: function (value) {
312 var coll = this._collection,
313 key = this.idAttribute ||
314 (coll && coll.model ? coll.model.prototype.idAttribute : false),
315 components = this.$.picker.getComponents(),
319 ret = value && key ? value.get(key) : value;
320 component = _.find(components, function (c) {
321 if (c.kind === "XV.PickerMenuItem") {
322 return (c.value ? c.value.get(key) : null) === ret;
328 this.$.picker.setSelected(null);
329 this.$.pickerButton.setContent("_none".loc());
331 this.$.picker.setSelected(component);
338 Programatically sets the value of this widget.
340 Value can be a model or the id of a model (String or Number).
341 If it is an ID, then the correct model will be fetched and this
342 function will be called again recursively with the model.
344 The value passed can also be an object with two properties:
345 "collection" and "value". If this is passed the collection will
346 be set to the passed collection, and the value will be set to "value."
348 @param {Number|XM.Model|Object}
349 @param {Object} options
351 setValue: function (value, options) {
352 options = options || {};
353 var key = this.idAttribute || (this._collection && this._collection.model ?
354 this._collection.model.prototype.idAttribute : null),
355 oldValue = this.getValue(),
356 attr = this.getAttr(),
362 // here is where we find the model and re-call this method if we're given
363 // an id instead of a whole model.
364 // note that we assume that all of the possible models are already
365 // populated in the menu items of the picker
366 // note: value may be a '0' value
367 if (key !== null && value !== null && typeof value !== 'object') {
368 actualMenuItem = _.find(this.$.picker.controls, function (menuItem) {
370 if (menuItem.value && menuItem.value.get) {
371 ret = menuItem.value.get(key) === value;
372 } else if (menuItem.value) {
373 ret = menuItem.value[key] === value;
377 if (actualMenuItem) {
378 // a menu item matches the selection. Use the model back backs the menu item
379 actualModel = actualMenuItem.value;
380 this.setValue(actualModel, options);
382 // (else "none" is selected and there's no need to do anything)
386 // Handle when a collection is passed in as part of a two part argument
387 if (_.isObject(value) && !(value instanceof Backbone.Model)) {
388 if (value.collection && value.collection !== this._collection) {
389 this.setCollection(value.collection);
391 this.setValue(value.value, options);
395 if (value !== oldValue) {
396 selectedValue = this.selectValue(value);
398 if (selectedValue !== oldValue) {
401 if (!options.silent) {
402 if (_.isObject(attr)) {
403 inEvent = {value: {collection: this._collection}};
404 inEvent.value[attr.value] = selectedValue;
406 inEvent = {value: selectedValue};
409 this.doValueChange(inEvent);
415 @todo Document the labelChanged method.
417 labelChanged: function () {
418 var attr = this.attr && _.isString(this.attr) ? this.attr : (this.attr ? this.attr.value : false),
419 label = this.getLabel() || (attr ? ("_" + attr).loc() : "");
420 this.$.label.setShowing(!!label);
421 this.$.label.setContent(label + ":");
424 @todo Document the showLabelChanged method.
426 showLabelChanged: function () {
427 this.$.label.setShowing(this.showLabel);
432 This is a subclass of the onyx.PickerButton that is used in the PickerDecorator.
433 The default behavior is that inside of the decorator, the first empty kind is
434 set to be a PickerButton. When the change event is fired, the content of this
435 button is changed to the content of the selection.
438 /** @lends XV.PickerButton */{
439 name: "XV.PickerButton",
440 kind: "onyx.PickerButton",
441 classes: "xv-picker-button",
443 {name: "text", content: "", classes: "picker-content"},
444 {tag: "i", classes: "icon-caret-down picker-icon"} // font-awesome icon
446 create: function () {
447 this.inherited(arguments);
448 this.contentChanged();
451 When the content is changed on the parent PickerButton,
452 this sets the content of the text component inside the button.
454 contentChanged: function () {
455 this.$.text.setContent(this.getContent());
460 This is a subclass of the onyx.MenuItem that is used in the Picker's menu.
461 Like the XV.PickerButton, it allows for content and an icon. In this case, the
462 icon is optional and may be made invisible if included.
465 /** @lends XV.PickerButton */{
466 name: "XV.PickerMenuItem",
467 kind: "onyx.MenuItem",
468 classes: "xv-picker-button",
476 {name: "text", content: ""},
477 {name: "icon", tag: "i", classes: "icon-dark picker-icon"} // font-awesome icon
479 create: function () {
480 this.inherited(arguments);
481 this.contentChanged();
482 this.iconVisibleChanged();
483 this.disabledChanged();
486 When the content is changed on the parent MenuItem,
487 this sets the content of the text component inside the button.
489 contentChanged: function () {
490 this.$.text.setContent(this.getContent());
492 disabledChanged: function () {
493 this.addRemoveClass("disabled", this.disabled);
496 If there is an icon class, we determine if it is
497 showing based on the iconVisible logic.
499 iconVisibleChanged: function () {
500 var showing = this.iconVisible;
501 if (_.isFunction(this.iconVisible)) {
502 showing = this.iconVisible(this.value);
504 this.$.icon.setShowing(showing);
506 // if the menu item is showing an icon,
507 // set the icon class for the menu item
508 // and also set the css style to not allow
511 this.$.icon.addClass(this.getIconClass());
512 this.$.text.addClass("picker-content");
514 this.$.icon.removeClass(this.getIconClass());
515 this.$.text.removeClass("picker-content");
518 tap: function (inSender) {
519 if (!this.disabled) { return this.inherited(arguments); }