@nepdatejs/vanilla 0.0.0-stage → 0.1.1

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 (105) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +124 -2
  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-BwlrlU5Y.js +560 -0
  21. package/dist/date-picker-BwlrlU5Y.js.map +1 -0
  22. package/dist/date-picker-CgjYsf1w.cjs +583 -0
  23. package/dist/date-picker-CgjYsf1w.cjs.map +1 -0
  24. package/dist/date-picker-DyvpYe29.d.ts +74 -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-D3eNvlgv.d.ts +98 -0
  30. package/dist/date-range-picker-NwetXUC-.js +449 -0
  31. package/dist/date-range-picker-NwetXUC-.js.map +1 -0
  32. package/dist/date-range-picker-SIl0vhUV.d.cts +98 -0
  33. package/dist/date-range-picker-VrQZFZED.cjs +472 -0
  34. package/dist/date-range-picker-VrQZFZED.cjs.map +1 -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-BzSyQxrf.d.cts +92 -0
  40. package/dist/date-time-picker-ChHIBTBw.cjs +492 -0
  41. package/dist/date-time-picker-ChHIBTBw.cjs.map +1 -0
  42. package/dist/date-time-picker-CxqC95aM.js +469 -0
  43. package/dist/date-time-picker-CxqC95aM.js.map +1 -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-B8NUA3yj.d.cts +16 -0
  50. package/dist/define-B8NUA3yj.d.ts +16 -0
  51. package/dist/define-BbTIFHtH.cjs +23 -0
  52. package/dist/define-BbTIFHtH.cjs.map +1 -0
  53. package/dist/define-D96w8Zu2.js +18 -0
  54. package/dist/define-D96w8Zu2.js.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-BUNf5zyJ.d.cts +105 -0
  63. package/dist/multi-date-picker-C3Lr0Dq-.js +424 -0
  64. package/dist/multi-date-picker-C3Lr0Dq-.js.map +1 -0
  65. package/dist/multi-date-picker-DUyuO-nH.cjs +447 -0
  66. package/dist/multi-date-picker-DUyuO-nH.cjs.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-BYCZm61I.cjs +97 -0
  74. package/dist/preset-list-BYCZm61I.cjs.map +1 -0
  75. package/dist/preset-list-DmE6fJjr.js +80 -0
  76. package/dist/preset-list-DmE6fJjr.js.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 +162 -3
  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 +434 -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 +323 -0
  101. package/src/panel.ts +61 -0
  102. package/src/picker-view.ts +423 -0
  103. package/src/preset-list.ts +51 -0
  104. package/src/range-view.ts +368 -0
  105. package/src/registry.ts +27 -0
