Source: d:/Users/cjolif/sdk/sdk-utils/deliteful/Select.js

/** @module deliteful/Select */
define([
	"dcl/dcl",
	"dojo/dom-class", // TODO: replace (when replacement confirmed)
	"dstore/Memory",
	"dstore/Trackable",
	"delite/register",
	"delite/FormWidget",
	"delite/StoreMap",
	"delite/Selection",
	"delite/handlebars!./Select/Select.html",
	"delite/theme!./Select/themes/{{theme}}/Select.css"
], function (dcl, domClass, Memory, Trackable, register,
	FormWidget, StoreMap, Selection, template) {

	/**
	 * A form-aware and store-aware widget leveraging the native HTML5 `<select>`
	 * element.
	 * It has the following characteristics:
	 * * The corresponding custom tag is `<d-select>`.
	 * * Store support (limitation: to avoid graphic glitches, the updates to the
	 * store should not be done while the native dropdown of the select is open).
	 * * The item rendering has the limitations of the `<option>` elements of the
	 * native `<select>`, in particular it is text-only.
	 * 
	 * TODO: improve doc.
	 * 
	 * Remarks:
	 * * The option items must be added, removed or updated exclusively using
	 * the store API. Direct operations using the DOM API are not supported.
	 * * The handling of the selected options of the underlying native `<select>`
	 * must be done using the API inherited by deliteful/Select from delite/Selection.
	 * 
	 * @example <caption>Using the default store</caption>
	 * JS:
	 * require(["delite/register", "deliteful/Select", "requirejs-domready/domReady!"],
	 *   function (register) {
	 *     register.parse();
	 *     select1.store.add({text: "Option 1", value: "1"});
	 *     ...
	 *   });
	 * HTML:
	 * <d-select id="select1"></d-select>
	 * @example <caption>Using user's store</caption>
	 * JS:
	 * require(["delite/register", "dstore/Memory", "dstore/Trackable",
	 *         "deliteful/Select", "requirejs-domready/domReady!"],
	 *   function (register, Memory, Trackable) {
	 *     register.parse();
	 *     var store = new (Memory.createSubclass(Trackable))({});
	 *     select1.store = store;
	 *     store.add({text: "Option 1", value: "1"});
	 *     ...
	 *   });
	 * HTML:
	 * <d-select selectionMode="multiple" id="select1"></d-select>
	 * 
	 * @class module:deliteful/Select
	 * @augments module:delite/FormWidget
	 * @augments module:delite/Store
	 */
	return register("d-select", [HTMLElement, FormWidget, StoreMap, Selection],
		/** @lends module:deliteful/Select# */ {
		
		// Note: the properties `store` and `query` are inherited from delite/Store, and
		// the property `disabled` is inherited from delite/FormWidget.
		
		/**
		 * The number of rows that should be visible at one time when the widget
		 * is presented as a scrollable list box. Corresponds to the `size` attribute
		 * of the underlying native HTML `<select>`.
		 * @member {number}
		 * @default 0
		 */
		size: 0,
		
		/**
		 * The name of the property of store items which contains the text
		 * of Select's options.
		 * @member {string}
		 * @default "text"
		 */
		textAttr: "text",
		
		/**
		 * The name of the property of store items which contains the value
		 * of Select's options.
		 * @member {string}
		 * @default "value"
		 */
		valueAttr: "value",
		
		/**
		 * The name of the property of store items which contains the disabled
		 * value of Select's options. To disable a given option, the `disabled`
		 * property of the corresponding data item must be set to a truthy value.
		 * Otherwise, the option is enabled if data item property is absent, or
		 * its value is falsy or the string "false".
		 * @member {string}
		 * @default "disabled"
		 */
		disabledAttr: "disabled",
		
		baseClass: "d-select",
		
		/**
		 * The chosen selection mode.
		 *
		 * Valid values are:
		 *
		 * 1. "single": Only one option can be selected at a time.
		 * 2. "multiple": Several options can be selected (by taping or using the
		 * control key modifier).
		 *
		 * Changing this value impacts the currently selected items to adapt the
		 * selection to the new mode. However, regardless of the selection mode,
		 * it is always possible to set several selected items using the
		 * `selectedItem` or `selectedItems` properties.
		 * The mode will be enforced only when using `setSelected` and/or
		 * `selectFromEvent` APIs.
		 *
		 * @member {string} module:deliteful/Select#selectionMode
		 * @default "single"
		 */
		// The purpose of the above pseudo-property is to adjust the documentation
		// of selectionMode as provided by delite/Selection.
		  
		template: template,
		
		startup: function () {
			if (!this.store) { // If not specified by the user
				this.store = new (Memory.createSubclass(Trackable))({});
			}
			
			this.focusNode = this.valueNode; // for delite/FormWidget's sake
			
			// To provide graphic feedback for focus, react to focus/blur events
			// on the underlying native select. The CSS class is used instead
			// of the focus pseudo-class because the browsers give the focus
			// to the underlying select, not to the widget.
			this.on("focus", function (evt) {
				domClass.toggle(this, "d-select-focus", evt.type === "focus");
			}.bind(this), this.valueNode);
			this.on("blur", function (evt) {
				domClass.toggle(this, "d-select-focus", evt.type === "focus");
			}.bind(this), this.valueNode);
			
			// Keep delite/Selection's selectedItem/selectedItems in sync after
			// interactive selection of options.
			this.on("change", function (event) {
				var selectedItems = this.selectedItems,
					selectedOptions = this.valueNode.selectedOptions;
				// HTMLSelectElement.selectedOptions is not present in all browsers...
				// At least IE10/Win misses it. Hence:
				if (selectedOptions === undefined) {
					// Convert to array
					var options = Array.prototype.slice.call(this.valueNode.options);
					selectedOptions = options.filter(function (option) {
						return option.selected;
					});
				} else {
					// convert HTMLCollection into array (to be able to use array.indexOf)
					selectedOptions = Array.prototype.slice.call(selectedOptions);
				}
				var nSelectedItems = selectedItems ? selectedItems.length : 0,
					nSelectedOptions = selectedOptions ? selectedOptions.length : 0;
				var i;
				var selectedOption, selectedItem;
				// Identify the options which changed their selection state. Two steps:
				// Step 1. Search options previously selected (currently in widget.selectedItems)
				// which are no longer selected in the native select.
				for (i = 0; i < nSelectedItems; i++) {
					selectedItem = selectedItems[i];
					if (selectedOptions.indexOf(selectedItem.visualItem) === -1) {
						this.selectFromEvent(event, selectedItem, selectedItem.visualItem, true);
					}
				}
				// Step 2. Search options newly selected in the native select which are not
				// present in the current selection (widget.selectedItems).
				for (i = 0; i < nSelectedOptions; i++) {
					selectedOption = selectedOptions[i];
					if (selectedItems.indexOf(selectedOption.renderItem) === - 1) {
						this.selectFromEvent(event, selectedOption.renderItem, selectedOption, true);
					}
				}
				
				// Update widget's value after interactive selection
				this._set("value", this.valueNode.value);
			}.bind(this), this.valueNode);
			
			// Thanks to the custom getter defined in deliteful/Select for widget's
			// `value` property, there is no need to add code for keeping the
			// property in sync after a form reset.
		},
		
		hasSelectionModifier: function () {
			// Override of the method from delite/Selection because the
			// default implementation is inappropriate: the "change" event
			// has no key modifier.
			return this.selectionMode === "multiple";
		},
		
		refreshRendering: function (props) {
			if ("renderItems" in props) {
				// Populate the select with the items retrieved from the store.
				var renderItems = this.renderItems;
				var n = renderItems ? renderItems.length : 0;
				// TODO: CHECKME/IMPROVEME. Also called after adding, deleting or updating just one item.
				// Worth optimizing to avoid recreating from scratch?
				this.valueNode.innerHTML = ""; // Remove the existing options from the DOM
				if (n > 0) {
					var fragment = this.ownerDocument.createDocumentFragment();
					var renderItem, option;
					for (var i = 0; i < n; i++) {
						renderItem = renderItems[i];
						option = this.ownerDocument.createElement("option");
						option.renderItem = renderItem;
						renderItem.visualItem = option;
						
						// According to http://www.w3.org/TR/html5/forms.html#the-option-element, we 
						// could use equivalently the label or the text IDL attribute of the option element.
						// However, using the label attr. breaks the rendering in FF29/Win7!
						// This is https://bugzilla.mozilla.org/show_bug.cgi?id=40545.
						// Hence don't do
						// option.label = renderItem.label;
						// Instead:
						if (renderItem.text !== undefined) { // optional
							option.text = renderItem.text;
						}
						if (renderItem.value !== undefined) { // optional
							option.setAttribute("value", renderItem.value);
						}
						if (this.isSelected(renderItem)) { // delite/Selection's API
							option.setAttribute("selected", "true");
						}
						if (renderItem.disabled !== undefined &&
							!!renderItem.disabled && renderItem.disabled !== "false") { // optional
							// Note that for an enabled option the attribute must NOT be set
							// (<option disabled="false"> is a disabled option!)
							option.setAttribute("disabled", "true");
						}
						
						fragment.appendChild(option);
					}
					this.valueNode.appendChild(fragment);
					
					if (this.selectionMode === "single") {
						// Since there is no native "change" event initially, initialize
						// the delite/Selection's selectedItem property with the currently
						// selected option of the native select.
						this.selectedItem =
							this.valueNode.options[this.valueNode.selectedIndex].renderItem;
					} // else for the native multi-select: it does not have any
					// option selected by default.
					
					// Initialize widget's value
					this._set("value", this.valueNode.value);
				}
			}
		},
		
		getIdentity: function (renderItem) {
			// Override of delite/Selection's method
			return renderItem.id;
		},
		
		updateRenderers: function () {
			// Override of delite/Selection's method
			// Trigger rerendering from scratch:
			this.notifyCurrentValue("renderItems");
		},
		
		_setValueAttr: function (value) {
			if (this.valueNode) {
				this.valueNode.value = value;
			}
			this._set("value", value);
		},
		
		_setSelectionModeAttr: dcl.superCall(function (sup) {
			// Override of the setter from delite/Selection to forbid the value "none"
			return function (value) {
				if (value !== "single" && value !== "multiple") {
					throw new TypeError("'" + value +
						"' not supported for selectionMode; keeping the previous value of '" +
						this.selectionMode + "'");
				} else {
					this._set("selectionMode", value);
				}
				sup.call(this, value);
			};
		})
	});
});