@lekoala/date-picker 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- /*** @lekoala/date-picker v0.3.0 - https://github.com/lekoala/date-picker ***/
1
+ /*** @lekoala/date-picker v0.4.0 - https://github.com/lekoala/date-picker ***/
2
2
  (() => {
3
3
  var __defProp = Object.defineProperty;
4
4
  var __getOwnPropNames = Object.getOwnPropertyNames;
@@ -176,15 +176,46 @@
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
+ break;
184
186
  cursor = addDays(cursor, 7);
185
187
  }
186
188
  return weeks;
187
189
  }
190
+ function dayOrdinal(year, month, day) {
191
+ const base = toUTCDate({ year: 1, month: 1, day: 1 });
192
+ return Math.round((toUTCDate({ year, month, day }).getTime() - base.getTime()) / 86400000);
193
+ }
194
+ function clampDisplayMonth(value, firstDay = 1) {
195
+ const normalized = normalizeFirstDay(firstDay);
196
+ const maxOrdinal = dayOrdinal(9999, 12, 31);
197
+ let key = monthKey(value);
198
+ for (;; ) {
199
+ const parsed = parseDate(`${key}-01`);
200
+ if (!parsed)
201
+ throw new TypeError(`Invalid civil date/month: ${value}`);
202
+ const startOrdinal = dayOrdinal(parsed.year, parsed.month, 1);
203
+ const startOffset = (toUTCDate(parsed).getUTCDay() - normalized + 7) % 7;
204
+ if (startOrdinal - startOffset < 0) {
205
+ key = shiftMonth(key, 1);
206
+ continue;
207
+ }
208
+ const lastDay = daysInMonth(parsed.year, parsed.month);
209
+ const endParts = { year: parsed.year, month: parsed.month, day: lastDay };
210
+ const endOrdinal = startOrdinal + lastDay - 1;
211
+ const endOffset = (toUTCDate(endParts).getUTCDay() - normalized + 7) % 7;
212
+ if (endOrdinal - endOffset + 6 > maxOrdinal) {
213
+ key = shiftMonth(key, -1);
214
+ continue;
215
+ }
216
+ return key;
217
+ }
218
+ }
188
219
  function clampDate(value, min = "", max = "") {
189
220
  if (!isDate(value))
190
221
  throw new TypeError(`Invalid civil date: ${value}`);
@@ -312,36 +343,140 @@
312
343
  focus(endpoint) {
313
344
  this.activeEndpoint = endpoint;
314
345
  }
315
- activate(date, editable = () => true) {
346
+ _refuse(endpoint) {
347
+ return {
348
+ status: "refused",
349
+ endpoint,
350
+ changedEndpoints: [],
351
+ range: this.range,
352
+ activeEndpoint: this.activeEndpoint
353
+ };
354
+ }
355
+ project(date, editable = () => true) {
316
356
  if (!isDate(date))
317
357
  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";
358
+ const { start, end } = this;
359
+ if (this.complete && (compareDates(date, start) < 0 || compareDates(date, end) > 0)) {
360
+ const endpoint = compareDates(date, start) < 0 ? "start" : "end";
320
361
  if (!editable(endpoint))
321
- return { status: "refused", endpoint };
322
- this[endpoint] = date;
323
- this.activeEndpoint = endpoint;
324
- return { status: "complete", endpoint };
362
+ return this._refuse(endpoint);
363
+ return {
364
+ status: "complete",
365
+ endpoint,
366
+ changedEndpoints: [endpoint],
367
+ range: endpoint === "start" ? { start: date, end } : { start, end: date },
368
+ activeEndpoint: endpoint
369
+ };
325
370
  }
326
371
  if (this.activeEndpoint === "end") {
327
- if (this.start && compareDates(date, this.start) < 0) {
328
- return { status: "refused", endpoint: "end" };
372
+ if (start && !end && compareDates(date, start) < 0) {
373
+ if (!editable("start"))
374
+ return this._refuse("start");
375
+ if (!editable("end"))
376
+ return this._refuse("end");
377
+ return {
378
+ status: "complete",
379
+ endpoint: "end",
380
+ changedEndpoints: ["start", "end"],
381
+ range: { start: date, end: start },
382
+ activeEndpoint: "end"
383
+ };
329
384
  }
385
+ if (start && compareDates(date, start) < 0)
386
+ return this._refuse("end");
330
387
  if (!editable("end"))
331
- return { status: "refused", endpoint: "end" };
332
- this.end = date;
333
- return { status: this.start ? "complete" : "pending", endpoint: "end" };
388
+ return this._refuse("end");
389
+ return {
390
+ status: start ? "complete" : "pending",
391
+ endpoint: "end",
392
+ changedEndpoints: ["end"],
393
+ range: { start, end: date },
394
+ activeEndpoint: "end"
395
+ };
334
396
  }
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 };
397
+ if (end && !start && compareDates(date, end) > 0) {
398
+ if (!editable("start"))
399
+ return this._refuse("start");
400
+ if (!editable("end"))
401
+ return this._refuse("end");
402
+ return {
403
+ status: "complete",
404
+ endpoint: "start",
405
+ changedEndpoints: ["start", "end"],
406
+ range: { start: end, end: date },
407
+ activeEndpoint: "start"
408
+ };
340
409
  }
341
- this.activeEndpoint = "end";
342
- return { status: "pending", endpoint: "start" };
410
+ if (!editable("start"))
411
+ return this._refuse("start");
412
+ const canContinue = editable("end");
413
+ return {
414
+ status: "pending",
415
+ endpoint: "start",
416
+ changedEndpoints: ["start"],
417
+ range: { start: date, end },
418
+ activeEndpoint: canContinue ? "end" : this.activeEndpoint,
419
+ ...canContinue ? {} : { close: true }
420
+ };
421
+ }
422
+ projectEndpoint(date, endpoint, editable = () => true) {
423
+ if (!isDate(date))
424
+ throw new TypeError(`Invalid range activation: ${date}`);
425
+ if (!this.complete || !editable(endpoint))
426
+ return this._refuse(endpoint);
427
+ const { start, end } = this;
428
+ const previous = endpoint === "start" ? start : end;
429
+ const target = endpoint === "start" ? earlier(date, end) : later(date, start);
430
+ return {
431
+ status: "complete",
432
+ endpoint,
433
+ changedEndpoints: target === previous ? [] : [endpoint],
434
+ range: endpoint === "start" ? { start: target, end } : { start, end: target },
435
+ activeEndpoint: endpoint
436
+ };
437
+ }
438
+ previewRange(date, endpoint = "", editable = () => true) {
439
+ if (!isDate(date))
440
+ return null;
441
+ if (!endpoint && Boolean(this.start) === Boolean(this.end))
442
+ return null;
443
+ const transition = endpoint ? this.projectEndpoint(date, endpoint, editable) : this.project(date, editable);
444
+ if (transition.status === "refused")
445
+ return null;
446
+ const { start, end } = transition.range;
447
+ if (!start || end && compareDates(end, start) < 0)
448
+ return null;
449
+ return { start, end };
450
+ }
451
+ _apply(transition) {
452
+ if (transition.status === "refused")
453
+ return transition;
454
+ this.start = transition.range.start;
455
+ this.end = transition.range.end;
456
+ this.activeEndpoint = transition.activeEndpoint;
457
+ return transition;
458
+ }
459
+ activate(date, editable = () => true) {
460
+ return this._apply(this.project(date, editable));
461
+ }
462
+ moveEndpoint(date, endpoint, editable = () => true) {
463
+ return this._apply(this.projectEndpoint(date, endpoint, editable));
343
464
  }
344
465
  }
466
+ function later(a, b) {
467
+ if (!a)
468
+ return b;
469
+ if (!b)
470
+ return a;
471
+ return compareDates(a, b) >= 0 ? a : b;
472
+ }
473
+ function earlier(a, b) {
474
+ if (!a)
475
+ return b;
476
+ if (!b)
477
+ return a;
478
+ return compareDates(a, b) <= 0 ? a : b;
479
+ }
345
480
 
346
481
  // src/intl.js
347
482
  var BIDI = /[\u200e\u200f\u061c\u202a-\u202e\u2066-\u2069]/g;
@@ -361,8 +496,10 @@
361
496
  map.set(formatter.format(i), String(i));
362
497
  return map;
363
498
  }
