@vanduo-oss/framework 1.3.5 → 1.3.7
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/README.md +5 -4
- package/css/components/cards.css +11 -1
- package/css/components/datepicker.css +10 -1
- package/css/components/expanding-cards.css +215 -0
- package/css/components/spotlight.css +8 -3
- package/css/components/timeline.css +47 -0
- package/css/effects/morph.css +0 -12
- package/css/vanduo.css +1 -0
- package/dist/build-info.json +3 -3
- package/dist/vanduo.cjs.js +647 -63
- package/dist/vanduo.cjs.js.map +3 -3
- package/dist/vanduo.cjs.min.js +4 -4
- package/dist/vanduo.cjs.min.js.map +4 -4
- package/dist/vanduo.css +255 -24
- package/dist/vanduo.css.map +1 -1
- package/dist/vanduo.esm.js +647 -63
- package/dist/vanduo.esm.js.map +3 -3
- package/dist/vanduo.esm.min.js +4 -4
- package/dist/vanduo.esm.min.js.map +4 -4
- package/dist/vanduo.js +647 -63
- package/dist/vanduo.js.map +3 -3
- package/dist/vanduo.min.css +2 -2
- package/dist/vanduo.min.css.map +1 -1
- package/dist/vanduo.min.js +4 -4
- package/dist/vanduo.min.js.map +4 -4
- package/js/components/datepicker.js +392 -70
- package/js/components/expanding-cards.js +136 -0
- package/js/components/morph.js +0 -3
- package/js/components/timeline.js +226 -0
- package/js/index.js +2 -0
- package/package.json +1 -1
|
@@ -7,7 +7,126 @@
|
|
|
7
7
|
'use strict';
|
|
8
8
|
|
|
9
9
|
const DAYS = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'];
|
|
10
|
-
const MONTHS = ['January','February','March','April','May','June','July','August','September','October','November','December'];
|
|
10
|
+
const MONTHS = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
|
|
11
|
+
|
|
12
|
+
function escapeRegexChar(c) {
|
|
13
|
+
return c.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function buildParseFormat(format) {
|
|
17
|
+
let regex = '^';
|
|
18
|
+
const order = [];
|
|
19
|
+
let i = 0;
|
|
20
|
+
while (i < format.length) {
|
|
21
|
+
const slice = format.slice(i);
|
|
22
|
+
if (slice.toLowerCase().startsWith('yyyy')) {
|
|
23
|
+
regex += '(\\d{4})';
|
|
24
|
+
order.push('y');
|
|
25
|
+
i += 4;
|
|
26
|
+
} else if (slice.toLowerCase().startsWith('mm')) {
|
|
27
|
+
regex += '(\\d{2})';
|
|
28
|
+
order.push('m');
|
|
29
|
+
i += 2;
|
|
30
|
+
} else if (slice.toLowerCase().startsWith('dd')) {
|
|
31
|
+
regex += '(\\d{2})';
|
|
32
|
+
order.push('d');
|
|
33
|
+
i += 2;
|
|
34
|
+
} else {
|
|
35
|
+
regex += escapeRegexChar(format[i]);
|
|
36
|
+
i++;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
regex += '$';
|
|
40
|
+
return { regex: new RegExp(regex), order };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function parseDateFromFormat(value, format) {
|
|
44
|
+
if (!value || !format) return null;
|
|
45
|
+
const { regex, order } = buildParseFormat(format);
|
|
46
|
+
const m = value.trim().match(regex);
|
|
47
|
+
if (!m) return null;
|
|
48
|
+
let y;
|
|
49
|
+
let mo;
|
|
50
|
+
let d;
|
|
51
|
+
let ci = 1;
|
|
52
|
+
for (let k = 0; k < order.length; k++) {
|
|
53
|
+
const part = order[k];
|
|
54
|
+
const v = parseInt(m[ci++], 10);
|
|
55
|
+
if (Number.isNaN(v)) return null;
|
|
56
|
+
if (part === 'y') y = v;
|
|
57
|
+
else if (part === 'm') mo = v - 1;
|
|
58
|
+
else if (part === 'd') d = v;
|
|
59
|
+
}
|
|
60
|
+
if (y === undefined || mo === undefined || d === undefined) return null;
|
|
61
|
+
const dt = new Date(y, mo, d);
|
|
62
|
+
if (dt.getFullYear() !== y || dt.getMonth() !== mo || dt.getDate() !== d) return null;
|
|
63
|
+
return dt;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function formatDate(d, format) {
|
|
67
|
+
const yyyy = String(d.getFullYear());
|
|
68
|
+
const mm = String(d.getMonth() + 1).padStart(2, '0');
|
|
69
|
+
const dd = String(d.getDate()).padStart(2, '0');
|
|
70
|
+
let out = '';
|
|
71
|
+
let i = 0;
|
|
72
|
+
while (i < format.length) {
|
|
73
|
+
const slice = format.slice(i);
|
|
74
|
+
if (slice.toLowerCase().startsWith('yyyy')) {
|
|
75
|
+
out += yyyy;
|
|
76
|
+
i += 4;
|
|
77
|
+
} else if (slice.toLowerCase().startsWith('mm')) {
|
|
78
|
+
out += mm;
|
|
79
|
+
i += 2;
|
|
80
|
+
} else if (slice.toLowerCase().startsWith('dd')) {
|
|
81
|
+
out += dd;
|
|
82
|
+
i += 2;
|
|
83
|
+
} else {
|
|
84
|
+
out += format[i];
|
|
85
|
+
i++;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return out;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function dateKey(d) {
|
|
92
|
+
return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function addDays(d, n) {
|
|
96
|
+
const x = new Date(d.getFullYear(), d.getMonth(), d.getDate());
|
|
97
|
+
x.setDate(x.getDate() + n);
|
|
98
|
+
return x;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function addMonthsClamped(d, n) {
|
|
102
|
+
return new Date(d.getFullYear(), d.getMonth() + n, d.getDate());
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function parseYmdLocal(ymd) {
|
|
106
|
+
if (!ymd || typeof ymd !== 'string') return null;
|
|
107
|
+
const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(ymd.trim());
|
|
108
|
+
if (!m) return null;
|
|
109
|
+
const y = +m[1];
|
|
110
|
+
const mo = +m[2] - 1;
|
|
111
|
+
const day = +m[3];
|
|
112
|
+
const dt = new Date(y, mo, day);
|
|
113
|
+
if (dt.getFullYear() !== y || dt.getMonth() !== mo || dt.getDate() !== day) return null;
|
|
114
|
+
return dt;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function startOfWeekSunday(d) {
|
|
118
|
+
const x = new Date(d.getFullYear(), d.getMonth(), d.getDate());
|
|
119
|
+
const day = x.getDay();
|
|
120
|
+
x.setDate(x.getDate() - day);
|
|
121
|
+
return x;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function endOfWeekSunday(d) {
|
|
125
|
+
const x = new Date(d.getFullYear(), d.getMonth(), d.getDate());
|
|
126
|
+
const day = x.getDay();
|
|
127
|
+
x.setDate(x.getDate() + (6 - day));
|
|
128
|
+
return x;
|
|
129
|
+
}
|
|
11
130
|
|
|
12
131
|
const Datepicker = {
|
|
13
132
|
instances: new Map(),
|
|
@@ -22,33 +141,79 @@
|
|
|
22
141
|
|
|
23
142
|
initInstance: function (input) {
|
|
24
143
|
const cleanup = [];
|
|
25
|
-
const format = input.getAttribute('data-vd-datepicker-format') || '
|
|
144
|
+
const format = input.getAttribute('data-vd-datepicker-format') || 'YYYY-MM-DD';
|
|
26
145
|
const minStr = input.getAttribute('data-vd-datepicker-min');
|
|
27
146
|
const maxStr = input.getAttribute('data-vd-datepicker-max');
|
|
28
|
-
const minDate = minStr ?
|
|
29
|
-
const maxDate = maxStr ?
|
|
147
|
+
const minDate = minStr ? parseYmdLocal(minStr) : null;
|
|
148
|
+
const maxDate = maxStr ? parseYmdLocal(maxStr) : null;
|
|
30
149
|
|
|
31
150
|
const today = new Date();
|
|
32
151
|
let viewYear = today.getFullYear();
|
|
33
152
|
let viewMonth = today.getMonth();
|
|
34
153
|
let selectedDate = null;
|
|
35
154
|
let viewMode = 'days'; // days | months | years
|
|
155
|
+
let focusedDate = null;
|
|
156
|
+
/** Prevents focus() after close from immediately re-opening the popup */
|
|
157
|
+
let skipNextFocusOpen = false;
|
|
158
|
+
|
|
159
|
+
const isDisabled = (d) => {
|
|
160
|
+
if (minDate) {
|
|
161
|
+
const t = new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime();
|
|
162
|
+
if (t < minDate.getTime()) return true;
|
|
163
|
+
}
|
|
164
|
+
if (maxDate) {
|
|
165
|
+
const t = new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime();
|
|
166
|
+
if (t > maxDate.getTime()) return true;
|
|
167
|
+
}
|
|
168
|
+
return false;
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const ensureMonthInRange = (y, m) => {
|
|
172
|
+
if (!minDate && !maxDate) return { y: y, m: m };
|
|
173
|
+
const first = new Date(y, m, 1);
|
|
174
|
+
const last = new Date(y, m + 1, 0);
|
|
175
|
+
if (minDate && last.getTime() < minDate.getTime()) {
|
|
176
|
+
return { y: minDate.getFullYear(), m: minDate.getMonth() };
|
|
177
|
+
}
|
|
178
|
+
if (maxDate && first.getTime() > maxDate.getTime()) {
|
|
179
|
+
return { y: maxDate.getFullYear(), m: maxDate.getMonth() };
|
|
180
|
+
}
|
|
181
|
+
return { y: y, m: m };
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const firstSelectableInMonth = (y, m) => {
|
|
185
|
+
const last = new Date(y, m + 1, 0).getDate();
|
|
186
|
+
for (let day = 1; day <= last; day++) {
|
|
187
|
+
const dt = new Date(y, m, day);
|
|
188
|
+
if (!isDisabled(dt)) return dt;
|
|
189
|
+
}
|
|
190
|
+
return new Date(y, m, 1);
|
|
191
|
+
};
|
|
36
192
|
|
|
37
|
-
// Parse existing value
|
|
38
193
|
if (input.value) {
|
|
39
|
-
const
|
|
40
|
-
|
|
194
|
+
const trimmed = input.value.trim();
|
|
195
|
+
let parsed = parseDateFromFormat(trimmed, format);
|
|
196
|
+
if (!parsed) {
|
|
197
|
+
const fallback = new Date(trimmed);
|
|
198
|
+
if (!isNaN(fallback.getTime())) parsed = fallback;
|
|
199
|
+
}
|
|
200
|
+
if (parsed) {
|
|
41
201
|
selectedDate = parsed;
|
|
42
202
|
viewYear = parsed.getFullYear();
|
|
43
203
|
viewMonth = parsed.getMonth();
|
|
44
204
|
}
|
|
45
205
|
}
|
|
46
206
|
|
|
207
|
+
const clampedInit = ensureMonthInRange(viewYear, viewMonth);
|
|
208
|
+
viewYear = clampedInit.y;
|
|
209
|
+
viewMonth = clampedInit.m;
|
|
210
|
+
|
|
47
211
|
// Create popup
|
|
48
212
|
const popup = document.createElement('div');
|
|
49
213
|
popup.className = 'vd-datepicker-popup';
|
|
50
214
|
popup.setAttribute('role', 'dialog');
|
|
51
215
|
popup.setAttribute('aria-label', 'Choose date');
|
|
216
|
+
popup.tabIndex = -1;
|
|
52
217
|
|
|
53
218
|
const wrapper = document.createElement('div');
|
|
54
219
|
wrapper.className = 'vd-suggest-wrapper';
|
|
@@ -58,24 +223,94 @@
|
|
|
58
223
|
wrapper.appendChild(input);
|
|
59
224
|
wrapper.appendChild(popup);
|
|
60
225
|
|
|
61
|
-
const formatDate = (d) => {
|
|
62
|
-
const yyyy = d.getFullYear();
|
|
63
|
-
const mm = String(d.getMonth() + 1).padStart(2, '0');
|
|
64
|
-
const dd = String(d.getDate()).padStart(2, '0');
|
|
65
|
-
return format.replace('yyyy', yyyy).replace('mm', mm).replace('dd', dd);
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
const isDisabled = (d) => {
|
|
69
|
-
if (minDate && d < minDate) return true;
|
|
70
|
-
if (maxDate && d > maxDate) return true;
|
|
71
|
-
return false;
|
|
72
|
-
};
|
|
73
|
-
|
|
74
226
|
const isSameDay = (a, b) => a && b &&
|
|
75
227
|
a.getFullYear() === b.getFullYear() &&
|
|
76
228
|
a.getMonth() === b.getMonth() &&
|
|
77
229
|
a.getDate() === b.getDate();
|
|
78
230
|
|
|
231
|
+
const selectDate = (date) => {
|
|
232
|
+
selectedDate = date;
|
|
233
|
+
viewYear = date.getFullYear();
|
|
234
|
+
viewMonth = date.getMonth();
|
|
235
|
+
input.value = formatDate(date, format);
|
|
236
|
+
skipNextFocusOpen = true;
|
|
237
|
+
close();
|
|
238
|
+
input.dispatchEvent(new CustomEvent('datepicker:select', {
|
|
239
|
+
detail: { date: date, formatted: input.value },
|
|
240
|
+
bubbles: true
|
|
241
|
+
}));
|
|
242
|
+
input.dispatchEvent(new Event('change', { bubbles: true }));
|
|
243
|
+
input.focus();
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
const focusFocusedDay = () => {
|
|
247
|
+
if (viewMode !== 'days' || !focusedDate) return;
|
|
248
|
+
const key = dateKey(focusedDate);
|
|
249
|
+
const btn = popup.querySelector('[data-vd-date="' + key + '"]');
|
|
250
|
+
if (btn && !btn.classList.contains('is-outside') && btn.getAttribute('aria-disabled') !== 'true') {
|
|
251
|
+
btn.focus();
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
const skipDisabled = (d, stepDir, maxSteps) => {
|
|
256
|
+
let x = new Date(d.getFullYear(), d.getMonth(), d.getDate());
|
|
257
|
+
const step = stepDir > 0 ? 1 : -1;
|
|
258
|
+
for (let i = 0; i < maxSteps; i++) {
|
|
259
|
+
if (!isDisabled(x)) return x;
|
|
260
|
+
x = addDays(x, step);
|
|
261
|
+
}
|
|
262
|
+
return d;
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
const createDayBtn = (day, outside, date) => {
|
|
266
|
+
const btn = document.createElement('button');
|
|
267
|
+
btn.type = 'button';
|
|
268
|
+
btn.className = 'vd-datepicker-day';
|
|
269
|
+
btn.textContent = day;
|
|
270
|
+
btn.setAttribute('role', 'gridcell');
|
|
271
|
+
|
|
272
|
+
if (outside) {
|
|
273
|
+
btn.classList.add('is-outside');
|
|
274
|
+
btn.tabIndex = -1;
|
|
275
|
+
btn.setAttribute('aria-disabled', 'true');
|
|
276
|
+
return btn;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
btn.setAttribute('data-vd-date', dateKey(date));
|
|
280
|
+
|
|
281
|
+
if (date && isSameDay(date, today)) btn.classList.add('is-today');
|
|
282
|
+
if (date && isSameDay(date, selectedDate)) btn.classList.add('is-selected');
|
|
283
|
+
if (date && isDisabled(date)) {
|
|
284
|
+
btn.classList.add('is-disabled');
|
|
285
|
+
btn.setAttribute('aria-disabled', 'true');
|
|
286
|
+
btn.tabIndex = -1;
|
|
287
|
+
return btn;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (date) {
|
|
291
|
+
const isFocused = focusedDate && isSameDay(date, focusedDate);
|
|
292
|
+
btn.tabIndex = isFocused ? 0 : -1;
|
|
293
|
+
|
|
294
|
+
btn.addEventListener('click', () => {
|
|
295
|
+
selectedDate = date;
|
|
296
|
+
viewYear = date.getFullYear();
|
|
297
|
+
viewMonth = date.getMonth();
|
|
298
|
+
focusedDate = new Date(date.getFullYear(), date.getMonth(), date.getDate());
|
|
299
|
+
input.value = formatDate(date, format);
|
|
300
|
+
skipNextFocusOpen = true;
|
|
301
|
+
close();
|
|
302
|
+
input.dispatchEvent(new CustomEvent('datepicker:select', {
|
|
303
|
+
detail: { date: date, formatted: input.value },
|
|
304
|
+
bubbles: true
|
|
305
|
+
}));
|
|
306
|
+
input.dispatchEvent(new Event('change', { bubbles: true }));
|
|
307
|
+
input.focus();
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return btn;
|
|
312
|
+
};
|
|
313
|
+
|
|
79
314
|
const render = () => {
|
|
80
315
|
popup.innerHTML = '';
|
|
81
316
|
|
|
@@ -121,46 +356,61 @@
|
|
|
121
356
|
popup.appendChild(header);
|
|
122
357
|
|
|
123
358
|
if (viewMode === 'days') {
|
|
124
|
-
|
|
359
|
+
const gridWrap = document.createElement('div');
|
|
360
|
+
gridWrap.className = 'vd-datepicker-grid';
|
|
361
|
+
gridWrap.setAttribute('role', 'grid');
|
|
362
|
+
gridWrap.setAttribute('aria-label', 'Calendar');
|
|
363
|
+
|
|
125
364
|
const weekdays = document.createElement('div');
|
|
126
365
|
weekdays.className = 'vd-datepicker-weekdays';
|
|
127
|
-
|
|
366
|
+
weekdays.setAttribute('role', 'row');
|
|
367
|
+
DAYS.forEach(function (d) {
|
|
128
368
|
const span = document.createElement('span');
|
|
369
|
+
span.setAttribute('role', 'columnheader');
|
|
370
|
+
span.setAttribute('aria-label', d);
|
|
129
371
|
span.textContent = d;
|
|
130
372
|
weekdays.appendChild(span);
|
|
131
373
|
});
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
// Days grid
|
|
135
|
-
const grid = document.createElement('div');
|
|
136
|
-
grid.className = 'vd-datepicker-days';
|
|
374
|
+
gridWrap.appendChild(weekdays);
|
|
137
375
|
|
|
138
376
|
const firstDay = new Date(viewYear, viewMonth, 1).getDay();
|
|
139
377
|
const daysInMonth = new Date(viewYear, viewMonth + 1, 0).getDate();
|
|
140
378
|
const daysInPrev = new Date(viewYear, viewMonth, 0).getDate();
|
|
141
379
|
|
|
142
|
-
|
|
380
|
+
const cells = [];
|
|
381
|
+
|
|
143
382
|
for (let i = firstDay - 1; i >= 0; i--) {
|
|
144
|
-
const
|
|
145
|
-
|
|
383
|
+
const dayNum = daysInPrev - i;
|
|
384
|
+
const prevMonth = viewMonth === 0 ? 11 : viewMonth - 1;
|
|
385
|
+
const prevYear = viewMonth === 0 ? viewYear - 1 : viewYear;
|
|
386
|
+
const date = new Date(prevYear, prevMonth, dayNum);
|
|
387
|
+
cells.push({ day: dayNum, outside: true, date: date });
|
|
146
388
|
}
|
|
147
389
|
|
|
148
|
-
// Current month
|
|
149
390
|
for (let d = 1; d <= daysInMonth; d++) {
|
|
150
391
|
const date = new Date(viewYear, viewMonth, d);
|
|
151
|
-
|
|
152
|
-
grid.appendChild(btn);
|
|
392
|
+
cells.push({ day: d, outside: false, date: date });
|
|
153
393
|
}
|
|
154
394
|
|
|
155
|
-
// Next month padding
|
|
156
395
|
const totalCells = firstDay + daysInMonth;
|
|
157
396
|
const remaining = totalCells % 7 === 0 ? 0 : 7 - (totalCells % 7);
|
|
158
397
|
for (let i = 1; i <= remaining; i++) {
|
|
159
|
-
const
|
|
160
|
-
|
|
398
|
+
const date = new Date(viewYear, viewMonth + 1, i);
|
|
399
|
+
cells.push({ day: i, outside: true, date: date });
|
|
161
400
|
}
|
|
162
401
|
|
|
163
|
-
|
|
402
|
+
for (let r = 0; r < cells.length; r += 7) {
|
|
403
|
+
const row = document.createElement('div');
|
|
404
|
+
row.className = 'vd-datepicker-row';
|
|
405
|
+
row.setAttribute('role', 'row');
|
|
406
|
+
for (let c = 0; c < 7; c++) {
|
|
407
|
+
const cell = cells[r + c];
|
|
408
|
+
row.appendChild(createDayBtn(cell.day, cell.outside, cell.date));
|
|
409
|
+
}
|
|
410
|
+
gridWrap.appendChild(row);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
popup.appendChild(gridWrap);
|
|
164
414
|
} else if (viewMode === 'months') {
|
|
165
415
|
const grid = document.createElement('div');
|
|
166
416
|
grid.className = 'vd-datepicker-months';
|
|
@@ -194,47 +444,105 @@
|
|
|
194
444
|
}
|
|
195
445
|
};
|
|
196
446
|
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
btn.textContent = day;
|
|
447
|
+
const handleGridKeydown = (e) => {
|
|
448
|
+
if (!popup.classList.contains('is-open') || viewMode !== 'days') return;
|
|
449
|
+
const grid = popup.querySelector('.vd-datepicker-grid');
|
|
450
|
+
if (!grid || !grid.contains(e.target)) return;
|
|
202
451
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
452
|
+
const key = e.key;
|
|
453
|
+
if (key !== 'ArrowLeft' && key !== 'ArrowRight' && key !== 'ArrowUp' && key !== 'ArrowDown' &&
|
|
454
|
+
key !== 'Home' && key !== 'End' && key !== 'PageUp' && key !== 'PageDown' &&
|
|
455
|
+
key !== 'Enter' && key !== ' ' && key !== 'Escape') {
|
|
456
|
+
return;
|
|
207
457
|
}
|
|
208
458
|
|
|
209
|
-
if (
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
459
|
+
if (key === 'Escape') {
|
|
460
|
+
e.preventDefault();
|
|
461
|
+
e.stopPropagation();
|
|
462
|
+
skipNextFocusOpen = true;
|
|
463
|
+
close();
|
|
464
|
+
input.focus();
|
|
465
|
+
return;
|
|
214
466
|
}
|
|
215
467
|
|
|
216
|
-
if (
|
|
217
|
-
|
|
218
|
-
selectedDate = date;
|
|
219
|
-
viewYear = date.getFullYear();
|
|
220
|
-
viewMonth = date.getMonth();
|
|
221
|
-
input.value = formatDate(date);
|
|
222
|
-
close();
|
|
223
|
-
input.dispatchEvent(new CustomEvent('datepicker:select', {
|
|
224
|
-
detail: { date, formatted: input.value },
|
|
225
|
-
bubbles: true
|
|
226
|
-
}));
|
|
227
|
-
input.dispatchEvent(new Event('change', { bubbles: true }));
|
|
228
|
-
});
|
|
468
|
+
if (!focusedDate) {
|
|
469
|
+
focusedDate = firstSelectableInMonth(viewYear, viewMonth);
|
|
229
470
|
}
|
|
230
471
|
|
|
231
|
-
|
|
472
|
+
if (key === 'Enter' || key === ' ') {
|
|
473
|
+
e.preventDefault();
|
|
474
|
+
if (focusedDate && !isDisabled(focusedDate)) {
|
|
475
|
+
selectDate(new Date(focusedDate.getFullYear(), focusedDate.getMonth(), focusedDate.getDate()));
|
|
476
|
+
}
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
e.preventDefault();
|
|
481
|
+
|
|
482
|
+
let next = new Date(focusedDate.getFullYear(), focusedDate.getMonth(), focusedDate.getDate());
|
|
483
|
+
let skipDir = 1;
|
|
484
|
+
|
|
485
|
+
if (key === 'ArrowLeft') {
|
|
486
|
+
next = addDays(next, -1);
|
|
487
|
+
skipDir = -1;
|
|
488
|
+
} else if (key === 'ArrowRight') {
|
|
489
|
+
next = addDays(next, 1);
|
|
490
|
+
skipDir = 1;
|
|
491
|
+
} else if (key === 'ArrowUp') {
|
|
492
|
+
next = addDays(next, -7);
|
|
493
|
+
skipDir = -1;
|
|
494
|
+
} else if (key === 'ArrowDown') {
|
|
495
|
+
next = addDays(next, 7);
|
|
496
|
+
skipDir = 1;
|
|
497
|
+
} else if (key === 'Home') {
|
|
498
|
+
next = startOfWeekSunday(next);
|
|
499
|
+
skipDir = 1;
|
|
500
|
+
} else if (key === 'End') {
|
|
501
|
+
next = endOfWeekSunday(next);
|
|
502
|
+
skipDir = -1;
|
|
503
|
+
} else if (key === 'PageUp') {
|
|
504
|
+
next = addMonthsClamped(next, -1);
|
|
505
|
+
skipDir = -1;
|
|
506
|
+
} else if (key === 'PageDown') {
|
|
507
|
+
next = addMonthsClamped(next, 1);
|
|
508
|
+
skipDir = 1;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
next = skipDisabled(next, skipDir, 400);
|
|
512
|
+
|
|
513
|
+
if (next.getMonth() !== viewMonth || next.getFullYear() !== viewYear) {
|
|
514
|
+
viewYear = next.getFullYear();
|
|
515
|
+
viewMonth = next.getMonth();
|
|
516
|
+
const cl = ensureMonthInRange(viewYear, viewMonth);
|
|
517
|
+
viewYear = cl.y;
|
|
518
|
+
viewMonth = cl.m;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
focusedDate = next;
|
|
522
|
+
render();
|
|
523
|
+
requestAnimationFrame(focusFocusedDay);
|
|
232
524
|
};
|
|
233
525
|
|
|
234
526
|
const open = () => {
|
|
527
|
+
viewMode = 'days';
|
|
528
|
+
if (selectedDate) {
|
|
529
|
+
viewYear = selectedDate.getFullYear();
|
|
530
|
+
viewMonth = selectedDate.getMonth();
|
|
531
|
+
}
|
|
532
|
+
const cl = ensureMonthInRange(viewYear, viewMonth);
|
|
533
|
+
viewYear = cl.y;
|
|
534
|
+
viewMonth = cl.m;
|
|
535
|
+
|
|
536
|
+
if (selectedDate) {
|
|
537
|
+
focusedDate = new Date(selectedDate.getFullYear(), selectedDate.getMonth(), selectedDate.getDate());
|
|
538
|
+
} else {
|
|
539
|
+
focusedDate = firstSelectableInMonth(viewYear, viewMonth);
|
|
540
|
+
}
|
|
541
|
+
|
|
235
542
|
render();
|
|
236
543
|
popup.classList.add('is-open');
|
|
237
544
|
input.setAttribute('aria-expanded', 'true');
|
|
545
|
+
requestAnimationFrame(focusFocusedDay);
|
|
238
546
|
};
|
|
239
547
|
|
|
240
548
|
const close = () => {
|
|
@@ -243,16 +551,29 @@
|
|
|
243
551
|
viewMode = 'days';
|
|
244
552
|
};
|
|
245
553
|
|
|
246
|
-
|
|
247
|
-
|
|
554
|
+
const focusHandler = () => {
|
|
555
|
+
if (skipNextFocusOpen) {
|
|
556
|
+
skipNextFocusOpen = false;
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
open();
|
|
560
|
+
};
|
|
248
561
|
const outsideHandler = (e) => {
|
|
249
562
|
if (!wrapper.contains(e.target)) close();
|
|
250
563
|
};
|
|
251
|
-
const escHandler = (e) => {
|
|
564
|
+
const escHandler = (e) => {
|
|
565
|
+
if (e.key === 'Escape' && popup.classList.contains('is-open')) {
|
|
566
|
+
skipNextFocusOpen = true;
|
|
567
|
+
close();
|
|
568
|
+
input.focus();
|
|
569
|
+
}
|
|
570
|
+
};
|
|
252
571
|
|
|
253
572
|
input.addEventListener('focus', focusHandler);
|
|
254
573
|
document.addEventListener('click', outsideHandler, true);
|
|
255
574
|
document.addEventListener('keydown', escHandler);
|
|
575
|
+
popup.addEventListener('keydown', handleGridKeydown);
|
|
576
|
+
|
|
256
577
|
input.setAttribute('aria-haspopup', 'dialog');
|
|
257
578
|
input.setAttribute('aria-expanded', 'false');
|
|
258
579
|
input.setAttribute('autocomplete', 'off');
|
|
@@ -260,10 +581,11 @@
|
|
|
260
581
|
cleanup.push(
|
|
261
582
|
() => input.removeEventListener('focus', focusHandler),
|
|
262
583
|
() => document.removeEventListener('click', outsideHandler, true),
|
|
263
|
-
() => document.removeEventListener('keydown', escHandler)
|
|
584
|
+
() => document.removeEventListener('keydown', escHandler),
|
|
585
|
+
() => popup.removeEventListener('keydown', handleGridKeydown)
|
|
264
586
|
);
|
|
265
587
|
|
|
266
|
-
this.instances.set(input, { cleanup, open, close, popup });
|
|
588
|
+
this.instances.set(input, { cleanup: cleanup, open: open, close: close, popup: popup });
|
|
267
589
|
},
|
|
268
590
|
|
|
269
591
|
destroy: function (el) {
|