@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.
Files changed (58) hide show
  1. package/dist/js/combobox/constants.combobox.d.mts +2 -0
  2. package/dist/js/combobox/constants.combobox.mjs +46 -0
  3. package/dist/js/combobox/data.combobox.d.mts +18 -0
  4. package/dist/js/combobox/data.combobox.mjs +2 -0
  5. package/dist/js/combobox/dom.combobox.d.mts +110 -0
  6. package/dist/js/combobox/dom.combobox.mjs +2 -0
  7. package/dist/js/combobox/event.combobox.d.mts +17 -0
  8. package/dist/js/combobox/event.combobox.mjs +2 -0
  9. package/dist/js/combobox/form.combobox.d.mts +6 -0
  10. package/dist/js/combobox/form.combobox.mjs +2 -0
  11. package/dist/js/combobox/i18n.combobox.d.mts +2 -0
  12. package/dist/js/combobox/i18n.combobox.mjs +2 -0
  13. package/dist/js/combobox/index.d.mts +2 -0
  14. package/dist/js/combobox/index.mjs +194 -0
  15. package/dist/js/combobox/misc.combobox.d.mts +14 -0
  16. package/dist/js/combobox/misc.combobox.mjs +2 -0
  17. package/dist/js/combobox/models.combobox.d.mts +2 -0
  18. package/dist/js/combobox/models.combobox.mjs +1 -0
  19. package/dist/js/combobox/state.combobox.d.mts +11 -0
  20. package/dist/js/combobox/state.combobox.mjs +2 -0
  21. package/dist/js/constants.combobox-DqNUGBLS.d.mts +243 -0
  22. package/dist/js/data.combobox-8RwBv5ey.mjs +1077 -0
  23. package/dist/js/dialog.mjs +3 -2
  24. package/dist/js/floatable/floatable.embedded.mjs +4 -4
  25. package/dist/js/focus-trap/focus-trap.embedded.mjs +3 -3
  26. package/dist/js/index.d.mts +14 -643
  27. package/dist/js/index.mjs +19 -4875
  28. package/dist/js/internal/draggable.mjs +6 -6
  29. package/dist/js/internal/navigable.mjs +3 -3
  30. package/dist/js/menu.d.mts +1 -1
  31. package/dist/js/menu.mjs +3 -2
  32. package/dist/js/movable/movable.embedded.mjs +3 -3
  33. package/dist/js/popover.mjs +1 -1
  34. package/dist/js/resizable/resizable.embedded.mjs +7 -7
  35. package/dist/js/sortable/sortable.embedded.mjs +7 -6
  36. package/dist/js/splitter.mjs +7 -13
  37. package/dist/js/toolbar.d.mts +1 -1
  38. package/dist/js/tooltip.mjs +3 -3
  39. package/package.json +4 -4
  40. package/src/js/combobox/constants.combobox.ts +62 -0
  41. package/src/js/combobox/data.combobox.ts +236 -0
  42. package/src/js/combobox/dom.combobox.ts +794 -0
  43. package/src/js/combobox/event.combobox.ts +454 -0
  44. package/src/js/combobox/form.combobox.ts +115 -0
  45. package/src/js/combobox/i18n.combobox.ts +179 -0
  46. package/src/js/combobox/index.ts +307 -0
  47. package/src/js/combobox/misc.combobox.ts +138 -0
  48. package/src/js/combobox/models.combobox.ts +94 -0
  49. package/src/js/combobox/state.combobox.ts +235 -0
  50. package/src/js/index.ts +1 -1
  51. package/src/js/internal/draggable.ts +3 -3
  52. package/src/js/movable/movable.embedded.ts +2 -2
  53. package/src/js/resizable/resizable.embedded.ts +3 -3
  54. package/src/js/sortable/sortable.embedded.ts +2 -2
  55. package/src/js/splitter.ts +2 -2
  56. package/dist/js/combobox.d.mts +0 -91
  57. package/dist/js/combobox.mjs +0 -797
  58. package/src/js/combobox.ts +0 -2410