364
- function normalizeDigits(text, locale) {
365
- let result = String(text || "").replace(BIDI, "").trim();
499
+ function normalizeDigits(text, locale, options = {}) {
500
+ let result = String(text || "").replace(BIDI, "");
501
+ if (options.trim !== false)
502
+ result = result.trim();
366
503
  for (const [local, ascii] of digitMap(locale))
367
504
  result = result.split(local).join(ascii);
368
505
  return result;
@@ -462,8 +599,8 @@
462
599
  return "(\\d{1,2})";
463
600
  if (part.type === "year")
464
601
  return "(\\d{4})";
465
- const literal = normalizeDigits(part.value, resolved).replace(/\s+/g, " ");
466
- return escapeRegExp(literal).replace(/\\ /g, "\\s*");
602
+ const literal = normalizeDigits(part.value, resolved, { trim: false });
603
+ return escapeRegExp(literal).replace(/\s+/g, "\\s*");
467
604
  }).join("");
468
605
  const regex = new RegExp(`^\\s*${pattern}\\s*$`);
469
606
  return {
@@ -557,19 +694,24 @@
557
694
  function yearOf(month) {
558
695
  return Number(month.slice(0, 4));
559
696
  }
697
+ function stepMonthKey(month, amount) {
698
+ const [year, index] = month.split("-").map(Number);
699
+ const absolute = year * 12 + (index - 1) + amount;
700
+ const stepYear = Math.floor(absolute / 12);
701
+ const stepMonth = (absolute % 12 + 12) % 12 + 1;
702
+ if (stepYear < 1 || stepYear > 9999)
703
+ return null;
704
+ return `${String(stepYear).padStart(4, "0")}-${String(stepMonth).padStart(2, "0")}`;
705
+ }
706
+ function rangeAttributeMap(position) {
707
+ return {
708
+ "data-range-start": position === "start" || position === "single",
709
+ "data-range-end": position === "end" || position === "single",
710
+ "data-in-range": position === "in"
711
+ };
712
+ }
560
713
  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
- }
714
+ return Object.entries(rangeAttributeMap(position)).filter(([, on]) => on).map(([name]) => ` ${name}="true"`).join("");
573
715
  }
