@lekoala/date-picker 0.4.0 → 0.5.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/README.md +281 -265
- package/custom-elements.json +10 -0
- package/dist/date-picker.css +20 -15
- package/dist/date-picker.js +199 -115
- package/dist/date-picker.min.css +1 -1
- package/dist/date-picker.min.js +13 -13
- package/dist/date-picker.standalone.min.js +34 -29
- package/dist/types/control-classes.d.ts +30 -0
- package/dist/types/control-classes.d.ts.map +1 -0
- package/dist/types/date-calendar.d.ts +15 -0
- package/dist/types/date-calendar.d.ts.map +1 -1
- package/dist/types/date-picker.d.ts +15 -0
- package/dist/types/date-picker.d.ts.map +1 -1
- package/dist/types/date.d.ts +4 -2
- package/dist/types/date.d.ts.map +1 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/intl.d.ts.map +1 -1
- package/dist/types/source.d.ts +1 -0
- package/dist/types/source.d.ts.map +1 -1
- package/docs/API.md +430 -408
- package/docs/DECISIONS.md +68 -62
- package/docs/USE_CASES.md +156 -144
- package/package.json +3 -3
- package/src/control-classes.js +50 -0
- package/src/date-calendar.js +47 -16
- package/src/date-field.js +1 -1
- package/src/date-picker.css +20 -15
- package/src/date-picker.js +47 -46
- package/src/date.js +8 -3
- package/src/index.js +1 -0
- package/src/intl.js +39 -10
- package/src/source.js +14 -4
package/src/date-calendar.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { CalendarModel } from "./calendar-model.js";
|
|
2
|
+
import { getDefaultControlClasses, resolveControlClasses, sameControlClasses } from "./control-classes.js";
|
|
2
3
|
import {
|
|
3
4
|
addDays,
|
|
4
5
|
clampDate,
|
|
@@ -155,7 +156,10 @@ export class DateCalendarElement extends HTMLElement {
|
|
|
155
156
|
/** @type {DateRange} */
|
|
156
157
|
this._paintedRange = { start: "", end: "" };
|
|
157
158
|
this._paintedPreview = false;
|
|
159
|
+
/** @type {Map<string, DateState>} States of the current rendered grid. */
|
|
160
|
+
this._renderedStates = new Map();
|
|
158
161
|
this._messages = getDefaultMessages();
|
|
162
|
+
this._controlClasses = getDefaultControlClasses();
|
|
159
163
|
this._onClick = this._onClick.bind(this);
|
|
160
164
|
this._onChange = this._onChange.bind(this);
|
|
161
165
|
this._onKeyDown = this._onKeyDown.bind(this);
|
|
@@ -323,6 +327,23 @@ export class DateCalendarElement extends HTMLElement {
|
|
|
323
327
|
this.toggleAttribute("show-week-numbers", Boolean(value));
|
|
324
328
|
}
|
|
325
329
|
|
|
330
|
+
/**
|
|
331
|
+
* Author classes for header controls; merged with global defaults.
|
|
332
|
+
* @public
|
|
333
|
+
* @returns {import("./control-classes.js").ControlClasses}
|
|
334
|
+
*/
|
|
335
|
+
get controlClasses() {
|
|
336
|
+
return { ...this._controlClasses };
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/** @param {Partial<import("./control-classes.js").ControlClasses> | null} value */
|
|
340
|
+
set controlClasses(value) {
|
|
341
|
+
const classes = resolveControlClasses(value);
|
|
342
|
+
if (sameControlClasses(classes, this._controlClasses)) return;
|
|
343
|
+
this._controlClasses = classes;
|
|
344
|
+
if (this._connected) this.render();
|
|
345
|
+
}
|
|
346
|
+
|
|
326
347
|
/** @public */
|
|
327
348
|
get messages() {
|
|
328
349
|
return this._messages;
|
|
@@ -420,16 +441,10 @@ export class DateCalendarElement extends HTMLElement {
|
|
|
420
441
|
}
|
|
421
442
|
|
|
422
443
|
_weeks(display = this.display) {
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
const week = [];
|
|
428
|
-
for (let i = 1; i <= 7; i++) week.push(addDays(cursor, i));
|
|
429
|
-
weeks.push(week);
|
|
430
|
-
cursor = week[6];
|
|
431
|
-
}
|
|
432
|
-
return weeks;
|
|
444
|
+
return getMonthWeeks(clampDisplayMonth(display, this.firstDay), {
|
|
445
|
+
firstDay: this.firstDay,
|
|
446
|
+
fixedWeeks: this.fixedWeeks,
|
|
447
|
+
});
|
|
433
448
|
}
|
|
434
449
|
|
|
435
450
|
/** @param {HTMLElement} element @returns {string} */
|
|
@@ -647,17 +662,27 @@ export class DateCalendarElement extends HTMLElement {
|
|
|
647
662
|
* hover of a live range preview.
|
|
648
663
|
*/
|
|
649
664
|
_updateRangeDom() {
|
|
665
|
+
const previousRange = this._paintedRange;
|
|
666
|
+
const previousPreview = this._paintedPreview;
|
|
650
667
|
this._paintedRange = { ...this._highlightedRange };
|
|
651
668
|
this._paintedPreview = this.hasAttribute("data-range-preview");
|
|
652
669
|
for (const cell of this.querySelectorAll(".dp-day[data-date]")) {
|
|
653
670
|
if (!(cell instanceof HTMLTableCellElement)) continue;
|
|
654
671
|
const date = cell.dataset.date || "";
|
|
655
672
|
const position = rangePosition(date, this._highlightedRange);
|
|
673
|
+
if (position === rangePosition(date, previousRange) && previousPreview === this._paintedPreview)
|
|
674
|
+
continue;
|
|
656
675
|
for (const [name, on] of Object.entries(rangeAttributeMap(position))) {
|
|
657
676
|
if (on) cell.setAttribute(name, "true");
|
|
658
677
|
else cell.removeAttribute(name);
|
|
659
678
|
}
|
|
660
|
-
|
|
679
|
+
// A moving preview changes only the band. Its accessible labels always
|
|
680
|
+
// omit range roles; entering/leaving preview still updates those labels.
|
|
681
|
+
if (!previousPreview || !this._paintedPreview) {
|
|
682
|
+
const state = this._renderedStates.get(date) || {};
|
|
683
|
+
const label = this._dayLabel(date, state, position);
|
|
684
|
+
if (cell.getAttribute("aria-label") !== label) cell.setAttribute("aria-label", label);
|
|
685
|
+
}
|
|
661
686
|
}
|
|
662
687
|
}
|
|
663
688
|
|
|
@@ -847,6 +872,7 @@ export class DateCalendarElement extends HTMLElement {
|
|
|
847
872
|
)
|
|
848
873
|
.join("");
|
|
849
874
|
|
|
875
|
+
this._renderedStates.clear();
|
|
850
876
|
const rows = weeks
|
|
851
877
|
.map((week) => {
|
|
852
878
|
const weekNumber = showWeeks
|
|
@@ -855,6 +881,7 @@ export class DateCalendarElement extends HTMLElement {
|
|
|
855
881
|
const cells = week
|
|
856
882
|
.map((date) => {
|
|
857
883
|
const state = this.getDateState(date);
|
|
884
|
+
this._renderedStates.set(date, state);
|
|
858
885
|
const outside = monthKey(date) !== display;
|
|
859
886
|
const selected = this.value === date;
|
|
860
887
|
const focused = this.focusedDate === date;
|
|
@@ -879,10 +906,14 @@ export class DateCalendarElement extends HTMLElement {
|
|
|
879
906
|
// select via `data-year-first`.
|
|
880
907
|
const yearFirst = monthYearOrder(locale)[0] === "year";
|
|
881
908
|
this.toggleAttribute("data-year-first", yearFirst);
|
|
909
|
+
const classes = this._controlClasses;
|
|
910
|
+
const monthClass = escapeHtml(classes.month || "dp-skin");
|
|
911
|
+
const yearClass = escapeHtml(classes.year || "dp-skin");
|
|
912
|
+
const navClass = escapeHtml(classes.nav || "dp-skin");
|
|
882
913
|
const monthControl = `<label class="dp-visually-hidden" for="${this._id}-month">${escapeHtml(this._messages.month)}</label>
|
|
883
|
-
<select id="${this._id}-month" class="dp-month-select" aria-label="${escapeHtml(this._messages.month)}">${monthOptions}</select>`;
|
|
914
|
+
<select id="${this._id}-month" class="dp-month-select ${monthClass}" aria-label="${escapeHtml(this._messages.month)}">${monthOptions}</select>`;
|
|
884
915
|
const yearControl = `<label class="dp-visually-hidden" for="${this._id}-year">${escapeHtml(this._messages.year)}</label>
|
|
885
|
-
<input id="${this._id}-year" class="dp-year-input" type="number" inputmode="numeric" value="${displayYear}"${minYearAttr}${maxYearAttr} aria-label="${escapeHtml(this._messages.year)}">`;
|
|
916
|
+
<input id="${this._id}-year" class="dp-year-input ${yearClass}" type="number" inputmode="numeric" value="${displayYear}"${minYearAttr}${maxYearAttr} aria-label="${escapeHtml(this._messages.year)}">`;
|
|
886
917
|
|
|
887
918
|
this._rendering = true;
|
|
888
919
|
this.innerHTML = `
|
|
@@ -890,8 +921,8 @@ export class DateCalendarElement extends HTMLElement {
|
|
|
890
921
|
<div class="dp-calendar-header">
|
|
891
922
|
${yearFirst ? yearControl : monthControl}
|
|
892
923
|
${yearFirst ? monthControl : yearControl}
|
|
893
|
-
<button type="button" class="dp-nav dp-prev" data-calendar-action="previous" aria-label="${escapeHtml(this._messages.previousMonth)}"${prevDisabled ? " disabled" : ""}><svg aria-hidden="true" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="m4 10 4-4 4 4"/></svg></button>
|
|
894
|
-
<button type="button" class="dp-nav dp-next" data-calendar-action="next" aria-label="${escapeHtml(this._messages.nextMonth)}"${nextDisabled ? " disabled" : ""}><svg aria-hidden="true" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="m4 6 4 4 4-4"/></svg></button>
|
|
924
|
+
<button type="button" class="dp-nav dp-prev ${navClass}" data-calendar-action="previous" aria-label="${escapeHtml(this._messages.previousMonth)}"${prevDisabled ? " disabled" : ""}><svg aria-hidden="true" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="m4 10 4-4 4 4"/></svg></button>
|
|
925
|
+
<button type="button" class="dp-nav dp-next ${navClass}" data-calendar-action="next" aria-label="${escapeHtml(this._messages.nextMonth)}"${nextDisabled ? " disabled" : ""}><svg aria-hidden="true" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="m4 6 4 4 4-4"/></svg></button>
|
|
895
926
|
</div>
|
|
896
927
|
<h2 id="${headingId}" class="dp-calendar-heading dp-visually-hidden" aria-live="polite">${escapeHtml(formatMonthYear(`${display}-15`, locale))}</h2>
|
|
897
928
|
<table id="${gridId}" class="dp-grid" role="grid" aria-labelledby="${headingId}">
|
|
@@ -925,7 +956,7 @@ export class DateCalendarElement extends HTMLElement {
|
|
|
925
956
|
const date = cell.dataset.date || "";
|
|
926
957
|
const extra = cell.querySelector(".dp-day-extra");
|
|
927
958
|
if (!(extra instanceof HTMLElement)) continue;
|
|
928
|
-
const content = this._renderDay(date, this.
|
|
959
|
+
const content = this._renderDay(date, this._renderedStates.get(date) || {});
|
|
929
960
|
if (content instanceof Node) extra.append(content);
|
|
930
961
|
else if (content !== undefined && content !== null) extra.textContent = String(content);
|
|
931
962
|
}
|
package/src/date-field.js
CHANGED
package/src/date-picker.css
CHANGED
|
@@ -46,7 +46,7 @@ date-calendar {
|
|
|
46
46
|
|
|
47
47
|
.dp-calendar-header {
|
|
48
48
|
display: grid;
|
|
49
|
-
grid-template-columns: minmax(0, 1fr)
|
|
49
|
+
grid-template-columns: minmax(0, 1fr) var(--dp-year-inline-size) auto auto;
|
|
50
50
|
/* Stretch (not center): once a UA natural height exceeds
|
|
51
51
|
--dp-header-control-size, the row grows to the tallest control and the
|
|
52
52
|
others stretch to match, so month/year/nav stay equal. */
|
|
@@ -62,12 +62,16 @@ date-calendar {
|
|
|
62
62
|
track stays under the month select so the fixed-width year input never
|
|
63
63
|
stretches. Visual mirroring itself is left to the inherited `dir`. */
|
|
64
64
|
date-calendar[data-year-first] .dp-calendar-header {
|
|
65
|
-
grid-template-columns:
|
|
65
|
+
grid-template-columns: var(--dp-year-inline-size) minmax(0, 1fr) auto auto;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
.dp-calendar-header :where(button, select, input) {
|
|
69
|
-
min-block-size: var(--dp-header-control-size);
|
|
70
69
|
box-sizing: border-box;
|
|
70
|
+
min-inline-size: 0;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.dp-calendar-header .dp-skin {
|
|
74
|
+
min-block-size: var(--dp-header-control-size);
|
|
71
75
|
/* Shared vertical metrics: UA default padding/line-height differ per control
|
|
72
76
|
(up to ~13px apart), which only shows once --dp-header-control-size drops
|
|
73
77
|
below content size. */
|
|
@@ -80,8 +84,7 @@ date-calendar[data-year-first] .dp-calendar-header {
|
|
|
80
84
|
line-height: 1.2;
|
|
81
85
|
}
|
|
82
86
|
|
|
83
|
-
.dp-calendar-header input {
|
|
84
|
-
min-inline-size: 0;
|
|
87
|
+
.dp-calendar-header input.dp-skin {
|
|
85
88
|
padding-inline: 0.45rem;
|
|
86
89
|
}
|
|
87
90
|
|
|
@@ -91,11 +94,13 @@ date-calendar[data-year-first] .dp-calendar-header {
|
|
|
91
94
|
control and the others stretch to match. */
|
|
92
95
|
.dp-month-select {
|
|
93
96
|
inline-size: 100%;
|
|
94
|
-
min-inline-size: 0;
|
|
95
97
|
/* Long month names in a narrow track (compact consumers, long locales)
|
|
96
98
|
would paint under the caret: truncate with an ellipsis instead. */
|
|
97
99
|
overflow: hidden;
|
|
98
100
|
text-overflow: ellipsis;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.dp-month-select.dp-skin {
|
|
99
104
|
appearance: none;
|
|
100
105
|
padding-inline: 0.45rem 2rem;
|
|
101
106
|
background-image:
|
|
@@ -111,18 +116,18 @@ date-calendar[data-year-first] .dp-calendar-header {
|
|
|
111
116
|
/* Inherited RTL mirrors the header layout via the grid. The logical
|
|
112
117
|
`padding-inline` already flips, so only the physically-positioned caret has
|
|
113
118
|
to follow it to the other side. */
|
|
114
|
-
[dir="rtl"] .dp-month-select,
|
|
115
|
-
html[dir="rtl"] .dp-month-select {
|
|
119
|
+
[dir="rtl"] .dp-month-select.dp-skin,
|
|
120
|
+
html[dir="rtl"] .dp-month-select.dp-skin {
|
|
116
121
|
background-position:
|
|
117
122
|
0.9rem 50%,
|
|
118
123
|
0.65rem 50%;
|
|
119
124
|
}
|
|
120
125
|
|
|
121
126
|
.dp-year-input {
|
|
122
|
-
inline-size:
|
|
127
|
+
inline-size: 100%;
|
|
123
128
|
}
|
|
124
129
|
|
|
125
|
-
.dp-nav {
|
|
130
|
+
.dp-nav.dp-skin {
|
|
126
131
|
display: grid;
|
|
127
132
|
place-items: center;
|
|
128
133
|
inline-size: var(--dp-header-control-size);
|
|
@@ -139,19 +144,19 @@ html[dir="rtl"] .dp-month-select {
|
|
|
139
144
|
pointer-events: none;
|
|
140
145
|
}
|
|
141
146
|
|
|
142
|
-
.dp-calendar-header :
|
|
147
|
+
.dp-calendar-header .dp-skin:disabled,
|
|
143
148
|
.dp-picker-button:disabled {
|
|
144
149
|
cursor: not-allowed;
|
|
145
150
|
opacity: 0.45;
|
|
146
151
|
}
|
|
147
152
|
|
|
148
|
-
.dp-nav:hover:not(:disabled) {
|
|
153
|
+
.dp-nav.dp-skin:hover:not(:disabled) {
|
|
149
154
|
background: var(--dp-hover);
|
|
150
155
|
}
|
|
151
156
|
|
|
152
157
|
/* Inner outlines survive forced colors and keep focus inside each control.
|
|
153
158
|
The picker trigger uses a tighter inset around its icon below. */
|
|
154
|
-
.dp-calendar-header :
|
|
159
|
+
.dp-calendar-header .dp-skin:focus-visible,
|
|
155
160
|
date-picker > input:not([type="hidden"]):focus-visible,
|
|
156
161
|
.dp-day:focus-visible {
|
|
157
162
|
outline: var(--dp-focus-width) solid var(--dp-focus);
|
|
@@ -446,8 +451,8 @@ date-picker[range] > .dp-picker-button[data-endpoint="end"] {
|
|
|
446
451
|
@media (forced-colors: active) {
|
|
447
452
|
/* The author-drawn caret is a gradient and disappears here: hand the
|
|
448
453
|
control back to the browser instead of trying to rebuild it. */
|
|
449
|
-
.dp-month-select,
|
|
450
|
-
[dir="rtl"] .dp-month-select {
|
|
454
|
+
.dp-month-select.dp-skin,
|
|
455
|
+
[dir="rtl"] .dp-month-select.dp-skin {
|
|
451
456
|
appearance: auto;
|
|
452
457
|
background-image: none;
|
|
453
458
|
padding-inline: 0.45rem;
|
package/src/date-picker.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { autoUpdate, reposition } from "@lekoala/floating";
|
|
2
|
+
import { getDefaultControlClasses, resolveControlClasses, sameControlClasses } from "./control-classes.js";
|
|
2
3
|
import { compareDates, isDate, monthKey, todayISO } from "./date.js";
|
|
3
4
|
import { DateCalendarElement } from "./date-calendar.js";
|
|
4
5
|
import { DateFieldController } from "./date-field.js";
|
|
@@ -63,6 +64,7 @@ export class DatePickerElement extends HTMLElement {
|
|
|
63
64
|
this._open = false;
|
|
64
65
|
this._suppressFocusOpen = false;
|
|
65
66
|
this._messages = getDefaultMessages();
|
|
67
|
+
this._controlClasses = getDefaultControlClasses();
|
|
66
68
|
/** @type {any} */
|
|
67
69
|
this._source = null;
|
|
68
70
|
/** @type {any} */
|
|
@@ -353,6 +355,23 @@ export class DatePickerElement extends HTMLElement {
|
|
|
353
355
|
return this._open;
|
|
354
356
|
}
|
|
355
357
|
|
|
358
|
+
/**
|
|
359
|
+
* Author classes for header controls; merged with global defaults.
|
|
360
|
+
* @public
|
|
361
|
+
* @returns {import("./control-classes.js").ControlClasses}
|
|
362
|
+
*/
|
|
363
|
+
get controlClasses() {
|
|
364
|
+
return { ...this._controlClasses };
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/** @param {Partial<import("./control-classes.js").ControlClasses> | null} value */
|
|
368
|
+
set controlClasses(value) {
|
|
369
|
+
const classes = resolveControlClasses(value);
|
|
370
|
+
if (sameControlClasses(classes, this._controlClasses)) return;
|
|
371
|
+
this._controlClasses = classes;
|
|
372
|
+
if (this._calendar) this._calendar.controlClasses = this._controlClasses;
|
|
373
|
+
}
|
|
374
|
+
|
|
356
375
|
/** @public */
|
|
357
376
|
get messages() {
|
|
358
377
|
return this._messages;
|
|
@@ -553,6 +572,31 @@ export class DatePickerElement extends HTMLElement {
|
|
|
553
572
|
this._refreshButtonLabel();
|
|
554
573
|
}
|
|
555
574
|
|
|
575
|
+
/** @param {AbortSignal} signal @param {HTMLInputElement | null | undefined} input @param {() => void} restore */
|
|
576
|
+
_bindPopup(signal, input, restore) {
|
|
577
|
+
this.ownerDocument.addEventListener(
|
|
578
|
+
"pointerdown",
|
|
579
|
+
(event) => {
|
|
580
|
+
if (!this._open || event.composedPath().includes(this)) return;
|
|
581
|
+
this.hide(false);
|
|
582
|
+
},
|
|
583
|
+
{ capture: true, signal },
|
|
584
|
+
);
|
|
585
|
+
this.ownerDocument.addEventListener(
|
|
586
|
+
"reset",
|
|
587
|
+
(event) => {
|
|
588
|
+
if (!(event.target instanceof HTMLFormElement) || !input || event.target !== input.form) return;
|
|
589
|
+
// Reset is cancelable and runs before native values have been restored.
|
|
590
|
+
this.hide(false);
|
|
591
|
+
queueMicrotask(() => {
|
|
592
|
+
if (!event.defaultPrevented) restore();
|
|
593
|
+
});
|
|
594
|
+
},
|
|
595
|
+
{ capture: true, signal },
|
|
596
|
+
);
|
|
597
|
+
this.addEventListener("keydown", (event) => this._onEscape(event), { signal });
|
|
598
|
+
}
|
|
599
|
+
|
|
556
600
|
_bind() {
|
|
557
601
|
const input = this._input;
|
|
558
602
|
const button = this._buttons[0]?.button;
|
|
@@ -641,30 +685,7 @@ export class DatePickerElement extends HTMLElement {
|
|
|
641
685
|
// Re-check availability after a source load. validate() commits with
|
|
642
686
|
// moveDisplay:false, so the visible month stays where the user navigated.
|
|
643
687
|
calendar.addEventListener("dateloadend", () => void this.validate(), { signal });
|
|
644
|
-
this.
|
|
645
|
-
"pointerdown",
|
|
646
|
-
(event) => {
|
|
647
|
-
if (!this._open || event.composedPath().includes(this)) return;
|
|
648
|
-
this.hide(false);
|
|
649
|
-
},
|
|
650
|
-
{ capture: true, signal },
|
|
651
|
-
);
|
|
652
|
-
this.ownerDocument.addEventListener(
|
|
653
|
-
"reset",
|
|
654
|
-
(event) => {
|
|
655
|
-
const form = event.target;
|
|
656
|
-
if (form instanceof HTMLFormElement && this._input && form === this._input.form) {
|
|
657
|
-
// The reset event fires before the control values are restored and
|
|
658
|
-
// is cancelable; only resync when the reset actually applies.
|
|
659
|
-
this.hide(false);
|
|
660
|
-
queueMicrotask(() => {
|
|
661
|
-
if (!event.defaultPrevented) this._restoreDefault();
|
|
662
|
-
});
|
|
663
|
-
}
|
|
664
|
-
},
|
|
665
|
-
{ capture: true, signal },
|
|
666
|
-
);
|
|
667
|
-
this.addEventListener("keydown", (event) => this._onEscape(event), { signal });
|
|
688
|
+
this._bindPopup(signal, this._input, () => this._restoreDefault());
|
|
668
689
|
}
|
|
669
690
|
|
|
670
691
|
_bindRange() {
|
|
@@ -772,28 +793,7 @@ export class DatePickerElement extends HTMLElement {
|
|
|
772
793
|
// Re-check availability after a source load. validate() commits with
|
|
773
794
|
// moveDisplay:false, so the visible month stays where the user navigated.
|
|
774
795
|
calendar.addEventListener("dateloadend", () => void this.validate(), { signal });
|
|
775
|
-
this.
|
|
776
|
-
"pointerdown",
|
|
777
|
-
(event) => {
|
|
778
|
-
if (!this._open || event.composedPath().includes(this)) return;
|
|
779
|
-
this.hide(false);
|
|
780
|
-
},
|
|
781
|
-
{ capture: true, signal },
|
|
782
|
-
);
|
|
783
|
-
this.ownerDocument.addEventListener(
|
|
784
|
-
"reset",
|
|
785
|
-
(event) => {
|
|
786
|
-
const form = event.target;
|
|
787
|
-
if (form instanceof HTMLFormElement && this._fields && form === this._fields[0].input.form) {
|
|
788
|
-
this.hide(false);
|
|
789
|
-
queueMicrotask(() => {
|
|
790
|
-
if (!event.defaultPrevented) this._restoreRangeDefault();
|
|
791
|
-
});
|
|
792
|
-
}
|
|
793
|
-
},
|
|
794
|
-
{ capture: true, signal },
|
|
795
|
-
);
|
|
796
|
-
this.addEventListener("keydown", (event) => this._onEscape(event), { signal });
|
|
796
|
+
this._bindPopup(signal, this._fields?.[0]?.input, () => this._restoreRangeDefault());
|
|
797
797
|
}
|
|
798
798
|
|
|
799
799
|
/**
|
|
@@ -1177,6 +1177,7 @@ export class DatePickerElement extends HTMLElement {
|
|
|
1177
1177
|
calendar.min = this.min;
|
|
1178
1178
|
calendar.max = this.max;
|
|
1179
1179
|
calendar.monthFormat = this.monthFormat;
|
|
1180
|
+
calendar.controlClasses = this._controlClasses;
|
|
1180
1181
|
calendar.messages = this._messages;
|
|
1181
1182
|
calendar.source = this._source;
|
|
1182
1183
|
calendar.dateState = this._dateState;
|
package/src/date.js
CHANGED
|
@@ -165,9 +165,10 @@ export function endOfWeek(value, firstDay = 1) {
|
|
|
165
165
|
}
|
|
166
166
|
|
|
167
167
|
/**
|
|
168
|
-
*
|
|
168
|
+
* Civil weeks covering the anchor month: 4..6 full rows. Optional fixed weeks
|
|
169
|
+
* pads to six rows when a full extra week fits inside the canonical window.
|
|
169
170
|
* @param {string} value YYYY-MM or YYYY-MM-DD
|
|
170
|
-
* @param {{firstDay?: number}} [options]
|
|
171
|
+
* @param {{firstDay?: number, fixedWeeks?: boolean}} [options]
|
|
171
172
|
*/
|
|
172
173
|
export function getMonthWeeks(value, options = {}) {
|
|
173
174
|
const firstDay = options.firstDay ?? 1;
|
|
@@ -183,7 +184,11 @@ export function getMonthWeeks(value, options = {}) {
|
|
|
183
184
|
// Stop from the built week's own end: advancing the cursor past the final
|
|
184
185
|
// week (e.g. from 9999-12-25 to 10000-01-01) would leave the canonical
|
|
185
186
|
// window even when every emitted day is valid.
|
|
186
|
-
if (compareDates(week[6], end) >= 0)
|
|
187
|
+
if (compareDates(week[6], end) >= 0) {
|
|
188
|
+
// Another complete week must fit before the last canonical date.
|
|
189
|
+
if (!options.fixedWeeks || weeks.length >= 6 || compareDates(week[6], addDays("9999-12-31", -7)) > 0)
|
|
190
|
+
break;
|
|
191
|
+
}
|
|
187
192
|
cursor = addDays(cursor, 7);
|
|
188
193
|
}
|
|
189
194
|
return weeks;
|
package/src/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { CalendarModel } from "./calendar-model.js";
|
|
2
|
+
export { getDefaultControlClasses, setDefaultControlClasses } from "./control-classes.js";
|
|
2
3
|
export * as dates from "./date.js";
|
|
3
4
|
export { DateCalendarElement } from "./date-calendar.js";
|
|
4
5
|
export { DatePickerElement } from "./date-picker.js";
|
package/src/intl.js
CHANGED
|
@@ -2,6 +2,33 @@ import { isDate, normalizeFirstDay, parseDate, toIntlDate, toISODate } from "./d
|
|
|
2
2
|
|
|
3
3
|
const BIDI = /[\u200e\u200f\u061c\u202a-\u202e\u2066-\u2069]/g;
|
|
4
4
|
|
|
5
|
+
/** @type {Map<string, Intl.DateTimeFormat>} */
|
|
6
|
+
const dateFormatters = new Map();
|
|
7
|
+
/** @type {Map<string, Map<string, string>>} */
|
|
8
|
+
const digitMaps = new Map();
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Bound retained locales/options even when an application cycles through many.
|
|
12
|
+
* @template T
|
|
13
|
+
* @param {Map<string, T>} cache @param {string} key @param {() => T} create
|
|
14
|
+
* @returns {T}
|
|
15
|
+
*/
|
|
16
|
+
function cached(cache, key, create) {
|
|
17
|
+
const existing = cache.get(key);
|
|
18
|
+
if (existing !== undefined) return existing;
|
|
19
|
+
const value = create();
|
|
20
|
+
const oldest = cache.keys().next().value;
|
|
21
|
+
if (cache.size >= 64 && oldest !== undefined) cache.delete(oldest);
|
|
22
|
+
cache.set(key, value);
|
|
23
|
+
return value;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** @param {string} locale @param {Intl.DateTimeFormatOptions} options */
|
|
27
|
+
function dateFormatter(locale, options) {
|
|
28
|
+
const key = JSON.stringify([locale, options]);
|
|
29
|
+
return cached(dateFormatters, key, () => new Intl.DateTimeFormat(locale, options));
|
|
30
|
+
}
|
|
31
|
+
|
|
5
32
|
/** @param {string} [locale] */
|
|
6
33
|
export function resolveLocale(locale = "") {
|
|
7
34
|
if (locale) return locale;
|
|
@@ -12,10 +39,12 @@ export function resolveLocale(locale = "") {
|
|
|
12
39
|
|
|
13
40
|
/** @param {string} locale */
|
|
14
41
|
function digitMap(locale) {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
42
|
+
return cached(digitMaps, locale, () => {
|
|
43
|
+
const formatter = new Intl.NumberFormat(locale, { useGrouping: false });
|
|
44
|
+
const map = new Map();
|
|
45
|
+
for (let i = 0; i <= 9; i++) map.set(formatter.format(i), String(i));
|
|
46
|
+
return map;
|
|
47
|
+
});
|
|
19
48
|
}
|
|
20
49
|
|
|
21
50
|
/**
|
|
@@ -33,7 +62,7 @@ function normalizeDigits(text, locale, options = {}) {
|
|
|
33
62
|
/** @param {string} value @param {string} [locale] */
|
|
34
63
|
export function formatLongDate(value, locale = "") {
|
|
35
64
|
const resolved = resolveLocale(locale);
|
|
36
|
-
return
|
|
65
|
+
return dateFormatter(resolved, {
|
|
37
66
|
calendar: "gregory",
|
|
38
67
|
weekday: "long",
|
|
39
68
|
year: "numeric",
|
|
@@ -46,7 +75,7 @@ export function formatLongDate(value, locale = "") {
|
|
|
46
75
|
/** @param {string} value @param {string} [locale] */
|
|
47
76
|
export function formatMonthYear(value, locale = "") {
|
|
48
77
|
const resolved = resolveLocale(locale);
|
|
49
|
-
return
|
|
78
|
+
return dateFormatter(resolved, {
|
|
50
79
|
calendar: "gregory",
|
|
51
80
|
year: "numeric",
|
|
52
81
|
month: "long",
|
|
@@ -65,7 +94,7 @@ export function formatMonthYear(value, locale = "") {
|
|
|
65
94
|
export function monthYearOrder(locale = "") {
|
|
66
95
|
const resolved = resolveLocale(locale);
|
|
67
96
|
try {
|
|
68
|
-
const parts =
|
|
97
|
+
const parts = dateFormatter(resolved, {
|
|
69
98
|
calendar: "gregory",
|
|
70
99
|
year: "numeric",
|
|
71
100
|
month: "long",
|
|
@@ -92,7 +121,7 @@ export function monthYearOrder(locale = "") {
|
|
|
92
121
|
/** @param {string} [locale] @param {"long"|"short"} [style] */
|
|
93
122
|
export function monthNames(locale = "", style = "long") {
|
|
94
123
|
const resolved = resolveLocale(locale);
|
|
95
|
-
const formatter =
|
|
124
|
+
const formatter = dateFormatter(resolved, { calendar: "gregory", month: style, timeZone: "UTC" });
|
|
96
125
|
return Array.from({ length: 12 }, (_, index) =>
|
|
97
126
|
formatter.format(toIntlDate(`2026-${String(index + 1).padStart(2, "0")}-15`)),
|
|
98
127
|
);
|
|
@@ -105,7 +134,7 @@ export function monthNames(locale = "", style = "long") {
|
|
|
105
134
|
*/
|
|
106
135
|
export function weekdayNames(locale = "", firstDay = 1, style = "short") {
|
|
107
136
|
const resolved = resolveLocale(locale);
|
|
108
|
-
const formatter =
|
|
137
|
+
const formatter = dateFormatter(resolved, {
|
|
109
138
|
calendar: "gregory",
|
|
110
139
|
weekday: style,
|
|
111
140
|
timeZone: "UTC",
|
|
@@ -131,7 +160,7 @@ function escapeRegExp(value) {
|
|
|
131
160
|
*/
|
|
132
161
|
export function createDateAdapter(locale = "") {
|
|
133
162
|
const resolved = resolveLocale(locale);
|
|
134
|
-
const formatter =
|
|
163
|
+
const formatter = dateFormatter(resolved, {
|
|
135
164
|
calendar: "gregory",
|
|
136
165
|
year: "numeric",
|
|
137
166
|
month: "2-digit",
|
package/src/source.js
CHANGED
|
@@ -28,17 +28,27 @@ export function normalizeDateStates(payload) {
|
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
30
|
* Small fetch adapter for legacy/configUrl-like endpoints.
|
|
31
|
+
* Resolve and validate the URL immediately against the document or worker location.
|
|
31
32
|
* @param {string | URL} url
|
|
32
33
|
* @param {{map?:(payload:unknown)=>unknown, params?:(range:{start:string,end:string})=>Record<string,string>}} [options]
|
|
33
34
|
*/
|
|
34
35
|
export function createFetchSource(url, options = {}) {
|
|
36
|
+
const base = typeof document !== "undefined" ? document.baseURI : globalThis.location?.href;
|
|
37
|
+
let resolved;
|
|
38
|
+
try {
|
|
39
|
+
resolved = new URL(String(url), base);
|
|
40
|
+
} catch (cause) {
|
|
41
|
+
throw new TypeError(
|
|
42
|
+
base
|
|
43
|
+
? `Invalid date source URL: ${url}`
|
|
44
|
+
: "Date source: absolute URL required outside a document or worker location",
|
|
45
|
+
{ cause },
|
|
46
|
+
);
|
|
47
|
+
}
|
|
35
48
|
return {
|
|
36
49
|
/** @param {{start:string,end:string}} range @param {{signal?:AbortSignal}} [context] */
|
|
37
50
|
async load(range, context = {}) {
|
|
38
|
-
const target = new URL(
|
|
39
|
-
String(url),
|
|
40
|
-
typeof document !== "undefined" ? document.baseURI : "http://localhost/",
|
|
41
|
-
);
|
|
51
|
+
const target = new URL(resolved);
|
|
42
52
|
const params = options.params ? options.params(range) : range;
|
|
43
53
|
for (const [key, value] of Object.entries(params)) target.searchParams.set(key, String(value));
|
|
44
54
|
const response = await fetch(target, { signal: context.signal });
|