@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,406 @@
|
|
|
1
|
+
import { getLocale, type CalendarDate, type LocaleCode } from '@nepdatejs/core';
|
|
2
|
+
import type {
|
|
3
|
+
CalendarState,
|
|
4
|
+
CalendarStore,
|
|
5
|
+
DayCell,
|
|
6
|
+
FocusKey,
|
|
7
|
+
PickerCell,
|
|
8
|
+
} from '@nepdatejs/calendar';
|
|
9
|
+
import { el, icon, setAttrs, setText } from './dom';
|
|
10
|
+
|
|
11
|
+
const FOCUS_KEYS: ReadonlySet<string> = new Set([
|
|
12
|
+
'ArrowLeft',
|
|
13
|
+
'ArrowRight',
|
|
14
|
+
'ArrowUp',
|
|
15
|
+
'ArrowDown',
|
|
16
|
+
'Home',
|
|
17
|
+
'End',
|
|
18
|
+
'PageUp',
|
|
19
|
+
'PageDown',
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
export interface CalendarViewOptions {
|
|
23
|
+
/** The locale for labels; read on every render so a change applies at once. */
|
|
24
|
+
readonly locale?: (() => LocaleCode | undefined) | undefined;
|
|
25
|
+
/** Called for a day click or Enter/Space; defaults to store.actions.select. */
|
|
26
|
+
readonly onDayActivate?: ((date: CalendarDate) => void) | undefined;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Renders a calendar store into `root` with the @nepdatejs/styles markup and
|
|
31
|
+
* keeps it in step. Elements are updated in place, so a click or focus never
|
|
32
|
+
* lands on a replaced button; only switching view or row count rebuilds the body.
|
|
33
|
+
*/
|
|
34
|
+
export class CalendarView {
|
|
35
|
+
readonly element: HTMLDivElement;
|
|
36
|
+
private readonly store: CalendarStore;
|
|
37
|
+
private readonly options: CalendarViewOptions;
|
|
38
|
+
private readonly prev: HTMLButtonElement;
|
|
39
|
+
private readonly next: HTMLButtonElement;
|
|
40
|
+
private readonly title: HTMLDivElement;
|
|
41
|
+
private readonly body: HTMLDivElement;
|
|
42
|
+
private bodyKey = '';
|
|
43
|
+
private cells: (DayCell | null)[] = [];
|
|
44
|
+
private tiles: PickerCell[] = [];
|
|
45
|
+
private focusAfterRender = false;
|
|
46
|
+
private readonly unsubscribe: () => void;
|
|
47
|
+
|
|
48
|
+
constructor(
|
|
49
|
+
root: HTMLElement,
|
|
50
|
+
store: CalendarStore,
|
|
51
|
+
options: CalendarViewOptions = {},
|
|
52
|
+
) {
|
|
53
|
+
this.store = store;
|
|
54
|
+
this.options = options;
|
|
55
|
+
this.prev = el(
|
|
56
|
+
'button',
|
|
57
|
+
{ type: 'button', class: 'ndp-icon-button', 'data-action': 'prev' },
|
|
58
|
+
[icon('chevron-left')],
|
|
59
|
+
);
|
|
60
|
+
this.next = el(
|
|
61
|
+
'button',
|
|
62
|
+
{ type: 'button', class: 'ndp-icon-button', 'data-action': 'next' },
|
|
63
|
+
[icon('chevron-right')],
|
|
64
|
+
);
|
|
65
|
+
this.title = el('div', { class: 'ndp-calendar__title', 'aria-live': 'polite' });
|
|
66
|
+
this.body = el('div');
|
|
67
|
+
this.element = el('div', { class: 'ndp-calendar' }, [
|
|
68
|
+
el('div', { class: 'ndp-calendar__header' }, [this.prev, this.title, this.next]),
|
|
69
|
+
this.body,
|
|
70
|
+
]);
|
|
71
|
+
this.element.addEventListener('click', this.onClick);
|
|
72
|
+
this.element.addEventListener('keydown', this.onKeyDown);
|
|
73
|
+
this.element.addEventListener('mouseover', this.onMouseOver);
|
|
74
|
+
this.element.addEventListener('mouseleave', this.onMouseLeave);
|
|
75
|
+
this.element.addEventListener('focusin', this.onFocusIn);
|
|
76
|
+
root.append(this.element);
|
|
77
|
+
this.unsubscribe = store.subscribe(() => {
|
|
78
|
+
this.render();
|
|
79
|
+
});
|
|
80
|
+
this.render();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Move keyboard focus to the day that should have it. */
|
|
84
|
+
focusGrid(): void {
|
|
85
|
+
this.element
|
|
86
|
+
.querySelector<HTMLElement>('.ndp-day[tabindex="0"], .ndp-option[tabindex="0"]')
|
|
87
|
+
?.focus();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
destroy(): void {
|
|
91
|
+
this.unsubscribe();
|
|
92
|
+
this.element.remove();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
render(): void {
|
|
96
|
+
const state = this.store.getState();
|
|
97
|
+
const labels = getLocale(this.options.locale?.()).labels;
|
|
98
|
+
const stepLabels =
|
|
99
|
+
state.view === 'day'
|
|
100
|
+
? [labels.previousMonth, labels.nextMonth]
|
|
101
|
+
: [labels.previousYear, labels.nextYear];
|
|
102
|
+
setAttrs(this.prev, { 'aria-label': stepLabels[0], disabled: !state.canPrev });
|
|
103
|
+
setAttrs(this.next, { 'aria-label': stepLabels[1], disabled: !state.canNext });
|
|
104
|
+
this.renderTitle(state, labels.month, labels.year);
|
|
105
|
+
|
|
106
|
+
const key = `${state.view}:${state.view === 'day' ? state.weeks.length : 12}`;
|
|
107
|
+
if (key !== this.bodyKey) {
|
|
108
|
+
this.bodyKey = key;
|
|
109
|
+
this.body.replaceChildren(
|
|
110
|
+
state.view === 'day' ? this.buildGrid(state) : this.buildOptions(),
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
if (state.view === 'day') {
|
|
114
|
+
this.patchGrid(state);
|
|
115
|
+
} else {
|
|
116
|
+
this.patchOptions(
|
|
117
|
+
state.view === 'month'
|
|
118
|
+
? state.months
|
|
119
|
+
: state.view === 'year'
|
|
120
|
+
? state.years
|
|
121
|
+
: state.fiscalYears,
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
this.flushFocus();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Focus the grid if a keyboard move or view change asked for it. */
|
|
128
|
+
private flushFocus(): void {
|
|
129
|
+
if (this.focusAfterRender) {
|
|
130
|
+
this.focusAfterRender = false;
|
|
131
|
+
this.focusGrid();
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
private renderTitle(state: CalendarState, monthLabel: string, yearLabel: string): void {
|
|
136
|
+
const [monthName, yearText] =
|
|
137
|
+
state.view === 'day' ? splitTitle(state.title) : ['', state.title];
|
|
138
|
+
const wanted =
|
|
139
|
+
state.view === 'day'
|
|
140
|
+
? [
|
|
141
|
+
{
|
|
142
|
+
action: 'view-month',
|
|
143
|
+
text: monthName,
|
|
144
|
+
label: `${monthLabel}: ${monthName}`,
|
|
145
|
+
},
|
|
146
|
+
{ action: 'view-year', text: yearText, label: `${yearLabel}: ${yearText}` },
|
|
147
|
+
]
|
|
148
|
+
: [
|
|
149
|
+
{
|
|
150
|
+
action: state.view === 'month' ? 'view-year' : 'view-day',
|
|
151
|
+
text: state.title,
|
|
152
|
+
label:
|
|
153
|
+
state.view === 'fiscal' ? state.title : `${yearLabel}: ${state.title}`,
|
|
154
|
+
},
|
|
155
|
+
];
|
|
156
|
+
const buttons = [...this.title.children] as HTMLButtonElement[];
|
|
157
|
+
if (buttons.length !== wanted.length) {
|
|
158
|
+
this.title.replaceChildren(
|
|
159
|
+
...wanted.map(() =>
|
|
160
|
+
el('button', { type: 'button', class: 'ndp-calendar__title-button' }),
|
|
161
|
+
),
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
wanted.forEach((item, index) => {
|
|
165
|
+
const button = this.title.children[index] as HTMLButtonElement;
|
|
166
|
+
setAttrs(button, { 'data-action': item.action, 'aria-label': item.label });
|
|
167
|
+
setText(button, item.text);
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
private buildGrid(state: CalendarState): HTMLTableElement {
|
|
172
|
+
const head = el('tr', { role: 'row' });
|
|
173
|
+
for (let i = 0; i < 7; i++) {
|
|
174
|
+
head.append(el('th', { class: 'ndp-weekday', scope: 'col', role: 'columnheader' }));
|
|
175
|
+
}
|
|
176
|
+
const rows = state.weeks.map(() => {
|
|
177
|
+
const row = el('tr', { role: 'row' });
|
|
178
|
+
for (let i = 0; i < 7; i++) {
|
|
179
|
+
row.append(
|
|
180
|
+
el('td', { role: 'gridcell' }, [
|
|
181
|
+
el('button', { type: 'button', class: 'ndp-day' }, [
|
|
182
|
+
el('span', { class: 'ndp-sr-only' }),
|
|
183
|
+
el('span', { class: 'ndp-day__label', 'aria-hidden': 'true' }),
|
|
184
|
+
el('span', { class: 'ndp-day__sub', 'aria-hidden': 'true' }),
|
|
185
|
+
el('span', { class: 'ndp-day__dot', 'aria-hidden': 'true', hidden: true }),
|
|
186
|
+
]),
|
|
187
|
+
]),
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
return row;
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
return el('table', { class: 'ndp-grid', role: 'grid' }, [
|
|
194
|
+
el('thead', {}, [head]),
|
|
195
|
+
el('tbody', {}, rows),
|
|
196
|
+
]);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
private patchGrid(state: CalendarState): void {
|
|
200
|
+
const table = this.body.firstElementChild as HTMLTableElement;
|
|
201
|
+
setAttrs(table, { 'aria-label': state.title });
|
|
202
|
+
const headers = table.querySelectorAll('th');
|
|
203
|
+
state.weekdays.forEach((weekday, index) => {
|
|
204
|
+
const th = headers[index];
|
|
205
|
+
if (th) {
|
|
206
|
+
setAttrs(th, { abbr: weekday.longLabel, 'data-weekend': weekday.isWeekend });
|
|
207
|
+
setText(th, weekday.label);
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
this.cells = state.weeks.flat();
|
|
211
|
+
const buttons = table.querySelectorAll<HTMLButtonElement>('.ndp-day');
|
|
212
|
+
this.cells.forEach((cell, index) => {
|
|
213
|
+
const button = buttons[index];
|
|
214
|
+
if (button) {
|
|
215
|
+
patchDay(button, cell);
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
private buildOptions(): HTMLDivElement {
|
|
221
|
+
const buttons = Array.from({ length: 12 }, () =>
|
|
222
|
+
el('button', { type: 'button', class: 'ndp-option' }, [
|
|
223
|
+
el('span', { class: 'ndp-sr-only' }),
|
|
224
|
+
el('span', { class: 'ndp-option__label', 'aria-hidden': 'true' }),
|
|
225
|
+
el('span', { class: 'ndp-option__sub', 'aria-hidden': 'true' }),
|
|
226
|
+
]),
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
return el('div', { class: 'ndp-options', role: 'group' }, buttons);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
private patchOptions(tiles: readonly PickerCell[]): void {
|
|
233
|
+
this.tiles = [...tiles];
|
|
234
|
+
const focusIndex = Math.max(
|
|
235
|
+
0,
|
|
236
|
+
tiles.findIndex((tile) => tile.isSelected) === -1
|
|
237
|
+
? tiles.findIndex((tile) => tile.isCurrent)
|
|
238
|
+
: tiles.findIndex((tile) => tile.isSelected),
|
|
239
|
+
);
|
|
240
|
+
const buttons = this.body.querySelectorAll<HTMLButtonElement>('.ndp-option');
|
|
241
|
+
tiles.forEach((tile, index) => {
|
|
242
|
+
const button = buttons[index];
|
|
243
|
+
if (!button) {
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
setAttrs(button, {
|
|
247
|
+
'data-state': tile.isSelected
|
|
248
|
+
? 'selected'
|
|
249
|
+
: tile.isCurrent
|
|
250
|
+
? 'current'
|
|
251
|
+
: 'default',
|
|
252
|
+
'aria-pressed': tile.isSelected ? 'true' : 'false',
|
|
253
|
+
tabindex: index === focusIndex ? 0 : -1,
|
|
254
|
+
disabled: tile.isDisabled,
|
|
255
|
+
});
|
|
256
|
+
const [name, label, sub] = button.children;
|
|
257
|
+
if (name && label && sub) {
|
|
258
|
+
setText(name, tile.subLabel ? `${tile.label} (${tile.subLabel})` : tile.label);
|
|
259
|
+
setText(label, tile.label);
|
|
260
|
+
setText(sub, tile.subLabel ?? '');
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
private cellOf(target: EventTarget | null): DayCell | null {
|
|
266
|
+
const button =
|
|
267
|
+
target instanceof Element ? target.closest<HTMLButtonElement>('.ndp-day') : null;
|
|
268
|
+
if (!button || !this.element.contains(button)) {
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
const index = [...this.element.querySelectorAll('.ndp-day')].indexOf(button);
|
|
272
|
+
|
|
273
|
+
return this.cells[index] ?? null;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
private readonly onClick = (event: MouseEvent): void => {
|
|
277
|
+
const target = event.target instanceof Element ? event.target : null;
|
|
278
|
+
const action = target?.closest<HTMLElement>('[data-action]')?.dataset['action'];
|
|
279
|
+
const { actions } = this.store;
|
|
280
|
+
switch (action) {
|
|
281
|
+
case 'prev':
|
|
282
|
+
actions.prev();
|
|
283
|
+
return;
|
|
284
|
+
case 'next':
|
|
285
|
+
actions.next();
|
|
286
|
+
return;
|
|
287
|
+
case 'view-month':
|
|
288
|
+
actions.setView('month');
|
|
289
|
+
this.focusAfterRender = true;
|
|
290
|
+
this.render();
|
|
291
|
+
return;
|
|
292
|
+
case 'view-year':
|
|
293
|
+
actions.setView('year');
|
|
294
|
+
this.focusAfterRender = true;
|
|
295
|
+
this.render();
|
|
296
|
+
return;
|
|
297
|
+
case 'view-day':
|
|
298
|
+
actions.setView('day');
|
|
299
|
+
return;
|
|
300
|
+
default:
|
|
301
|
+
break;
|
|
302
|
+
}
|
|
303
|
+
const option = target?.closest<HTMLButtonElement>('.ndp-option');
|
|
304
|
+
if (option) {
|
|
305
|
+
const index = [...this.body.querySelectorAll('.ndp-option')].indexOf(option);
|
|
306
|
+
const tile = this.tiles[index];
|
|
307
|
+
if (tile && !tile.isDisabled) {
|
|
308
|
+
this.focusAfterRender = true;
|
|
309
|
+
const view = this.store.getState().view;
|
|
310
|
+
if (view === 'month') {
|
|
311
|
+
actions.chooseMonth(tile.value as 1);
|
|
312
|
+
} else if (view === 'year') {
|
|
313
|
+
actions.chooseYear(tile.value);
|
|
314
|
+
} else {
|
|
315
|
+
actions.chooseFiscalYear(tile.value);
|
|
316
|
+
}
|
|
317
|
+
this.render();
|
|
318
|
+
}
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
const cell = this.cellOf(target);
|
|
322
|
+
if (cell && !cell.isDisabled) {
|
|
323
|
+
(this.options.onDayActivate ?? ((date: CalendarDate) => actions.select(date)))(
|
|
324
|
+
cell.date,
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
private readonly onKeyDown = (event: KeyboardEvent): void => {
|
|
330
|
+
if (!FOCUS_KEYS.has(event.key) || !this.cellOf(event.target)) {
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
event.preventDefault();
|
|
334
|
+
this.focusAfterRender = true;
|
|
335
|
+
this.store.actions.moveFocus(event.key as FocusKey, { shift: event.shiftKey });
|
|
336
|
+
// A move that changes nothing (at a limit) still keeps focus where it is.
|
|
337
|
+
this.flushFocus();
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
private readonly onMouseOver = (event: MouseEvent): void => {
|
|
341
|
+
const cell = this.cellOf(event.target);
|
|
342
|
+
if (cell && this.store.getState().selectionMode === 'range') {
|
|
343
|
+
this.store.actions.hover(cell.isDisabled ? null : cell.date);
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
private readonly onMouseLeave = (): void => {
|
|
348
|
+
if (this.store.getState().selectionMode === 'range') {
|
|
349
|
+
this.store.actions.hover(null);
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
private readonly onFocusIn = (event: FocusEvent): void => {
|
|
354
|
+
const cell = this.cellOf(event.target);
|
|
355
|
+
if (cell && !cell.isFocusTarget) {
|
|
356
|
+
this.store.actions.focus(cell.date);
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function patchDay(button: HTMLButtonElement, cell: DayCell | null): void {
|
|
362
|
+
const td = button.parentElement;
|
|
363
|
+
if (!cell) {
|
|
364
|
+
button.hidden = true;
|
|
365
|
+
setAttrs(button, { tabindex: -1, 'data-key': null });
|
|
366
|
+
if (td) {
|
|
367
|
+
setAttrs(td, { 'aria-selected': null });
|
|
368
|
+
}
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
button.hidden = false;
|
|
372
|
+
setAttrs(button, {
|
|
373
|
+
'data-key': cell.key,
|
|
374
|
+
'data-state': cell.state,
|
|
375
|
+
'data-weekend': cell.isWeekend,
|
|
376
|
+
'data-preview': cell.isInRangePreview && !cell.isSelected,
|
|
377
|
+
'data-outside': cell.isOutside,
|
|
378
|
+
tabindex: cell.isFocusTarget ? 0 : -1,
|
|
379
|
+
disabled: cell.isDisabled,
|
|
380
|
+
'aria-pressed': cell.isSelected ? 'true' : 'false',
|
|
381
|
+
'aria-current': cell.isToday ? 'date' : null,
|
|
382
|
+
});
|
|
383
|
+
if (td) {
|
|
384
|
+
setAttrs(td, { 'aria-selected': cell.isSelected ? 'true' : null });
|
|
385
|
+
}
|
|
386
|
+
const [name, label, sub, dot] = button.children as unknown as [
|
|
387
|
+
HTMLElement,
|
|
388
|
+
HTMLElement,
|
|
389
|
+
HTMLElement,
|
|
390
|
+
HTMLElement,
|
|
391
|
+
];
|
|
392
|
+
setText(name, cell.ariaLabel);
|
|
393
|
+
setText(label, cell.label);
|
|
394
|
+
setText(sub, cell.subLabel ?? '');
|
|
395
|
+
sub.hidden = cell.subLabel === null;
|
|
396
|
+
const event = cell.events[0];
|
|
397
|
+
dot.hidden = !event;
|
|
398
|
+
dot.style.setProperty('--ndp-dot-color', event?.color ?? null);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/** "Kartik 2083" → ["Kartik", "2083"]; the year is the last word in every locale we ship. */
|
|
402
|
+
function splitTitle(title: string): [string, string] {
|
|
403
|
+
const space = title.lastIndexOf(' ');
|
|
404
|
+
|
|
405
|
+
return space === -1 ? [title, ''] : [title.slice(0, space), title.slice(space + 1)];
|
|
406
|
+
}
|
package/src/calendar.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/** `@nepdatejs/vanilla/calendar`: just the standalone calendar. */
|
|
2
|
+
import {
|
|
3
|
+
createCalendar as createCalendarStore,
|
|
4
|
+
type CalendarOptions,
|
|
5
|
+
type CalendarStore,
|
|
6
|
+
} from '@nepdatejs/calendar';
|
|
7
|
+
import { NepCalendarElement } from './calendar-element';
|
|
8
|
+
import { CalendarView } from './calendar-view';
|
|
9
|
+
import { defineElement, ELEMENT_NAMES } from './registry';
|
|
10
|
+
|
|
11
|
+
export { NepCalendarElement };
|
|
12
|
+
export { CalendarView, type CalendarViewOptions } from './calendar-view';
|
|
13
|
+
|
|
14
|
+
/** Register <nep-calendar> (skipped if already defined). */
|
|
15
|
+
export function defineCalendar(registry?: CustomElementRegistry): void {
|
|
16
|
+
defineElement(ELEMENT_NAMES.calendar, NepCalendarElement, registry);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface CalendarHandle {
|
|
20
|
+
readonly store: CalendarStore;
|
|
21
|
+
readonly view: CalendarView;
|
|
22
|
+
setOptions(changes: Partial<CalendarOptions>): void;
|
|
23
|
+
destroy(): void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Mount a standalone calendar into a container. */
|
|
27
|
+
export function createCalendar(
|
|
28
|
+
container: HTMLElement,
|
|
29
|
+
options: CalendarOptions = {},
|
|
30
|
+
): CalendarHandle {
|
|
31
|
+
const store = createCalendarStore(options);
|
|
32
|
+
const view = new CalendarView(container, store, { locale: () => options.locale });
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
store,
|
|
36
|
+
view,
|
|
37
|
+
setOptions: (changes) => {
|
|
38
|
+
store.setOptions(changes);
|
|
39
|
+
},
|
|
40
|
+
destroy: () => {
|
|
41
|
+
view.destroy();
|
|
42
|
+
store.destroy();
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { fromIsoDate } from '@nepdatejs/core';
|
|
2
|
+
import type { SinglePreset } from '@nepdatejs/datepicker';
|
|
3
|
+
import { DatePickerView, type DatePickerViewOptions } from './picker-view';
|
|
4
|
+
import {
|
|
5
|
+
Base,
|
|
6
|
+
str,
|
|
7
|
+
bool,
|
|
8
|
+
onUnlessFalse,
|
|
9
|
+
calendarAttr,
|
|
10
|
+
weekday,
|
|
11
|
+
panelLayout,
|
|
12
|
+
theme,
|
|
13
|
+
presetIds,
|
|
14
|
+
swallowInnerChange,
|
|
15
|
+
isoOf,
|
|
16
|
+
type RichProps,
|
|
17
|
+
} from './element-utils';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* <nep-date-picker name="dob" min="2020-01-01"></nep-date-picker>
|
|
21
|
+
*
|
|
22
|
+
* A form-associated BS/AD date field: the form receives the AD "YYYY-MM-DD"
|
|
23
|
+
* value under `name`. Fires `change` with { value, ad, bs }, plus `nep-open`,
|
|
24
|
+
* `nep-close` and `nep-calendar-change`.
|
|
25
|
+
*/
|
|
26
|
+
export class NepDatePickerElement extends Base {
|
|
27
|
+
static readonly formAssociated = true;
|
|
28
|
+
static readonly observedAttributes: readonly string[] = [
|
|
29
|
+
'value',
|
|
30
|
+
'min',
|
|
31
|
+
'max',
|
|
32
|
+
'calendar',
|
|
33
|
+
'default-calendar',
|
|
34
|
+
'locale',
|
|
35
|
+
'name',
|
|
36
|
+
'placeholder',
|
|
37
|
+
'required',
|
|
38
|
+
'disabled',
|
|
39
|
+
'clearable',
|
|
40
|
+
'show-equivalent',
|
|
41
|
+
'theme',
|
|
42
|
+
'panel-layout',
|
|
43
|
+
'mask',
|
|
44
|
+
'inline',
|
|
45
|
+
'persist-calendar',
|
|
46
|
+
'week-starts-on',
|
|
47
|
+
'display-format',
|
|
48
|
+
'label',
|
|
49
|
+
'close-on-select',
|
|
50
|
+
'input-id',
|
|
51
|
+
'presets',
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
/** The view, once connected. */
|
|
55
|
+
view: DatePickerView | null = null;
|
|
56
|
+
private readonly internals: ElementInternals | null;
|
|
57
|
+
private rich: RichProps = {};
|
|
58
|
+
private customPresets: readonly SinglePreset[] | undefined;
|
|
59
|
+
private unsubscribe: (() => void) | null = null;
|
|
60
|
+
|
|
61
|
+
constructor() {
|
|
62
|
+
super();
|
|
63
|
+
this.internals =
|
|
64
|
+
typeof this.attachInternals === 'function' ? this.attachInternals() : null;
|
|
65
|
+
swallowInnerChange(this);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
get value(): string {
|
|
69
|
+
return this.view?.getState().value ?? isoOf(fromIsoDate(this.getAttribute('value')));
|
|
70
|
+
}
|
|
71
|
+
set value(next: string) {
|
|
72
|
+
this.view?.store.actions.setValue(next);
|
|
73
|
+
}
|
|
74
|
+
get holidays(): RichProps['holidays'] {
|
|
75
|
+
return this.rich.holidays;
|
|
76
|
+
}
|
|
77
|
+
set holidays(next: RichProps['holidays']) {
|
|
78
|
+
this.setRich({ holidays: next });
|
|
79
|
+
}
|
|
80
|
+
get events(): RichProps['events'] {
|
|
81
|
+
return this.rich.events;
|
|
82
|
+
}
|
|
83
|
+
set events(next: RichProps['events']) {
|
|
84
|
+
this.setRich({ events: next });
|
|
85
|
+
}
|
|
86
|
+
get isDateDisabled(): RichProps['isDateDisabled'] {
|
|
87
|
+
return this.rich.isDateDisabled;
|
|
88
|
+
}
|
|
89
|
+
set isDateDisabled(next: RichProps['isDateDisabled']) {
|
|
90
|
+
this.setRich({ isDateDisabled: next });
|
|
91
|
+
}
|
|
92
|
+
/** Shortcuts, including custom { id, label, resolve } ones. Overrides the presets attribute. */
|
|
93
|
+
get presets(): readonly SinglePreset[] | undefined {
|
|
94
|
+
return this.customPresets ?? (presetIds(this) as readonly SinglePreset[] | undefined);
|
|
95
|
+
}
|
|
96
|
+
set presets(next: readonly SinglePreset[] | undefined) {
|
|
97
|
+
this.customPresets = next;
|
|
98
|
+
this.view?.setOptions({ presets: this.presets });
|
|
99
|
+
}
|
|
100
|
+
get form(): HTMLFormElement | null {
|
|
101
|
+
return this.internals?.form ?? null;
|
|
102
|
+
}
|
|
103
|
+
get validity(): ValidityState | undefined {
|
|
104
|
+
return this.internals?.validity;
|
|
105
|
+
}
|
|
106
|
+
get validationMessage(): string {
|
|
107
|
+
return this.internals?.validationMessage ?? '';
|
|
108
|
+
}
|
|
109
|
+
checkValidity(): boolean {
|
|
110
|
+
return this.internals?.checkValidity() ?? true;
|
|
111
|
+
}
|
|
112
|
+
reportValidity(): boolean {
|
|
113
|
+
return this.internals?.reportValidity() ?? true;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
connectedCallback(): void {
|
|
117
|
+
if (this.view) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
const view = new DatePickerView(this, {
|
|
121
|
+
...this.readOptions(),
|
|
122
|
+
defaultValue: str(this, 'value'),
|
|
123
|
+
onChange: (value, detail) => {
|
|
124
|
+
this.dispatchEvent(
|
|
125
|
+
new CustomEvent('change', {
|
|
126
|
+
bubbles: true,
|
|
127
|
+
detail: { value, ad: detail?.ad ?? null, bs: detail?.bs ?? null },
|
|
128
|
+
}),
|
|
129
|
+
);
|
|
130
|
+
},
|
|
131
|
+
onCalendarChange: (calendar) => {
|
|
132
|
+
this.dispatchEvent(
|
|
133
|
+
new CustomEvent('nep-calendar-change', { bubbles: true, detail: { calendar } }),
|
|
134
|
+
);
|
|
135
|
+
},
|
|
136
|
+
onOpenChange: (open) => {
|
|
137
|
+
this.dispatchEvent(
|
|
138
|
+
new CustomEvent(open ? 'nep-open' : 'nep-close', { bubbles: true }),
|
|
139
|
+
);
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
this.view = view;
|
|
143
|
+
this.unsubscribe = view.store.subscribe(() => {
|
|
144
|
+
this.syncForm();
|
|
145
|
+
});
|
|
146
|
+
this.syncForm();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
disconnectedCallback(): void {
|
|
150
|
+
this.unsubscribe?.();
|
|
151
|
+
this.view?.destroy();
|
|
152
|
+
this.view = null;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
attributeChangedCallback(
|
|
156
|
+
name: string,
|
|
157
|
+
previous: string | null,
|
|
158
|
+
next: string | null,
|
|
159
|
+
): void {
|
|
160
|
+
if (!this.view || previous === next) {
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
if (name === 'value') {
|
|
164
|
+
this.view.store.actions.setValue(next ?? '');
|
|
165
|
+
}
|
|
166
|
+
this.view.setOptions(this.readOptions());
|
|
167
|
+
this.syncForm();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
formResetCallback(): void {
|
|
171
|
+
this.view?.store.actions.reset();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
formDisabledCallback(disabled: boolean): void {
|
|
175
|
+
this.view?.setOptions({ disabled: disabled || bool(this, 'disabled') });
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
formStateRestoreCallback(state: string | File | FormData | null): void {
|
|
179
|
+
if (typeof state === 'string') {
|
|
180
|
+
this.view?.store.actions.setValue(state);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
override focus(options?: FocusOptions): void {
|
|
185
|
+
this.view?.input.focus(options);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
open(): void {
|
|
189
|
+
this.view?.open();
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
close(): void {
|
|
193
|
+
this.view?.close();
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
private setRich(changes: RichProps): void {
|
|
197
|
+
this.rich = { ...this.rich, ...changes };
|
|
198
|
+
this.view?.setOptions(changes);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
private readOptions(): DatePickerViewOptions {
|
|
202
|
+
return {
|
|
203
|
+
...this.rich,
|
|
204
|
+
min: str(this, 'min'),
|
|
205
|
+
max: str(this, 'max'),
|
|
206
|
+
calendar: calendarAttr(this, 'calendar'),
|
|
207
|
+
defaultCalendar: calendarAttr(this, 'default-calendar'),
|
|
208
|
+
locale: str(this, 'locale'),
|
|
209
|
+
name: str(this, 'name'),
|
|
210
|
+
hiddenInput: this.internals === null,
|
|
211
|
+
placeholder: str(this, 'placeholder'),
|
|
212
|
+
required: bool(this, 'required'),
|
|
213
|
+
disabled: bool(this, 'disabled'),
|
|
214
|
+
clearable: onUnlessFalse(this, 'clearable'),
|
|
215
|
+
showEquivalent: onUnlessFalse(this, 'show-equivalent'),
|
|
216
|
+
closeOnSelect: onUnlessFalse(this, 'close-on-select'),
|
|
217
|
+
theme: theme(this),
|
|
218
|
+
panelLayout: panelLayout(this),
|
|
219
|
+
mask: onUnlessFalse(this, 'mask'),
|
|
220
|
+
inline: this.hasAttribute('inline'),
|
|
221
|
+
persistCalendar: str(this, 'persist-calendar'),
|
|
222
|
+
weekStartsOn: weekday(this, 'week-starts-on'),
|
|
223
|
+
displayFormat: str(this, 'display-format'),
|
|
224
|
+
ariaLabel: str(this, 'label'),
|
|
225
|
+
id: str(this, 'input-id'),
|
|
226
|
+
presets: this.presets,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
private syncForm(): void {
|
|
231
|
+
const view = this.view;
|
|
232
|
+
if (!view || !this.internals) {
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
const state = view.getState();
|
|
236
|
+
this.internals.setFormValue(state.value === '' ? null : state.value, state.value);
|
|
237
|
+
if (state.error) {
|
|
238
|
+
this.internals.setValidity({ customError: true }, state.error.message, view.input);
|
|
239
|
+
} else if (bool(this, 'required') && state.value === '') {
|
|
240
|
+
this.internals.setValidity(
|
|
241
|
+
{ valueMissing: true },
|
|
242
|
+
view.input.validationMessage || 'Please choose a date.',
|
|
243
|
+
view.input,
|
|
244
|
+
);
|
|
245
|
+
} else {
|
|
246
|
+
this.internals.setValidity({});
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|