@nepdatejs/vanilla 0.0.0-bootstrap → 0.1.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 (104) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +123 -1
  3. package/dist/auto-define.cjs +11 -0
  4. package/dist/auto-define.cjs.map +1 -0
  5. package/dist/auto-define.d.cts +2 -0
  6. package/dist/auto-define.d.ts +2 -0
  7. package/dist/auto-define.js +9 -0
  8. package/dist/auto-define.js.map +1 -0
  9. package/dist/calendar-BAcO-jYq.js +142 -0
  10. package/dist/calendar-BAcO-jYq.js.map +1 -0
  11. package/dist/calendar-BNdN8qJv.d.ts +87 -0
  12. package/dist/calendar-C1LZI7XN.cjs +159 -0
  13. package/dist/calendar-C1LZI7XN.cjs.map +1 -0
  14. package/dist/calendar-QWPX0IfD.d.cts +87 -0
  15. package/dist/calendar.cjs +7 -0
  16. package/dist/calendar.d.cts +2 -0
  17. package/dist/calendar.d.ts +2 -0
  18. package/dist/calendar.js +3 -0
  19. package/dist/date-picker-BidCGOz0.d.cts +74 -0
  20. package/dist/date-picker-CQoabjlj.cjs +600 -0
  21. package/dist/date-picker-CQoabjlj.cjs.map +1 -0
  22. package/dist/date-picker-DyvpYe29.d.ts +74 -0
  23. package/dist/date-picker-ucoEFnMW.js +577 -0
  24. package/dist/date-picker-ucoEFnMW.js.map +1 -0
  25. package/dist/date-picker.cjs +6 -0
  26. package/dist/date-picker.d.cts +3 -0
  27. package/dist/date-picker.d.ts +3 -0
  28. package/dist/date-picker.js +1 -0
  29. package/dist/date-range-picker-BQKcZeqG.cjs +489 -0
  30. package/dist/date-range-picker-BQKcZeqG.cjs.map +1 -0
  31. package/dist/date-range-picker-D3eNvlgv.d.ts +98 -0
  32. package/dist/date-range-picker-DAkCzVgT.js +466 -0
  33. package/dist/date-range-picker-DAkCzVgT.js.map +1 -0
  34. package/dist/date-range-picker-SIl0vhUV.d.cts +98 -0
  35. package/dist/date-range-picker.cjs +6 -0
  36. package/dist/date-range-picker.d.cts +2 -0
  37. package/dist/date-range-picker.d.ts +2 -0
  38. package/dist/date-range-picker.js +1 -0
  39. package/dist/date-time-picker-BqweOP2u.js +487 -0
  40. package/dist/date-time-picker-BqweOP2u.js.map +1 -0
  41. package/dist/date-time-picker-Bxfn-a9T.cjs +510 -0
  42. package/dist/date-time-picker-Bxfn-a9T.cjs.map +1 -0
  43. package/dist/date-time-picker-BzSyQxrf.d.cts +92 -0
  44. package/dist/date-time-picker-DzDpzLMZ.d.ts +92 -0
  45. package/dist/date-time-picker.cjs +6 -0
  46. package/dist/date-time-picker.d.cts +2 -0
  47. package/dist/date-time-picker.d.ts +2 -0
  48. package/dist/date-time-picker.js +1 -0
  49. package/dist/define-7natoEH4.js +18 -0
  50. package/dist/define-7natoEH4.js.map +1 -0
  51. package/dist/define-B8NUA3yj.d.cts +16 -0
  52. package/dist/define-B8NUA3yj.d.ts +16 -0
  53. package/dist/define-DTTdLfIM.cjs +23 -0
  54. package/dist/define-DTTdLfIM.cjs.map +1 -0
  55. package/dist/element-utils-BVye8UQq.d.cts +82 -0
  56. package/dist/element-utils-C-r5gPlk.d.ts +82 -0
  57. package/dist/index.cjs +30 -0
  58. package/dist/index.d.cts +8 -0
  59. package/dist/index.d.ts +8 -0
  60. package/dist/index.js +8 -0
  61. package/dist/multi-date-picker-B2mQbjZM.d.ts +105 -0
  62. package/dist/multi-date-picker-BMEYUPBa.cjs +462 -0
  63. package/dist/multi-date-picker-BMEYUPBa.cjs.map +1 -0
  64. package/dist/multi-date-picker-BUNf5zyJ.d.cts +105 -0
  65. package/dist/multi-date-picker-XCg8Y0sF.js +439 -0
  66. package/dist/multi-date-picker-XCg8Y0sF.js.map +1 -0
  67. package/dist/multi-date-picker.cjs +6 -0
  68. package/dist/multi-date-picker.d.cts +2 -0
  69. package/dist/multi-date-picker.d.ts +2 -0
  70. package/dist/multi-date-picker.js +1 -0
  71. package/dist/nepdate-picker.global.js +3 -0
  72. package/dist/nepdate-picker.global.js.map +1 -0
  73. package/dist/preset-list-CASwnwmS.js +45 -0
  74. package/dist/preset-list-CASwnwmS.js.map +1 -0
  75. package/dist/preset-list-Cs8H12Wp.cjs +50 -0
  76. package/dist/preset-list-Cs8H12Wp.cjs.map +1 -0
  77. package/dist/registry-3qzdVB7H.cjs +591 -0
  78. package/dist/registry-3qzdVB7H.cjs.map +1 -0
  79. package/dist/registry-BvkGnkvV.js +466 -0
  80. package/dist/registry-BvkGnkvV.js.map +1 -0
  81. package/package.json +159 -4
  82. package/src/auto-define.ts +6 -0
  83. package/src/calendar-element.ts +129 -0
  84. package/src/calendar-view.ts +406 -0
  85. package/src/calendar.ts +45 -0
  86. package/src/date-picker-element.ts +249 -0
  87. package/src/date-picker.ts +91 -0
  88. package/src/date-range-picker-element.ts +201 -0
  89. package/src/date-range-picker.ts +20 -0
  90. package/src/date-time-picker-element.ts +191 -0
  91. package/src/date-time-picker.ts +20 -0
  92. package/src/datetime-view.ts +459 -0
  93. package/src/define.ts +16 -0
  94. package/src/dom.ts +109 -0
  95. package/src/element-utils.ts +86 -0
  96. package/src/global.ts +7 -0
  97. package/src/index.ts +15 -0
  98. package/src/multi-date-picker-element.ts +187 -0
  99. package/src/multi-date-picker.ts +53 -0
  100. package/src/multi-view.ts +345 -0
  101. package/src/picker-view.ts +448 -0
  102. package/src/preset-list.ts +51 -0
  103. package/src/range-view.ts +393 -0
  104. package/src/registry.ts +27 -0
