@lekoala/date-picker 0.3.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.
Files changed (41) hide show
  1. package/README.md +281 -263
  2. package/custom-elements.json +30 -0
  3. package/dist/date-picker.css +51 -46
  4. package/dist/date-picker.js +685 -204
  5. package/dist/date-picker.min.css +1 -1
  6. package/dist/date-picker.min.js +13 -13
  7. package/dist/date-picker.standalone.min.js +65 -60
  8. package/dist/types/control-classes.d.ts +30 -0
  9. package/dist/types/control-classes.d.ts.map +1 -0
  10. package/dist/types/date-calendar.d.ts +49 -3
  11. package/dist/types/date-calendar.d.ts.map +1 -1
  12. package/dist/types/date-field.d.ts +12 -4
  13. package/dist/types/date-field.d.ts.map +1 -1
  14. package/dist/types/date-picker.d.ts +106 -9
  15. package/dist/types/date-picker.d.ts.map +1 -1
  16. package/dist/types/date-range.d.ts +102 -20
  17. package/dist/types/date-range.d.ts.map +1 -1
  18. package/dist/types/date.d.ts +14 -2
  19. package/dist/types/date.d.ts.map +1 -1
  20. package/dist/types/index.d.ts +1 -0
  21. package/dist/types/index.d.ts.map +1 -1
  22. package/dist/types/intl.d.ts.map +1 -1
  23. package/dist/types/source.d.ts +1 -0
  24. package/dist/types/source.d.ts.map +1 -1
  25. package/docs/ACCESSIBILITY.md +6 -2
  26. package/docs/API.md +430 -358
  27. package/docs/DECISIONS.md +68 -49
  28. package/docs/MIGRATION-FLATPICKR.md +1 -1
  29. package/docs/TESTING.md +1 -1
  30. package/docs/USE_CASES.md +156 -141
  31. package/package.json +8 -8
  32. package/src/control-classes.js +50 -0
  33. package/src/date-calendar.js +188 -54
  34. package/src/date-field.js +9 -5
  35. package/src/date-picker.css +51 -46
  36. package/src/date-picker.js +392 -83
  37. package/src/date-range.js +184 -30
  38. package/src/date.js +57 -3
  39. package/src/index.js +1 -0
  40. package/src/intl.js +52 -17
  41. package/src/source.js +14 -4
