@oscarpalmer/oui 0.33.0 → 0.35.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/js/combobox/constants.combobox.d.mts +2 -0
- package/dist/js/combobox/constants.combobox.mjs +46 -0
- package/dist/js/combobox/data.combobox.d.mts +18 -0
- package/dist/js/combobox/data.combobox.mjs +2 -0
- package/dist/js/combobox/dom.combobox.d.mts +110 -0
- package/dist/js/combobox/dom.combobox.mjs +2 -0
- package/dist/js/combobox/event.combobox.d.mts +17 -0
- package/dist/js/combobox/event.combobox.mjs +2 -0
- package/dist/js/combobox/form.combobox.d.mts +6 -0
- package/dist/js/combobox/form.combobox.mjs +2 -0
- package/dist/js/combobox/index.d.mts +2 -0
- package/dist/js/combobox/index.mjs +195 -0
- package/dist/js/combobox/misc.combobox.d.mts +2 -0
- package/dist/js/combobox/misc.combobox.mjs +2 -0
- package/dist/js/combobox/models.combobox.d.mts +2 -0
- package/dist/js/combobox/models.combobox.mjs +1 -0
- package/dist/js/combobox/state.combobox.d.mts +13 -0
- package/dist/js/combobox/state.combobox.mjs +2 -0
- package/dist/js/constants.combobox-h_kyDegw.d.mts +248 -0
- package/dist/js/data.combobox-B8HMkDOh.mjs +1030 -0
- package/dist/js/dialog.mjs +3 -2
- package/dist/js/floatable/floatable.embedded.mjs +4 -4
- package/dist/js/focus-trap/focus-trap.embedded.mjs +3 -3
- package/dist/js/index.d.mts +14 -643
- package/dist/js/index.mjs +19 -4875
- package/dist/js/internal/draggable.mjs +6 -6
- package/dist/js/internal/navigable.mjs +3 -3
- package/dist/js/l10n.d.mts +20 -0
- package/dist/js/l10n.mjs +80 -0
- package/dist/js/menu.d.mts +1 -1
- package/dist/js/menu.mjs +3 -2
- package/dist/js/movable/movable.embedded.mjs +3 -3
- package/dist/js/popover.mjs +1 -1
- package/dist/js/resizable/resizable.embedded.mjs +7 -7
- package/dist/js/sortable/sortable.embedded.mjs +7 -6
- package/dist/js/splitter.mjs +7 -13
- package/dist/js/toolbar.d.mts +1 -1
- package/dist/js/tooltip.mjs +3 -3
- package/package.json +4 -4
- package/src/js/combobox/constants.combobox.ts +62 -0
- package/src/js/combobox/data.combobox.ts +236 -0
- package/src/js/combobox/dom.combobox.ts +825 -0
- package/src/js/combobox/event.combobox.ts +454 -0
- package/src/js/combobox/form.combobox.ts +116 -0
- package/src/js/combobox/index.ts +313 -0
- package/src/js/combobox/misc.combobox.ts +188 -0
- package/src/js/combobox/models.combobox.ts +94 -0
- package/src/js/combobox/state.combobox.ts +247 -0
- package/src/js/index.ts +1 -1
- package/src/js/internal/draggable.ts +3 -3
- package/src/js/l10n.ts +192 -0
- package/src/js/movable/movable.embedded.ts +2 -2
- package/src/js/resizable/resizable.embedded.ts +3 -3
- package/src/js/sortable/sortable.embedded.ts +2 -2
- package/src/js/splitter.ts +2 -2
- package/dist/js/combobox.d.mts +0 -91
- package/dist/js/combobox.mjs +0 -797
- package/src/js/combobox.ts +0 -2410
|
@@ -0,0 +1,1030 @@
|
|
|
1
|
+
import { createEmbeddedFloatable } from "./floatable/floatable.embedded.mjs";
|
|
2
|
+
import { getLocalization, getLocalizations, localize, updateLocalizations } from "./l10n.mjs";
|
|
3
|
+
import { COMBOBOX_ATTRIBUTE_ANCHOR, COMBOBOX_ATTRIBUTE_CONTENT, COMBOBOX_ATTRIBUTE_FILTER, COMBOBOX_ATTRIBUTE_FILTER_INPUT, COMBOBOX_ATTRIBUTE_HEADER, COMBOBOX_ATTRIBUTE_HIDDEN, COMBOBOX_ATTRIBUTE_LIST, COMBOBOX_ATTRIBUTE_MESSAGE, COMBOBOX_ATTRIBUTE_OPTION, COMBOBOX_ATTRIBUTE_OPTION_ACTIVE, COMBOBOX_ATTRIBUTE_SELECTION, COMBOBOX_ATTRIBUTE_SELECTION_ITEM, COMBOBOX_AUTOCOMPLETES, COMBOBOX_KEYS_ACTIVATION, COMBOBOX_KEYS_NAVIGATION, COMBOBOX_KEYS_SELECTION, COMBOBOX_MESSAGE_LABEL, COMBOBOX_MESSAGE_SELECT, COMBOBOX_SELECTOR_ANCHOR, COMBOBOX_SELECTOR_OPTION, COMBOBOX_SELECTOR_SELECTION_ITEM, COMBOBOX_TAGNAME, COMBOBOX_TYPES, comboboxScrollOptions } from "./combobox/constants.combobox.mjs";
|
|
4
|
+
import { getAria, setAria } from "@oscarpalmer/toretto/aria";
|
|
5
|
+
import { dispatch, on } from "@oscarpalmer/toretto/event";
|
|
6
|
+
import { getAttribute, setAttribute } from "@oscarpalmer/toretto/attribute";
|
|
7
|
+
import { clamp } from "@oscarpalmer/atoms/number";
|
|
8
|
+
import { getString } from "@oscarpalmer/atoms/string";
|
|
9
|
+
import { getData } from "@oscarpalmer/toretto/data";
|
|
10
|
+
import { createElement } from "@oscarpalmer/toretto/create";
|
|
11
|
+
import { dispatch as dispatch$1, findAncestor, getData as getData$1 } from "@oscarpalmer/toretto";
|
|
12
|
+
//#region src/js/combobox/misc.combobox.ts
|
|
13
|
+
function generateComboboxIds(prefix, select) {
|
|
14
|
+
const elements = [...select.children].flatMap((child) => child instanceof HTMLOptGroupElement ? [child, ...[...child.children].filter((groupChild) => groupChild instanceof HTMLOptionElement)] : child instanceof HTMLOptionElement ? [child] : []);
|
|
15
|
+
const { length } = elements;
|
|
16
|
+
for (let index = 0; index < length; index += 1) {
|
|
17
|
+
const element = elements[index];
|
|
18
|
+
if (element.id === "") element.id = `${prefix}${element instanceof HTMLOptGroupElement ? "_group_" : "_option_"}${index}`;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function getComboboxAttributeValue(element, attribute, possible, defaultValue) {
|
|
22
|
+
const value = getAttribute(element, attribute);
|
|
23
|
+
return possible.has(value) ? value : defaultValue;
|
|
24
|
+
}
|
|
25
|
+
function getComboboxAutoComplete(element, select) {
|
|
26
|
+
let value;
|
|
27
|
+
if (select) value = "list";
|
|
28
|
+
else value = getComboboxAttributeValue(element, "autocomplete", COMBOBOX_AUTOCOMPLETES, "none");
|
|
29
|
+
return {
|
|
30
|
+
value,
|
|
31
|
+
both: value === "both",
|
|
32
|
+
inline: value === "inline",
|
|
33
|
+
list: value === "list",
|
|
34
|
+
none: value === "none"
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function getComboboxBooleanAria(element, attribute) {
|
|
38
|
+
const value = getAria(element, attribute);
|
|
39
|
+
return value === true || value === "true";
|
|
40
|
+
}
|
|
41
|
+
function getComboboxId() {
|
|
42
|
+
index += 1;
|
|
43
|
+
return `oui_combobox_${index}`;
|
|
44
|
+
}
|
|
45
|
+
function getComboboxVisibleOptions(state) {
|
|
46
|
+
return state.options.all.filter((option) => !option.hidden);
|
|
47
|
+
}
|
|
48
|
+
function hasTextSelection(element) {
|
|
49
|
+
return element instanceof HTMLInputElement && element.selectionStart !== element.selectionEnd;
|
|
50
|
+
}
|
|
51
|
+
function setComboboxFilter(state, value) {
|
|
52
|
+
if (typeof value !== "string") return;
|
|
53
|
+
const type = value.length < state.filter.element.value.length ? "deleteContentBackward" : "insertText";
|
|
54
|
+
state.filter.element.value = value;
|
|
55
|
+
onComboboxFilter(type, state);
|
|
56
|
+
}
|
|
57
|
+
function setComboboxLimit(type, state, value) {
|
|
58
|
+
let actual = value;
|
|
59
|
+
if (typeof actual === "string") actual = Number.parseInt(actual, 10);
|
|
60
|
+
if (typeof actual === "number" && !Number.isNaN(actual) && actual >= 0 ? actual !== state[type] : actual == null) {
|
|
61
|
+
state[type] = actual;
|
|
62
|
+
setAttribute(state.host, type, actual);
|
|
63
|
+
updateComboboxValidity(state);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function setComboboxLocalizations(first, second) {
|
|
67
|
+
updateLocalizations(comboboxLocalizations, typeof first === "string" ? { [first]: second } : first, comboboxLocalizations.values, () => {
|
|
68
|
+
for (const state of comboboxStates.set) {
|
|
69
|
+
updateComboboxAnchor(state);
|
|
70
|
+
updateComboboxMessage(state, getComboboxVisibleOptions(state).length);
|
|
71
|
+
updateComboboxSelectionItems(state);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
let index = 0;
|
|
76
|
+
//#endregion
|
|
77
|
+
//#region src/js/combobox/event.combobox.ts
|
|
78
|
+
function onComboboxClickContent(event, content) {
|
|
79
|
+
const state = comboboxStates.map.get(content);
|
|
80
|
+
if (state == null) return;
|
|
81
|
+
if (state.disabled) return;
|
|
82
|
+
const option = findAncestor(event, COMBOBOX_SELECTOR_OPTION);
|
|
83
|
+
if (!(option instanceof HTMLElement)) state.filter.element.focus();
|
|
84
|
+
else if (state.readonly) updateComboboxPreview("set", state, option);
|
|
85
|
+
else toggleComboboxOption(state, option);
|
|
86
|
+
}
|
|
87
|
+
function onComboboxClickDocument(event) {
|
|
88
|
+
const combobox = findAncestor(event, COMBOBOX_TAGNAME);
|
|
89
|
+
const state = comboboxStates.map.get(combobox);
|
|
90
|
+
if (combobox == null || state == null) return;
|
|
91
|
+
const label = findAncestor(event, "label");
|
|
92
|
+
if (label != null) {
|
|
93
|
+
if (label === state.label.element && combobox.type === "select") {
|
|
94
|
+
state.anchor.element.focus();
|
|
95
|
+
if (!state.disabled) state.floatable.show();
|
|
96
|
+
}
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const selection = findAncestor(event, `[${COMBOBOX_ATTRIBUTE_SELECTION_ITEM}]`);
|
|
100
|
+
if (selection != null) {
|
|
101
|
+
const option = state.options.selected.find((option) => option.id === getData$1(selection, "option", false));
|
|
102
|
+
if (state.disabled) state.anchor.element.focus();
|
|
103
|
+
else if (state.readonly) updateComboboxTabindices(state, option);
|
|
104
|
+
else toggleComboboxOption(state, option);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (findAncestor(event, COMBOBOX_SELECTOR_ANCHOR) != null && !state.disabled) {
|
|
108
|
+
state.floatable.show();
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function onComboboxEnter(state) {
|
|
113
|
+
if (state.disabled || state.readonly) return;
|
|
114
|
+
if (hasTextSelection(state.anchor.element)) onComboboxSelection(state);
|
|
115
|
+
else toggleComboboxOption(state, state.active);
|
|
116
|
+
}
|
|
117
|
+
function onComboboxEscape(state) {
|
|
118
|
+
state.floatable.hide();
|
|
119
|
+
}
|
|
120
|
+
function onComboboxFilter(type, state, manual, emit) {
|
|
121
|
+
if (state.disabled || state.readonly) return;
|
|
122
|
+
const diff = state.filter.value !== state.filter.element.value;
|
|
123
|
+
if (!state.autocomplete.inline && state.filter.element.value.length > 0 && !(manual ?? false)) state.floatable.show();
|
|
124
|
+
const option = state.autocomplete.none ? state.active ?? state.options.all[0] : filterComboboxOptions(state);
|
|
125
|
+
let update;
|
|
126
|
+
if (type.includes("delete")) update = "none";
|
|
127
|
+
else if (type.includes("insert")) update = "typed";
|
|
128
|
+
if (update != null) if (state.autocomplete.none) applyComboboxSelection({
|
|
129
|
+
state,
|
|
130
|
+
added: false,
|
|
131
|
+
emit: (emit ?? true) && diff
|
|
132
|
+
});
|
|
133
|
+
else updateComboboxPreview(update, state, option);
|
|
134
|
+
if (diff) {
|
|
135
|
+
state.filter.value = state.filter.element.value;
|
|
136
|
+
dispatch$1(state.host, "input");
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
function onComboboxFocusout(state) {
|
|
140
|
+
if (state.disabled || state.readonly || state.type === "select" || state.host === document.activeElement || state.host.contains(document.activeElement)) return;
|
|
141
|
+
const option = filterComboboxOption(state, state.filter.element.value, true);
|
|
142
|
+
applyComboboxSelection({
|
|
143
|
+
option,
|
|
144
|
+
state,
|
|
145
|
+
added: false,
|
|
146
|
+
selected: option == null ? [] : [option]
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
function onComboboxInput(event) {
|
|
150
|
+
const combobox = findAncestor(event, COMBOBOX_TAGNAME);
|
|
151
|
+
const state = comboboxStates.map.get(combobox);
|
|
152
|
+
if (combobox != null && state != null && event.target === state.filter.element) onComboboxFilter(event.inputType, state);
|
|
153
|
+
}
|
|
154
|
+
function onComboboxKeydown(event) {
|
|
155
|
+
const combobox = findAncestor(event, COMBOBOX_TAGNAME);
|
|
156
|
+
const state = comboboxStates.map.get(combobox);
|
|
157
|
+
if (combobox == null || state == null) return;
|
|
158
|
+
const item = findAncestor(event, COMBOBOX_SELECTOR_SELECTION_ITEM);
|
|
159
|
+
if (item instanceof HTMLElement) {
|
|
160
|
+
onComboboxKeydownSelection(event, state, item);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
onComboboxKeydownFilter(event, state);
|
|
164
|
+
}
|
|
165
|
+
function onComboboxKeydownFilter(event, state) {
|
|
166
|
+
const ignore = state.disabled || state.readonly;
|
|
167
|
+
const isSelect = state.type === "select";
|
|
168
|
+
let preventDefault = ignore;
|
|
169
|
+
switch (true) {
|
|
170
|
+
case event.key === " ":
|
|
171
|
+
if (!state.disabled) state.floatable.show();
|
|
172
|
+
break;
|
|
173
|
+
case event.key === "Enter":
|
|
174
|
+
preventDefault = state.floatable.open;
|
|
175
|
+
if (preventDefault) onComboboxEnter(state);
|
|
176
|
+
else if (!ignore && state.type === "select") state.floatable.show();
|
|
177
|
+
else if (!ignore) state.internals.form?.requestSubmit();
|
|
178
|
+
break;
|
|
179
|
+
case event.key === "Escape":
|
|
180
|
+
preventDefault = false;
|
|
181
|
+
onComboboxEscape(state);
|
|
182
|
+
break;
|
|
183
|
+
case !isSelect && COMBOBOX_KEYS_SELECTION.has(event.key):
|
|
184
|
+
if (!ignore && hasTextSelection(state.anchor.element)) onComboboxSelection(state);
|
|
185
|
+
break;
|
|
186
|
+
case COMBOBOX_KEYS_NAVIGATION.has(event.key):
|
|
187
|
+
if (!state.disabled) onComboboxNavigation(event, state);
|
|
188
|
+
break;
|
|
189
|
+
default:
|
|
190
|
+
preventDefault = false;
|
|
191
|
+
if (event.key === "Tab") onComboboxTab(state);
|
|
192
|
+
}
|
|
193
|
+
if (preventDefault) {
|
|
194
|
+
event.preventDefault();
|
|
195
|
+
event.stopPropagation();
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
function onComboboxKeydownSelection(event, state, item) {
|
|
199
|
+
if (COMBOBOX_KEYS_ACTIVATION.has(event.key)) {
|
|
200
|
+
event.preventDefault();
|
|
201
|
+
onComboboxRemoveSelection(state, item);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
if (state.disabled || state.selection == null) return;
|
|
205
|
+
const { length } = state.selection.items;
|
|
206
|
+
const index = state.selection.items.findIndex((selection) => selection.button === item);
|
|
207
|
+
let next;
|
|
208
|
+
switch (event.key) {
|
|
209
|
+
case "ArrowDown":
|
|
210
|
+
state.anchor.element.focus();
|
|
211
|
+
state.anchor.element.click();
|
|
212
|
+
return;
|
|
213
|
+
case "ArrowRight":
|
|
214
|
+
case "ArrowLeft":
|
|
215
|
+
next = clamp(index + (event.key === "ArrowRight" ? 1 : -1), 0, length - 1, true);
|
|
216
|
+
break;
|
|
217
|
+
case "End":
|
|
218
|
+
next = length - 1;
|
|
219
|
+
break;
|
|
220
|
+
case "Home": next = 0;
|
|
221
|
+
}
|
|
222
|
+
if (next == null) return;
|
|
223
|
+
event.preventDefault();
|
|
224
|
+
for (let index = 0; index < length; index += 1) {
|
|
225
|
+
const { button } = state.selection.items[index];
|
|
226
|
+
const selected = index === next;
|
|
227
|
+
button.tabIndex = selected ? 0 : -1;
|
|
228
|
+
if (selected) button.focus();
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
function onComboboxNavigation(event, state) {
|
|
232
|
+
let skip = false;
|
|
233
|
+
if (!state.floatable.open) {
|
|
234
|
+
skip = true;
|
|
235
|
+
state.floatable.show();
|
|
236
|
+
}
|
|
237
|
+
const visible = getComboboxVisibleOptions(state);
|
|
238
|
+
if (visible.length === 0) return;
|
|
239
|
+
let next;
|
|
240
|
+
if (skip || state.active == null) next = state.active ?? visible.at(event.key === "ArrowDown" ? 0 : -1);
|
|
241
|
+
else {
|
|
242
|
+
const current = visible.findIndex((option) => option.id === state.active?.id);
|
|
243
|
+
let index;
|
|
244
|
+
switch (event.key) {
|
|
245
|
+
case "ArrowDown":
|
|
246
|
+
case "ArrowUp":
|
|
247
|
+
index = clamp(current + (event.key === "ArrowDown" ? 1 : -1), 0, visible.length - 1, true);
|
|
248
|
+
break;
|
|
249
|
+
case "End":
|
|
250
|
+
index = visible.length - 1;
|
|
251
|
+
break;
|
|
252
|
+
case "Home":
|
|
253
|
+
index = 0;
|
|
254
|
+
break;
|
|
255
|
+
case "PageDown":
|
|
256
|
+
case "PageUp": index = clamp(current + (event.key === "PageDown" ? 10 : -10), 0, visible.length - 1);
|
|
257
|
+
}
|
|
258
|
+
if (index != null) next = visible[index];
|
|
259
|
+
}
|
|
260
|
+
let preview;
|
|
261
|
+
if (state.type === "autocomplete" && (state.autocomplete.both || state.autocomplete.inline)) preview = "set";
|
|
262
|
+
else preview = "none";
|
|
263
|
+
updateComboboxPreview(preview, state, next ?? state.active ?? visible[0]);
|
|
264
|
+
}
|
|
265
|
+
function onComboboxRemoveSelection(state, item) {
|
|
266
|
+
if (state.disabled || state.readonly) return;
|
|
267
|
+
const option = state.options.selected.find((option) => option.id === getData$1(item, "option", false));
|
|
268
|
+
if (option != null) toggleComboboxOption(state, option);
|
|
269
|
+
}
|
|
270
|
+
function onComboboxSelection(state) {
|
|
271
|
+
state.filter.value = state.anchor.element === state.filter.element ? state.filter.element.value : "";
|
|
272
|
+
toggleComboboxOption(state, filterComboboxOptions(state) ?? state.active);
|
|
273
|
+
}
|
|
274
|
+
function onComboboxTab(state) {
|
|
275
|
+
setTimeout(() => {
|
|
276
|
+
if (state.host === document.activeElement) return;
|
|
277
|
+
if (state.selection?.container.contains(document.activeElement) || !state.host.contains(document.activeElement)) state.floatable.hide();
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
on(document, "click", onComboboxClickDocument);
|
|
281
|
+
on(document, "input", onComboboxInput, { passive: false });
|
|
282
|
+
on(document, "keydown", onComboboxKeydown, { passive: false });
|
|
283
|
+
//#endregion
|
|
284
|
+
//#region src/js/combobox/state.combobox.ts
|
|
285
|
+
function attachCombobox(state) {
|
|
286
|
+
const { anchor, content, filter, label, listeners, select } = state;
|
|
287
|
+
label.element.htmlFor = filter.element.id;
|
|
288
|
+
label.element.id = `${state.id}_label`;
|
|
289
|
+
select.hidden = true;
|
|
290
|
+
select.insertAdjacentElement("afterend", anchor.container ?? anchor.element);
|
|
291
|
+
(anchor.container ?? anchor.element).insertAdjacentElement("afterend", content);
|
|
292
|
+
select.remove();
|
|
293
|
+
state.floatable = createEmbeddedFloatable(anchor.container ?? anchor.element, content, {
|
|
294
|
+
position: "below-start",
|
|
295
|
+
reusable: false
|
|
296
|
+
});
|
|
297
|
+
state.floatable.update();
|
|
298
|
+
listeners.push(on(state.host, "focusout", () => {
|
|
299
|
+
setTimeout(() => {
|
|
300
|
+
onComboboxFocusout(state);
|
|
301
|
+
});
|
|
302
|
+
}), on(content, "click", (event) => {
|
|
303
|
+
onComboboxClickContent(event, content);
|
|
304
|
+
}), on(content, "toggle", () => {
|
|
305
|
+
setAria(anchor.element, "expanded", state.floatable.open);
|
|
306
|
+
setAria(filter.element, "activedescendant", state.floatable.open ? state.active?.id : void 0);
|
|
307
|
+
if (state.floatable.open) filter.element.focus();
|
|
308
|
+
}));
|
|
309
|
+
comboboxStates.map.set(state.host, state);
|
|
310
|
+
comboboxStates.map.set(state.content, state);
|
|
311
|
+
comboboxStates.set.add(state);
|
|
312
|
+
}
|
|
313
|
+
function createComboboxState(element, internals) {
|
|
314
|
+
const select = element.querySelector("select");
|
|
315
|
+
if (select == null || select.id === "") throw new Error(COMBOBOX_MESSAGE_SELECT);
|
|
316
|
+
const label = element.querySelector(`label[for="${select.id}"]`);
|
|
317
|
+
if (label == null) throw new Error(COMBOBOX_MESSAGE_LABEL);
|
|
318
|
+
element.id = select.id;
|
|
319
|
+
setAttribute(element, "name", select.name === "" ? select.id : select.name);
|
|
320
|
+
const disabled = getComboboxBooleanAria(element, "disabled") || select.disabled;
|
|
321
|
+
const multiple = element.multiple || select.multiple;
|
|
322
|
+
const readonly = getComboboxBooleanAria(element, "readonly");
|
|
323
|
+
const required = getComboboxBooleanAria(element, "required") || select.required;
|
|
324
|
+
const id = getComboboxId();
|
|
325
|
+
const type = multiple ? "select" : getComboboxAttributeValue(element, "type", COMBOBOX_TYPES, "select");
|
|
326
|
+
const autocomplete = getComboboxAutoComplete(element, type === "select");
|
|
327
|
+
const labelId = label.id;
|
|
328
|
+
label.htmlFor = id;
|
|
329
|
+
label.id = `${id}_label`;
|
|
330
|
+
generateComboboxIds(id, select);
|
|
331
|
+
const anchor = createComboboxAnchor({
|
|
332
|
+
autocomplete,
|
|
333
|
+
id,
|
|
334
|
+
label,
|
|
335
|
+
multiple,
|
|
336
|
+
select,
|
|
337
|
+
type
|
|
338
|
+
});
|
|
339
|
+
const content = createComboboxContent({
|
|
340
|
+
autocomplete,
|
|
341
|
+
id,
|
|
342
|
+
label,
|
|
343
|
+
multiple,
|
|
344
|
+
select,
|
|
345
|
+
type
|
|
346
|
+
});
|
|
347
|
+
const values = content.list.selected.length === 0 ? [""] : content.list.selected.map((option) => getData(option, "value", false) ?? "");
|
|
348
|
+
const state = {
|
|
349
|
+
autocomplete,
|
|
350
|
+
disabled,
|
|
351
|
+
id,
|
|
352
|
+
internals,
|
|
353
|
+
multiple,
|
|
354
|
+
readonly,
|
|
355
|
+
required,
|
|
356
|
+
select,
|
|
357
|
+
type,
|
|
358
|
+
anchor: {
|
|
359
|
+
container: anchor.container,
|
|
360
|
+
element: anchor.element
|
|
361
|
+
},
|
|
362
|
+
active: content.list.selected[0],
|
|
363
|
+
content: content.element,
|
|
364
|
+
filter: {
|
|
365
|
+
element: content.filter?.element ?? anchor.element,
|
|
366
|
+
value: ""
|
|
367
|
+
},
|
|
368
|
+
floatable: void 0,
|
|
369
|
+
groups: content.list.groups,
|
|
370
|
+
host: element,
|
|
371
|
+
label: {
|
|
372
|
+
element: label,
|
|
373
|
+
id: labelId
|
|
374
|
+
},
|
|
375
|
+
listeners: [],
|
|
376
|
+
localizations: {},
|
|
377
|
+
message: content.message,
|
|
378
|
+
name: element.name,
|
|
379
|
+
options: {
|
|
380
|
+
all: content.list.options,
|
|
381
|
+
selected: content.list.selected
|
|
382
|
+
},
|
|
383
|
+
selection: anchor.selection,
|
|
384
|
+
values: {
|
|
385
|
+
current: values.slice(),
|
|
386
|
+
initial: values.slice()
|
|
387
|
+
}
|
|
388
|
+
};
|
|
389
|
+
attachCombobox(state);
|
|
390
|
+
setComboboxDisabled(state, disabled);
|
|
391
|
+
setComboboxLimit("max", state, getAttribute(element, "max"));
|
|
392
|
+
setComboboxLimit("min", state, getAttribute(element, "min"));
|
|
393
|
+
setComboboxReadonly(state, readonly);
|
|
394
|
+
setComboboxRequired(state, required);
|
|
395
|
+
filterComboboxOptions(state);
|
|
396
|
+
if (content.list.selected.length > 0) updateComboboxPreview("set", state, content.list.selected[0]);
|
|
397
|
+
updateComboboxForm(state, state.values.current, false);
|
|
398
|
+
updateComboboxMessage(state, getComboboxVisibleOptions(state).length);
|
|
399
|
+
updateComboboxSelectionItems(state);
|
|
400
|
+
return state;
|
|
401
|
+
}
|
|
402
|
+
function detachCombobox(state) {
|
|
403
|
+
const { content, floatable, host, label, select } = state;
|
|
404
|
+
const listeners = state.listeners.splice(0);
|
|
405
|
+
const { length } = listeners;
|
|
406
|
+
for (let index = 0; index < length; index += 1) listeners[index]();
|
|
407
|
+
floatable.destroy();
|
|
408
|
+
const anchor = state.anchor.container ?? state.anchor.element;
|
|
409
|
+
anchor.insertAdjacentElement("beforebegin", select);
|
|
410
|
+
anchor.remove();
|
|
411
|
+
content.remove();
|
|
412
|
+
label.element.htmlFor = select.id;
|
|
413
|
+
label.element.id = state.label.id;
|
|
414
|
+
select.hidden = false;
|
|
415
|
+
comboboxStates.map.delete(host);
|
|
416
|
+
comboboxStates.map.delete(content);
|
|
417
|
+
comboboxStates.set.delete(state);
|
|
418
|
+
}
|
|
419
|
+
const comboboxLocalizations = getLocalizations({
|
|
420
|
+
empty: "No results found",
|
|
421
|
+
input: "Please enter a value",
|
|
422
|
+
invalid: "Please select an option from the list",
|
|
423
|
+
max: (max) => `Please select fewer options (maximum is ${max})`,
|
|
424
|
+
min: (min) => `Please select more options (minimum is ${min})`,
|
|
425
|
+
remove: (label) => `Remove ${label}`,
|
|
426
|
+
results: (amount) => `${amount} results available`,
|
|
427
|
+
selected: (amount) => `${amount} selected`
|
|
428
|
+
});
|
|
429
|
+
const comboboxStates = {
|
|
430
|
+
map: /* @__PURE__ */ new WeakMap(),
|
|
431
|
+
set: /* @__PURE__ */ new Set()
|
|
432
|
+
};
|
|
433
|
+
//#endregion
|
|
434
|
+
//#region src/js/combobox/form.combobox.ts
|
|
435
|
+
function updateComboboxForm(state, values, emit) {
|
|
436
|
+
let value;
|
|
437
|
+
if (state.multiple) {
|
|
438
|
+
const { length } = values;
|
|
439
|
+
value = new FormData();
|
|
440
|
+
for (let index = 0; index < length; index += 1) value.append(state.name, values[index]);
|
|
441
|
+
} else value = values[0];
|
|
442
|
+
state.values.current = values;
|
|
443
|
+
state.internals.setFormValue(value, JSON.stringify(values));
|
|
444
|
+
updateComboboxAnchor(state);
|
|
445
|
+
updateComboboxValidity(state);
|
|
446
|
+
if (emit ?? true) dispatch(state.host, "change");
|
|
447
|
+
}
|
|
448
|
+
function updateComboboxValidity(state) {
|
|
449
|
+
if (state.autocomplete.none) {
|
|
450
|
+
const valueMissing = state.required && state.filter.element.value.length === 0;
|
|
451
|
+
const localization = getLocalization(comboboxLocalizations, state.host, "invalid", state.localizations);
|
|
452
|
+
state.internals.setValidity({ valueMissing }, localize(localization, {}, []));
|
|
453
|
+
state.internals.checkValidity();
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
let data = {};
|
|
457
|
+
let error;
|
|
458
|
+
let key;
|
|
459
|
+
let parameters = [];
|
|
460
|
+
switch (true) {
|
|
461
|
+
case state.required && state.options.selected.length === 0:
|
|
462
|
+
error = "badInput";
|
|
463
|
+
key = "input";
|
|
464
|
+
break;
|
|
465
|
+
case state.multiple && typeof state.max === "number" && state.options.selected.length > state.max:
|
|
466
|
+
data = { amount: state.max };
|
|
467
|
+
error = "rangeOverflow";
|
|
468
|
+
key = "max";
|
|
469
|
+
parameters = [state.max];
|
|
470
|
+
break;
|
|
471
|
+
case state.multiple && typeof state.min === "number" && state.options.selected.length < state.min:
|
|
472
|
+
data = { amount: state.min };
|
|
473
|
+
error = "rangeUnderflow";
|
|
474
|
+
key = "min";
|
|
475
|
+
parameters = [state.min];
|
|
476
|
+
}
|
|
477
|
+
const localization = key == null ? void 0 : getLocalization(comboboxLocalizations, state.host, key, state.localizations);
|
|
478
|
+
const message = localization == null ? void 0 : localize(localization, data, parameters);
|
|
479
|
+
state.internals.setValidity(error == null ? {} : { [error]: true }, message);
|
|
480
|
+
state.internals.checkValidity();
|
|
481
|
+
}
|
|
482
|
+
//#endregion
|
|
483
|
+
//#region src/js/combobox/dom.combobox.ts
|
|
484
|
+
function createComboboxAnchor(parameters) {
|
|
485
|
+
const { autocomplete, id, label, select, type } = parameters;
|
|
486
|
+
let container;
|
|
487
|
+
let element;
|
|
488
|
+
let selection;
|
|
489
|
+
if (parameters.type === "autocomplete") element = createComboboxInput({
|
|
490
|
+
id,
|
|
491
|
+
label,
|
|
492
|
+
type,
|
|
493
|
+
autocomplete: autocomplete.value,
|
|
494
|
+
value: select.selectedOptions[0]?.textContent ?? ""
|
|
495
|
+
});
|
|
496
|
+
else ({container, element, selection} = createComboboxButton(parameters));
|
|
497
|
+
setAttribute(container ?? element, COMBOBOX_ATTRIBUTE_ANCHOR, "");
|
|
498
|
+
return {
|
|
499
|
+
container,
|
|
500
|
+
element,
|
|
501
|
+
selection
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
function createComboboxButton(parameters) {
|
|
505
|
+
const { id, label, multiple, select } = parameters;
|
|
506
|
+
const element = createElement("button", {
|
|
507
|
+
aria: {
|
|
508
|
+
controls: `${id}_content`,
|
|
509
|
+
expanded: false,
|
|
510
|
+
haspopup: "listbox",
|
|
511
|
+
labelledby: `${label.id} ${id}`
|
|
512
|
+
},
|
|
513
|
+
property: { textContent: multiple ? "" : select.selectedOptions[0]?.textContent ?? "" }
|
|
514
|
+
});
|
|
515
|
+
if (!multiple) return { element };
|
|
516
|
+
const container = createElement("div");
|
|
517
|
+
const selection = createComboboxSelection(id, select);
|
|
518
|
+
container.append(selection.container, element);
|
|
519
|
+
return {
|
|
520
|
+
container,
|
|
521
|
+
element,
|
|
522
|
+
selection
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
function createComboboxContent(parameters) {
|
|
526
|
+
const { autocomplete, id, label, multiple, select, type } = parameters;
|
|
527
|
+
const element = createElement("div", {
|
|
528
|
+
aria: { labelledby: label.id },
|
|
529
|
+
attribute: {
|
|
530
|
+
[COMBOBOX_ATTRIBUTE_CONTENT]: "",
|
|
531
|
+
popover: ""
|
|
532
|
+
},
|
|
533
|
+
property: {
|
|
534
|
+
id: `${id}_content`,
|
|
535
|
+
tabIndex: -1
|
|
536
|
+
}
|
|
537
|
+
});
|
|
538
|
+
const filter = createComboboxFilter({
|
|
539
|
+
id,
|
|
540
|
+
label,
|
|
541
|
+
type,
|
|
542
|
+
autocomplete: autocomplete.value
|
|
543
|
+
});
|
|
544
|
+
const list = createComboboxList({
|
|
545
|
+
autocomplete,
|
|
546
|
+
id,
|
|
547
|
+
label,
|
|
548
|
+
multiple,
|
|
549
|
+
select
|
|
550
|
+
});
|
|
551
|
+
const message = createComboboxMessage();
|
|
552
|
+
if (filter != null) element.append(filter.container);
|
|
553
|
+
element.append(list.element, message);
|
|
554
|
+
return {
|
|
555
|
+
element,
|
|
556
|
+
filter,
|
|
557
|
+
list,
|
|
558
|
+
message
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
function createComboboxFilter(parameters) {
|
|
562
|
+
const { autocomplete, id, label, type } = parameters;
|
|
563
|
+
if (type === "autocomplete") return;
|
|
564
|
+
const container = createElement("div", { attribute: { [COMBOBOX_ATTRIBUTE_FILTER]: "" } });
|
|
565
|
+
const element = createComboboxInput({
|
|
566
|
+
autocomplete,
|
|
567
|
+
id,
|
|
568
|
+
label,
|
|
569
|
+
type,
|
|
570
|
+
value: ""
|
|
571
|
+
});
|
|
572
|
+
setAttribute(element, COMBOBOX_ATTRIBUTE_FILTER_INPUT, "");
|
|
573
|
+
container.append(element);
|
|
574
|
+
return {
|
|
575
|
+
container,
|
|
576
|
+
element
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
function createComboboxGroup(parameters) {
|
|
580
|
+
const { autocomplete, group, groupOptions, selected } = parameters;
|
|
581
|
+
const element = createElement("li", {
|
|
582
|
+
aria: { label: group.label },
|
|
583
|
+
property: { role: "group" }
|
|
584
|
+
});
|
|
585
|
+
const list = createElement("ul", { property: { role: "none" } });
|
|
586
|
+
const header = createComboboxHeader(group);
|
|
587
|
+
element.append(header, list);
|
|
588
|
+
return {
|
|
589
|
+
element,
|
|
590
|
+
header,
|
|
591
|
+
options: createComboboxOptions({
|
|
592
|
+
autocomplete,
|
|
593
|
+
list,
|
|
594
|
+
selected,
|
|
595
|
+
group: header.id,
|
|
596
|
+
options: groupOptions
|
|
597
|
+
})
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
function createComboboxHeader(group) {
|
|
601
|
+
return createElement("div", {
|
|
602
|
+
aria: { hidden: true },
|
|
603
|
+
attribute: { [COMBOBOX_ATTRIBUTE_HEADER]: "" },
|
|
604
|
+
property: {
|
|
605
|
+
id: group.id,
|
|
606
|
+
textContent: group.label
|
|
607
|
+
}
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
function createComboboxInput(parameters) {
|
|
611
|
+
const { autocomplete, id, label, type, value } = parameters;
|
|
612
|
+
return createElement("input", {
|
|
613
|
+
aria: {
|
|
614
|
+
autocomplete,
|
|
615
|
+
controls: `${id}_list`,
|
|
616
|
+
expanded: type === "select",
|
|
617
|
+
haspopup: "listbox",
|
|
618
|
+
labelledby: label.id
|
|
619
|
+
},
|
|
620
|
+
attribute: {
|
|
621
|
+
autocorrect: "off",
|
|
622
|
+
autocomplete: "off",
|
|
623
|
+
form: "",
|
|
624
|
+
spellcheck: "false",
|
|
625
|
+
type: "text"
|
|
626
|
+
},
|
|
627
|
+
property: {
|
|
628
|
+
id,
|
|
629
|
+
value,
|
|
630
|
+
role: "combobox"
|
|
631
|
+
}
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
function createComboboxList(parameters) {
|
|
635
|
+
const { autocomplete, id, label, multiple, select } = parameters;
|
|
636
|
+
const element = createElement("ul", {
|
|
637
|
+
aria: {
|
|
638
|
+
labelledby: label.id,
|
|
639
|
+
multiselectable: multiple
|
|
640
|
+
},
|
|
641
|
+
attribute: {
|
|
642
|
+
[COMBOBOX_ATTRIBUTE_LIST]: "",
|
|
643
|
+
tabindex: -1
|
|
644
|
+
},
|
|
645
|
+
property: {
|
|
646
|
+
id: `${id}_list`,
|
|
647
|
+
role: "listbox"
|
|
648
|
+
}
|
|
649
|
+
});
|
|
650
|
+
const children = [...select.children];
|
|
651
|
+
const childrenLength = children.length;
|
|
652
|
+
const options = [];
|
|
653
|
+
const selected = [];
|
|
654
|
+
let groups;
|
|
655
|
+
for (let childIndex = 0; childIndex < childrenLength; childIndex += 1) {
|
|
656
|
+
const child = children[childIndex];
|
|
657
|
+
if (child instanceof HTMLOptionElement) {
|
|
658
|
+
const option = createComboboxOption({
|
|
659
|
+
autocomplete,
|
|
660
|
+
index: childIndex,
|
|
661
|
+
option: child
|
|
662
|
+
});
|
|
663
|
+
element.append(option);
|
|
664
|
+
options.push(option);
|
|
665
|
+
if (child.selected) selected.push(option);
|
|
666
|
+
continue;
|
|
667
|
+
}
|
|
668
|
+
if (!(child instanceof HTMLOptGroupElement)) continue;
|
|
669
|
+
const group = createComboboxGroup({
|
|
670
|
+
autocomplete,
|
|
671
|
+
selected,
|
|
672
|
+
group: child,
|
|
673
|
+
groupOptions: [...child.children]
|
|
674
|
+
});
|
|
675
|
+
groups ??= [];
|
|
676
|
+
element.append(group.element, ...group.options);
|
|
677
|
+
groups.push(group);
|
|
678
|
+
options.push(...group.options);
|
|
679
|
+
}
|
|
680
|
+
return {
|
|
681
|
+
element,
|
|
682
|
+
groups,
|
|
683
|
+
options,
|
|
684
|
+
selected
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
function createComboboxMessage() {
|
|
688
|
+
return createElement("div", {
|
|
689
|
+
aria: { live: "polite" },
|
|
690
|
+
attribute: { [COMBOBOX_ATTRIBUTE_MESSAGE]: "" },
|
|
691
|
+
property: {
|
|
692
|
+
hidden: true,
|
|
693
|
+
role: "status"
|
|
694
|
+
}
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
function createComboboxOption(parameters) {
|
|
698
|
+
const { autocomplete, group, option } = parameters;
|
|
699
|
+
return createElement("li", {
|
|
700
|
+
aria: { selected: !autocomplete.none && option.selected },
|
|
701
|
+
attribute: { [COMBOBOX_ATTRIBUTE_OPTION]: "" },
|
|
702
|
+
data: {
|
|
703
|
+
group,
|
|
704
|
+
value: option.value
|
|
705
|
+
},
|
|
706
|
+
property: {
|
|
707
|
+
id: option.id,
|
|
708
|
+
role: "option",
|
|
709
|
+
textContent: option.textContent
|
|
710
|
+
}
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
function createComboboxOptions(parameters) {
|
|
714
|
+
const { autocomplete, group, list, options, selected } = parameters;
|
|
715
|
+
const items = [];
|
|
716
|
+
for (let index = 0; index < options.length; index += 1) {
|
|
717
|
+
const option = options[index];
|
|
718
|
+
const item = createComboboxOption({
|
|
719
|
+
autocomplete,
|
|
720
|
+
group,
|
|
721
|
+
index,
|
|
722
|
+
option
|
|
723
|
+
});
|
|
724
|
+
items.push(item);
|
|
725
|
+
if (option.selected) selected.push(item);
|
|
726
|
+
list.append(item);
|
|
727
|
+
}
|
|
728
|
+
return items;
|
|
729
|
+
}
|
|
730
|
+
function createComboboxSelection(id, select) {
|
|
731
|
+
const container = createElement("ul", {
|
|
732
|
+
attribute: { [COMBOBOX_ATTRIBUTE_SELECTION]: "" },
|
|
733
|
+
property: { id: `${id}_selection` }
|
|
734
|
+
});
|
|
735
|
+
const options = [...select.selectedOptions];
|
|
736
|
+
const items = [];
|
|
737
|
+
createComboboxSelectionItems({
|
|
738
|
+
container,
|
|
739
|
+
items,
|
|
740
|
+
options
|
|
741
|
+
});
|
|
742
|
+
const { button } = items[0] ?? {};
|
|
743
|
+
if (button != null) button.tabIndex = 0;
|
|
744
|
+
return {
|
|
745
|
+
items,
|
|
746
|
+
container
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
function createComboboxSelectionItem(option, state) {
|
|
750
|
+
const optionLabel = option.textContent;
|
|
751
|
+
const optionValue = getData(option, "value", false) ?? "";
|
|
752
|
+
const element = createElement("li");
|
|
753
|
+
const localization = state == null ? option.textContent : getLocalization(comboboxLocalizations, state.host, "remove", state.localizations);
|
|
754
|
+
const button = createElement("button", {
|
|
755
|
+
aria: { label: localize(localization, {
|
|
756
|
+
label: optionLabel,
|
|
757
|
+
value: optionValue
|
|
758
|
+
}, [optionLabel, optionValue]) },
|
|
759
|
+
attribute: {
|
|
760
|
+
[COMBOBOX_ATTRIBUTE_SELECTION_ITEM]: "",
|
|
761
|
+
tabindex: -1
|
|
762
|
+
},
|
|
763
|
+
data: { option: option.id }
|
|
764
|
+
});
|
|
765
|
+
const icon = createElement("span", {
|
|
766
|
+
aria: { hidden: true },
|
|
767
|
+
property: { innerHTML: "×" }
|
|
768
|
+
});
|
|
769
|
+
const label = createElement("span", { property: { textContent: option.textContent } });
|
|
770
|
+
button.append(icon, label);
|
|
771
|
+
element.append(button);
|
|
772
|
+
return {
|
|
773
|
+
button,
|
|
774
|
+
element
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
function createComboboxSelectionItems(parameters) {
|
|
778
|
+
const { container, items, options, state } = parameters;
|
|
779
|
+
const { length } = options;
|
|
780
|
+
for (let index = 0; index < length; index += 1) {
|
|
781
|
+
const option = options[index];
|
|
782
|
+
const item = createComboboxSelectionItem(option, state);
|
|
783
|
+
container.append(item.element);
|
|
784
|
+
items.push(item);
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
function setComboboxDisabled(state, value) {
|
|
788
|
+
state.filter.element.readOnly = state.readonly || value;
|
|
789
|
+
setProperties("disabled", state, value);
|
|
790
|
+
}
|
|
791
|
+
function setProperties(key, state, value) {
|
|
792
|
+
state[key] = value;
|
|
793
|
+
const elements = [
|
|
794
|
+
state.host,
|
|
795
|
+
state.anchor.element,
|
|
796
|
+
state.filter.element
|
|
797
|
+
];
|
|
798
|
+
const { length } = elements;
|
|
799
|
+
for (let index = 0; index < length; index += 1) {
|
|
800
|
+
const element = elements[index];
|
|
801
|
+
setAria(element, key, key !== "readonly" || !(element instanceof HTMLButtonElement) ? value : void 0);
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
function setComboboxReadonly(state, value) {
|
|
805
|
+
state.filter.element.readOnly = state.disabled || value;
|
|
806
|
+
setProperties("readonly", state, value);
|
|
807
|
+
}
|
|
808
|
+
function setComboboxRequired(state, value) {
|
|
809
|
+
setProperties("required", state, value);
|
|
810
|
+
updateComboboxValidity(state);
|
|
811
|
+
}
|
|
812
|
+
function updateComboboxTabindices(state, active) {
|
|
813
|
+
if (active == null || state.selection == null) return;
|
|
814
|
+
const next = state.selection.items.find((item) => getData(item.button, "option", false) === active.id);
|
|
815
|
+
if (next == null) return;
|
|
816
|
+
const { length } = state.selection.items;
|
|
817
|
+
for (let index = 0; index < length; index += 1) {
|
|
818
|
+
const item = state.selection.items[index];
|
|
819
|
+
item.button.tabIndex = item === next ? 0 : -1;
|
|
820
|
+
}
|
|
821
|
+
next.button.focus();
|
|
822
|
+
}
|
|
823
|
+
function updateComboboxAnchor(state) {
|
|
824
|
+
if (!state.multiple) return;
|
|
825
|
+
const localization = getLocalization(comboboxLocalizations, state.host, "selected", state.localizations);
|
|
826
|
+
const { length } = state.options.selected;
|
|
827
|
+
setAria(state.anchor.element, "label", localize(localization, { amount: length }, [length]));
|
|
828
|
+
}
|
|
829
|
+
function updateComboboxAutocomplete(preview, state, active) {
|
|
830
|
+
if (state.multiple) return;
|
|
831
|
+
const content = active?.textContent ?? state.filter.element.value;
|
|
832
|
+
if (preview === "typed" && !content.toLocaleLowerCase().startsWith(state.filter.element.value.toLocaleLowerCase())) return;
|
|
833
|
+
if (state.anchor.element instanceof HTMLInputElement) {
|
|
834
|
+
const contentLength = content.length;
|
|
835
|
+
const filterLength = state.filter.element.value.length;
|
|
836
|
+
if (preview === "set" || preview === "typed") {
|
|
837
|
+
state.anchor.element.value = content;
|
|
838
|
+
state.anchor.element.setSelectionRange(preview === "typed" ? filterLength : contentLength, contentLength);
|
|
839
|
+
}
|
|
840
|
+
} else state.anchor.element.textContent = content;
|
|
841
|
+
}
|
|
842
|
+
function updateComboboxMessage(state, visible) {
|
|
843
|
+
setAttribute(state.message, COMBOBOX_ATTRIBUTE_HIDDEN, visible > 0 ? "" : void 0);
|
|
844
|
+
if (visible === 0) {
|
|
845
|
+
const localization = getLocalization(comboboxLocalizations, state.host, "empty", state.localizations);
|
|
846
|
+
state.message.textContent = localize(localization, {}, []);
|
|
847
|
+
} else {
|
|
848
|
+
const localization = getLocalization(comboboxLocalizations, state.host, "results", state.localizations);
|
|
849
|
+
state.message.textContent = localize(localization, { amount: visible }, [visible]);
|
|
850
|
+
}
|
|
851
|
+
state.message.hidden = false;
|
|
852
|
+
}
|
|
853
|
+
function updateComboboxPreview(preview, state, active) {
|
|
854
|
+
const { length } = state.options.all;
|
|
855
|
+
for (let index = 0; index < length; index += 1) {
|
|
856
|
+
const option = state.options.all[index];
|
|
857
|
+
setAttribute(option, COMBOBOX_ATTRIBUTE_OPTION_ACTIVE, option === active ? "" : void 0);
|
|
858
|
+
}
|
|
859
|
+
state.active = active;
|
|
860
|
+
setAria(state.filter.element, "activedescendant", state.floatable.open ? active?.id : void 0);
|
|
861
|
+
if (state.floatable.open) active?.scrollIntoView(comboboxScrollOptions);
|
|
862
|
+
if (preview !== "none") updateComboboxAutocomplete(preview, state, active);
|
|
863
|
+
}
|
|
864
|
+
function updateComboboxSelection(state, active, added, selected) {
|
|
865
|
+
if (state.selection == null) return;
|
|
866
|
+
let options = active == null ? [] : [active];
|
|
867
|
+
if (added && selected != null) {
|
|
868
|
+
options = selected;
|
|
869
|
+
const { length } = state.selection.items;
|
|
870
|
+
for (let index = 0; index < length; index += 1) state.selection.items[index].element.remove();
|
|
871
|
+
state.selection.items.splice(0);
|
|
872
|
+
}
|
|
873
|
+
if (added) {
|
|
874
|
+
createComboboxSelectionItems({
|
|
875
|
+
options,
|
|
876
|
+
state,
|
|
877
|
+
container: state.selection.container,
|
|
878
|
+
items: state.selection.items
|
|
879
|
+
});
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
882
|
+
let current = state.selection.items.findIndex((item) => getData(item.button, "option", false) === active?.id);
|
|
883
|
+
if (current === -1) return;
|
|
884
|
+
state.selection.items[current].element.remove();
|
|
885
|
+
state.selection.items.splice(current, 1);
|
|
886
|
+
if (state.selection.items.length === 0) {
|
|
887
|
+
state.anchor.element.focus();
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
890
|
+
const { length } = state.selection.items;
|
|
891
|
+
if (current < 0) current = 0;
|
|
892
|
+
else if (current >= length) current = length - 1;
|
|
893
|
+
for (let index = 0; index < length; index += 1) {
|
|
894
|
+
const { button } = state.selection.items[index];
|
|
895
|
+
button.tabIndex = index === current ? 0 : -1;
|
|
896
|
+
}
|
|
897
|
+
state.selection.items[current].button.focus();
|
|
898
|
+
}
|
|
899
|
+
function updateComboboxSelectionItems(state) {
|
|
900
|
+
if (state.selection == null) return;
|
|
901
|
+
const localization = getLocalization(comboboxLocalizations, state.host, "remove", state.localizations);
|
|
902
|
+
const { length } = state.selection.items;
|
|
903
|
+
for (let index = 0; index < length; index += 1) {
|
|
904
|
+
const item = state.selection.items[index];
|
|
905
|
+
const option = state.options.selected.find((option) => option.id === getData(item.button, "option", false));
|
|
906
|
+
if (option == null) continue;
|
|
907
|
+
const optionLabel = option.textContent;
|
|
908
|
+
const optionValue = getData(option, "value", false) ?? "";
|
|
909
|
+
setAria(item.button, "label", localize(localization, {
|
|
910
|
+
label: optionLabel,
|
|
911
|
+
value: optionValue
|
|
912
|
+
}, [optionLabel, optionValue]));
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
//#endregion
|
|
916
|
+
//#region src/js/combobox/data.combobox.ts
|
|
917
|
+
function applyComboboxSelection(parameters) {
|
|
918
|
+
const { added, emit, option, selected, state } = parameters;
|
|
919
|
+
const { length } = state.options.all;
|
|
920
|
+
for (let index = 0; index < length; index += 1) {
|
|
921
|
+
const option = state.options.all[index];
|
|
922
|
+
setAria(option, "selected", (selected ?? state.options.selected).includes(option));
|
|
923
|
+
}
|
|
924
|
+
const values = parameters.values ?? (state.autocomplete.none ? [state.filter.element.value] : (selected ?? state.options.selected).map((option) => getData(option, "value", false) ?? ""));
|
|
925
|
+
if (values.length === 0) values.push("");
|
|
926
|
+
if (selected != null) state.options.selected = selected;
|
|
927
|
+
if (!(values.length === state.values.current.length ? values.some((value) => !state.values.current.includes(value)) : true)) return;
|
|
928
|
+
updateComboboxForm(state, values, emit);
|
|
929
|
+
updateComboboxPreview("set", state, option);
|
|
930
|
+
updateComboboxSelection(state, option, added, selected);
|
|
931
|
+
}
|
|
932
|
+
function filterComboboxOption(state, value, textContent) {
|
|
933
|
+
const filterTextContent = textContent ?? false;
|
|
934
|
+
const filter = getString(value).toLocaleLowerCase();
|
|
935
|
+
filterComboboxOptions(state);
|
|
936
|
+
const { length } = state.options.all;
|
|
937
|
+
let match;
|
|
938
|
+
for (let index = 0; index < length; index += 1) {
|
|
939
|
+
const option = state.options.all[index];
|
|
940
|
+
if ((filterTextContent ? option.textContent : getData(option, "value", false) ?? "").toLocaleLowerCase() === filter) {
|
|
941
|
+
match = option;
|
|
942
|
+
break;
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
return match;
|
|
946
|
+
}
|
|
947
|
+
function filterComboboxOptions(state) {
|
|
948
|
+
function filter(options) {
|
|
949
|
+
const { length } = options;
|
|
950
|
+
let count = 0;
|
|
951
|
+
for (let index = 0; index < length; index += 1) {
|
|
952
|
+
const option = options[index];
|
|
953
|
+
option.hidden = !state.autocomplete.none && !option.textContent.toLocaleLowerCase().includes(value);
|
|
954
|
+
if (!option.hidden) {
|
|
955
|
+
count += 1;
|
|
956
|
+
visible.push(option);
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
return count === 0;
|
|
960
|
+
}
|
|
961
|
+
const value = state.filter.element.value.toLocaleLowerCase();
|
|
962
|
+
const visible = [];
|
|
963
|
+
if (state.groups == null) filter(state.options.all);
|
|
964
|
+
else {
|
|
965
|
+
const groupsLength = state.groups.length;
|
|
966
|
+
for (let groupIndex = 0; groupIndex < groupsLength; groupIndex += 1) {
|
|
967
|
+
const { element, options } = state.groups[groupIndex];
|
|
968
|
+
element.hidden = filter(options);
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
updateComboboxMessage(state, visible.length);
|
|
972
|
+
return state.active != null && visible.includes(state.active) ? state.active : visible[0];
|
|
973
|
+
}
|
|
974
|
+
function setComboboxValue(state, value, emit) {
|
|
975
|
+
if (state.autocomplete.none) {
|
|
976
|
+
state.filter.element.value = getString(value);
|
|
977
|
+
onComboboxFilter("insertText", state, true, emit);
|
|
978
|
+
return;
|
|
979
|
+
}
|
|
980
|
+
if (filterComboboxOption(state, value) != null) setComboboxValues(state, [value], emit);
|
|
981
|
+
}
|
|
982
|
+
function setComboboxValues(state, value, emit) {
|
|
983
|
+
if (!Array.isArray(value)) return;
|
|
984
|
+
const values = (state.multiple ? value : value.slice(0, 1)).map((value) => getString(value).toLocaleLowerCase());
|
|
985
|
+
if (values.length === 0) values.push("");
|
|
986
|
+
if (state.autocomplete.none) {
|
|
987
|
+
setComboboxValue(state, values[0], emit);
|
|
988
|
+
return;
|
|
989
|
+
}
|
|
990
|
+
const { length } = state.options.all;
|
|
991
|
+
const selected = [];
|
|
992
|
+
for (let index = 0; index < length; index += 1) {
|
|
993
|
+
const option = state.options.all[index];
|
|
994
|
+
if (values.includes(getData(option, "value", false)?.toLocaleLowerCase() ?? "")) selected.push(option);
|
|
995
|
+
}
|
|
996
|
+
applyComboboxSelection({
|
|
997
|
+
emit,
|
|
998
|
+
selected,
|
|
999
|
+
state,
|
|
1000
|
+
added: true,
|
|
1001
|
+
option: selected[0] ?? state.options.all[0],
|
|
1002
|
+
values: state.autocomplete.none ? values : void 0
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
1005
|
+
function toggleComboboxOption(state, option) {
|
|
1006
|
+
if (option == null || !state.multiple && state.options.selected.includes(option)) {
|
|
1007
|
+
state.floatable.hide();
|
|
1008
|
+
return;
|
|
1009
|
+
}
|
|
1010
|
+
const selected = state.options.selected.slice();
|
|
1011
|
+
let added = true;
|
|
1012
|
+
if (state.multiple) {
|
|
1013
|
+
const index = selected.indexOf(option);
|
|
1014
|
+
if (index === -1) selected.push(option);
|
|
1015
|
+
else {
|
|
1016
|
+
added = false;
|
|
1017
|
+
selected.splice(index, 1);
|
|
1018
|
+
}
|
|
1019
|
+
} else selected.splice(0, selected.length, option);
|
|
1020
|
+
applyComboboxSelection({
|
|
1021
|
+
added,
|
|
1022
|
+
option,
|
|
1023
|
+
selected,
|
|
1024
|
+
state,
|
|
1025
|
+
values: state.autocomplete.none && option != null ? [option.textContent] : void 0
|
|
1026
|
+
});
|
|
1027
|
+
if (!state.multiple) state.floatable.hide();
|
|
1028
|
+
}
|
|
1029
|
+
//#endregion
|
|
1030
|
+
export { getComboboxAutoComplete as $, updateComboboxTabindices as A, onComboboxEnter as B, setComboboxRequired as C, updateComboboxPreview as D, updateComboboxMessage as E, comboboxStates as F, onComboboxKeydown as G, onComboboxFilter as H, createComboboxState as I, onComboboxRemoveSelection as J, onComboboxKeydownSelection as K, detachCombobox as L, updateComboboxValidity as M, attachCombobox as N, updateComboboxSelection as O, comboboxLocalizations as P, getComboboxAttributeValue as Q, onComboboxClickContent as R, setComboboxReadonly as S, updateComboboxAutocomplete as T, onComboboxFocusout as U, onComboboxEscape as V, onComboboxInput as W, onComboboxTab as X, onComboboxSelection as Y, generateComboboxIds as Z, createComboboxOptions as _, setComboboxValues as a, setComboboxLimit as at, createComboboxSelectionItems as b, createComboboxButton as c, createComboboxGroup as d, getComboboxBooleanAria as et, createComboboxHeader as f, createComboboxOption as g, createComboboxMessage as h, setComboboxValue as i, setComboboxFilter as it, updateComboboxForm as j, updateComboboxSelectionItems as k, createComboboxContent as l, createComboboxList as m, filterComboboxOption as n, getComboboxVisibleOptions as nt, toggleComboboxOption as o, setComboboxLocalizations as ot, createComboboxInput as p, onComboboxNavigation as q, filterComboboxOptions as r, hasTextSelection as rt, createComboboxAnchor as s, applyComboboxSelection as t, getComboboxId as tt, createComboboxFilter as u, createComboboxSelection as v, updateComboboxAnchor as w, setComboboxDisabled as x, createComboboxSelectionItem as y, onComboboxClickDocument as z };
|