574
716
  function rangeLabelKey(position) {
575
717
  if (position === "single")
@@ -612,6 +754,8 @@
612
754
  this._renderDay = null;
613
755
  this._isDateDisabled = null;
614
756
  this._highlightedRange = { start: "", end: "" };
757
+ this._paintedRange = { start: "", end: "" };
758
+ this._paintedPreview = false;
615
759
  this._messages = getDefaultMessages();
616
760
  this._onClick = this._onClick.bind(this);
617
761
  this._onChange = this._onChange.bind(this);
@@ -730,11 +874,12 @@
730
874
  }
731
875
  set highlightedRange(value) {
732
876
  const next = normalizeRange(value);
733
- if (next.start === this._highlightedRange.start && next.end === this._highlightedRange.end)
877
+ const preview = this.hasAttribute("data-range-preview");
878
+ if (next.start === this._paintedRange.start && next.end === this._paintedRange.end && preview === this._paintedPreview)
734
879
  return;
735
880
  this._highlightedRange = next;
736
881
  if (this._connected)
737
- this.render();
882
+ this._updateRangeDom();
738
883
  }
739
884
  get fixedWeeks() {
740
885
  return this.hasAttribute("fixed-weeks");
@@ -796,7 +941,7 @@
796
941
  const attrDisplay = this.getAttribute("display") || "";
797
942
  if (attrDisplay) {
798
943
  try {
799
- this._model.setDisplay(attrDisplay);
944
+ this._model.setDisplay(clampDisplayMonth(attrDisplay, firstDay));
800
945
  } catch {}
801
946
  } else if (this._model.value) {
802
947
  this._model.display = monthKey(this._model.value);
@@ -817,10 +962,18 @@
817
962
  this._rendering = false;
818
963
  }
819
964
  _clamp(value) {
820
- return clampDate(value, this.min, this.max);
965
+ const bounded = clampDate(value, this.min, this.max);
966
+ const month = monthKey(bounded);
967
+ const safeMonth = clampDisplayMonth(month, this.firstDay);
968
+ if (safeMonth === month)
969
+ return bounded;
970
+ const day = Number(bounded.slice(8, 10)) || 1;
971
+ const candidate = addDays(`${safeMonth}-01`, day - 1);
972
+ const last = endOfMonth(safeMonth);
973
+ return compareDates(candidate, last) > 0 ? last : candidate;
821
974
  }
822
975
  _weeks(display = this.display) {
823
- const weeks = getMonthWeeks(display, { firstDay: this.firstDay });
976
+ const weeks = getMonthWeeks(clampDisplayMonth(display, this.firstDay), { firstDay: this.firstDay });
824
977
  if (!this.fixedWeeks || weeks.length >= 6)
825
978
  return weeks;
826
979
  let cursor = weeks[weeks.length - 1]?.[6] || "";
@@ -934,7 +1087,7 @@
934
1087
  }
935
1088
  _setDisplay(display, options = {}) {
936
1089
  const previous = this._model.display;
937
- this._model.setDisplay(display);
1090
+ this._model.setDisplay(clampDisplayMonth(display, this.firstDay));
938
1091
  this._model.focused = this._clamp(this._model.focused);
939
1092
  this._model.display = monthKey(this._model.focused);
940
1093
  this._reflect("display", this._model.display);
@@ -952,10 +1105,10 @@
952
1105
  }));
953
1106
  }
954
1107
  previousMonth() {
955
- return this._setDisplay(shiftMonth(this.display, -1));
1108
+ return this._setDisplay(stepMonthKey(this.display, -1) ?? this.display);
956
1109
  }
957
1110
  nextMonth() {
958
- return this._setDisplay(shiftMonth(this.display, 1));
1111
+ return this._setDisplay(stepMonthKey(this.display, 1) ?? this.display);
959
1112
  }
960
1113
  focusDate(date, options = {}) {
961
1114
  if (!isDate(date))
@@ -1007,12 +1160,39 @@
1007
1160
  }
1008
1161
  return false;
1009
1162
  }