@@ -1,4 +1,4 @@
1
- /*** @lekoala/date-picker v0.3.0 - https://github.com/lekoala/date-picker ***/
1
+ /*** @lekoala/date-picker v0.5.0 - https://github.com/lekoala/date-picker ***/
2
2
  (() => {
3
3
  var __defProp = Object.defineProperty;
4
4
  var __getOwnPropNames = Object.getOwnPropertyNames;
@@ -176,15 +176,48 @@
176
176
  const end = endOfWeek(endOfMonth(value), firstDay);
177
177
  const weeks = [];
178
178
  let cursor = start;
179
- while (compareDates(cursor, end) <= 0) {
179
+ for (;; ) {
180
180
  const week = [];
181
181
  for (let i = 0;i < 7; i++)
182
182
  week.push(addDays(cursor, i));
183
183
  weeks.push(week);
184
+ if (compareDates(week[6], end) >= 0) {
185
+ if (!options.fixedWeeks || weeks.length >= 6 || compareDates(week[6], addDays("9999-12-31", -7)) > 0)
186
+ break;
187
+ }
184
188
  cursor = addDays(cursor, 7);
185
189
  }
186
190
  return weeks;
187
191
  }
192
+ function dayOrdinal(year, month, day) {
193
+ const base = toUTCDate({ year: 1, month: 1, day: 1 });
194
+ return Math.round((toUTCDate({ year, month, day }).getTime() - base.getTime()) / 86400000);
195
+ }
196
+ function clampDisplayMonth(value, firstDay = 1) {
197
+ const normalized = normalizeFirstDay(firstDay);
198
+ const maxOrdinal = dayOrdinal(9999, 12, 31);
199
+ let key = monthKey(value);
200
+ for (;; ) {
201
+ const parsed = parseDate(`${key}-01`);
202
+ if (!parsed)
203
+ throw new TypeError(`Invalid civil date/month: ${value}`);
204
+ const startOrdinal = dayOrdinal(parsed.year, parsed.month, 1);
205
+ const startOffset = (toUTCDate(parsed).getUTCDay() - normalized + 7) % 7;
206
+ if (startOrdinal - startOffset < 0) {
207
+ key = shiftMonth(key, 1);
208
+ continue;
209
+ }
210
+ const lastDay = daysInMonth(parsed.year, parsed.month);
211
+ const endParts = { year: parsed.year, month: parsed.month, day: lastDay };
212
+ const endOrdinal = startOrdinal + lastDay - 1;
213
+ const endOffset = (toUTCDate(endParts).getUTCDay() - normalized + 7) % 7;
214
+ if (endOrdinal - endOffset + 6 > maxOrdinal) {
215
+ key = shiftMonth(key, -1);
216
+ continue;
217
+ }
218
+ return key;
219
+ }
220
+ }
188
221
  function clampDate(value, min = "", max = "") {
189
222
  if (!isDate(value))
190
223
  throw new TypeError(`Invalid civil date: ${value}`);
@@ -265,6 +298,30 @@
265
298
  }
266
299
  }
267
300
 
301
+ // src/control-classes.js
302
+ function normalizeControlClass(value) {
303
+ if (typeof value !== "string")
304
+ return "";
305
+ return [
306
+ ...new Set(value.split(/\s+/).filter((token) => token && !token.startsWith("dp-") && ![...token].some((char) => char.charCodeAt(0) < 32 || char.charCodeAt(0) === 127)))
307
+ ].join(" ");
308
+ }
309
+ function sameControlClasses(a, b) {
310
+ return a.month === b.month && a.year === b.year && a.nav === b.nav;
311
+ }
312
+ var defaults = { month: "", year: "", nav: "" };
313
+ function getDefaultControlClasses() {
314
+ return { ...defaults };
315
+ }
316
+ function resolveControlClasses(classes) {
317
+ const result = getDefaultControlClasses();
318
+ for (const role of ["month", "year", "nav"]) {
319
+ if (classes && Object.hasOwn(classes, role))
320
+ result[role] = normalizeControlClass(classes[role]);
321
+ }
322
+ return result;
323
+ }
324
+
268
325
  // src/date-range.js
269
326
  function normalizeRange(range) {
270
327
  if (range == null)
@@ -312,39 +369,160 @@
312
369
  focus(endpoint) {
313
370
  this.activeEndpoint = endpoint;
314
371
  }
315
- activate(date, editable = () => true) {
372
+ _refuse(endpoint) {
373
+ return {
374
+ status: "refused",
375
+ endpoint,
376
+ changedEndpoints: [],
377
+ range: this.range,
378
+ activeEndpoint: this.activeEndpoint
379
+ };
380
+ }
381
+ project(date, editable = () => true) {
316
382
  if (!isDate(date))
317
383
  throw new TypeError(`Invalid range activation: ${date}`);
318
- if (this.complete && (compareDates(date, this.start) < 0 || compareDates(date, this.end) > 0)) {
319
- const endpoint = compareDates(date, this.start) < 0 ? "start" : "end";
384
+ const { start, end } = this;
385
+ if (this.complete && (compareDates(date, start) < 0 || compareDates(date, end) > 0)) {
386
+ const endpoint = compareDates(date, start) < 0 ? "start" : "end";
320
387
  if (!editable(endpoint))
321
- return { status: "refused", endpoint };
322
- this[endpoint] = date;
323
- this.activeEndpoint = endpoint;
324
- return { status: "complete", endpoint };
388
+ return this._refuse(endpoint);
389
+ return {
390
+ status: "complete",
391
+ endpoint,
392
+ changedEndpoints: [endpoint],
393
+ range: endpoint === "start" ? { start: date, end } : { start, end: date },
394
+ activeEndpoint: endpoint
395
+ };
325
396
  }
326
397
  if (this.activeEndpoint === "end") {
327
- if (this.start && compareDates(date, this.start) < 0) {
328
- return { status: "refused", endpoint: "end" };
398
+ if (start && !end && compareDates(date, start) < 0) {
399
+ if (!editable("start"))
400
+ return this._refuse("start");
401
+ if (!editable("end"))
402
+ return this._refuse("end");
403
+ return {
404
+ status: "complete",
405
+ endpoint: "end",
406
+ changedEndpoints: ["start", "end"],
407
+ range: { start: date, end: start },
408
+ activeEndpoint: "end"
409
+ };
329
410
  }
411
+ if (start && compareDates(date, start) < 0)
412
+ return this._refuse("end");
330
413
  if (!editable("end"))
331
- return { status: "refused", endpoint: "end" };
332
- this.end = date;
333
- return { status: this.start ? "complete" : "pending", endpoint: "end" };
414
+ return this._refuse("end");
415
+ return {
416
+ status: start ? "complete" : "pending",
417
+ endpoint: "end",
418
+ changedEndpoints: ["end"],
419
+ range: { start, end: date },
420
+ activeEndpoint: "end"
421
+ };
334
422
  }
335
- if (!editable("start"))
336
- return { status: "refused", endpoint: "start" };
337
- this.start = date;
338
- if (!editable("end")) {
339
- return { status: "pending", endpoint: "start", close: true };
423
+ if (end && !start && compareDates(date, end) > 0) {
424
+ if (!editable("start"))
425
+ return this._refuse("start");
426
+ if (!editable("end"))
427
+ return this._refuse("end");
428
+ return {
429
+ status: "complete",
430
+ endpoint: "start",
431
+ changedEndpoints: ["start", "end"],
432
+ range: { start: end, end: date },
433
+ activeEndpoint: "start"
434
+ };
340
435
  }
341
- this.activeEndpoint = "end";
342
- return { status: "pending", endpoint: "start" };
436
+ if (!editable("start"))
437
+ return this._refuse("start");
438
+ const canContinue = editable("end");
439
+ return {
440
+ status: "pending",
441
+ endpoint: "start",
442
+ changedEndpoints: ["start"],
443
+ range: { start: date, end },
444
+ activeEndpoint: canContinue ? "end" : this.activeEndpoint,
445
+ ...canContinue ? {} : { close: true }
446
+ };
447
+ }
448
+ projectEndpoint(date, endpoint, editable = () => true) {
449
+ if (!isDate(date))
450
+ throw new TypeError(`Invalid range activation: ${date}`);
451
+ if (!this.complete || !editable(endpoint))
452
+ return this._refuse(endpoint);
453
+ const { start, end } = this;
454
+ const previous = endpoint === "start" ? start : end;
455
+ const target = endpoint === "start" ? earlier(date, end) : later(date, start);
456
+ return {
457
+ status: "complete",
458
+ endpoint,
459
+ changedEndpoints: target === previous ? [] : [endpoint],
460
+ range: endpoint === "start" ? { start: target, end } : { start, end: target },
461
+ activeEndpoint: endpoint
462
+ };
463
+ }
464
+ previewRange(date, endpoint = "", editable = () => true) {
465
+ if (!isDate(date))
466
+ return null;
467
+ if (!endpoint && Boolean(this.start) === Boolean(this.end))
468
+ return null;
469
+ const transition = endpoint ? this.projectEndpoint(date, endpoint, editable) : this.project(date, editable);
470
+ if (transition.status === "refused")
471
+ return null;
472
+ const { start, end } = transition.range;
473
+ if (!start || end && compareDates(end, start) < 0)
474
+ return null;
475
+ return { start, end };
476
+ }
477
+ _apply(transition) {
478
+ if (transition.status === "refused")
479
+ return transition;
480
+ this.start = transition.range.start;
481
+ this.end = transition.range.end;
482
+ this.activeEndpoint = transition.activeEndpoint;
483
+ return transition;
484
+ }
485
+ activate(date, editable = () => true) {
486
+ return this._apply(this.project(date, editable));
487
+ }
488
+ moveEndpoint(date, endpoint, editable = () => true) {
489
+ return this._apply(this.projectEndpoint(date, endpoint, editable));
343
490
  }
344
491
  }
492
+ function later(a, b) {
493
+ if (!a)
494
+ return b;
495
+ if (!b)
496
+ return a;
497
+ return compareDates(a, b) >= 0 ? a : b;
498
+ }
499
+ function earlier(a, b) {
500
+ if (!a)
501
+ return b;
502
+ if (!b)
503
+ return a;
504
+ return compareDates(a, b) <= 0 ? a : b;
505
+ }
345
506
 
346
507
  // src/intl.js
347
508
  var BIDI = /[\u200e\u200f\u061c\u202a-\u202e\u2066-\u2069]/g;
509
+ var dateFormatters = new Map;
510
+ var digitMaps = new Map;
511
+ function cached(cache, key, create) {
512
+ const existing = cache.get(key);
513
+ if (existing !== undefined)
514
+ return existing;
515
+ const value = create();
516
+ const oldest = cache.keys().next().value;
517
+ if (cache.size >= 64 && oldest !== undefined)
518
+ cache.delete(oldest);
519
+ cache.set(key, value);
520
+ return value;
521
+ }
522
+ function dateFormatter(locale, options) {
523
+ const key = JSON.stringify([locale, options]);
524
+ return cached(dateFormatters, key, () => new Intl.DateTimeFormat(locale, options));
525
+ }
348
526
  function resolveLocale(locale = "") {
349
527
  if (locale)
350
528
  return locale;
@@ -355,21 +533,25 @@
355
533
  return "en-US";
356
534
  }
357
535
  function digitMap(locale) {
358
- const formatter = new Intl.NumberFormat(locale, { useGrouping: false });
359
- const map = new Map;
360
- for (let i = 0;i <= 9; i++)
361
- map.set(formatter.format(i), String(i));
362
- return map;
536
+ return cached(digitMaps, locale, () => {
537
+ const formatter = new Intl.NumberFormat(locale, { useGrouping: false });
538
+ const map = new Map;
539
+ for (let i = 0;i <= 9; i++)
540
+ map.set(formatter.format(i), String(i));
541
+ return map;
542
+ });
363
543
  }
364
- function normalizeDigits(text, locale) {
365
- let result = String(text || "").replace(BIDI, "").trim();
544
+ function normalizeDigits(text, locale, options = {}) {
545
+ let result = String(text || "").replace(BIDI, "");
546
+ if (options.trim !== false)
547
+ result = result.trim();
366
548
  for (const [local, ascii] of digitMap(locale))
367
549
  result = result.split(local).join(ascii);
368
550
  return result;
369
551
  }
370
552
  function formatLongDate(value, locale = "") {
371
553
  const resolved = resolveLocale(locale);
372
- return new Intl.DateTimeFormat(resolved, {
554
+ return dateFormatter(resolved, {
373
555
  calendar: "gregory",
374
556
  weekday: "long",
375
557
  year: "numeric",
@@ -380,7 +562,7 @@
380
562
  }
381
563
  function formatMonthYear(value, locale = "") {
382
564
  const resolved = resolveLocale(locale);
383
- return new Intl.DateTimeFormat(resolved, {
565
+ return dateFormatter(resolved, {
384
566
  calendar: "gregory",
385
567
  year: "numeric",
386
568
  month: "long",
@@ -390,7 +572,7 @@
390
572
  function monthYearOrder(locale = "") {
391
573
  const resolved = resolveLocale(locale);
392
574
  try {
393
- const parts = new Intl.DateTimeFormat(resolved, {
575
+ const parts = dateFormatter(resolved, {
394
576
  calendar: "gregory",
395
577
  year: "numeric",
396
578
  month: "long",
@@ -415,12 +597,12 @@
415
597
  }
416
598
  function monthNames(locale = "", style = "long") {
417
599
  const resolved = resolveLocale(locale);
418
- const formatter = new Intl.DateTimeFormat(resolved, { calendar: "gregory", month: style, timeZone: "UTC" });
600
+ const formatter = dateFormatter(resolved, { calendar: "gregory", month: style, timeZone: "UTC" });
419
601
  return Array.from({ length: 12 }, (_, index) => formatter.format(toIntlDate(`2026-${String(index + 1).padStart(2, "0")}-15`)));
420
602
  }
421
603
  function weekdayNames(locale = "", firstDay = 1, style = "short") {
422
604
  const resolved = resolveLocale(locale);
423
- const formatter = new Intl.DateTimeFormat(resolved, {
605
+ const formatter = dateFormatter(resolved, {
424
606
  calendar: "gregory",
425
607
  weekday: style,
426
608
  timeZone: "UTC"
@@ -438,7 +620,7 @@
438
620
  }
439
621
  function createDateAdapter(locale = "") {
440
622
  const resolved = resolveLocale(locale);
441
- const formatter = new Intl.DateTimeFormat(resolved, {
623
+ const formatter = dateFormatter(resolved, {
442
624
  calendar: "gregory",
443
625
  year: "numeric",
444
626
  month: "2-digit",
@@ -462,8 +644,8 @@
462
644
  return "(\\d{1,2})";
463
645
  if (part.type === "year")
464
646
  return "(\\d{4})";
465
- const literal = normalizeDigits(part.value, resolved).replace(/\s+/g, " ");
466
- return escapeRegExp(literal).replace(/\\ /g, "\\s*");
647
+ const literal = normalizeDigits(part.value, resolved, { trim: false });
648
+ return escapeRegExp(literal).replace(/\s+/g, "\\s*");
467
649
  }).join("");
468
650
  const regex = new RegExp(`^\\s*${pattern}\\s*$`);
469
651
  return {
@@ -517,9 +699,9 @@
517
699
  rangeOrderStart: "Start must be on or before end",
518
700
  rangeOrderEnd: "End must be on or after start"
519
701
  };
520
- var defaults = { ...DEFAULT_MESSAGES };
702
+ var defaults2 = { ...DEFAULT_MESSAGES };
521
703
  function getDefaultMessages() {
522
- return { ...defaults };
704
+ return { ...defaults2 };
523
705
  }
524
706
 
525
707
  // src/source.js
@@ -557,19 +739,24 @@
557
739
  function yearOf(month) {
558
740
  return Number(month.slice(0, 4));
559
741
  }
742
+ function stepMonthKey(month, amount) {
743
+ const [year, index] = month.split("-").map(Number);
744
+ const absolute = year * 12 + (index - 1) + amount;
745
+ const stepYear = Math.floor(absolute / 12);
746
+ const stepMonth = (absolute % 12 + 12) % 12 + 1;
747
+ if (stepYear < 1 || stepYear > 9999)
748
+ return null;
749
+ return `${String(stepYear).padStart(4, "0")}-${String(stepMonth).padStart(2, "0")}`;
750
+ }
751
+ function rangeAttributeMap(position) {
752
+ return {
753
+ "data-range-start": position === "start" || position === "single",
754
+ "data-range-end": position === "end" || position === "single",
755
+ "data-in-range": position === "in"
756
+ };
757
+ }
560
758
  function rangeAttributes(position) {
561
- switch (position) {
562
- case "single":
563
- return ' data-range-start="true" data-range-end="true"';
564
- case "start":
565
- return ' data-range-start="true"';
566
- case "end":
567
- return ' data-range-end="true"';
568
- case "in":
569
- return ' data-in-range="true"';
570
- default:
571
- return "";
572
- }
759
+ return Object.entries(rangeAttributeMap(position)).filter(([, on]) => on).map(([name]) => ` ${name}="true"`).join("");
573
760
  }
574
761
  function rangeLabelKey(position) {
575
762
  if (position === "single")
@@ -612,7 +799,11 @@
612
799
  this._renderDay = null;
613
800
  this._isDateDisabled = null;
614
801
  this._highlightedRange = { start: "", end: "" };
802
+ this._paintedRange = { start: "", end: "" };
803
+ this._paintedPreview = false;
804
+ this._renderedStates = new Map;
615
805
  this._messages = getDefaultMessages();
806
+ this._controlClasses = getDefaultControlClasses();
616
807
  this._onClick = this._onClick.bind(this);
617
808
  this._onChange = this._onChange.bind(this);
618
809
  this._onKeyDown = this._onKeyDown.bind(this);
@@ -730,11 +921,12 @@
730
921
  }
731
922
  set highlightedRange(value) {
732
923
  const next = normalizeRange(value);
733
- if (next.start === this._highlightedRange.start && next.end === this._highlightedRange.end)
924
+ const preview = this.hasAttribute("data-range-preview");
925
+ if (next.start === this._paintedRange.start && next.end === this._paintedRange.end && preview === this._paintedPreview)
734
926
  return;
735
927
  this._highlightedRange = next;
736
928
  if (this._connected)
737
- this.render();
929
+ this._updateRangeDom();
738
930
  }
739
931
  get fixedWeeks() {
740
932
  return this.hasAttribute("fixed-weeks");
@@ -748,6 +940,17 @@
748
940
  set showWeekNumbers(value) {
749
941
  this.toggleAttribute("show-week-numbers", Boolean(value));
750
942
  }
943
+ get controlClasses() {
944
+ return { ...this._controlClasses };
945
+ }
946
+ set controlClasses(value) {
947
+ const classes = resolveControlClasses(value);
948
+ if (sameControlClasses(classes, this._controlClasses))
949
+ return;
950
+ this._controlClasses = classes;
951
+ if (this._connected)
952
+ this.render();
953
+ }
751
954
  get messages() {
752
955
  return this._messages;
753
956
  }
@@ -796,7 +999,7 @@
796
999
  const attrDisplay = this.getAttribute("display") || "";
797
1000
  if (attrDisplay) {
798
1001
  try {
799
- this._model.setDisplay(attrDisplay);
1002
+ this._model.setDisplay(clampDisplayMonth(attrDisplay, firstDay));
800
1003
  } catch {}
801
1004
  } else if (this._model.value) {
802
1005
  this._model.display = monthKey(this._model.value);
@@ -817,21 +1020,21 @@
817
1020
  this._rendering = false;
818
1021
  }
819
1022
  _clamp(value) {
820
- return clampDate(value, this.min, this.max);
1023
+ const bounded = clampDate(value, this.min, this.max);
1024
+ const month = monthKey(bounded);
1025
+ const safeMonth = clampDisplayMonth(month, this.firstDay);
1026
+ if (safeMonth === month)
1027
+ return bounded;
1028
+ const day = Number(bounded.slice(8, 10)) || 1;
1029
+ const candidate = addDays(`${safeMonth}-01`, day - 1);
1030
+ const last = endOfMonth(safeMonth);
1031
+ return compareDates(candidate, last) > 0 ? last : candidate;
821
1032
  }
822
1033
  _weeks(display = this.display) {
823
- const weeks = getMonthWeeks(display, { firstDay: this.firstDay });
824
- if (!this.fixedWeeks || weeks.length >= 6)
825
- return weeks;
826
- let cursor = weeks[weeks.length - 1]?.[6] || "";
827
- while (weeks.length < 6 && cursor) {
828
- const week = [];
829
- for (let i = 1;i <= 7; i++)
830
- week.push(addDays(cursor, i));
831
- weeks.push(week);
832
- cursor = week[6];
833
- }
834
- return weeks;
1034
+ return getMonthWeeks(clampDisplayMonth(display, this.firstDay), {
1035
+ firstDay: this.firstDay,
1036
+ fixedWeeks: this.fixedWeeks
1037
+ });
835
1038
  }
836
1039
  _focusTargetKey(element) {
837
1040
  if (element.matches(".dp-month-select, .dp-year-input") && element.id) {
@@ -934,7 +1137,7 @@
934
1137
  }
935
1138
  _setDisplay(display, options = {}) {
936
1139
  const previous = this._model.display;
937
- this._model.setDisplay(display);
1140
+ this._model.setDisplay(clampDisplayMonth(display, this.firstDay));
938
1141
  this._model.focused = this._clamp(this._model.focused);
939
1142
  this._model.display = monthKey(this._model.focused);
940
1143
  this._reflect("display", this._model.display);
@@ -952,10 +1155,10 @@
952
1155
  }));
953
1156
  }
954
1157
  previousMonth() {
955
- return this._setDisplay(shiftMonth(this.display, -1));
1158
+ return this._setDisplay(stepMonthKey(this.display, -1) ?? this.display);
956
1159
  }
957
1160
  nextMonth() {
958
- return this._setDisplay(shiftMonth(this.display, 1));
1161
+ return this._setDisplay(stepMonthKey(this.display, 1) ?? this.display);
959
1162
  }
960
1163
  focusDate(date, options = {}) {
961
1164
  if (!isDate(date))
@@ -1007,12 +1210,48 @@
1007
1210
  }
1008
1211
  return false;
1009
1212
  }
1213
+ _dayLabel(date, state, position) {
1214
+ const rangeLabel = this.hasAttribute("data-range-preview") ? "" : rangeLabelKey(position);
1215
+ const description = typeof state.description === "string" ? state.description : "";
1216
+ return [
1217
+ formatLongDate(date, this.locale),
1218
+ description,
1219
+ rangeLabel ? this._messages[rangeLabel] : "",
1220
+ state.disabled ? this._messages.unavailable : ""
1221
+ ].filter(Boolean).join(". ");
1222
+ }
1223
+ _updateRangeDom() {
1224
+ const previousRange = this._paintedRange;
1225
+ const previousPreview = this._paintedPreview;
1226
+ this._paintedRange = { ...this._highlightedRange };
1227
+ this._paintedPreview = this.hasAttribute("data-range-preview");
1228
+ for (const cell of this.querySelectorAll(".dp-day[data-date]")) {
1229
+ if (!(cell instanceof HTMLTableCellElement))
1230
+ continue;
1231
+ const date = cell.dataset.date || "";
1232
+ const position = rangePosition(date, this._highlightedRange);
1233
+ if (position === rangePosition(date, previousRange) && previousPreview === this._paintedPreview)
1234
+ continue;
1235
+ for (const [name, on] of Object.entries(rangeAttributeMap(position))) {
1236
+ if (on)
1237
+ cell.setAttribute(name, "true");
1238
+ else
1239
+ cell.removeAttribute(name);
1240
+ }
1241
+ if (!previousPreview || !this._paintedPreview) {
1242
+ const state = this._renderedStates.get(date) || {};
1243
+ const label = this._dayLabel(date, state, position);
1244
+ if (cell.getAttribute("aria-label") !== label)
1245
+ cell.setAttribute("aria-label", label);
1246
+ }
1247
+ }
1248
+ }
1010
1249
  focusGrid() {
1011
1250
  const target = this.querySelector(`.dp-day[data-date="${CSS.escape(this.focusedDate)}"]`);
1012
1251
  if (target instanceof HTMLElement)
1013
1252
  target.focus();
1014
1253
  }
1015
- async _activate(date) {
1254
+ async activateDate(date) {
1016
1255
  const confirmed = await this.ensureDate(date);
1017
1256
  const state = this.getDateState(date);
1018
1257
  if (!confirmed || state.disabled) {
@@ -1068,7 +1307,7 @@
1068
1307
  this._reflect("display", this._model.display);
1069
1308
  if (previousDisplay !== this.display)
1070
1309
  this._emitDisplayChange();
1071
- this._activate(date);
1310
+ this.activateDate(date);
1072
1311
  }
1073
1312
  _onChange(event) {
1074
1313
  const target = event.target;
@@ -1078,7 +1317,7 @@
1078
1317
  }
1079
1318
  if (target instanceof HTMLInputElement && target.matches(".dp-year-input")) {
1080
1319
  const year = Number(target.value);
1081
- if (!Number.isInteger(year) || year < 1 || year > 9999) {
1320
+ if (!Number.isInteger(year) || year < 1 || year > 9998) {
1082
1321
  if (this._connected)
1083
1322
  this.render(false);
1084
1323
  return;
@@ -1091,8 +1330,10 @@
1091
1330
  if (!(target instanceof HTMLTableCellElement) || !target.matches(".dp-day[data-date]"))
1092
1331
  return;
1093
1332
  const date = target.dataset.date || "";
1094
- if (isDate(date))
1095
- this._model.focused = date;
1333
+ if (!isDate(date))
1334
+ return;
1335
+ this._model.focused = date;
1336
+ this.dispatchEvent(new CustomEvent("datefocus", { detail: { date, state: this.getDateState(date) }, bubbles: true }));
1096
1337
  }
1097
1338
  _onKeyDown(event) {
1098
1339
  const target = event.target;
@@ -1101,7 +1342,7 @@
1101
1342
  const date = target.dataset.date || this.focusedDate;
1102
1343
  if (event.key === "Enter" || event.key === " ") {
1103
1344
  event.preventDefault();
1104
- this._activate(date);
1345
+ this.activateDate(date);
1105
1346
  return;
1106
1347
  }
1107
1348
  const anchorFocused = this._model.focused;
@@ -1150,8 +1391,10 @@
1150
1391
  const gridId = `${this._id}-grid`;
1151
1392
  const minMonth = this.min ? monthKey(this.min) : "";
1152
1393
  const maxMonth = this.max ? monthKey(this.max) : "";
1153
- const prevDisabled = Boolean(minMonth && shiftMonth(display, -1) < minMonth);
1154
- const nextDisabled = Boolean(maxMonth && shiftMonth(display, 1) > maxMonth);
1394
+ const prevKey = stepMonthKey(display, -1);
1395
+ const nextKey = stepMonthKey(display, 1);
1396
+ const prevDisabled = Boolean(minMonth && prevKey !== null && prevKey < minMonth);
1397
+ const nextDisabled = Boolean(maxMonth && nextKey !== null && nextKey > maxMonth);
1155
1398
  const minYearAttr = this.min ? ` min="${Number(this.min.slice(0, 4))}"` : "";
1156
1399
  const maxYearAttr = this.max ? ` max="${Number(this.max.slice(0, 4))}"` : "";
1157
1400
  const showWeeks = this.showWeekNumbers && this.firstDay === 1;
@@ -1163,23 +1406,18 @@
1163
1406
  return `<option value="${String(month).padStart(2, "0")}"${month === displayMonth ? " selected" : ""}${disabled ? " disabled" : ""}>${escapeHtml(name)}</option>`;
1164
1407
  }).join("");
1165
1408
  const dayHeaders = weekdayShort.map((name, index) => `<th scope="col" abbr="${escapeHtml(weekdayLong[index])}">${escapeHtml(name.replace(".", ""))}</th>`).join("");
1409
+ this._renderedStates.clear();
1166
1410
  const rows = weeks.map((week) => {
1167
1411
  const weekNumber = showWeeks ? `<th scope="row" class="dp-week-number">${isoWeekNumber(week[0])}</th>` : "";
1168
1412
  const cells = week.map((date) => {
1169
1413
  const state = this.getDateState(date);
1414
+ this._renderedStates.set(date, state);
1170
1415
  const outside = monthKey(date) !== display;
1171
1416
  const selected = this.value === date;
1172
1417
  const focused = this.focusedDate === date;
1173
1418
  const isToday = date === today;
1174
1419
  const position = rangePosition(date, this._highlightedRange);
1175
- const rangeLabel = rangeLabelKey(position);
1176
- const description = typeof state.description === "string" ? state.description : "";
1177
- const label = [
1178
- formatLongDate(date, locale),
1179
- description,
1180
- rangeLabel ? this._messages[rangeLabel] : "",
1181
- state.disabled ? this._messages.unavailable : ""
1182
- ].filter(Boolean).join(". ");
1420
+ const label = this._dayLabel(date, state, position);
1183
1421
  return `<td class="dp-day" data-date="${date}"${outside ? ' data-outside-month="true"' : ""}${isToday ? ' data-today="true" aria-current="date"' : ""}${selected ? ' data-selected="true" aria-selected="true"' : ""}${rangeAttributes(position)}${state.disabled ? ' data-disabled="true" aria-disabled="true"' : ""} tabindex="${focused ? "0" : "-1"}" aria-label="${escapeHtml(label)}"><span class="dp-day-number" aria-hidden="true">${Number(date.slice(8, 10))}</span><span class="dp-day-extra" aria-hidden="true"></span></td>`;
1184
1422
  }).join("");
1185
1423
  return `<tr>${weekNumber}${cells}</tr>`;
@@ -1188,18 +1426,22 @@
1188
1426
  const focusKey = focusedElement instanceof HTMLElement && this.contains(focusedElement) ? this._focusTargetKey(focusedElement) : "";
1189
1427
  const yearFirst = monthYearOrder(locale)[0] === "year";
1190
1428
  this.toggleAttribute("data-year-first", yearFirst);
1429
+ const classes = this._controlClasses;
1430
+ const monthClass = escapeHtml(classes.month || "dp-skin");
1431
+ const yearClass = escapeHtml(classes.year || "dp-skin");
1432
+ const navClass = escapeHtml(classes.nav || "dp-skin");
1191
1433
  const monthControl = `<label class="dp-visually-hidden" for="${this._id}-month">${escapeHtml(this._messages.month)}</label>
1192
- <select id="${this._id}-month" class="dp-month-select" aria-label="${escapeHtml(this._messages.month)}">${monthOptions}</select>`;
1434
+ <select id="${this._id}-month" class="dp-month-select ${monthClass}" aria-label="${escapeHtml(this._messages.month)}">${monthOptions}</select>`;
1193
1435
  const yearControl = `<label class="dp-visually-hidden" for="${this._id}-year">${escapeHtml(this._messages.year)}</label>
1194
- <input id="${this._id}-year" class="dp-year-input" type="number" inputmode="numeric" value="${displayYear}"${minYearAttr}${maxYearAttr} aria-label="${escapeHtml(this._messages.year)}">`;
1436
+ <input id="${this._id}-year" class="dp-year-input ${yearClass}" type="number" inputmode="numeric" value="${displayYear}"${minYearAttr}${maxYearAttr} aria-label="${escapeHtml(this._messages.year)}">`;
1195
1437
  this._rendering = true;
1196
1438
  this.innerHTML = `
1197
1439
  <div class="dp-calendar-shell">
1198
1440
  <div class="dp-calendar-header">
1199
1441
  ${yearFirst ? yearControl : monthControl}
1200
1442
  ${yearFirst ? monthControl : yearControl}
1201
- <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="m10 4-4 4 4 4"/></svg></button>
1202
- <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="m6 4 4 4-4 4"/></svg></button>
1443
+ <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>
1444
+ <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>
1203
1445
  </div>
1204
1446
  <h2 id="${headingId}" class="dp-calendar-heading dp-visually-hidden" aria-live="polite">${escapeHtml(formatMonthYear(`${display}-15`, locale))}</h2>
1205
1447
  <table id="${gridId}" class="dp-grid" role="grid" aria-labelledby="${headingId}">
@@ -1208,6 +1450,8 @@
1208
1450
  </table>
1209
1451
  </div>`;
1210
1452
  this._rendering = false;
1453
+ this._paintedRange = { ...this._highlightedRange };
1454
+ this._paintedPreview = this.hasAttribute("data-range-preview");
1211
1455
  if (focusKey) {
1212
1456
  queueMicrotask(() => {
1213
1457
  let selector = focusKey;
@@ -1227,7 +1471,7 @@
1227
1471
  const extra = cell.querySelector(".dp-day-extra");
1228
1472
  if (!(extra instanceof HTMLElement))
1229
1473
  continue;
1230
- const content = this._renderDay(date, this.getDateState(date));
1474
+ const content = this._renderDay(date, this._renderedStates.get(date) || {});
1231
1475
  if (content instanceof Node)
1232
1476
  extra.append(content);
1233
1477
  else if (content !== undefined && content !== null)
@@ -1243,13 +1487,15 @@
1243
1487
  function crossAxisFor(side) {
1244
1488
  return side === "top" || side === "bottom" ? "x" : "y";
1245
1489
  }
1490
+ var OPPOSITE = { top: "bottom", bottom: "top", left: "right", right: "left" };
1491
+ var PLACEMENTS = new Set(Object.keys(OPPOSITE).flatMap((side) => [side, `${side}-start`, `${side}-end`]));
1492
+ var COORDINATE_SPACES = new Set(["viewport", "document"]);
1246
1493
  function parsePlacement(placement) {
1494
+ if (!PLACEMENTS.has(placement))
1495
+ throw new TypeError(`Unknown placement: ${placement}`);
1247
1496
  const [side, align = null] = placement.split("-");
1248
1497
  return { side, align, crossAxis: crossAxisFor(side) };
1249
1498
  }
1250
- function flipSide(side) {
1251
- return { top: "bottom", bottom: "top", left: "right", right: "left" }[side] || side;
1252
- }
1253
1499
  function computeCoords(reference, floating, side, align, rtl, distance) {
1254
1500
  const crossAxis = crossAxisFor(side);
1255
1501
  const commonX = reference.x + reference.width / 2 - floating.width / 2;
@@ -1281,17 +1527,12 @@
1281
1527
  function overflowOn(position, size, start, end) {
1282
1528
  return Math.max(start - position, 0) + Math.max(position + size - end, 0);
1283
1529
  }
1284
- function isRTL(element) {
1285
- const direction = "dir" in element ? element.dir : "";
1286
- if (direction === "rtl")
1287
- return true;
1288
- if (direction === "ltr")
1289
- return false;
1290
- const win = element.ownerDocument?.defaultView;
1291
- if (win?.CSS?.supports?.("selector(:dir(rtl))") && typeof element.matches === "function") {
1292
- return element.matches(":dir(rtl)");
1530
+ function isRTL(reference) {
1531
+ const win = reference.ownerDocument?.defaultView;
1532
+ if (win && reference instanceof win.Element) {
1533
+ return win.getComputedStyle(reference).direction === "rtl";
1293
1534
  }
1294
- return Boolean(win?.Element && element instanceof win.Element && win.getComputedStyle(element).direction === "rtl");
1535
+ return "dir" in reference && reference.dir === "rtl";
1295
1536
  }
1296
1537
  var STABLE_SCROLLBAR_MAX_WIDTH = 25;
1297
1538
  function getViewportBoundary(doc) {
@@ -1329,8 +1570,14 @@
1329
1570
  const percent = (referenceCenter - boxStart) / size * 100;
1330
1571
  return Math.round(Math.min(100, Math.max(0, percent)) * 1000) / 1000;
1331
1572
  }
1332
- function isOutsideBoundary(rect, boundary) {
1333
- return rect.right < boundary.x || rect.left > boundary.right || rect.bottom < boundary.y || rect.top > boundary.bottom;
1573
+ function isOutsideBoundary(rects, boundary) {
1574
+ for (let i = 0;i < rects.length; i++) {
1575
+ const rect = rects[i];
1576
+ if (rect.right >= boundary.x && rect.left <= boundary.right && rect.bottom >= boundary.y && rect.top <= boundary.bottom) {
1577
+ return false;
1578
+ }
1579
+ }
1580
+ return true;
1334
1581
  }
1335
1582
  function getAvailableHeight(referenceRect, side, boundary, distance, padding) {
1336
1583
  if (side === "top") {
@@ -1385,10 +1632,22 @@
1385
1632
  for (const [subscription, type] of notifications) {
1386
1633
  if (!subscriptions.has(subscription) || !subscription.floating.isConnected)
1387
1634
  continue;
1388
- subscription.callback({ type });
1635
+ try {
1636
+ subscription.callback({ type });
1637
+ } catch (error) {
1638
+ reportError(error);
1639
+ }
1389
1640
  }
1390
1641
  });
1391
1642
  }
1643
+ function reportError(error) {
1644
+ if (typeof win.reportError === "function")
1645
+ win.reportError(error);
1646
+ else
1647
+ win.setTimeout(() => {
1648
+ throw error;
1649
+ });
1650
+ }
1392
1651
  function notifyAll(type) {
1393
1652
  for (const subscription of subscriptions)
1394
1653
  queue(subscription, type);
@@ -1479,28 +1738,32 @@
1479
1738
  return trackerFor(floating).add(reference, floating, callback);
1480
1739
  }
1481
1740
  function positionOnce(reference, floating, options) {
1741
+ const placement = options.placement === undefined ? "bottom-start" : options.placement;
1742
+ let { side, align, crossAxis } = parsePlacement(placement);
1743
+ const coordinateSpace = options.coordinateSpace === undefined ? "viewport" : options.coordinateSpace;
1744
+ if (!COORDINATE_SPACES.has(coordinateSpace)) {
1745
+ throw new TypeError(`Unknown coordinateSpace: ${coordinateSpace}`);
1746
+ }
1482
1747
  if (!isVisible(floating))
1483
1748
  return null;
1484
- const placement = options.placement || "bottom-start";
1485
1749
  const distance = options.distance || 0;
1486
1750
  const flip = options.flip !== false;
1487
1751
  const shift = options.shift !== false;
1488
1752
  const shiftPadding = options.shiftPadding ?? 4;
1489
- let { side, align, crossAxis } = parsePlacement(placement);
1490
1753
  const rtl = align ? isRTL(reference) : false;
1491
1754
  const rects = reference.getClientRects();
1492
- const referenceRect = side === "bottom" ? rects[rects.length - 1] : rects[0];
1493
- if (!referenceRect)
1755
+ if (!rects.length)
1494
1756
  return null;
1757
+ const rectFor = (s) => s === "bottom" ? rects[rects.length - 1] : rects[0];
1495
1758
  const boundary = getBoundary(reference, options);
1496
- if (!boundary || isOutsideBoundary(referenceRect, boundary))
1759
+ if (!boundary || isOutsideBoundary(rects, boundary))
1497
1760
  return null;
1498
1761
  const floatingRect = getFloatingSize(floating);
1499
1762
  const limits = {
1500
1763
  x: { size: floatingRect.width, start: boundary.x, end: boundary.right },
1501
1764
  y: { size: floatingRect.height, start: boundary.y, end: boundary.bottom }
1502
1765
  };
1503
- const place = (nextSide, nextAlign) => computeCoords(referenceRect, floatingRect, nextSide, nextAlign, rtl, distance);
1766
+ const place = (nextSide, nextAlign) => computeCoords(rectFor(nextSide), floatingRect, nextSide, nextAlign, rtl, distance);
1504
1767
  const overflowAt = (position, axis) => {
1505
1768
  const { size, start, end } = limits[axis];
1506
1769
  return overflowOn(position, size, start + shiftPadding, end - shiftPadding);
@@ -1510,31 +1773,30 @@
1510
1773
  return overflowAt(shift ? clampToBoundary(position, size, start, end, shiftPadding) : position, axis);
1511
1774
  };
1512
1775
  let coords = place(side, align);
1776
+ const overflowFor = (nextSide, at) => {
1777
+ const cross = crossAxisFor(nextSide);
1778
+ const main = cross === "x" ? "y" : "x";
1779
+ const mainOverflow = overflowAt(at[main], main);
1780
+ return { main: mainOverflow, total: mainOverflow + shiftedOverflowAt(at[cross], cross) };
1781
+ };
1513
1782
  if (flip) {
1514
- const mainAxis = crossAxis === "x" ? "y" : "x";
1515
- let overflow = overflowAt(coords[mainAxis], mainAxis);
1516
- if (overflow > 0) {
1517
- const opposite = flipSide(side);
1518
- const flipped = place(opposite, align);
1519
- const flippedOverflow = overflowAt(flipped[mainAxis], mainAxis);
1520
- if (flippedOverflow < overflow) {
1521
- side = opposite;
1522
- coords = flipped;
1523
- overflow = flippedOverflow;
1524
- }
1525
- }
1526
- if (mainAxis === "x" && overflow > 0) {
1527
- const above = place("top", align);
1528
- const below = place("bottom", align);
1529
- const useBottom = overflowAt(below.y, "y") < overflowAt(above.y, "y");
1530
- const candidate = useBottom ? below : above;
1531
- const swapped = overflowAt(candidate.y, "y") + shiftedOverflowAt(candidate.x, "x");
1532
- if (swapped < overflow + shiftedOverflowAt(coords.y, "y")) {
1533
- side = useBottom ? "bottom" : "top";
1534
- crossAxis = "x";
1535
- coords = candidate;
1783
+ let best = overflowFor(side, coords);
1784
+ const consider = (nextSide) => {
1785
+ const next = place(nextSide, align);
1786
+ const overflow = overflowFor(nextSide, next);
1787
+ if (overflow.total < best.total) {
1788
+ side = nextSide;
1789
+ coords = next;
1790
+ best = overflow;
1536
1791
  }
1792
+ };
1793
+ if (best.main > 0)
1794
+ consider(OPPOSITE[side]);
1795
+ if (crossAxis === "y" && best.main > 0) {
1796
+ consider("top");
1797
+ consider("bottom");
1537
1798
  }
1799
+ crossAxis = crossAxisFor(side);
1538
1800
  }
1539
1801
  if (crossAxis === "x" && shift && align) {
1540
1802
  const overflow = overflowAt(coords.x, "x");
@@ -1551,12 +1813,13 @@
1551
1813
  const { size, start, end } = limits[crossAxis];
1552
1814
  coords[crossAxis] = clampToBoundary(coords[crossAxis], size, start, end, shiftPadding);
1553
1815
  }
1554
- const arrowX = arrowPercent(referenceRect.x + referenceRect.width / 2, coords.x, floatingRect.width);
1555
- const arrowY = arrowPercent(referenceRect.y + referenceRect.height / 2, coords.y, floatingRect.height);
1556
- const availableHeight = `${getAvailableHeight(referenceRect, side, boundary, distance, shiftPadding)}px`;
1816
+ const resolvedRect = rectFor(side);
1817
+ const arrowX = arrowPercent(resolvedRect.x + resolvedRect.width / 2, coords.x, floatingRect.width);
1818
+ const arrowY = arrowPercent(resolvedRect.y + resolvedRect.height / 2, coords.y, floatingRect.height);
1819
+ const availableHeight = `${getAvailableHeight(resolvedRect, side, boundary, distance, shiftPadding)}px`;
1557
1820
  const { style } = floating;
1558
1821
  const roomChanged = style.getPropertyValue("--available-height") !== availableHeight;
1559
- const win = options.coordinateSpace === "document" ? reference.ownerDocument.defaultView : null;
1822
+ const win = coordinateSpace === "document" ? reference.ownerDocument.defaultView : null;
1560
1823
  const originX = win ? win.scrollX : 0;
1561
1824
  const originY = win ? win.scrollY : 0;
1562
1825
  style.left = `${coords.x + originX}px`;
@@ -1684,9 +1947,9 @@
1684
1947
  input.setCustomValidity("");
1685
1948
  return;
1686
1949
  }
1687
- input.setCustomValidity(text === canonical ? "" : this.messages.invalidDate);
1950
+ input.setCustomValidity(this.messages.invalidDate);
1688
1951
  }
1689
- async commit() {
1952
+ async commit(options = {}) {
1690
1953
  const input = this.input;
1691
1954
  const commitId = ++this._commitId;
1692
1955
  const text = input.value.trim();
@@ -1706,7 +1969,7 @@
1706
1969
  return { status: "invalid" };
1707
1970
  }
1708
1971
  if (this.confirm) {
1709
- const result = await this.confirm(parsed);
1972
+ const result = await this.confirm(parsed, options);
1710
1973
  if (commitId !== this._commitId)
1711
1974
  return { status: "stale" };
1712
1975
  if (!result?.ok) {
@@ -1778,6 +2041,7 @@
1778
2041
 
1779
2042
  // src/date-picker.js
1780
2043
  var uid2 = 0;
2044
+ var DRAG_THRESHOLD = 4;
1781
2045
 
1782
2046
  class DatePickerElement extends HTMLElement {
1783
2047
  static observedAttributes = ["value", "locale", "min", "max", "open-on-focus", "month-format"];
@@ -1797,6 +2061,9 @@
1797
2061
  this._lastFocusEndpoint = "";
1798
2062
  this._rangeCommitId = 0;
1799
2063
  this._pendingIntents = new Map;
2064
+ this._previewDate = "";
2065
+ this._drag = null;
2066
+ this._suppressGridClick = false;
1800
2067
  this._buttons = [];
1801
2068
  this._returnFocus = null;
1802
2069
  this._panel = null;
@@ -1810,6 +2077,7 @@
1810
2077
  this._open = false;
1811
2078
  this._suppressFocusOpen = false;
1812
2079
  this._messages = getDefaultMessages();
2080
+ this._controlClasses = getDefaultControlClasses();
1813
2081
  this._source = null;
1814
2082
  this._dateState = null;
1815
2083
  this._renderDay = null;
@@ -1839,7 +2107,7 @@
1839
2107
  this._discoverTimeFields();
1840
2108
  this._field = new DateFieldController(input, { messages: this._messages, locale: this.locale });
1841
2109
  this._field.onAttributesChanged = () => this._syncInputState();
1842
- this._field.confirm = (date) => this._confirmDate(date);
2110
+ this._field.confirm = (date, options) => this._confirmDate(date, options);
1843
2111
  this._setupFormValue();
1844
2112
  this._build();
1845
2113
  this._syncInputState();
@@ -1903,7 +2171,7 @@
1903
2171
  ];
1904
2172
  for (const field of this._fields) {
1905
2173
  field.onAttributesChanged = () => this._syncRangeFields();
1906
- field.confirm = (date) => this._confirmDate(date);
2174
+ field.confirm = (date, options) => this._confirmDate(date, options);
1907
2175
  field.setupFormValue();
1908
2176
  }
1909
2177
  this._build();
@@ -2064,6 +2332,17 @@
2064
2332
  get open() {
2065
2333
  return this._open;
2066
2334
  }
2335
+ get controlClasses() {
2336
+ return { ...this._controlClasses };
2337
+ }
2338
+ set controlClasses(value) {
2339
+ const classes = resolveControlClasses(value);
2340
+ if (sameControlClasses(classes, this._controlClasses))
2341
+ return;
2342
+ this._controlClasses = classes;
2343
+ if (this._calendar)
2344
+ this._calendar.controlClasses = this._controlClasses;
2345
+ }
2067
2346
  get messages() {
2068
2347
  return this._messages;
2069
2348
  }
@@ -2148,6 +2427,7 @@
2148
2427
  const field = this._fields[index];
2149
2428
  button.disabled = Boolean(field?.input.disabled || field?.input.readOnly);
2150
2429
  }
2430
+ this._syncDragAffordance();
2151
2431
  const active = this._range?.activeEndpoint === "end" ? 1 : 0;
2152
2432
  if (this._open && (this._fields[active]?.input.disabled || this._fields[active]?.input.readOnly)) {
2153
2433
  this.hide(false);
@@ -2226,6 +2506,23 @@
2226
2506
  this._refreshLocale();
2227
2507
  this._refreshButtonLabel();
2228
2508
  }
2509
+ _bindPopup(signal, input, restore) {
2510
+ this.ownerDocument.addEventListener("pointerdown", (event) => {
2511
+ if (!this._open || event.composedPath().includes(this))
2512
+ return;
2513
+ this.hide(false);
2514
+ }, { capture: true, signal });
2515
+ this.ownerDocument.addEventListener("reset", (event) => {
2516
+ if (!(event.target instanceof HTMLFormElement) || !input || event.target !== input.form)
2517
+ return;
2518
+ this.hide(false);
2519
+ queueMicrotask(() => {
2520
+ if (!event.defaultPrevented)
2521
+ restore();
2522
+ });
2523
+ }, { capture: true, signal });
2524
+ this.addEventListener("keydown", (event) => this._onEscape(event), { signal });
2525
+ }
2229
2526
  _bind() {
2230
2527
  const input = this._input;
2231
2528
  const button = this._buttons[0]?.button;
@@ -2288,22 +2585,7 @@
2288
2585
  });
2289
2586
  }, { signal });
2290
2587
  calendar.addEventListener("dateloadend", () => void this.validate(), { signal });
2291
- this.ownerDocument.addEventListener("pointerdown", (event) => {
2292
- if (!this._open || event.composedPath().includes(this))
2293
- return;
2294
- this.hide(false);
2295
- }, { capture: true, signal });
2296
- this.ownerDocument.addEventListener("reset", (event) => {
2297
- const form = event.target;
2298
- if (form instanceof HTMLFormElement && this._input && form === this._input.form) {
2299
- this.hide(false);
2300
- queueMicrotask(() => {
2301
- if (!event.defaultPrevented)
2302
- this._restoreDefault();
2303
- });
2304
- }
2305
- }, { capture: true, signal });
2306
- this.addEventListener("keydown", (event) => this._onEscape(event), { signal });
2588
+ this._bindPopup(signal, this._input, () => this._restoreDefault());
2307
2589
  }
2308
2590
  _bindRange() {
2309
2591
  const calendar = this._calendar;
@@ -2331,7 +2613,15 @@
2331
2613
  signal
2332
2614
  });
2333
2615
  }
2334
- calendar.addEventListener("click", (event) => this._captureGridIntent(event), { capture: true, signal });
2616
+ calendar.addEventListener("click", (event) => {
2617
+ if (this._suppressGridClick) {
2618
+ this._suppressGridClick = false;
2619
+ event.preventDefault();
2620
+ event.stopImmediatePropagation();
2621
+ return;
2622
+ }
2623
+ this._captureGridIntent(event);
2624
+ }, { capture: true, signal });
2335
2625
  calendar.addEventListener("keydown", (event) => this._captureGridIntent(event), {
2336
2626
  capture: true,
2337
2627
  signal
@@ -2344,15 +2634,12 @@
2344
2634
  queueMicrotask(() => {
2345
2635
  if (custom.defaultPrevented || !this._connected || !this._range)
2346
2636
  return;
2347
- const pending = this._pendingIntents.get(date);
2348
- if (pending === undefined || pending !== this._rangeCommitId)
2637
+ const intent = this._pendingIntents.get(date);
2638
+ if (intent === undefined || intent.id !== this._rangeCommitId)
2349
2639
  return;
2350
2640
  this._pendingIntents.delete(date);
2351
- const result = this._range.activate(date, (which) => {
2352
- const index = which === "end" ? 1 : 0;
2353
- const field = this._fields?.[index];
2354
- return field != null && !field.input.disabled && !field.input.readOnly;
2355
- });
2641
+ const editable = (bound) => this._boundEditable(bound);
2642
+ const result = intent.endpoint ? this._range.moveEndpoint(date, intent.endpoint, editable) : this._range.activate(date, editable);
2356
2643
  if (result.status === "refused") {
2357
2644
  this._calendar?.dispatchEvent(new CustomEvent("dateinvalid", {
2358
2645
  detail: { date, state: this._calendar.getDateState(date) },
@@ -2360,30 +2647,25 @@
2360
2647
  }));
2361
2648
  return;
2362
2649
  }
2363
- this._setBound(result.endpoint, date, { emit: true, user: true });
2650
+ this._applyRangeTransition(result);
2651
+ if (intent.endpoint)
2652
+ return;
2364
2653
  if (result.close || result.status === "complete") {
2365
2654
  this.hide(false);
2366
2655
  this._focusField(result.endpoint);
2367
2656
  }
2368
2657
  });
2369
2658
  }, { signal });
2659
+ calendar.addEventListener("datefocus", (event) => this._onGridDateFocus(event), { signal });
2660
+ calendar.addEventListener("pointerover", (event) => this._onGridPointerOver(event), { signal });
2661
+ calendar.addEventListener("pointerleave", () => this._onGridPointerLeave(), { signal });
2662
+ calendar.addEventListener("pointerdown", (event) => this._onGridPointerDown(event), { signal });
2663
+ calendar.addEventListener("pointermove", (event) => this._onGridPointerMove(event), { signal });
2664
+ calendar.addEventListener("pointerup", (event) => this._onGridPointerUp(event), { signal });
2665
+ calendar.addEventListener("pointercancel", (event) => this._onGridPointerCancel(event), { signal });
2666
+ calendar.addEventListener("lostpointercapture", (event) => this._onGridPointerCancel(event), { signal });
2370
2667
  calendar.addEventListener("dateloadend", () => void this.validate(), { signal });
2371
- this.ownerDocument.addEventListener("pointerdown", (event) => {
2372
- if (!this._open || event.composedPath().includes(this))
2373
- return;
2374
- this.hide(false);
2375
- }, { capture: true, signal });
2376
- this.ownerDocument.addEventListener("reset", (event) => {
2377
- const form = event.target;
2378
- if (form instanceof HTMLFormElement && this._fields && form === this._fields[0].input.form) {
2379
- this.hide(false);
2380
- queueMicrotask(() => {
2381
- if (!event.defaultPrevented)
2382
- this._restoreRangeDefault();
2383
- });
2384
- }
2385
- }, { capture: true, signal });
2386
- this.addEventListener("keydown", (event) => this._onEscape(event), { signal });
2668
+ this._bindPopup(signal, this._fields?.[0]?.input, () => this._restoreRangeDefault());
2387
2669
  }
2388
2670
  _activateRangeEndpoint(endpoint) {
2389
2671
  const calendar = this._calendar;
@@ -2396,6 +2678,7 @@
2396
2678
  const target = this._boundCanonical(endpoint) || todayISO();
2397
2679
  calendar.display = monthKey(target);
2398
2680
  calendar.focusedDate = target;
2681
+ this._clearPreview();
2399
2682
  }
2400
2683
  _onRangeTriggerClick(endpoint, event) {
2401
2684
  if (!this._open) {
@@ -2478,7 +2761,190 @@
2478
2761
  if (!isDate(date))
2479
2762
  return;
2480
2763
  this._pendingIntents.clear();
2481
- this._pendingIntents.set(date, ++this._rangeCommitId);
2764
+ this._pendingIntents.set(date, { id: ++this._rangeCommitId, endpoint: "" });
2765
+ }
2766
+ _boundEditable(bound) {
2767
+ const field = this._fields?.[bound === "end" ? 1 : 0];
2768
+ return field != null && !field.input.disabled && !field.input.readOnly;
2769
+ }
2770
+ _applyRangeTransition(transition) {
2771
+ const fields = this._fields;
2772
+ if (!fields || !this._range || transition.status === "refused")
2773
+ return;
2774
+ const moved = [];
2775
+ for (const which of transition.changedEndpoints) {
2776
+ const field = fields[which === "end" ? 1 : 0];
2777
+ const next = which === "end" ? transition.range.end : transition.range.start;
2778
+ if (!field || field.canonical === next)
2779
+ continue;
2780
+ field.setCanonical(next, { format: true });
2781
+ moved.push(field);
2782
+ }
2783
+ this._syncHighlight();
2784
+ this._refreshRangeButtonLabel();
2785
+ this._revalidateRange(transition.endpoint);
2786
+ if (!moved.length)
2787
+ return;
2788
+ this._emitRangeChange();
2789
+ for (const field of moved) {
2790
+ field.input.dispatchEvent(new Event("input", { bubbles: true }));
2791
+ field.input.dispatchEvent(new Event("change", { bubbles: true }));
2792
+ }
2793
+ }
2794
+ _setPreview(date, endpoint = "") {
2795
+ const calendar = this._calendar;
2796
+ if (!calendar || !this._range || !this._open)
2797
+ return;
2798
+ const candidate = isDate(date) && !calendar.getDateState(date).disabled ? date : "";
2799
+ const preview = candidate ? this._range.previewRange(candidate, endpoint, (bound) => this._boundEditable(bound)) : null;
2800
+ if (!preview) {
2801
+ this._clearPreview();
2802
+ return;
2803
+ }
2804
+ this._previewDate = candidate;
2805
+ calendar.toggleAttribute("data-range-preview", true);
2806
+ calendar.highlightedRange = preview;
2807
+ }
2808
+ _clearPreview() {
2809
+ if (!this._previewDate)
2810
+ return;
2811
+ this._syncHighlight();
2812
+ }
2813
+ _onGridDateFocus(event) {
2814
+ if (this._drag)
2815
+ return;
2816
+ const date = event.detail?.date;
2817
+ if (isDate(date))
2818
+ this._setPreview(date);
2819
+ }
2820
+ _onGridPointerOver(event) {
2821
+ const pointer = event;
2822
+ if (this._drag || pointer.pointerType === "touch")
2823
+ return;
2824
+ this._setPreview(this._cellDate(pointer.target));
2825
+ }
2826
+ _onGridPointerLeave() {
2827
+ if (this._drag?.active === false)
2828
+ this._drag = null;
2829
+ if (this._drag)
2830
+ return;
2831
+ this._clearPreview();
2832
+ }
2833
+ _cellDate(target) {
2834
+ const cell = target instanceof Element ? target.closest(".dp-day[data-date]") : null;
2835
+ const date = cell?.getAttribute("data-date") || "";
2836
+ return isDate(date) ? date : "";
2837
+ }
2838
+ _dateAtPoint(x, y) {
2839
+ const element = this.ownerDocument.elementFromPoint(x, y);
2840
+ if (!(element instanceof Element) || !this._calendar?.contains(element))
2841
+ return "";
2842
+ return this._cellDate(element);
2843
+ }
2844
+ _onGridPointerDown(event) {
2845
+ const pointer = event;
2846
+ this._suppressGridClick = false;
2847
+ if (this._drag?.active === false)
2848
+ this._drag = null;
2849
+ if (!this._open || !this._range || this._drag)
2850
+ return;
2851
+ if (!pointer.isPrimary || pointer.button !== 0)
2852
+ return;
2853
+ if (pointer.pointerType !== "mouse" && pointer.pointerType !== "pen")
2854
+ return;
2855
+ if (!this._range.complete)
2856
+ return;
2857
+ const cell = pointer.target instanceof Element ? pointer.target.closest(".dp-day[data-date]") : null;
2858
+ if (!(cell instanceof HTMLElement))
2859
+ return;
2860
+ const isStart = cell.hasAttribute("data-range-start");
2861
+ const isEnd = cell.hasAttribute("data-range-end");
2862
+ if (isStart === isEnd)
2863
+ return;
2864
+ const endpoint = isStart ? "start" : "end";
2865
+ const date = this._cellDate(cell);
2866
+ if (!date || !this._boundEditable(endpoint))
2867
+ return;
2868
+ this._drag = {
2869
+ pointerId: pointer.pointerId,
2870
+ endpoint,
2871
+ x: pointer.clientX,
2872
+ y: pointer.clientY,
2873
+ date,
2874
+ active: false
2875
+ };
2876
+ }
2877
+ _onGridPointerMove(event) {
2878
+ const pointer = event;
2879
+ const drag = this._drag;
2880
+ if (!drag || pointer.pointerId !== drag.pointerId)
2881
+ return;
2882
+ if (!drag.active) {
2883
+ const travelled = Math.abs(pointer.clientX - drag.x) > DRAG_THRESHOLD || Math.abs(pointer.clientY - drag.y) > DRAG_THRESHOLD;
2884
+ if (!travelled)
2885
+ return;
2886
+ drag.active = true;
2887
+ try {
2888
+ this._calendar?.setPointerCapture(drag.pointerId);
2889
+ } catch {}
2890
+ this._syncDragAffordance();
2891
+ }
2892
+ const date = this._dateAtPoint(pointer.clientX, pointer.clientY);
2893
+ if (!date || this._calendar?.getDateState(date).disabled)
2894
+ return;
2895
+ drag.date = date;
2896
+ this._setPreview(date, drag.endpoint);
2897
+ }
2898
+ _onGridPointerUp(event) {
2899
+ const pointer = event;
2900
+ const drag = this._drag;
2901
+ if (!drag || pointer.pointerId !== drag.pointerId)
2902
+ return;
2903
+ this._drag = null;
2904
+ this._releaseDragCapture(drag.pointerId);
2905
+ if (!drag.active) {
2906
+ this._syncDragAffordance();
2907
+ return;
2908
+ }
2909
+ this._suppressGridClick = true;
2910
+ const transition = this._range?.projectEndpoint(drag.date, drag.endpoint, (bound) => this._boundEditable(bound));
2911
+ if (!transition || transition.status === "refused" || !transition.changedEndpoints.length) {
2912
+ this._syncHighlight();
2913
+ return;
2914
+ }
2915
+ const target = drag.endpoint === "start" ? transition.range.start : transition.range.end;
2916
+ this._pendingIntents.clear();
2917
+ this._pendingIntents.set(target, { id: ++this._rangeCommitId, endpoint: drag.endpoint });
2918
+ this._calendar?.activateDate(target);
2919
+ }
2920
+ _onGridPointerCancel(event) {
2921
+ const pointer = event;
2922
+ const drag = this._drag;
2923
+ if (!drag || pointer.pointerId !== drag.pointerId)
2924
+ return;
2925
+ this._drag = null;
2926
+ this._releaseDragCapture(drag.pointerId);
2927
+ this._syncHighlight();
2928
+ }
2929
+ _releaseDragCapture(pointerId) {
2930
+ const calendar = this._calendar;
2931
+ if (calendar?.hasPointerCapture(pointerId))
2932
+ calendar.releasePointerCapture(pointerId);
2933
+ }
2934
+ _syncDragAffordance() {
2935
+ const calendar = this._calendar;
2936
+ if (!calendar || !this._range)
2937
+ return;
2938
+ if (this._drag?.active) {
2939
+ calendar.setAttribute("data-range-drag", "active");
2940
+ return;
2941
+ }
2942
+ const { start, end } = this._range;
2943
+ const draggable = this._range.complete && start !== end && (this._boundEditable("start") || this._boundEditable("end"));
2944
+ if (draggable)
2945
+ calendar.setAttribute("data-range-drag", "ready");
2946
+ else
2947
+ calendar.removeAttribute("data-range-drag");
2482
2948
  }
2483
2949
  _onEscape(event) {
2484
2950
  if (event.key === "Escape" && this._open) {
@@ -2496,6 +2962,7 @@
2496
2962
  calendar.min = this.min;
2497
2963
  calendar.max = this.max;
2498
2964
  calendar.monthFormat = this.monthFormat;
2965
+ calendar.controlClasses = this._controlClasses;
2499
2966
  calendar.messages = this._messages;
2500
2967
  calendar.source = this._source;
2501
2968
  calendar.dateState = this._dateState;
@@ -2581,7 +3048,7 @@
2581
3048
  this._field?.setCanonical(this._value, { format: options.format !== false });
2582
3049
  if (this._calendar) {
2583
3050
  this._calendar.value = this._value;
2584
- if (this._value)
3051
+ if (this._value && options.moveDisplay !== false)
2585
3052
  this._calendar.focusedDate = this._value;
2586
3053
  }
2587
3054
  this._refreshButtonLabel();
@@ -2594,13 +3061,15 @@
2594
3061
  }
2595
3062
  }
2596
3063
  }
2597
- async _confirmDate(date) {
3064
+ async _confirmDate(date, options = {}) {
2598
3065
  const calendar = this._calendar;
2599
3066
  if (!calendar)
2600
3067
  return { ok: true };
2601
- const display = monthKey(date);
2602
- if (calendar.display !== display)
2603
- calendar.display = display;
3068
+ if (options.moveDisplay !== false) {
3069
+ const display = monthKey(date);
3070
+ if (calendar.display !== display)
3071
+ calendar.display = display;
3072
+ }
2604
3073
  const confirmed = await calendar.ensureDate(date);
2605
3074
  const state = calendar.getDateState(date);
2606
3075
  if (!confirmed || state.disabled)
@@ -2613,10 +3082,14 @@
2613
3082
  return false;
2614
3083
  if (!options.force && !field.isDirty)
2615
3084
  return false;
2616
- const result = await field.commit();
3085
+ const result = await field.commit(options);
2617
3086
  if (result.status === "stale" || result.status === "invalid")
2618
3087
  return false;
2619
- this._setValue(result.value || "", { emit, format: result.status === "ok" });
3088
+ this._setValue(result.value || "", {
3089
+ emit,
3090
+ format: result.status === "ok",
3091
+ moveDisplay: options.moveDisplay
3092
+ });
2620
3093
  return true;
2621
3094
  }
2622
3095
  async _commitFieldText(which) {
@@ -2691,19 +3164,21 @@
2691
3164
  return;
2692
3165
  this.dispatchEvent(new CustomEvent("rangechange", { detail: { ...this._range.range }, bubbles: true }));
2693
3166
  }
3167
+ _displayRange() {
3168
+ const { start, end } = this._range?.range ?? { start: "", end: "" };
3169
+ if (!start)
3170
+ return { start: "", end: "" };
3171
+ if (end && compareDates(end, start) < 0)
3172
+ return { start, end: "" };
3173
+ return { start, end };
3174
+ }
2694
3175
  _syncHighlight() {
2695
3176
  if (!this._calendar || !this._range)
2696
3177
  return;
2697
- const { start, end } = this._range;
2698
- if (!start) {
2699
- this._calendar.highlightedRange = { start: "", end: "" };
2700
- return;
2701
- }
2702
- if (end && compareDates(end, start) < 0) {
2703
- this._calendar.highlightedRange = { start, end: "" };
2704
- } else {
2705
- this._calendar.highlightedRange = { start, end };
2706
- }
3178
+ this._previewDate = "";
3179
+ this._calendar.removeAttribute("data-range-preview");
3180
+ this._calendar.highlightedRange = this._displayRange();
3181
+ this._syncDragAffordance();
2707
3182
  }
2708
3183
  _revalidateRange(which) {
2709
3184
  if (!this._fields || !this._range)
@@ -2825,7 +3300,7 @@
2825
3300
  if (!fields)
2826
3301
  return true;
2827
3302
  for (const [index, field] of fields.entries()) {
2828
- const result = await field.commit();
3303
+ const result = await field.commit({ moveDisplay: false });
2829
3304
  if (result.status === "ok" || result.status === "clear") {
2830
3305
  const which = index === 1 ? "end" : "start";
2831
3306
  this._setBound(which, result.value || "", { format: result.status === "ok" });
@@ -2841,7 +3316,7 @@
2841
3316
  if (!input.value.trim()) {
2842
3317
  input.setCustomValidity("");
2843
3318
  } else {
2844
- await this._commitText(false, { force: true });
3319
+ await this._commitText(false, { force: true, moveDisplay: false });
2845
3320
  }
2846
3321
  this._revalidateTimeOrder("start");
2847
3322
  this._revalidateTimeOrder("end");
@@ -2904,6 +3379,12 @@
2904
3379
  this._setExpanded(false);
2905
3380
  this._rangeCommitId++;
2906
3381
  this._pendingIntents.clear();
3382
+ this._suppressGridClick = false;
3383
+ if (this._drag) {
3384
+ this._releaseDragCapture(this._drag.pointerId);
3385
+ this._drag = null;
3386
+ }
3387
+ this._syncHighlight();
2907
3388
  if (restoreFocus) {
2908
3389
  if (this._rangeMode()) {
2909
3390
  const endpoint = this._restoreFocusEndpoint();