@@ -0,0 +1,368 @@
1
+ import { getLocale } from '@nepdatejs/core';
2
+ import {
3
+ createDateRangePicker,
4
+ type DateRangePickerOptions,
5
+ type DateRangePickerState,
6
+ type DateRangePickerStore,
7
+ type RangeField,
8
+ } from '@nepdatejs/datepicker';
9
+ import { restoreFocus, type PanelLayout } from '@nepdatejs/dom';
10
+ import { CalendarView } from './calendar-view';
11
+ import { openPanel, renderSegment } from './panel';
12
+ import { el, icon, setAttrs, setPopupMode, setText, uid } from './dom';
13
+ import { PANEL_WIDTH, PANEL_WIDTH_WITH_PRESETS, PresetList } from './preset-list';
14
+ import type { Theme } from './picker-view';
15
+
16
+ export interface DateRangePickerViewOptions extends DateRangePickerOptions {
17
+ /** Field names for plain form posts; hidden inputs are rendered when set. */
18
+ readonly startName?: string | undefined;
19
+ readonly endName?: string | undefined;
20
+ readonly hiddenInput?: boolean | undefined;
21
+ readonly disabled?: boolean | undefined;
22
+ readonly required?: boolean | undefined;
23
+ /** 'sheet' opens a bottom sheet; 'popover' sits beside the field; 'auto' (default) uses the sheet below 480px. */
24
+ readonly panelLayout?: PanelLayout | undefined;
25
+ readonly theme?: Theme | undefined;
26
+ readonly startLabel?: string | undefined;
27
+ readonly endLabel?: string | undefined;
28
+ /** Show a "Fiscal year" button that switches the panel to fiscal-year tiles. */
29
+ readonly fiscalYearView?: boolean | undefined;
30
+ readonly onOpenChange?: ((open: boolean) => void) | undefined;
31
+ }
32
+
33
+ const VIEW_KEYS = new Set([
34
+ 'startName',
35
+ 'endName',
36
+ 'hiddenInput',
37
+ 'disabled',
38
+ 'required',
39
+ 'theme',
40
+ 'panelLayout',
41
+ 'startLabel',
42
+ 'endLabel',
43
+ 'fiscalYearView',
44
+ 'onOpenChange',
45
+ 'onChange',
46
+ 'onCalendarChange',
47
+ ]);
48
+
49
+ /** Two date fields (start, end) sharing one panel calendar in range mode. */
50
+ export class DateRangePickerView {
51
+ readonly store: DateRangePickerStore;
52
+ readonly inputs: Record<RangeField, HTMLInputElement>;
53
+ private options: DateRangePickerViewOptions;
54
+ private readonly field: HTMLDivElement;
55
+ private readonly boxes: Record<RangeField, HTMLDivElement>;
56
+ private readonly toggleButton: HTMLButtonElement;
57
+ private readonly hint: HTMLParagraphElement;
58
+ private readonly panel: HTMLDivElement;
59
+ private readonly segment: HTMLDivElement;
60
+ private readonly fiscalButton: HTMLButtonElement;
61
+ private readonly footer: HTMLDivElement;
62
+ private readonly hidden: Record<RangeField, HTMLInputElement>;
63
+ private readonly calendarView: CalendarView;
64
+ private readonly presetList: PresetList;
65
+ private readonly unsubscribe: () => void;
66
+ private closePanel: (() => void) | null = null;
67
+ private wasOpen = false;
68
+
69
+ constructor(host: HTMLElement, options: DateRangePickerViewOptions = {}) {
70
+ this.options = { ...options };
71
+ this.store = createDateRangePicker({
72
+ ...storeOptions(options),
73
+ onChange: (value, detail) => {
74
+ this.options.onChange?.(value, detail);
75
+ },
76
+ onCalendarChange: (calendar) => {
77
+ this.options.onCalendarChange?.(calendar);
78
+ },
79
+ });
80
+ const id = uid('ndp-range');
81
+ const makeInput = (field: RangeField): HTMLInputElement => {
82
+ const input = el('input', {
83
+ class: 'ndp-input__control',
84
+ id: `${id}-${field}`,
85
+ type: 'text',
86
+ inputmode: 'numeric',
87
+ autocomplete: 'off',
88
+ 'aria-controls': `${id}-panel`,
89
+ 'data-field': field,
90
+ });
91
+ input.addEventListener('input', () => {
92
+ this.store.actions.type(field, input.value);
93
+ });
94
+ input.addEventListener('blur', () => {
95
+ this.store.actions.blur(field);
96
+ });
97
+ input.addEventListener('keydown', (event) => {
98
+ if (event.key === 'ArrowDown' && (event.altKey || input.value === '')) {
99
+ event.preventDefault();
100
+ this.open();
101
+ }
102
+ });
103
+ return input;
104
+ };
105
+ this.inputs = { start: makeInput('start'), end: makeInput('end') };
106
+ this.toggleButton = el(
107
+ 'button',
108
+ { type: 'button', class: 'ndp-icon-button', 'data-action': 'toggle' },
109
+ [icon('calendar')],
110
+ );
111
+ this.boxes = {
112
+ start: el('div', { class: 'ndp-input' }, [this.inputs.start]),
113
+ end: el('div', { class: 'ndp-input' }, [
114
+ this.inputs.end,
115
+ el('span', { class: 'ndp-input__actions' }, [this.toggleButton]),
116
+ ]),
117
+ };
118
+ this.hint = el('p', {
119
+ class: 'ndp-hint',
120
+ id: `${id}-hint`,
121
+ 'aria-live': 'polite',
122
+ hidden: true,
123
+ });
124
+ this.hidden = {
125
+ start: el('input', { type: 'hidden' }),
126
+ end: el('input', { type: 'hidden' }),
127
+ };
128
+ this.segment = el('div', { class: 'ndp-segment', role: 'group' }, [
129
+ el('button', { type: 'button', class: 'ndp-segment__item', 'data-calendar': 'BS' }),
130
+ el('button', { type: 'button', class: 'ndp-segment__item', 'data-calendar': 'AD' }),
131
+ ]);
132
+ this.fiscalButton = el('button', {
133
+ type: 'button',
134
+ class: 'ndp-button',
135
+ 'data-action': 'fiscal',
136
+ });
137
+ this.footer = el('div', { class: 'ndp-panel__footer' }, [
138
+ this.segment,
139
+ this.fiscalButton,
140
+ ]);
141
+ const calendarRoot = el('div', { class: 'ndp-panel__calendar' });
142
+ this.presetList = new PresetList((id) => {
143
+ if (this.store.actions.applyPreset(id) && !this.store.getState().open) {
144
+ restoreFocus(this.inputs.start);
145
+ }
146
+ });
147
+ this.panel = el(
148
+ 'div',
149
+ {
150
+ class: 'ndp-panel',
151
+ id: `${id}-panel`,
152
+
153
+ hidden: true,
154
+ },
155
+ [
156
+ el('div', { class: 'ndp-panel__body' }, [this.presetList.element, calendarRoot]),
157
+ this.footer,
158
+ ],
159
+ );
160
+ const row = el('div', { class: 'ndp-range' }, [
161
+ this.boxes.start,
162
+ el('span', { class: 'ndp-range__separator', 'aria-hidden': 'true' }, ['→']),
163
+ this.boxes.end,
164
+ ]);
165
+ this.field = el('div', { class: 'ndp-field' }, [
166
+ row,
167
+ this.hint,
168
+ this.panel,
169
+ this.hidden.start,
170
+ this.hidden.end,
171
+ ]);
172
+ host.append(this.field);
173
+ this.calendarView = new CalendarView(calendarRoot, this.store.calendar, {
174
+ locale: () => this.options.locale,
175
+ onDayActivate: (date) => {
176
+ if (this.store.actions.select(date) && !this.store.getState().open) {
177
+ restoreFocus(this.inputs.end);
178
+ }
179
+ },
180
+ });
181
+ this.field.addEventListener('click', this.onClick);
182
+ this.unsubscribe = this.store.subscribe(() => {
183
+ this.render();
184
+ });
185
+ this.render();
186
+ }
187
+
188
+ getState(): DateRangePickerState {
189
+ return this.store.getState();
190
+ }
191
+
192
+ setOptions(changes: Partial<DateRangePickerViewOptions>): void {
193
+ this.options = { ...this.options, ...changes };
194
+ const forStore = storeOptions(changes);
195
+ if (Object.keys(forStore).length > 0) {
196
+ this.store.setOptions(forStore);
197
+ } else {
198
+ this.render();
199
+ }
200
+ }
201
+
202
+ open(): void {
203
+ if (!this.options.disabled) {
204
+ this.store.actions.open();
205
+ }
206
+ }
207
+
208
+ close(): void {
209
+ this.store.actions.close();
210
+ }
211
+
212
+ destroy(): void {
213
+ this.closePanel?.();
214
+ this.unsubscribe();
215
+ this.calendarView.destroy();
216
+ this.field.remove();
217
+ }
218
+
219
+ private render(): void {
220
+ const state = this.store.getState();
221
+ const locale = getLocale(this.options.locale);
222
+ const disabled = this.options.disabled ?? false;
223
+ const hintText = state.error?.message ?? null;
224
+ const texts: Record<RangeField, string> = {
225
+ start: state.startText,
226
+ end: state.endText,
227
+ };
228
+ const labels: Record<RangeField, string | undefined> = {
229
+ start: this.options.startLabel,
230
+ end: this.options.endLabel,
231
+ };
232
+ for (const field of ['start', 'end'] as const) {
233
+ const input = this.inputs[field];
234
+ if (input.value !== texts[field]) {
235
+ input.value = texts[field];
236
+ }
237
+ const error = state.error;
238
+ const fieldError =
239
+ error !== null && (error.field === field || error.field === 'range');
240
+ input.setCustomValidity(fieldError ? error.message : '');
241
+ setAttrs(input, {
242
+ placeholder: state.placeholder,
243
+ disabled,
244
+ required: this.options.required ?? false,
245
+ 'aria-label': labels[field] ?? null,
246
+ 'aria-invalid': fieldError ? 'true' : null,
247
+ 'aria-describedby': hintText ? this.hint.id : null,
248
+ });
249
+ setAttrs(this.boxes[field], {
250
+ 'data-invalid': fieldError,
251
+ 'data-disabled': disabled,
252
+ });
253
+ const name =
254
+ this.options.hiddenInput === false
255
+ ? undefined
256
+ : field === 'start'
257
+ ? this.options.startName
258
+ : this.options.endName;
259
+ setAttrs(this.hidden[field], { name: name ?? null });
260
+ this.hidden[field].value = state.value[field];
261
+ this.hidden[field].disabled = name === undefined || disabled;
262
+ }
263
+ setAttrs(this.field, { 'data-ndp-theme': this.options.theme ?? null });
264
+ setAttrs(this.panel, {
265
+ 'data-ndp-theme': this.options.theme ?? null,
266
+ 'aria-label': locale.labels.chooseDate,
267
+ });
268
+ setAttrs(this.toggleButton, {
269
+ disabled,
270
+ 'aria-label': locale.labels.openCalendar,
271
+ 'aria-expanded': state.open ? 'true' : 'false',
272
+ });
273
+ this.hint.hidden = hintText === null;
274
+ setText(this.hint, hintText ?? '');
275
+ setAttrs(this.hint, { 'data-error': hintText !== null });
276
+ renderSegment(this.segment, state, locale);
277
+ this.presetList.render(state.presets, locale.labels.presets);
278
+ const fiscal = this.options.fiscalYearView ?? false;
279
+ this.fiscalButton.hidden = !fiscal;
280
+ setText(this.fiscalButton, locale.labels.fiscalYear);
281
+ setAttrs(this.fiscalButton, {
282
+ 'aria-pressed': state.calendarState.view === 'fiscal' ? 'true' : 'false',
283
+ });
284
+ this.footer.hidden = !fiscal && !state.canSwitchCalendar;
285
+ setPopupMode(
286
+ state,
287
+ [this.inputs.start, this.inputs.end],
288
+ this.toggleButton,
289
+ this.panel,
290
+ );
291
+ this.syncPanel(state.open && !state.inline);
292
+ this.panel.hidden = !state.inline && !this.wasOpen;
293
+ this.panel.style.setProperty(
294
+ '--ndp-panel-width',
295
+ state.inline ? `${String(this.panelWidth())}px` : null,
296
+ );
297
+ }
298
+
299
+ private panelWidth(): number {
300
+ return this.store.getState().presets.length > 0
301
+ ? PANEL_WIDTH_WITH_PRESETS
302
+ : PANEL_WIDTH;
303
+ }
304
+
305
+ private syncPanel(open: boolean): void {
306
+ if (open === this.wasOpen) {
307
+ return;
308
+ }
309
+ this.wasOpen = open;
310
+ this.options.onOpenChange?.(open);
311
+ if (open) {
312
+ const anchor = this.field.querySelector('.ndp-range') ?? this.field;
313
+ this.closePanel = openPanel({
314
+ panel: this.panel,
315
+ anchor,
316
+ inside: [this.field],
317
+ width: this.panelWidth(),
318
+ layout: this.options.panelLayout,
319
+ close: () => {
320
+ this.store.actions.close();
321
+ },
322
+ returnFocus: this.inputs.start,
323
+ });
324
+ this.calendarView.focusGrid();
325
+ } else {
326
+ this.closePanel?.();
327
+ this.closePanel = null;
328
+ }
329
+ }
330
+
331
+ private readonly onClick = (event: MouseEvent): void => {
332
+ const target = event.target instanceof Element ? event.target : null;
333
+ if (!target || this.calendarView.element.contains(target)) {
334
+ return;
335
+ }
336
+ const calendar = target.closest<HTMLElement>('[data-calendar]')?.dataset['calendar'];
337
+ if (calendar === 'BS' || calendar === 'AD') {
338
+ this.store.actions.switchCalendar(calendar);
339
+ return;
340
+ }
341
+ if (target.closest('[data-action="fiscal"]')) {
342
+ const calendar = this.store.calendar;
343
+ calendar.actions.setView(calendar.getState().view === 'fiscal' ? 'day' : 'fiscal');
344
+ this.calendarView.focusGrid();
345
+ return;
346
+ }
347
+ if (target.closest('[data-action="toggle"]')) {
348
+ if (this.store.getState().open) {
349
+ this.close();
350
+ } else {
351
+ this.open();
352
+ }
353
+ }
354
+ };
355
+ }
356
+
357
+ function storeOptions(
358
+ options: Partial<DateRangePickerViewOptions>,
359
+ ): Partial<DateRangePickerOptions> {
360
+ const out: Record<string, unknown> = {};
361
+ for (const [key, value] of Object.entries(options)) {
362
+ if (!VIEW_KEYS.has(key)) {
363
+ out[key] = value;
364
+ }
365
+ }
366
+
367
+ return out;
368
+ }
@@ -0,0 +1,27 @@
1
+ /** The tag names this package registers. */
2
+ export const ELEMENT_NAMES: {
3
+ readonly datePicker: 'nep-date-picker';
4
+ readonly calendar: 'nep-calendar';
5
+ readonly dateRangePicker: 'nep-date-range-picker';
6
+ readonly dateTimePicker: 'nep-date-time-picker';
7
+ readonly multiDatePicker: 'nep-multi-date-picker';
8
+ } = Object.freeze({
9
+ datePicker: 'nep-date-picker',
10
+ calendar: 'nep-calendar',
11
+ dateRangePicker: 'nep-date-range-picker',
12
+ dateTimePicker: 'nep-date-time-picker',
13
+ multiDatePicker: 'nep-multi-date-picker',
14
+ });
15
+
16
+ /** Define one element unless the name is taken. No-op during server rendering. */
17
+ export function defineElement(
18
+ name: string,
19
+ element: CustomElementConstructor,
20
+ registry?: CustomElementRegistry,
21
+ ): void {
22
+ const target =
23
+ registry ?? (typeof customElements === 'undefined' ? undefined : customElements);
24
+ if (target && !target.get(name)) {
25
+ target.define(name, element);
26
+ }
27
+ }