-
Notifications
You must be signed in to change notification settings - Fork 36
/
Select.js
337 lines (313 loc) · 13 KB
/
Select.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
/** @module deliteful/Select */
define([
"dcl/dcl",
"ibm-decor/sniff",
"delite/register",
"delite/FormWidget",
"delite/StoreMap",
"delite/Selection",
"delite/handlebars!./Select/Select.html",
"requirejs-dplugins/css!./Select/Select.css"
], function (dcl, has, 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>`.
* * Allows to select one or more items among a number of options (in single
* or multiple selection mode; see `selectionMode`).
* * 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 attributes of data items used for the `label`, `value`, and `disabled`
* attributes of option elements can be customized using respectively the
* `labelAttr`, `valueAttr`, and `disabledAttr` properties, or using
* `labelFunc`, `valueFunc`, and `disabledFunc` properties (for details, see
* the documentation of the `delite/StoreMap` superclass).
* * Form support (inherits from `delite/FormWidget`).
* * The item rendering has the limitations of the `<option>` elements of the
* native `<select>`, in particular it is text-only.
*
* 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 store custom element in markup</caption>
* JS:
* require(["deliteful/Select", "requirejs-domready/domReady!"],
* function () {
* });
* HTML:
* <d-select id="select">
* {text: "Option 1", value: "1"}
* ...
* </d-select>
* @example <caption>Using programmatically created store</caption>
* JS:
* require(["dojo-dstore/Memory", "dojo-dstore/Trackable",
* "deliteful/Select", "requirejs-domready/domReady!"],
* function (Memory, Trackable) {
* var store = new (Memory.createSubclass(Trackable))({});
* select1.source = store;
* store.add({text: "Option 1", value: "1"});
* ...
* });
* HTML:
* <d-select selectionMode="multiple" id="select"></d-select>
*
* @class module:deliteful/Select
* @augments module:delite/FormWidget
* @augments module:delite/StoreMap
* @augments module:delite/Selection
*/
// Have to keep StoreMap after Selection to get Store definition of getIdentity function
// eslint-disable-next-line max-len
return register("d-select", [ HTMLElement, FormWidget, Selection, StoreMap ], /** @lends module:deliteful/Select# */ {
// TODO: improve doc.
// 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,
afterFormResetCallback: function () {
this.valueNode.selectedIndex =
this.selectionMode === "single" ?
// First option selected in "single" selection mode, and
// no option selected in "multiple" mode
0 : -1;
this.value = this.valueNode.value;
},
afterInitializeRendering: function () {
// 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) {
this.classList.toggle("d-select-focus", evt.type === "focus");
}.bind(this), this.valueNode);
this.on("blur", function (evt) {
this.classList.toggle("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) {
this._duringInteractiveSelection = true;
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.__dataItem) === -1) {
this.selectFromEvent(event, selectedOption.__dataItem, selectedOption, true);
}
}
// Update widget's value after interactive selection
this._set("value", this.valueNode.value);
this._duringInteractiveSelection = false;
}.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) {
// Rerender the <select> content when the choices or the selection is programatically changed.
// However, the re-rendering must not be triggered while the user clicks items,
// because it would disturb user's interaction with a Select in
// multiple mode (#510): with more options than the available height, after
// scrolling and clicking an item, the rerendered Select may not have
// the same scroll amount as before the click, which isn't ergonomical.
// (Differently, in single selection mode, the popup closes right after
// the interactive selection.)
if ("renderItems" in props || ("selectedItems" in props && !this._duringInteractiveSelection)) {
// 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");
// to allow retrieving the data item from the option element
option.__dataItem = renderItem.__item; // __item is set by StoreMap.itemToRenderItem()
// to allow retrieving the option element from widget's selectedItems
// (which are data items, not render items).
option.__dataItem.__visualItem = option;
this.discardChanges(); // to avoid infinity loop
// 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);
} else if (has("ie") && renderItem.text !== undefined) { // #546
option.setAttribute("value", renderItem.text);
}
// The selection API (delite/Selection) needs to be called consistently
// for data items, not for render items.
// renderItem.__item is the data item instance for which
// StoreMap.itemToRenderItem() has created the render item.
// For now there is no public API for accessing it.
if (this.isSelected(renderItem.__item)) { // 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.
var index = this.valueNode.selectedIndex >= 0 ? this.valueNode.selectedIndex : 0;
this.selectedItem =
this.valueNode.options[index].__dataItem;
} // else for the native multi-select: it does not have any
// option selected by default.
// Initialize widget's value
this.value = this.valueNode.value;
}
}
},
getIdentity: dcl.superCall(function (sup) {
return function (dataItem) {
return sup.call(this, dataItem);
};
}),
value: dcl.prop({
set: function (value) {
if (this.valueNode) {
this.valueNode.value = value;
}
this._set("value", value);
},
get: function () {
return this._get("value");
},
enumerable: true,
configurable: true
}),
selectionMode: dcl.prop({
set: dcl.superCall(function (sup) {
// Override of the setter from delite/Selection to forbid the values
// "none" and "radio"
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);
};
}),
get: dcl.superCall(function (sup) {
return function () {
return sup.call(this);
};
}),
enumerable: true,
configurable: true
})
});
});