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