@oscarpalmer/oui 0.33.0 → 0.34.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/i18n.combobox.d.mts +2 -0
- package/dist/js/combobox/i18n.combobox.mjs +2 -0
- package/dist/js/combobox/index.d.mts +2 -0
- package/dist/js/combobox/index.mjs +194 -0
- package/dist/js/combobox/misc.combobox.d.mts +14 -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 +11 -0
- package/dist/js/combobox/state.combobox.mjs +2 -0
- package/dist/js/constants.combobox-DqNUGBLS.d.mts +243 -0
- package/dist/js/data.combobox-8RwBv5ey.mjs +1077 -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/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 +794 -0
- package/src/js/combobox/event.combobox.ts +454 -0
- package/src/js/combobox/form.combobox.ts +115 -0
- package/src/js/combobox/i18n.combobox.ts +179 -0
- package/src/js/combobox/index.ts +307 -0
- package/src/js/combobox/misc.combobox.ts +138 -0
- package/src/js/combobox/models.combobox.ts +94 -0
- package/src/js/combobox/state.combobox.ts +235 -0
- package/src/js/index.ts +1 -1
- package/src/js/internal/draggable.ts +3 -3
- 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
package/src/js/combobox.ts
DELETED
|
@@ -1,2410 +0,0 @@
|
|
|
1
|
-
import type {GenericCallback} from '@oscarpalmer/atoms/models';
|
|
2
|
-
import {clamp} from '@oscarpalmer/atoms/number';
|
|
3
|
-
import {getString, parse} from '@oscarpalmer/atoms/string';
|
|
4
|
-
import {getAria, setAria} from '@oscarpalmer/toretto/aria';
|
|
5
|
-
import {getAttribute, setAttribute} from '@oscarpalmer/toretto/attribute';
|
|
6
|
-
import {createElement} from '@oscarpalmer/toretto/create';
|
|
7
|
-
import {getData, setData} from '@oscarpalmer/toretto/data';
|
|
8
|
-
import {dispatch, on} from '@oscarpalmer/toretto/event';
|
|
9
|
-
import {findAncestor} from '@oscarpalmer/toretto/find';
|
|
10
|
-
import type {AnyAriaBooleanAttribute, RemovableEventListener} from '@oscarpalmer/toretto/models';
|
|
11
|
-
import {createEmbeddedFloatable, type OuiFloatable} from './floatable/floatable.embedded';
|
|
12
|
-
|
|
13
|
-
// #region Types
|
|
14
|
-
|
|
15
|
-
declare global {
|
|
16
|
-
interface HTMLElementTagNameMap {
|
|
17
|
-
[TAGNAME]: OuiComboboxElement;
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
type ApplySelectionParameters = {
|
|
22
|
-
added: boolean;
|
|
23
|
-
emit?: boolean;
|
|
24
|
-
option?: HTMLElement;
|
|
25
|
-
selected?: HTMLElement[];
|
|
26
|
-
state: OuiComboboxState;
|
|
27
|
-
values?: string[];
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
type CreateAnchor = {
|
|
31
|
-
container?: HTMLElement;
|
|
32
|
-
element: HTMLElement;
|
|
33
|
-
selection?: OuiComboboxSelection;
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
type CreatAnchorParameters = {
|
|
37
|
-
autocomplete: OuiComboboxAutoComplete;
|
|
38
|
-
id: string;
|
|
39
|
-
label: HTMLLabelElement;
|
|
40
|
-
multiple: boolean;
|
|
41
|
-
select: HTMLSelectElement;
|
|
42
|
-
type: OuiComboboxType;
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
type CreateContent = {
|
|
46
|
-
element: HTMLElement;
|
|
47
|
-
filter?: CreateFilter;
|
|
48
|
-
list: CreateList;
|
|
49
|
-
message: HTMLElement;
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
type CreateContentParameters = {
|
|
53
|
-
autocomplete: OuiComboboxAutoComplete;
|
|
54
|
-
id: string;
|
|
55
|
-
label: HTMLLabelElement;
|
|
56
|
-
multiple: boolean;
|
|
57
|
-
select: HTMLSelectElement;
|
|
58
|
-
type: OuiComboboxType;
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
type CreateFilter = {
|
|
62
|
-
container: HTMLElement;
|
|
63
|
-
element: HTMLInputElement;
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
type CreateFilterParameters = {
|
|
67
|
-
autocomplete: OuiComboboxAutoCompleteType;
|
|
68
|
-
id: string;
|
|
69
|
-
label: HTMLLabelElement;
|
|
70
|
-
type: OuiComboboxType;
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
type CreateGroupParameters = {
|
|
74
|
-
autocomplete: OuiComboboxAutoComplete;
|
|
75
|
-
group: HTMLOptGroupElement;
|
|
76
|
-
groupOptions: HTMLOptionElement[];
|
|
77
|
-
selected: HTMLElement[];
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
type CreateInputParameters = {
|
|
81
|
-
autocomplete: OuiComboboxAutoCompleteType;
|
|
82
|
-
id: string;
|
|
83
|
-
label: HTMLLabelElement;
|
|
84
|
-
type: OuiComboboxType;
|
|
85
|
-
value: string;
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
type CreateList = {
|
|
89
|
-
element: HTMLElement;
|
|
90
|
-
groups?: OuiComboboxGroup[];
|
|
91
|
-
options: HTMLElement[];
|
|
92
|
-
selected: HTMLElement[];
|
|
93
|
-
};
|
|
94
|
-
|
|
95
|
-
type CreateListParameters = {
|
|
96
|
-
autocomplete: OuiComboboxAutoComplete;
|
|
97
|
-
id: string;
|
|
98
|
-
label: HTMLLabelElement;
|
|
99
|
-
multiple: boolean;
|
|
100
|
-
select: HTMLSelectElement;
|
|
101
|
-
};
|
|
102
|
-
|
|
103
|
-
type CreateOptionParameters = {
|
|
104
|
-
autocomplete: OuiComboboxAutoComplete;
|
|
105
|
-
group?: string;
|
|
106
|
-
index: number;
|
|
107
|
-
option: HTMLOptionElement;
|
|
108
|
-
};
|
|
109
|
-
|
|
110
|
-
type CreateOptionsParameters = {
|
|
111
|
-
autocomplete: OuiComboboxAutoComplete;
|
|
112
|
-
group?: string;
|
|
113
|
-
list: HTMLElement;
|
|
114
|
-
options: HTMLOptionElement[];
|
|
115
|
-
selected: HTMLElement[];
|
|
116
|
-
};
|
|
117
|
-
|
|
118
|
-
type CreateSelectionItemsParameters = {
|
|
119
|
-
container: HTMLElement;
|
|
120
|
-
items: OuiComboboxSelectionItem[];
|
|
121
|
-
options: HTMLElement[];
|
|
122
|
-
state?: OuiComboboxState;
|
|
123
|
-
};
|
|
124
|
-
|
|
125
|
-
type OuiComboboxAnchor = {
|
|
126
|
-
container?: HTMLElement;
|
|
127
|
-
element: HTMLElement;
|
|
128
|
-
};
|
|
129
|
-
|
|
130
|
-
type OuiComboboxAutoComplete = Record<OuiComboboxAutoCompleteType, boolean> & {
|
|
131
|
-
value: OuiComboboxAutoCompleteType;
|
|
132
|
-
};
|
|
133
|
-
|
|
134
|
-
type OuiComboboxAutoCompleteType = 'both' | 'inline' | 'list' | 'none';
|
|
135
|
-
|
|
136
|
-
export class OuiComboboxElement extends HTMLElement {
|
|
137
|
-
static formAssociated = true;
|
|
138
|
-
|
|
139
|
-
static observedAttributes = ['max', 'min', 'required', 'value'];
|
|
140
|
-
|
|
141
|
-
readonly #internals: ElementInternals;
|
|
142
|
-
|
|
143
|
-
#restored?: string[];
|
|
144
|
-
|
|
145
|
-
#state!: OuiComboboxState;
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* Is the combobox disabled?
|
|
149
|
-
*/
|
|
150
|
-
get disabled(): boolean {
|
|
151
|
-
return this.#state?.disabled ?? false;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* Should the combobox be disabled?
|
|
156
|
-
*/
|
|
157
|
-
set disabled(value: boolean) {
|
|
158
|
-
if (typeof value === 'boolean' && value !== this.disabled) {
|
|
159
|
-
setDisabled(this.#state, value);
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
/**
|
|
164
|
-
* Current filter value
|
|
165
|
-
*/
|
|
166
|
-
get filter(): string {
|
|
167
|
-
return this.#state.filter.element.value;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
/**
|
|
171
|
-
* Set the filter value
|
|
172
|
-
*/
|
|
173
|
-
set filter(value: string) {
|
|
174
|
-
setFilter(this.#state, value);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
/**
|
|
178
|
-
* Maximum number of selected options
|
|
179
|
-
*/
|
|
180
|
-
get max(): number | undefined {
|
|
181
|
-
return this.#state?.max;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
/**
|
|
185
|
-
* Set the maximum number of selected options
|
|
186
|
-
*/
|
|
187
|
-
set max(value: number) {
|
|
188
|
-
setLimit('max', this.#state, value);
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
/**
|
|
192
|
-
* Minimum number of selected options
|
|
193
|
-
*/
|
|
194
|
-
get min(): number | undefined {
|
|
195
|
-
return this.#state?.min;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
/**
|
|
199
|
-
* Set the minimum number of selected options
|
|
200
|
-
*/
|
|
201
|
-
set min(value: number) {
|
|
202
|
-
setLimit('min', this.#state, value);
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
/**
|
|
206
|
-
* Does the combobox allow multiple values?
|
|
207
|
-
*/
|
|
208
|
-
get multiple(): boolean {
|
|
209
|
-
return this.hasAttribute('multiple');
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
/**
|
|
213
|
-
* Name of the combobox
|
|
214
|
-
*/
|
|
215
|
-
get name(): string {
|
|
216
|
-
return getAttribute(this, 'name') ?? '';
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
/**
|
|
220
|
-
* Is the combobox readonly?
|
|
221
|
-
*/
|
|
222
|
-
get readonly(): boolean {
|
|
223
|
-
return this.#state?.readonly ?? false;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
/**
|
|
227
|
-
* Should the combobox be readonly?
|
|
228
|
-
*/
|
|
229
|
-
set readonly(value: boolean) {
|
|
230
|
-
if (typeof value === 'boolean' && value !== this.readonly) {
|
|
231
|
-
setReadonly(this.#state, value);
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
/**
|
|
236
|
-
* Is the combobox required?
|
|
237
|
-
*/
|
|
238
|
-
get required(): boolean {
|
|
239
|
-
return this.#state?.required ?? false;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
/**
|
|
243
|
-
* Should the combobox be required?
|
|
244
|
-
*/
|
|
245
|
-
set required(value: boolean) {
|
|
246
|
-
if (typeof value === 'boolean' && value !== this.required) {
|
|
247
|
-
setRequired(this.#state, value);
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
/**
|
|
252
|
-
* Selected options
|
|
253
|
-
*/
|
|
254
|
-
get selected(): HTMLElement[] {
|
|
255
|
-
return this.#state.options.selected.slice();
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
/**
|
|
259
|
-
* Type of combobox
|
|
260
|
-
*/
|
|
261
|
-
get type(): OuiComboboxType {
|
|
262
|
-
return this.#state.type;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
/**
|
|
266
|
-
* Current value
|
|
267
|
-
*/
|
|
268
|
-
get value(): string {
|
|
269
|
-
return this.#state.values.current[0];
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
/**
|
|
273
|
-
* Set the value
|
|
274
|
-
*/
|
|
275
|
-
set value(value: unknown) {
|
|
276
|
-
setValue(this.#state, value);
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
/**
|
|
280
|
-
* Current values
|
|
281
|
-
*/
|
|
282
|
-
get values(): string[] {
|
|
283
|
-
return this.#state.values.current.slice();
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
/**
|
|
287
|
-
* Set the values
|
|
288
|
-
*/
|
|
289
|
-
set values(value: unknown[]) {
|
|
290
|
-
setValues(this.#state, value);
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
constructor() {
|
|
294
|
-
super();
|
|
295
|
-
|
|
296
|
-
this.#internals = this.attachInternals();
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
attributeChangedCallback(name: string, _: string | null, value: string | null): void {
|
|
300
|
-
if (this.#state == null) {
|
|
301
|
-
return;
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
switch (name) {
|
|
305
|
-
case 'required':
|
|
306
|
-
this[name] = this.hasAttribute(name);
|
|
307
|
-
break;
|
|
308
|
-
|
|
309
|
-
case 'max':
|
|
310
|
-
case 'min':
|
|
311
|
-
setLimit(name, this.#state, value);
|
|
312
|
-
break;
|
|
313
|
-
|
|
314
|
-
case 'value':
|
|
315
|
-
this.value = value ?? '';
|
|
316
|
-
break;
|
|
317
|
-
|
|
318
|
-
default:
|
|
319
|
-
break;
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
/**
|
|
324
|
-
* Clear all selected options
|
|
325
|
-
*/
|
|
326
|
-
clear(): void {
|
|
327
|
-
setValues(this.#state, []);
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
connectedCallback(): void {
|
|
331
|
-
if (this.#state == null) {
|
|
332
|
-
this.#initialize();
|
|
333
|
-
} else {
|
|
334
|
-
attachCombobox(this.#state);
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
disconnectedCallback(): void {
|
|
339
|
-
if (this.#state != null) {
|
|
340
|
-
detachCombobox(this.#state);
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
formDisabledCallback(disabled: boolean): void {
|
|
345
|
-
this.disabled = disabled;
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
formResetCallback(): void {
|
|
349
|
-
setValues(this.#state, this.#state.values.initial, false);
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
formStateRestoreCallback(state: string | File | FormData, reason: unknown): void {
|
|
353
|
-
if (reason !== 'restore') {
|
|
354
|
-
return;
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
const parsed = typeof state === 'string' ? parse(state) : undefined;
|
|
358
|
-
|
|
359
|
-
if (!Array.isArray(parsed)) {
|
|
360
|
-
return;
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
if (this.#state == null) {
|
|
364
|
-
this.#restored = parsed;
|
|
365
|
-
} else {
|
|
366
|
-
setValues(this.#state, parsed);
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
/**
|
|
371
|
-
* Set combobox localizations for a specific language
|
|
372
|
-
*
|
|
373
|
-
* @param language Language code
|
|
374
|
-
* @param localizations Localizations for language
|
|
375
|
-
*/
|
|
376
|
-
setLocalizations(language: string, localizations: Partial<OuiComboboxLocalizations>): void;
|
|
377
|
-
|
|
378
|
-
/**
|
|
379
|
-
* Set combobox localizations for one or more languages
|
|
380
|
-
*
|
|
381
|
-
* @param localizations Localizations for one or more languages
|
|
382
|
-
*/
|
|
383
|
-
setLocalizations(localizations: Record<string, Partial<OuiComboboxLocalizations>>): void;
|
|
384
|
-
|
|
385
|
-
setLocalizations(
|
|
386
|
-
first: string | Record<string, Partial<OuiComboboxLocalizations>>,
|
|
387
|
-
second?: Partial<OuiComboboxLocalizations>,
|
|
388
|
-
): void {
|
|
389
|
-
updateLocalizations(
|
|
390
|
-
typeof first === 'string'
|
|
391
|
-
? {
|
|
392
|
-
[first]: second!,
|
|
393
|
-
}
|
|
394
|
-
: first,
|
|
395
|
-
this.#state.localizations,
|
|
396
|
-
);
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
#initialize(): void {
|
|
400
|
-
this.tabIndex = -1;
|
|
401
|
-
|
|
402
|
-
this.#state = createState(this, this.#internals);
|
|
403
|
-
|
|
404
|
-
if (this.#restored != null) {
|
|
405
|
-
setValues(this.#state, this.#restored, false);
|
|
406
|
-
|
|
407
|
-
this.#restored = undefined;
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
type OuiComboboxFilter = {
|
|
413
|
-
element: HTMLInputElement;
|
|
414
|
-
value: string;
|
|
415
|
-
};
|
|
416
|
-
|
|
417
|
-
type OuiComboboxGroup = {
|
|
418
|
-
element: HTMLElement;
|
|
419
|
-
header: HTMLElement;
|
|
420
|
-
options: HTMLElement[];
|
|
421
|
-
};
|
|
422
|
-
|
|
423
|
-
type OuiComboboxLabel = {
|
|
424
|
-
element: HTMLLabelElement;
|
|
425
|
-
id: string;
|
|
426
|
-
};
|
|
427
|
-
|
|
428
|
-
type OuiComboboxLocalizations = {
|
|
429
|
-
empty: string | (() => string);
|
|
430
|
-
input: string | (() => string);
|
|
431
|
-
invalid: string | (() => string);
|
|
432
|
-
max: string | ((max: number) => string);
|
|
433
|
-
min: string | ((min: number) => string);
|
|
434
|
-
remove: string | ((label: string, value: string) => string);
|
|
435
|
-
results: string | ((amount: number) => string);
|
|
436
|
-
selected: string | ((amount: number) => string);
|
|
437
|
-
};
|
|
438
|
-
|
|
439
|
-
type OuiComboboxOptions = {
|
|
440
|
-
all: HTMLElement[];
|
|
441
|
-
selected: HTMLElement[];
|
|
442
|
-
};
|
|
443
|
-
|
|
444
|
-
type OuiComboboxPreview = 'none' | 'set' | 'typed';
|
|
445
|
-
|
|
446
|
-
type OuiComboboxSelection = {
|
|
447
|
-
container: HTMLElement;
|
|
448
|
-
items: OuiComboboxSelectionItem[];
|
|
449
|
-
};
|
|
450
|
-
|
|
451
|
-
type OuiComboboxSelectionItem = {
|
|
452
|
-
button: HTMLButtonElement;
|
|
453
|
-
element: HTMLElement;
|
|
454
|
-
};
|
|
455
|
-
|
|
456
|
-
type OuiComboboxState = {
|
|
457
|
-
active?: HTMLElement;
|
|
458
|
-
anchor: OuiComboboxAnchor;
|
|
459
|
-
autocomplete: OuiComboboxAutoComplete;
|
|
460
|
-
content: HTMLElement;
|
|
461
|
-
disabled: boolean;
|
|
462
|
-
filter: OuiComboboxFilter;
|
|
463
|
-
floatable: OuiFloatable;
|
|
464
|
-
groups?: OuiComboboxGroup[];
|
|
465
|
-
host: OuiComboboxElement;
|
|
466
|
-
id: string;
|
|
467
|
-
internals: ElementInternals;
|
|
468
|
-
label: OuiComboboxLabel;
|
|
469
|
-
listeners: RemovableEventListener[];
|
|
470
|
-
localizations: Record<string, Partial<OuiComboboxLocalizations>>;
|
|
471
|
-
max?: number;
|
|
472
|
-
min?: number;
|
|
473
|
-
message: HTMLElement;
|
|
474
|
-
name: string;
|
|
475
|
-
multiple: boolean;
|
|
476
|
-
options: OuiComboboxOptions;
|
|
477
|
-
readonly: boolean;
|
|
478
|
-
required: boolean;
|
|
479
|
-
select: HTMLSelectElement;
|
|
480
|
-
selection?: OuiComboboxSelection;
|
|
481
|
-
type: OuiComboboxType;
|
|
482
|
-
values: OuiComboboxValues;
|
|
483
|
-
};
|
|
484
|
-
|
|
485
|
-
type OuiComboboxType = 'autocomplete' | 'select';
|
|
486
|
-
|
|
487
|
-
type OuiComboboxValues = {
|
|
488
|
-
current: string[];
|
|
489
|
-
initial: string[];
|
|
490
|
-
};
|
|
491
|
-
|
|
492
|
-
// #endregion
|
|
493
|
-
|
|
494
|
-
// #region Functions
|
|
495
|
-
|
|
496
|
-
function applySelection(parameters: ApplySelectionParameters): void {
|
|
497
|
-
const {added, emit, option, selected, state} = parameters;
|
|
498
|
-
|
|
499
|
-
const {length} = state.options.all;
|
|
500
|
-
|
|
501
|
-
for (let index = 0; index < length; index += 1) {
|
|
502
|
-
const option = state.options.all[index];
|
|
503
|
-
|
|
504
|
-
setAria(option, 'selected', (selected ?? state.options.selected).includes(option));
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
const values =
|
|
508
|
-
parameters.values ??
|
|
509
|
-
(state.autocomplete.none
|
|
510
|
-
? [state.filter.element.value]
|
|
511
|
-
: (selected ?? state.options.selected).map(option => getData(option, 'value', false) ?? ''));
|
|
512
|
-
|
|
513
|
-
if (values.length === 0) {
|
|
514
|
-
values.push('');
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
if (selected != null) {
|
|
518
|
-
state.options.selected = selected;
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
const update =
|
|
522
|
-
values.length === state.values.current.length
|
|
523
|
-
? values.some(value => !state.values.current.includes(value))
|
|
524
|
-
: true;
|
|
525
|
-
|
|
526
|
-
if (!update) {
|
|
527
|
-
return;
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
updateForm(state, values, emit);
|
|
531
|
-
updatePreview('set', state, option);
|
|
532
|
-
updateSelection(state, option, added, selected);
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
function attachCombobox(state: OuiComboboxState): void {
|
|
536
|
-
const {anchor, content, filter, label, listeners, select} = state;
|
|
537
|
-
|
|
538
|
-
label.element.htmlFor = filter.element.id;
|
|
539
|
-
label.element.id = `${state.id}_label`;
|
|
540
|
-
|
|
541
|
-
select.hidden = true;
|
|
542
|
-
|
|
543
|
-
select.insertAdjacentElement('afterend', anchor.container ?? anchor.element);
|
|
544
|
-
(anchor.container ?? anchor.element).insertAdjacentElement('afterend', content);
|
|
545
|
-
|
|
546
|
-
select.remove();
|
|
547
|
-
|
|
548
|
-
state.floatable = createEmbeddedFloatable(anchor.container ?? anchor.element, content, {
|
|
549
|
-
position: 'below-start',
|
|
550
|
-
reusable: false,
|
|
551
|
-
});
|
|
552
|
-
|
|
553
|
-
state.floatable.update();
|
|
554
|
-
|
|
555
|
-
listeners.push(
|
|
556
|
-
on(state.host, 'focusout', () => {
|
|
557
|
-
setTimeout(() => {
|
|
558
|
-
onFocusout(state);
|
|
559
|
-
});
|
|
560
|
-
}),
|
|
561
|
-
on(content, 'click', event => {
|
|
562
|
-
onClickContent(event, content);
|
|
563
|
-
}),
|
|
564
|
-
on(content, 'toggle', () => {
|
|
565
|
-
setAria(anchor.element, 'expanded', state.floatable.open);
|
|
566
|
-
|
|
567
|
-
setAria(
|
|
568
|
-
filter.element,
|
|
569
|
-
'activedescendant',
|
|
570
|
-
state.floatable.open ? state.active?.id : undefined,
|
|
571
|
-
);
|
|
572
|
-
|
|
573
|
-
if (state.floatable.open) {
|
|
574
|
-
filter.element.focus();
|
|
575
|
-
}
|
|
576
|
-
}),
|
|
577
|
-
);
|
|
578
|
-
|
|
579
|
-
states.map.set(state.host, state);
|
|
580
|
-
states.map.set(state.content, state);
|
|
581
|
-
states.set.add(state);
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
function createAnchor(parameters: CreatAnchorParameters): CreateAnchor {
|
|
585
|
-
const {autocomplete, id, label, select, type} = parameters;
|
|
586
|
-
|
|
587
|
-
let container: HTMLElement | undefined;
|
|
588
|
-
let element: HTMLElement;
|
|
589
|
-
let selection: OuiComboboxSelection | undefined;
|
|
590
|
-
|
|
591
|
-
if (parameters.type === 'autocomplete') {
|
|
592
|
-
element = createInput({
|
|
593
|
-
id,
|
|
594
|
-
label,
|
|
595
|
-
type,
|
|
596
|
-
autocomplete: autocomplete.value,
|
|
597
|
-
value: select.selectedOptions[0]?.textContent ?? '',
|
|
598
|
-
});
|
|
599
|
-
} else {
|
|
600
|
-
({container, element, selection} = createButton(parameters));
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
setAttribute(container ?? element, ATTRIBUTE_ANCHOR, '');
|
|
604
|
-
|
|
605
|
-
return {container, element, selection};
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
function createButton(parameters: CreatAnchorParameters): CreateAnchor {
|
|
609
|
-
const {id, label, multiple, select} = parameters;
|
|
610
|
-
|
|
611
|
-
const element = createElement('button', {
|
|
612
|
-
aria: {
|
|
613
|
-
controls: `${id}_content`,
|
|
614
|
-
expanded: false,
|
|
615
|
-
haspopup: 'listbox',
|
|
616
|
-
labelledby: `${label.id} ${id}`,
|
|
617
|
-
},
|
|
618
|
-
property: {
|
|
619
|
-
id,
|
|
620
|
-
textContent: multiple ? '' : (select.selectedOptions[0]?.textContent ?? ''),
|
|
621
|
-
},
|
|
622
|
-
});
|
|
623
|
-
|
|
624
|
-
if (!multiple) {
|
|
625
|
-
return {element};
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
const container = createElement('div');
|
|
629
|
-
const selection = createSelection(id, select);
|
|
630
|
-
|
|
631
|
-
container.append(selection.container, element);
|
|
632
|
-
|
|
633
|
-
return {container, element, selection};
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
function createContent(parameters: CreateContentParameters): CreateContent {
|
|
637
|
-
const {autocomplete, id, label, multiple, select, type} = parameters;
|
|
638
|
-
|
|
639
|
-
const element = createElement('div', {
|
|
640
|
-
aria: {
|
|
641
|
-
labelledby: label.id,
|
|
642
|
-
},
|
|
643
|
-
attribute: {
|
|
644
|
-
[ATTRIBUTE_CONTENT]: '',
|
|
645
|
-
popover: '',
|
|
646
|
-
},
|
|
647
|
-
property: {
|
|
648
|
-
id: `${id}_content`,
|
|
649
|
-
tabIndex: -1,
|
|
650
|
-
},
|
|
651
|
-
});
|
|
652
|
-
|
|
653
|
-
const filter = createFilter({id, label, type, autocomplete: autocomplete.value});
|
|
654
|
-
const list = createList({autocomplete, id, label, multiple, select});
|
|
655
|
-
const message = createMessage();
|
|
656
|
-
|
|
657
|
-
if (filter != null) {
|
|
658
|
-
element.append(filter.container);
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
element.append(list.element, message);
|
|
662
|
-
|
|
663
|
-
return {element, filter, list, message};
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
function createFilter(parameters: CreateFilterParameters): CreateFilter | undefined {
|
|
667
|
-
const {autocomplete, id, label, type} = parameters;
|
|
668
|
-
|
|
669
|
-
if (type === 'autocomplete') {
|
|
670
|
-
return;
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
const container = createElement('div', {
|
|
674
|
-
attribute: {
|
|
675
|
-
[ATTRIBUTE_FILTER]: '',
|
|
676
|
-
},
|
|
677
|
-
});
|
|
678
|
-
|
|
679
|
-
const element = createInput({autocomplete, id, label, type, value: ''});
|
|
680
|
-
|
|
681
|
-
setAttribute(element, ATTRIBUTE_FILTER_INPUT, '');
|
|
682
|
-
|
|
683
|
-
container.append(element);
|
|
684
|
-
|
|
685
|
-
return {container, element};
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
function createGroup(parameters: CreateGroupParameters): OuiComboboxGroup {
|
|
689
|
-
const {autocomplete, group, groupOptions, selected} = parameters;
|
|
690
|
-
|
|
691
|
-
const element = createElement('li', {
|
|
692
|
-
aria: {
|
|
693
|
-
label: group.label,
|
|
694
|
-
},
|
|
695
|
-
property: {
|
|
696
|
-
role: 'group',
|
|
697
|
-
},
|
|
698
|
-
});
|
|
699
|
-
|
|
700
|
-
const list = createElement('ul', {
|
|
701
|
-
property: {
|
|
702
|
-
role: 'none',
|
|
703
|
-
},
|
|
704
|
-
});
|
|
705
|
-
|
|
706
|
-
const header = createHeader(group);
|
|
707
|
-
|
|
708
|
-
element.append(header, list);
|
|
709
|
-
|
|
710
|
-
return {
|
|
711
|
-
element,
|
|
712
|
-
header,
|
|
713
|
-
options: createOptions({
|
|
714
|
-
autocomplete,
|
|
715
|
-
list,
|
|
716
|
-
selected,
|
|
717
|
-
group: header.id,
|
|
718
|
-
options: groupOptions,
|
|
719
|
-
}),
|
|
720
|
-
};
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
function createHeader(group: HTMLOptGroupElement): HTMLElement {
|
|
724
|
-
return createElement('div', {
|
|
725
|
-
aria: {
|
|
726
|
-
hidden: true,
|
|
727
|
-
},
|
|
728
|
-
attribute: {
|
|
729
|
-
[ATTRIBUTE_HEADER]: '',
|
|
730
|
-
},
|
|
731
|
-
property: {
|
|
732
|
-
id: group.id,
|
|
733
|
-
textContent: group.label,
|
|
734
|
-
},
|
|
735
|
-
});
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
function createInput(parameters: CreateInputParameters): HTMLInputElement {
|
|
739
|
-
const {autocomplete, id, label, type, value} = parameters;
|
|
740
|
-
|
|
741
|
-
return createElement('input', {
|
|
742
|
-
aria: {
|
|
743
|
-
autocomplete: autocomplete,
|
|
744
|
-
controls: `${id}_list`,
|
|
745
|
-
expanded: type === 'select',
|
|
746
|
-
haspopup: 'listbox',
|
|
747
|
-
labelledby: label.id,
|
|
748
|
-
},
|
|
749
|
-
attribute: {
|
|
750
|
-
autocorrect: 'off',
|
|
751
|
-
autocomplete: 'off',
|
|
752
|
-
form: '',
|
|
753
|
-
spellcheck: 'false',
|
|
754
|
-
type: 'text',
|
|
755
|
-
},
|
|
756
|
-
property: {
|
|
757
|
-
id,
|
|
758
|
-
value,
|
|
759
|
-
role: 'combobox',
|
|
760
|
-
},
|
|
761
|
-
});
|
|
762
|
-
}
|
|
763
|
-
|
|
764
|
-
function createList(parameters: CreateListParameters): CreateList {
|
|
765
|
-
const {autocomplete, id, label, multiple, select} = parameters;
|
|
766
|
-
|
|
767
|
-
const element = createElement('ul', {
|
|
768
|
-
aria: {
|
|
769
|
-
labelledby: label.id,
|
|
770
|
-
multiselectable: multiple,
|
|
771
|
-
},
|
|
772
|
-
attribute: {
|
|
773
|
-
[ATTRIBUTE_LIST]: '',
|
|
774
|
-
tabindex: -1,
|
|
775
|
-
},
|
|
776
|
-
property: {
|
|
777
|
-
id: `${id}_list`,
|
|
778
|
-
role: 'listbox',
|
|
779
|
-
},
|
|
780
|
-
});
|
|
781
|
-
|
|
782
|
-
const children = [...select.children];
|
|
783
|
-
const childrenLength = children.length;
|
|
784
|
-
|
|
785
|
-
const options: HTMLElement[] = [];
|
|
786
|
-
const selected: HTMLElement[] = [];
|
|
787
|
-
|
|
788
|
-
let groups: OuiComboboxGroup[] | undefined;
|
|
789
|
-
|
|
790
|
-
for (let childIndex = 0; childIndex < childrenLength; childIndex += 1) {
|
|
791
|
-
const child = children[childIndex];
|
|
792
|
-
|
|
793
|
-
if (child instanceof HTMLOptionElement) {
|
|
794
|
-
const option = createOption({
|
|
795
|
-
autocomplete,
|
|
796
|
-
index: childIndex,
|
|
797
|
-
option: child,
|
|
798
|
-
});
|
|
799
|
-
|
|
800
|
-
element.append(option);
|
|
801
|
-
options.push(option);
|
|
802
|
-
|
|
803
|
-
if (child.selected) {
|
|
804
|
-
selected.push(option);
|
|
805
|
-
}
|
|
806
|
-
|
|
807
|
-
continue;
|
|
808
|
-
}
|
|
809
|
-
|
|
810
|
-
if (!(child instanceof HTMLOptGroupElement)) {
|
|
811
|
-
continue;
|
|
812
|
-
}
|
|
813
|
-
|
|
814
|
-
const group = createGroup({
|
|
815
|
-
autocomplete,
|
|
816
|
-
selected,
|
|
817
|
-
group: child,
|
|
818
|
-
groupOptions: [...child.children] as HTMLOptionElement[],
|
|
819
|
-
});
|
|
820
|
-
|
|
821
|
-
groups ??= [];
|
|
822
|
-
|
|
823
|
-
element.append(group.element, ...group.options);
|
|
824
|
-
groups.push(group);
|
|
825
|
-
options.push(...group.options);
|
|
826
|
-
}
|
|
827
|
-
|
|
828
|
-
return {element, groups, options, selected};
|
|
829
|
-
}
|
|
830
|
-
|
|
831
|
-
function createMessage(): HTMLElement {
|
|
832
|
-
return createElement('div', {
|
|
833
|
-
aria: {
|
|
834
|
-
live: 'polite',
|
|
835
|
-
},
|
|
836
|
-
attribute: {
|
|
837
|
-
[ATTRIBUTE_MESSAGE]: '',
|
|
838
|
-
},
|
|
839
|
-
property: {
|
|
840
|
-
hidden: true,
|
|
841
|
-
role: 'status',
|
|
842
|
-
},
|
|
843
|
-
});
|
|
844
|
-
}
|
|
845
|
-
|
|
846
|
-
function createOption(parameters: CreateOptionParameters): HTMLElement {
|
|
847
|
-
const {autocomplete, group, option} = parameters;
|
|
848
|
-
|
|
849
|
-
const item = createElement('li', {
|
|
850
|
-
aria: {
|
|
851
|
-
selected: !autocomplete.none && option.selected,
|
|
852
|
-
},
|
|
853
|
-
attribute: {
|
|
854
|
-
[ATTRIBUTE_OPTION]: '',
|
|
855
|
-
},
|
|
856
|
-
property: {
|
|
857
|
-
id: option.id,
|
|
858
|
-
role: 'option',
|
|
859
|
-
textContent: option.textContent,
|
|
860
|
-
},
|
|
861
|
-
});
|
|
862
|
-
|
|
863
|
-
setData(item, {
|
|
864
|
-
group,
|
|
865
|
-
value: option.value,
|
|
866
|
-
});
|
|
867
|
-
|
|
868
|
-
return item;
|
|
869
|
-
}
|
|
870
|
-
|
|
871
|
-
function createOptions(parameters: CreateOptionsParameters): HTMLElement[] {
|
|
872
|
-
const {autocomplete, group, list, options, selected} = parameters;
|
|
873
|
-
|
|
874
|
-
const items: HTMLElement[] = [];
|
|
875
|
-
|
|
876
|
-
for (let index = 0; index < options.length; index += 1) {
|
|
877
|
-
const option = options[index];
|
|
878
|
-
|
|
879
|
-
const item = createOption({autocomplete, group, index, option});
|
|
880
|
-
|
|
881
|
-
items.push(item);
|
|
882
|
-
|
|
883
|
-
if (option.selected) {
|
|
884
|
-
selected.push(item);
|
|
885
|
-
}
|
|
886
|
-
|
|
887
|
-
list.append(item);
|
|
888
|
-
}
|
|
889
|
-
|
|
890
|
-
return items;
|
|
891
|
-
}
|
|
892
|
-
|
|
893
|
-
function createSelection(id: string, select: HTMLSelectElement): OuiComboboxSelection {
|
|
894
|
-
const container = createElement('ul', {
|
|
895
|
-
attribute: {
|
|
896
|
-
[ATTRIBUTE_SELECTION]: '',
|
|
897
|
-
},
|
|
898
|
-
property: {
|
|
899
|
-
id: `${id}_selection`,
|
|
900
|
-
},
|
|
901
|
-
});
|
|
902
|
-
|
|
903
|
-
const options = [...select.selectedOptions];
|
|
904
|
-
|
|
905
|
-
const items: OuiComboboxSelectionItem[] = [];
|
|
906
|
-
|
|
907
|
-
createSelectionItems({container, items, options});
|
|
908
|
-
|
|
909
|
-
const {button} = items[0] ?? {};
|
|
910
|
-
|
|
911
|
-
if (button != null) {
|
|
912
|
-
button.tabIndex = 0;
|
|
913
|
-
}
|
|
914
|
-
|
|
915
|
-
return {
|
|
916
|
-
items,
|
|
917
|
-
container: container,
|
|
918
|
-
};
|
|
919
|
-
}
|
|
920
|
-
|
|
921
|
-
function createSelectionItem(
|
|
922
|
-
option: HTMLElement,
|
|
923
|
-
state?: OuiComboboxState,
|
|
924
|
-
): OuiComboboxSelectionItem {
|
|
925
|
-
const element = createElement('li');
|
|
926
|
-
|
|
927
|
-
const localization = state == null ? option.textContent : getLocalization('remove', state);
|
|
928
|
-
|
|
929
|
-
const button = createElement('button', {
|
|
930
|
-
aria: {
|
|
931
|
-
label:
|
|
932
|
-
typeof localization === 'function'
|
|
933
|
-
? localization(option.textContent ?? '', getData(option, 'value', false) ?? '')
|
|
934
|
-
: localization,
|
|
935
|
-
},
|
|
936
|
-
attribute: {
|
|
937
|
-
[ATTRIBUTE_SELECTION_ITEM]: '',
|
|
938
|
-
tabindex: -1,
|
|
939
|
-
},
|
|
940
|
-
data: {
|
|
941
|
-
option: option.id,
|
|
942
|
-
},
|
|
943
|
-
});
|
|
944
|
-
|
|
945
|
-
const icon = createElement('span', {
|
|
946
|
-
aria: {
|
|
947
|
-
hidden: true,
|
|
948
|
-
},
|
|
949
|
-
property: {
|
|
950
|
-
innerHTML: '×',
|
|
951
|
-
},
|
|
952
|
-
});
|
|
953
|
-
|
|
954
|
-
const label = createElement('span', {
|
|
955
|
-
property: {
|
|
956
|
-
textContent: option.textContent,
|
|
957
|
-
},
|
|
958
|
-
});
|
|
959
|
-
|
|
960
|
-
button.append(icon, label);
|
|
961
|
-
element.append(button);
|
|
962
|
-
|
|
963
|
-
return {
|
|
964
|
-
button,
|
|
965
|
-
element,
|
|
966
|
-
};
|
|
967
|
-
}
|
|
968
|
-
|
|
969
|
-
function createSelectionItems(parameters: CreateSelectionItemsParameters): void {
|
|
970
|
-
const {container, items, options, state} = parameters;
|
|
971
|
-
|
|
972
|
-
const {length} = options;
|
|
973
|
-
|
|
974
|
-
for (let index = 0; index < length; index += 1) {
|
|
975
|
-
const option = options[index];
|
|
976
|
-
|
|
977
|
-
const item = createSelectionItem(option, state);
|
|
978
|
-
|
|
979
|
-
container.append(item.element);
|
|
980
|
-
items.push(item);
|
|
981
|
-
}
|
|
982
|
-
}
|
|
983
|
-
|
|
984
|
-
function createState(element: OuiComboboxElement, internals: ElementInternals): OuiComboboxState {
|
|
985
|
-
const select = element.querySelector('select');
|
|
986
|
-
|
|
987
|
-
if (select == null || select.id === '') {
|
|
988
|
-
throw new Error(MESSAGE_SELECT);
|
|
989
|
-
}
|
|
990
|
-
|
|
991
|
-
const label = element.querySelector(`label[for="${select.id}"]`) as HTMLLabelElement;
|
|
992
|
-
|
|
993
|
-
if (label == null) {
|
|
994
|
-
throw new Error(MESSAGE_LABEL);
|
|
995
|
-
}
|
|
996
|
-
|
|
997
|
-
element.id = select.id;
|
|
998
|
-
|
|
999
|
-
setAttribute(element, 'name', select.name === '' ? select.id : select.name);
|
|
1000
|
-
|
|
1001
|
-
const disabled = getBooleanAria(element, 'disabled') || select.disabled;
|
|
1002
|
-
const multiple = element.multiple || select.multiple;
|
|
1003
|
-
const readonly = getBooleanAria(element, 'readonly');
|
|
1004
|
-
const required = getBooleanAria(element, 'required') || select.required;
|
|
1005
|
-
|
|
1006
|
-
const id = getId();
|
|
1007
|
-
const type = multiple ? 'select' : getAttributeValue(element, 'type', TYPES, 'select');
|
|
1008
|
-
|
|
1009
|
-
const autocomplete = getAutoComplete(element, type === 'select');
|
|
1010
|
-
|
|
1011
|
-
const labelId = label.id;
|
|
1012
|
-
|
|
1013
|
-
label.htmlFor = id;
|
|
1014
|
-
label.id = `${id}_label`;
|
|
1015
|
-
|
|
1016
|
-
generateIds(id, select);
|
|
1017
|
-
|
|
1018
|
-
const anchor = createAnchor({autocomplete, id, label, multiple, select, type});
|
|
1019
|
-
const content = createContent({autocomplete, id, label, multiple, select, type});
|
|
1020
|
-
|
|
1021
|
-
const values =
|
|
1022
|
-
content.list.selected.length === 0
|
|
1023
|
-
? ['']
|
|
1024
|
-
: content.list.selected.map(option => getData(option, 'value', false) ?? '');
|
|
1025
|
-
|
|
1026
|
-
const state: OuiComboboxState = {
|
|
1027
|
-
autocomplete,
|
|
1028
|
-
disabled,
|
|
1029
|
-
id,
|
|
1030
|
-
internals,
|
|
1031
|
-
multiple,
|
|
1032
|
-
readonly,
|
|
1033
|
-
required,
|
|
1034
|
-
select,
|
|
1035
|
-
type,
|
|
1036
|
-
anchor: {
|
|
1037
|
-
container: anchor.container,
|
|
1038
|
-
element: anchor.element,
|
|
1039
|
-
},
|
|
1040
|
-
active: content.list.selected[0],
|
|
1041
|
-
content: content.element,
|
|
1042
|
-
filter: {
|
|
1043
|
-
element: content.filter?.element ?? (anchor.element as HTMLInputElement),
|
|
1044
|
-
value: '',
|
|
1045
|
-
},
|
|
1046
|
-
floatable: undefined as never,
|
|
1047
|
-
groups: content.list.groups,
|
|
1048
|
-
host: element,
|
|
1049
|
-
label: {
|
|
1050
|
-
element: label,
|
|
1051
|
-
id: labelId,
|
|
1052
|
-
},
|
|
1053
|
-
listeners: [],
|
|
1054
|
-
localizations: {},
|
|
1055
|
-
message: content.message,
|
|
1056
|
-
name: element.name,
|
|
1057
|
-
options: {
|
|
1058
|
-
all: content.list.options,
|
|
1059
|
-
selected: content.list.selected,
|
|
1060
|
-
},
|
|
1061
|
-
selection: anchor.selection,
|
|
1062
|
-
values: {
|
|
1063
|
-
current: values.slice(),
|
|
1064
|
-
initial: values.slice(),
|
|
1065
|
-
},
|
|
1066
|
-
};
|
|
1067
|
-
|
|
1068
|
-
attachCombobox(state);
|
|
1069
|
-
|
|
1070
|
-
setDisabled(state, disabled);
|
|
1071
|
-
setLimit('max', state, getAttribute(element, 'max'));
|
|
1072
|
-
setLimit('min', state, getAttribute(element, 'min'));
|
|
1073
|
-
setReadonly(state, readonly);
|
|
1074
|
-
setRequired(state, required);
|
|
1075
|
-
|
|
1076
|
-
filterOptions(state);
|
|
1077
|
-
|
|
1078
|
-
if (content.list.selected.length > 0) {
|
|
1079
|
-
updatePreview('set', state, content.list.selected[0]);
|
|
1080
|
-
}
|
|
1081
|
-
|
|
1082
|
-
updateForm(state, state.values.current, false);
|
|
1083
|
-
updateMessage(state, getVisible(state).length);
|
|
1084
|
-
|
|
1085
|
-
return state;
|
|
1086
|
-
}
|
|
1087
|
-
|
|
1088
|
-
function detachCombobox(state: OuiComboboxState): void {
|
|
1089
|
-
const {content, floatable, host, label, select} = state;
|
|
1090
|
-
|
|
1091
|
-
const listeners = state.listeners.splice(0);
|
|
1092
|
-
const {length} = listeners;
|
|
1093
|
-
|
|
1094
|
-
for (let index = 0; index < length; index += 1) {
|
|
1095
|
-
listeners[index]();
|
|
1096
|
-
}
|
|
1097
|
-
|
|
1098
|
-
floatable.destroy();
|
|
1099
|
-
|
|
1100
|
-
const anchor = state.anchor.container ?? state.anchor.element;
|
|
1101
|
-
|
|
1102
|
-
anchor.insertAdjacentElement('beforebegin', select);
|
|
1103
|
-
|
|
1104
|
-
anchor.remove();
|
|
1105
|
-
content.remove();
|
|
1106
|
-
|
|
1107
|
-
label.element.htmlFor = select.id;
|
|
1108
|
-
label.element.id = state.label.id;
|
|
1109
|
-
|
|
1110
|
-
select.hidden = false;
|
|
1111
|
-
|
|
1112
|
-
states.map.delete(host);
|
|
1113
|
-
states.map.delete(content);
|
|
1114
|
-
states.set.delete(state);
|
|
1115
|
-
}
|
|
1116
|
-
|
|
1117
|
-
function filterOptions(state: OuiComboboxState): HTMLElement | undefined {
|
|
1118
|
-
function filter(options: HTMLElement[]): boolean {
|
|
1119
|
-
const {length} = options;
|
|
1120
|
-
|
|
1121
|
-
let count = 0;
|
|
1122
|
-
|
|
1123
|
-
for (let index = 0; index < length; index += 1) {
|
|
1124
|
-
const option = options[index];
|
|
1125
|
-
|
|
1126
|
-
option.hidden =
|
|
1127
|
-
!state.autocomplete.none && !option.textContent.toLocaleLowerCase().includes(value);
|
|
1128
|
-
|
|
1129
|
-
if (!option.hidden) {
|
|
1130
|
-
count += 1;
|
|
1131
|
-
|
|
1132
|
-
visible.push(option);
|
|
1133
|
-
}
|
|
1134
|
-
}
|
|
1135
|
-
|
|
1136
|
-
return count === 0;
|
|
1137
|
-
}
|
|
1138
|
-
|
|
1139
|
-
const value = state.filter.element.value.toLocaleLowerCase();
|
|
1140
|
-
|
|
1141
|
-
const visible: HTMLElement[] = [];
|
|
1142
|
-
|
|
1143
|
-
if (state.groups == null) {
|
|
1144
|
-
filter(state.options.all);
|
|
1145
|
-
} else {
|
|
1146
|
-
const groupsLength = state.groups.length;
|
|
1147
|
-
|
|
1148
|
-
for (let groupIndex = 0; groupIndex < groupsLength; groupIndex += 1) {
|
|
1149
|
-
const {element, options} = state.groups[groupIndex];
|
|
1150
|
-
|
|
1151
|
-
element.hidden = filter(options);
|
|
1152
|
-
}
|
|
1153
|
-
}
|
|
1154
|
-
|
|
1155
|
-
updateMessage(state, visible.length);
|
|
1156
|
-
|
|
1157
|
-
return state.active != null && visible.includes(state.active) ? state.active : visible[0];
|
|
1158
|
-
}
|
|
1159
|
-
|
|
1160
|
-
function filterOption(
|
|
1161
|
-
state: OuiComboboxState,
|
|
1162
|
-
value: unknown,
|
|
1163
|
-
textContent?: boolean,
|
|
1164
|
-
): HTMLElement | undefined {
|
|
1165
|
-
const filterTextContent = textContent ?? false;
|
|
1166
|
-
|
|
1167
|
-
const filter = getString(value).toLocaleLowerCase();
|
|
1168
|
-
|
|
1169
|
-
filterOptions(state);
|
|
1170
|
-
|
|
1171
|
-
const {length} = state.options.all;
|
|
1172
|
-
let match: HTMLElement | undefined;
|
|
1173
|
-
|
|
1174
|
-
for (let index = 0; index < length; index += 1) {
|
|
1175
|
-
const option = state.options.all[index];
|
|
1176
|
-
|
|
1177
|
-
const value = (
|
|
1178
|
-
filterTextContent ? option.textContent : (getData(option, 'value', false) ?? '')
|
|
1179
|
-
).toLocaleLowerCase();
|
|
1180
|
-
|
|
1181
|
-
if (value === filter) {
|
|
1182
|
-
match = option;
|
|
1183
|
-
|
|
1184
|
-
break;
|
|
1185
|
-
}
|
|
1186
|
-
}
|
|
1187
|
-
|
|
1188
|
-
return match;
|
|
1189
|
-
}
|
|
1190
|
-
|
|
1191
|
-
function generateIds(prefix: string, select: HTMLSelectElement): void {
|
|
1192
|
-
const elements = [...select.children].flatMap(child =>
|
|
1193
|
-
child instanceof HTMLOptGroupElement
|
|
1194
|
-
? [
|
|
1195
|
-
child,
|
|
1196
|
-
...[...child.children].filter(groupChild => groupChild instanceof HTMLOptionElement),
|
|
1197
|
-
]
|
|
1198
|
-
: child instanceof HTMLOptionElement
|
|
1199
|
-
? [child]
|
|
1200
|
-
: [],
|
|
1201
|
-
);
|
|
1202
|
-
|
|
1203
|
-
const {length} = elements;
|
|
1204
|
-
|
|
1205
|
-
for (let index = 0; index < length; index += 1) {
|
|
1206
|
-
const element = elements[index];
|
|
1207
|
-
|
|
1208
|
-
if (element.id === '') {
|
|
1209
|
-
element.id = `${prefix}${element instanceof HTMLOptGroupElement ? '_group_' : '_option_'}${index}`;
|
|
1210
|
-
}
|
|
1211
|
-
}
|
|
1212
|
-
}
|
|
1213
|
-
|
|
1214
|
-
function getAttributeValue<Value>(
|
|
1215
|
-
element: HTMLElement,
|
|
1216
|
-
attribute: string,
|
|
1217
|
-
possible: Set<Value>,
|
|
1218
|
-
defaultValue: Value,
|
|
1219
|
-
): Value {
|
|
1220
|
-
const value = getAttribute(element, attribute);
|
|
1221
|
-
|
|
1222
|
-
return possible.has(value as Value) ? (value as Value) : defaultValue;
|
|
1223
|
-
}
|
|
1224
|
-
|
|
1225
|
-
function getAutoComplete(element: OuiComboboxElement, select: boolean): OuiComboboxAutoComplete {
|
|
1226
|
-
let value: OuiComboboxAutoCompleteType;
|
|
1227
|
-
|
|
1228
|
-
if (select) {
|
|
1229
|
-
value = 'list';
|
|
1230
|
-
} else {
|
|
1231
|
-
value = getAttributeValue(element, 'autocomplete', AUTOCOMPLETES, 'none');
|
|
1232
|
-
}
|
|
1233
|
-
|
|
1234
|
-
return {
|
|
1235
|
-
value,
|
|
1236
|
-
both: value === 'both',
|
|
1237
|
-
inline: value === 'inline',
|
|
1238
|
-
list: value === 'list',
|
|
1239
|
-
none: value === 'none',
|
|
1240
|
-
};
|
|
1241
|
-
}
|
|
1242
|
-
|
|
1243
|
-
function getBooleanAria(element: HTMLElement, attribute: AnyAriaBooleanAttribute): boolean {
|
|
1244
|
-
const value = getAria(element, attribute);
|
|
1245
|
-
|
|
1246
|
-
return value === true || value === 'true';
|
|
1247
|
-
}
|
|
1248
|
-
|
|
1249
|
-
function getId(): string {
|
|
1250
|
-
index += 1;
|
|
1251
|
-
|
|
1252
|
-
return `oui_combobox_${index}`;
|
|
1253
|
-
}
|
|
1254
|
-
|
|
1255
|
-
function getLanguage(state: OuiComboboxState): string {
|
|
1256
|
-
return (
|
|
1257
|
-
state.host.closest('[lang]')?.getAttribute('lang') ?? document.documentElement.lang
|
|
1258
|
-
).toLowerCase();
|
|
1259
|
-
}
|
|
1260
|
-
|
|
1261
|
-
function getLocalization<Key extends keyof OuiComboboxLocalizations>(
|
|
1262
|
-
key: Key,
|
|
1263
|
-
state: OuiComboboxState,
|
|
1264
|
-
): OuiComboboxLocalizations[Key] {
|
|
1265
|
-
const language = getLanguage(state);
|
|
1266
|
-
|
|
1267
|
-
const allLocalizations = [state.localizations, LOCALIZATIONS.globals];
|
|
1268
|
-
const {length} = allLocalizations;
|
|
1269
|
-
|
|
1270
|
-
let localization: OuiComboboxLocalizations[Key] | undefined;
|
|
1271
|
-
let shortLanguage: string | undefined;
|
|
1272
|
-
|
|
1273
|
-
for (let index = 0; index < length; index += 1) {
|
|
1274
|
-
const localizations = allLocalizations[index];
|
|
1275
|
-
|
|
1276
|
-
localization = localizations?.[language]?.[key];
|
|
1277
|
-
|
|
1278
|
-
if (localization != null) {
|
|
1279
|
-
break;
|
|
1280
|
-
}
|
|
1281
|
-
|
|
1282
|
-
shortLanguage ??= language.split('-')[0];
|
|
1283
|
-
|
|
1284
|
-
if (shortLanguage === language) {
|
|
1285
|
-
continue;
|
|
1286
|
-
}
|
|
1287
|
-
|
|
1288
|
-
localization = localizations?.[shortLanguage]?.[key];
|
|
1289
|
-
|
|
1290
|
-
if (localization != null) {
|
|
1291
|
-
break;
|
|
1292
|
-
}
|
|
1293
|
-
}
|
|
1294
|
-
|
|
1295
|
-
return localization ?? LOCALIZATIONS.defaults[key];
|
|
1296
|
-
}
|
|
1297
|
-
|
|
1298
|
-
function getVisible(state: OuiComboboxState): HTMLElement[] {
|
|
1299
|
-
return state.options.all.filter(option => !option.hidden);
|
|
1300
|
-
}
|
|
1301
|
-
|
|
1302
|
-
function hasTextSelection(element: HTMLElement): element is HTMLInputElement {
|
|
1303
|
-
return element instanceof HTMLInputElement && element.selectionStart !== element.selectionEnd;
|
|
1304
|
-
}
|
|
1305
|
-
|
|
1306
|
-
function onClickContent(event: PointerEvent, content: HTMLElement): void {
|
|
1307
|
-
const state = states.map.get(content);
|
|
1308
|
-
|
|
1309
|
-
if (state == null) {
|
|
1310
|
-
return;
|
|
1311
|
-
}
|
|
1312
|
-
|
|
1313
|
-
if (state.disabled) {
|
|
1314
|
-
return;
|
|
1315
|
-
}
|
|
1316
|
-
|
|
1317
|
-
const option = findAncestor(event, SELECTOR_OPTION);
|
|
1318
|
-
|
|
1319
|
-
if (!(option instanceof HTMLElement)) {
|
|
1320
|
-
state.filter.element.focus();
|
|
1321
|
-
} else if (state.readonly) {
|
|
1322
|
-
updatePreview('set', state, option);
|
|
1323
|
-
} else {
|
|
1324
|
-
toggleOption(state, option);
|
|
1325
|
-
}
|
|
1326
|
-
}
|
|
1327
|
-
|
|
1328
|
-
function onClickDocument(event: PointerEvent): void {
|
|
1329
|
-
const combobox = findAncestor(event, TAGNAME);
|
|
1330
|
-
const state = states.map.get(combobox!);
|
|
1331
|
-
|
|
1332
|
-
if (combobox == null || state == null) {
|
|
1333
|
-
return;
|
|
1334
|
-
}
|
|
1335
|
-
|
|
1336
|
-
const label = findAncestor(event, 'label');
|
|
1337
|
-
|
|
1338
|
-
if (label != null) {
|
|
1339
|
-
if (label === state.label.element && combobox.type === 'select') {
|
|
1340
|
-
state.anchor.element.focus();
|
|
1341
|
-
|
|
1342
|
-
if (!state.disabled) {
|
|
1343
|
-
state.floatable.show();
|
|
1344
|
-
}
|
|
1345
|
-
}
|
|
1346
|
-
|
|
1347
|
-
return;
|
|
1348
|
-
}
|
|
1349
|
-
|
|
1350
|
-
const selection = findAncestor(event, `[${ATTRIBUTE_SELECTION_ITEM}]`);
|
|
1351
|
-
|
|
1352
|
-
if (selection != null) {
|
|
1353
|
-
const option = state.options.selected.find(
|
|
1354
|
-
option => option.id === getData(selection, 'option', false),
|
|
1355
|
-
);
|
|
1356
|
-
|
|
1357
|
-
if (state.disabled) {
|
|
1358
|
-
state.anchor.element.focus();
|
|
1359
|
-
} else if (state.readonly) {
|
|
1360
|
-
updateTabindices(state, option);
|
|
1361
|
-
} else {
|
|
1362
|
-
toggleOption(state, option);
|
|
1363
|
-
}
|
|
1364
|
-
|
|
1365
|
-
return;
|
|
1366
|
-
}
|
|
1367
|
-
|
|
1368
|
-
const anchor = findAncestor(event, SELECTOR_ANCHOR);
|
|
1369
|
-
|
|
1370
|
-
if (anchor != null && !state.disabled) {
|
|
1371
|
-
state.floatable.show();
|
|
1372
|
-
|
|
1373
|
-
return;
|
|
1374
|
-
}
|
|
1375
|
-
}
|
|
1376
|
-
|
|
1377
|
-
function onEnter(state: OuiComboboxState): void {
|
|
1378
|
-
if (state.disabled || state.readonly) {
|
|
1379
|
-
return;
|
|
1380
|
-
}
|
|
1381
|
-
|
|
1382
|
-
if (hasTextSelection(state.anchor.element)) {
|
|
1383
|
-
onSelection(state);
|
|
1384
|
-
} else {
|
|
1385
|
-
toggleOption(state, state.active);
|
|
1386
|
-
}
|
|
1387
|
-
}
|
|
1388
|
-
|
|
1389
|
-
function onEscape(state: OuiComboboxState): void {
|
|
1390
|
-
state.floatable.hide();
|
|
1391
|
-
}
|
|
1392
|
-
|
|
1393
|
-
function onFilter(
|
|
1394
|
-
type: InputEvent['inputType'],
|
|
1395
|
-
state: OuiComboboxState,
|
|
1396
|
-
manual?: boolean,
|
|
1397
|
-
emit?: boolean,
|
|
1398
|
-
): void {
|
|
1399
|
-
if (state.disabled || state.readonly) {
|
|
1400
|
-
return;
|
|
1401
|
-
}
|
|
1402
|
-
|
|
1403
|
-
const diff = state.filter.value !== state.filter.element.value;
|
|
1404
|
-
|
|
1405
|
-
if (!state.autocomplete.inline && state.filter.element.value.length > 0 && !(manual ?? false)) {
|
|
1406
|
-
state.floatable.show();
|
|
1407
|
-
}
|
|
1408
|
-
|
|
1409
|
-
const option = state.autocomplete.none
|
|
1410
|
-
? (state.active ?? state.options.all[0])
|
|
1411
|
-
: filterOptions(state);
|
|
1412
|
-
|
|
1413
|
-
let update: OuiComboboxPreview | undefined;
|
|
1414
|
-
|
|
1415
|
-
if (type.includes('delete')) {
|
|
1416
|
-
update = 'none';
|
|
1417
|
-
} else if (type.includes('insert')) {
|
|
1418
|
-
update = 'typed';
|
|
1419
|
-
}
|
|
1420
|
-
|
|
1421
|
-
if (update != null) {
|
|
1422
|
-
if (state.autocomplete.none) {
|
|
1423
|
-
applySelection({state, added: false, emit: (emit ?? true) && diff});
|
|
1424
|
-
} else {
|
|
1425
|
-
updatePreview(update, state, option);
|
|
1426
|
-
}
|
|
1427
|
-
}
|
|
1428
|
-
|
|
1429
|
-
if (diff) {
|
|
1430
|
-
state.filter.value = state.filter.element.value;
|
|
1431
|
-
|
|
1432
|
-
dispatch(state.host, 'input');
|
|
1433
|
-
}
|
|
1434
|
-
}
|
|
1435
|
-
|
|
1436
|
-
function onFocusout(state: OuiComboboxState): void {
|
|
1437
|
-
if (
|
|
1438
|
-
state.disabled ||
|
|
1439
|
-
state.readonly ||
|
|
1440
|
-
state.type === 'select' ||
|
|
1441
|
-
state.host === document.activeElement ||
|
|
1442
|
-
state.host.contains(document.activeElement)
|
|
1443
|
-
) {
|
|
1444
|
-
return;
|
|
1445
|
-
}
|
|
1446
|
-
|
|
1447
|
-
const option = filterOption(state, state.filter.element.value, true);
|
|
1448
|
-
|
|
1449
|
-
applySelection({
|
|
1450
|
-
option,
|
|
1451
|
-
state,
|
|
1452
|
-
added: false,
|
|
1453
|
-
selected: option == null ? [] : [option],
|
|
1454
|
-
});
|
|
1455
|
-
}
|
|
1456
|
-
|
|
1457
|
-
function onInput(event: InputEvent): void {
|
|
1458
|
-
const combobox = findAncestor(event, TAGNAME);
|
|
1459
|
-
const state = states.map.get(combobox!);
|
|
1460
|
-
|
|
1461
|
-
if (combobox != null && state != null && event.target === state.filter.element) {
|
|
1462
|
-
onFilter(event.inputType, state);
|
|
1463
|
-
}
|
|
1464
|
-
}
|
|
1465
|
-
|
|
1466
|
-
function onKeydown(event: KeyboardEvent): void {
|
|
1467
|
-
const combobox = findAncestor(event, TAGNAME);
|
|
1468
|
-
const state = states.map.get(combobox!);
|
|
1469
|
-
|
|
1470
|
-
if (combobox == null || state == null) {
|
|
1471
|
-
return;
|
|
1472
|
-
}
|
|
1473
|
-
|
|
1474
|
-
const item = findAncestor(event, SELECTOR_SELECTION_ITEM);
|
|
1475
|
-
|
|
1476
|
-
if (item instanceof HTMLElement) {
|
|
1477
|
-
onKeydownSelection(event, state, item);
|
|
1478
|
-
|
|
1479
|
-
return;
|
|
1480
|
-
}
|
|
1481
|
-
|
|
1482
|
-
onKeydownFilter(event, state);
|
|
1483
|
-
}
|
|
1484
|
-
|
|
1485
|
-
function onKeydownFilter(event: KeyboardEvent, state: OuiComboboxState): void {
|
|
1486
|
-
const ignore = state.disabled || state.readonly;
|
|
1487
|
-
const isSelect = state.type === 'select';
|
|
1488
|
-
|
|
1489
|
-
let preventDefault = ignore;
|
|
1490
|
-
|
|
1491
|
-
switch (true) {
|
|
1492
|
-
case event.key === ' ': {
|
|
1493
|
-
if (!state.disabled) {
|
|
1494
|
-
state.floatable.show();
|
|
1495
|
-
}
|
|
1496
|
-
|
|
1497
|
-
break;
|
|
1498
|
-
}
|
|
1499
|
-
|
|
1500
|
-
case event.key === 'Enter': {
|
|
1501
|
-
preventDefault = state.floatable.open;
|
|
1502
|
-
|
|
1503
|
-
if (preventDefault) {
|
|
1504
|
-
onEnter(state);
|
|
1505
|
-
} else if (!ignore && state.type === 'select') {
|
|
1506
|
-
state.floatable.show();
|
|
1507
|
-
} else if (!ignore) {
|
|
1508
|
-
state.internals.form?.requestSubmit();
|
|
1509
|
-
}
|
|
1510
|
-
|
|
1511
|
-
break;
|
|
1512
|
-
}
|
|
1513
|
-
|
|
1514
|
-
case event.key === 'Escape': {
|
|
1515
|
-
preventDefault = false;
|
|
1516
|
-
|
|
1517
|
-
onEscape(state);
|
|
1518
|
-
|
|
1519
|
-
break;
|
|
1520
|
-
}
|
|
1521
|
-
|
|
1522
|
-
case !isSelect && KEYS_SELECTION.has(event.key): {
|
|
1523
|
-
if (!ignore && hasTextSelection(state.anchor.element)) {
|
|
1524
|
-
onSelection(state);
|
|
1525
|
-
}
|
|
1526
|
-
|
|
1527
|
-
break;
|
|
1528
|
-
}
|
|
1529
|
-
|
|
1530
|
-
case KEYS_NAVIGATION.has(event.key): {
|
|
1531
|
-
if (!state.disabled) {
|
|
1532
|
-
onNavigation(event, state);
|
|
1533
|
-
}
|
|
1534
|
-
|
|
1535
|
-
break;
|
|
1536
|
-
}
|
|
1537
|
-
|
|
1538
|
-
default: {
|
|
1539
|
-
preventDefault = false;
|
|
1540
|
-
|
|
1541
|
-
if (event.key === 'Tab') {
|
|
1542
|
-
onTab(state);
|
|
1543
|
-
}
|
|
1544
|
-
|
|
1545
|
-
break;
|
|
1546
|
-
}
|
|
1547
|
-
}
|
|
1548
|
-
|
|
1549
|
-
if (preventDefault) {
|
|
1550
|
-
event.preventDefault();
|
|
1551
|
-
event.stopPropagation();
|
|
1552
|
-
}
|
|
1553
|
-
}
|
|
1554
|
-
|
|
1555
|
-
function onKeydownSelection(
|
|
1556
|
-
event: KeyboardEvent,
|
|
1557
|
-
state: OuiComboboxState,
|
|
1558
|
-
item: HTMLElement,
|
|
1559
|
-
): void {
|
|
1560
|
-
if (KEYS_ACTIVATION.has(event.key)) {
|
|
1561
|
-
event.preventDefault();
|
|
1562
|
-
|
|
1563
|
-
onRemoveSelection(state, item);
|
|
1564
|
-
|
|
1565
|
-
return;
|
|
1566
|
-
}
|
|
1567
|
-
|
|
1568
|
-
if (state.disabled || state.selection == null) {
|
|
1569
|
-
return;
|
|
1570
|
-
}
|
|
1571
|
-
|
|
1572
|
-
const {length} = state.selection.items;
|
|
1573
|
-
|
|
1574
|
-
const index = state.selection.items.findIndex(selection => selection.button === item);
|
|
1575
|
-
|
|
1576
|
-
let next: number | undefined;
|
|
1577
|
-
|
|
1578
|
-
switch (event.key) {
|
|
1579
|
-
case 'ArrowDown':
|
|
1580
|
-
state.anchor.element.focus();
|
|
1581
|
-
state.anchor.element.click();
|
|
1582
|
-
return;
|
|
1583
|
-
|
|
1584
|
-
case 'ArrowRight':
|
|
1585
|
-
case 'ArrowLeft':
|
|
1586
|
-
next = clamp(index + (event.key === 'ArrowRight' ? 1 : -1), 0, length - 1, true);
|
|
1587
|
-
break;
|
|
1588
|
-
|
|
1589
|
-
case 'End':
|
|
1590
|
-
next = length - 1;
|
|
1591
|
-
break;
|
|
1592
|
-
|
|
1593
|
-
case 'Home':
|
|
1594
|
-
next = 0;
|
|
1595
|
-
break;
|
|
1596
|
-
|
|
1597
|
-
default:
|
|
1598
|
-
break;
|
|
1599
|
-
}
|
|
1600
|
-
|
|
1601
|
-
if (next == null) {
|
|
1602
|
-
return;
|
|
1603
|
-
}
|
|
1604
|
-
|
|
1605
|
-
event.preventDefault();
|
|
1606
|
-
|
|
1607
|
-
for (let index = 0; index < length; index += 1) {
|
|
1608
|
-
const {button} = state.selection.items[index];
|
|
1609
|
-
const selected = index === next;
|
|
1610
|
-
|
|
1611
|
-
button.tabIndex = selected ? 0 : -1;
|
|
1612
|
-
|
|
1613
|
-
if (selected) {
|
|
1614
|
-
button.focus();
|
|
1615
|
-
}
|
|
1616
|
-
}
|
|
1617
|
-
}
|
|
1618
|
-
|
|
1619
|
-
function onNavigation(event: KeyboardEvent, state: OuiComboboxState): void {
|
|
1620
|
-
let skip = false;
|
|
1621
|
-
|
|
1622
|
-
if (!state.floatable.open) {
|
|
1623
|
-
skip = true;
|
|
1624
|
-
|
|
1625
|
-
state.floatable.show();
|
|
1626
|
-
}
|
|
1627
|
-
|
|
1628
|
-
const visible = getVisible(state);
|
|
1629
|
-
|
|
1630
|
-
if (visible.length === 0) {
|
|
1631
|
-
return;
|
|
1632
|
-
}
|
|
1633
|
-
|
|
1634
|
-
let next: HTMLElement | undefined;
|
|
1635
|
-
|
|
1636
|
-
if (skip || state.active == null) {
|
|
1637
|
-
next = state.active ?? visible.at(event.key === 'ArrowDown' ? 0 : -1);
|
|
1638
|
-
} else {
|
|
1639
|
-
const current = visible.findIndex(option => option.id === state.active?.id);
|
|
1640
|
-
|
|
1641
|
-
let index: number | undefined;
|
|
1642
|
-
|
|
1643
|
-
switch (event.key) {
|
|
1644
|
-
case 'ArrowDown':
|
|
1645
|
-
case 'ArrowUp':
|
|
1646
|
-
index = clamp(current + (event.key === 'ArrowDown' ? 1 : -1), 0, visible.length - 1, true);
|
|
1647
|
-
break;
|
|
1648
|
-
|
|
1649
|
-
case 'End':
|
|
1650
|
-
index = visible.length - 1;
|
|
1651
|
-
break;
|
|
1652
|
-
|
|
1653
|
-
case 'Home':
|
|
1654
|
-
index = 0;
|
|
1655
|
-
break;
|
|
1656
|
-
|
|
1657
|
-
case 'PageDown':
|
|
1658
|
-
case 'PageUp':
|
|
1659
|
-
index = clamp(current + (event.key === 'PageDown' ? 10 : -10), 0, visible.length - 1);
|
|
1660
|
-
break;
|
|
1661
|
-
|
|
1662
|
-
default:
|
|
1663
|
-
break;
|
|
1664
|
-
}
|
|
1665
|
-
|
|
1666
|
-
if (index != null) {
|
|
1667
|
-
next = visible[index];
|
|
1668
|
-
}
|
|
1669
|
-
}
|
|
1670
|
-
|
|
1671
|
-
let preview: OuiComboboxPreview;
|
|
1672
|
-
|
|
1673
|
-
if (state.type === 'autocomplete' && (state.autocomplete.both || state.autocomplete.inline)) {
|
|
1674
|
-
preview = 'set';
|
|
1675
|
-
} else {
|
|
1676
|
-
preview = 'none';
|
|
1677
|
-
}
|
|
1678
|
-
|
|
1679
|
-
updatePreview(preview, state, next ?? state.active ?? visible[0]);
|
|
1680
|
-
}
|
|
1681
|
-
|
|
1682
|
-
function onRemoveSelection(state: OuiComboboxState, item: HTMLElement): void {
|
|
1683
|
-
if (state.disabled || state.readonly) {
|
|
1684
|
-
return;
|
|
1685
|
-
}
|
|
1686
|
-
|
|
1687
|
-
const option = state.options.selected.find(
|
|
1688
|
-
option => option.id === getData(item, 'option', false),
|
|
1689
|
-
);
|
|
1690
|
-
|
|
1691
|
-
if (option != null) {
|
|
1692
|
-
toggleOption(state, option);
|
|
1693
|
-
}
|
|
1694
|
-
}
|
|
1695
|
-
|
|
1696
|
-
function onSelection(state: OuiComboboxState): void {
|
|
1697
|
-
state.filter.value =
|
|
1698
|
-
state.anchor.element === state.filter.element ? state.filter.element.value : '';
|
|
1699
|
-
|
|
1700
|
-
filterOptions(state);
|
|
1701
|
-
|
|
1702
|
-
toggleOption(state, state.active);
|
|
1703
|
-
}
|
|
1704
|
-
|
|
1705
|
-
function onTab(state: OuiComboboxState): void {
|
|
1706
|
-
setTimeout(() => {
|
|
1707
|
-
if (state.host === document.activeElement) {
|
|
1708
|
-
return;
|
|
1709
|
-
}
|
|
1710
|
-
|
|
1711
|
-
if (
|
|
1712
|
-
state.selection?.container.contains(document.activeElement) ||
|
|
1713
|
-
!state.host.contains(document.activeElement)
|
|
1714
|
-
) {
|
|
1715
|
-
state.floatable.hide();
|
|
1716
|
-
}
|
|
1717
|
-
});
|
|
1718
|
-
}
|
|
1719
|
-
|
|
1720
|
-
function setDisabled(state: OuiComboboxState, value: boolean): void {
|
|
1721
|
-
state.filter.element.readOnly = state.readonly || value;
|
|
1722
|
-
|
|
1723
|
-
setProperties('disabled', state, value);
|
|
1724
|
-
}
|
|
1725
|
-
|
|
1726
|
-
function setFilter(state: OuiComboboxState, value: unknown): void {
|
|
1727
|
-
if (typeof value !== 'string') {
|
|
1728
|
-
return;
|
|
1729
|
-
}
|
|
1730
|
-
|
|
1731
|
-
const type: InputEvent['inputType'] =
|
|
1732
|
-
value.length < state.filter.element.value.length ? 'deleteContentBackward' : 'insertText';
|
|
1733
|
-
|
|
1734
|
-
state.filter.element.value = value;
|
|
1735
|
-
|
|
1736
|
-
onFilter(type, state);
|
|
1737
|
-
}
|
|
1738
|
-
|
|
1739
|
-
function setLimit(type: 'max' | 'min', state: OuiComboboxState, value?: unknown): void {
|
|
1740
|
-
let actual = value;
|
|
1741
|
-
|
|
1742
|
-
if (typeof actual === 'string') {
|
|
1743
|
-
actual = Number.parseInt(actual, 10);
|
|
1744
|
-
}
|
|
1745
|
-
|
|
1746
|
-
const updateLimit =
|
|
1747
|
-
typeof actual === 'number' && !Number.isNaN(actual) && actual >= 0
|
|
1748
|
-
? actual !== state[type]
|
|
1749
|
-
: actual == null;
|
|
1750
|
-
|
|
1751
|
-
if (updateLimit) {
|
|
1752
|
-
state[type] = actual as number | undefined;
|
|
1753
|
-
|
|
1754
|
-
setAttribute(state.host, type, actual);
|
|
1755
|
-
|
|
1756
|
-
updateValidity(state);
|
|
1757
|
-
}
|
|
1758
|
-
}
|
|
1759
|
-
|
|
1760
|
-
/**
|
|
1761
|
-
* Set combobox localizations for a specific language
|
|
1762
|
-
*
|
|
1763
|
-
* @param language Language code
|
|
1764
|
-
* @param localizations Localizations for language
|
|
1765
|
-
*/
|
|
1766
|
-
export function setLocalizations(
|
|
1767
|
-
language: string,
|
|
1768
|
-
localizations: Partial<OuiComboboxLocalizations>,
|
|
1769
|
-
): void;
|
|
1770
|
-
|
|
1771
|
-
/**
|
|
1772
|
-
* Set combobox localizations for one or more languages
|
|
1773
|
-
*
|
|
1774
|
-
* @param localizations Localizations for one or more languages
|
|
1775
|
-
*/
|
|
1776
|
-
export function setLocalizations(
|
|
1777
|
-
localizations: Record<string, Partial<OuiComboboxLocalizations>>,
|
|
1778
|
-
): void;
|
|
1779
|
-
|
|
1780
|
-
export function setLocalizations(
|
|
1781
|
-
first: string | Record<string, Partial<OuiComboboxLocalizations>>,
|
|
1782
|
-
second?: Partial<OuiComboboxLocalizations>,
|
|
1783
|
-
): void {
|
|
1784
|
-
updateLocalizations(
|
|
1785
|
-
typeof first === 'string'
|
|
1786
|
-
? {
|
|
1787
|
-
[first]: second!,
|
|
1788
|
-
}
|
|
1789
|
-
: first,
|
|
1790
|
-
);
|
|
1791
|
-
}
|
|
1792
|
-
|
|
1793
|
-
function setProperties(
|
|
1794
|
-
key: 'disabled' | 'readonly' | 'required',
|
|
1795
|
-
state: OuiComboboxState,
|
|
1796
|
-
value: boolean,
|
|
1797
|
-
): void {
|
|
1798
|
-
state[key] = value;
|
|
1799
|
-
|
|
1800
|
-
const elements = [state.host, state.anchor.element, state.filter.element];
|
|
1801
|
-
const {length} = elements;
|
|
1802
|
-
|
|
1803
|
-
for (let index = 0; index < length; index += 1) {
|
|
1804
|
-
const element = elements[index];
|
|
1805
|
-
|
|
1806
|
-
setAria(
|
|
1807
|
-
element,
|
|
1808
|
-
key,
|
|
1809
|
-
key !== 'readonly' || !(element instanceof HTMLButtonElement) ? value : undefined,
|
|
1810
|
-
);
|
|
1811
|
-
}
|
|
1812
|
-
}
|
|
1813
|
-
|
|
1814
|
-
function setReadonly(state: OuiComboboxState, value: boolean): void {
|
|
1815
|
-
state.filter.element.readOnly = state.disabled || value;
|
|
1816
|
-
|
|
1817
|
-
setProperties('readonly', state, value);
|
|
1818
|
-
}
|
|
1819
|
-
|
|
1820
|
-
function setRequired(state: OuiComboboxState, value: boolean): void {
|
|
1821
|
-
setProperties('required', state, value);
|
|
1822
|
-
updateValidity(state);
|
|
1823
|
-
}
|
|
1824
|
-
|
|
1825
|
-
function setValue(state: OuiComboboxState, value: unknown, emit?: boolean): void {
|
|
1826
|
-
if (state.autocomplete.none) {
|
|
1827
|
-
state.filter.element.value = getString(value);
|
|
1828
|
-
|
|
1829
|
-
onFilter('insertText', state, true, emit);
|
|
1830
|
-
|
|
1831
|
-
return;
|
|
1832
|
-
}
|
|
1833
|
-
|
|
1834
|
-
const option = filterOption(state, value);
|
|
1835
|
-
|
|
1836
|
-
if (option != null) {
|
|
1837
|
-
setValues(state, [value], emit);
|
|
1838
|
-
}
|
|
1839
|
-
}
|
|
1840
|
-
|
|
1841
|
-
function setValues(state: OuiComboboxState, value: unknown, emit?: boolean): void {
|
|
1842
|
-
if (!Array.isArray(value)) {
|
|
1843
|
-
return;
|
|
1844
|
-
}
|
|
1845
|
-
|
|
1846
|
-
const values = (state.multiple ? value : value.slice(0, 1)).map(value =>
|
|
1847
|
-
getString(value).toLocaleLowerCase(),
|
|
1848
|
-
);
|
|
1849
|
-
|
|
1850
|
-
if (values.length === 0) {
|
|
1851
|
-
values.push('');
|
|
1852
|
-
}
|
|
1853
|
-
|
|
1854
|
-
if (state.autocomplete.none) {
|
|
1855
|
-
setValue(state, values[0], emit);
|
|
1856
|
-
|
|
1857
|
-
return;
|
|
1858
|
-
}
|
|
1859
|
-
|
|
1860
|
-
const {length} = state.options.all;
|
|
1861
|
-
|
|
1862
|
-
const selected: HTMLElement[] = [];
|
|
1863
|
-
|
|
1864
|
-
for (let index = 0; index < length; index += 1) {
|
|
1865
|
-
const option = state.options.all[index];
|
|
1866
|
-
|
|
1867
|
-
if (values.includes(getData(option, 'value', false)?.toLocaleLowerCase() ?? '')) {
|
|
1868
|
-
selected.push(option);
|
|
1869
|
-
}
|
|
1870
|
-
}
|
|
1871
|
-
|
|
1872
|
-
applySelection({
|
|
1873
|
-
emit,
|
|
1874
|
-
selected,
|
|
1875
|
-
state,
|
|
1876
|
-
added: true,
|
|
1877
|
-
option: selected[0] ?? state.options.all[0],
|
|
1878
|
-
values: state.autocomplete.none ? values : undefined,
|
|
1879
|
-
});
|
|
1880
|
-
}
|
|
1881
|
-
|
|
1882
|
-
function toggleOption(state: OuiComboboxState, option?: HTMLElement): void {
|
|
1883
|
-
if (option == null || (!state.multiple && state.options.selected.includes(option))) {
|
|
1884
|
-
state.floatable.hide();
|
|
1885
|
-
|
|
1886
|
-
return;
|
|
1887
|
-
}
|
|
1888
|
-
|
|
1889
|
-
const selected = state.options.selected.slice();
|
|
1890
|
-
|
|
1891
|
-
let added = true;
|
|
1892
|
-
|
|
1893
|
-
if (state.multiple) {
|
|
1894
|
-
const index = selected.indexOf(option);
|
|
1895
|
-
|
|
1896
|
-
if (index === -1) {
|
|
1897
|
-
selected.push(option);
|
|
1898
|
-
} else {
|
|
1899
|
-
added = false;
|
|
1900
|
-
|
|
1901
|
-
selected.splice(index, 1);
|
|
1902
|
-
}
|
|
1903
|
-
} else {
|
|
1904
|
-
selected.splice(0, selected.length, option);
|
|
1905
|
-
}
|
|
1906
|
-
|
|
1907
|
-
applySelection({
|
|
1908
|
-
added,
|
|
1909
|
-
option,
|
|
1910
|
-
selected,
|
|
1911
|
-
state,
|
|
1912
|
-
values: state.autocomplete.none && option != null ? [option.textContent] : undefined,
|
|
1913
|
-
});
|
|
1914
|
-
|
|
1915
|
-
if (!state.multiple) {
|
|
1916
|
-
state.floatable.hide();
|
|
1917
|
-
}
|
|
1918
|
-
}
|
|
1919
|
-
|
|
1920
|
-
function updateAnchor(state: OuiComboboxState): void {
|
|
1921
|
-
if (!state.multiple) {
|
|
1922
|
-
return;
|
|
1923
|
-
}
|
|
1924
|
-
|
|
1925
|
-
const localization = getLocalization('selected', state);
|
|
1926
|
-
|
|
1927
|
-
setAria(
|
|
1928
|
-
state.anchor.element,
|
|
1929
|
-
'label',
|
|
1930
|
-
typeof localization === 'function'
|
|
1931
|
-
? localization(state.options.selected.length)
|
|
1932
|
-
: localization.replace('{amount}', state.options.selected.length.toString()),
|
|
1933
|
-
);
|
|
1934
|
-
}
|
|
1935
|
-
|
|
1936
|
-
function updateAutocomplete(
|
|
1937
|
-
preview: OuiComboboxPreview,
|
|
1938
|
-
state: OuiComboboxState,
|
|
1939
|
-
active?: HTMLElement,
|
|
1940
|
-
): void {
|
|
1941
|
-
if (state.multiple) {
|
|
1942
|
-
return;
|
|
1943
|
-
}
|
|
1944
|
-
|
|
1945
|
-
const content = active?.textContent ?? state.filter.element.value;
|
|
1946
|
-
|
|
1947
|
-
if (
|
|
1948
|
-
preview === 'typed' &&
|
|
1949
|
-
!content.toLocaleLowerCase().startsWith(state.filter.element.value.toLocaleLowerCase())
|
|
1950
|
-
) {
|
|
1951
|
-
return;
|
|
1952
|
-
}
|
|
1953
|
-
|
|
1954
|
-
if (state.anchor.element instanceof HTMLInputElement) {
|
|
1955
|
-
const contentLength = content.length;
|
|
1956
|
-
const filterLength = state.filter.element.value.length;
|
|
1957
|
-
|
|
1958
|
-
if (preview === 'set' || preview === 'typed') {
|
|
1959
|
-
state.anchor.element.value = content;
|
|
1960
|
-
|
|
1961
|
-
state.anchor.element.setSelectionRange(
|
|
1962
|
-
preview === 'typed' ? filterLength : contentLength,
|
|
1963
|
-
contentLength,
|
|
1964
|
-
);
|
|
1965
|
-
}
|
|
1966
|
-
} else {
|
|
1967
|
-
state.anchor.element.textContent = content;
|
|
1968
|
-
}
|
|
1969
|
-
}
|
|
1970
|
-
|
|
1971
|
-
function updateForm(state: OuiComboboxState, values: string[], emit?: boolean): void {
|
|
1972
|
-
let value: string | FormData;
|
|
1973
|
-
|
|
1974
|
-
if (state.multiple) {
|
|
1975
|
-
const {length} = values;
|
|
1976
|
-
|
|
1977
|
-
value = new FormData();
|
|
1978
|
-
|
|
1979
|
-
for (let index = 0; index < length; index += 1) {
|
|
1980
|
-
(value as FormData).append(state.name, values[index]);
|
|
1981
|
-
}
|
|
1982
|
-
} else {
|
|
1983
|
-
value = values[0];
|
|
1984
|
-
}
|
|
1985
|
-
|
|
1986
|
-
state.values.current = values;
|
|
1987
|
-
|
|
1988
|
-
state.internals.setFormValue(value, JSON.stringify(values));
|
|
1989
|
-
|
|
1990
|
-
updateAnchor(state);
|
|
1991
|
-
updateValidity(state);
|
|
1992
|
-
|
|
1993
|
-
if (emit ?? true) {
|
|
1994
|
-
dispatch(state.host, 'change');
|
|
1995
|
-
}
|
|
1996
|
-
}
|
|
1997
|
-
|
|
1998
|
-
function updateLocalizations(
|
|
1999
|
-
values: Record<string, Partial<Record<string, string | GenericCallback>>>,
|
|
2000
|
-
destination?: Record<string, Partial<OuiComboboxLocalizations>>,
|
|
2001
|
-
): void {
|
|
2002
|
-
if (typeof values !== 'object' || values == null) {
|
|
2003
|
-
return;
|
|
2004
|
-
}
|
|
2005
|
-
|
|
2006
|
-
const localizations = destination ?? LOCALIZATIONS.globals;
|
|
2007
|
-
|
|
2008
|
-
const languages = Object.keys(values);
|
|
2009
|
-
const languagesLength = languages.length;
|
|
2010
|
-
|
|
2011
|
-
let updated = false;
|
|
2012
|
-
|
|
2013
|
-
for (let languagesIndex = 0; languagesIndex < languagesLength; languagesIndex += 1) {
|
|
2014
|
-
const language = languages[languagesIndex];
|
|
2015
|
-
const languageValue = values[language];
|
|
2016
|
-
|
|
2017
|
-
if (typeof languageValue !== 'object' || languageValue == null) {
|
|
2018
|
-
continue;
|
|
2019
|
-
}
|
|
2020
|
-
|
|
2021
|
-
const normalizedLanguage = language.toLowerCase();
|
|
2022
|
-
|
|
2023
|
-
localizations[normalizedLanguage] ??= {};
|
|
2024
|
-
|
|
2025
|
-
const keys = Object.keys(languageValue);
|
|
2026
|
-
const keysLength = keys.length;
|
|
2027
|
-
|
|
2028
|
-
for (let keysIndex = 0; keysIndex < keysLength; keysIndex += 1) {
|
|
2029
|
-
const key = keys[keysIndex];
|
|
2030
|
-
const keyValue = languageValue[key];
|
|
2031
|
-
|
|
2032
|
-
const normalizedKey = key.toLowerCase() as keyof OuiComboboxLocalizations;
|
|
2033
|
-
|
|
2034
|
-
if (
|
|
2035
|
-
LOCALIZATIONS.keys.has(normalizedKey) &&
|
|
2036
|
-
(typeof keyValue === 'string' || typeof keyValue === 'function')
|
|
2037
|
-
) {
|
|
2038
|
-
updated = true;
|
|
2039
|
-
|
|
2040
|
-
localizations[normalizedLanguage][normalizedKey] = keyValue as never;
|
|
2041
|
-
}
|
|
2042
|
-
}
|
|
2043
|
-
}
|
|
2044
|
-
|
|
2045
|
-
if (!updated) {
|
|
2046
|
-
return;
|
|
2047
|
-
}
|
|
2048
|
-
|
|
2049
|
-
for (const state of states.set) {
|
|
2050
|
-
updateAnchor(state);
|
|
2051
|
-
updateMessage(state, getVisible(state).length);
|
|
2052
|
-
updateSelectionItems(state);
|
|
2053
|
-
}
|
|
2054
|
-
}
|
|
2055
|
-
|
|
2056
|
-
function updateMessage(state: OuiComboboxState, visible: number): void {
|
|
2057
|
-
setAttribute(state.message, ATTRIBUTE_HIDDEN, visible > 0 ? '' : undefined);
|
|
2058
|
-
|
|
2059
|
-
if (visible === 0) {
|
|
2060
|
-
const localization = getLocalization('empty', state);
|
|
2061
|
-
|
|
2062
|
-
state.message.textContent = typeof localization === 'function' ? localization() : localization;
|
|
2063
|
-
} else {
|
|
2064
|
-
const localization = getLocalization('results', state);
|
|
2065
|
-
|
|
2066
|
-
state.message.textContent =
|
|
2067
|
-
typeof localization === 'function'
|
|
2068
|
-
? localization(visible)
|
|
2069
|
-
: localization.replace('{amount}', visible.toString());
|
|
2070
|
-
}
|
|
2071
|
-
|
|
2072
|
-
state.message.hidden = false;
|
|
2073
|
-
}
|
|
2074
|
-
|
|
2075
|
-
function updatePreview(
|
|
2076
|
-
preview: OuiComboboxPreview,
|
|
2077
|
-
state: OuiComboboxState,
|
|
2078
|
-
active?: HTMLElement,
|
|
2079
|
-
): void {
|
|
2080
|
-
const {length} = state.options.all;
|
|
2081
|
-
|
|
2082
|
-
for (let index = 0; index < length; index += 1) {
|
|
2083
|
-
const option = state.options.all[index];
|
|
2084
|
-
|
|
2085
|
-
setAttribute(option, ATTRIBUTE_OPTION_ACTIVE, option === active ? '' : undefined);
|
|
2086
|
-
}
|
|
2087
|
-
|
|
2088
|
-
state.active = active;
|
|
2089
|
-
|
|
2090
|
-
setAria(state.filter.element, 'activedescendant', state.floatable.open ? active?.id : undefined);
|
|
2091
|
-
|
|
2092
|
-
if (state.floatable.open) {
|
|
2093
|
-
active?.scrollIntoView(scrollOptions);
|
|
2094
|
-
}
|
|
2095
|
-
|
|
2096
|
-
if (preview !== 'none') {
|
|
2097
|
-
updateAutocomplete(preview, state, active);
|
|
2098
|
-
}
|
|
2099
|
-
}
|
|
2100
|
-
|
|
2101
|
-
function updateSelection(
|
|
2102
|
-
state: OuiComboboxState,
|
|
2103
|
-
active: HTMLElement | undefined,
|
|
2104
|
-
added: boolean,
|
|
2105
|
-
selected?: HTMLElement[],
|
|
2106
|
-
): void {
|
|
2107
|
-
if (state.selection == null) {
|
|
2108
|
-
return;
|
|
2109
|
-
}
|
|
2110
|
-
|
|
2111
|
-
let options = active == null ? [] : [active];
|
|
2112
|
-
|
|
2113
|
-
if (selected != null) {
|
|
2114
|
-
options = selected;
|
|
2115
|
-
|
|
2116
|
-
const {length} = state.selection.items;
|
|
2117
|
-
|
|
2118
|
-
for (let index = 0; index < length; index += 1) {
|
|
2119
|
-
state.selection.items[index].element.remove();
|
|
2120
|
-
}
|
|
2121
|
-
|
|
2122
|
-
state.selection.items.splice(0);
|
|
2123
|
-
}
|
|
2124
|
-
|
|
2125
|
-
if (added) {
|
|
2126
|
-
createSelectionItems({
|
|
2127
|
-
options,
|
|
2128
|
-
state,
|
|
2129
|
-
container: state.selection.container,
|
|
2130
|
-
items: state.selection.items,
|
|
2131
|
-
});
|
|
2132
|
-
|
|
2133
|
-
return;
|
|
2134
|
-
}
|
|
2135
|
-
|
|
2136
|
-
let current = state.selection.items.findIndex(
|
|
2137
|
-
item => getData(item.button, 'option', false) === active?.id,
|
|
2138
|
-
);
|
|
2139
|
-
|
|
2140
|
-
if (current === -1) {
|
|
2141
|
-
return;
|
|
2142
|
-
}
|
|
2143
|
-
|
|
2144
|
-
const item = state.selection.items[current];
|
|
2145
|
-
|
|
2146
|
-
item.element.remove();
|
|
2147
|
-
|
|
2148
|
-
state.selection.items.splice(current, 1);
|
|
2149
|
-
|
|
2150
|
-
if (state.selection.items.length === 0) {
|
|
2151
|
-
state.anchor.element.focus();
|
|
2152
|
-
|
|
2153
|
-
return;
|
|
2154
|
-
}
|
|
2155
|
-
|
|
2156
|
-
const {length} = state.selection.items;
|
|
2157
|
-
|
|
2158
|
-
if (current < 0) {
|
|
2159
|
-
current = 0;
|
|
2160
|
-
} else if (current >= length) {
|
|
2161
|
-
current = length - 1;
|
|
2162
|
-
}
|
|
2163
|
-
|
|
2164
|
-
for (let index = 0; index < length; index += 1) {
|
|
2165
|
-
const {button} = state.selection.items[index];
|
|
2166
|
-
|
|
2167
|
-
button.tabIndex = index === current ? 0 : -1;
|
|
2168
|
-
}
|
|
2169
|
-
|
|
2170
|
-
state.selection.items[current].button.focus();
|
|
2171
|
-
}
|
|
2172
|
-
|
|
2173
|
-
function updateSelectionItems(state: OuiComboboxState): void {
|
|
2174
|
-
if (state.selection == null) {
|
|
2175
|
-
return;
|
|
2176
|
-
}
|
|
2177
|
-
|
|
2178
|
-
const localization = getLocalization('remove', state);
|
|
2179
|
-
|
|
2180
|
-
const {length} = state.selection.items;
|
|
2181
|
-
|
|
2182
|
-
for (let index = 0; index < length; index += 1) {
|
|
2183
|
-
const item = state.selection.items[index];
|
|
2184
|
-
|
|
2185
|
-
const option = state.options.selected.find(
|
|
2186
|
-
option => option.id === getData(item.button, 'option', false),
|
|
2187
|
-
);
|
|
2188
|
-
|
|
2189
|
-
if (option == null) {
|
|
2190
|
-
continue;
|
|
2191
|
-
}
|
|
2192
|
-
|
|
2193
|
-
setAria(
|
|
2194
|
-
item.button,
|
|
2195
|
-
'label',
|
|
2196
|
-
typeof localization === 'function'
|
|
2197
|
-
? localization(option?.textContent ?? '', getData(option, 'value', false) ?? '')
|
|
2198
|
-
: localization.replace('{label}', option?.textContent ?? ''),
|
|
2199
|
-
);
|
|
2200
|
-
}
|
|
2201
|
-
}
|
|
2202
|
-
|
|
2203
|
-
function updateTabindices(state: OuiComboboxState, active?: HTMLElement): void {
|
|
2204
|
-
if (active == null || state.selection == null) {
|
|
2205
|
-
return;
|
|
2206
|
-
}
|
|
2207
|
-
|
|
2208
|
-
const next = state.selection.items.find(
|
|
2209
|
-
item => getData(item.button, 'option', false) === active.id,
|
|
2210
|
-
);
|
|
2211
|
-
|
|
2212
|
-
if (next == null) {
|
|
2213
|
-
return;
|
|
2214
|
-
}
|
|
2215
|
-
|
|
2216
|
-
const {length} = state.selection.items;
|
|
2217
|
-
|
|
2218
|
-
for (let index = 0; index < length; index += 1) {
|
|
2219
|
-
const item = state.selection.items[index];
|
|
2220
|
-
|
|
2221
|
-
item.button.tabIndex = item === next ? 0 : -1;
|
|
2222
|
-
}
|
|
2223
|
-
|
|
2224
|
-
next.button.focus();
|
|
2225
|
-
}
|
|
2226
|
-
|
|
2227
|
-
function updateValidity(state: OuiComboboxState): void {
|
|
2228
|
-
if (state.autocomplete.none) {
|
|
2229
|
-
const valueMissing = state.required && state.filter.element.value.length === 0;
|
|
2230
|
-
|
|
2231
|
-
const localization = getLocalization('input', state);
|
|
2232
|
-
|
|
2233
|
-
state.internals.setValidity(
|
|
2234
|
-
{valueMissing},
|
|
2235
|
-
typeof localization === 'function' ? localization() : localization,
|
|
2236
|
-
);
|
|
2237
|
-
|
|
2238
|
-
state.internals.checkValidity();
|
|
2239
|
-
|
|
2240
|
-
return;
|
|
2241
|
-
}
|
|
2242
|
-
|
|
2243
|
-
let error: 'badInput' | 'rangeOverflow' | 'rangeUnderflow' | undefined;
|
|
2244
|
-
let message: string | undefined;
|
|
2245
|
-
|
|
2246
|
-
switch (true) {
|
|
2247
|
-
case state.required && state.options.selected.length === 0: {
|
|
2248
|
-
error = 'badInput';
|
|
2249
|
-
|
|
2250
|
-
const localization = getLocalization('input', state);
|
|
2251
|
-
|
|
2252
|
-
message = typeof localization === 'function' ? localization() : localization;
|
|
2253
|
-
|
|
2254
|
-
break;
|
|
2255
|
-
}
|
|
2256
|
-
|
|
2257
|
-
case state.multiple &&
|
|
2258
|
-
typeof state.max === 'number' &&
|
|
2259
|
-
state.options.selected.length > state.max: {
|
|
2260
|
-
error = 'rangeOverflow';
|
|
2261
|
-
|
|
2262
|
-
const localization = getLocalization('max', state);
|
|
2263
|
-
|
|
2264
|
-
message =
|
|
2265
|
-
typeof localization === 'function'
|
|
2266
|
-
? localization(state.max)
|
|
2267
|
-
: localization.replace('{max}', state.max.toString());
|
|
2268
|
-
|
|
2269
|
-
break;
|
|
2270
|
-
}
|
|
2271
|
-
|
|
2272
|
-
case state.multiple &&
|
|
2273
|
-
typeof state.min === 'number' &&
|
|
2274
|
-
state.options.selected.length < state.min: {
|
|
2275
|
-
error = 'rangeUnderflow';
|
|
2276
|
-
|
|
2277
|
-
const localization = getLocalization('min', state);
|
|
2278
|
-
|
|
2279
|
-
message =
|
|
2280
|
-
typeof localization === 'function'
|
|
2281
|
-
? localization(state.min)
|
|
2282
|
-
: localization.replace('{min}', state.min.toString());
|
|
2283
|
-
|
|
2284
|
-
break;
|
|
2285
|
-
}
|
|
2286
|
-
|
|
2287
|
-
default:
|
|
2288
|
-
break;
|
|
2289
|
-
}
|
|
2290
|
-
|
|
2291
|
-
state.internals.setValidity(
|
|
2292
|
-
error == null
|
|
2293
|
-
? {}
|
|
2294
|
-
: {
|
|
2295
|
-
[error]: true,
|
|
2296
|
-
},
|
|
2297
|
-
message,
|
|
2298
|
-
);
|
|
2299
|
-
|
|
2300
|
-
state.internals.checkValidity();
|
|
2301
|
-
}
|
|
2302
|
-
|
|
2303
|
-
// #endregion
|
|
2304
|
-
|
|
2305
|
-
// #region Variables
|
|
2306
|
-
|
|
2307
|
-
const TAGNAME = 'oui-combobox';
|
|
2308
|
-
|
|
2309
|
-
const ATTRIBUTE_ANCHOR = `${TAGNAME}-anchor`;
|
|
2310
|
-
|
|
2311
|
-
const ATTRIBUTE_CONTENT = `${TAGNAME}-content`;
|
|
2312
|
-
|
|
2313
|
-
const ATTRIBUTE_FILTER = `${TAGNAME}-filter`;
|
|
2314
|
-
|
|
2315
|
-
const ATTRIBUTE_FILTER_INPUT = `${ATTRIBUTE_FILTER}-input`;
|
|
2316
|
-
|
|
2317
|
-
const ATTRIBUTE_HEADER = `${TAGNAME}-header`;
|
|
2318
|
-
|
|
2319
|
-
const ATTRIBUTE_HIDDEN = `${TAGNAME}-hidden`;
|
|
2320
|
-
|
|
2321
|
-
const ATTRIBUTE_LIST = `${TAGNAME}-list`;
|
|
2322
|
-
|
|
2323
|
-
const ATTRIBUTE_MESSAGE = `${TAGNAME}-message`;
|
|
2324
|
-
|
|
2325
|
-
const ATTRIBUTE_OPTION = `${TAGNAME}-option`;
|
|
2326
|
-
|
|
2327
|
-
const ATTRIBUTE_OPTION_ACTIVE = `${TAGNAME}-option-active`;
|
|
2328
|
-
|
|
2329
|
-
const ATTRIBUTE_SELECTION = `${TAGNAME}-selection`;
|
|
2330
|
-
|
|
2331
|
-
const ATTRIBUTE_SELECTION_ITEM = `${TAGNAME}-selection-item`;
|
|
2332
|
-
|
|
2333
|
-
const AUTOCOMPLETES = new Set<OuiComboboxAutoCompleteType>(['both', 'inline', 'list', 'none']);
|
|
2334
|
-
|
|
2335
|
-
const KEYS_ACTIVATION = new Set([' ', 'Backspace', 'Delete', 'Enter']);
|
|
2336
|
-
|
|
2337
|
-
const KEYS_NAVIGATION = new Set(['ArrowDown', 'ArrowUp', 'PageDown', 'PageUp']);
|
|
2338
|
-
|
|
2339
|
-
const KEYS_SELECTION = new Set(['ArrowRight', 'End']);
|
|
2340
|
-
|
|
2341
|
-
const LOCALIZATIONS: {
|
|
2342
|
-
defaults: OuiComboboxLocalizations;
|
|
2343
|
-
globals: Record<string, Partial<OuiComboboxLocalizations>>;
|
|
2344
|
-
keys: Set<keyof OuiComboboxLocalizations>;
|
|
2345
|
-
} = {
|
|
2346
|
-
defaults: {
|
|
2347
|
-
empty: 'No results found',
|
|
2348
|
-
input: 'Please enter a value',
|
|
2349
|
-
invalid: 'Please select an option from the list',
|
|
2350
|
-
max: max => `Please select fewer options (maximum is ${max})`,
|
|
2351
|
-
min: min => `Please select more options (minimum is ${min})`,
|
|
2352
|
-
remove: label => `Remove ${label}`,
|
|
2353
|
-
results: amount => `${amount} results available`,
|
|
2354
|
-
selected: amount => `${amount} selected`,
|
|
2355
|
-
},
|
|
2356
|
-
globals: {},
|
|
2357
|
-
keys: new Set<keyof OuiComboboxLocalizations>([
|
|
2358
|
-
'empty',
|
|
2359
|
-
'input',
|
|
2360
|
-
'invalid',
|
|
2361
|
-
'max',
|
|
2362
|
-
'min',
|
|
2363
|
-
'remove',
|
|
2364
|
-
'results',
|
|
2365
|
-
'selected',
|
|
2366
|
-
]),
|
|
2367
|
-
};
|
|
2368
|
-
|
|
2369
|
-
const MESSAGE_LABEL = `<${TAGNAME}> must contain a <label> element for the <select> element`;
|
|
2370
|
-
|
|
2371
|
-
const MESSAGE_SELECT = `<${TAGNAME}> must contain a valid <select> element`;
|
|
2372
|
-
|
|
2373
|
-
const SELECTOR_ANCHOR = `[${ATTRIBUTE_ANCHOR}]`;
|
|
2374
|
-
|
|
2375
|
-
const SELECTOR_SELECTION_ITEM = `[${ATTRIBUTE_SELECTION_ITEM}]`;
|
|
2376
|
-
|
|
2377
|
-
const SELECTOR_OPTION = `[${ATTRIBUTE_OPTION}]`;
|
|
2378
|
-
|
|
2379
|
-
const TYPES = new Set<OuiComboboxType>(['autocomplete', 'select']);
|
|
2380
|
-
|
|
2381
|
-
const scrollOptions: ScrollIntoViewOptions = {
|
|
2382
|
-
behavior: 'auto',
|
|
2383
|
-
block: 'nearest',
|
|
2384
|
-
inline: 'nearest',
|
|
2385
|
-
};
|
|
2386
|
-
|
|
2387
|
-
const states = {
|
|
2388
|
-
map: new WeakMap<HTMLElement, OuiComboboxState>(),
|
|
2389
|
-
set: new Set<OuiComboboxState>(),
|
|
2390
|
-
};
|
|
2391
|
-
|
|
2392
|
-
let index = 0;
|
|
2393
|
-
|
|
2394
|
-
// #endregion
|
|
2395
|
-
|
|
2396
|
-
// #region Initialization
|
|
2397
|
-
|
|
2398
|
-
on(document, 'click', onClickDocument);
|
|
2399
|
-
|
|
2400
|
-
on(document, 'input', onInput, {
|
|
2401
|
-
passive: false,
|
|
2402
|
-
});
|
|
2403
|
-
|
|
2404
|
-
on(document, 'keydown', onKeydown, {
|
|
2405
|
-
passive: false,
|
|
2406
|
-
});
|
|
2407
|
-
|
|
2408
|
-
customElements.define(TAGNAME, OuiComboboxElement);
|
|
2409
|
-
|
|
2410
|
-
// #endregion
|