@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,593 @@
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
+ export {
591
+ register,
592
+ ElDmDatepicker
593
+ };