@@ -0,0 +1,454 @@
1
+ import {clamp} from '@oscarpalmer/atoms/number';
2
+ import {dispatch, findAncestor, getData} from '@oscarpalmer/toretto';
3
+ import {on} from '@oscarpalmer/toretto/event';
4
+ import {
5
+ COMBOBOX_ATTRIBUTE_SELECTION_ITEM,
6
+ COMBOBOX_KEYS_ACTIVATION,
7
+ COMBOBOX_KEYS_NAVIGATION,
8
+ COMBOBOX_KEYS_SELECTION,
9
+ COMBOBOX_SELECTOR_ANCHOR,
10
+ COMBOBOX_SELECTOR_OPTION,
11
+ COMBOBOX_SELECTOR_SELECTION_ITEM,
12
+ COMBOBOX_TAGNAME,
13
+ } from './constants.combobox';
14
+ import {
15
+ applyComboboxSelection,
16
+ filterComboboxOption,
17
+ filterComboboxOptions,
18
+ toggleComboboxOption,
19
+ } from './data.combobox';
20
+ import {updateComboboxPreview, updateComboboxTabindices} from './dom.combobox';
21
+ import type {OuiComboboxElement} from './index';
22
+ import {getComboboxVisibleOptions, hasTextSelection} from './misc.combobox';
23
+ import type {OuiComboboxPreview, OuiComboboxState} from './models.combobox';
24
+ import {comboboxStates} from './state.combobox';
25
+
26
+ // #region Functions
27
+
28
+ export function onComboboxClickContent(event: PointerEvent, content: HTMLElement): void {
29
+ const state = comboboxStates.map.get(content);
30
+
31
+ if (state == null) {
32
+ return;
33
+ }
34
+
35
+ if (state.disabled) {
36
+ return;
37
+ }
38
+
39
+ const option = findAncestor(event, COMBOBOX_SELECTOR_OPTION);
40
+
41
+ if (!(option instanceof HTMLElement)) {
42
+ state.filter.element.focus();
43
+ } else if (state.readonly) {
44
+ updateComboboxPreview('set', state, option);
45
+ } else {
46
+ toggleComboboxOption(state, option);
47
+ }
48
+ }
49
+
50
+ export function onComboboxClickDocument(event: PointerEvent): void {
51
+ const combobox = findAncestor(event, COMBOBOX_TAGNAME) as OuiComboboxElement | null;
52
+ const state = comboboxStates.map.get(combobox!);
53
+
54
+ if (combobox == null || state == null) {
55
+ return;
56
+ }
57
+
58
+ const label = findAncestor(event, 'label');
59
+
60
+ if (label != null) {
61
+ if (label === state.label.element && combobox.type === 'select') {
62
+ state.anchor.element.focus();
63
+
64
+ if (!state.disabled) {
65
+ state.floatable.show();
66
+ }
67
+ }
68
+
69
+ return;
70
+ }
71
+
72
+ const selection = findAncestor(event, `[${COMBOBOX_ATTRIBUTE_SELECTION_ITEM}]`);
73
+
74
+ if (selection != null) {
75
+ const option = state.options.selected.find(
76
+ option => option.id === getData(selection, 'option', false),
77
+ );
78
+
79
+ if (state.disabled) {
80
+ state.anchor.element.focus();
81
+ } else if (state.readonly) {
82
+ updateComboboxTabindices(state, option);
83
+ } else {
84
+ toggleComboboxOption(state, option);
85
+ }
86
+
87
+ return;
88
+ }
89
+
90
+ const anchor = findAncestor(event, COMBOBOX_SELECTOR_ANCHOR);
91
+
92
+ if (anchor != null && !state.disabled) {
93
+ state.floatable.show();
94
+
95
+ return;
96
+ }
97
+ }
98
+
99
+ export function onComboboxEnter(state: OuiComboboxState): void {
100
+ if (state.disabled || state.readonly) {
101
+ return;
102
+ }
103
+
104
+ if (hasTextSelection(state.anchor.element)) {
105
+ onComboboxSelection(state);
106
+ } else {
107
+ toggleComboboxOption(state, state.active);
108
+ }
109
+ }
110
+
111
+ export function onComboboxEscape(state: OuiComboboxState): void {
112
+ state.floatable.hide();
113
+ }
114
+
115
+ export function onComboboxFilter(
116
+ type: InputEvent['inputType'],
117
+ state: OuiComboboxState,
118
+ manual?: boolean,
119
+ emit?: boolean,
120
+ ): void {
121
+ if (state.disabled || state.readonly) {
122
+ return;
123
+ }
124
+
125
+ const diff = state.filter.value !== state.filter.element.value;
126
+
127
+ if (!state.autocomplete.inline && state.filter.element.value.length > 0 && !(manual ?? false)) {
128
+ state.floatable.show();
129
+ }
130
+
131
+ const option = state.autocomplete.none
132
+ ? (state.active ?? state.options.all[0])
133
+ : filterComboboxOptions(state);
134
+
135
+ let update: OuiComboboxPreview | undefined;
136
+
137
+ if (type.includes('delete')) {
138
+ update = 'none';
139
+ } else if (type.includes('insert')) {
140
+ update = 'typed';
141
+ }
142
+
143
+ if (update != null) {
144
+ if (state.autocomplete.none) {
145
+ applyComboboxSelection({state, added: false, emit: (emit ?? true) && diff});
146
+ } else {
147
+ updateComboboxPreview(update, state, option);
148
+ }
149
+ }
150
+
151
+ if (diff) {
152
+ state.filter.value = state.filter.element.value;
153
+
154
+ dispatch(state.host, 'input');
155
+ }
156
+ }
157
+
158
+ export function onComboboxFocusout(state: OuiComboboxState): void {
159
+ if (
160
+ state.disabled ||
161
+ state.readonly ||
162
+ state.type === 'select' ||
163
+ state.host === document.activeElement ||
164
+ state.host.contains(document.activeElement)
165
+ ) {
166
+ return;
167
+ }
168
+
169
+ const option = filterComboboxOption(state, state.filter.element.value, true);
170
+
171
+ applyComboboxSelection({
172
+ option,
173
+ state,
174
+ added: false,
175
+ selected: option == null ? [] : [option],
176
+ });
177
+ }
178
+
179
+ export function onComboboxInput(event: InputEvent): void {
180
+ const combobox = findAncestor(event, COMBOBOX_TAGNAME) as OuiComboboxElement | null;
181
+ const state = comboboxStates.map.get(combobox!);
182
+
183
+ if (combobox != null && state != null && event.target === state.filter.element) {
184
+ onComboboxFilter(event.inputType, state);
185
+ }
186
+ }
187
+
188
+ export function onComboboxKeydown(event: KeyboardEvent): void {
189
+ const combobox = findAncestor(event, COMBOBOX_TAGNAME) as OuiComboboxElement | null;
190
+ const state = comboboxStates.map.get(combobox!);
191
+
192
+ if (combobox == null || state == null) {
193
+ return;
194
+ }
195
+
196
+ const item = findAncestor(event, COMBOBOX_SELECTOR_SELECTION_ITEM);
197
+
198
+ if (item instanceof HTMLElement) {
199
+ onComboboxKeydownSelection(event, state, item);
200
+
201
+ return;
202
+ }
203
+
204
+ onComboboxKeydownFilter(event, state);
205
+ }
206
+
207
+ function onComboboxKeydownFilter(event: KeyboardEvent, state: OuiComboboxState): void {
208
+ const ignore = state.disabled || state.readonly;
209
+ const isSelect = state.type === 'select';
210
+
211
+ let preventDefault = ignore;
212
+
213
+ switch (true) {
214
+ case event.key === ' ': {
215
+ if (!state.disabled) {
216
+ state.floatable.show();
217
+ }
218
+
219
+ break;
220
+ }
221
+
222
+ case event.key === 'Enter': {
223
+ preventDefault = state.floatable.open;
224
+
225
+ if (preventDefault) {
226
+ onComboboxEnter(state);
227
+ } else if (!ignore && state.type === 'select') {
228
+ state.floatable.show();
229
+ } else if (!ignore) {
230
+ state.internals.form?.requestSubmit();
231
+ }
232
+
233
+ break;
234
+ }
235
+
236
+ case event.key === 'Escape': {
237
+ preventDefault = false;
238
+
239
+ onComboboxEscape(state);
240
+
241
+ break;
242
+ }
243
+
244
+ case !isSelect && COMBOBOX_KEYS_SELECTION.has(event.key): {
245
+ if (!ignore && hasTextSelection(state.anchor.element)) {
246
+ onComboboxSelection(state);
247
+ }
248
+
249
+ break;
250
+ }
251
+
252
+ case COMBOBOX_KEYS_NAVIGATION.has(event.key): {
253
+ if (!state.disabled) {
254
+ onComboboxNavigation(event, state);
255
+ }
256
+
257
+ break;
258
+ }
259
+
260
+ default: {
261
+ preventDefault = false;
262
+
263
+ if (event.key === 'Tab') {
264
+ onComboboxTab(state);
265
+ }
266
+
267
+ break;
268
+ }
269
+ }
270
+
271
+ if (preventDefault) {
272
+ event.preventDefault();
273
+ event.stopPropagation();
274
+ }
275
+ }
276
+
277
+ export function onComboboxKeydownSelection(
278
+ event: KeyboardEvent,
279
+ state: OuiComboboxState,
280
+ item: HTMLElement,
281
+ ): void {
282
+ if (COMBOBOX_KEYS_ACTIVATION.has(event.key)) {
283
+ event.preventDefault();
284
+
285
+ onComboboxRemoveSelection(state, item);
286
+
287
+ return;
288
+ }
289
+
290
+ if (state.disabled || state.selection == null) {
291
+ return;
292
+ }
293
+
294
+ const {length} = state.selection.items;
295
+
296
+ const index = state.selection.items.findIndex(selection => selection.button === item);
297
+
298
+ let next: number | undefined;
299
+
300
+ switch (event.key) {
301
+ case 'ArrowDown':
302
+ state.anchor.element.focus();
303
+ state.anchor.element.click();
304
+ return;
305
+
306
+ case 'ArrowRight':
307
+ case 'ArrowLeft':
308
+ next = clamp(index + (event.key === 'ArrowRight' ? 1 : -1), 0, length - 1, true);
309
+ break;
310
+
311
+ case 'End':
312
+ next = length - 1;
313
+ break;
314
+
315
+ case 'Home':
316
+ next = 0;
317
+ break;
318
+
319
+ default:
320
+ break;
321
+ }
322
+
323
+ if (next == null) {
324
+ return;
325
+ }
326
+
327
+ event.preventDefault();
328
+
329
+ for (let index = 0; index < length; index += 1) {
330
+ const {button} = state.selection.items[index];
331
+ const selected = index === next;
332
+
333
+ button.tabIndex = selected ? 0 : -1;
334
+
335
+ if (selected) {
336
+ button.focus();
337
+ }
338
+ }
339
+ }
340
+
341
+ export function onComboboxNavigation(event: KeyboardEvent, state: OuiComboboxState): void {
342
+ let skip = false;
343
+
344
+ if (!state.floatable.open) {
345
+ skip = true;
346
+
347
+ state.floatable.show();
348
+ }
349
+
350
+ const visible = getComboboxVisibleOptions(state);
351
+
352
+ if (visible.length === 0) {
353
+ return;
354
+ }
355
+
356
+ let next: HTMLElement | undefined;
357
+
358
+ if (skip || state.active == null) {
359
+ next = state.active ?? visible.at(event.key === 'ArrowDown' ? 0 : -1);
360
+ } else {
361
+ const current = visible.findIndex(option => option.id === state.active?.id);
362
+
363
+ let index: number | undefined;
364
+
365
+ switch (event.key) {
366
+ case 'ArrowDown':
367
+ case 'ArrowUp':
368
+ index = clamp(current + (event.key === 'ArrowDown' ? 1 : -1), 0, visible.length - 1, true);
369
+ break;
370
+
371
+ case 'End':
372
+ index = visible.length - 1;
373
+ break;
374
+
375
+ case 'Home':
376
+ index = 0;
377
+ break;
378
+
379
+ case 'PageDown':
380
+ case 'PageUp':
381
+ index = clamp(current + (event.key === 'PageDown' ? 10 : -10), 0, visible.length - 1);
382
+ break;
383
+
384
+ default:
385
+ break;
386
+ }
387
+
388
+ if (index != null) {
389
+ next = visible[index];
390
+ }
391
+ }
392
+
393
+ let preview: OuiComboboxPreview;
394
+
395
+ if (state.type === 'autocomplete' && (state.autocomplete.both || state.autocomplete.inline)) {
396
+ preview = 'set';
397
+ } else {
398
+ preview = 'none';
399
+ }
400
+
401
+ updateComboboxPreview(preview, state, next ?? state.active ?? visible[0]);
402
+ }
403
+
404
+ export function onComboboxRemoveSelection(state: OuiComboboxState, item: HTMLElement): void {
405
+ if (state.disabled || state.readonly) {
406
+ return;
407
+ }
408
+
409
+ const option = state.options.selected.find(
410
+ option => option.id === getData(item, 'option', false),
411
+ );
412
+
413
+ if (option != null) {
414
+ toggleComboboxOption(state, option);
415
+ }
416
+ }
417
+
418
+ export function onComboboxSelection(state: OuiComboboxState): void {
419
+ state.filter.value =
420
+ state.anchor.element === state.filter.element ? state.filter.element.value : '';
421
+
422
+ toggleComboboxOption(state, filterComboboxOptions(state) ?? state.active);
423
+ }
424
+
425
+ export function onComboboxTab(state: OuiComboboxState): void {
426
+ setTimeout(() => {
427
+ if (state.host === document.activeElement) {
428
+ return;
429
+ }
430
+
431
+ if (
432
+ state.selection?.container.contains(document.activeElement) ||
433
+ !state.host.contains(document.activeElement)
434
+ ) {
435
+ state.floatable.hide();
436
+ }
437
+ });
438
+ }
439
+
440
+ // #endregion
441
+
442
+ // #region Initialization
443
+
444
+ on(document, 'click', onComboboxClickDocument);
445
+
446
+ on(document, 'input', onComboboxInput, {
447
+ passive: false,
448
+ });
449
+
450
+ on(document, 'keydown', onComboboxKeydown, {
451
+ passive: false,
452
+ });
453
+
454
+ // #endregion
@@ -0,0 +1,115 @@
1
+ import {dispatch} from '@oscarpalmer/toretto/event';
2
+ import {updateComboboxAnchor} from './dom.combobox';
3
+ import {getComboboxLocalization} from './i18n.combobox';
4
+ import type {OuiComboboxState} from './models.combobox';
5
+
6
+ // #region Functions
7
+
8
+ export function updateComboboxForm(
9
+ state: OuiComboboxState,
10
+ values: string[],
11
+ emit?: boolean,
12
+ ): void {
13
+ let value: string | FormData;
14
+
15
+ if (state.multiple) {
16
+ const {length} = values;
17
+
18
+ value = new FormData();
19
+
20
+ for (let index = 0; index < length; index += 1) {
21
+ (value as FormData).append(state.name, values[index]);
22
+ }
23
+ } else {
24
+ value = values[0];
25
+ }
26
+
27
+ state.values.current = values;
28
+
29
+ state.internals.setFormValue(value, JSON.stringify(values));
30
+
31
+ updateComboboxAnchor(state);
32
+ updateComboboxValidity(state);
33
+
34
+ if (emit ?? true) {
35
+ dispatch(state.host, 'change');
36
+ }
37
+ }
38
+
39
+ export function updateComboboxValidity(state: OuiComboboxState): void {
40
+ if (state.autocomplete.none) {
41
+ const valueMissing = state.required && state.filter.element.value.length === 0;
42
+
43
+ const localization = getComboboxLocalization('invalid', state);
44
+
45
+ state.internals.setValidity(
46
+ {valueMissing},
47
+ typeof localization === 'function' ? localization() : localization,
48
+ );
49
+
50
+ state.internals.checkValidity();
51
+
52
+ return;
53
+ }
54
+
55
+ let error: 'badInput' | 'rangeOverflow' | 'rangeUnderflow' | undefined;
56
+ let message: string | undefined;
57
+
58
+ switch (true) {
59
+ case state.required && state.options.selected.length === 0: {
60
+ error = 'badInput';
61
+
62
+ const localization = getComboboxLocalization('input', state);
63
+
64
+ message = typeof localization === 'function' ? localization() : localization;
65
+
66
+ break;
67
+ }
68
+
69
+ case state.multiple &&
70
+ typeof state.max === 'number' &&
71
+ state.options.selected.length > state.max: {
72
+ error = 'rangeOverflow';
73
+
74
+ const localization = getComboboxLocalization('max', state);
75
+
76
+ message =
77
+ typeof localization === 'function'
78
+ ? localization(state.max)
79
+ : localization.replace('{max}', state.max.toString());
80
+
81
+ break;
82
+ }
83
+
84
+ case state.multiple &&
85
+ typeof state.min === 'number' &&
86
+ state.options.selected.length < state.min: {
87
+ error = 'rangeUnderflow';
88
+
89
+ const localization = getComboboxLocalization('min', state);
90
+
91
+ message =
92
+ typeof localization === 'function'
93
+ ? localization(state.min)
94
+ : localization.replace('{min}', state.min.toString());
95
+
96
+ break;
97
+ }
98
+
99
+ default:
100
+ break;
101
+ }
102
+
103
+ state.internals.setValidity(
104
+ error == null
105
+ ? {}
106
+ : {
107
+ [error]: true,
108
+ },
109
+ message,
110
+ );
111
+
112
+ state.internals.checkValidity();
113
+ }
114
+
115
+ // #endregion