@duskmoon-dev/el-datepicker 0.4.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/dist/cjs/index.js +625 -0
- package/dist/cjs/register.js +628 -0
- package/dist/esm/index.js +593 -0
- package/dist/esm/register.js +592 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/dist/types/el-dm-datepicker.d.ts +113 -0
- package/dist/types/el-dm-datepicker.d.ts.map +1 -0
- package/dist/types/index.d.ts +19 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/register.d.ts +2 -0
- package/dist/types/register.d.ts.map +1 -0
- package/package.json +60 -0
- package/src/duskmoon-core.d.ts +4 -0
- package/src/el-dm-datepicker.ts +701 -0
- package/src/index.ts +25 -0
- package/src/register.ts +2 -0
|
@@ -0,0 +1,701 @@
|
|
|
1
|
+
import { BaseElement, css as cssTag } from '@duskmoon-dev/el-core';
|
|
2
|
+
import { css } from '@duskmoon-dev/core/components/datepicker';
|
|
3
|
+
|
|
4
|
+
// Strip @layer components wrapper for Shadow DOM
|
|
5
|
+
const strippedCss = css
|
|
6
|
+
.replace(/@layer\s+components\s*\{/, '')
|
|
7
|
+
.replace(/\}[\s]*$/, '');
|
|
8
|
+
|
|
9
|
+
const styles = cssTag`
|
|
10
|
+
${strippedCss}
|
|
11
|
+
|
|
12
|
+
:host {
|
|
13
|
+
display: block;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
:host([hidden]) {
|
|
17
|
+
display: none;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.datepicker {
|
|
21
|
+
width: 100%;
|
|
22
|
+
}
|
|
23
|
+
`;
|
|
24
|
+
|
|
25
|
+
export type DatepickerSize = 'sm' | 'md' | 'lg';
|
|
26
|
+
type ViewMode = 'days' | 'months' | 'years';
|
|
27
|
+
|
|
28
|
+
const WEEKDAYS = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'];
|
|
29
|
+
const MONTHS = [
|
|
30
|
+
'January',
|
|
31
|
+
'February',
|
|
32
|
+
'March',
|
|
33
|
+
'April',
|
|
34
|
+
'May',
|
|
35
|
+
'June',
|
|
36
|
+
'July',
|
|
37
|
+
'August',
|
|
38
|
+
'September',
|
|
39
|
+
'October',
|
|
40
|
+
'November',
|
|
41
|
+
'December',
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
export class ElDmDatepicker extends BaseElement {
|
|
45
|
+
static properties = {
|
|
46
|
+
value: { type: String, reflect: true, default: '' },
|
|
47
|
+
disabled: { type: Boolean, reflect: true, default: false },
|
|
48
|
+
placeholder: { type: String, reflect: true, default: 'Select date' },
|
|
49
|
+
format: { type: String, reflect: true, default: 'YYYY-MM-DD' },
|
|
50
|
+
minDate: { type: String, reflect: true, default: '' },
|
|
51
|
+
maxDate: { type: String, reflect: true, default: '' },
|
|
52
|
+
range: { type: Boolean, reflect: true, default: false },
|
|
53
|
+
showTime: { type: Boolean, reflect: true, default: false },
|
|
54
|
+
size: { type: String, reflect: true, default: 'md' },
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
value!: string;
|
|
58
|
+
disabled!: boolean;
|
|
59
|
+
placeholder!: string;
|
|
60
|
+
format!: string;
|
|
61
|
+
minDate!: string;
|
|
62
|
+
maxDate!: string;
|
|
63
|
+
range!: boolean;
|
|
64
|
+
showTime!: boolean;
|
|
65
|
+
size!: DatepickerSize;
|
|
66
|
+
|
|
67
|
+
private _isOpen = false;
|
|
68
|
+
private _viewMode: ViewMode = 'days';
|
|
69
|
+
private _viewDate = new Date();
|
|
70
|
+
private _selectedDate: Date | null = null;
|
|
71
|
+
private _rangeStart: Date | null = null;
|
|
72
|
+
private _rangeEnd: Date | null = null;
|
|
73
|
+
private _hours = 12;
|
|
74
|
+
private _minutes = 0;
|
|
75
|
+
private _period: 'AM' | 'PM' = 'AM';
|
|
76
|
+
|
|
77
|
+
constructor() {
|
|
78
|
+
super();
|
|
79
|
+
this.attachStyles(styles);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
connectedCallback() {
|
|
83
|
+
super.connectedCallback();
|
|
84
|
+
this._parseValue();
|
|
85
|
+
document.addEventListener('click', this._handleOutsideClick);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
disconnectedCallback() {
|
|
89
|
+
super.disconnectedCallback();
|
|
90
|
+
document.removeEventListener('click', this._handleOutsideClick);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
private _handleOutsideClick = (e: MouseEvent) => {
|
|
94
|
+
if (!this.contains(e.target as Node)) {
|
|
95
|
+
this._close();
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
private _parseValue() {
|
|
100
|
+
if (!this.value) return;
|
|
101
|
+
|
|
102
|
+
if (this.range) {
|
|
103
|
+
const [start, end] = this.value.split(' - ');
|
|
104
|
+
if (start) this._rangeStart = new Date(start);
|
|
105
|
+
if (end) this._rangeEnd = new Date(end);
|
|
106
|
+
if (this._rangeStart) {
|
|
107
|
+
this._viewDate = new Date(this._rangeStart);
|
|
108
|
+
}
|
|
109
|
+
} else {
|
|
110
|
+
const date = new Date(this.value);
|
|
111
|
+
if (!isNaN(date.getTime())) {
|
|
112
|
+
this._selectedDate = date;
|
|
113
|
+
this._viewDate = new Date(date);
|
|
114
|
+
|
|
115
|
+
if (this.showTime) {
|
|
116
|
+
this._hours = date.getHours() % 12 || 12;
|
|
117
|
+
this._minutes = date.getMinutes();
|
|
118
|
+
this._period = date.getHours() >= 12 ? 'PM' : 'AM';
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private _open() {
|
|
125
|
+
if (this.disabled) return;
|
|
126
|
+
this._isOpen = true;
|
|
127
|
+
this._viewMode = 'days';
|
|
128
|
+
this.emit('open');
|
|
129
|
+
this.update();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
private _close() {
|
|
133
|
+
if (!this._isOpen) return;
|
|
134
|
+
this._isOpen = false;
|
|
135
|
+
this.emit('close');
|
|
136
|
+
this.update();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
private _toggle() {
|
|
140
|
+
if (this._isOpen) {
|
|
141
|
+
this._close();
|
|
142
|
+
} else {
|
|
143
|
+
this._open();
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
private _formatDate(date: Date | null): string {
|
|
148
|
+
if (!date) return '';
|
|
149
|
+
|
|
150
|
+
const year = date.getFullYear();
|
|
151
|
+
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
152
|
+
const day = String(date.getDate()).padStart(2, '0');
|
|
153
|
+
|
|
154
|
+
let formatted = this.format
|
|
155
|
+
.replace('YYYY', String(year))
|
|
156
|
+
.replace('MM', month)
|
|
157
|
+
.replace('DD', day);
|
|
158
|
+
|
|
159
|
+
if (this.showTime) {
|
|
160
|
+
const hours = String(this._hours).padStart(2, '0');
|
|
161
|
+
const minutes = String(this._minutes).padStart(2, '0');
|
|
162
|
+
formatted += ` ${hours}:${minutes} ${this._period}`;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return formatted;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
private _getDisplayValue(): string {
|
|
169
|
+
if (this.range) {
|
|
170
|
+
if (this._rangeStart && this._rangeEnd) {
|
|
171
|
+
return `${this._formatDate(this._rangeStart)} - ${this._formatDate(this._rangeEnd)}`;
|
|
172
|
+
} else if (this._rangeStart) {
|
|
173
|
+
return this._formatDate(this._rangeStart);
|
|
174
|
+
}
|
|
175
|
+
return '';
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return this._formatDate(this._selectedDate);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
private _prevMonth() {
|
|
182
|
+
this._viewDate.setMonth(this._viewDate.getMonth() - 1);
|
|
183
|
+
this.update();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
private _nextMonth() {
|
|
187
|
+
this._viewDate.setMonth(this._viewDate.getMonth() + 1);
|
|
188
|
+
this.update();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
private _prevYear() {
|
|
192
|
+
this._viewDate.setFullYear(this._viewDate.getFullYear() - 1);
|
|
193
|
+
this.update();
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
private _nextYear() {
|
|
197
|
+
this._viewDate.setFullYear(this._viewDate.getFullYear() + 1);
|
|
198
|
+
this.update();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
private _setViewMode(mode: ViewMode) {
|
|
202
|
+
this._viewMode = mode;
|
|
203
|
+
this.update();
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
private _selectMonth(month: number) {
|
|
207
|
+
this._viewDate.setMonth(month);
|
|
208
|
+
this._viewMode = 'days';
|
|
209
|
+
this.update();
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
private _selectYear(year: number) {
|
|
213
|
+
this._viewDate.setFullYear(year);
|
|
214
|
+
this._viewMode = 'months';
|
|
215
|
+
this.update();
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
private _isDateDisabled(date: Date): boolean {
|
|
219
|
+
if (this.minDate) {
|
|
220
|
+
const min = new Date(this.minDate);
|
|
221
|
+
if (date < min) return true;
|
|
222
|
+
}
|
|
223
|
+
if (this.maxDate) {
|
|
224
|
+
const max = new Date(this.maxDate);
|
|
225
|
+
if (date > max) return true;
|
|
226
|
+
}
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
private _isToday(date: Date): boolean {
|
|
231
|
+
const today = new Date();
|
|
232
|
+
return (
|
|
233
|
+
date.getDate() === today.getDate() &&
|
|
234
|
+
date.getMonth() === today.getMonth() &&
|
|
235
|
+
date.getFullYear() === today.getFullYear()
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
private _isSelected(date: Date): boolean {
|
|
240
|
+
if (this.range) {
|
|
241
|
+
if (this._rangeStart) {
|
|
242
|
+
if (this._isSameDay(date, this._rangeStart)) return true;
|
|
243
|
+
}
|
|
244
|
+
if (this._rangeEnd) {
|
|
245
|
+
if (this._isSameDay(date, this._rangeEnd)) return true;
|
|
246
|
+
}
|
|
247
|
+
return false;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return this._selectedDate ? this._isSameDay(date, this._selectedDate) : false;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
private _isInRange(date: Date): boolean {
|
|
254
|
+
if (!this.range || !this._rangeStart || !this._rangeEnd) return false;
|
|
255
|
+
return date > this._rangeStart && date < this._rangeEnd;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
private _isRangeStart(date: Date): boolean {
|
|
259
|
+
return this._rangeStart ? this._isSameDay(date, this._rangeStart) : false;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
private _isRangeEnd(date: Date): boolean {
|
|
263
|
+
return this._rangeEnd ? this._isSameDay(date, this._rangeEnd) : false;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
private _isSameDay(a: Date, b: Date): boolean {
|
|
267
|
+
return (
|
|
268
|
+
a.getDate() === b.getDate() &&
|
|
269
|
+
a.getMonth() === b.getMonth() &&
|
|
270
|
+
a.getFullYear() === b.getFullYear()
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
private _selectDate(date: Date) {
|
|
275
|
+
if (this._isDateDisabled(date)) return;
|
|
276
|
+
|
|
277
|
+
if (this.range) {
|
|
278
|
+
if (!this._rangeStart || (this._rangeStart && this._rangeEnd)) {
|
|
279
|
+
this._rangeStart = date;
|
|
280
|
+
this._rangeEnd = null;
|
|
281
|
+
} else {
|
|
282
|
+
if (date < this._rangeStart) {
|
|
283
|
+
this._rangeEnd = this._rangeStart;
|
|
284
|
+
this._rangeStart = date;
|
|
285
|
+
} else {
|
|
286
|
+
this._rangeEnd = date;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
this._updateValue();
|
|
290
|
+
if (!this.showTime) {
|
|
291
|
+
this._close();
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
} else {
|
|
295
|
+
this._selectedDate = date;
|
|
296
|
+
this._updateValue();
|
|
297
|
+
if (!this.showTime) {
|
|
298
|
+
this._close();
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
this.update();
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
private _updateValue() {
|
|
306
|
+
if (this.range) {
|
|
307
|
+
if (this._rangeStart && this._rangeEnd) {
|
|
308
|
+
this.value = `${this._formatDate(this._rangeStart)} - ${this._formatDate(this._rangeEnd)}`;
|
|
309
|
+
} else if (this._rangeStart) {
|
|
310
|
+
this.value = this._formatDate(this._rangeStart);
|
|
311
|
+
}
|
|
312
|
+
} else if (this._selectedDate) {
|
|
313
|
+
if (this.showTime) {
|
|
314
|
+
const hours24 =
|
|
315
|
+
this._period === 'PM'
|
|
316
|
+
? this._hours === 12
|
|
317
|
+
? 12
|
|
318
|
+
: this._hours + 12
|
|
319
|
+
: this._hours === 12
|
|
320
|
+
? 0
|
|
321
|
+
: this._hours;
|
|
322
|
+
this._selectedDate.setHours(hours24, this._minutes);
|
|
323
|
+
}
|
|
324
|
+
this.value = this._selectedDate.toISOString();
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
this.emit('change', {
|
|
328
|
+
value: this.value,
|
|
329
|
+
date: this._selectedDate,
|
|
330
|
+
rangeStart: this._rangeStart,
|
|
331
|
+
rangeEnd: this._rangeEnd,
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
private _getCalendarDays(): Array<{ date: Date; isOtherMonth: boolean }> {
|
|
336
|
+
const year = this._viewDate.getFullYear();
|
|
337
|
+
const month = this._viewDate.getMonth();
|
|
338
|
+
|
|
339
|
+
const firstDay = new Date(year, month, 1);
|
|
340
|
+
const lastDay = new Date(year, month + 1, 0);
|
|
341
|
+
|
|
342
|
+
const days: Array<{ date: Date; isOtherMonth: boolean }> = [];
|
|
343
|
+
|
|
344
|
+
// Previous month days
|
|
345
|
+
const startPadding = firstDay.getDay();
|
|
346
|
+
for (let i = startPadding - 1; i >= 0; i--) {
|
|
347
|
+
const date = new Date(year, month, -i);
|
|
348
|
+
days.push({ date, isOtherMonth: true });
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Current month days
|
|
352
|
+
for (let i = 1; i <= lastDay.getDate(); i++) {
|
|
353
|
+
const date = new Date(year, month, i);
|
|
354
|
+
days.push({ date, isOtherMonth: false });
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Next month days (fill to complete 6 rows)
|
|
358
|
+
const remaining = 42 - days.length;
|
|
359
|
+
for (let i = 1; i <= remaining; i++) {
|
|
360
|
+
const date = new Date(year, month + 1, i);
|
|
361
|
+
days.push({ date, isOtherMonth: true });
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return days;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
private _renderDays(): string {
|
|
368
|
+
const days = this._getCalendarDays();
|
|
369
|
+
|
|
370
|
+
return `
|
|
371
|
+
<div class="datepicker-weekdays">
|
|
372
|
+
${WEEKDAYS.map((d) => `<div class="datepicker-weekday">${d}</div>`).join('')}
|
|
373
|
+
</div>
|
|
374
|
+
<div class="datepicker-days">
|
|
375
|
+
${days
|
|
376
|
+
.map(({ date, isOtherMonth }) => {
|
|
377
|
+
const classes = [
|
|
378
|
+
'datepicker-day',
|
|
379
|
+
isOtherMonth ? 'datepicker-day-other-month' : '',
|
|
380
|
+
this._isToday(date) ? 'datepicker-day-today' : '',
|
|
381
|
+
this._isSelected(date) ? 'datepicker-day-selected' : '',
|
|
382
|
+
this._isInRange(date) ? 'datepicker-day-in-range' : '',
|
|
383
|
+
this._isRangeStart(date) ? 'datepicker-day-range-start' : '',
|
|
384
|
+
this._isRangeEnd(date) ? 'datepicker-day-range-end' : '',
|
|
385
|
+
]
|
|
386
|
+
.filter(Boolean)
|
|
387
|
+
.join(' ');
|
|
388
|
+
|
|
389
|
+
return `
|
|
390
|
+
<button
|
|
391
|
+
type="button"
|
|
392
|
+
class="${classes}"
|
|
393
|
+
data-date="${date.toISOString()}"
|
|
394
|
+
${this._isDateDisabled(date) ? 'disabled' : ''}
|
|
395
|
+
>
|
|
396
|
+
${date.getDate()}
|
|
397
|
+
</button>
|
|
398
|
+
`;
|
|
399
|
+
})
|
|
400
|
+
.join('')}
|
|
401
|
+
</div>
|
|
402
|
+
`;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
private _renderMonths(): string {
|
|
406
|
+
const currentMonth = this._viewDate.getMonth();
|
|
407
|
+
|
|
408
|
+
return `
|
|
409
|
+
<div class="datepicker-months">
|
|
410
|
+
${MONTHS.map(
|
|
411
|
+
(name, i) => `
|
|
412
|
+
<button
|
|
413
|
+
type="button"
|
|
414
|
+
class="datepicker-month ${i === currentMonth ? 'selected' : ''}"
|
|
415
|
+
data-month="${i}"
|
|
416
|
+
>
|
|
417
|
+
${name.slice(0, 3)}
|
|
418
|
+
</button>
|
|
419
|
+
`
|
|
420
|
+
).join('')}
|
|
421
|
+
</div>
|
|
422
|
+
`;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
private _renderYears(): string {
|
|
426
|
+
const currentYear = this._viewDate.getFullYear();
|
|
427
|
+
const startYear = currentYear - 5;
|
|
428
|
+
|
|
429
|
+
return `
|
|
430
|
+
<div class="datepicker-years">
|
|
431
|
+
${Array.from(
|
|
432
|
+
{ length: 12 },
|
|
433
|
+
(_, i) => `
|
|
434
|
+
<button
|
|
435
|
+
type="button"
|
|
436
|
+
class="datepicker-year ${i + startYear === currentYear ? 'selected' : ''}"
|
|
437
|
+
data-year="${i + startYear}"
|
|
438
|
+
>
|
|
439
|
+
${i + startYear}
|
|
440
|
+
</button>
|
|
441
|
+
`
|
|
442
|
+
).join('')}
|
|
443
|
+
</div>
|
|
444
|
+
`;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
private _renderTime(): string {
|
|
448
|
+
if (!this.showTime) return '';
|
|
449
|
+
|
|
450
|
+
return `
|
|
451
|
+
<div class="datepicker-time">
|
|
452
|
+
<input
|
|
453
|
+
type="text"
|
|
454
|
+
class="datepicker-time-input"
|
|
455
|
+
value="${String(this._hours).padStart(2, '0')}"
|
|
456
|
+
data-time="hours"
|
|
457
|
+
maxlength="2"
|
|
458
|
+
/>
|
|
459
|
+
<span class="datepicker-time-separator">:</span>
|
|
460
|
+
<input
|
|
461
|
+
type="text"
|
|
462
|
+
class="datepicker-time-input"
|
|
463
|
+
value="${String(this._minutes).padStart(2, '0')}"
|
|
464
|
+
data-time="minutes"
|
|
465
|
+
maxlength="2"
|
|
466
|
+
/>
|
|
467
|
+
<div class="datepicker-time-period">
|
|
468
|
+
<button
|
|
469
|
+
type="button"
|
|
470
|
+
class="datepicker-time-period-btn ${this._period === 'AM' ? 'active' : ''}"
|
|
471
|
+
data-period="AM"
|
|
472
|
+
>AM</button>
|
|
473
|
+
<button
|
|
474
|
+
type="button"
|
|
475
|
+
class="datepicker-time-period-btn ${this._period === 'PM' ? 'active' : ''}"
|
|
476
|
+
data-period="PM"
|
|
477
|
+
>PM</button>
|
|
478
|
+
</div>
|
|
479
|
+
</div>
|
|
480
|
+
`;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
private _renderHeader(): string {
|
|
484
|
+
const title =
|
|
485
|
+
this._viewMode === 'days'
|
|
486
|
+
? `${MONTHS[this._viewDate.getMonth()]} ${this._viewDate.getFullYear()}`
|
|
487
|
+
: this._viewMode === 'months'
|
|
488
|
+
? String(this._viewDate.getFullYear())
|
|
489
|
+
: `${this._viewDate.getFullYear() - 5} - ${this._viewDate.getFullYear() + 6}`;
|
|
490
|
+
|
|
491
|
+
return `
|
|
492
|
+
<div class="datepicker-header">
|
|
493
|
+
<div class="datepicker-nav">
|
|
494
|
+
<button type="button" class="datepicker-nav-btn" data-nav="prev"><</button>
|
|
495
|
+
</div>
|
|
496
|
+
<button type="button" class="datepicker-title" data-action="toggle-view">
|
|
497
|
+
${title}
|
|
498
|
+
</button>
|
|
499
|
+
<div class="datepicker-nav">
|
|
500
|
+
<button type="button" class="datepicker-nav-btn" data-nav="next">></button>
|
|
501
|
+
</div>
|
|
502
|
+
</div>
|
|
503
|
+
`;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
render() {
|
|
507
|
+
const sizeClass = this.size !== 'md' ? `datepicker-${this.size}` : '';
|
|
508
|
+
const openClass = this._isOpen ? 'datepicker-open' : '';
|
|
509
|
+
|
|
510
|
+
let calendarContent = '';
|
|
511
|
+
switch (this._viewMode) {
|
|
512
|
+
case 'days':
|
|
513
|
+
calendarContent = this._renderDays();
|
|
514
|
+
break;
|
|
515
|
+
case 'months':
|
|
516
|
+
calendarContent = this._renderMonths();
|
|
517
|
+
break;
|
|
518
|
+
case 'years':
|
|
519
|
+
calendarContent = this._renderYears();
|
|
520
|
+
break;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
return `
|
|
524
|
+
<div class="datepicker ${sizeClass} ${openClass}">
|
|
525
|
+
<input
|
|
526
|
+
type="text"
|
|
527
|
+
class="datepicker-input"
|
|
528
|
+
placeholder="${this.placeholder}"
|
|
529
|
+
value="${this._getDisplayValue()}"
|
|
530
|
+
${this.disabled ? 'disabled' : ''}
|
|
531
|
+
readonly
|
|
532
|
+
/>
|
|
533
|
+
<div class="datepicker-icon">
|
|
534
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
535
|
+
<rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect>
|
|
536
|
+
<line x1="16" y1="2" x2="16" y2="6"></line>
|
|
537
|
+
<line x1="8" y1="2" x2="8" y2="6"></line>
|
|
538
|
+
<line x1="3" y1="10" x2="21" y2="10"></line>
|
|
539
|
+
</svg>
|
|
540
|
+
</div>
|
|
541
|
+
<div class="datepicker-dropdown">
|
|
542
|
+
${this._renderHeader()}
|
|
543
|
+
<div class="datepicker-calendar">
|
|
544
|
+
${calendarContent}
|
|
545
|
+
</div>
|
|
546
|
+
${this._renderTime()}
|
|
547
|
+
</div>
|
|
548
|
+
</div>
|
|
549
|
+
`;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
update() {
|
|
553
|
+
super.update();
|
|
554
|
+
this._attachEventListeners();
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
private _attachEventListeners() {
|
|
558
|
+
const input = this.shadowRoot?.querySelector('.datepicker-input');
|
|
559
|
+
if (input) {
|
|
560
|
+
input.removeEventListener('click', this._toggle.bind(this));
|
|
561
|
+
input.addEventListener('click', this._toggle.bind(this));
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// Navigation
|
|
565
|
+
const prevBtn = this.shadowRoot?.querySelector('[data-nav="prev"]');
|
|
566
|
+
const nextBtn = this.shadowRoot?.querySelector('[data-nav="next"]');
|
|
567
|
+
|
|
568
|
+
if (prevBtn) {
|
|
569
|
+
prevBtn.removeEventListener('click', this._handlePrev);
|
|
570
|
+
prevBtn.addEventListener('click', this._handlePrev);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
if (nextBtn) {
|
|
574
|
+
nextBtn.removeEventListener('click', this._handleNext);
|
|
575
|
+
nextBtn.addEventListener('click', this._handleNext);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// Title toggle view
|
|
579
|
+
const titleBtn = this.shadowRoot?.querySelector('[data-action="toggle-view"]');
|
|
580
|
+
if (titleBtn) {
|
|
581
|
+
titleBtn.removeEventListener('click', this._handleToggleView);
|
|
582
|
+
titleBtn.addEventListener('click', this._handleToggleView);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// Day selection
|
|
586
|
+
const dayBtns = this.shadowRoot?.querySelectorAll('.datepicker-day');
|
|
587
|
+
dayBtns?.forEach((btn) => {
|
|
588
|
+
btn.removeEventListener('click', this._handleDayClick);
|
|
589
|
+
btn.addEventListener('click', this._handleDayClick);
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
// Month selection
|
|
593
|
+
const monthBtns = this.shadowRoot?.querySelectorAll('.datepicker-month');
|
|
594
|
+
monthBtns?.forEach((btn) => {
|
|
595
|
+
btn.removeEventListener('click', this._handleMonthClick);
|
|
596
|
+
btn.addEventListener('click', this._handleMonthClick);
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
// Year selection
|
|
600
|
+
const yearBtns = this.shadowRoot?.querySelectorAll('.datepicker-year');
|
|
601
|
+
yearBtns?.forEach((btn) => {
|
|
602
|
+
btn.removeEventListener('click', this._handleYearClick);
|
|
603
|
+
btn.addEventListener('click', this._handleYearClick);
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
// Time inputs
|
|
607
|
+
const timeInputs = this.shadowRoot?.querySelectorAll('.datepicker-time-input');
|
|
608
|
+
timeInputs?.forEach((input) => {
|
|
609
|
+
input.removeEventListener('change', this._handleTimeChange);
|
|
610
|
+
input.addEventListener('change', this._handleTimeChange);
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
// Period buttons
|
|
614
|
+
const periodBtns = this.shadowRoot?.querySelectorAll('.datepicker-time-period-btn');
|
|
615
|
+
periodBtns?.forEach((btn) => {
|
|
616
|
+
btn.removeEventListener('click', this._handlePeriodClick);
|
|
617
|
+
btn.addEventListener('click', this._handlePeriodClick);
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
private _handlePrev = () => {
|
|
622
|
+
if (this._viewMode === 'days') {
|
|
623
|
+
this._prevMonth();
|
|
624
|
+
} else if (this._viewMode === 'months') {
|
|
625
|
+
this._prevYear();
|
|
626
|
+
} else {
|
|
627
|
+
this._viewDate.setFullYear(this._viewDate.getFullYear() - 12);
|
|
628
|
+
this.update();
|
|
629
|
+
}
|
|
630
|
+
};
|
|
631
|
+
|
|
632
|
+
private _handleNext = () => {
|
|
633
|
+
if (this._viewMode === 'days') {
|
|
634
|
+
this._nextMonth();
|
|
635
|
+
} else if (this._viewMode === 'months') {
|
|
636
|
+
this._nextYear();
|
|
637
|
+
} else {
|
|
638
|
+
this._viewDate.setFullYear(this._viewDate.getFullYear() + 12);
|
|
639
|
+
this.update();
|
|
640
|
+
}
|
|
641
|
+
};
|
|
642
|
+
|
|
643
|
+
private _handleToggleView = () => {
|
|
644
|
+
if (this._viewMode === 'days') {
|
|
645
|
+
this._setViewMode('months');
|
|
646
|
+
} else if (this._viewMode === 'months') {
|
|
647
|
+
this._setViewMode('years');
|
|
648
|
+
} else {
|
|
649
|
+
this._setViewMode('days');
|
|
650
|
+
}
|
|
651
|
+
};
|
|
652
|
+
|
|
653
|
+
private _handleDayClick = (e: Event) => {
|
|
654
|
+
const btn = e.currentTarget as HTMLElement;
|
|
655
|
+
const dateStr = btn.dataset.date;
|
|
656
|
+
if (dateStr) {
|
|
657
|
+
this._selectDate(new Date(dateStr));
|
|
658
|
+
}
|
|
659
|
+
};
|
|
660
|
+
|
|
661
|
+
private _handleMonthClick = (e: Event) => {
|
|
662
|
+
const btn = e.currentTarget as HTMLElement;
|
|
663
|
+
const month = btn.dataset.month;
|
|
664
|
+
if (month !== undefined) {
|
|
665
|
+
this._selectMonth(parseInt(month, 10));
|
|
666
|
+
}
|
|
667
|
+
};
|
|
668
|
+
|
|
669
|
+
private _handleYearClick = (e: Event) => {
|
|
670
|
+
const btn = e.currentTarget as HTMLElement;
|
|
671
|
+
const year = btn.dataset.year;
|
|
672
|
+
if (year !== undefined) {
|
|
673
|
+
this._selectYear(parseInt(year, 10));
|
|
674
|
+
}
|
|
675
|
+
};
|
|
676
|
+
|
|
677
|
+
private _handleTimeChange = (e: Event) => {
|
|
678
|
+
const input = e.target as HTMLInputElement;
|
|
679
|
+
const type = input.dataset.time;
|
|
680
|
+
const value = parseInt(input.value, 10);
|
|
681
|
+
|
|
682
|
+
if (type === 'hours') {
|
|
683
|
+
this._hours = Math.max(1, Math.min(12, value || 12));
|
|
684
|
+
} else if (type === 'minutes') {
|
|
685
|
+
this._minutes = Math.max(0, Math.min(59, value || 0));
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
this._updateValue();
|
|
689
|
+
this.update();
|
|
690
|
+
};
|
|
691
|
+
|
|
692
|
+
private _handlePeriodClick = (e: Event) => {
|
|
693
|
+
const btn = e.currentTarget as HTMLElement;
|
|
694
|
+
const period = btn.dataset.period as 'AM' | 'PM';
|
|
695
|
+
if (period) {
|
|
696
|
+
this._period = period;
|
|
697
|
+
this._updateValue();
|
|
698
|
+
this.update();
|
|
699
|
+
}
|
|
700
|
+
};
|
|
701
|
+
}
|