@nepdatejs/datepicker 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,1320 @@
1
+ import { addDays, addMonths, compareDates, differenceInDays, endOfMonth, endOfWeek, endOfYear, fiscalYear, fiscalYearRange, formatDate, formatMessage, formatNumber, formatTime, fromIsoDate, fromIsoDateTime, getConfig, getLocale, isWithin, now, parseDate, parseTime, startOfMonth, startOfWeek, startOfYear, toCalendar, toIsoDate, toIsoDateTime, today } from "@nepdatejs/core";
2
+ import { createCalendar, isDayList } from "@nepdatejs/calendar";
3
+ //#region src/mask.ts
4
+ const DIGIT = "[0-9०-९]";
5
+ const DIGITS_ONLY = new RegExp(`^${DIGIT}{1,8}$`);
6
+ const YEAR_MONTH = new RegExp(`^(${DIGIT}{4}-${DIGIT}{2})(${DIGIT}?)$`);
7
+ /**
8
+ * Add the dashes of YYYY-MM-DD while the user types digits: "2083" becomes
9
+ * "2083-", "208306" becomes "2083-06-" and a pasted "20830624" becomes
10
+ * "2083-06-24". ASCII and Devanagari digits keep their script, and a
11
+ * separator typed straight after a dash is dropped, so typing the dashes
12
+ * yourself works too. Only text that grew at the end (typing, or a paste into
13
+ * an empty field) is touched: deleting, editing in the middle and other
14
+ * separators ("2083/6/4") are left as the user wrote them. Pure.
15
+ */
16
+ function maskDateText(next, previous = "") {
17
+ if (next.length <= previous.length || !next.startsWith(previous)) return next;
18
+ if (previous.endsWith("-") && /^[-./]$/.test(next.slice(previous.length))) return previous;
19
+ if (DIGITS_ONLY.test(next)) {
20
+ let out = next.slice(0, 4);
21
+ if (next.length >= 4) out += "-";
22
+ out += next.slice(4, 6);
23
+ if (next.length >= 6) out += "-";
24
+ return out + next.slice(6);
25
+ }
26
+ const yearMonth = YEAR_MONTH.exec(next);
27
+ if (yearMonth) return `${yearMonth[1] ?? ""}-${yearMonth[2] ?? ""}`;
28
+ return next;
29
+ }
30
+ //#endregion
31
+ //#region src/input.ts
32
+ /**
33
+ * Read what the user typed: year first, ASCII or Devanagari digits, in the
34
+ * calendar on screen. All date work is done by core (docs/plan D-005).
35
+ */
36
+ function parseInput(text, calendar, limits, locale, format = "iso") {
37
+ if (text.trim() === "") return { kind: "empty" };
38
+ const messages = getLocale(locale).messages;
39
+ const date = parseDate(text, calendar);
40
+ if (!date) return {
41
+ kind: "error",
42
+ error: {
43
+ code: "format",
44
+ message: messages.invalidFormat[calendar]
45
+ }
46
+ };
47
+ const error = checkDate(date, calendar, limits, locale, format);
48
+ return error ? {
49
+ kind: "error",
50
+ error
51
+ } : {
52
+ kind: "date",
53
+ date: toCalendar(date, "AD")
54
+ };
55
+ }
56
+ /** Null when the day is allowed; otherwise a translated error. */
57
+ function checkDate(date, calendar, limits, locale, format = "iso") {
58
+ const messages = getLocale(locale).messages;
59
+ if (!isWithin(date, limits.min, limits.max)) {
60
+ const values = {
61
+ min: formatInput(limits.min, calendar, format, locale),
62
+ max: formatInput(limits.max, calendar, format, locale)
63
+ };
64
+ const template = limits.hasMin === limits.hasMax ? messages.outOfRange : limits.hasMin ? messages.onOrAfter : messages.onOrBefore;
65
+ return {
66
+ code: "range",
67
+ message: formatMessage(template, values)
68
+ };
69
+ }
70
+ if (limits.isDateDisabled?.(date)) return {
71
+ code: "disabled",
72
+ message: messages.unavailable
73
+ };
74
+ return null;
75
+ }
76
+ /** How the input shows a date: in the calendar on screen, with the locale's digits. */
77
+ function formatInput(date, calendar, format, locale) {
78
+ return date ? formatDate(toCalendar(date, calendar), format, locale) : "";
79
+ }
80
+ /** "10 October 2026 AD": the same day in the other calendar. */
81
+ function equivalentText(date, calendar, locale) {
82
+ if (!date) return null;
83
+ const other = calendar === "BS" ? "AD" : "BS";
84
+ return `${formatDate(toCalendar(date, other), "long", locale)} ${getLocale(locale).calendarNames[other]}`;
85
+ }
86
+ function placeholderText(calendar, locale) {
87
+ return `YYYY-MM-DD (${getLocale(locale).calendarNames[calendar]})`;
88
+ }
89
+ /** An AD "YYYY-MM-DD" string as a date; anything else is null. */
90
+ function readIso(value) {
91
+ return value ? fromIsoDate(value) : null;
92
+ }
93
+ function detailOf(date) {
94
+ return date ? Object.freeze({
95
+ ad: toCalendar(date, "AD"),
96
+ bs: toCalendar(date, "BS")
97
+ }) : null;
98
+ }
99
+ //#endregion
100
+ //#region src/base.ts
101
+ /** The panel calendar's options that come straight from the picker's options. */
102
+ function panelOptions(options, calendar) {
103
+ return {
104
+ calendar,
105
+ locale: options.locale,
106
+ min: readIso(options.min),
107
+ max: readIso(options.max),
108
+ isDateDisabled: options.isDateDisabled,
109
+ holidays: options.holidays,
110
+ events: options.events,
111
+ showOtherCalendar: options.showOtherCalendar,
112
+ weekStartsOn: options.weekStartsOn,
113
+ weekendDays: options.weekendDays,
114
+ today: options.today
115
+ };
116
+ }
117
+ /** What may be typed: the panel's effective range, so typing and clicking agree. */
118
+ function inputLimits(options, panel) {
119
+ const { minDate, maxDate } = panel.getState();
120
+ return {
121
+ min: minDate,
122
+ max: maxDate,
123
+ hasMin: readIso(options.min) !== null,
124
+ hasMax: readIso(options.max) !== null,
125
+ isDateDisabled: options.isDateDisabled
126
+ };
127
+ }
128
+ function localeOf(options) {
129
+ return options.locale ?? getConfig().defaultLocale;
130
+ }
131
+ function formatOf(options) {
132
+ return options.displayFormat ?? "iso";
133
+ }
134
+ function todayOf(options) {
135
+ return options.today ?? today("AD");
136
+ }
137
+ function isRange(selection) {
138
+ return !isDayList(selection) && !("calendar" in selection);
139
+ }
140
+ function isSingleDay(selection) {
141
+ return !isDayList(selection) && "calendar" in selection;
142
+ }
143
+ /**
144
+ * Listener set plus batching: changes made inside `act` notify once at the end,
145
+ * and callbacks queued with `after` run after listeners have seen the new state.
146
+ */
147
+ function createEmitter(rebuild) {
148
+ const listeners = /* @__PURE__ */ new Set();
149
+ const queued = [];
150
+ let depth = 0;
151
+ function notify() {
152
+ rebuild();
153
+ for (const listener of [...listeners]) listener();
154
+ }
155
+ return {
156
+ subscribe(listener) {
157
+ listeners.add(listener);
158
+ return () => {
159
+ listeners.delete(listener);
160
+ };
161
+ },
162
+ act(change) {
163
+ depth++;
164
+ try {
165
+ change();
166
+ } finally {
167
+ depth--;
168
+ if (depth === 0) {
169
+ const callbacks = queued.splice(0);
170
+ try {
171
+ notify();
172
+ } finally {
173
+ for (const callback of callbacks) callback();
174
+ }
175
+ }
176
+ }
177
+ },
178
+ after(callback) {
179
+ queued.push(callback);
180
+ },
181
+ notify() {
182
+ if (depth === 0) notify();
183
+ },
184
+ clear() {
185
+ listeners.clear();
186
+ }
187
+ };
188
+ }
189
+ //#endregion
190
+ //#region src/presets.ts
191
+ /** The date a built-in single preset stands for. */
192
+ function resolveSinglePreset(id, context) {
193
+ const today = toCalendar(context.today, context.calendar);
194
+ switch (id) {
195
+ case "today": return today;
196
+ case "yesterday": return addDays(today, -1);
197
+ case "tomorrow": return addDays(today, 1);
198
+ }
199
+ }
200
+ /** The range a built-in range preset stands for, in the calendar on screen. */
201
+ function resolveRangePreset(id, context) {
202
+ const today = toCalendar(context.today, context.calendar);
203
+ const week = context.weekStartsOn;
204
+ switch (id) {
205
+ case "today": return {
206
+ start: today,
207
+ end: today
208
+ };
209
+ case "yesterday": {
210
+ const day = addDays(today, -1);
211
+ return {
212
+ start: day,
213
+ end: day
214
+ };
215
+ }
216
+ case "thisWeek": return {
217
+ start: startOfWeek(today, week),
218
+ end: endOfWeek(today, week)
219
+ };
220
+ case "lastWeek": {
221
+ const day = addDays(today, -7);
222
+ return {
223
+ start: startOfWeek(day, week),
224
+ end: endOfWeek(day, week)
225
+ };
226
+ }
227
+ case "last7Days": return {
228
+ start: addDays(today, -6),
229
+ end: today
230
+ };
231
+ case "last30Days": return {
232
+ start: addDays(today, -29),
233
+ end: today
234
+ };
235
+ case "thisMonth": return {
236
+ start: startOfMonth(today),
237
+ end: endOfMonth(today)
238
+ };
239
+ case "lastMonth": {
240
+ const month = addMonths(startOfMonth(today), -1);
241
+ return {
242
+ start: month,
243
+ end: endOfMonth(month)
244
+ };
245
+ }
246
+ case "thisYear": return {
247
+ start: startOfYear(today),
248
+ end: endOfYear(today)
249
+ };
250
+ case "thisFiscalYear": {
251
+ const fy = fiscalYear(today);
252
+ return {
253
+ start: fy.firstDay,
254
+ end: fy.lastDay
255
+ };
256
+ }
257
+ case "lastFiscalYear": {
258
+ const fy = fiscalYearRange(fiscalYear(today).startYear - 1);
259
+ return {
260
+ start: fy.firstDay,
261
+ end: fy.lastDay
262
+ };
263
+ }
264
+ }
265
+ }
266
+ /** Shared by both pickers: label + resolver for a preset entry. */
267
+ function describePreset(preset, locale, resolveBuiltIn) {
268
+ if (typeof preset === "string") {
269
+ const labels = getLocale(locale).presets;
270
+ return {
271
+ id: preset,
272
+ label: Object.prototype.hasOwnProperty.call(labels, preset) ? labels[preset] ?? preset : preset,
273
+ resolve: (context) => resolveBuiltIn(preset, context)
274
+ };
275
+ }
276
+ return preset;
277
+ }
278
+ //#endregion
279
+ //#region src/picker.ts
280
+ /**
281
+ * A BS/AD date picker without UI: typed text ⇄ value, validation, the BS/AD
282
+ * switch and open/close, plus the panel's calendar store. The value is always
283
+ * an AD "YYYY-MM-DD" string (docs/plan D-003); all date work goes through core.
284
+ *
285
+ * Like the calendar, the picker keeps its own value; passing `value` through
286
+ * setOptions replaces it (controlled use). Passing back the value the user just
287
+ * typed leaves their text alone, so the cursor does not jump.
288
+ */
289
+ function createDatePicker(initial = {}) {
290
+ let options = { ...initial };
291
+ let calendar = options.calendar ?? options.defaultCalendar ?? "BS";
292
+ let date = readIso(options.value ?? options.defaultValue);
293
+ let value = date ? toIsoDate(date) : "";
294
+ let text = formatInput(date, calendar, formatOf(options), localeOf(options));
295
+ let error = null;
296
+ let open = false;
297
+ let destroyed = false;
298
+ const panel = createCalendar({
299
+ ...panelOptions(options, calendar),
300
+ selectionMode: "single",
301
+ selected: date,
302
+ onSelect: (selection) => {
303
+ if (isSingleDay(selection)) pick(selection);
304
+ }
305
+ });
306
+ let snapshot = build();
307
+ const emitter = createEmitter(() => {
308
+ snapshot = build();
309
+ });
310
+ panel.subscribe(() => {
311
+ emitter.notify();
312
+ });
313
+ function build() {
314
+ const locale = localeOf(options);
315
+ return Object.freeze({
316
+ value,
317
+ date: detailOf(date),
318
+ text,
319
+ calendar,
320
+ canSwitchCalendar: options.calendar === void 0,
321
+ open: open || options.inline === true,
322
+ inline: options.inline === true,
323
+ error,
324
+ equivalentText: error ? null : equivalentText(date, calendar, locale),
325
+ placeholder: placeholderText(calendar, locale),
326
+ presets: presetStates(),
327
+ calendarState: panel.getState()
328
+ });
329
+ }
330
+ function presetContext() {
331
+ return {
332
+ today: toCalendar(todayOf(options), "AD"),
333
+ calendar,
334
+ weekStartsOn: options.weekStartsOn ?? getConfig().weekStartsOn
335
+ };
336
+ }
337
+ /** Each shortcut's date, or null when it cannot be used now. */
338
+ function resolvedPresets() {
339
+ const context = presetContext();
340
+ const limits = inputLimits(options, panel);
341
+ return (options.presets ?? []).map((preset) => {
342
+ const entry = describePreset(preset, options.locale, resolveSinglePreset);
343
+ const date = (() => {
344
+ try {
345
+ const candidate = entry.resolve(context);
346
+ return checkDate(candidate, calendar, limits, localeOf(options)) ? null : candidate;
347
+ } catch {
348
+ return null;
349
+ }
350
+ })();
351
+ return {
352
+ id: entry.id,
353
+ label: entry.label,
354
+ date
355
+ };
356
+ });
357
+ }
358
+ function presetStates() {
359
+ return resolvedPresets().map(({ id, label, date }) => Object.freeze({
360
+ id,
361
+ label,
362
+ isActive: date !== null && toIsoDate(date) === value,
363
+ isDisabled: date === null
364
+ }));
365
+ }
366
+ function syncPanel(showDate) {
367
+ panel.setOptions({
368
+ ...panelOptions(options, calendar),
369
+ selected: date,
370
+ ...showDate && date ? { visibleMonth: date } : {}
371
+ });
372
+ }
373
+ /** Change the value; queue onChange when it differs and the user caused it. */
374
+ function setDate(next, byUser) {
375
+ const nextValue = next ? toIsoDate(next) : "";
376
+ const changed = nextValue !== value;
377
+ date = next;
378
+ value = nextValue;
379
+ syncPanel(next !== null);
380
+ if (changed && byUser) {
381
+ const detail = detailOf(next);
382
+ emitter.after(() => options.onChange?.(nextValue, detail));
383
+ }
384
+ }
385
+ function format(next) {
386
+ return formatInput(next, calendar, formatOf(options), localeOf(options));
387
+ }
388
+ function pick(selection) {
389
+ emitter.act(() => {
390
+ const next = toCalendar(selection, "AD");
391
+ error = null;
392
+ text = format(next);
393
+ setDate(next, true);
394
+ if (options.closeOnSelect ?? true) open = false;
395
+ });
396
+ }
397
+ const actions = {
398
+ type(next) {
399
+ if (destroyed) return;
400
+ emitter.act(() => {
401
+ text = options.mask === false ? next : maskDateText(next, text);
402
+ const result = parseInput(text, calendar, inputLimits(options, panel), localeOf(options), formatOf(options));
403
+ error = result.kind === "error" ? result.error : null;
404
+ setDate(result.kind === "date" ? result.date : null, true);
405
+ });
406
+ },
407
+ blur() {
408
+ emitter.act(() => {
409
+ if (!error) text = format(date);
410
+ });
411
+ },
412
+ clear() {
413
+ emitter.act(() => {
414
+ text = "";
415
+ error = null;
416
+ setDate(null, true);
417
+ });
418
+ },
419
+ setValue(next) {
420
+ emitter.act(() => {
421
+ applyValue(next);
422
+ });
423
+ },
424
+ open() {
425
+ emitter.act(() => {
426
+ open = true;
427
+ panel.actions.goTo(date ?? todayOf(options));
428
+ });
429
+ },
430
+ close() {
431
+ emitter.act(() => {
432
+ open = false;
433
+ });
434
+ },
435
+ toggle() {
436
+ if (open) actions.close();
437
+ else actions.open();
438
+ },
439
+ switchCalendar(next) {
440
+ if (options.calendar !== void 0 || next === calendar || destroyed) return;
441
+ emitter.act(() => {
442
+ calendar = next;
443
+ if (!error) text = format(date);
444
+ syncPanel(date !== null);
445
+ emitter.after(() => options.onCalendarChange?.(next));
446
+ });
447
+ },
448
+ select(day) {
449
+ let picked = false;
450
+ if (!destroyed) emitter.act(() => {
451
+ picked = panel.actions.select(day);
452
+ });
453
+ return picked;
454
+ },
455
+ selectToday() {
456
+ return actions.select(todayOf(options));
457
+ },
458
+ reset() {
459
+ emitter.act(() => {
460
+ const next = readIso(options.defaultValue);
461
+ error = null;
462
+ text = format(next);
463
+ setDate(next, true);
464
+ });
465
+ },
466
+ applyPreset(id) {
467
+ const preset = resolvedPresets().find((entry) => entry.id === id);
468
+ if (destroyed || !preset?.date) return false;
469
+ pick(preset.date);
470
+ return true;
471
+ }
472
+ };
473
+ /** An outside value: replaces the text only when it is a different value (echo guard). */
474
+ function applyValue(next) {
475
+ const nextDate = readIso(next);
476
+ if ((nextDate ? toIsoDate(nextDate) : "") === value) return;
477
+ error = null;
478
+ text = format(nextDate);
479
+ setDate(nextDate, false);
480
+ }
481
+ return {
482
+ getState: () => snapshot,
483
+ subscribe: (listener) => emitter.subscribe(listener),
484
+ setOptions(changes) {
485
+ if (destroyed) return;
486
+ emitter.act(() => {
487
+ const before = {
488
+ calendar,
489
+ locale: localeOf(options),
490
+ format: formatOf(options)
491
+ };
492
+ options = {
493
+ ...options,
494
+ ...changes
495
+ };
496
+ if (options.calendar !== void 0) calendar = options.calendar;
497
+ if ("value" in changes) applyValue(changes.value);
498
+ if ((before.calendar !== calendar || before.locale !== localeOf(options) || before.format !== formatOf(options)) && !error) text = format(date);
499
+ syncPanel(false);
500
+ });
501
+ },
502
+ actions,
503
+ calendar: panel,
504
+ destroy() {
505
+ destroyed = true;
506
+ panel.destroy();
507
+ emitter.clear();
508
+ }
509
+ };
510
+ }
511
+ //#endregion
512
+ //#region src/range.ts
513
+ /**
514
+ * A BS/AD date range picker without UI: two typed fields (start, end), a panel
515
+ * calendar in range mode, minDays/maxDays checks and the BS/AD switch. Values
516
+ * are AD "YYYY-MM-DD" strings; an unfinished or invalid end is "".
517
+ */
518
+ function createDateRangePicker(initial = {}) {
519
+ let options = { ...initial };
520
+ let calendar = options.calendar ?? options.defaultCalendar ?? "BS";
521
+ let start = readIso(options.value?.start ?? options.defaultValue?.start);
522
+ let end = readIso(options.value?.end ?? options.defaultValue?.end);
523
+ [start, end] = ordered(start, end);
524
+ let value = valueOf(start, end);
525
+ let startText = format(start);
526
+ let endText = format(end);
527
+ let error = null;
528
+ let open = false;
529
+ let destroyed = false;
530
+ const panel = createCalendar({
531
+ ...panelOptions(options, calendar),
532
+ selectionMode: "range",
533
+ selected: {
534
+ start,
535
+ end
536
+ },
537
+ onSelect: (selection) => {
538
+ if (isRange(selection)) pick(selection.start, selection.end);
539
+ }
540
+ });
541
+ let snapshot = build();
542
+ const emitter = createEmitter(() => {
543
+ snapshot = build();
544
+ });
545
+ panel.subscribe(() => {
546
+ emitter.notify();
547
+ });
548
+ function format(date) {
549
+ return formatInput(date, calendar, formatOf(options), localeOf(options));
550
+ }
551
+ function build() {
552
+ const locale = localeOf(options);
553
+ return Object.freeze({
554
+ value,
555
+ date: Object.freeze({
556
+ start: detailOf(start),
557
+ end: detailOf(end)
558
+ }),
559
+ startText,
560
+ endText,
561
+ calendar,
562
+ canSwitchCalendar: options.calendar === void 0,
563
+ open: open || options.inline === true,
564
+ inline: options.inline === true,
565
+ error,
566
+ equivalentText: Object.freeze({
567
+ start: error?.field === "start" ? null : equivalentText(start, calendar, locale),
568
+ end: error?.field === "end" ? null : equivalentText(end, calendar, locale)
569
+ }),
570
+ placeholder: placeholderText(calendar, locale),
571
+ presets: presetStates(),
572
+ calendarState: panel.getState()
573
+ });
574
+ }
575
+ function presetContext() {
576
+ return {
577
+ today: toCalendar(todayOf(options), "AD"),
578
+ calendar,
579
+ weekStartsOn: options.weekStartsOn ?? getConfig().weekStartsOn
580
+ };
581
+ }
582
+ /** Each shortcut's range, or null when it cannot be used now. */
583
+ function resolvedPresets() {
584
+ const context = presetContext();
585
+ const limits = inputLimits(options, panel);
586
+ const locale = localeOf(options);
587
+ return (options.presets ?? []).map((preset) => {
588
+ const entry = describePreset(preset, options.locale, resolveRangePreset);
589
+ const range = (() => {
590
+ try {
591
+ const candidate = entry.resolve(context);
592
+ const from = toCalendar(candidate.start, "AD");
593
+ const to = toCalendar(candidate.end, "AD");
594
+ return compareDates(from, to) <= 0 && !checkDate(from, calendar, limits, locale) && !checkDate(to, calendar, limits, locale) && lengthError(from, to) === null ? candidate : null;
595
+ } catch {
596
+ return null;
597
+ }
598
+ })();
599
+ return {
600
+ id: entry.id,
601
+ label: entry.label,
602
+ range
603
+ };
604
+ });
605
+ }
606
+ function presetStates() {
607
+ return resolvedPresets().map(({ id, label, range }) => Object.freeze({
608
+ id,
609
+ label,
610
+ isActive: range !== null && toIsoDate(range.start) === value.start && toIsoDate(range.end) === value.end,
611
+ isDisabled: range === null
612
+ }));
613
+ }
614
+ function syncPanel() {
615
+ panel.setOptions({
616
+ ...panelOptions(options, calendar),
617
+ selected: {
618
+ start,
619
+ end
620
+ },
621
+ ...start ? { visibleMonth: start } : {}
622
+ });
623
+ }
624
+ /** Too short or too long, or null. Counts both ends. */
625
+ function lengthError(from, to) {
626
+ const days = differenceInDays(to, from) + 1;
627
+ const messages = getLocale(localeOf(options)).messages;
628
+ if (options.minDays !== void 0 && days < options.minDays) return {
629
+ field: "range",
630
+ code: "tooShort",
631
+ message: formatMessage(messages.rangeTooShort, { min: String(options.minDays) })
632
+ };
633
+ if (options.maxDays !== void 0 && days > options.maxDays) return {
634
+ field: "range",
635
+ code: "tooLong",
636
+ message: formatMessage(messages.rangeTooLong, { max: String(options.maxDays) })
637
+ };
638
+ return null;
639
+ }
640
+ /** Store the dates, keep the panel in step, and queue onChange when the value moved. */
641
+ function commit(nextStart, nextEnd, byUser) {
642
+ start = nextStart;
643
+ end = nextEnd;
644
+ const next = valueOf(start, end);
645
+ const changed = next.start !== value.start || next.end !== value.end;
646
+ value = next;
647
+ syncPanel();
648
+ if (changed && byUser) {
649
+ const detail = Object.freeze({
650
+ start: detailOf(start),
651
+ end: detailOf(end)
652
+ });
653
+ emitter.after(() => options.onChange?.(next, detail));
654
+ }
655
+ }
656
+ /** Put the ends in order, check the length, then commit. False when the length is not allowed. */
657
+ function settle(nextStart, nextEnd) {
658
+ const [a, b] = ordered(nextStart, nextEnd);
659
+ if (a !== nextStart) [startText, endText] = [endText, startText];
660
+ const tooFar = a && b ? lengthError(a, b) : null;
661
+ error = tooFar ?? error;
662
+ commit(a, tooFar ? null : b, true);
663
+ return tooFar === null;
664
+ }
665
+ function pick(from, to) {
666
+ emitter.act(() => {
667
+ const a = from ? toCalendar(from, "AD") : null;
668
+ const b = to ? toCalendar(to, "AD") : null;
669
+ error = null;
670
+ startText = format(a);
671
+ endText = format(b);
672
+ const allowed = settle(a, b);
673
+ if (b && allowed && (options.closeOnSelect ?? true)) open = false;
674
+ });
675
+ }
676
+ const actions = {
677
+ type(field, text) {
678
+ if (destroyed) return;
679
+ emitter.act(() => {
680
+ if (options.mask !== false) text = maskDateText(text, field === "start" ? startText : endText);
681
+ if (field === "start") startText = text;
682
+ else endText = text;
683
+ const result = parseInput(text, calendar, inputLimits(options, panel), localeOf(options), formatOf(options));
684
+ const date = result.kind === "date" ? result.date : null;
685
+ error = result.kind === "error" ? {
686
+ ...result.error,
687
+ field
688
+ } : null;
689
+ const other = field === "start" ? end : start;
690
+ const otherError = other ? checkDate(other, calendar, inputLimits(options, panel), localeOf(options)) : null;
691
+ if (!error && otherError) error = {
692
+ ...otherError,
693
+ field: field === "start" ? "end" : "start"
694
+ };
695
+ if (field === "start") settle(date, end);
696
+ else settle(start, date);
697
+ });
698
+ },
699
+ blur(field) {
700
+ emitter.act(() => {
701
+ if (error?.field === field) return;
702
+ if (field === "start") startText = format(start);
703
+ else if (end || error?.field !== "range") endText = format(end);
704
+ });
705
+ },
706
+ clear() {
707
+ emitter.act(() => {
708
+ startText = "";
709
+ endText = "";
710
+ error = null;
711
+ commit(null, null, true);
712
+ });
713
+ },
714
+ setValue(next) {
715
+ emitter.act(() => {
716
+ applyValue(next);
717
+ });
718
+ },
719
+ open() {
720
+ emitter.act(() => {
721
+ open = true;
722
+ panel.actions.goTo(start ?? todayOf(options));
723
+ });
724
+ },
725
+ close() {
726
+ emitter.act(() => {
727
+ open = false;
728
+ });
729
+ },
730
+ toggle() {
731
+ if (open) actions.close();
732
+ else actions.open();
733
+ },
734
+ switchCalendar(next) {
735
+ if (options.calendar !== void 0 || next === calendar || destroyed) return;
736
+ emitter.act(() => {
737
+ calendar = next;
738
+ if (!error) {
739
+ startText = format(start);
740
+ endText = format(end);
741
+ }
742
+ syncPanel();
743
+ emitter.after(() => options.onCalendarChange?.(next));
744
+ });
745
+ },
746
+ select(day) {
747
+ let picked = false;
748
+ if (!destroyed) emitter.act(() => {
749
+ picked = panel.actions.select(day);
750
+ });
751
+ return picked;
752
+ },
753
+ applyPreset(id) {
754
+ const preset = resolvedPresets().find((entry) => entry.id === id);
755
+ if (destroyed || !preset?.range) return false;
756
+ pick(preset.range.start, preset.range.end);
757
+ return true;
758
+ },
759
+ reset() {
760
+ emitter.act(() => {
761
+ const [a, b] = ordered(readIso(options.defaultValue?.start), readIso(options.defaultValue?.end));
762
+ error = null;
763
+ startText = format(a);
764
+ endText = format(b);
765
+ commit(a, b, true);
766
+ });
767
+ }
768
+ };
769
+ /** An outside value: replaces the texts only when the value differs (echo guard). */
770
+ function applyValue(next) {
771
+ const [a, b] = ordered(readIso(next?.start), readIso(next?.end));
772
+ const incoming = valueOf(a, b);
773
+ if (incoming.start === value.start && incoming.end === value.end) return;
774
+ error = null;
775
+ startText = format(a);
776
+ endText = format(b);
777
+ commit(a, b, false);
778
+ }
779
+ return {
780
+ getState: () => snapshot,
781
+ subscribe: (listener) => emitter.subscribe(listener),
782
+ setOptions(changes) {
783
+ if (destroyed) return;
784
+ emitter.act(() => {
785
+ const before = {
786
+ calendar,
787
+ locale: localeOf(options),
788
+ format: formatOf(options)
789
+ };
790
+ options = {
791
+ ...options,
792
+ ...changes
793
+ };
794
+ if (options.calendar !== void 0) calendar = options.calendar;
795
+ if ("value" in changes) applyValue(changes.value);
796
+ if ((before.calendar !== calendar || before.locale !== localeOf(options) || before.format !== formatOf(options)) && !error) {
797
+ startText = format(start);
798
+ endText = format(end);
799
+ }
800
+ syncPanel();
801
+ });
802
+ },
803
+ actions,
804
+ calendar: panel,
805
+ destroy() {
806
+ destroyed = true;
807
+ panel.destroy();
808
+ emitter.clear();
809
+ }
810
+ };
811
+ }
812
+ function ordered(a, b) {
813
+ return a && b && compareDates(b, a) < 0 ? [b, a] : [a, b];
814
+ }
815
+ function valueOf(start, end) {
816
+ return Object.freeze({
817
+ start: start ? toIsoDate(start) : "",
818
+ end: end ? toIsoDate(end) : ""
819
+ });
820
+ }
821
+ //#endregion
822
+ //#region src/multi.ts
823
+ const SEPARATOR = /[,\s\u060c]+/;
824
+ /**
825
+ * A picker for several separate days (PICK-001). The value is a sorted list of
826
+ * AD "YYYY-MM-DD" strings; the input lists the days in the calendar on screen,
827
+ * separated by ", ", and clicking a day in the panel adds or removes it. The
828
+ * list is always written as YYYY-MM-DD so it can be typed back.
829
+ */
830
+ function createMultiDatePicker(initial = {}) {
831
+ let options = { ...initial };
832
+ let calendar = options.calendar ?? options.defaultCalendar ?? "BS";
833
+ let dates = readAll(options.value ?? options.defaultValue);
834
+ let value = valuesOf(dates);
835
+ let text = format(dates);
836
+ let error = null;
837
+ /** False while the typed text has a problem: then it is left as typed. */
838
+ let textOk = true;
839
+ let open = false;
840
+ let destroyed = false;
841
+ const panel = createCalendar({
842
+ ...panelOptions(options, calendar),
843
+ selectionMode: "multiple",
844
+ selected: dates,
845
+ onSelect: (selection) => {
846
+ if (isDayList(selection)) pick(selection.map((day) => toCalendar(day, "AD")));
847
+ }
848
+ });
849
+ let snapshot = build();
850
+ const emitter = createEmitter(() => {
851
+ snapshot = build();
852
+ });
853
+ panel.subscribe(() => {
854
+ emitter.notify();
855
+ });
856
+ function build() {
857
+ return Object.freeze({
858
+ value,
859
+ dates: detailsOf(dates),
860
+ text,
861
+ calendar,
862
+ canSwitchCalendar: options.calendar === void 0,
863
+ open: open || options.inline === true,
864
+ inline: options.inline === true,
865
+ error,
866
+ placeholder: placeholderText(calendar, localeOf(options)),
867
+ calendarState: panel.getState()
868
+ });
869
+ }
870
+ function format(list) {
871
+ return list.map((day) => formatInput(day, calendar, "iso", localeOf(options))).join(", ");
872
+ }
873
+ function tooMany(count) {
874
+ const max = options.maxDates;
875
+ return max !== void 0 && count > max ? {
876
+ code: "tooMany",
877
+ message: formatMessage(getLocale(localeOf(options)).messages.tooManyDates, { max: String(max) })
878
+ } : null;
879
+ }
880
+ function syncPanel() {
881
+ panel.setOptions({
882
+ ...panelOptions(options, calendar),
883
+ selected: dates
884
+ });
885
+ }
886
+ /** Change the value; queue onChange when it differs and the user caused it. */
887
+ function setDates(next, byUser) {
888
+ const sorted = unique(next);
889
+ const nextValue = valuesOf(sorted);
890
+ const changed = nextValue.join() !== value.join();
891
+ dates = sorted;
892
+ value = nextValue;
893
+ syncPanel();
894
+ if (changed && byUser) {
895
+ const details = detailsOf(sorted);
896
+ emitter.after(() => options.onChange?.(nextValue, details));
897
+ }
898
+ }
899
+ function pick(next) {
900
+ emitter.act(() => {
901
+ error = tooMany(next.length);
902
+ if (error) {
903
+ syncPanel();
904
+ return;
905
+ }
906
+ text = format(unique(next));
907
+ textOk = true;
908
+ setDates(next, true);
909
+ if (options.closeOnSelect === true) open = false;
910
+ });
911
+ }
912
+ const actions = {
913
+ type(next) {
914
+ if (destroyed) return;
915
+ emitter.act(() => {
916
+ text = options.mask === false ? next : maskLast(next, text);
917
+ const limits = inputLimits(options, panel);
918
+ const parsed = [];
919
+ let firstError = null;
920
+ for (const token of text.split(SEPARATOR)) {
921
+ const result = parseInput(token, calendar, limits, localeOf(options));
922
+ if (result.kind === "date") parsed.push(result.date);
923
+ else if (result.kind === "error") firstError ?? (firstError = result.error);
924
+ }
925
+ error = firstError ?? tooMany(unique(parsed).length);
926
+ textOk = error === null;
927
+ setDates(parsed, true);
928
+ });
929
+ },
930
+ blur() {
931
+ emitter.act(() => {
932
+ if (textOk) text = format(dates);
933
+ });
934
+ },
935
+ clear() {
936
+ emitter.act(() => {
937
+ text = "";
938
+ error = null;
939
+ textOk = true;
940
+ setDates([], true);
941
+ });
942
+ },
943
+ setValue(next) {
944
+ emitter.act(() => {
945
+ applyValue(next);
946
+ });
947
+ },
948
+ open() {
949
+ emitter.act(() => {
950
+ open = true;
951
+ const first = dates[0];
952
+ if (first) panel.actions.goTo(first);
953
+ });
954
+ },
955
+ close() {
956
+ emitter.act(() => {
957
+ open = false;
958
+ });
959
+ },
960
+ toggle() {
961
+ if (open) actions.close();
962
+ else actions.open();
963
+ },
964
+ switchCalendar(next) {
965
+ if (options.calendar !== void 0 || next === calendar || destroyed) return;
966
+ emitter.act(() => {
967
+ calendar = next;
968
+ if (textOk) text = format(dates);
969
+ syncPanel();
970
+ emitter.after(() => options.onCalendarChange?.(next));
971
+ });
972
+ },
973
+ select(day) {
974
+ if (destroyed) return false;
975
+ const before = value;
976
+ emitter.act(() => {
977
+ panel.actions.select(day);
978
+ });
979
+ return value !== before;
980
+ },
981
+ remove(iso) {
982
+ emitter.act(() => {
983
+ error = null;
984
+ const next = dates.filter((day) => toIsoDate(day) !== iso);
985
+ text = format(next);
986
+ textOk = true;
987
+ setDates(next, true);
988
+ });
989
+ },
990
+ reset() {
991
+ emitter.act(() => {
992
+ const next = readAll(options.defaultValue);
993
+ error = null;
994
+ text = format(next);
995
+ textOk = true;
996
+ setDates(next, true);
997
+ });
998
+ }
999
+ };
1000
+ /** An outside value: replaces the text only when it is a different value (echo guard). */
1001
+ function applyValue(next) {
1002
+ const nextDates = readAll(next);
1003
+ if (valuesOf(nextDates).join() === value.join()) return;
1004
+ error = null;
1005
+ text = format(nextDates);
1006
+ textOk = true;
1007
+ setDates(nextDates, false);
1008
+ }
1009
+ return {
1010
+ getState: () => snapshot,
1011
+ subscribe: (listener) => emitter.subscribe(listener),
1012
+ setOptions(changes) {
1013
+ if (destroyed) return;
1014
+ emitter.act(() => {
1015
+ const before = {
1016
+ calendar,
1017
+ locale: localeOf(options)
1018
+ };
1019
+ options = {
1020
+ ...options,
1021
+ ...changes
1022
+ };
1023
+ if (options.calendar !== void 0) calendar = options.calendar;
1024
+ if ("value" in changes) applyValue(changes.value);
1025
+ if ((before.calendar !== calendar || before.locale !== localeOf(options)) && textOk) text = format(dates);
1026
+ syncPanel();
1027
+ });
1028
+ },
1029
+ actions,
1030
+ calendar: panel,
1031
+ destroy() {
1032
+ destroyed = true;
1033
+ panel.destroy();
1034
+ emitter.clear();
1035
+ }
1036
+ };
1037
+ }
1038
+ function readAll(values) {
1039
+ return unique((values ?? []).map(readIso).filter((day) => day !== null));
1040
+ }
1041
+ function unique(list) {
1042
+ return [...new Map(list.map((day) => [toIsoDate(day), day])).entries()].sort(([a], [b]) => a < b ? -1 : 1).map(([, day]) => day);
1043
+ }
1044
+ function valuesOf(list) {
1045
+ return Object.freeze(list.map((day) => toIsoDate(day)));
1046
+ }
1047
+ function detailsOf(list) {
1048
+ return Object.freeze(list.map((day) => Object.freeze({
1049
+ ad: day,
1050
+ bs: toCalendar(day, "BS")
1051
+ })));
1052
+ }
1053
+ /** Mask only the day being typed: the text after the last separator. */
1054
+ function maskLast(next, previous) {
1055
+ if (!next.startsWith(previous)) return next;
1056
+ const match = /^(.*[,\s\u060c])?([^,\s\u060c]*)$/.exec(next);
1057
+ const head = match?.[1] ?? "";
1058
+ return head + maskDateText(match?.[2] ?? next, previous.length > head.length ? previous.slice(head.length) : "");
1059
+ }
1060
+ //#endregion
1061
+ //#region src/datetime.ts
1062
+ /**
1063
+ * A BS/AD date and time picker without UI. The date half is a regular date
1064
+ * picker; the value is the instant as an ISO timestamp carrying the zone's
1065
+ * offset, so servers store one unambiguous moment.
1066
+ */
1067
+ function createDateTimePicker(initial = {}) {
1068
+ let options = { ...initial };
1069
+ const start = fromIsoDateTime(options.value ?? options.defaultValue, zone());
1070
+ let time = start?.time ?? null;
1071
+ let timeText = time ? fmt(time) : "";
1072
+ let timeError = null;
1073
+ let value = "";
1074
+ let destroyed = false;
1075
+ const datePicker = createDatePicker({
1076
+ ...dateOptions(),
1077
+ defaultValue: start ? toIsoDay(start.date) : "",
1078
+ closeOnSelect: false,
1079
+ onChange: () => {
1080
+ emitter.act(() => {
1081
+ if (datePicker.getState().date && !time) {
1082
+ time = parseTime(options.defaultTime) ?? Object.freeze({
1083
+ hour: 0,
1084
+ minute: 0
1085
+ });
1086
+ timeText = fmt(time);
1087
+ timeError = null;
1088
+ }
1089
+ commit(true);
1090
+ });
1091
+ }
1092
+ });
1093
+ value = compose();
1094
+ let snapshot = build();
1095
+ const emitter = createEmitter(() => {
1096
+ snapshot = build();
1097
+ });
1098
+ datePicker.subscribe(() => {
1099
+ emitter.notify();
1100
+ });
1101
+ function zone() {
1102
+ return options.timeZone ?? getConfig().timeZone;
1103
+ }
1104
+ function fmt(t) {
1105
+ return formatTime(t, {
1106
+ hour12: options.hour12 ?? false,
1107
+ locale: localeOf(options)
1108
+ });
1109
+ }
1110
+ function dateOptions() {
1111
+ const { value: _value, defaultValue: _default, onChange: _onChange, timeZone: _zone, hour12: _hour12, minuteStep: _step, defaultTime: _defaultTime, ...rest } = options;
1112
+ return rest;
1113
+ }
1114
+ function compose() {
1115
+ const day = datePicker.getState().date;
1116
+ return day && time ? toIsoDateTime(day.ad, time, zone()) : "";
1117
+ }
1118
+ function detail() {
1119
+ const day = datePicker.getState().date;
1120
+ return day && time && value ? Object.freeze({
1121
+ ad: day.ad,
1122
+ bs: day.bs,
1123
+ time,
1124
+ iso: value
1125
+ }) : null;
1126
+ }
1127
+ /** Recompute the value; queue onChange when it changed and the user caused it. */
1128
+ function commit(byUser) {
1129
+ const next = compose();
1130
+ if (next === value) return;
1131
+ value = next;
1132
+ if (byUser) {
1133
+ const info = detail();
1134
+ emitter.after(() => options.onChange?.(next, info));
1135
+ }
1136
+ }
1137
+ function build() {
1138
+ const inner = datePicker.getState();
1139
+ const locale = localeOf(options);
1140
+ const step = Math.min(30, Math.max(1, Math.floor(options.minuteStep ?? 5)));
1141
+ const { am, pm } = getLocale(locale).time;
1142
+ const hour12 = options.hour12 ?? false;
1143
+ return Object.freeze({
1144
+ value,
1145
+ detail: detail(),
1146
+ dateText: inner.text,
1147
+ timeText,
1148
+ time,
1149
+ calendar: inner.calendar,
1150
+ canSwitchCalendar: inner.canSwitchCalendar,
1151
+ open: inner.open,
1152
+ inline: inner.inline,
1153
+ error: inner.error ? {
1154
+ ...inner.error,
1155
+ field: "date"
1156
+ } : timeError ? {
1157
+ ...timeError,
1158
+ field: "time"
1159
+ } : null,
1160
+ equivalentText: inner.equivalentText,
1161
+ placeholder: inner.placeholder,
1162
+ timePlaceholder: hour12 ? `h:mm ${am}` : "HH:MM",
1163
+ hours: Array.from({ length: 24 }, (_, hour) => Object.freeze({
1164
+ value: hour,
1165
+ label: hour12 ? `${formatNumber(hour % 12 === 0 ? 12 : hour % 12, locale)} ${hour < 12 ? am : pm}` : formatNumber(String(hour).padStart(2, "0"), locale),
1166
+ isSelected: time?.hour === hour
1167
+ })),
1168
+ minutes: Array.from({ length: Math.ceil(60 / step) }, (_, index) => Object.freeze({
1169
+ value: index * step,
1170
+ label: formatNumber(String(index * step).padStart(2, "0"), locale),
1171
+ isSelected: time?.minute === index * step
1172
+ })),
1173
+ presets: inner.presets,
1174
+ calendarState: inner.calendarState
1175
+ });
1176
+ }
1177
+ function setTime(next, text, byUser) {
1178
+ time = next;
1179
+ timeText = text;
1180
+ timeError = null;
1181
+ commit(byUser);
1182
+ }
1183
+ const actions = {
1184
+ typeDate(text) {
1185
+ datePicker.actions.type(text);
1186
+ },
1187
+ blurDate() {
1188
+ datePicker.actions.blur();
1189
+ },
1190
+ typeTime(text) {
1191
+ if (destroyed) return;
1192
+ emitter.act(() => {
1193
+ const parsed = parseTime(text);
1194
+ if (text.trim() === "" || parsed) setTime(parsed, text, true);
1195
+ else {
1196
+ time = null;
1197
+ timeText = text;
1198
+ timeError = {
1199
+ code: "format",
1200
+ message: getLocale(localeOf(options)).messages.invalidTime
1201
+ };
1202
+ commit(true);
1203
+ }
1204
+ });
1205
+ },
1206
+ blurTime() {
1207
+ emitter.act(() => {
1208
+ if (!timeError && time) timeText = fmt(time);
1209
+ });
1210
+ },
1211
+ selectDate(date) {
1212
+ return !destroyed && datePicker.actions.select(date);
1213
+ },
1214
+ selectHour(hour) {
1215
+ emitter.act(() => {
1216
+ const next = Object.freeze({
1217
+ hour,
1218
+ minute: time?.minute ?? 0
1219
+ });
1220
+ setTime(next, fmt(next), true);
1221
+ });
1222
+ },
1223
+ selectMinute(minute) {
1224
+ emitter.act(() => {
1225
+ const next = Object.freeze({
1226
+ hour: time?.hour ?? 0,
1227
+ minute
1228
+ });
1229
+ setTime(next, fmt(next), true);
1230
+ });
1231
+ },
1232
+ selectNow() {
1233
+ if (destroyed) return false;
1234
+ const current = now(zone());
1235
+ let picked = false;
1236
+ emitter.act(() => {
1237
+ time = current.time;
1238
+ timeText = fmt(current.time);
1239
+ timeError = null;
1240
+ picked = datePicker.actions.select(current.date);
1241
+ commit(true);
1242
+ });
1243
+ return picked;
1244
+ },
1245
+ applyPreset(id) {
1246
+ return !destroyed && datePicker.actions.applyPreset(id);
1247
+ },
1248
+ open() {
1249
+ datePicker.actions.open();
1250
+ },
1251
+ close() {
1252
+ datePicker.actions.close();
1253
+ },
1254
+ toggle() {
1255
+ datePicker.actions.toggle();
1256
+ },
1257
+ clear() {
1258
+ emitter.act(() => {
1259
+ datePicker.actions.setValue("");
1260
+ setTime(null, "", true);
1261
+ });
1262
+ },
1263
+ setValue(next) {
1264
+ emitter.act(() => {
1265
+ applyValue(next);
1266
+ });
1267
+ },
1268
+ switchCalendar(calendar) {
1269
+ datePicker.actions.switchCalendar(calendar);
1270
+ },
1271
+ reset() {
1272
+ emitter.act(() => {
1273
+ const start = fromIsoDateTime(options.defaultValue, zone());
1274
+ datePicker.actions.setValue(start ? toIsoDay(start.date) : "");
1275
+ setTime(start?.time ?? null, start ? fmt(start.time) : "", true);
1276
+ });
1277
+ }
1278
+ };
1279
+ /** An outside value; ignored when it is the instant already shown (echo guard). */
1280
+ function applyValue(next) {
1281
+ const parsed = fromIsoDateTime(next, zone());
1282
+ if ((parsed ? toIsoDateTime(parsed.date, parsed.time, zone()) : "") === value) return;
1283
+ datePicker.actions.setValue(parsed ? toIsoDay(parsed.date) : "");
1284
+ time = parsed?.time ?? null;
1285
+ timeText = time ? fmt(time) : "";
1286
+ timeError = null;
1287
+ commit(false);
1288
+ }
1289
+ return {
1290
+ getState: () => snapshot,
1291
+ subscribe: (listener) => emitter.subscribe(listener),
1292
+ setOptions(changes) {
1293
+ if (destroyed) return;
1294
+ emitter.act(() => {
1295
+ options = {
1296
+ ...options,
1297
+ ...changes
1298
+ };
1299
+ datePicker.setOptions(dateOptions());
1300
+ if ("value" in changes) applyValue(changes.value);
1301
+ if (time && !timeError) timeText = fmt(time);
1302
+ commit(false);
1303
+ });
1304
+ },
1305
+ actions,
1306
+ calendar: datePicker.calendar,
1307
+ destroy() {
1308
+ destroyed = true;
1309
+ datePicker.destroy();
1310
+ emitter.clear();
1311
+ }
1312
+ };
1313
+ }
1314
+ function toIsoDay(date) {
1315
+ return toIsoDate(date);
1316
+ }
1317
+ //#endregion
1318
+ export { checkDate, createDatePicker, createDateRangePicker, createDateTimePicker, createMultiDatePicker, equivalentText, formatInput, maskDateText, parseInput, placeholderText, resolveRangePreset, resolveSinglePreset };
1319
+
1320
+ //# sourceMappingURL=index.js.map