/** @module delite/Selection */
define(["dcl/dcl", "decor/sniff", "./Widget"], function (dcl, has, Widget) {
/**
* Selection change event. Dispatched after the selection has
* been modified through user interaction.
* @example
* widget.on("selection-change", function (evt) {
* console.log("old value: " + evt.oldValue);
* console.log("new value: " + evt.newValue);
* }
* @event module:delite/Selection#selection-change
* @property {number} oldValue - The previously selected item.
* @property {number} newValue- The new selected item.
* @property {Object} renderer - The visual renderer of the selected/deselected item.
* @property {Event} triggerEvent - The event that lead to the selection of the item.
*/
/**
* Mixin for widgets that manage a list of selected data items.
* @mixin module:delite/Selection
* @augments module:delite/Widget
*/
return dcl(Widget, /** @lends module:delite/Selection# */{
preRender: function () {
this._set("selectedItems", []);
},
/**
* The chosen selection mode.
*
* Valid values are:
*
* 1. "none": No selection can be done.
* 2. "single": Only one or zero items can be selected at a time. Interactively selecting a new item deselects
* the previously selected one.
* 3. "radio": Initially only one or zero items can be selected. Once an item has been selected, interactively
* selecting another item deselects the previously selected item, and the user cannot deselect the currently
* selected item.
* 4. "multiple": Multiple items can be selected. By default ctrl key must be used to select additional items.
* However that behavior might be specialized by subclasses.
*
* Changing this value impacts the current selected items to adapt the selection to the new mode. However
* whatever the selection mode is you can always set several selected items using the selectItem(s) API.
* The mode will be enforced only when using setSelected and/or selectFromEvent APIs.
*
* @member {string}
* @default "single"
*/
selectionMode: "single",
_setSelectionModeAttr: function (value) {
if (value !== "none" && value !== "single" && value !== "multiple" && value !== "radio") {
throw new TypeError("selectionMode invalid value");
}
if (value !== this.selectionMode) {
this._set("selectionMode", value);
if (value === "none") {
this.selectedItems = null;
} else if ((value === "single" || value === "radio") && this.selectedItem) {
this.selectedItems = [this.selectedItem];
}
}
},
/**
* In single selection mode, the selected item or in multiple selection mode the last selected item.
* @member {Object}
* @default null
*/
selectedItem: null,
_setSelectedItemAttr: function (value) {
if (this.selectedItem !== value) {
this.selectedItems = (value == null ? null : [value]);
}
},
/**
* The list of selected items.
* @member {Object[]}
* @default null
*/
selectedItems: null,
_setSelectedItemsAttr: function (value) {
var oldSelectedItems = this.selectedItems;
this._set("selectedItems", value);
if (oldSelectedItems != null && oldSelectedItems.length > 0) {
this.updateRenderers(oldSelectedItems);
}
if (this.selectedItems && this.selectedItems.length > 0) {
this._set("selectedItem", this.selectedItems[0]);
this.updateRenderers(this.selectedItems);
} else {
this._set("selectedItem", null);
}
},
_getSelectedItemsAttr: function () {
return this._get("selectedItems") == null ? [] : this._get("selectedItems").concat();
},
/**
* Tests if an event has a selection modifier.
*
* If it has a selection modifier, that means that:
*
* * if selectionMode is "single", the event will be able to deselect a selected item
* * if selectionMode is "multiple", the event will trigger the selection state of the item
*
* The default implementation of this method returns true if the event.ctrlKey attribute is
* true, which means that:
*
* * if selectionMode is "single", the Ctrl (or Command on MacOS) key must be pressed for the
* * event to deselect the currently selected item
* * if selectionMode is "multiple", the Ctrl (or Command on MacOS) key must be pressed for the
* event to toggle the selection status of the item.
*
* @param {Event} event - The event that led to the selection .
* @returns {boolean} Whether the event has selection modifier.
* @protected
*/
hasSelectionModifier: function (event) {
return !has("mac") ? event.ctrlKey : event.metaKey;
},
/**
* Returns whether an item is selected or not.
* @param {item} object The item to test.
* @returns {Object} The item to test the selection for.
*/
isSelected: function (item) {
if (this.selectedItems == null || this.selectedItems.length === 0) {
return false;
}
var identity = this.getIdentity(item);
return this.selectedItems.some(function (sitem) {
return this.getIdentity(sitem) === identity;
}, this);
},
/**
* This function must be implemented to return the id of a item.
* @param {item} object - The item the identity of must be returned.
* @returns {string} The identity of the item.
*/
getIdentity: function (/*jshint unused: vars */item) {
},
/**
* This function must be implemented to update the rendering of the items based on whether they are
* selected or not. The implementation must check for their new selection state and update
* accordingly.
* @param {Object[]} items - The array of items changing their selection state.
* @protected
*/
updateRenderers: function (/*jshint unused: vars */items) {
},
/**
* Change the selection state of an item.
* @param {Object} item - The item to change the selection state for.
* @param {boolean} value - True to select the item, false to deselect it.
*/
setSelected: function (item, value) {
if (this.selectionMode === "none" || item == null) {
return;
}
this._setSelected(item, value);
},
/* jshint maxcomplexity: 11*/
_setSelected: function (item, value) {
// copy is returned
var sel = this.selectedItems, res, identity;
if (this.selectionMode === "single" || this.selectionMode === "radio") {
if (value) {
this.selectedItem = item;
} else if (this.selectionMode === "single" && this.isSelected(item)) {
this.selectedItems = null;
}
} else { // multiple
if (value) {
if (this.isSelected(item)) {
return; // already selected
}
if (sel == null) {
sel = [item];
} else {
sel.unshift(item);
}
this.selectedItems = sel;
} else {
identity = this.getIdentity(item);
res = sel ? sel.filter(function (sitem) {
return this.getIdentity(sitem) !== identity;
}, this) : [];
if (res == null || res.length === sel.length) {
return; // already not selected
}
this.selectedItems = res;
}
}
},
/* jshint maxcomplexity: 10*/
/**
* Applies selection triggered by an user interaction.
* @param {Event} event - The source event of the user interaction.
* @param {Object} item - The item that has been selected/deselected.
* @param {Object} renderer - The visual renderer of the selected/deselected item.
* @param {boolean} dispatch - Whether an event must be dispatched or not.
* @returns {boolean} True if the selection has changed and false otherwise.
* @protected
*/
selectFromEvent: function (event, item, renderer, dispatch) {
if (this.selectionMode === "none") {
return false;
}
return this._selectFromEvent(event, item, renderer, dispatch);
},
_selectFromEvent: function (event, item, renderer, dispatch) {
var changed;
var oldSelectedItem = this.selectedItem;
var selected = item == null ? false : this.isSelected(item);
if (item == null) {
if ((this.selectionMode === "multiple" && !this.hasSelectionModifier(event))
&& this.selectedItem != null) {
this.selectedItem = null;
changed = true;
}
} else if (this.selectionMode === "multiple") {
if (this.hasSelectionModifier(event)) {
this.setSelected(item, !selected);
changed = true;
} else {
this.selectedItem = item;
changed = true;
}
} else { // single
if (this.selectionMode === "single" && this.hasSelectionModifier(event)) {
//if the object is selected deselects it.
this.selectedItem = (selected ? null : item);
changed = true;
} else {
if (!selected) {
this.selectedItem = item;
changed = true;
}
}
}
if (dispatch && changed) {
this.dispatchSelectionChange(oldSelectedItem, this.selectedItem, renderer, event);
}
return changed;
},
/**
* Dispatch a selection change event.
* @param {Object} oldSelectedItem - The previously selected item.
* @param {Object} newSelectedItem - The new selected item.
* @param {Object} renderer - The visual renderer of the selected/deselected item.
* @param {Event} triggerEvent - The event that lead to the selection of the item.
* @protected
* @fires module:delite/Selection#selection-change
*/
dispatchSelectionChange: function (oldSelectedItem, newSelectedItem, renderer, triggerEvent) {
this.emit("selection-change", {
oldValue: oldSelectedItem,
newValue: newSelectedItem,
renderer: renderer,
triggerEvent: triggerEvent
});
}
});
});