@lekoala/date-picker 0.5.0 → 0.5.1
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 +15 -2
- package/custom-elements.json +42 -2
- package/dist/date-picker.js +187 -54
- package/dist/date-picker.min.js +13 -13
- package/dist/date-picker.standalone.min.js +13 -13
- package/dist/types/control-classes.d.ts +11 -1
- package/dist/types/control-classes.d.ts.map +1 -1
- package/dist/types/date-calendar.d.ts +26 -49
- package/dist/types/date-calendar.d.ts.map +1 -1
- package/dist/types/date-picker.d.ts +35 -59
- package/dist/types/date-picker.d.ts.map +1 -1
- package/dist/types/messages.d.ts +2 -0
- package/dist/types/messages.d.ts.map +1 -1
- package/dist/types/upgrade-properties.d.ts +8 -0
- package/dist/types/upgrade-properties.d.ts.map +1 -0
- package/docs/API.md +15 -3
- package/docs/DECISIONS.md +6 -0
- package/package.json +2 -2
- package/src/control-classes.js +36 -6
- package/src/date-calendar.js +89 -23
- package/src/date-picker.js +93 -35
- package/src/messages.js +12 -0
- package/src/upgrade-properties.js +14 -0
package/src/date-picker.js
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
import { autoUpdate, reposition } from "@lekoala/floating";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
getDefaultControlClasses,
|
|
4
|
+
normalizeControlClasses,
|
|
5
|
+
resolveControlClasses,
|
|
6
|
+
setDefaultControlClasses,
|
|
7
|
+
} from "./control-classes.js";
|
|
3
8
|
import { compareDates, isDate, monthKey, todayISO } from "./date.js";
|
|
4
9
|
import { DateCalendarElement } from "./date-calendar.js";
|
|
5
10
|
import { DateFieldController } from "./date-field.js";
|
|
6
11
|
import { DateRangeController, normalizeRange } from "./date-range.js";
|
|
7
12
|
import { createDateAdapter, formatLongDate, resolveLocale } from "./intl.js";
|
|
8
|
-
import { getDefaultMessages } from "./messages.js";
|
|
13
|
+
import { getDefaultMessages, setDefaultMessages, subscribeDefaultMessages } from "./messages.js";
|
|
9
14
|
import { compareTimes, isTime } from "./time.js";
|
|
15
|
+
import { upgradeProperties } from "./upgrade-properties.js";
|
|
10
16
|
|
|
11
17
|
let uid = 0;
|
|
12
18
|
|
|
@@ -16,6 +22,26 @@ const DRAG_THRESHOLD = 4;
|
|
|
16
22
|
export class DatePickerElement extends HTMLElement {
|
|
17
23
|
static observedAttributes = ["value", "locale", "min", "max", "open-on-focus", "month-format"];
|
|
18
24
|
|
|
25
|
+
/** @public @param {Partial<import("./control-classes.js").ControlClasses>} classes */
|
|
26
|
+
static setDefaultControlClasses(classes) {
|
|
27
|
+
setDefaultControlClasses(classes);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** @public */
|
|
31
|
+
static getDefaultControlClasses() {
|
|
32
|
+
return getDefaultControlClasses();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** @public @param {Partial<typeof import("./messages.js").DEFAULT_MESSAGES>} messages */
|
|
36
|
+
static setDefaultMessages(messages) {
|
|
37
|
+
setDefaultMessages(messages);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** @public */
|
|
41
|
+
static getDefaultMessages() {
|
|
42
|
+
return getDefaultMessages();
|
|
43
|
+
}
|
|
44
|
+
|
|
19
45
|
constructor() {
|
|
20
46
|
super();
|
|
21
47
|
this._id = `date-picker-${++uid}`;
|
|
@@ -63,8 +89,12 @@ export class DatePickerElement extends HTMLElement {
|
|
|
63
89
|
this._stopTracking = null;
|
|
64
90
|
this._open = false;
|
|
65
91
|
this._suppressFocusOpen = false;
|
|
66
|
-
|
|
67
|
-
this.
|
|
92
|
+
/** @type {typeof import("./messages.js").DEFAULT_MESSAGES | null} */
|
|
93
|
+
this._messages = null;
|
|
94
|
+
/** @type {Partial<import("./control-classes.js").ControlClasses> | null} */
|
|
95
|
+
this._controlClasses = null;
|
|
96
|
+
/** @type {(() => void) | null} */
|
|
97
|
+
this._stopDefaultMessages = null;
|
|
68
98
|
/** @type {any} */
|
|
69
99
|
this._source = null;
|
|
70
100
|
/** @type {any} */
|
|
@@ -83,11 +113,29 @@ export class DatePickerElement extends HTMLElement {
|
|
|
83
113
|
|
|
84
114
|
connectedCallback() {
|
|
85
115
|
if (this._connected) return;
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
116
|
+
upgradeProperties(this, [
|
|
117
|
+
"locale",
|
|
118
|
+
"min",
|
|
119
|
+
"max",
|
|
120
|
+
"monthFormat",
|
|
121
|
+
"openOnFocus",
|
|
122
|
+
"coordinateSpace",
|
|
123
|
+
"controlClasses",
|
|
124
|
+
"messages",
|
|
125
|
+
"source",
|
|
126
|
+
"dateState",
|
|
127
|
+
"renderDay",
|
|
128
|
+
"isDateDisabled",
|
|
129
|
+
]);
|
|
130
|
+
if (this.hasAttribute("range")) this._connectRange();
|
|
131
|
+
else this._connectSingle();
|
|
132
|
+
// The range setter requires the controller created by _connectRange().
|
|
133
|
+
if (this._connected) {
|
|
134
|
+
upgradeProperties(this, ["value", "range"]);
|
|
135
|
+
this._stopDefaultMessages = subscribeDefaultMessages(() => {
|
|
136
|
+
if (this._messages === null) this._refreshMessages();
|
|
137
|
+
});
|
|
89
138
|
}
|
|
90
|
-
this._connectSingle();
|
|
91
139
|
}
|
|
92
140
|
|
|
93
141
|
_connectSingle() {
|
|
@@ -101,7 +149,7 @@ export class DatePickerElement extends HTMLElement {
|
|
|
101
149
|
this._connected = true;
|
|
102
150
|
this._input = input;
|
|
103
151
|
this._discoverTimeFields();
|
|
104
|
-
this._field = new DateFieldController(input, { messages: this.
|
|
152
|
+
this._field = new DateFieldController(input, { messages: this.messages, locale: this.locale });
|
|
105
153
|
this._field.onAttributesChanged = () => this._syncInputState();
|
|
106
154
|
this._field.confirm = (date, options) => this._confirmDate(date, options);
|
|
107
155
|
this._setupFormValue();
|
|
@@ -115,7 +163,7 @@ export class DatePickerElement extends HTMLElement {
|
|
|
115
163
|
this.getAttribute("value") || (isDate(input.value) ? input.value : adapter.parse(input.value));
|
|
116
164
|
if (initial && isDate(initial)) this._setValue(initial, { emit: false, format: true });
|
|
117
165
|
else if (!input.value) this._setValue("", { emit: false, format: false });
|
|
118
|
-
else input.setCustomValidity(this.
|
|
166
|
+
else input.setCustomValidity(this.messages.invalidDate);
|
|
119
167
|
|
|
120
168
|
// Native form reset restores control defaultValues, then _restoreDefault()
|
|
121
169
|
// recommits the canonical value from them. Never redefine an author-provided
|
|
@@ -171,8 +219,8 @@ export class DatePickerElement extends HTMLElement {
|
|
|
171
219
|
this._connected = true;
|
|
172
220
|
this._input = null;
|
|
173
221
|
this._fields = [
|
|
174
|
-
new DateFieldController(startInput, { messages: this.
|
|
175
|
-
new DateFieldController(endInput, { messages: this.
|
|
222
|
+
new DateFieldController(startInput, { messages: this.messages, locale: this.locale }),
|
|
223
|
+
new DateFieldController(endInput, { messages: this.messages, locale: this.locale }),
|
|
176
224
|
];
|
|
177
225
|
this._range = new DateRangeController();
|
|
178
226
|
this._rangeOriginalDescribedBy = [
|
|
@@ -195,8 +243,8 @@ export class DatePickerElement extends HTMLElement {
|
|
|
195
243
|
const initialEnd = isDate(endInput.value) ? endInput.value : adapter.parse(endInput.value);
|
|
196
244
|
this._setRange(initialStart || "", initialEnd || "", { emit: false });
|
|
197
245
|
if (startInput.value.trim() && !isDate(initialStart))
|
|
198
|
-
startInput.setCustomValidity(this.
|
|
199
|
-
if (endInput.value.trim() && !isDate(initialEnd)) endInput.setCustomValidity(this.
|
|
246
|
+
startInput.setCustomValidity(this.messages.invalidDate);
|
|
247
|
+
if (endInput.value.trim() && !isDate(initialEnd)) endInput.setCustomValidity(this.messages.invalidDate);
|
|
200
248
|
|
|
201
249
|
if (this._fields[0].hidden) this._fields[0].hidden.defaultValue = this._range.start;
|
|
202
250
|
if (this._fields[1].hidden) this._fields[1].hidden.defaultValue = this._range.end;
|
|
@@ -209,6 +257,8 @@ export class DatePickerElement extends HTMLElement {
|
|
|
209
257
|
}
|
|
210
258
|
|
|
211
259
|
disconnectedCallback() {
|
|
260
|
+
this._stopDefaultMessages?.();
|
|
261
|
+
this._stopDefaultMessages = null;
|
|
212
262
|
this._connected = false;
|
|
213
263
|
this.hide(false);
|
|
214
264
|
this._controller?.abort();
|
|
@@ -356,34 +406,42 @@ export class DatePickerElement extends HTMLElement {
|
|
|
356
406
|
}
|
|
357
407
|
|
|
358
408
|
/**
|
|
359
|
-
* Author classes for header controls;
|
|
409
|
+
* Author classes for header controls; omitted roles follow current global defaults.
|
|
360
410
|
* @public
|
|
361
411
|
* @returns {import("./control-classes.js").ControlClasses}
|
|
362
412
|
*/
|
|
363
413
|
get controlClasses() {
|
|
364
|
-
return
|
|
414
|
+
return resolveControlClasses(this._controlClasses);
|
|
365
415
|
}
|
|
366
416
|
|
|
367
417
|
/** @param {Partial<import("./control-classes.js").ControlClasses> | null} value */
|
|
368
418
|
set controlClasses(value) {
|
|
369
|
-
|
|
370
|
-
if (sameControlClasses(classes, this._controlClasses)) return;
|
|
371
|
-
this._controlClasses = classes;
|
|
419
|
+
this._controlClasses = normalizeControlClasses(value);
|
|
372
420
|
if (this._calendar) this._calendar.controlClasses = this._controlClasses;
|
|
373
421
|
}
|
|
374
422
|
|
|
375
|
-
/** @public */
|
|
423
|
+
/** @public @returns {typeof import("./messages.js").DEFAULT_MESSAGES} */
|
|
376
424
|
get messages() {
|
|
377
|
-
return this._messages;
|
|
425
|
+
return this._messages ?? getDefaultMessages();
|
|
378
426
|
}
|
|
379
427
|
|
|
428
|
+
/** @param {Partial<typeof import("./messages.js").DEFAULT_MESSAGES> | null} value */
|
|
380
429
|
set messages(value) {
|
|
381
|
-
this._messages = { ...getDefaultMessages(), ...
|
|
430
|
+
this._messages = value == null ? null : { ...getDefaultMessages(), ...value };
|
|
382
431
|
if (this._calendar) this._calendar.messages = this._messages;
|
|
383
|
-
|
|
384
|
-
|
|
432
|
+
this._refreshMessages();
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
_refreshMessages() {
|
|
436
|
+
const messages = this.messages;
|
|
437
|
+
for (const field of this._fields || []) field.setMessages(messages);
|
|
438
|
+
this._field?.setMessages(messages);
|
|
439
|
+
this._panel?.setAttribute("aria-label", messages.calendar);
|
|
440
|
+
const field = this._field ?? this._fields?.[0];
|
|
441
|
+
if (this._formatHint && field)
|
|
442
|
+
this._formatHint.textContent = `${messages.formatHint}: ${field.placeholder}`;
|
|
385
443
|
this._refreshButtonLabel();
|
|
386
|
-
void this.validate();
|
|
444
|
+
if (this._connected) void this.validate();
|
|
387
445
|
}
|
|
388
446
|
|
|
389
447
|
/** @public @returns {any} */
|
|
@@ -541,7 +599,7 @@ export class DatePickerElement extends HTMLElement {
|
|
|
541
599
|
panel.className = "dp-picker-panel";
|
|
542
600
|
panel.setAttribute("popover", "manual");
|
|
543
601
|
panel.setAttribute("role", "dialog");
|
|
544
|
-
panel.setAttribute("aria-label", this.
|
|
602
|
+
panel.setAttribute("aria-label", this.messages.calendar);
|
|
545
603
|
|
|
546
604
|
const calendar = new DateCalendarElement();
|
|
547
605
|
calendar.selection = "none";
|
|
@@ -1184,7 +1242,7 @@ export class DatePickerElement extends HTMLElement {
|
|
|
1184
1242
|
calendar.renderDay = this._renderDay;
|
|
1185
1243
|
calendar.isDateDisabled = this._isDateDisabled;
|
|
1186
1244
|
if (!this._rangeMode() && this._value) calendar.value = this._value;
|
|
1187
|
-
if (panel) panel.setAttribute("aria-label", this.
|
|
1245
|
+
if (panel) panel.setAttribute("aria-label", this.messages.calendar);
|
|
1188
1246
|
}
|
|
1189
1247
|
|
|
1190
1248
|
_refreshLocale() {
|
|
@@ -1193,7 +1251,7 @@ export class DatePickerElement extends HTMLElement {
|
|
|
1193
1251
|
if (this._rangeMode() && this._fields) {
|
|
1194
1252
|
this._fields[0].setLocale(this.locale);
|
|
1195
1253
|
this._fields[1].setLocale(this.locale);
|
|
1196
|
-
hint.textContent = `${this.
|
|
1254
|
+
hint.textContent = `${this.messages.formatHint}: ${this._fields[0].placeholder}`;
|
|
1197
1255
|
for (const [index, field] of this._fields.entries()) {
|
|
1198
1256
|
if (!field.input.hasAttribute("placeholder") || this._generatedPlaceholder) {
|
|
1199
1257
|
field.input.placeholder = field.placeholder;
|
|
@@ -1208,7 +1266,7 @@ export class DatePickerElement extends HTMLElement {
|
|
|
1208
1266
|
const field = this._field;
|
|
1209
1267
|
if (!field) return;
|
|
1210
1268
|
field.setLocale(this.locale);
|
|
1211
|
-
hint.textContent = `${this.
|
|
1269
|
+
hint.textContent = `${this.messages.formatHint}: ${field.placeholder}`;
|
|
1212
1270
|
if (!field.input.hasAttribute("placeholder") || this._generatedPlaceholder) {
|
|
1213
1271
|
field.input.placeholder = field.placeholder;
|
|
1214
1272
|
this._generatedPlaceholder = true;
|
|
@@ -1224,7 +1282,7 @@ export class DatePickerElement extends HTMLElement {
|
|
|
1224
1282
|
this._refreshRangeButtonLabel();
|
|
1225
1283
|
return;
|
|
1226
1284
|
}
|
|
1227
|
-
const prefix = this._value ? this.
|
|
1285
|
+
const prefix = this._value ? this.messages.changeDate : this.messages.chooseDate;
|
|
1228
1286
|
const suffix = this._value ? `, ${formatLongDate(this._value, this.locale)}` : "";
|
|
1229
1287
|
single.setAttribute("aria-label", `${prefix}${suffix}`);
|
|
1230
1288
|
}
|
|
@@ -1236,9 +1294,9 @@ export class DatePickerElement extends HTMLElement {
|
|
|
1236
1294
|
const { start, end } = this._range;
|
|
1237
1295
|
for (const { button, endpoint } of this._buttons) {
|
|
1238
1296
|
const bound = endpoint === "end" ? end : start;
|
|
1239
|
-
const prefix = bound ? this.
|
|
1297
|
+
const prefix = bound ? this.messages.changeDate : this.messages.chooseDate;
|
|
1240
1298
|
const suffix = bound ? `, ${formatLongDate(bound, this.locale)}` : "";
|
|
1241
|
-
const boundLabel = endpoint === "end" ? this.
|
|
1299
|
+
const boundLabel = endpoint === "end" ? this.messages.rangeEnd : this.messages.rangeStart;
|
|
1242
1300
|
button.setAttribute("aria-label", `${prefix}${suffix}, ${boundLabel}`);
|
|
1243
1301
|
}
|
|
1244
1302
|
}
|
|
@@ -1294,7 +1352,7 @@ export class DatePickerElement extends HTMLElement {
|
|
|
1294
1352
|
}
|
|
1295
1353
|
const confirmed = await calendar.ensureDate(date);
|
|
1296
1354
|
const state = calendar.getDateState(date);
|
|
1297
|
-
if (!confirmed || state.disabled) return { ok: false, message: this.
|
|
1355
|
+
if (!confirmed || state.disabled) return { ok: false, message: this.messages.unavailableDate };
|
|
1298
1356
|
return { ok: true };
|
|
1299
1357
|
}
|
|
1300
1358
|
|
|
@@ -1425,7 +1483,7 @@ export class DatePickerElement extends HTMLElement {
|
|
|
1425
1483
|
for (const bound of bounds) {
|
|
1426
1484
|
const index = bound === "end" ? 1 : 0;
|
|
1427
1485
|
const input = this._fields[index].input;
|
|
1428
|
-
const message = bound === "end" ? this.
|
|
1486
|
+
const message = bound === "end" ? this.messages.rangeOrderEnd : this.messages.rangeOrderStart;
|
|
1429
1487
|
const shouldHaveOrder = inverted && bound === which;
|
|
1430
1488
|
if (shouldHaveOrder) {
|
|
1431
1489
|
input.setCustomValidity(message);
|
|
@@ -1461,7 +1519,7 @@ export class DatePickerElement extends HTMLElement {
|
|
|
1461
1519
|
for (const [index, bound] of bounds.entries()) {
|
|
1462
1520
|
const input = inputs[index];
|
|
1463
1521
|
if (!input) continue;
|
|
1464
|
-
const message = bound === "end" ? this.
|
|
1522
|
+
const message = bound === "end" ? this.messages.rangeOrderEnd : this.messages.rangeOrderStart;
|
|
1465
1523
|
const shouldHaveOrder = inverted && bound === which;
|
|
1466
1524
|
if (shouldHaveOrder) {
|
|
1467
1525
|
input.setCustomValidity(message);
|
package/src/messages.js
CHANGED
|
@@ -28,4 +28,16 @@ export function getDefaultMessages() {
|
|
|
28
28
|
/** @param {Partial<typeof DEFAULT_MESSAGES>} messages */
|
|
29
29
|
export function setDefaultMessages(messages) {
|
|
30
30
|
defaults = { ...defaults, ...messages };
|
|
31
|
+
for (const listener of listeners) listener();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** @type {Set<() => void>} */
|
|
35
|
+
const listeners = new Set();
|
|
36
|
+
|
|
37
|
+
/** @param {() => void} listener */
|
|
38
|
+
export function subscribeDefaultMessages(listener) {
|
|
39
|
+
listeners.add(listener);
|
|
40
|
+
return () => {
|
|
41
|
+
listeners.delete(listener);
|
|
42
|
+
};
|
|
31
43
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Replay properties assigned before custom-element definition through their setters.
|
|
3
|
+
* Call only once the element is ready to accept the listed properties.
|
|
4
|
+
* @param {HTMLElement} element
|
|
5
|
+
* @param {string[]} properties
|
|
6
|
+
*/
|
|
7
|
+
export function upgradeProperties(element, properties) {
|
|
8
|
+
for (const property of properties) {
|
|
9
|
+
if (!Object.hasOwn(element, property)) continue;
|
|
10
|
+
const value = Reflect.get(element, property);
|
|
11
|
+
Reflect.deleteProperty(element, property);
|
|
12
|
+
Reflect.set(element, property, value);
|
|
13
|
+
}
|
|
14
|
+
}
|