/*jshint indent:2, curly:true, eqeqeq:true, immed:true, latedef:true, newcap:true, noarg:true,
regexp:true, undef:true, trailing:true, white:true, strict:false */
/*global XT:true, Backbone:true, enyo:true, _:true */
(function () {
/**
@name XV.Picker
@class A picker control that implements a dropdown list of items which can be selected.<br />
Unlike the {@link XV.RelationWidget}, the collection is stored local to the widget.<br />
The superkind of {@link XV.CharacteristicPicker}.<br />
Accepts a single attribute mapping or an object attribute mapping where the object requires
two properties "colleciton" and "value." This technique can be used to bind the picker
to a collection on a local model in addition to the selected value.
Derived from <a href="http://enyojs.com/api/#enyo.Control">enyo.Control</a>.
@extends enyo.Control
*/
enyo.kind(
/** @lends XV.PickerWidget# */{
name: "XV.PickerWidget",
kind: "enyo.Control",
classes: "xv-pickerwidget",
events: /** @lends XV.PickerWidget# */{
/**
@property {Object} inEvent The payload that's attached to bubbled-up events
@property {XV.PickerWidget} inEvent.originator This
@property inEvent.value The value passed up is the key of the object and not the object itself
*/
onValueChange: ""
},
published: {
attr: null,
value: null,
collection: null,
disabled: false,
nameAttribute: "name",
orderBy: null,
noneText: "_none".loc(),
noneClasses: "",
showNone: true,
prerender: true,
defaultValue: null,
showLabel: true,
label: ""
},
handlers: {
onSelect: "itemSelected"
},
components: [
{controlClasses: 'enyo-inline', components: [
{name: "label", classes: "xv-label"},
{kind: "onyx.InputDecorator", name: "inputWrapper", components: [
{kind: "onyx.PickerDecorator", components: [
{kind: "XV.PickerButton", name: "pickerButton", content: "_none".loc(), onkeyup: "keyUp"},
{name: "picker", kind: "onyx.Picker"}
]}
]}
]}
],
/**
@todo Document the buildList method.
*/
buildList: function (options) {
var nameAttribute = this.getNameAttribute(),
models = this.filteredList(options),
none = this.getNoneText(),
classes = this.getNoneClasses(),
picker = this.$.picker,
iconClass = this.iconClass,
iconVisible = this.iconVisible,
component,
i;
picker.destroyClientControls();
if (this.showNone) {
picker.createComponent({
kind: "XV.PickerMenuItem",
value: null,
content: none,
classes: classes
});
}
_.each(models, function (model) {
var name = model.getValue ? model.getValue(nameAttribute) : model.get(nameAttribute),
isActive = model && model.getValue ? model.getValue("isActive") !== false :
(model && _.isBoolean(model.get("isActive")) ? model.get("isActive") : true);
picker.createComponent({
kind: "XV.PickerMenuItem",
value: model,
disabled: !isActive,
content: name,
iconClass: iconClass,
iconVisible: iconVisible
});
});
// this is for an Enyo Bug relating
// to pickers inside of a popup
if (this.prerender) {
this.$.picker.render();
}
},
/**
@todo Document the clear method.
*/
clear: function (options) {
this.setValue(null, options);
},
/**
Collection can either be a pointer to a real collection, or a string
that will be resolved to a real collection.
*/
collectionChanged: function () {
var collection = _.isObject(this.collection) ? this.collection :
XT.getObjectByName(this.collection),
that = this,
didStartup = false,
callback;
// Remove any old bindings
if (this._collection) {
this._collection.off("add remove reset", this.buildList, this);
}
// If we don't have data yet, try again after start up tasks complete
if (!collection) {
if (didStartup) {
XT.log('Could not find collection ' + this.getCollection());
return;
}
callback = function () {
didStartup = true;
that.collectionChanged();
};
XT.getStartupManager().registerCallback(callback);
return;
}
this._collection = collection;
this._collection.on("add remove reset", this.buildList, this);
this.orderByChanged();
if (this._collection.comparator) { this._collection.sort(); }
this.buildList();
},
/**
@todo Document the create method.
*/
create: function () {
var defaultValue = this.getDefaultValue();
this.inherited(arguments);
this.noneTextChanged();
if (this.getCollection()) {
this.collectionChanged();
}
if (defaultValue) {
this.setValue(defaultValue, {silent: true});
}
this.labelChanged();
this.showLabelChanged();
},
destroy: function () {
if (this._collection && this._collection.off) {
this._collection.off("add remove reset", this.buildList, this);
}
this.inherited(arguments);
},
/**
@todo Document the disabledChanged method.
*/
disabledChanged: function (inSender, inEvent) {
var disabled = this.getDisabled();
this.$.pickerButton.setDisabled(disabled);
this.$.label.addRemoveClass("disabled", disabled);
},
/**
@todo Document the getValueToString method.
*/
getValueToString: function () {
return this.$.pickerButton.getContent();
},
/**
@todo Document the itemSelected method.
*/
itemSelected: function (inSender, inEvent) {
var value = this.$.picker.getSelected().value;
this.setValue(value);
},
/**
Implement your own filter function here. By default
simply returns the array of models passed.
@param {Array}
@returns {Array}
*/
filter: function (models, options) {
return models || [];
},
/**
Returns array of models for current collection instance with `filter`
applied.
*/
filteredList: function (options) {
return this._collection ? this.filter(this._collection.models, options) : [];
},
keyUp: function (inSender, inEvent) {
var keyCode = inEvent.keyCode,
currentSelection = this.$.picker.getSelected(),
controlsContents = _.map(this.$.picker.controls, function (control) {
return control.content;
}),
currentIndex = _.indexOf(controlsContents, currentSelection && currentSelection.content),
newSelection,
newIndex;
if (keyCode === 40) {
// down key: go down one
newIndex = Math.min(currentIndex + 1, this.$.picker.controls.length - 1);
this.$.picker.setSelected(this.$.picker.controls[newIndex]);
this.itemSelected(); // TODO: only select item on blur
} else if (keyCode === 38) {
// up key: go up one
// looks like the minimum picker option we want to allow is at index 1 ("none"), and not
// the undefined-value backed index 0
newIndex = Math.max(currentIndex - 1, 1);
this.$.picker.setSelected(this.$.picker.controls[newIndex]);
this.itemSelected();
} else if (keyCode >= 65 && keyCode <= 90) {
// alpha keycode: find the first option that starts with that letter
newSelection = _.find(this.$.picker.$, function (control) {
return control.content.charCodeAt(0) === keyCode;
});
if (newSelection) {
this.$.picker.setSelected(newSelection);
this.itemSelected();
}
}
},
/**
@todo Document the noneTextChanged method.
*/
noneTextChanged: function () {
var noneText = this.getNoneText(),
button = this.$.pickerButton;
if (!this.value) {
button.setContent(noneText);
}
this.buildList();
},
/**
@todo Document the noneClassesChanged method.
*/
noneClassesChanged: function () {
this.buildList();
},
/**
@todo Document the orderByChanged method.
*/
orderByChanged: function () {
var orderBy = this.getOrderBy();
if (this._collection && orderBy) {
this._collection.comparator = function (a, b) {
var aval,
bval,
aValue,
bValue,
attr,
i;
for (i = 0; i < orderBy.length; i++) {
attr = orderBy[i].attribute;
// Add support for Backbone.Models in static.js
aValue = a.getValue ? a.getValue(attr) : a.get(attr);
bValue = b.getValue ? b.getValue(attr) : b.get(attr);
aval = orderBy[i].descending ? bValue : aValue;
bval = orderBy[i].descending ? aValue : bValue;
// Bad hack for null 'order' values
if (attr === "order" && !_.isNumber(aval)) { aval = 9999; }
if (attr === "order" && !_.isNumber(bval)) { bval = 9999; }
aval = !isNaN(aval) ? aval - 0 : aval;
bval = !isNaN(aval) ? bval - 0 : bval;
if (aval !== bval) {
return aval > bval ? 1 : -1;
}
}
return 0;
};
}
},
/**
@todo Document the select method.
*/
select: function (index) {
var i = 0,
component = _.find(this.$.picker.getComponents(), function (c) {
if (c.kind === "onyx.MenuItem") { i++; }
return i > index;
});
if (component) {
this.setValue(component.value);
}
},
selectValue: function (value) {
var coll = this._collection,
key = this.idAttribute ||
(coll && coll.model ? coll.model.prototype.idAttribute : false),
components = this.$.picker.getComponents(),
component,
ret;
ret = value && key ? value.get(key) : value;
component = _.find(components, function (c) {
if (c.kind === "XV.PickerMenuItem") {
return (c.value ? c.value.get(key) : null) === ret;
}
});
if (!component) {
ret = null;
this.$.picker.setSelected(null);
this.$.pickerButton.setContent("_none".loc());
} else {
this.$.picker.setSelected(component);
}
return ret;
},
/**
Programatically sets the value of this widget.
Value can be a model or the id of a model (String or Number).
If it is an ID, then the correct model will be fetched and this
function will be called again recursively with the model.
The value passed can also be an object with two properties:
"collection" and "value". If this is passed the collection will
be set to the passed collection, and the value will be set to "value."
@param {Number|XM.Model|Object}
@param {Object} options
*/
setValue: function (value, options) {
options = options || {};
var key = this.idAttribute || (this._collection && this._collection.model ?
this._collection.model.prototype.idAttribute : null),
oldValue = this.getValue(),
attr = this.getAttr(),
actualMenuItem,
actualModel,
inEvent,
selectedValue;
// here is where we find the model and re-call this method if we're given
// an id instead of a whole model.
// note that we assume that all of the possible models are already
// populated in the menu items of the picker
// note: value may be a '0' value
if (key !== null && value !== null && typeof value !== 'object') {
actualMenuItem = _.find(this.$.picker.controls, function (menuItem) {
var ret = false;
if (menuItem.value && menuItem.value.get) {
ret = menuItem.value.get(key) === value;
} else if (menuItem.value) {
ret = menuItem.value[key] === value;
}
return ret;
});
if (actualMenuItem) {
// a menu item matches the selection. Use the model back backs the menu item
actualModel = actualMenuItem.value;
this.setValue(actualModel, options);
}
// (else "none" is selected and there's no need to do anything)
return;
}
// Handle when a collection is passed in as part of a two part argument
if (_.isObject(value) && !(value instanceof Backbone.Model)) {
if (value.collection && value.collection !== this._collection) {
this.setCollection(value.collection);
}
this.setValue(value.value, options);
return;
}
if (value !== oldValue) {
selectedValue = this.selectValue(value);
if (selectedValue !== oldValue) {
this.value = value;
if (!options.silent) {
if (_.isObject(attr)) {
inEvent = {value: {collection: this._collection}};
inEvent.value[attr.value] = selectedValue;
} else {
inEvent = {value: selectedValue};
}
this.doValueChange(inEvent);
}
}
}
},
/**
@todo Document the labelChanged method.
*/
labelChanged: function () {
var attr = this.attr && _.isString(this.attr) ? this.attr : (this.attr ? this.attr.value : false),
label = this.getLabel() || (attr ? ("_" + attr).loc() : "");
this.$.label.setShowing(!!label);
this.$.label.setContent(label + ":");
},
/**
@todo Document the showLabelChanged method.
*/
showLabelChanged: function () {
this.$.label.setShowing(this.showLabel);
}
});
/**
This is a subclass of the onyx.PickerButton that is used in the PickerDecorator.
The default behavior is that inside of the decorator, the first empty kind is
set to be a PickerButton. When the change event is fired, the content of this
button is changed to the content of the selection.
*/
enyo.kind(
/** @lends XV.PickerButton */{
name: "XV.PickerButton",
kind: "onyx.PickerButton",
classes: "xv-picker-button",
components: [
{name: "text", content: "", classes: "picker-content"},
{tag: "i", classes: "icon-caret-down picker-icon"} // font-awesome icon
],
create: function () {
this.inherited(arguments);
this.contentChanged();
},
/**
When the content is changed on the parent PickerButton,
this sets the content of the text component inside the button.
*/
contentChanged: function () {
this.$.text.setContent(this.getContent());
}
});
/**
This is a subclass of the onyx.MenuItem that is used in the Picker's menu.
Like the XV.PickerButton, it allows for content and an icon. In this case, the
icon is optional and may be made invisible if included.
*/
enyo.kind(
/** @lends XV.PickerButton */{
name: "XV.PickerMenuItem",
kind: "onyx.MenuItem",
classes: "xv-picker-button",
value: null,
published: {
iconClass: "",
iconVisible: null,
disabled: false
},
components: [
{name: "text", content: ""},
{name: "icon", tag: "i", classes: "icon-dark picker-icon"} // font-awesome icon
],
create: function () {
this.inherited(arguments);
this.contentChanged();
this.iconVisibleChanged();
this.disabledChanged();
},
/**
When the content is changed on the parent MenuItem,
this sets the content of the text component inside the button.
*/
contentChanged: function () {
this.$.text.setContent(this.getContent());
},
disabledChanged: function () {
this.addRemoveClass("disabled", this.disabled);
},
/**
If there is an icon class, we determine if it is
showing based on the iconVisible logic.
*/
iconVisibleChanged: function () {
var showing = this.iconVisible;
if (_.isFunction(this.iconVisible)) {
showing = this.iconVisible(this.value);
}
this.$.icon.setShowing(showing);
// if the menu item is showing an icon,
// set the icon class for the menu item
// and also set the css style to not allow
// for text overlap
if (showing) {
this.$.icon.addClass(this.getIconClass());
this.$.text.addClass("picker-content");
} else {
this.$.icon.removeClass(this.getIconClass());
this.$.text.removeClass("picker-content");
}
},
tap: function (inSender) {
if (!this.disabled) { return this.inherited(arguments); }
}
});
}());