1163
+ _dayLabel(date, state, position) {
1164
+ const rangeLabel = this.hasAttribute("data-range-preview") ? "" : rangeLabelKey(position);
1165
+ const description = typeof state.description === "string" ? state.description : "";
1166
+ return [
1167
+ formatLongDate(date, this.locale),
1168
+ description,
1169
+ rangeLabel ? this._messages[rangeLabel] : "",
1170
+ state.disabled ? this._messages.unavailable : ""
1171
+ ].filter(Boolean).join(". ");
1172
+ }
1173
+ _updateRangeDom() {
1174
+ this._paintedRange = { ...this._highlightedRange };
1175
+ this._paintedPreview = this.hasAttribute("data-range-preview");
1176
+ for (const cell of this.querySelectorAll(".dp-day[data-date]")) {
1177
+ if (!(cell instanceof HTMLTableCellElement))
1178
+ continue;
1179
+ const date = cell.dataset.date || "";
1180
+ const position = rangePosition(date, this._highlightedRange);
1181
+ for (const [name, on] of Object.entries(rangeAttributeMap(position))) {
1182
+ if (on)
1183
+ cell.setAttribute(name, "true");
1184
+ else
1185
+ cell.removeAttribute(name);
1186
+ }
1187
+ cell.setAttribute("aria-label", this._dayLabel(date, this.getDateState(date), position));
1188
+ }
1189
+ }
1010
1190
  focusGrid() {
1011
1191
  const target = this.querySelector(`.dp-day[data-date="${CSS.escape(this.focusedDate)}"]`);
1012
1192
  if (target instanceof HTMLElement)
1013
1193
  target.focus();
1014
1194
  }