@@ -0,0 +1,448 @@
1
+ import { getLocale, type CalendarSystem } from '@nepdatejs/core';
2
+ import {
3
+ createDatePicker,
4
+ type DateDetail,
5
+ type DatePickerOptions,
6
+ type DatePickerState,
7
+ type DatePickerStore,
8
+ } from '@nepdatejs/datepicker';
9
+ import {
10
+ onBackdropPointer,
11
+ onEscape,
12
+ onOutsidePointer,
13
+ restoreFocus,
14
+ trackPosition,
15
+ type PanelLayout,
16
+ } from '@nepdatejs/dom';
17
+ import { CalendarView } from './calendar-view';
18
+ import { el, icon, setAttrs, setPopupMode, setText, uid } from './dom';
19
+ import { PANEL_WIDTH, PANEL_WIDTH_WITH_PRESETS, PresetList } from './preset-list';
20
+
21
+ export type Theme = 'light' | 'dark' | 'auto';
22
+
23
+ /** Everything the date picker DOM needs on top of the store's options. */
24
+ export interface DatePickerViewOptions extends DatePickerOptions {
25
+ /** Submit the AD value with a plain form under this name (hidden input). */
26
+ readonly name?: string | undefined;
27
+ /** Render a hidden input for `name`. Off for the form-associated custom element. */
28
+ readonly hiddenInput?: boolean | undefined;
29
+ readonly id?: string | undefined;
30
+ readonly placeholder?: string | undefined;
31
+ readonly required?: boolean | undefined;
32
+ readonly disabled?: boolean | undefined;
33
+ /** Show the × button. Default true. */
34
+ readonly clearable?: boolean | undefined;
35
+ /** Show the same day in the other calendar under the field. Default true. */
36
+ readonly showEquivalent?: boolean | undefined;
37
+ /** 'sheet' opens a bottom sheet; 'popover' sits beside the field; 'auto' (default) uses the sheet below 480px. */
38
+ readonly panelLayout?: PanelLayout | undefined;
39
+ readonly theme?: Theme | undefined;
40
+ /** Remember the viewer's BS/AD choice in localStorage under this key. */
41
+ readonly persistCalendar?: string | undefined;
42
+ readonly ariaLabel?: string | undefined;
43
+ readonly onOpenChange?: ((open: boolean) => void) | undefined;
44
+ /** Called on every keystroke with the current text. */
45
+ readonly onInput?: ((text: string) => void) | undefined;
46
+ }
47
+
48
+ const VIEW_KEYS = new Set([
49
+ 'name',
50
+ 'hiddenInput',
51
+ 'id',
52
+ 'placeholder',
53
+ 'required',
54
+ 'disabled',
55
+ 'clearable',
56
+ 'showEquivalent',
57
+ 'theme',
58
+ 'panelLayout',
59
+ 'persistCalendar',
60
+ 'ariaLabel',
61
+ 'onOpenChange',
62
+ 'onInput',
63
+ ]);
64
+
65
+ /**
66
+ * The date picker field and panel, rendered with the @nepdatejs/styles markup
67
+ * into `host`. Used by <nep-date-picker> and by createDatePicker().
68
+ */
69
+ export class DatePickerView {
70
+ readonly store: DatePickerStore;
71
+ readonly input: HTMLInputElement;
72
+ private options: DatePickerViewOptions;
73
+ private readonly field: HTMLDivElement;
74
+ private readonly box: HTMLDivElement;
75
+ private readonly clearButton: HTMLButtonElement;
76
+ private readonly switchButton: HTMLButtonElement;
77
+ private readonly toggleButton: HTMLButtonElement;
78
+ private readonly hint: HTMLParagraphElement;
79
+ private readonly panel: HTMLDivElement;
80
+ private readonly segment: HTMLDivElement;
81
+ private readonly todayButton: HTMLButtonElement;
82
+ private readonly hidden: HTMLInputElement;
83
+ private readonly calendarView: CalendarView;
84
+ private readonly presetList: PresetList;
85
+ private readonly unsubscribe: () => void;
86
+ private closePanel: (() => void) | null = null;
87
+ private wasOpen = false;
88
+
89
+ constructor(host: HTMLElement, options: DatePickerViewOptions = {}) {
90
+ this.options = { ...options };
91
+ this.store = createDatePicker({
92
+ ...storeOptions(options),
93
+ onChange: (value, detail) => {
94
+ this.options.onChange?.(value, detail);
95
+ },
96
+ onCalendarChange: (calendar) => {
97
+ this.remember(calendar);
98
+ this.options.onCalendarChange?.(calendar);
99
+ },
100
+ });
101
+
102
+ const inputId = options.id ?? uid('ndp-input');
103
+ const panelId = `${inputId}-panel`;
104
+ this.input = el('input', {
105
+ class: 'ndp-input__control',
106
+ id: inputId,
107
+ type: 'text',
108
+ inputmode: 'numeric',
109
+ autocomplete: 'off',
110
+ spellcheck: 'false',
111
+ 'aria-controls': panelId,
112
+ });
113
+ this.clearButton = el(
114
+ 'button',
115
+ { type: 'button', class: 'ndp-icon-button', 'data-action': 'clear' },
116
+ [icon('close')],
117
+ );
118
+ this.switchButton = el('button', {
119
+ type: 'button',
120
+ class: 'ndp-input__switch',
121
+ 'data-action': 'switch',
122
+ });
123
+ this.toggleButton = el(
124
+ 'button',
125
+ { type: 'button', class: 'ndp-icon-button', 'data-action': 'toggle' },
126
+ [icon('calendar')],
127
+ );
128
+ this.box = el('div', { class: 'ndp-input' }, [
129
+ this.input,
130
+ el('span', { class: 'ndp-input__actions' }, [
131
+ this.clearButton,
132
+ this.switchButton,
133
+ this.toggleButton,
134
+ ]),
135
+ ]);
136
+ this.hint = el('p', {
137
+ class: 'ndp-hint',
138
+ id: `${inputId}-hint`,
139
+ 'aria-live': 'polite',
140
+ hidden: true,
141
+ });
142
+ this.hidden = el('input', { type: 'hidden' });
143
+ this.segment = el('div', { class: 'ndp-segment', role: 'group' }, [
144
+ el('button', { type: 'button', class: 'ndp-segment__item', 'data-calendar': 'BS' }),
145
+ el('button', { type: 'button', class: 'ndp-segment__item', 'data-calendar': 'AD' }),
146
+ ]);
147
+ this.todayButton = el('button', {
148
+ type: 'button',
149
+ class: 'ndp-button',
150
+ 'data-action': 'today',
151
+ });
152
+ const calendarRoot = el('div', { class: 'ndp-panel__calendar' });
153
+ this.presetList = new PresetList((id) => {
154
+ if (this.store.actions.applyPreset(id) && !this.store.getState().open) {
155
+ restoreFocus(this.input);
156
+ }
157
+ });
158
+ this.panel = el(
159
+ 'div',
160
+ {
161
+ class: 'ndp-panel',
162
+ id: panelId,
163
+ hidden: true,
164
+ },
165
+ [
166
+ el('div', { class: 'ndp-panel__body' }, [this.presetList.element, calendarRoot]),
167
+ el('div', { class: 'ndp-panel__footer' }, [this.segment, this.todayButton]),
168
+ ],
169
+ );
170
+ this.field = el('div', { class: 'ndp-field' }, [
171
+ this.box,
172
+ this.hint,
173
+ this.panel,
174
+ this.hidden,
175
+ ]);
176
+ host.append(this.field);
177
+
178
+ this.calendarView = new CalendarView(calendarRoot, this.store.calendar, {
179
+ locale: () => this.options.locale,
180
+ onDayActivate: (date) => {
181
+ if (this.store.actions.select(date) && !this.store.getState().open) {
182
+ restoreFocus(this.input);
183
+ }
184
+ },
185
+ });
186
+
187
+ this.input.addEventListener('input', this.onTextInput);
188
+ this.input.addEventListener('blur', () => {
189
+ this.store.actions.blur();
190
+ });
191
+ this.input.addEventListener('keydown', this.onInputKeyDown);
192
+ this.field.addEventListener('click', this.onClick);
193
+
194
+ this.unsubscribe = this.store.subscribe(() => {
195
+ this.render();
196
+ });
197
+ this.restoreCalendar();
198
+ this.render();
199
+ }
200
+
201
+ getState(): DatePickerState {
202
+ return this.store.getState();
203
+ }
204
+
205
+ /** Change view or store options. Unknown keys are ignored. */
206
+ setOptions(changes: Partial<DatePickerViewOptions>): void {
207
+ this.options = { ...this.options, ...changes };
208
+ const forStore = storeOptions(changes);
209
+ if (Object.keys(forStore).length > 0) {
210
+ this.store.setOptions(forStore);
211
+ } else {
212
+ this.render();
213
+ }
214
+ }
215
+
216
+ open(): void {
217
+ if (!this.options.disabled) {
218
+ this.store.actions.open();
219
+ }
220
+ }
221
+
222
+ close(): void {
223
+ this.store.actions.close();
224
+ }
225
+
226
+ destroy(): void {
227
+ this.closePanel?.();
228
+ this.unsubscribe();
229
+ this.calendarView.destroy();
230
+ this.field.remove();
231
+ }
232
+
233
+ private render(): void {
234
+ const state = this.store.getState();
235
+ const locale = getLocale(this.options.locale);
236
+ const disabled = this.options.disabled ?? false;
237
+ const hintText =
238
+ state.error?.message ??
239
+ ((this.options.showEquivalent ?? true) ? state.equivalentText : null);
240
+
241
+ if (this.input.value !== state.text) {
242
+ this.input.value = state.text;
243
+ }
244
+ this.input.setCustomValidity(state.error?.message ?? '');
245
+ setAttrs(this.input, {
246
+ placeholder: this.options.placeholder ?? state.placeholder,
247
+ disabled,
248
+ required: this.options.required ?? false,
249
+ 'aria-label': this.options.ariaLabel ?? null,
250
+ 'aria-invalid': state.error ? 'true' : null,
251
+ 'aria-describedby': hintText ? this.hint.id : null,
252
+ });
253
+ setAttrs(this.box, {
254
+ 'data-invalid': state.error !== null,
255
+ 'data-disabled': disabled,
256
+ });
257
+ setAttrs(this.field, { 'data-ndp-theme': this.options.theme ?? null });
258
+ setAttrs(this.panel, {
259
+ 'data-ndp-theme': this.options.theme ?? null,
260
+ 'aria-label': this.options.ariaLabel
261
+ ? `${this.options.ariaLabel}: ${locale.labels.calendar}`
262
+ : locale.labels.chooseDate,
263
+ });
264
+
265
+ this.clearButton.hidden =
266
+ !(this.options.clearable ?? true) || state.text === '' || disabled;
267
+ setAttrs(this.clearButton, { 'aria-label': locale.labels.clear });
268
+ this.switchButton.hidden = !state.canSwitchCalendar;
269
+ setText(this.switchButton, locale.calendarNames[state.calendar]);
270
+ const other: CalendarSystem = state.calendar === 'BS' ? 'AD' : 'BS';
271
+ setAttrs(this.switchButton, {
272
+ disabled,
273
+ 'aria-label': `${locale.labels.calendar}: ${locale.calendarNames[state.calendar]} → ${locale.calendarNames[other]}`,
274
+ });
275
+ setAttrs(this.toggleButton, {
276
+ disabled,
277
+ 'aria-label': locale.labels.openCalendar,
278
+ 'aria-expanded': state.open ? 'true' : 'false',
279
+ });
280
+
281
+ this.hint.hidden = hintText === null;
282
+ setText(this.hint, hintText ?? '');
283
+ setAttrs(this.hint, { 'data-error': state.error !== null });
284
+
285
+ this.segment.hidden = !state.canSwitchCalendar;
286
+ setAttrs(this.segment, { 'aria-label': locale.labels.calendar });
287
+ for (const button of this.segment.querySelectorAll<HTMLButtonElement>('button')) {
288
+ const calendar = button.dataset['calendar'] as CalendarSystem;
289
+ setText(button, locale.calendarNames[calendar]);
290
+ setAttrs(button, {
291
+ 'aria-pressed': calendar === state.calendar ? 'true' : 'false',
292
+ });
293
+ }
294
+ setText(this.todayButton, locale.labels.today);
295
+ this.presetList.render(state.presets, locale.labels.presets);
296
+
297
+ const name = this.options.hiddenInput === false ? undefined : this.options.name;
298
+ setAttrs(this.hidden, { name: name ?? null, value: state.value });
299
+ this.hidden.value = state.value;
300
+ this.hidden.disabled = name === undefined || disabled;
301
+
302
+ setPopupMode(state, [this.input], this.toggleButton, this.panel);
303
+ this.syncPanel(state.open && !state.inline);
304
+ this.panel.hidden = !state.inline && !this.wasOpen;
305
+ this.panel.style.setProperty(
306
+ '--ndp-panel-width',
307
+ state.inline ? `${String(this.panelWidth())}px` : null,
308
+ );
309
+ }
310
+
311
+ private panelWidth(): number {
312
+ return this.store.getState().presets.length > 0
313
+ ? PANEL_WIDTH_WITH_PRESETS
314
+ : PANEL_WIDTH;
315
+ }
316
+
317
+ private syncPanel(open: boolean): void {
318
+ if (open === this.wasOpen) {
319
+ return;
320
+ }
321
+ this.wasOpen = open;
322
+ this.options.onOpenChange?.(open);
323
+ if (open) {
324
+ this.panel.hidden = false;
325
+ const stopTracking = trackPosition(this.box, this.panel, {
326
+ width: this.panelWidth(),
327
+ layout: this.options.panelLayout,
328
+ });
329
+ const stopOutside = onOutsidePointer([this.field, this.panel], () => {
330
+ this.store.actions.close();
331
+ });
332
+ const stopBackdrop = onBackdropPointer(this.panel, () => {
333
+ this.store.actions.close();
334
+ });
335
+ const stopEscape = onEscape(() => {
336
+ this.store.actions.close();
337
+ restoreFocus(this.input);
338
+ });
339
+ this.closePanel = () => {
340
+ stopTracking();
341
+ stopOutside();
342
+ stopBackdrop();
343
+ stopEscape();
344
+ this.panel.hidden = true;
345
+ };
346
+ this.calendarView.focusGrid();
347
+ } else {
348
+ this.closePanel?.();
349
+ this.closePanel = null;
350
+ }
351
+ }
352
+
353
+ private readonly onTextInput = (): void => {
354
+ this.store.actions.type(this.input.value);
355
+ this.options.onInput?.(this.input.value);
356
+ };
357
+
358
+ private readonly onInputKeyDown = (event: KeyboardEvent): void => {
359
+ if (event.key === 'ArrowDown' && (event.altKey || this.input.value === '')) {
360
+ event.preventDefault();
361
+ this.open();
362
+ }
363
+ };
364
+
365
+ private readonly onClick = (event: MouseEvent): void => {
366
+ const target = event.target instanceof Element ? event.target : null;
367
+ if (!target || this.calendarView.element.contains(target)) {
368
+ return;
369
+ }
370
+ const calendar = target.closest<HTMLElement>('[data-calendar]')?.dataset['calendar'];
371
+ if (calendar === 'BS' || calendar === 'AD') {
372
+ this.store.actions.switchCalendar(calendar);
373
+ return;
374
+ }
375
+ switch (target.closest<HTMLElement>('[data-action]')?.dataset['action']) {
376
+ case 'clear':
377
+ this.store.actions.clear();
378
+ this.input.focus();
379
+ break;
380
+ case 'switch':
381
+ this.store.actions.switchCalendar(
382
+ this.store.getState().calendar === 'BS' ? 'AD' : 'BS',
383
+ );
384
+ break;
385
+ case 'toggle':
386
+ if (this.store.getState().open) {
387
+ this.close();
388
+ } else {
389
+ this.open();
390
+ }
391
+ break;
392
+ case 'today':
393
+ if (this.store.actions.selectToday()) {
394
+ restoreFocus(this.input);
395
+ }
396
+ break;
397
+ default:
398
+ break;
399
+ }
400
+ };
401
+
402
+ private remember(calendar: CalendarSystem): void {
403
+ const key = this.options.persistCalendar;
404
+ if (!key) {
405
+ return;
406
+ }
407
+ try {
408
+ window.localStorage.setItem(key, calendar);
409
+ } catch {
410
+ // Storage can be blocked (private mode); the choice then lasts for this page only.
411
+ }
412
+ }
413
+
414
+ private restoreCalendar(): void {
415
+ const key = this.options.persistCalendar;
416
+ if (!key) {
417
+ return;
418
+ }
419
+ const stored = readStorage(key);
420
+ if (stored === 'BS' || stored === 'AD') {
421
+ this.store.actions.switchCalendar(stored);
422
+ }
423
+ }
424
+ }
425
+
426
+ /** Only the keys the picker store understands, minus the view's own callbacks. */
427
+ function storeOptions(
428
+ options: Partial<DatePickerViewOptions>,
429
+ ): Partial<DatePickerOptions> {
430
+ const out: Record<string, unknown> = {};
431
+ for (const [key, value] of Object.entries(options)) {
432
+ if (!VIEW_KEYS.has(key) && key !== 'onChange' && key !== 'onCalendarChange') {
433
+ out[key] = value;
434
+ }
435
+ }
436
+
437
+ return out;
438
+ }
439
+
440
+ function readStorage(key: string): string | null {
441
+ try {
442
+ return window.localStorage.getItem(key);
443
+ } catch {
444
+ return null; // storage blocked
445
+ }
446
+ }
447
+
448
+ export type { DateDetail };
@@ -0,0 +1,51 @@
1
+ import type { PresetState } from '@nepdatejs/datepicker';
2
+ import { el, setAttrs, setText } from './dom';
3
+
4
+ /** Panel width with and without the presets column. */
5
+ export const PANEL_WIDTH = 320;
6
+ export const PANEL_WIDTH_WITH_PRESETS = 492;
7
+ export const TIME_COLUMNS_WIDTH = 136;
8
+
9
+ /** The shortcut buttons beside the calendar. Hidden when there are none. */
10
+ export class PresetList {
11
+ readonly element: HTMLDivElement;
12
+ private ids = '';
13
+
14
+ constructor(onApply: (id: string) => void) {
15
+ this.element = el('div', { class: 'ndp-presets', role: 'group', hidden: true });
16
+ this.element.addEventListener('click', (event) => {
17
+ const button =
18
+ event.target instanceof Element
19
+ ? event.target.closest<HTMLButtonElement>('.ndp-preset')
20
+ : null;
21
+ const id = button?.dataset['preset'];
22
+ if (id !== undefined && button && !button.disabled) {
23
+ onApply(id);
24
+ }
25
+ });
26
+ }
27
+
28
+ render(presets: readonly PresetState[], label: string): void {
29
+ this.element.hidden = presets.length === 0;
30
+ setAttrs(this.element, { 'aria-label': label });
31
+ const ids = presets.map((preset) => preset.id).join('\n');
32
+ if (ids !== this.ids) {
33
+ this.ids = ids;
34
+ this.element.replaceChildren(
35
+ ...presets.map((preset) =>
36
+ el('button', { type: 'button', class: 'ndp-preset', 'data-preset': preset.id }),
37
+ ),
38
+ );
39
+ }
40
+ presets.forEach((preset, index) => {
41
+ const button = this.element.children[index];
42
+ if (button) {
43
+ setText(button, preset.label);
44
+ setAttrs(button, {
45
+ 'aria-pressed': preset.isActive ? 'true' : 'false',
46
+ disabled: preset.isDisabled,
47
+ });
48
+ }
49
+ });
50
+ }
51
+ }