@oscarpalmer/oui 0.33.0 → 0.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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/index.d.mts +2 -0
  12. package/dist/js/combobox/index.mjs +195 -0
  13. package/dist/js/combobox/misc.combobox.d.mts +2 -0
  14. package/dist/js/combobox/misc.combobox.mjs +2 -0
  15. package/dist/js/combobox/models.combobox.d.mts +2 -0
  16. package/dist/js/combobox/models.combobox.mjs +1 -0
  17. package/dist/js/combobox/state.combobox.d.mts +13 -0
  18. package/dist/js/combobox/state.combobox.mjs +2 -0
  19. package/dist/js/constants.combobox-h_kyDegw.d.mts +248 -0
  20. package/dist/js/data.combobox-B8HMkDOh.mjs +1030 -0
  21. package/dist/js/dialog.mjs +3 -2
  22. package/dist/js/floatable/floatable.embedded.mjs +4 -4
  23. package/dist/js/focus-trap/focus-trap.embedded.mjs +3 -3
  24. package/dist/js/index.d.mts +14 -643
  25. package/dist/js/index.mjs +19 -4875
  26. package/dist/js/internal/draggable.mjs +6 -6
  27. package/dist/js/internal/navigable.mjs +3 -3
  28. package/dist/js/l10n.d.mts +20 -0
  29. package/dist/js/l10n.mjs +80 -0
  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 +825 -0
  43. package/src/js/combobox/event.combobox.ts +454 -0
  44. package/src/js/combobox/form.combobox.ts +116 -0
  45. package/src/js/combobox/index.ts +313 -0
  46. package/src/js/combobox/misc.combobox.ts +188 -0
  47. package/src/js/combobox/models.combobox.ts +94 -0
  48. package/src/js/combobox/state.combobox.ts +247 -0
  49. package/src/js/index.ts +1 -1
  50. package/src/js/internal/draggable.ts +3 -3
  51. package/src/js/l10n.ts +192 -0
  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,116 @@
1
+ import {dispatch} from '@oscarpalmer/toretto/event';
2
+ import {getLocalization, localize} from '../l10n';
3
+ import {updateComboboxAnchor} from './dom.combobox';
4
+ import type {OuiComboboxState} from './models.combobox';
5
+ import {comboboxLocalizations} from './state.combobox';
6
+
7
+ // #region Functions
8
+
9
+ export function updateComboboxForm(
10
+ state: OuiComboboxState,
11
+ values: string[],
12
+ emit?: boolean,
13
+ ): void {
14
+ let value: string | FormData;
15
+
16
+ if (state.multiple) {
17
+ const {length} = values;
18
+
19
+ value = new FormData();
20
+
21
+ for (let index = 0; index < length; index += 1) {
22
+ (value as FormData).append(state.name, values[index]);
23
+ }
24
+ } else {
25
+ value = values[0];
26
+ }
27
+
28
+ state.values.current = values;
29
+
30
+ state.internals.setFormValue(value, JSON.stringify(values));
31
+
32
+ updateComboboxAnchor(state);
33
+ updateComboboxValidity(state);
34
+
35
+ if (emit ?? true) {
36
+ dispatch(state.host, 'change');
37
+ }
38
+ }
39
+
40
+ export function updateComboboxValidity(state: OuiComboboxState): void {
41
+ if (state.autocomplete.none) {
42
+ const valueMissing = state.required && state.filter.element.value.length === 0;
43
+
44
+ const localization = getLocalization(
45
+ comboboxLocalizations,
46
+ state.host,
47
+ 'invalid',
48
+ state.localizations,
49
+ );
50
+
51
+ state.internals.setValidity({valueMissing}, localize(localization, {}, []));
52
+
53
+ state.internals.checkValidity();
54
+
55
+ return;
56
+ }
57
+
58
+ let data: Record<string, unknown> = {};
59
+ let error: 'badInput' | 'rangeOverflow' | 'rangeUnderflow' | undefined;
60
+ let key: 'input' | 'max' | 'min' | undefined;
61
+ let parameters: unknown[] = [];
62
+
63
+ switch (true) {
64
+ case state.required && state.options.selected.length === 0: {
65
+ error = 'badInput';
66
+ key = 'input';
67
+
68
+ break;
69
+ }
70
+
71
+ case state.multiple &&
72
+ typeof state.max === 'number' &&
73
+ state.options.selected.length > state.max: {
74
+ data = {amount: state.max};
75
+ error = 'rangeOverflow';
76
+ key = 'max';
77
+ parameters = [state.max];
78
+
79
+ break;
80
+ }
81
+
82
+ case state.multiple &&
83
+ typeof state.min === 'number' &&
84
+ state.options.selected.length < state.min: {
85
+ data = {amount: state.min};
86
+ error = 'rangeUnderflow';
87
+ key = 'min';
88
+ parameters = [state.min];
89
+
90
+ break;
91
+ }
92
+
93
+ default:
94
+ break;
95
+ }
96
+
97
+ const localization =
98
+ key == null
99
+ ? undefined
100
+ : getLocalization(comboboxLocalizations, state.host, key, state.localizations);
101
+
102
+ const message = localization == null ? undefined : localize(localization, data, parameters);
103
+
104
+ state.internals.setValidity(
105
+ error == null
106
+ ? {}
107
+ : {
108
+ [error]: true,
109
+ },
110
+ message,
111
+ );
112
+
113
+ state.internals.checkValidity();
114
+ }
115
+
116
+ // #endregion