@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.
- package/LICENSE +21 -0
- package/README.md +123 -1
- package/dist/auto-define.cjs +11 -0
- package/dist/auto-define.cjs.map +1 -0
- package/dist/auto-define.d.cts +2 -0
- package/dist/auto-define.d.ts +2 -0
- package/dist/auto-define.js +9 -0
- package/dist/auto-define.js.map +1 -0
- package/dist/calendar-BAcO-jYq.js +142 -0
- package/dist/calendar-BAcO-jYq.js.map +1 -0
- package/dist/calendar-BNdN8qJv.d.ts +87 -0
- package/dist/calendar-C1LZI7XN.cjs +159 -0
- package/dist/calendar-C1LZI7XN.cjs.map +1 -0
- package/dist/calendar-QWPX0IfD.d.cts +87 -0
- package/dist/calendar.cjs +7 -0
- package/dist/calendar.d.cts +2 -0
- package/dist/calendar.d.ts +2 -0
- package/dist/calendar.js +3 -0
- package/dist/date-picker-BidCGOz0.d.cts +74 -0
- package/dist/date-picker-CQoabjlj.cjs +600 -0
- package/dist/date-picker-CQoabjlj.cjs.map +1 -0
- package/dist/date-picker-DyvpYe29.d.ts +74 -0
- package/dist/date-picker-ucoEFnMW.js +577 -0
- package/dist/date-picker-ucoEFnMW.js.map +1 -0
- package/dist/date-picker.cjs +6 -0
- package/dist/date-picker.d.cts +3 -0
- package/dist/date-picker.d.ts +3 -0
- package/dist/date-picker.js +1 -0
- package/dist/date-range-picker-BQKcZeqG.cjs +489 -0
- package/dist/date-range-picker-BQKcZeqG.cjs.map +1 -0
- package/dist/date-range-picker-D3eNvlgv.d.ts +98 -0
- package/dist/date-range-picker-DAkCzVgT.js +466 -0
- package/dist/date-range-picker-DAkCzVgT.js.map +1 -0
- package/dist/date-range-picker-SIl0vhUV.d.cts +98 -0
- package/dist/date-range-picker.cjs +6 -0
- package/dist/date-range-picker.d.cts +2 -0
- package/dist/date-range-picker.d.ts +2 -0
- package/dist/date-range-picker.js +1 -0
- package/dist/date-time-picker-BqweOP2u.js +487 -0
- package/dist/date-time-picker-BqweOP2u.js.map +1 -0
- package/dist/date-time-picker-Bxfn-a9T.cjs +510 -0
- package/dist/date-time-picker-Bxfn-a9T.cjs.map +1 -0
- package/dist/date-time-picker-BzSyQxrf.d.cts +92 -0
- package/dist/date-time-picker-DzDpzLMZ.d.ts +92 -0
- package/dist/date-time-picker.cjs +6 -0
- package/dist/date-time-picker.d.cts +2 -0
- package/dist/date-time-picker.d.ts +2 -0
- package/dist/date-time-picker.js +1 -0
- package/dist/define-7natoEH4.js +18 -0
- package/dist/define-7natoEH4.js.map +1 -0
- package/dist/define-B8NUA3yj.d.cts +16 -0
- package/dist/define-B8NUA3yj.d.ts +16 -0
- package/dist/define-DTTdLfIM.cjs +23 -0
- package/dist/define-DTTdLfIM.cjs.map +1 -0
- package/dist/element-utils-BVye8UQq.d.cts +82 -0
- package/dist/element-utils-C-r5gPlk.d.ts +82 -0
- package/dist/index.cjs +30 -0
- package/dist/index.d.cts +8 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +8 -0
- package/dist/multi-date-picker-B2mQbjZM.d.ts +105 -0
- package/dist/multi-date-picker-BMEYUPBa.cjs +462 -0
- package/dist/multi-date-picker-BMEYUPBa.cjs.map +1 -0
- package/dist/multi-date-picker-BUNf5zyJ.d.cts +105 -0
- package/dist/multi-date-picker-XCg8Y0sF.js +439 -0
- package/dist/multi-date-picker-XCg8Y0sF.js.map +1 -0
- package/dist/multi-date-picker.cjs +6 -0
- package/dist/multi-date-picker.d.cts +2 -0
- package/dist/multi-date-picker.d.ts +2 -0
- package/dist/multi-date-picker.js +1 -0
- package/dist/nepdate-picker.global.js +3 -0
- package/dist/nepdate-picker.global.js.map +1 -0
- package/dist/preset-list-CASwnwmS.js +45 -0
- package/dist/preset-list-CASwnwmS.js.map +1 -0
- package/dist/preset-list-Cs8H12Wp.cjs +50 -0
- package/dist/preset-list-Cs8H12Wp.cjs.map +1 -0
- package/dist/registry-3qzdVB7H.cjs +591 -0
- package/dist/registry-3qzdVB7H.cjs.map +1 -0
- package/dist/registry-BvkGnkvV.js +466 -0
- package/dist/registry-BvkGnkvV.js.map +1 -0
- package/package.json +159 -4
- package/src/auto-define.ts +6 -0
- package/src/calendar-element.ts +129 -0
- package/src/calendar-view.ts +406 -0
- package/src/calendar.ts +45 -0
- package/src/date-picker-element.ts +249 -0
- package/src/date-picker.ts +91 -0
- package/src/date-range-picker-element.ts +201 -0
- package/src/date-range-picker.ts +20 -0
- package/src/date-time-picker-element.ts +191 -0
- package/src/date-time-picker.ts +20 -0
- package/src/datetime-view.ts +459 -0
- package/src/define.ts +16 -0
- package/src/dom.ts +109 -0
- package/src/element-utils.ts +86 -0
- package/src/global.ts +7 -0
- package/src/index.ts +15 -0
- package/src/multi-date-picker-element.ts +187 -0
- package/src/multi-date-picker.ts +53 -0
- package/src/multi-view.ts +345 -0
- package/src/picker-view.ts +448 -0
- package/src/preset-list.ts +51 -0
- package/src/range-view.ts +393 -0
- package/src/registry.ts +27 -0
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
import { getLocale, type CalendarSystem } from '@nepdatejs/core';
|
|
2
|
+
import {
|
|
3
|
+
createDateTimePicker,
|
|
4
|
+
type DateTimePickerOptions,
|
|
5
|
+
type DateTimePickerState,
|
|
6
|
+
type DateTimePickerStore,
|
|
7
|
+
type TimeCell,
|
|
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 type { Theme } from './picker-view';
|
|
20
|
+
import {
|
|
21
|
+
PANEL_WIDTH,
|
|
22
|
+
PANEL_WIDTH_WITH_PRESETS,
|
|
23
|
+
PresetList,
|
|
24
|
+
TIME_COLUMNS_WIDTH,
|
|
25
|
+
} from './preset-list';
|
|
26
|
+
|
|
27
|
+
export interface DateTimePickerViewOptions extends DateTimePickerOptions {
|
|
28
|
+
readonly name?: string | undefined;
|
|
29
|
+
readonly hiddenInput?: boolean | undefined;
|
|
30
|
+
readonly required?: boolean | undefined;
|
|
31
|
+
readonly disabled?: boolean | undefined;
|
|
32
|
+
/** 'sheet' opens a bottom sheet; 'popover' sits beside the field; 'auto' (default) uses the sheet below 480px. */
|
|
33
|
+
readonly panelLayout?: PanelLayout | undefined;
|
|
34
|
+
readonly theme?: Theme | undefined;
|
|
35
|
+
readonly ariaLabel?: string | undefined;
|
|
36
|
+
readonly timeLabel?: string | undefined;
|
|
37
|
+
readonly id?: string | undefined;
|
|
38
|
+
readonly onOpenChange?: ((open: boolean) => void) | undefined;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const VIEW_KEYS = new Set([
|
|
42
|
+
'name',
|
|
43
|
+
'hiddenInput',
|
|
44
|
+
'required',
|
|
45
|
+
'disabled',
|
|
46
|
+
'theme',
|
|
47
|
+
'panelLayout',
|
|
48
|
+
'ariaLabel',
|
|
49
|
+
'timeLabel',
|
|
50
|
+
'id',
|
|
51
|
+
'onOpenChange',
|
|
52
|
+
'onChange',
|
|
53
|
+
]);
|
|
54
|
+
|
|
55
|
+
/** A date input and a time input sharing one panel: calendar plus hour/minute columns. */
|
|
56
|
+
export class DateTimePickerView {
|
|
57
|
+
readonly store: DateTimePickerStore;
|
|
58
|
+
readonly dateInput: HTMLInputElement;
|
|
59
|
+
readonly timeInput: HTMLInputElement;
|
|
60
|
+
private options: DateTimePickerViewOptions;
|
|
61
|
+
private readonly field: HTMLDivElement;
|
|
62
|
+
private readonly row: HTMLDivElement;
|
|
63
|
+
private readonly dateBox: HTMLDivElement;
|
|
64
|
+
private readonly timeBox: HTMLDivElement;
|
|
65
|
+
private readonly toggleButton: HTMLButtonElement;
|
|
66
|
+
private readonly hint: HTMLParagraphElement;
|
|
67
|
+
private readonly panel: HTMLDivElement;
|
|
68
|
+
private readonly segment: HTMLDivElement;
|
|
69
|
+
private readonly nowButton: HTMLButtonElement;
|
|
70
|
+
private readonly doneButton: HTMLButtonElement;
|
|
71
|
+
private readonly hours: HTMLDivElement;
|
|
72
|
+
private readonly minutes: HTMLDivElement;
|
|
73
|
+
private readonly time: HTMLDivElement;
|
|
74
|
+
private readonly hidden: HTMLInputElement;
|
|
75
|
+
private readonly calendarView: CalendarView;
|
|
76
|
+
private readonly presetList: PresetList;
|
|
77
|
+
private readonly unsubscribe: () => void;
|
|
78
|
+
private closePanel: (() => void) | null = null;
|
|
79
|
+
private wasOpen = false;
|
|
80
|
+
|
|
81
|
+
constructor(host: HTMLElement, options: DateTimePickerViewOptions = {}) {
|
|
82
|
+
this.options = { ...options };
|
|
83
|
+
this.store = createDateTimePicker({
|
|
84
|
+
...storeOptions(options),
|
|
85
|
+
onChange: (value, detail) => {
|
|
86
|
+
this.options.onChange?.(value, detail);
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
const id = options.id ?? uid('ndp-datetime');
|
|
90
|
+
this.dateInput = el('input', {
|
|
91
|
+
class: 'ndp-input__control',
|
|
92
|
+
id,
|
|
93
|
+
type: 'text',
|
|
94
|
+
inputmode: 'numeric',
|
|
95
|
+
autocomplete: 'off',
|
|
96
|
+
'aria-controls': `${id}-panel`,
|
|
97
|
+
});
|
|
98
|
+
this.timeInput = el('input', {
|
|
99
|
+
class: 'ndp-input__control',
|
|
100
|
+
id: `${id}-time`,
|
|
101
|
+
type: 'text',
|
|
102
|
+
autocomplete: 'off',
|
|
103
|
+
});
|
|
104
|
+
this.toggleButton = el(
|
|
105
|
+
'button',
|
|
106
|
+
{ type: 'button', class: 'ndp-icon-button', 'data-action': 'toggle' },
|
|
107
|
+
[icon('calendar')],
|
|
108
|
+
);
|
|
109
|
+
this.dateBox = el('div', { class: 'ndp-input' }, [
|
|
110
|
+
this.dateInput,
|
|
111
|
+
el('span', { class: 'ndp-input__actions' }, [this.toggleButton]),
|
|
112
|
+
]);
|
|
113
|
+
this.timeBox = el('div', { class: 'ndp-input ndp-input--time' }, [this.timeInput]);
|
|
114
|
+
this.row = el('div', { class: 'ndp-datetime' }, [this.dateBox, this.timeBox]);
|
|
115
|
+
this.hint = el('p', {
|
|
116
|
+
class: 'ndp-hint',
|
|
117
|
+
id: `${id}-hint`,
|
|
118
|
+
'aria-live': 'polite',
|
|
119
|
+
hidden: true,
|
|
120
|
+
});
|
|
121
|
+
this.hidden = el('input', { type: 'hidden' });
|
|
122
|
+
this.segment = el('div', { class: 'ndp-segment', role: 'group' }, [
|
|
123
|
+
el('button', { type: 'button', class: 'ndp-segment__item', 'data-calendar': 'BS' }),
|
|
124
|
+
el('button', { type: 'button', class: 'ndp-segment__item', 'data-calendar': 'AD' }),
|
|
125
|
+
]);
|
|
126
|
+
this.nowButton = el('button', {
|
|
127
|
+
type: 'button',
|
|
128
|
+
class: 'ndp-button',
|
|
129
|
+
'data-action': 'now',
|
|
130
|
+
});
|
|
131
|
+
this.doneButton = el('button', {
|
|
132
|
+
type: 'button',
|
|
133
|
+
class: 'ndp-button',
|
|
134
|
+
'data-variant': 'primary',
|
|
135
|
+
'data-action': 'done',
|
|
136
|
+
});
|
|
137
|
+
this.hours = el('div', {
|
|
138
|
+
class: 'ndp-time__column',
|
|
139
|
+
role: 'group',
|
|
140
|
+
'data-unit': 'hour',
|
|
141
|
+
});
|
|
142
|
+
this.minutes = el('div', {
|
|
143
|
+
class: 'ndp-time__column',
|
|
144
|
+
role: 'group',
|
|
145
|
+
'data-unit': 'minute',
|
|
146
|
+
});
|
|
147
|
+
this.time = el('div', { class: 'ndp-time', role: 'group' }, [
|
|
148
|
+
this.hours,
|
|
149
|
+
this.minutes,
|
|
150
|
+
]);
|
|
151
|
+
const calendarRoot = el('div', { class: 'ndp-panel__calendar' });
|
|
152
|
+
this.presetList = new PresetList((presetId) => {
|
|
153
|
+
this.store.actions.applyPreset(presetId);
|
|
154
|
+
});
|
|
155
|
+
this.panel = el(
|
|
156
|
+
'div',
|
|
157
|
+
{
|
|
158
|
+
class: 'ndp-panel',
|
|
159
|
+
id: `${id}-panel`,
|
|
160
|
+
|
|
161
|
+
hidden: true,
|
|
162
|
+
},
|
|
163
|
+
[
|
|
164
|
+
el('div', { class: 'ndp-panel__body' }, [
|
|
165
|
+
this.presetList.element,
|
|
166
|
+
calendarRoot,
|
|
167
|
+
this.time,
|
|
168
|
+
]),
|
|
169
|
+
el('div', { class: 'ndp-panel__footer' }, [
|
|
170
|
+
this.segment,
|
|
171
|
+
el('span', {}, [this.nowButton, this.doneButton]),
|
|
172
|
+
]),
|
|
173
|
+
],
|
|
174
|
+
);
|
|
175
|
+
this.field = el('div', { class: 'ndp-field' }, [
|
|
176
|
+
this.row,
|
|
177
|
+
this.hint,
|
|
178
|
+
this.panel,
|
|
179
|
+
this.hidden,
|
|
180
|
+
]);
|
|
181
|
+
host.append(this.field);
|
|
182
|
+
this.calendarView = new CalendarView(calendarRoot, this.store.calendar, {
|
|
183
|
+
locale: () => this.options.locale,
|
|
184
|
+
onDayActivate: (date) => {
|
|
185
|
+
this.store.actions.selectDate(date);
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
this.dateInput.addEventListener('input', () => {
|
|
190
|
+
this.store.actions.typeDate(this.dateInput.value);
|
|
191
|
+
});
|
|
192
|
+
this.dateInput.addEventListener('blur', () => {
|
|
193
|
+
this.store.actions.blurDate();
|
|
194
|
+
});
|
|
195
|
+
this.dateInput.addEventListener('keydown', (event) => {
|
|
196
|
+
if (event.key === 'ArrowDown' && (event.altKey || this.dateInput.value === '')) {
|
|
197
|
+
event.preventDefault();
|
|
198
|
+
this.open();
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
this.timeInput.addEventListener('input', () => {
|
|
202
|
+
this.store.actions.typeTime(this.timeInput.value);
|
|
203
|
+
});
|
|
204
|
+
this.timeInput.addEventListener('blur', () => {
|
|
205
|
+
this.store.actions.blurTime();
|
|
206
|
+
});
|
|
207
|
+
this.field.addEventListener('click', this.onClick);
|
|
208
|
+
this.unsubscribe = this.store.subscribe(() => {
|
|
209
|
+
this.render();
|
|
210
|
+
});
|
|
211
|
+
this.render();
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
getState(): DateTimePickerState {
|
|
215
|
+
return this.store.getState();
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
setOptions(changes: Partial<DateTimePickerViewOptions>): void {
|
|
219
|
+
this.options = { ...this.options, ...changes };
|
|
220
|
+
const forStore = storeOptions(changes);
|
|
221
|
+
if (Object.keys(forStore).length > 0) {
|
|
222
|
+
this.store.setOptions(forStore);
|
|
223
|
+
} else {
|
|
224
|
+
this.render();
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
open(): void {
|
|
229
|
+
if (!this.options.disabled) {
|
|
230
|
+
this.store.actions.open();
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
close(): void {
|
|
235
|
+
this.store.actions.close();
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
destroy(): void {
|
|
239
|
+
this.closePanel?.();
|
|
240
|
+
this.unsubscribe();
|
|
241
|
+
this.calendarView.destroy();
|
|
242
|
+
this.field.remove();
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
private render(): void {
|
|
246
|
+
const state = this.store.getState();
|
|
247
|
+
const locale = getLocale(this.options.locale);
|
|
248
|
+
const disabled = this.options.disabled ?? false;
|
|
249
|
+
const hint = state.error?.message ?? state.equivalentText;
|
|
250
|
+
|
|
251
|
+
if (this.dateInput.value !== state.dateText) {
|
|
252
|
+
this.dateInput.value = state.dateText;
|
|
253
|
+
}
|
|
254
|
+
if (this.timeInput.value !== state.timeText) {
|
|
255
|
+
this.timeInput.value = state.timeText;
|
|
256
|
+
}
|
|
257
|
+
this.dateInput.setCustomValidity(
|
|
258
|
+
state.error?.field === 'date' ? state.error.message : '',
|
|
259
|
+
);
|
|
260
|
+
this.timeInput.setCustomValidity(
|
|
261
|
+
state.error?.field === 'time' ? state.error.message : '',
|
|
262
|
+
);
|
|
263
|
+
setAttrs(this.dateInput, {
|
|
264
|
+
placeholder: state.placeholder,
|
|
265
|
+
disabled,
|
|
266
|
+
required: this.options.required ?? false,
|
|
267
|
+
'aria-label': this.options.ariaLabel ?? null,
|
|
268
|
+
'aria-invalid': state.error?.field === 'date' ? 'true' : null,
|
|
269
|
+
'aria-describedby': hint ? this.hint.id : null,
|
|
270
|
+
});
|
|
271
|
+
setAttrs(this.timeInput, {
|
|
272
|
+
placeholder: state.timePlaceholder,
|
|
273
|
+
disabled,
|
|
274
|
+
'aria-label': this.options.timeLabel ?? locale.labels.time,
|
|
275
|
+
'aria-invalid': state.error?.field === 'time' ? 'true' : null,
|
|
276
|
+
'aria-describedby': hint ? this.hint.id : null,
|
|
277
|
+
});
|
|
278
|
+
setAttrs(this.dateBox, {
|
|
279
|
+
'data-invalid': state.error?.field === 'date',
|
|
280
|
+
'data-disabled': disabled,
|
|
281
|
+
});
|
|
282
|
+
setAttrs(this.timeBox, {
|
|
283
|
+
'data-invalid': state.error?.field === 'time',
|
|
284
|
+
'data-disabled': disabled,
|
|
285
|
+
});
|
|
286
|
+
setAttrs(this.field, { 'data-ndp-theme': this.options.theme ?? null });
|
|
287
|
+
setAttrs(this.panel, {
|
|
288
|
+
'data-ndp-theme': this.options.theme ?? null,
|
|
289
|
+
'aria-label': locale.labels.chooseDate,
|
|
290
|
+
});
|
|
291
|
+
setAttrs(this.toggleButton, {
|
|
292
|
+
disabled,
|
|
293
|
+
'aria-label': locale.labels.openCalendar,
|
|
294
|
+
'aria-expanded': state.open ? 'true' : 'false',
|
|
295
|
+
});
|
|
296
|
+
this.hint.hidden = hint === null;
|
|
297
|
+
setText(this.hint, hint ?? '');
|
|
298
|
+
setAttrs(this.hint, { 'data-error': state.error !== null });
|
|
299
|
+
|
|
300
|
+
this.segment.hidden = !state.canSwitchCalendar;
|
|
301
|
+
setAttrs(this.segment, { 'aria-label': locale.labels.calendar });
|
|
302
|
+
for (const button of this.segment.querySelectorAll<HTMLButtonElement>('button')) {
|
|
303
|
+
const calendar = button.dataset['calendar'] as CalendarSystem;
|
|
304
|
+
setText(button, locale.calendarNames[calendar]);
|
|
305
|
+
setAttrs(button, {
|
|
306
|
+
'aria-pressed': calendar === state.calendar ? 'true' : 'false',
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
setText(this.nowButton, locale.labels.now);
|
|
310
|
+
setText(this.doneButton, locale.labels.done);
|
|
311
|
+
setAttrs(this.time, { 'aria-label': locale.labels.time });
|
|
312
|
+
setAttrs(this.hours, { 'aria-label': locale.labels.hour });
|
|
313
|
+
setAttrs(this.minutes, { 'aria-label': locale.labels.minute });
|
|
314
|
+
renderColumn(this.hours, state.hours);
|
|
315
|
+
renderColumn(this.minutes, state.minutes);
|
|
316
|
+
this.presetList.render(state.presets, locale.labels.presets);
|
|
317
|
+
|
|
318
|
+
const name = this.options.hiddenInput === false ? undefined : this.options.name;
|
|
319
|
+
setAttrs(this.hidden, { name: name ?? null });
|
|
320
|
+
this.hidden.value = state.value;
|
|
321
|
+
this.hidden.disabled = name === undefined || disabled;
|
|
322
|
+
setPopupMode(state, [this.dateInput], this.toggleButton, this.panel);
|
|
323
|
+
this.syncPanel(state.open && !state.inline);
|
|
324
|
+
this.panel.hidden = !state.inline && !this.wasOpen;
|
|
325
|
+
this.panel.style.setProperty(
|
|
326
|
+
'--ndp-panel-width',
|
|
327
|
+
state.inline ? `${String(this.panelWidth())}px` : null,
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
private panelWidth(): number {
|
|
332
|
+
return (
|
|
333
|
+
(this.store.getState().presets.length > 0
|
|
334
|
+
? PANEL_WIDTH_WITH_PRESETS
|
|
335
|
+
: PANEL_WIDTH) + TIME_COLUMNS_WIDTH
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
private syncPanel(open: boolean): void {
|
|
340
|
+
if (open === this.wasOpen) {
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
this.wasOpen = open;
|
|
344
|
+
this.options.onOpenChange?.(open);
|
|
345
|
+
if (open) {
|
|
346
|
+
this.panel.hidden = false;
|
|
347
|
+
const stopTracking = trackPosition(this.row, this.panel, {
|
|
348
|
+
width: this.panelWidth(),
|
|
349
|
+
layout: this.options.panelLayout,
|
|
350
|
+
});
|
|
351
|
+
const stopOutside = onOutsidePointer([this.field], () => {
|
|
352
|
+
this.store.actions.close();
|
|
353
|
+
});
|
|
354
|
+
const stopBackdrop = onBackdropPointer(this.panel, () => {
|
|
355
|
+
this.store.actions.close();
|
|
356
|
+
});
|
|
357
|
+
const stopEscape = onEscape(() => {
|
|
358
|
+
this.store.actions.close();
|
|
359
|
+
restoreFocus(this.dateInput);
|
|
360
|
+
});
|
|
361
|
+
this.closePanel = () => {
|
|
362
|
+
stopTracking();
|
|
363
|
+
stopOutside();
|
|
364
|
+
stopBackdrop();
|
|
365
|
+
stopEscape();
|
|
366
|
+
this.panel.hidden = true;
|
|
367
|
+
};
|
|
368
|
+
for (const item of this.time.querySelectorAll<HTMLElement>(
|
|
369
|
+
'[aria-pressed="true"]',
|
|
370
|
+
)) {
|
|
371
|
+
if (typeof item.scrollIntoView === 'function') {
|
|
372
|
+
item.scrollIntoView({ block: 'nearest' });
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
this.calendarView.focusGrid();
|
|
376
|
+
} else {
|
|
377
|
+
this.closePanel?.();
|
|
378
|
+
this.closePanel = null;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
private readonly onClick = (event: MouseEvent): void => {
|
|
383
|
+
const target = event.target instanceof Element ? event.target : null;
|
|
384
|
+
if (!target || this.calendarView.element.contains(target)) {
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
const item = target.closest<HTMLButtonElement>('.ndp-time-item');
|
|
388
|
+
if (item) {
|
|
389
|
+
const unit = item.parentElement?.dataset['unit'];
|
|
390
|
+
const value = Number(item.dataset['value']);
|
|
391
|
+
if (unit === 'hour') {
|
|
392
|
+
this.store.actions.selectHour(value);
|
|
393
|
+
} else {
|
|
394
|
+
this.store.actions.selectMinute(value);
|
|
395
|
+
}
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
const calendar = target.closest<HTMLElement>('[data-calendar]')?.dataset['calendar'];
|
|
399
|
+
if (calendar === 'BS' || calendar === 'AD') {
|
|
400
|
+
this.store.actions.switchCalendar(calendar);
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
switch (target.closest<HTMLElement>('[data-action]')?.dataset['action']) {
|
|
404
|
+
case 'toggle':
|
|
405
|
+
if (this.store.getState().open) {
|
|
406
|
+
this.close();
|
|
407
|
+
} else {
|
|
408
|
+
this.open();
|
|
409
|
+
}
|
|
410
|
+
break;
|
|
411
|
+
case 'now':
|
|
412
|
+
this.store.actions.selectNow();
|
|
413
|
+
break;
|
|
414
|
+
case 'done':
|
|
415
|
+
this.close();
|
|
416
|
+
this.dateInput.focus();
|
|
417
|
+
break;
|
|
418
|
+
default:
|
|
419
|
+
break;
|
|
420
|
+
}
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function renderColumn(column: HTMLElement, cells: readonly TimeCell[]): void {
|
|
425
|
+
if (column.children.length !== cells.length) {
|
|
426
|
+
column.replaceChildren(
|
|
427
|
+
...cells.map((cell) =>
|
|
428
|
+
el('button', {
|
|
429
|
+
type: 'button',
|
|
430
|
+
class: 'ndp-time-item',
|
|
431
|
+
'data-value': cell.value,
|
|
432
|
+
}),
|
|
433
|
+
),
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
cells.forEach((cell, index) => {
|
|
437
|
+
const button = column.children[index];
|
|
438
|
+
if (button) {
|
|
439
|
+
setText(button, cell.label);
|
|
440
|
+
setAttrs(button, {
|
|
441
|
+
'data-value': cell.value,
|
|
442
|
+
'aria-pressed': cell.isSelected ? 'true' : 'false',
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function storeOptions(
|
|
449
|
+
options: Partial<DateTimePickerViewOptions>,
|
|
450
|
+
): Partial<DateTimePickerOptions> {
|
|
451
|
+
const out: Record<string, unknown> = {};
|
|
452
|
+
for (const [key, value] of Object.entries(options)) {
|
|
453
|
+
if (!VIEW_KEYS.has(key)) {
|
|
454
|
+
out[key] = value;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
return out;
|
|
459
|
+
}
|
package/src/define.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { defineCalendar } from './calendar';
|
|
2
|
+
import { defineDatePicker } from './date-picker';
|
|
3
|
+
import { defineDateRangePicker } from './date-range-picker';
|
|
4
|
+
import { defineDateTimePicker } from './date-time-picker';
|
|
5
|
+
import { defineMultiDatePicker } from './multi-date-picker';
|
|
6
|
+
|
|
7
|
+
export { ELEMENT_NAMES } from './registry';
|
|
8
|
+
|
|
9
|
+
/** Register all five custom elements (skips any already defined). Safe to call more than once. */
|
|
10
|
+
export function defineElements(registry?: CustomElementRegistry): void {
|
|
11
|
+
defineDatePicker(registry);
|
|
12
|
+
defineCalendar(registry);
|
|
13
|
+
defineDateRangePicker(registry);
|
|
14
|
+
defineDateTimePicker(registry);
|
|
15
|
+
defineMultiDatePicker(registry);
|
|
16
|
+
}
|
package/src/dom.ts
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/** Tiny DOM builders. Text goes through textContent; nothing user-supplied becomes HTML. */
|
|
2
|
+
|
|
3
|
+
type Attrs = Record<string, string | number | boolean | null | undefined>;
|
|
4
|
+
|
|
5
|
+
export function el<K extends keyof HTMLElementTagNameMap>(
|
|
6
|
+
tag: K,
|
|
7
|
+
attrs: Attrs = {},
|
|
8
|
+
children: (Node | string)[] = [],
|
|
9
|
+
): HTMLElementTagNameMap[K] {
|
|
10
|
+
const node = document.createElement(tag);
|
|
11
|
+
setAttrs(node, attrs);
|
|
12
|
+
node.append(...children);
|
|
13
|
+
|
|
14
|
+
return node;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Set, update or remove attributes: false/null/undefined removes, true sets "". */
|
|
18
|
+
export function setAttrs(node: Element, attrs: Attrs): void {
|
|
19
|
+
for (const [name, value] of Object.entries(attrs)) {
|
|
20
|
+
if (value === false || value === null || value === undefined) {
|
|
21
|
+
node.removeAttribute(name);
|
|
22
|
+
} else {
|
|
23
|
+
const text = value === true ? '' : String(value);
|
|
24
|
+
if (node.getAttribute(name) !== text) {
|
|
25
|
+
node.setAttribute(name, text);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function setText(node: Node, text: string): void {
|
|
32
|
+
if (node.textContent !== text) {
|
|
33
|
+
node.textContent = text;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
let counter = 0;
|
|
38
|
+
/** A document-unique id for aria-controls / aria-describedby. */
|
|
39
|
+
export function uid(prefix: string): string {
|
|
40
|
+
counter += 1;
|
|
41
|
+
|
|
42
|
+
return `${prefix}-${counter}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const SVG = 'http://www.w3.org/2000/svg';
|
|
46
|
+
const PATHS: Record<IconName, string[]> = {
|
|
47
|
+
'chevron-left': ['M10 3.5 5.5 8l4.5 4.5'],
|
|
48
|
+
'chevron-right': ['M6 3.5 10.5 8 6 12.5'],
|
|
49
|
+
calendar: [
|
|
50
|
+
'M2 6.5h12',
|
|
51
|
+
'M5.5 1.5v3',
|
|
52
|
+
'M10.5 1.5v3',
|
|
53
|
+
'M4 3h8a2 2 0 0 1 2 2v7a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2Z',
|
|
54
|
+
],
|
|
55
|
+
close: ['M4 4l8 8', 'M12 4l-8 8'],
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export type IconName = 'chevron-left' | 'chevron-right' | 'calendar' | 'close';
|
|
59
|
+
|
|
60
|
+
/** The Figma icon set at 16px, drawn with currentColor. */
|
|
61
|
+
export function icon(name: IconName): SVGSVGElement {
|
|
62
|
+
const svg = document.createElementNS(SVG, 'svg');
|
|
63
|
+
svg.setAttribute('viewBox', '0 0 16 16');
|
|
64
|
+
svg.setAttribute('fill', 'none');
|
|
65
|
+
svg.setAttribute('stroke', 'currentColor');
|
|
66
|
+
svg.setAttribute('stroke-width', '1.5');
|
|
67
|
+
svg.setAttribute('stroke-linecap', 'round');
|
|
68
|
+
svg.setAttribute('stroke-linejoin', 'round');
|
|
69
|
+
svg.setAttribute('aria-hidden', 'true');
|
|
70
|
+
svg.setAttribute('focusable', 'false');
|
|
71
|
+
for (const d of PATHS[name]) {
|
|
72
|
+
const path = document.createElementNS(SVG, 'path');
|
|
73
|
+
path.setAttribute('d', d);
|
|
74
|
+
svg.append(path);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return svg;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Popup or inline: an inline picker's calendar is always in the page, so its
|
|
82
|
+
* inputs are plain text fields, its toggle is hidden and its panel is a group
|
|
83
|
+
* in normal flow rather than a top-layer dialog.
|
|
84
|
+
*/
|
|
85
|
+
export function setPopupMode(
|
|
86
|
+
state: { readonly open: boolean; readonly inline: boolean },
|
|
87
|
+
inputs: readonly HTMLElement[],
|
|
88
|
+
toggle: HTMLElement,
|
|
89
|
+
panel: HTMLElement,
|
|
90
|
+
): void {
|
|
91
|
+
const { inline } = state;
|
|
92
|
+
for (const input of inputs) {
|
|
93
|
+
setAttrs(input, {
|
|
94
|
+
role: inline ? null : 'combobox',
|
|
95
|
+
'aria-haspopup': inline ? null : 'dialog',
|
|
96
|
+
'aria-expanded': inline ? null : String(state.open),
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
toggle.hidden = inline;
|
|
100
|
+
setAttrs(panel, {
|
|
101
|
+
role: inline ? 'group' : 'dialog',
|
|
102
|
+
popover: inline ? null : 'manual',
|
|
103
|
+
});
|
|
104
|
+
if (inline) {
|
|
105
|
+
panel.dataset['layout'] = 'inline';
|
|
106
|
+
} else if (panel.dataset['layout'] === 'inline') {
|
|
107
|
+
delete panel.dataset['layout'];
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/** Attribute parsing and shared helpers for the custom elements. */
|
|
2
|
+
import {
|
|
3
|
+
toIsoDate,
|
|
4
|
+
type CalendarDate,
|
|
5
|
+
type CalendarSystem,
|
|
6
|
+
type Weekday,
|
|
7
|
+
} from '@nepdatejs/core';
|
|
8
|
+
import type { EventMarker, HolidayInput, HolidayLookup } from '@nepdatejs/calendar';
|
|
9
|
+
import type { PanelLayout } from '@nepdatejs/dom';
|
|
10
|
+
import type { Theme } from './picker-view';
|
|
11
|
+
|
|
12
|
+
/** Lets this module load during server rendering, where HTMLElement does not exist. */
|
|
13
|
+
export const Base: typeof HTMLElement =
|
|
14
|
+
typeof HTMLElement === 'undefined'
|
|
15
|
+
? (Object as unknown as typeof HTMLElement)
|
|
16
|
+
: HTMLElement;
|
|
17
|
+
|
|
18
|
+
/** Options that cannot be attributes: functions and lists. Set them as properties. */
|
|
19
|
+
export interface RichProps {
|
|
20
|
+
holidays?: readonly HolidayInput[] | HolidayLookup | undefined;
|
|
21
|
+
events?: ((date: CalendarDate) => readonly EventMarker[]) | undefined;
|
|
22
|
+
isDateDisabled?: ((date: CalendarDate) => boolean) | undefined;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function str(element: Element, name: string): string | undefined {
|
|
26
|
+
return element.getAttribute(name) ?? undefined;
|
|
27
|
+
}
|
|
28
|
+
export function bool(element: Element, name: string): boolean {
|
|
29
|
+
return element.hasAttribute(name);
|
|
30
|
+
}
|
|
31
|
+
/** An attribute that is on unless set to "false". */
|
|
32
|
+
export function onUnlessFalse(element: Element, name: string): boolean | undefined {
|
|
33
|
+
const value = element.getAttribute(name);
|
|
34
|
+
return value === null ? undefined : value !== 'false';
|
|
35
|
+
}
|
|
36
|
+
export function calendarAttr(element: Element, name: string): CalendarSystem | undefined {
|
|
37
|
+
const value = element.getAttribute(name)?.toUpperCase();
|
|
38
|
+
return value === 'BS' || value === 'AD' ? value : undefined;
|
|
39
|
+
}
|
|
40
|
+
export function weekday(element: Element, name: string): Weekday | undefined {
|
|
41
|
+
const value = Number(element.getAttribute(name) ?? NaN);
|
|
42
|
+
return Number.isInteger(value) && value >= 0 && value <= 6
|
|
43
|
+
? (value as Weekday)
|
|
44
|
+
: undefined;
|
|
45
|
+
}
|
|
46
|
+
export function int(element: Element, name: string): number | undefined {
|
|
47
|
+
const value = Number(element.getAttribute(name) ?? NaN);
|
|
48
|
+
return Number.isInteger(value) ? value : undefined;
|
|
49
|
+
}
|
|
50
|
+
export function theme(element: Element): Theme | undefined {
|
|
51
|
+
const value = element.getAttribute('theme');
|
|
52
|
+
return value === 'light' || value === 'dark' || value === 'auto' ? value : undefined;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function panelLayout(element: Element): PanelLayout | undefined {
|
|
56
|
+
const value = element.getAttribute('panel-layout');
|
|
57
|
+
return value === 'auto' || value === 'popover' || value === 'sheet' ? value : undefined;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** presets="today, last7Days" → ['today', 'last7Days']; unknown ids show as disabled. */
|
|
61
|
+
export function presetIds(element: Element): readonly string[] | undefined {
|
|
62
|
+
const value = element.getAttribute('presets');
|
|
63
|
+
return value === null
|
|
64
|
+
? undefined
|
|
65
|
+
: value
|
|
66
|
+
.split(',')
|
|
67
|
+
.map((id) => id.trim())
|
|
68
|
+
.filter(Boolean);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Stop the inner inputs' native change events: the element fires its own. */
|
|
72
|
+
export function swallowInnerChange(host: HTMLElement): void {
|
|
73
|
+
host.addEventListener(
|
|
74
|
+
'change',
|
|
75
|
+
(event) => {
|
|
76
|
+
if (event.target !== host) {
|
|
77
|
+
event.stopPropagation();
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
true,
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function isoOf(date: CalendarDate | null): string {
|
|
85
|
+
return date ? toIsoDate(date) : '';
|
|
86
|
+
}
|
package/src/global.ts
ADDED
package/src/index.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @nepdatejs/vanilla: the BS/AD date picker and calendar for plain JavaScript,
|
|
3
|
+
* as custom elements and as a small imperative API. No side effects; import
|
|
4
|
+
* '@nepdatejs/vanilla/define' (or call defineElements) to register the elements.
|
|
5
|
+
*
|
|
6
|
+
* Each element also has its own entry, so a page that needs one picker ships
|
|
7
|
+
* only that one: '@nepdatejs/vanilla/date-picker', '/calendar',
|
|
8
|
+
* '/date-range-picker', '/date-time-picker' and '/multi-date-picker'.
|
|
9
|
+
*/
|
|
10
|
+
export * from './date-picker';
|
|
11
|
+
export * from './calendar';
|
|
12
|
+
export * from './date-range-picker';
|
|
13
|
+
export * from './date-time-picker';
|
|
14
|
+
export * from './multi-date-picker';
|
|
15
|
+
export { defineElements, ELEMENT_NAMES } from './define';
|