1015
- async _activate(date) {
1195
+ async activateDate(date) {
1016
1196
  const confirmed = await this.ensureDate(date);
1017
1197
  const state = this.getDateState(date);
1018
1198
  if (!confirmed || state.disabled) {
@@ -1068,7 +1248,7 @@
1068
1248
  this._reflect("display", this._model.display);
1069
1249
  if (previousDisplay !== this.display)
1070
1250
  this._emitDisplayChange();
1071
- this._activate(date);
1251
+ this.activateDate(date);
1072
1252
  }
1073
1253
  _onChange(event) {
1074
1254
  const target = event.target;
@@ -1078,7 +1258,7 @@
1078
1258
  }
1079
1259
  if (target instanceof HTMLInputElement && target.matches(".dp-year-input")) {
1080
1260
  const year = Number(target.value);
1081
- if (!Number.isInteger(year) || year < 1 || year > 9999) {
1261
+ if (!Number.isInteger(year) || year < 1 || year > 9998) {
1082
1262
  if (this._connected)
1083
1263
  this.render(false);
1084
1264
  return;
@@ -1091,8 +1271,10 @@
1091
1271
  if (!(target instanceof HTMLTableCellElement) || !target.matches(".dp-day[data-date]"))
1092
1272
  return;
1093
1273
  const date = target.dataset.date || "";
1094
- if (isDate(date))
1095
- this._model.focused = date;
1274
+ if (!isDate(date))
1275
+ return;
1276
+ this._model.focused = date;
1277
+ this.dispatchEvent(new CustomEvent("datefocus", { detail: { date, state: this.getDateState(date) }, bubbles: true }));
1096
1278
  }
1097
1279
  _onKeyDown(event) {
1098
1280
  const target = event.target;
@@ -1101,7 +1283,7 @@
1101
1283
  const date = target.dataset.date || this.focusedDate;
1102
1284
  if (event.key === "Enter" || event.key === " ") {
1103
1285
  event.preventDefault();
1104
- this._activate(date);
1286
+ this.activateDate(date);
1105
1287
  return;
1106
1288
  }
1107
1289
  const anchorFocused = this._model.focused;
@@ -1150,8 +1332,10 @@
1150
1332
  const gridId = `${this._id}-grid`;
1151
1333
  const minMonth = this.min ? monthKey(this.min) : "";
1152
1334
  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);
1335
+ const prevKey = stepMonthKey(display, -1);
1336
+ const nextKey = stepMonthKey(display, 1);
1337
+ const prevDisabled = Boolean(minMonth && prevKey !== null && prevKey < minMonth);
1338
+ const nextDisabled = Boolean(maxMonth && nextKey !== null && nextKey > maxMonth);
1155
1339
  const minYearAttr = this.min ? ` min="${Number(this.min.slice(0, 4))}"` : "";
1156
1340
  const maxYearAttr = this.max ? ` max="${Number(this.max.slice(0, 4))}"` : "";
1157
1341
  const showWeeks = this.showWeekNumbers && this.firstDay === 1;
@@ -1172,14 +1356,7 @@
1172
1356
  const focused = this.focusedDate === date;
1173
1357
  const isToday = date === today;
1174
1358
  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(". ");
1359
+ const label = this._dayLabel(date, state, position);
1183
1360
  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
1361
  }).join("");
1185
1362
  return `<tr>${weekNumber}${cells}</tr>`;
@@ -1198,8 +1375,8 @@
1198
1375
  <div class="dp-calendar-header">
1199
1376
  ${yearFirst ? yearControl : monthControl}
1200
1377
  ${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>
1378
+ <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>
1379
+ <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>
1203
1380
  </div>
1204
1381
  <h2 id="${headingId}" class="dp-calendar-heading dp-visually-hidden" aria-live="polite">${escapeHtml(formatMonthYear(`${display}-15`, locale))}</h2>
1205
1382
  <table id="${gridId}" class="dp-grid" role="grid" aria-labelledby="${headingId}">
@@ -1208,6 +1385,8 @@
1208
1385
  </table>
1209
1386
  </div>`;
1210
1387
  this._rendering = false;
1388
+ this._paintedRange = { ...this._highlightedRange };
1389
+ this._paintedPreview = this.hasAttribute("data-range-preview");
1211
1390
  if (focusKey) {
1212
1391
  queueMicrotask(() => {
1213
1392
  let selector = focusKey;
@@ -1686,7 +1865,7 @@
1686
1865
  }
1687
1866
  input.setCustomValidity(text === canonical ? "" : this.messages.invalidDate);
1688
1867
  }
1689
- async commit() {
1868
+ async commit(options = {}) {
1690
1869
  const input = this.input;
1691
1870
  const commitId = ++this._commitId;
1692
1871
  const text = input.value.trim();
@@ -1706,7 +1885,7 @@
1706
1885
  return { status: "invalid" };
1707
1886
  }
1708
1887
  if (this.confirm) {
1709
- const result = await this.confirm(parsed);
1888
+ const result = await this.confirm(parsed, options);
1710
1889
  if (commitId !== this._commitId)
1711
1890
  return { status: "stale" };
1712
1891
  if (!result?.ok) {
@@ -1778,6 +1957,7 @@
1778
1957
 
1779
1958
  // src/date-picker.js
1780
1959
  var uid2 = 0;
1960
+ var DRAG_THRESHOLD = 4;
1781
1961
 
1782
1962
  class DatePickerElement extends HTMLElement {
1783
1963
  static observedAttributes = ["value", "locale", "min", "max", "open-on-focus", "month-format"];
@@ -1797,6 +1977,9 @@
1797
1977
  this._lastFocusEndpoint = "";
1798
1978
  this._rangeCommitId = 0;
1799
1979
  this._pendingIntents = new Map;
1980
+ this._previewDate = "";
1981
+ this._drag = null;
1982
+ this._suppressGridClick = false;
1800
1983
  this._buttons = [];
1801
1984
  this._returnFocus = null;
1802
1985
  this._panel = null;
@@ -1839,7 +2022,7 @@
1839
2022
  this._discoverTimeFields();
1840
2023
  this._field = new DateFieldController(input, { messages: this._messages, locale: this.locale });
1841
2024
  this._field.onAttributesChanged = () => this._syncInputState();
1842
- this._field.confirm = (date) => this._confirmDate(date);
2025
+ this._field.confirm = (date, options) => this._confirmDate(date, options);
1843
2026
  this._setupFormValue();
1844
2027
  this._build();
1845
2028
  this._syncInputState();
@@ -1903,7 +2086,7 @@
1903
2086
  ];
1904
2087
  for (const field of this._fields) {
1905
2088
  field.onAttributesChanged = () => this._syncRangeFields();
1906
- field.confirm = (date) => this._confirmDate(date);
2089
+ field.confirm = (date, options) => this._confirmDate(date, options);
1907
2090
  field.setupFormValue();
1908
2091
  }
1909
2092
  this._build();
@@ -2148,6 +2331,7 @@
2148
2331
  const field = this._fields[index];
2149
2332
  button.disabled = Boolean(field?.input.disabled || field?.input.readOnly);
2150
2333
  }
2334
+ this._syncDragAffordance();
2151
2335
  const active = this._range?.activeEndpoint === "end" ? 1 : 0;
2152
2336
  if (this._open && (this._fields[active]?.input.disabled || this._fields[active]?.input.readOnly)) {
2153
2337
  this.hide(false);
@@ -2331,7 +2515,15 @@
2331
2515
  signal
2332
2516
  });
2333
2517
  }
2334
- calendar.addEventListener("click", (event) => this._captureGridIntent(event), { capture: true, signal });
2518
+ calendar.addEventListener("click", (event) => {
2519
+ if (this._suppressGridClick) {
2520
+ this._suppressGridClick = false;
2521
+ event.preventDefault();
2522
+ event.stopImmediatePropagation();
2523
+ return;
2524
+ }
2525
+ this._captureGridIntent(event);
2526
+ }, { capture: true, signal });
2335
2527
  calendar.addEventListener("keydown", (event) => this._captureGridIntent(event), {
2336
2528
  capture: true,
2337
2529
  signal
@@ -2344,15 +2536,12 @@
2344
2536
  queueMicrotask(() => {
2345
2537
  if (custom.defaultPrevented || !this._connected || !this._range)
2346
2538
  return;
2347
- const pending = this._pendingIntents.get(date);
2348
- if (pending === undefined || pending !== this._rangeCommitId)
2539
+ const intent = this._pendingIntents.get(date);
2540
+ if (intent === undefined || intent.id !== this._rangeCommitId)
2349
2541
  return;
2350
2542
  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
- });
2543
+ const editable = (bound) => this._boundEditable(bound);
2544
+ const result = intent.endpoint ? this._range.moveEndpoint(date, intent.endpoint, editable) : this._range.activate(date, editable);
2356
2545
  if (result.status === "refused") {
2357
2546
  this._calendar?.dispatchEvent(new CustomEvent("dateinvalid", {
2358
2547
  detail: { date, state: this._calendar.getDateState(date) },
@@ -2360,13 +2549,23 @@
2360
2549
  }));
2361
2550
  return;
2362
2551
  }
2363
- this._setBound(result.endpoint, date, { emit: true, user: true });
2552
+ this._applyRangeTransition(result);
2553
+ if (intent.endpoint)
2554
+ return;
2364
2555
  if (result.close || result.status === "complete") {
2365
2556
  this.hide(false);
2366
2557
  this._focusField(result.endpoint);
2367
2558
  }
2368
2559
  });
2369
2560
  }, { signal });
2561
+ calendar.addEventListener("datefocus", (event) => this._onGridDateFocus(event), { signal });
2562
+ calendar.addEventListener("pointerover", (event) => this._onGridPointerOver(event), { signal });
2563
+ calendar.addEventListener("pointerleave", () => this._onGridPointerLeave(), { signal });
2564
+ calendar.addEventListener("pointerdown", (event) => this._onGridPointerDown(event), { signal });
2565
+ calendar.addEventListener("pointermove", (event) => this._onGridPointerMove(event), { signal });
2566
+ calendar.addEventListener("pointerup", (event) => this._onGridPointerUp(event), { signal });
2567
+ calendar.addEventListener("pointercancel", (event) => this._onGridPointerCancel(event), { signal });
2568
+ calendar.addEventListener("lostpointercapture", (event) => this._onGridPointerCancel(event), { signal });
2370
2569
  calendar.addEventListener("dateloadend", () => void this.validate(), { signal });
2371
2570
  this.ownerDocument.addEventListener("pointerdown", (event) => {
2372
2571
  if (!this._open || event.composedPath().includes(this))
@@ -2396,6 +2595,7 @@
2396
2595
  const target = this._boundCanonical(endpoint) || todayISO();
2397
2596
  calendar.display = monthKey(target);
2398
2597
  calendar.focusedDate = target;
2598
+ this._clearPreview();
2399
2599
  }
2400
2600
  _onRangeTriggerClick(endpoint, event) {
2401
2601
  if (!this._open) {
@@ -2478,7 +2678,190 @@
2478
2678
  if (!isDate(date))
2479
2679
  return;
2480
2680
  this._pendingIntents.clear();
2481
- this._pendingIntents.set(date, ++this._rangeCommitId);
2681
+ this._pendingIntents.set(date, { id: ++this._rangeCommitId, endpoint: "" });
2682
+ }
2683
+ _boundEditable(bound) {
2684
+ const field = this._fields?.[bound === "end" ? 1 : 0];
2685
+ return field != null && !field.input.disabled && !field.input.readOnly;
2686
+ }
2687
+ _applyRangeTransition(transition) {
2688
+ const fields = this._fields;
2689
+ if (!fields || !this._range || transition.status === "refused")
2690
+ return;
2691
+ const moved = [];
2692
+ for (const which of transition.changedEndpoints) {
2693
+ const field = fields[which === "end" ? 1 : 0];
2694
+ const next = which === "end" ? transition.range.end : transition.range.start;
2695
+ if (!field || field.canonical === next)
2696
+ continue;
2697
+ field.setCanonical(next, { format: true });
2698
+ moved.push(field);
2699
+ }
2700
+ this._syncHighlight();
2701
+ this._refreshRangeButtonLabel();
2702
+ this._revalidateRange(transition.endpoint);
2703
+ if (!moved.length)
2704
+ return;
2705
+ this._emitRangeChange();
2706
+ for (const field of moved) {
2707
+ field.input.dispatchEvent(new Event("input", { bubbles: true }));
2708
+ field.input.dispatchEvent(new Event("change", { bubbles: true }));
2709
+ }
2710
+ }
2711
+ _setPreview(date, endpoint = "") {
2712
+ const calendar = this._calendar;
2713
+ if (!calendar || !this._range || !this._open)
2714
+ return;
2715
+ const candidate = isDate(date) && !calendar.getDateState(date).disabled ? date : "";
2716
+ const preview = candidate ? this._range.previewRange(candidate, endpoint, (bound) => this._boundEditable(bound)) : null;
2717
+ if (!preview) {
2718
+ this._clearPreview();
2719
+ return;
2720
+ }
2721
+ this._previewDate = candidate;
2722
+ calendar.toggleAttribute("data-range-preview", true);
2723
+ calendar.highlightedRange = preview;
2724
+ }
2725
+ _clearPreview() {
2726
+ if (!this._previewDate)
2727
+ return;
2728
+ this._syncHighlight();
2729
+ }
2730
+ _onGridDateFocus(event) {
2731
+ if (this._drag)
2732
+ return;
2733
+ const date = event.detail?.date;
2734
+ if (isDate(date))
2735
+ this._setPreview(date);
2736
+ }
2737
+ _onGridPointerOver(event) {
2738
+ const pointer = event;
2739
+ if (this._drag || pointer.pointerType === "touch")
2740
+ return;
2741
+ this._setPreview(this._cellDate(pointer.target));
2742
+ }
2743
+ _onGridPointerLeave() {
2744
+ if (this._drag?.active === false)
2745
+ this._drag = null;
2746
+ if (this._drag)
2747
+ return;
2748
+ this._clearPreview();
2749
+ }
2750
+ _cellDate(target) {
2751
+ const cell = target instanceof Element ? target.closest(".dp-day[data-date]") : null;
2752
+ const date = cell?.getAttribute("data-date") || "";
2753
+ return isDate(date) ? date : "";
2754
+ }
2755
+ _dateAtPoint(x, y) {
2756
+ const element = this.ownerDocument.elementFromPoint(x, y);
2757
+ if (!(element instanceof Element) || !this._calendar?.contains(element))
2758
+ return "";
2759
+ return this._cellDate(element);
2760
+ }
2761
+ _onGridPointerDown(event) {
2762
+ const pointer = event;
2763
+ this._suppressGridClick = false;
2764
+ if (this._drag?.active === false)
2765
+ this._drag = null;
2766
+ if (!this._open || !this._range || this._drag)
2767
+ return;
2768
+ if (!pointer.isPrimary || pointer.button !== 0)
2769
+ return;
2770
+ if (pointer.pointerType !== "mouse" && pointer.pointerType !== "pen")
2771
+ return;
2772
+ if (!this._range.complete)
2773
+ return;
2774
+ const cell = pointer.target instanceof Element ? pointer.target.closest(".dp-day[data-date]") : null;
2775
+ if (!(cell instanceof HTMLElement))
2776
+ return;
2777
+ const isStart = cell.hasAttribute("data-range-start");
2778
+ const isEnd = cell.hasAttribute("data-range-end");
2779
+ if (isStart === isEnd)
2780
+ return;
2781
+ const endpoint = isStart ? "start" : "end";
2782
+ const date = this._cellDate(cell);
2783
+ if (!date || !this._boundEditable(endpoint))
2784
+ return;
2785
+ this._drag = {
2786
+ pointerId: pointer.pointerId,
2787
+ endpoint,
2788
+ x: pointer.clientX,
2789
+ y: pointer.clientY,
2790
+ date,
2791
+ active: false
2792
+ };
2793
+ }
2794
+ _onGridPointerMove(event) {
2795
+ const pointer = event;
2796
+ const drag = this._drag;
2797
+ if (!drag || pointer.pointerId !== drag.pointerId)
2798
+ return;
2799
+ if (!drag.active) {
2800
+ const travelled = Math.abs(pointer.clientX - drag.x) > DRAG_THRESHOLD || Math.abs(pointer.clientY - drag.y) > DRAG_THRESHOLD;
2801
+ if (!travelled)
2802
+ return;
2803
+ drag.active = true;
2804
+ try {
2805
+ this._calendar?.setPointerCapture(drag.pointerId);
2806
+ } catch {}
2807
+ this._syncDragAffordance();
2808
+ }
2809
+ const date = this._dateAtPoint(pointer.clientX, pointer.clientY);
2810
+ if (!date || this._calendar?.getDateState(date).disabled)
2811
+ return;
2812
+ drag.date = date;
2813
+ this._setPreview(date, drag.endpoint);
2814
+ }
2815
+ _onGridPointerUp(event) {
2816
+ const pointer = event;
2817
+ const drag = this._drag;
2818
+ if (!drag || pointer.pointerId !== drag.pointerId)
2819
+ return;
2820
+ this._drag = null;
2821
+ this._releaseDragCapture(drag.pointerId);
2822
+ if (!drag.active) {
2823
+ this._syncDragAffordance();
2824
+ return;
2825
+ }
2826
+ this._suppressGridClick = true;
2827
+ const transition = this._range?.projectEndpoint(drag.date, drag.endpoint, (bound) => this._boundEditable(bound));
2828
+ if (!transition || transition.status === "refused" || !transition.changedEndpoints.length) {
2829
+ this._syncHighlight();
2830
+ return;
2831
+ }
2832
+ const target = drag.endpoint === "start" ? transition.range.start : transition.range.end;
2833
+ this._pendingIntents.clear();
2834
+ this._pendingIntents.set(target, { id: ++this._rangeCommitId, endpoint: drag.endpoint });
2835
+ this._calendar?.activateDate(target);
2836
+ }
2837
+ _onGridPointerCancel(event) {
2838
+ const pointer = event;
2839
+ const drag = this._drag;
2840
+ if (!drag || pointer.pointerId !== drag.pointerId)
2841
+ return;
2842
+ this._drag = null;
2843
+ this._releaseDragCapture(drag.pointerId);
2844
+ this._syncHighlight();
2845
+ }
2846
+ _releaseDragCapture(pointerId) {
2847
+ const calendar = this._calendar;
2848
+ if (calendar?.hasPointerCapture(pointerId))
2849
+ calendar.releasePointerCapture(pointerId);
2850
+ }
2851
+ _syncDragAffordance() {
2852
+ const calendar = this._calendar;
2853
+ if (!calendar || !this._range)
2854
+ return;
2855
+ if (this._drag?.active) {
2856
+ calendar.setAttribute("data-range-drag", "active");
2857
+ return;
2858
+ }
2859
+ const { start, end } = this._range;
2860
+ const draggable = this._range.complete && start !== end && (this._boundEditable("start") || this._boundEditable("end"));
2861
+ if (draggable)
2862
+ calendar.setAttribute("data-range-drag", "ready");
2863
+ else
2864
+ calendar.removeAttribute("data-range-drag");
2482
2865
  }
2483
2866
  _onEscape(event) {
2484
2867
  if (event.key === "Escape" && this._open) {
@@ -2581,7 +2964,7 @@
2581
2964
  this._field?.setCanonical(this._value, { format: options.format !== false });
2582
2965
  if (this._calendar) {
2583
2966
  this._calendar.value = this._value;
2584
- if (this._value)
2967
+ if (this._value && options.moveDisplay !== false)
2585
2968
  this._calendar.focusedDate = this._value;
2586
2969
  }
2587
2970
  this._refreshButtonLabel();
@@ -2594,13 +2977,15 @@
2594
2977
  }
2595
2978
  }
2596
2979
  }
2597
- async _confirmDate(date) {
2980
+ async _confirmDate(date, options = {}) {
2598
2981
  const calendar = this._calendar;
2599
2982
  if (!calendar)
2600
2983
  return { ok: true };
2601
- const display = monthKey(date);
2602
- if (calendar.display !== display)
2603
- calendar.display = display;
2984
+ if (options.moveDisplay !== false) {
2985
+ const display = monthKey(date);
2986
+ if (calendar.display !== display)
2987
+ calendar.display = display;
2988
+ }
2604
2989
  const confirmed = await calendar.ensureDate(date);
2605
2990
  const state = calendar.getDateState(date);
2606
2991
  if (!confirmed || state.disabled)
@@ -2613,10 +2998,14 @@
2613
2998
  return false;
2614
2999
  if (!options.force && !field.isDirty)
2615
3000
  return false;
2616
- const result = await field.commit();
3001
+ const result = await field.commit(options);
2617
3002
  if (result.status === "stale" || result.status === "invalid")
2618
3003
  return false;
2619
- this._setValue(result.value || "", { emit, format: result.status === "ok" });
3004
+ this._setValue(result.value || "", {
3005
+ emit,
3006
+ format: result.status === "ok",
3007
+ moveDisplay: options.moveDisplay
3008
+ });
2620
3009
  return true;
2621
3010
  }
2622
3011
  async _commitFieldText(which) {
@@ -2691,19 +3080,21 @@
2691
3080
  return;
2692
3081
  this.dispatchEvent(new CustomEvent("rangechange", { detail: { ...this._range.range }, bubbles: true }));
2693
3082
  }
3083
+ _displayRange() {
3084
+ const { start, end } = this._range?.range ?? { start: "", end: "" };
3085
+ if (!start)
3086
+ return { start: "", end: "" };
3087
+ if (end && compareDates(end, start) < 0)
3088
+ return { start, end: "" };
3089
+ return { start, end };
3090
+ }
2694
3091
  _syncHighlight() {
2695
3092
  if (!this._calendar || !this._range)
2696
3093
  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
- }
3094
+ this._previewDate = "";
3095
+ this._calendar.removeAttribute("data-range-preview");
3096
+ this._calendar.highlightedRange = this._displayRange();
3097
+ this._syncDragAffordance();
2707
3098
  }
2708
3099
  _revalidateRange(which) {
2709
3100
  if (!this._fields || !this._range)
@@ -2825,7 +3216,7 @@
2825
3216
  if (!fields)
2826
3217
  return true;
2827
3218
  for (const [index, field] of fields.entries()) {
2828
- const result = await field.commit();
3219
+ const result = await field.commit({ moveDisplay: false });
2829
3220
  if (result.status === "ok" || result.status === "clear") {
2830
3221
  const which = index === 1 ? "end" : "start";
2831
3222
  this._setBound(which, result.value || "", { format: result.status === "ok" });
@@ -2841,7 +3232,7 @@
2841
3232
  if (!input.value.trim()) {
2842
3233
  input.setCustomValidity("");
2843
3234
  } else {
2844
- await this._commitText(false, { force: true });
3235
+ await this._commitText(false, { force: true, moveDisplay: false });
2845
3236
  }
2846
3237
  this._revalidateTimeOrder("start");
2847
3238
  this._revalidateTimeOrder("end");
@@ -2904,6 +3295,12 @@
2904
3295
  this._setExpanded(false);
2905
3296
  this._rangeCommitId++;
2906
3297
  this._pendingIntents.clear();
3298
+ this._suppressGridClick = false;
3299
+ if (this._drag) {
3300
+ this._releaseDragCapture(this._drag.pointerId);
3301
+ this._drag = null;
3302
+ }
3303
+ this._syncHighlight();
2907
3304
  if (restoreFocus) {
2908
3305
  if (this._rangeMode()) {
2909
3306
  const endpoint = this._restoreFocusEndpoint();