@oscarpalmer/oui 0.30.0 → 0.32.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/css/badge.css +1 -5
- package/dist/css/combobox.css +224 -0
- package/dist/css/form.css +18 -14
- package/dist/js/combobox.d.mts +65 -1
- package/dist/js/combobox.mjs +587 -206
- package/dist/js/focus-trap/focus-trap.embedded.mjs +3 -4
- package/dist/js/internal/draggable.mjs +7 -15
- package/dist/js/menu.d.mts +1 -1
- package/dist/js/menu.mjs +5 -2
- package/dist/js/movable/movable.embedded.mjs +6 -10
- package/dist/js/resizable/resizable.embedded.mjs +9 -16
- package/dist/js/sortable/sortable.embedded.mjs +21 -26
- package/dist/js/toolbar.d.mts +1 -1
- package/package.json +4 -4
- package/src/css/badge.scss +1 -18
- package/src/css/combobox.scss +192 -2
- package/src/css/form.scss +18 -80
- package/src/css/mixins.scss +78 -0
- package/src/js/combobox.ts +1053 -336
- package/src/js/focus-trap/focus-trap.embedded.ts +3 -7
- package/src/js/internal/draggable.ts +4 -10
- package/src/js/menu.ts +5 -2
- package/src/js/movable/movable.embedded.ts +3 -6
- package/src/js/resizable/resizable.embedded.ts +6 -19
- package/src/js/sortable/sortable.embedded.ts +17 -14
package/dist/js/combobox.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { createEmbeddedFloatable } from "./floatable/floatable.embedded.mjs";
|
|
2
2
|
import { clamp } from "@oscarpalmer/atoms/number";
|
|
3
|
-
import { getString } from "@oscarpalmer/atoms/string";
|
|
4
|
-
import { getAria, setAria } from "@oscarpalmer/toretto/aria";
|
|
5
|
-
import { getAttribute, setAttribute
|
|
3
|
+
import { getString, join } from "@oscarpalmer/atoms/string";
|
|
4
|
+
import { getAria, setAria, setRole } from "@oscarpalmer/toretto/aria";
|
|
5
|
+
import { getAttribute, setAttribute } from "@oscarpalmer/toretto/attribute";
|
|
6
6
|
import { createElement } from "@oscarpalmer/toretto/create";
|
|
7
7
|
import { dispatch, on } from "@oscarpalmer/toretto/event";
|
|
8
8
|
import { findAncestor } from "@oscarpalmer/toretto/find";
|
|
@@ -13,41 +13,122 @@ var OuiComboboxElement = class extends HTMLElement {
|
|
|
13
13
|
static formAssociated = true;
|
|
14
14
|
static observedAttributes = [
|
|
15
15
|
"disabled",
|
|
16
|
+
"max",
|
|
17
|
+
"min",
|
|
18
|
+
"readonly",
|
|
16
19
|
"required",
|
|
17
20
|
"value"
|
|
18
21
|
];
|
|
19
22
|
#internals;
|
|
20
23
|
#state;
|
|
24
|
+
/**
|
|
25
|
+
* Is the combobox disabled?
|
|
26
|
+
*/
|
|
21
27
|
get disabled() {
|
|
22
|
-
return this
|
|
28
|
+
return this.hasAttribute("disabled");
|
|
23
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* Should the combobox be disabled?
|
|
32
|
+
*/
|
|
24
33
|
set disabled(value) {
|
|
25
34
|
if (typeof value === "boolean" && value !== this.#state.disabled) setDisabled(this, this.#state, value);
|
|
26
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* Maximum number of selected options
|
|
38
|
+
*/
|
|
39
|
+
get max() {
|
|
40
|
+
return this.#state.max;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Set the maximum number of selected options
|
|
44
|
+
*/
|
|
45
|
+
set max(value) {
|
|
46
|
+
setLimit("max", this, this.#state, value);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Minimum number of selected options
|
|
50
|
+
*/
|
|
51
|
+
get min() {
|
|
52
|
+
return this.#state.min;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Set the minimum number of selected options
|
|
56
|
+
*/
|
|
57
|
+
set min(value) {
|
|
58
|
+
setLimit("min", this, this.#state, value);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Does the combobox allow multiple values?
|
|
62
|
+
*/
|
|
27
63
|
get multiple() {
|
|
28
64
|
return this.hasAttribute("multiple");
|
|
29
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* Name of the combobox
|
|
68
|
+
*/
|
|
30
69
|
get name() {
|
|
31
|
-
return
|
|
70
|
+
return getAttribute(this, "name") ?? "";
|
|
32
71
|
}
|
|
33
|
-
|
|
34
|
-
|
|
72
|
+
/**
|
|
73
|
+
* Is the combobox readonly?
|
|
74
|
+
*/
|
|
75
|
+
get readonly() {
|
|
76
|
+
return this.hasAttribute("readonly");
|
|
35
77
|
}
|
|
78
|
+
/**
|
|
79
|
+
* Should the combobox be readonly?
|
|
80
|
+
*/
|
|
81
|
+
set readonly(value) {
|
|
82
|
+
if (typeof value === "boolean" && value !== this.#state.readonly) setReadonly(this, this.#state, value);
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Is the combobox required?
|
|
86
|
+
*/
|
|
36
87
|
get required() {
|
|
37
88
|
return this.hasAttribute("required");
|
|
38
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* Should the combobox be required?
|
|
92
|
+
*/
|
|
39
93
|
set required(value) {
|
|
40
94
|
if (typeof value === "boolean" && value !== this.#state.required) setRequired(this, this.#state, value);
|
|
41
95
|
}
|
|
96
|
+
/**
|
|
97
|
+
* Selected options
|
|
98
|
+
*/
|
|
99
|
+
get selected() {
|
|
100
|
+
return this.#state.selected.slice();
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Type of combobox
|
|
104
|
+
*/
|
|
42
105
|
get type() {
|
|
43
106
|
return this.#state.type;
|
|
44
107
|
}
|
|
108
|
+
/**
|
|
109
|
+
* Current value
|
|
110
|
+
*/
|
|
45
111
|
get value() {
|
|
46
|
-
return this.#state.
|
|
112
|
+
return this.#state.values[0];
|
|
47
113
|
}
|
|
114
|
+
/**
|
|
115
|
+
* Set the value
|
|
116
|
+
*/
|
|
48
117
|
set value(value) {
|
|
49
118
|
const [option, all] = getOption(this.#state, value);
|
|
50
|
-
if (option != null) setNext(this, this.#state, all,
|
|
119
|
+
if (option != null) setNext("set", this, this.#state, all, option);
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Current values
|
|
123
|
+
*/
|
|
124
|
+
get values() {
|
|
125
|
+
return this.#state.values.slice();
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Set the values
|
|
129
|
+
*/
|
|
130
|
+
set values(value) {
|
|
131
|
+
setValues(this, this.#state, value);
|
|
51
132
|
}
|
|
52
133
|
constructor() {
|
|
53
134
|
super();
|
|
@@ -60,50 +141,210 @@ var OuiComboboxElement = class extends HTMLElement {
|
|
|
60
141
|
attributeChangedCallback(name, _, value) {
|
|
61
142
|
switch (name) {
|
|
62
143
|
case "disabled":
|
|
63
|
-
|
|
64
|
-
break;
|
|
144
|
+
case "readonly":
|
|
65
145
|
case "required":
|
|
66
|
-
this.required = this.hasAttribute("required");
|
|
67
|
-
break;
|
|
68
146
|
case "value":
|
|
69
|
-
this
|
|
147
|
+
this[name] = this.hasAttribute(name);
|
|
70
148
|
break;
|
|
149
|
+
case "max":
|
|
150
|
+
case "min":
|
|
151
|
+
setLimit(name, this, this.#state, value == null ? void 0 : Number.parseInt(value, 10));
|
|
152
|
+
break;
|
|
153
|
+
default: break;
|
|
71
154
|
}
|
|
72
155
|
}
|
|
156
|
+
clear() {
|
|
157
|
+
setValues(this, this.#state, []);
|
|
158
|
+
}
|
|
159
|
+
formResetCallback() {
|
|
160
|
+
this.#state.touched = false;
|
|
161
|
+
this.clear();
|
|
162
|
+
}
|
|
73
163
|
};
|
|
164
|
+
function createAnchor(type, id, label, select, autocomplete, multiple) {
|
|
165
|
+
const [anchor, selection] = type === "autocomplete" ? [createInput(id, select)] : createBox(id, select, multiple);
|
|
166
|
+
setAria(anchor, {
|
|
167
|
+
autocomplete: type === "autocomplete" ? autocomplete.value : void 0,
|
|
168
|
+
controls: `${id}_content`,
|
|
169
|
+
expanded: false,
|
|
170
|
+
haspopup: "listbox",
|
|
171
|
+
labelledby: label.id
|
|
172
|
+
});
|
|
173
|
+
setAttribute(anchor, ATTRIBUTE_ANCHOR, "");
|
|
174
|
+
setRole(anchor, "combobox");
|
|
175
|
+
return [anchor, selection];
|
|
176
|
+
}
|
|
177
|
+
function createBox(id, select, multiple) {
|
|
178
|
+
const box = createElement("div", {
|
|
179
|
+
id,
|
|
180
|
+
tabIndex: 0,
|
|
181
|
+
textContent: multiple ? "" : select.selectedOptions[0]?.textContent ?? ""
|
|
182
|
+
});
|
|
183
|
+
let selection;
|
|
184
|
+
if (multiple) {
|
|
185
|
+
selection = createSelection(select);
|
|
186
|
+
box.append(selection);
|
|
187
|
+
}
|
|
188
|
+
return [box, selection];
|
|
189
|
+
}
|
|
190
|
+
function createContent(type, id, label, select, autocomplete, multiple) {
|
|
191
|
+
const content = createElement("div", { id: `${id}_content` }, {
|
|
192
|
+
[ATTRIBUTE_CONTENT]: "",
|
|
193
|
+
popover: "",
|
|
194
|
+
tabindex: -1,
|
|
195
|
+
"aria-labelledby": label.id
|
|
196
|
+
});
|
|
197
|
+
const [filter, input] = createFilter(type, id) ?? [];
|
|
198
|
+
const [list, headers, selected] = createList(label, select, autocomplete, multiple);
|
|
199
|
+
const empty = createEmpty();
|
|
200
|
+
if (filter != null) content.append(filter);
|
|
201
|
+
content.append(list, empty);
|
|
202
|
+
return [
|
|
203
|
+
content,
|
|
204
|
+
input,
|
|
205
|
+
headers,
|
|
206
|
+
selected,
|
|
207
|
+
empty
|
|
208
|
+
];
|
|
209
|
+
}
|
|
210
|
+
function createEmpty() {
|
|
211
|
+
return createElement("div", {
|
|
212
|
+
hidden: true,
|
|
213
|
+
role: "status",
|
|
214
|
+
textContent: "No results found"
|
|
215
|
+
}, {
|
|
216
|
+
[ATTRIBUTE_EMPTY]: "",
|
|
217
|
+
"aria-live": "polite"
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
function createFilter(type, id) {
|
|
221
|
+
if (type === "autocomplete") return;
|
|
222
|
+
const container = createElement("div", {}, { [ATTRIBUTE_FILTER]: "" });
|
|
223
|
+
const label = createElement("label", {
|
|
224
|
+
htmlFor: `${id}_filter`,
|
|
225
|
+
textContent: "Filter options"
|
|
226
|
+
}, { [ATTRIBUTE_FILTER_LABEL]: "" });
|
|
227
|
+
const filter = createElement("input", { id: `${id}_filter` }, { [ATTRIBUTE_FILTER_INPUT]: "" });
|
|
228
|
+
container.append(label, filter);
|
|
229
|
+
return [container, filter];
|
|
230
|
+
}
|
|
231
|
+
function createHeader(group) {
|
|
232
|
+
return createElement("li", {
|
|
233
|
+
id: group.id,
|
|
234
|
+
role: "presentation",
|
|
235
|
+
textContent: group.label
|
|
236
|
+
}, { [ATTRIBUTE_HEADER]: "" });
|
|
237
|
+
}
|
|
238
|
+
function createInput(id, select) {
|
|
239
|
+
return createElement("input", {
|
|
240
|
+
id,
|
|
241
|
+
value: select.selectedOptions[0]?.textContent ?? ""
|
|
242
|
+
}, {
|
|
243
|
+
[ATTRIBUTE_ANCHOR]: "",
|
|
244
|
+
autocomplete: "off",
|
|
245
|
+
type: "text"
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
function createList(label, select, autocomplete, multiple) {
|
|
249
|
+
const list = createElement("ul", { role: "listbox" }, {
|
|
250
|
+
[ATTRIBUTE_LIST]: "",
|
|
251
|
+
"aria-labelledby": label.id,
|
|
252
|
+
"aria-multiselectable": multiple
|
|
253
|
+
});
|
|
254
|
+
const groups = [...select.querySelectorAll("optgroup")];
|
|
255
|
+
const groupsLength = groups.length;
|
|
256
|
+
const selected = [];
|
|
257
|
+
let headers;
|
|
258
|
+
if (groupsLength > 0) {
|
|
259
|
+
headers = [];
|
|
260
|
+
let firstGroup;
|
|
261
|
+
for (let groupIndex = 0; groupIndex < groupsLength; groupIndex += 1) {
|
|
262
|
+
const group = groups[groupIndex];
|
|
263
|
+
const options = [...group.querySelectorAll("option")];
|
|
264
|
+
if (options.length === 0) continue;
|
|
265
|
+
firstGroup ??= groupIndex;
|
|
266
|
+
const header = createHeader(group);
|
|
267
|
+
headers.push(header);
|
|
268
|
+
list.append(header);
|
|
269
|
+
createOptions(options, autocomplete, list, selected, groupIndex === firstGroup, header.id);
|
|
270
|
+
}
|
|
271
|
+
} else createOptions([...select.options], autocomplete, list, selected, true);
|
|
272
|
+
return [
|
|
273
|
+
list,
|
|
274
|
+
headers,
|
|
275
|
+
selected
|
|
276
|
+
];
|
|
277
|
+
}
|
|
278
|
+
function createOption(index, option, autocomplete, active, group) {
|
|
279
|
+
return createElement("li", {
|
|
280
|
+
id: option.id,
|
|
281
|
+
role: "option",
|
|
282
|
+
textContent: option.textContent
|
|
283
|
+
}, {
|
|
284
|
+
group,
|
|
285
|
+
[ATTRIBUTE_OPTION]: "",
|
|
286
|
+
[ATTRIBUTE_OPTION_ACTIVE]: active && index === 0 ? "" : void 0,
|
|
287
|
+
value: option.value,
|
|
288
|
+
"aria-selected": !autocomplete.none && option.selected
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
function createOptions(options, autocomplete, list, selected, active, group) {
|
|
292
|
+
for (let index = 0; index < options.length; index += 1) {
|
|
293
|
+
const option = options[index];
|
|
294
|
+
const item = createOption(index, option, autocomplete, active, group);
|
|
295
|
+
if (option.selected) selected.push(item);
|
|
296
|
+
list.append(item);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
function createSelection(select) {
|
|
300
|
+
const selection = createElement("ul", {}, { [ATTRIBUTE_SELECTION]: "" });
|
|
301
|
+
for (const option of select.selectedOptions) selection.append(getSelectionItem(option));
|
|
302
|
+
return selection;
|
|
303
|
+
}
|
|
74
304
|
function filterOptions(state, options, clear) {
|
|
75
|
-
if (clear || state.autocomplete.none) state.filter = "";
|
|
76
|
-
const filter = state.filter.toLocaleLowerCase();
|
|
305
|
+
if (clear || state.autocomplete.none) state.filter.value = "";
|
|
306
|
+
const filter = state.filter.value.toLocaleLowerCase();
|
|
307
|
+
const groups = state.headers?.reduce((record, header) => {
|
|
308
|
+
record[header.id] = 0;
|
|
309
|
+
return record;
|
|
310
|
+
}, {}) ?? {};
|
|
77
311
|
const all = options ?? getOptions(state)[0];
|
|
78
|
-
|
|
312
|
+
let { length } = all;
|
|
79
313
|
const visible = [];
|
|
80
314
|
for (let index = 0; index < length; index += 1) {
|
|
81
315
|
const option = all[index];
|
|
82
316
|
option.hidden = option.textContent?.toLocaleLowerCase().includes(filter) === false;
|
|
83
|
-
if (!option.hidden)
|
|
317
|
+
if (!option.hidden) {
|
|
318
|
+
groups[getAttribute(option, "group") ?? ""] += 1;
|
|
319
|
+
visible.push(option);
|
|
320
|
+
}
|
|
84
321
|
}
|
|
85
|
-
if (
|
|
322
|
+
if (state.headers != null) {
|
|
323
|
+
length = state.headers.length;
|
|
324
|
+
for (let index = 0; index < length; index += 1) {
|
|
325
|
+
const header = state.headers[index];
|
|
326
|
+
header.hidden = groups[header.id] === 0;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
state.empty.hidden = visible.length > 0;
|
|
330
|
+
if (state.empty.hidden) return state.active != null && visible.includes(state.active) ? state.active : visible[0];
|
|
86
331
|
}
|
|
87
|
-
function
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
[
|
|
92
|
-
"
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
return anchor;
|
|
332
|
+
function generateIds(select, prefix) {
|
|
333
|
+
const elements = [...select.querySelectorAll("optgroup"), ...select.options];
|
|
334
|
+
const { length } = elements;
|
|
335
|
+
for (let index = 0; index < length; index += 1) {
|
|
336
|
+
const element = elements[index];
|
|
337
|
+
if (element.id === "") element.id = `${prefix}${element instanceof HTMLOptGroupElement ? "_group_" : "_option_"}${index}`;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
function getAttributeValue(element, attribute, possible, defaultValue) {
|
|
341
|
+
const value = getAttribute(element, attribute);
|
|
342
|
+
return possible.has(value) ? value : defaultValue;
|
|
99
343
|
}
|
|
100
344
|
function getAutoComplete(element, select) {
|
|
101
345
|
let value;
|
|
102
346
|
if (select) value = "list";
|
|
103
|
-
else
|
|
104
|
-
const attribute = getAttribute(element, "autocomplete");
|
|
105
|
-
value = AUTOCOMPLETES.has(attribute) ? attribute : "none";
|
|
106
|
-
}
|
|
347
|
+
else value = getAttributeValue(element, "autocomplete", AUTOCOMPLETES, "none");
|
|
107
348
|
return {
|
|
108
349
|
value,
|
|
109
350
|
both: value === "both",
|
|
@@ -112,62 +353,30 @@ function getAutoComplete(element, select) {
|
|
|
112
353
|
none: value === "none"
|
|
113
354
|
};
|
|
114
355
|
}
|
|
115
|
-
function
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
});
|
|
356
|
+
function getFormValue(state, values) {
|
|
357
|
+
if (values.length === 0 || !state.multiple) return values[0];
|
|
358
|
+
let formValues = [];
|
|
359
|
+
for (const value of values) formValues.push(`${state.name}=${value}`);
|
|
360
|
+
return join(formValues, "&");
|
|
121
361
|
}
|
|
122
|
-
function
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
});
|
|
130
|
-
const list = createElement("ul", {}, { [ATTRIBUTE_LIST]: "" });
|
|
131
|
-
const options = [...select.options];
|
|
132
|
-
const selected = [];
|
|
133
|
-
for (let index = 0; index < options.length; index += 1) {
|
|
134
|
-
const option = options[index];
|
|
135
|
-
const item = createElement("li", {
|
|
136
|
-
id: `${id}_option_${index}`,
|
|
137
|
-
textContent: option.textContent
|
|
138
|
-
}, {
|
|
139
|
-
[ATTRIBUTE_OPTION]: "",
|
|
140
|
-
[ATTRIBUTE_OPTION_CURRENT]: index === 0 ? "" : void 0,
|
|
141
|
-
role: "option",
|
|
142
|
-
value: option.value,
|
|
143
|
-
"aria-selected": !autocomplete.none && option.selected
|
|
144
|
-
});
|
|
145
|
-
if (option.selected) selected.push(item);
|
|
146
|
-
list.append(item);
|
|
147
|
-
}
|
|
148
|
-
content.append(list);
|
|
149
|
-
return [content, selected];
|
|
362
|
+
function getFormValues(state, added) {
|
|
363
|
+
let values;
|
|
364
|
+
if (state.multiple) values = typeof added === "boolean" ? state.selected.map((option) => getAttribute(option, "value") ?? "") : state.values;
|
|
365
|
+
else values = [getAttribute(state.selected[0], "value") ?? ""];
|
|
366
|
+
const previous = getFormValue(state, state.values);
|
|
367
|
+
const next = getFormValue(state, values);
|
|
368
|
+
return [values, next !== previous];
|
|
150
369
|
}
|
|
151
370
|
function getId() {
|
|
152
371
|
index += 1;
|
|
153
372
|
return `oui_combobox_${index}`;
|
|
154
373
|
}
|
|
155
|
-
function getInput(id, element, label, select) {
|
|
156
|
-
return createElement("input", {
|
|
157
|
-
id,
|
|
158
|
-
value: select.selectedOptions[0]?.textContent ?? ""
|
|
159
|
-
}, {
|
|
160
|
-
[ATTRIBUTE_ANCHOR]: "",
|
|
161
|
-
autocomplete: "off",
|
|
162
|
-
type: "text"
|
|
163
|
-
});
|
|
164
|
-
}
|
|
165
374
|
function getOption(state, value) {
|
|
166
375
|
const [all] = getOptions(state);
|
|
167
|
-
const valueAsString = getString(value);
|
|
376
|
+
const valueAsString = getString(value).toLocaleLowerCase();
|
|
168
377
|
filterOptions(state, all, true);
|
|
169
378
|
let match;
|
|
170
|
-
for (const option of all) if (
|
|
379
|
+
for (const option of all) if (getAttribute(option, "value")?.toLocaleLowerCase() === valueAsString) {
|
|
171
380
|
match = option;
|
|
172
381
|
break;
|
|
173
382
|
}
|
|
@@ -179,32 +388,50 @@ function getOptions(state) {
|
|
|
179
388
|
}
|
|
180
389
|
function getSelected(update, state, element, next) {
|
|
181
390
|
if (state.autocomplete.none) return false;
|
|
391
|
+
const selected = getAria(element, "selected") === "true";
|
|
182
392
|
if (update === "navigation") {
|
|
183
393
|
if (state.autocomplete.both) return element === next;
|
|
184
|
-
return
|
|
394
|
+
return selected;
|
|
185
395
|
}
|
|
186
|
-
if (update === "none") return
|
|
187
|
-
if (update === "set")
|
|
188
|
-
|
|
396
|
+
if (update === "none") return selected;
|
|
397
|
+
if (update === "set") {
|
|
398
|
+
if (next == null) return selected;
|
|
399
|
+
if (state.multiple) return element === next ? !selected : selected;
|
|
400
|
+
return element === next;
|
|
401
|
+
}
|
|
402
|
+
return selected;
|
|
403
|
+
}
|
|
404
|
+
function getSelectionItem(option) {
|
|
405
|
+
const icon = createElement("span", { innerHTML: "×" }, { "aria-hidden": true });
|
|
406
|
+
const label = createElement("span", { textContent: option.textContent });
|
|
407
|
+
const item = createElement("li", {}, {
|
|
408
|
+
[ATTRIBUTE_SELECTION_ITEM]: "",
|
|
409
|
+
option: option.id
|
|
410
|
+
});
|
|
411
|
+
item.append(icon, label);
|
|
412
|
+
return item;
|
|
189
413
|
}
|
|
190
414
|
function getState(element, internals) {
|
|
191
415
|
const select = element.querySelector("select");
|
|
192
|
-
if (select == null) throw new Error(MESSAGE_SELECT);
|
|
416
|
+
if (select == null || select.id === "") throw new Error(MESSAGE_SELECT);
|
|
193
417
|
const label = element.querySelector(`label[for="${select.id}"]`);
|
|
194
418
|
if (label == null) throw new Error(MESSAGE_LABEL);
|
|
195
419
|
select.hidden = true;
|
|
196
420
|
element.id = select.id;
|
|
197
|
-
element
|
|
421
|
+
setAttribute(element, "name", select.name === "" ? select.id : select.name);
|
|
198
422
|
const disabled = element.disabled || select.disabled;
|
|
199
423
|
const multiple = element.multiple || select.multiple;
|
|
424
|
+
const readonly = element.readonly;
|
|
200
425
|
const required = element.required || select.required;
|
|
201
426
|
const id = getId();
|
|
202
|
-
const type = multiple ? "select" :
|
|
427
|
+
const type = multiple ? "select" : getAttributeValue(element, "type", TYPES, "select");
|
|
203
428
|
const autocomplete = getAutoComplete(element, type === "select");
|
|
204
429
|
label.htmlFor = id;
|
|
205
430
|
label.id = `${id}_label`;
|
|
206
|
-
|
|
207
|
-
const [
|
|
431
|
+
generateIds(select, id);
|
|
432
|
+
const [anchor, selection] = createAnchor(type, id, label, select, autocomplete, multiple);
|
|
433
|
+
const [content, filter, headers, selected, empty] = createContent(type, id, label, select, autocomplete, multiple);
|
|
434
|
+
const values = selected.length === 0 ? [""] : selected.map((option) => getAttribute(option, "value") ?? "");
|
|
208
435
|
const floatable = createEmbeddedFloatable(anchor, content, {
|
|
209
436
|
position: "below-start",
|
|
210
437
|
reusable: false
|
|
@@ -218,53 +445,118 @@ function getState(element, internals) {
|
|
|
218
445
|
autocomplete,
|
|
219
446
|
content,
|
|
220
447
|
disabled,
|
|
448
|
+
empty,
|
|
221
449
|
floatable,
|
|
450
|
+
headers,
|
|
222
451
|
id,
|
|
223
452
|
internals,
|
|
224
453
|
label,
|
|
225
454
|
multiple,
|
|
455
|
+
readonly,
|
|
226
456
|
required,
|
|
227
457
|
select,
|
|
228
458
|
selected,
|
|
459
|
+
selection,
|
|
229
460
|
type,
|
|
230
|
-
|
|
461
|
+
values,
|
|
462
|
+
active: selected[0],
|
|
463
|
+
filter: {
|
|
464
|
+
element: filter,
|
|
465
|
+
value: ""
|
|
466
|
+
},
|
|
231
467
|
listeners: [],
|
|
232
|
-
|
|
468
|
+
name: element.name,
|
|
469
|
+
touched: false
|
|
233
470
|
};
|
|
234
|
-
state.listeners.push(on(
|
|
471
|
+
state.listeners.push(on(element, "focusout", () => {
|
|
472
|
+
state.touched = true;
|
|
473
|
+
updateValidity(state);
|
|
474
|
+
}), on(content, "click", (event) => {
|
|
235
475
|
onContent(event, content);
|
|
236
476
|
}), on(content, "toggle", () => {
|
|
237
477
|
setAria(anchor, {
|
|
238
478
|
activedescendant: floatable.open ? state.active?.id : void 0,
|
|
239
479
|
expanded: floatable.open
|
|
240
480
|
});
|
|
241
|
-
if (floatable.open)
|
|
481
|
+
if (floatable.open) {
|
|
482
|
+
state.filter.element?.focus();
|
|
483
|
+
updateWidth(state);
|
|
484
|
+
}
|
|
242
485
|
}));
|
|
243
486
|
setDisabled(element, state, disabled);
|
|
487
|
+
setLimit("max", element, state, getAttribute(element, "max"));
|
|
488
|
+
setLimit("min", element, state, getAttribute(element, "min"));
|
|
489
|
+
setReadonly(element, state, readonly);
|
|
490
|
+
setRequired(element, state, required);
|
|
244
491
|
return state;
|
|
245
492
|
}
|
|
246
|
-
function
|
|
247
|
-
|
|
248
|
-
|
|
493
|
+
function hasTextSelection(element) {
|
|
494
|
+
return element instanceof HTMLInputElement && element.selectionStart !== element.selectionEnd;
|
|
495
|
+
}
|
|
496
|
+
function onClick(event) {
|
|
497
|
+
const combobox = findAncestor(event, TAGNAME);
|
|
498
|
+
const state = states.get(combobox);
|
|
499
|
+
if (combobox == null || state == null) return;
|
|
500
|
+
const label = findAncestor(event, "label");
|
|
501
|
+
if (label != null) {
|
|
502
|
+
if (label === state.label && combobox.type === "select") {
|
|
503
|
+
state.anchor.focus();
|
|
504
|
+
state.floatable.show();
|
|
505
|
+
}
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
const selection = findAncestor(event, `[${ATTRIBUTE_SELECTION_ITEM}]`);
|
|
509
|
+
if (selection != null) {
|
|
510
|
+
setValue("set", combobox, state, state.selected.find((option) => option.id === getAttribute(selection, "option")));
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
if (findAncestor(event, SELECTOR_ANCHOR) != null) {
|
|
514
|
+
state.floatable.show();
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
function onContent(event, content) {
|
|
519
|
+
const combobox = comboboxes.get(content);
|
|
520
|
+
const state = states.get(content);
|
|
521
|
+
if (combobox == null || state == null) return;
|
|
522
|
+
const option = findAncestor(event, SELECTOR_OPTION);
|
|
523
|
+
if (option instanceof HTMLElement) setNext("set", combobox, state, getOptions(state)[0], option);
|
|
524
|
+
(state.filter.element ?? state.anchor).focus();
|
|
525
|
+
}
|
|
526
|
+
function onEnter(combobox, state) {
|
|
527
|
+
if (hasTextSelection(state.anchor)) {
|
|
528
|
+
onSelection(combobox, state);
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
if (!state.floatable.open) {
|
|
532
|
+
state.floatable.show();
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
setNext("set", combobox, state, getOptions(state)[0], state.active);
|
|
536
|
+
}
|
|
537
|
+
function onEscape(state) {
|
|
538
|
+
state.floatable.hide();
|
|
249
539
|
}
|
|
250
|
-
function
|
|
251
|
-
if (event.ctrlKey || event.metaKey
|
|
540
|
+
function onInput(event, handler) {
|
|
541
|
+
if (event.ctrlKey || event.metaKey) return;
|
|
252
542
|
const combobox = findAncestor(event, TAGNAME);
|
|
253
543
|
const state = states.get(combobox);
|
|
254
544
|
if (combobox == null || state == null) return;
|
|
255
|
-
if (findAncestor(event, SELECTOR_ANCHOR) != null) handler(event, combobox, state);
|
|
545
|
+
if ((findAncestor(event, SELECTOR_ANCHOR) ?? findAncestor(event, SELECTOR_FILTER)) != null) handler(event, combobox, state);
|
|
256
546
|
}
|
|
257
|
-
function
|
|
547
|
+
function onInputKeydown(event, combobox, state) {
|
|
548
|
+
const isSelect = state.type === "select";
|
|
258
549
|
let preventDefault = true;
|
|
259
550
|
switch (true) {
|
|
260
551
|
case event.key === "Enter":
|
|
261
552
|
onEnter(combobox, state);
|
|
262
553
|
break;
|
|
263
554
|
case event.key === "Escape":
|
|
264
|
-
onEscape(
|
|
555
|
+
onEscape(state);
|
|
265
556
|
break;
|
|
266
|
-
case
|
|
267
|
-
|
|
557
|
+
case !isSelect && KEYS_SELECTION.has(event.key):
|
|
558
|
+
preventDefault = false;
|
|
559
|
+
if (hasTextSelection(state.anchor)) onSelection(combobox, state);
|
|
268
560
|
break;
|
|
269
561
|
case KEYS_NAVIGATION.has(event.key):
|
|
270
562
|
onNavigation(event, combobox, state);
|
|
@@ -279,67 +571,24 @@ function onAnchorKeydown(event, combobox, state) {
|
|
|
279
571
|
event.stopPropagation();
|
|
280
572
|
}
|
|
281
573
|
}
|
|
282
|
-
function
|
|
283
|
-
|
|
284
|
-
let preventDefault = true;
|
|
574
|
+
function onInputKeyup(event, combobox, state) {
|
|
575
|
+
const filter = state.filter.element ?? state.anchor;
|
|
285
576
|
const typed = event.key.length === 1 && EXPRESSION_TYPED.test(event.key);
|
|
286
|
-
if (typed) state.filter
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
case KEYS_REMOVE.has(event.key):
|
|
296
|
-
onRemove(combobox, state);
|
|
297
|
-
break;
|
|
298
|
-
default:
|
|
299
|
-
preventDefault = false;
|
|
300
|
-
break;
|
|
301
|
-
}
|
|
302
|
-
if (preventDefault) {
|
|
303
|
-
event.preventDefault();
|
|
304
|
-
event.stopPropagation();
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
function onClick(event) {
|
|
308
|
-
const combobox = findAncestor(event, TAGNAME);
|
|
309
|
-
const state = states.get(combobox);
|
|
310
|
-
if (combobox == null || state == null) return;
|
|
311
|
-
const label = findAncestor(event, "label");
|
|
312
|
-
if (label != null) {
|
|
313
|
-
if (label === state.label && combobox.type === "select") state.anchor.focus();
|
|
314
|
-
return;
|
|
315
|
-
}
|
|
316
|
-
if (findAncestor(event, SELECTOR_ANCHOR) != null) {
|
|
317
|
-
state.floatable.show();
|
|
318
|
-
return;
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
function onContent(event, content) {
|
|
322
|
-
const combobox = comboboxes.get(content);
|
|
323
|
-
const state = states.get(content);
|
|
324
|
-
if (combobox == null || state == null) return;
|
|
325
|
-
const option = findAncestor(event, SELECTOR_OPTION);
|
|
326
|
-
if (option instanceof HTMLElement) setNext(combobox, state, getOptions(state)[0], "set", option);
|
|
327
|
-
state.anchor.focus();
|
|
328
|
-
}
|
|
329
|
-
function onEnter(combobox, state) {
|
|
330
|
-
if (state.floatable.open) setNext(combobox, state, getOptions(state)[0], "set", state.active);
|
|
331
|
-
else state.floatable.show();
|
|
332
|
-
}
|
|
333
|
-
function onEscape(combobox, state) {
|
|
334
|
-
if (!state.floatable.open) return;
|
|
335
|
-
if (state.type === "select") state.floatable.hide();
|
|
336
|
-
else setNext(combobox, state, getOptions(state)[0], "set", state.active);
|
|
577
|
+
if (!(filter instanceof HTMLInputElement) || !(typed || KEYS_REMOVE.has(event.key) || filter.value.length < state.filter.value.length)) return;
|
|
578
|
+
event.preventDefault();
|
|
579
|
+
event.stopPropagation();
|
|
580
|
+
if (typed) state.filter.value += event.key;
|
|
581
|
+
else state.filter.value = filter.value;
|
|
582
|
+
const [options] = getOptions(state);
|
|
583
|
+
const next = filterOptions(state, options);
|
|
584
|
+
if (typed) onTyped(combobox, state, options, next);
|
|
585
|
+
else setNext("remove", combobox, state, options, next);
|
|
337
586
|
}
|
|
338
587
|
function onKeydown(event) {
|
|
339
|
-
|
|
588
|
+
onInput(event, onInputKeydown);
|
|
340
589
|
}
|
|
341
590
|
function onKeyup(event) {
|
|
342
|
-
|
|
591
|
+
onInput(event, onInputKeyup);
|
|
343
592
|
}
|
|
344
593
|
function onNavigation(event, combobox, state) {
|
|
345
594
|
let skip = false;
|
|
@@ -350,7 +599,7 @@ function onNavigation(event, combobox, state) {
|
|
|
350
599
|
const [all, visible] = getOptions(state);
|
|
351
600
|
if (visible.length === 0) return;
|
|
352
601
|
let next;
|
|
353
|
-
if (skip
|
|
602
|
+
if (skip || state.active == null) next = state.active ?? visible.at(event.key === "ArrowDown" ? 0 : -1);
|
|
354
603
|
else {
|
|
355
604
|
const current = visible.findIndex((option) => option.id === state.active?.id);
|
|
356
605
|
let index;
|
|
@@ -373,71 +622,197 @@ function onNavigation(event, combobox, state) {
|
|
|
373
622
|
}
|
|
374
623
|
if (index != null) next = visible[index];
|
|
375
624
|
}
|
|
376
|
-
setNext(
|
|
625
|
+
setNext(state.autocomplete.none ? "none" : "navigation", combobox, state, all, next);
|
|
377
626
|
}
|
|
378
|
-
function
|
|
379
|
-
|
|
380
|
-
|
|
627
|
+
function onSelection(combobox, state) {
|
|
628
|
+
state.filter.value = state.anchor instanceof HTMLInputElement ? state.anchor.value : "";
|
|
629
|
+
filterOptions(state);
|
|
630
|
+
setNext("set", combobox, state, getOptions(state)[0], state.active);
|
|
381
631
|
}
|
|
382
632
|
function onTab(combobox, state) {
|
|
383
633
|
if (combobox !== document.activeElement || !combobox.contains(document.activeElement)) state.floatable.hide();
|
|
384
634
|
}
|
|
385
|
-
function onTyped(combobox, state) {
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
const next = filterOptions(state);
|
|
391
|
-
state.active = next != null && next.textContent?.toLocaleLowerCase().indexOf(state.filter.toLocaleLowerCase()) === 0 ? next : void 0;
|
|
392
|
-
if (next != null) open();
|
|
393
|
-
setNext(combobox, state, getOptions(state)[0], "typed", next);
|
|
394
|
-
} else open();
|
|
635
|
+
function onTyped(combobox, state, options, next) {
|
|
636
|
+
const filter = state.filter.element ?? state.anchor;
|
|
637
|
+
if (!state.autocomplete.inline && filter instanceof HTMLInputElement && filter.value.length > 0) state.floatable.show();
|
|
638
|
+
const all = options ?? getOptions(state)[0];
|
|
639
|
+
setNext("typed", combobox, state, all, state.autocomplete.none ? state.active ?? all[0] : next ?? filterOptions(state, options));
|
|
395
640
|
}
|
|
396
641
|
function setDisabled(combobox, state, value) {
|
|
397
|
-
|
|
398
|
-
setAria(state.anchor, "disabled", value);
|
|
399
|
-
setProperty(combobox, "disabled", value);
|
|
400
|
-
if (state.anchor instanceof HTMLInputElement) state.anchor.readOnly = value;
|
|
642
|
+
setProperties("disabled", "disabled", combobox, state, value);
|
|
401
643
|
}
|
|
402
|
-
function
|
|
403
|
-
|
|
644
|
+
function setLimit(type, combobox, state, value) {
|
|
645
|
+
if (typeof value === "number" && !Number.isNaN(value) ? value !== state[type] : value == null) {
|
|
646
|
+
state[type] = value;
|
|
647
|
+
setAttribute(combobox, type, value);
|
|
648
|
+
updateValidity(state);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
function setNext(update, combobox, state, all, next) {
|
|
652
|
+
for (const option of all) setAttribute(option, ATTRIBUTE_OPTION_ACTIVE, option === next ? "" : void 0);
|
|
404
653
|
state.active = next;
|
|
405
654
|
setAria(state.anchor, "activedescendant", next?.id);
|
|
406
655
|
next?.scrollIntoView(scrollOptions);
|
|
407
|
-
setValue(combobox, state,
|
|
656
|
+
setValue(update, combobox, state, next);
|
|
657
|
+
}
|
|
658
|
+
function setProperties(key, property, combobox, state, value) {
|
|
659
|
+
state[key] = value;
|
|
660
|
+
setAria(state.anchor, key, value);
|
|
661
|
+
setProperty(combobox, key, value);
|
|
662
|
+
if (state.filter.element != null) state.filter.element[property] = value;
|
|
663
|
+
if (state.anchor instanceof HTMLInputElement) state.anchor[property] = value;
|
|
664
|
+
}
|
|
665
|
+
function setReadonly(combobox, state, value) {
|
|
666
|
+
setProperties("readonly", "readOnly", combobox, state, value);
|
|
408
667
|
}
|
|
409
668
|
function setRequired(combobox, state, value) {
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
669
|
+
setProperties("required", "required", combobox, state, value);
|
|
670
|
+
updateValidity(state);
|
|
671
|
+
}
|
|
672
|
+
function setValue(update, combobox, state, next) {
|
|
673
|
+
if (state.disabled || state.readonly || state.type === "select" && update !== "set") return;
|
|
674
|
+
let added;
|
|
675
|
+
let shouldUpdate;
|
|
676
|
+
let values;
|
|
677
|
+
if (update === "set" || !(state.anchor instanceof HTMLInputElement)) {
|
|
678
|
+
added = updateSelected(update, state, next);
|
|
679
|
+
[values, shouldUpdate] = getFormValues(state, added);
|
|
680
|
+
} else {
|
|
681
|
+
const [all] = getOptions(state);
|
|
682
|
+
for (const option of all) setAria(option, "selected", false);
|
|
683
|
+
const input = state.anchor.value;
|
|
684
|
+
const content = next?.textContent ?? "";
|
|
685
|
+
const value = next == null ? "" : getAttribute(next, "value") ?? "";
|
|
686
|
+
let selected;
|
|
687
|
+
if (content.toLocaleLowerCase() === input.toLocaleLowerCase()) {
|
|
688
|
+
values = [value];
|
|
689
|
+
if (next != null) {
|
|
690
|
+
selected = next;
|
|
691
|
+
setAria(next, "selected", true);
|
|
692
|
+
}
|
|
693
|
+
} else values = [state.autocomplete.none ? input : ""];
|
|
694
|
+
state.selected = selected == null ? [] : [selected];
|
|
695
|
+
shouldUpdate = state.values[0] !== values[0];
|
|
696
|
+
}
|
|
697
|
+
if (shouldUpdate) updateForm(combobox, state, values);
|
|
698
|
+
if (next == null) return;
|
|
699
|
+
if (state.type === "select") updateSelect(update, state, next, added);
|
|
700
|
+
else if (update !== "remove") updateAutoComplete(update, state, next);
|
|
701
|
+
if (update === "set" && !state.multiple) state.floatable.hide();
|
|
702
|
+
setTimeout(() => {
|
|
703
|
+
updateWidth(state);
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
function setValues(combobox, state, value) {
|
|
707
|
+
if (!Array.isArray(value)) return;
|
|
708
|
+
const strings = (state.multiple ? value : [value]).map((value) => getString(value).toLocaleLowerCase());
|
|
709
|
+
const [all] = getOptions(state);
|
|
710
|
+
const selected = [];
|
|
711
|
+
const values = [];
|
|
712
|
+
let next;
|
|
713
|
+
for (const option of all) {
|
|
714
|
+
const value = getAttribute(option, "value");
|
|
715
|
+
if (value == null || !strings.includes(value.toLocaleLowerCase())) continue;
|
|
716
|
+
selected.push(option);
|
|
717
|
+
values.push(value);
|
|
718
|
+
next ??= option;
|
|
719
|
+
}
|
|
720
|
+
for (const option of state.selected) {
|
|
721
|
+
setAria(option, "selected", false);
|
|
722
|
+
state.selection?.querySelector(`[option="${option.id}"]`)?.remove();
|
|
723
|
+
}
|
|
724
|
+
for (const option of selected) {
|
|
725
|
+
setAria(option, "selected", true);
|
|
726
|
+
state.selection?.append(getSelectionItem(option));
|
|
727
|
+
}
|
|
728
|
+
state.selected = selected.slice();
|
|
729
|
+
setNext("navigation", combobox, state, all, next);
|
|
730
|
+
updateForm(combobox, state, values);
|
|
731
|
+
}
|
|
732
|
+
function updateAutoComplete(update, state, next) {
|
|
733
|
+
if (!(update === "set" || state.autocomplete.both || state.autocomplete.inline)) return;
|
|
734
|
+
const content = next.textContent ?? "";
|
|
735
|
+
if (update === "typed" && !content.toLocaleLowerCase().startsWith(state.filter.value.toLocaleLowerCase())) return;
|
|
736
|
+
const { length } = content;
|
|
737
|
+
if (state.anchor instanceof HTMLInputElement) {
|
|
738
|
+
state.anchor.value = content;
|
|
739
|
+
state.anchor.setSelectionRange(update === "typed" ? state.filter.value.length : length, length);
|
|
740
|
+
} else state.anchor.textContent = content;
|
|
413
741
|
}
|
|
414
|
-
function
|
|
415
|
-
|
|
742
|
+
function updateForm(combobox, state, values) {
|
|
743
|
+
const formValues = values.length === 0 ? [""] : values;
|
|
744
|
+
const formData = new FormData();
|
|
745
|
+
for (const value of formValues) formData.append(state.name, value);
|
|
746
|
+
state.values = formValues;
|
|
747
|
+
state.internals.setFormValue(formData);
|
|
748
|
+
updateValidity(state);
|
|
749
|
+
dispatch(combobox, "change");
|
|
750
|
+
}
|
|
751
|
+
function updateSelect(update, state, next, added) {
|
|
752
|
+
if (update !== "set") return;
|
|
753
|
+
if (!state.multiple) {
|
|
754
|
+
state.anchor.textContent = next.textContent ?? "";
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
if (added === true) state.selection?.append(getSelectionItem(next));
|
|
758
|
+
else if (added === false) state.selection?.querySelector(`[option="${next.id}"]`)?.remove();
|
|
759
|
+
}
|
|
760
|
+
function updateSelected(update, state, next) {
|
|
416
761
|
const [all] = getOptions(state);
|
|
417
762
|
for (const option of all) setAria(option, "selected", getSelected(update, state, option, next));
|
|
418
|
-
|
|
419
|
-
if (state.
|
|
420
|
-
state.
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
763
|
+
if (next == null) return;
|
|
764
|
+
if (state.type !== "select") {
|
|
765
|
+
state.selected = [next];
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
if (update !== "set") return;
|
|
769
|
+
if (!state.multiple) {
|
|
770
|
+
state.selected = [next];
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
const index = state.selected.indexOf(next);
|
|
774
|
+
const added = index === -1;
|
|
775
|
+
if (added) state.selected.push(next);
|
|
776
|
+
else state.selected.splice(index, 1);
|
|
777
|
+
return added;
|
|
778
|
+
}
|
|
779
|
+
function updateValidity(state) {
|
|
780
|
+
if (state.autocomplete.none) {
|
|
781
|
+
const valueMissing = state.required && state.values[0].length === 0;
|
|
782
|
+
state.internals.setValidity({ valueMissing }, INVALID_INPUT);
|
|
783
|
+
state.internals.checkValidity();
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
786
|
+
const rangeOverflow = state.multiple && typeof state.max === "number" && state.selected.length > state.max;
|
|
787
|
+
const rangeUnderflow = state.multiple && typeof state.min === "number" && state.selected.length < state.min;
|
|
788
|
+
const badInput = state.required && state.selected.length === 0;
|
|
789
|
+
state.internals.setValidity({
|
|
790
|
+
badInput,
|
|
791
|
+
rangeOverflow,
|
|
792
|
+
rangeUnderflow
|
|
793
|
+
}, join([
|
|
794
|
+
badInput ? INVALID_BAD : void 0,
|
|
795
|
+
rangeOverflow ? getString(INVALID_OVERFLOW).replace("{max}", state.max.toString()) : void 0,
|
|
796
|
+
rangeUnderflow ? getString(INVALID_UNDERFLOW).replace("{min}", state.min.toString()) : void 0
|
|
797
|
+
], "; "));
|
|
798
|
+
state.internals.checkValidity();
|
|
799
|
+
}
|
|
800
|
+
function updateWidth(state) {
|
|
801
|
+
setStyle(state.content, PROPERTY_STYLE, `${state.anchor.getBoundingClientRect().width}px`);
|
|
433
802
|
}
|
|
434
803
|
const TAGNAME = "oui-combobox";
|
|
435
804
|
const ATTRIBUTE_ANCHOR = `${TAGNAME}-anchor`;
|
|
436
805
|
const ATTRIBUTE_CONTENT = `${TAGNAME}-content`;
|
|
806
|
+
const ATTRIBUTE_EMPTY = `${TAGNAME}-empty`;
|
|
807
|
+
const ATTRIBUTE_FILTER = `${TAGNAME}-filter`;
|
|
808
|
+
const ATTRIBUTE_FILTER_INPUT = `${ATTRIBUTE_FILTER}-input`;
|
|
809
|
+
const ATTRIBUTE_FILTER_LABEL = `${ATTRIBUTE_FILTER}-label`;
|
|
810
|
+
const ATTRIBUTE_HEADER = `${TAGNAME}-header`;
|
|
437
811
|
const ATTRIBUTE_LIST = `${TAGNAME}-list`;
|
|
438
812
|
const ATTRIBUTE_OPTION = `${TAGNAME}-option`;
|
|
439
|
-
const
|
|
440
|
-
const
|
|
813
|
+
const ATTRIBUTE_OPTION_ACTIVE = `${TAGNAME}-option-active`;
|
|
814
|
+
const ATTRIBUTE_SELECTION = `${TAGNAME}-selection`;
|
|
815
|
+
const ATTRIBUTE_SELECTION_ITEM = `${TAGNAME}-selection-item`;
|
|
441
816
|
const AUTOCOMPLETES = /* @__PURE__ */ new Set([
|
|
442
817
|
"both",
|
|
443
818
|
"inline",
|
|
@@ -445,7 +820,10 @@ const AUTOCOMPLETES = /* @__PURE__ */ new Set([
|
|
|
445
820
|
"none"
|
|
446
821
|
]);
|
|
447
822
|
const EXPRESSION_TYPED = /\p{L}|\s/u;
|
|
448
|
-
const
|
|
823
|
+
const INVALID_BAD = "Please select an option from the list";
|
|
824
|
+
const INVALID_INPUT = "Please enter a value";
|
|
825
|
+
const INVALID_OVERFLOW = "Please select fewer options (maximum is {max})";
|
|
826
|
+
const INVALID_UNDERFLOW = "Please select more options (minimum is {min})";
|
|
449
827
|
const KEYS_NAVIGATION = /* @__PURE__ */ new Set([
|
|
450
828
|
"ArrowDown",
|
|
451
829
|
"ArrowUp",
|
|
@@ -453,9 +831,12 @@ const KEYS_NAVIGATION = /* @__PURE__ */ new Set([
|
|
|
453
831
|
"PageUp"
|
|
454
832
|
]);
|
|
455
833
|
const KEYS_REMOVE = /* @__PURE__ */ new Set(["Backspace", "Delete"]);
|
|
834
|
+
const KEYS_SELECTION = /* @__PURE__ */ new Set(["ArrowRight", "End"]);
|
|
456
835
|
const MESSAGE_LABEL = `<${TAGNAME}> must contain a <label> element for the <select> element`;
|
|
457
|
-
const MESSAGE_SELECT = `<${TAGNAME}> must contain a <select> element`;
|
|
836
|
+
const MESSAGE_SELECT = `<${TAGNAME}> must contain a valid <select> element`;
|
|
837
|
+
const PROPERTY_STYLE = "--oui-combobox-width";
|
|
458
838
|
const SELECTOR_ANCHOR = `[${ATTRIBUTE_ANCHOR}]`;
|
|
839
|
+
const SELECTOR_FILTER = `[${ATTRIBUTE_FILTER_INPUT}]`;
|
|
459
840
|
const SELECTOR_OPTION = `[${ATTRIBUTE_OPTION}]`;
|
|
460
841
|
const TYPES = /* @__PURE__ */ new Set(["autocomplete", "select"]);
|
|
461
842
|
const comboboxes = /* @__PURE__ */ new WeakMap();
|