@maihcx/super-date 0.3.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.
@@ -0,0 +1,1094 @@
1
+ /*! SuperDate v0.3.0 | MIT License */
2
+ (function (global, factory) {
3
+ typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
4
+ typeof define === 'function' && define.amd ? define(factory) :
5
+ (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.SuperDate = factory());
6
+ })(this, (function () { 'use strict';
7
+
8
+ /**
9
+ * format.ts — format string parsing, token metadata, and date/time read/write helpers.
10
+ */
11
+ function getInputKind(input) {
12
+ const t = input.type;
13
+ if (t === "time" || t === "datetime-local")
14
+ return t;
15
+ return "date";
16
+ }
17
+ // ─── Format parsing ───────────────────────────────────────────────────────────
18
+ /**
19
+ * Cached regex — created once, reset via lastIndex before each use.
20
+ * yyyy/yy must precede MM/M; HH before H; hh before h; dd before d.
21
+ */
22
+ const TOKEN_RE = /yyyy|yy|YYYY|YY|MM|M|HH|H|hh|h|mm|ss|dd|DD|d|D/g;
23
+ /**
24
+ * Parse a format string into an ordered array of Segment descriptors.
25
+ */
26
+ function parseFormat(format) {
27
+ TOKEN_RE.lastIndex = 0;
28
+ const segments = [];
29
+ let cursor = 0;
30
+ let match;
31
+ while ((match = TOKEN_RE.exec(format)) !== null) {
32
+ if (match.index > cursor) {
33
+ const lit = format.slice(cursor, match.index);
34
+ segments.push({
35
+ token: null,
36
+ text: lit,
37
+ start: cursor,
38
+ end: match.index,
39
+ });
40
+ }
41
+ segments.push({
42
+ token: match[0],
43
+ text: match[0],
44
+ start: match.index,
45
+ end: match.index + match[0].length,
46
+ });
47
+ cursor = match.index + match[0].length;
48
+ }
49
+ if (cursor < format.length) {
50
+ const lit = format.slice(cursor);
51
+ segments.push({
52
+ token: null,
53
+ text: lit,
54
+ start: cursor,
55
+ end: format.length,
56
+ });
57
+ }
58
+ return segments;
59
+ }
60
+ /**
61
+ * Build a combined format string for datetime-local inputs.
62
+ */
63
+ function buildDateTimeFormat(dateFormat, timeFormat, delimiter) {
64
+ return `${dateFormat}${delimiter}${timeFormat}`;
65
+ }
66
+ const TOKEN_META = {
67
+ dd: { maxDigits: 2, maxValue: 31, minValue: 1, placeholder: "dd" },
68
+ d: { maxDigits: 2, maxValue: 31, minValue: 1, placeholder: "d" },
69
+ MM: { maxDigits: 2, maxValue: 12, minValue: 1, placeholder: "mm" },
70
+ M: { maxDigits: 2, maxValue: 12, minValue: 1, placeholder: "m" },
71
+ yyyy: { maxDigits: 4, maxValue: 9999, minValue: 1, placeholder: "yyyy" },
72
+ yy: { maxDigits: 2, maxValue: 99, minValue: 0, placeholder: "yy" },
73
+ HH: { maxDigits: 2, maxValue: 23, minValue: 0, placeholder: "HH" },
74
+ H: { maxDigits: 2, maxValue: 23, minValue: 0, placeholder: "H" },
75
+ hh: { maxDigits: 2, maxValue: 12, minValue: 1, placeholder: "hh" },
76
+ h: { maxDigits: 2, maxValue: 12, minValue: 1, placeholder: "h" },
77
+ mm: { maxDigits: 2, maxValue: 59, minValue: 0, placeholder: "mm" },
78
+ ss: { maxDigits: 2, maxValue: 59, minValue: 0, placeholder: "ss" },
79
+ };
80
+ function tokenMaxDigits(token) {
81
+ return TOKEN_META[token].maxDigits;
82
+ }
83
+ function tokenMaxValue(token) {
84
+ return TOKEN_META[token].maxValue;
85
+ }
86
+ function tokenMinValue(token) {
87
+ return TOKEN_META[token].minValue;
88
+ }
89
+ function tokenPlaceholder(token) {
90
+ return TOKEN_META[token].placeholder;
91
+ }
92
+ // ─── Token value from Date ────────────────────────────────────────────────────
93
+ function tokenValue(token, date) {
94
+ if (!date)
95
+ return "";
96
+ switch (token) {
97
+ case "dd":
98
+ return pad(date.getDate(), 2);
99
+ case "d":
100
+ return String(date.getDate());
101
+ case "MM":
102
+ return pad(date.getMonth() + 1, 2);
103
+ case "M":
104
+ return String(date.getMonth() + 1);
105
+ case "yyyy":
106
+ return pad(date.getFullYear(), 4);
107
+ case "yy":
108
+ return pad(date.getFullYear() % 100, 2);
109
+ case "HH":
110
+ return pad(date.getHours(), 2);
111
+ case "H":
112
+ return String(date.getHours());
113
+ case "hh":
114
+ return pad(to12h(date.getHours()), 2);
115
+ case "h":
116
+ return String(to12h(date.getHours()));
117
+ case "mm":
118
+ return pad(date.getMinutes(), 2);
119
+ case "ss":
120
+ return pad(date.getSeconds(), 2);
121
+ }
122
+ }
123
+ // ─── Token current value from Date (numeric) ─────────────────────────────────
124
+ /**
125
+ * Return the numeric value a token would display for the given date.
126
+ * Used by verifySegment and stepValue.
127
+ */
128
+ function tokenCurrentValue(token, date) {
129
+ switch (token) {
130
+ case "dd":
131
+ case "d":
132
+ return date.getDate();
133
+ case "MM":
134
+ case "M":
135
+ return date.getMonth() + 1;
136
+ case "yyyy":
137
+ return date.getFullYear();
138
+ case "yy":
139
+ return date.getFullYear() % 100;
140
+ case "HH":
141
+ case "H":
142
+ return date.getHours();
143
+ case "hh":
144
+ case "h":
145
+ return to12h(date.getHours());
146
+ case "mm":
147
+ return date.getMinutes();
148
+ case "ss":
149
+ return date.getSeconds();
150
+ }
151
+ }
152
+ // ─── Date/time construction ───────────────────────────────────────────────────
153
+ /** Clamp a year/month/day combination to a real calendar date. */
154
+ function buildDate(current, token, newRaw) {
155
+ const base = current ?? new Date();
156
+ let y = base.getFullYear();
157
+ let mo = base.getMonth() + 1;
158
+ let d = base.getDate();
159
+ let h = base.getHours();
160
+ let mi = base.getMinutes();
161
+ let s = base.getSeconds();
162
+ switch (token) {
163
+ case "dd":
164
+ case "d":
165
+ d = newRaw;
166
+ break;
167
+ case "MM":
168
+ case "M":
169
+ mo = newRaw;
170
+ break;
171
+ case "yyyy":
172
+ y = newRaw;
173
+ break;
174
+ case "yy":
175
+ y = 2000 + newRaw;
176
+ break;
177
+ case "HH":
178
+ case "H":
179
+ h = newRaw;
180
+ break;
181
+ case "hh":
182
+ case "h":
183
+ h = from12h(newRaw, h);
184
+ break;
185
+ case "mm":
186
+ mi = newRaw;
187
+ break;
188
+ case "ss":
189
+ s = newRaw;
190
+ break;
191
+ }
192
+ // Clamp day to valid range for the given month/year
193
+ const maxDay = new Date(y, mo, 0).getDate();
194
+ d = Math.max(1, Math.min(d, maxDay));
195
+ return new Date(y, mo - 1, d, h, mi, s);
196
+ }
197
+ // ─── Input read/write ─────────────────────────────────────────────────────────
198
+ /** Read a Date from input. Supports date, time, and datetime-local inputs. */
199
+ function readInputDate(input) {
200
+ if (!input.value)
201
+ return null;
202
+ const kind = getInputKind(input);
203
+ if (kind === "date")
204
+ return parseDateISO(input.value);
205
+ if (kind === "time")
206
+ return parseTimeISO(input.value);
207
+ return parseDateTimeISO(input.value);
208
+ }
209
+ /** Write a Date back as ISO string appropriate for the input type, and fire native events. */
210
+ function writeInputDate(input, date) {
211
+ const kind = getInputKind(input);
212
+ input.value =
213
+ kind === "date"
214
+ ? formatDateISO(date)
215
+ : kind === "time"
216
+ ? formatTimeISO(date)
217
+ : formatDateTimeISO(date);
218
+ input.dispatchEvent(new Event("input", { bubbles: true }));
219
+ input.dispatchEvent(new Event("change", { bubbles: true }));
220
+ }
221
+ // ─── ISO format helpers ───────────────────────────────────────────────────────
222
+ function parseDateISO(value) {
223
+ const parts = value.split("-");
224
+ if (parts.length !== 3)
225
+ return null;
226
+ const [y, m, d] = parts.map(Number);
227
+ if (isNaN(y) || isNaN(m) || isNaN(d))
228
+ return null;
229
+ return new Date(y, m - 1, d);
230
+ }
231
+ function parseTimeISO(value) {
232
+ const parts = value.split(":");
233
+ if (parts.length < 2)
234
+ return null;
235
+ const [h, mi, s = 0] = parts.map(Number);
236
+ if (isNaN(h) || isNaN(mi))
237
+ return null;
238
+ const now = new Date();
239
+ return new Date(now.getFullYear(), now.getMonth(), now.getDate(), h, mi, s);
240
+ }
241
+ function parseDateTimeISO(value) {
242
+ const tIdx = value.indexOf("T");
243
+ if (tIdx === -1)
244
+ return null;
245
+ const dateOnly = parseDateISO(value.slice(0, tIdx));
246
+ if (!dateOnly)
247
+ return null;
248
+ const timeParts = value
249
+ .slice(tIdx + 1)
250
+ .split(":")
251
+ .map(Number);
252
+ const [h = 0, mi = 0, s = 0] = timeParts;
253
+ return new Date(dateOnly.getFullYear(), dateOnly.getMonth(), dateOnly.getDate(), h, mi, s);
254
+ }
255
+ function formatDateISO(date) {
256
+ return `${date.getFullYear()}-${pad(date.getMonth() + 1, 2)}-${pad(date.getDate(), 2)}`;
257
+ }
258
+ function formatTimeISO(date) {
259
+ return `${pad(date.getHours(), 2)}:${pad(date.getMinutes(), 2)}:${pad(date.getSeconds(), 2)}`;
260
+ }
261
+ function formatDateTimeISO(date) {
262
+ return `${formatDateISO(date)}T${formatTimeISO(date)}`;
263
+ }
264
+ // ─── Paste parser ─────────────────────────────────────────────────────────────
265
+ const PASTE_DATE_FALLBACKS = [
266
+ "yyyy-MM-dd",
267
+ "dd/MM/yyyy",
268
+ "MM/dd/yyyy",
269
+ "d.M.yyyy",
270
+ "MM-dd-yyyy",
271
+ ];
272
+ const PASTE_TIME_FALLBACKS = ["HH:mm:ss", "HH:mm", "H:mm"];
273
+ const PASTE_DATETIME_FALLBACKS = [
274
+ "yyyy-MM-dd HH:mm:ss",
275
+ "yyyy-MM-dd HH:mm",
276
+ "dd/MM/yyyy HH:mm",
277
+ ];
278
+ function parsePastedDate(text, preferredFormat, kind = "date") {
279
+ const fallbacks = kind === "time"
280
+ ? PASTE_TIME_FALLBACKS
281
+ : kind === "datetime-local"
282
+ ? PASTE_DATETIME_FALLBACKS
283
+ : PASTE_DATE_FALLBACKS;
284
+ const formats = [
285
+ preferredFormat,
286
+ ...fallbacks.filter((f) => f !== preferredFormat),
287
+ ];
288
+ for (const fmt of formats) {
289
+ const d = parseWithFormat(text, fmt, kind);
290
+ if (d)
291
+ return d;
292
+ }
293
+ return null;
294
+ }
295
+ function parseWithFormat(text, format, kind) {
296
+ const segs = parseFormat(format);
297
+ let pos = 0;
298
+ const now = new Date();
299
+ let d = now.getDate(), mo = now.getMonth() + 1, y = now.getFullYear();
300
+ let h = 0, mi = 0, s = 0;
301
+ for (const seg of segs) {
302
+ if (pos >= text.length)
303
+ break;
304
+ if (!seg.token) {
305
+ if (text.slice(pos, pos + seg.text.length) === seg.text)
306
+ pos += seg.text.length;
307
+ continue;
308
+ }
309
+ const maxDigits = tokenMaxDigits(seg.token);
310
+ let chunk = "";
311
+ for (let i = 0; i < maxDigits && pos < text.length; i++, pos++) {
312
+ if (!/\d/.test(text[pos]))
313
+ break;
314
+ chunk += text[pos];
315
+ }
316
+ if (!chunk)
317
+ return null;
318
+ const num = parseInt(chunk, 10);
319
+ switch (seg.token) {
320
+ case "dd":
321
+ case "d":
322
+ d = num;
323
+ break;
324
+ case "MM":
325
+ case "M":
326
+ mo = num;
327
+ break;
328
+ case "yyyy":
329
+ y = num;
330
+ break;
331
+ case "yy":
332
+ y = 2000 + num;
333
+ break;
334
+ case "HH":
335
+ case "H":
336
+ h = num;
337
+ break;
338
+ case "hh":
339
+ case "h":
340
+ h = from12h(num, h);
341
+ break;
342
+ case "mm":
343
+ mi = num;
344
+ break;
345
+ case "ss":
346
+ s = num;
347
+ break;
348
+ }
349
+ }
350
+ if (kind === "date" || kind === "datetime-local") {
351
+ if (mo < 1 || mo > 12 || d < 1 || d > 31)
352
+ return null;
353
+ }
354
+ if (kind === "time") {
355
+ if (h < 0 || h > 23 || mi < 0 || mi > 59)
356
+ return null;
357
+ return new Date(now.getFullYear(), now.getMonth(), now.getDate(), h, mi, s);
358
+ }
359
+ return new Date(y, mo - 1, d, h, mi, s);
360
+ }
361
+ // ─── Utility ──────────────────────────────────────────────────────────────────
362
+ function pad(n, len) {
363
+ return String(n).padStart(len, "0");
364
+ }
365
+ function to12h(h24) {
366
+ if (h24 === 0)
367
+ return 12;
368
+ if (h24 > 12)
369
+ return h24 - 12;
370
+ return h24;
371
+ }
372
+ function from12h(h12, currentH24) {
373
+ const isPM = currentH24 >= 12;
374
+ if (h12 === 12)
375
+ return isPM ? 12 : 0;
376
+ return isPM ? h12 + 12 : h12;
377
+ }
378
+
379
+ /**
380
+ * overlay.ts — builds and updates the visual overlay that sits on top of
381
+ * the hidden native date/time input.
382
+ */
383
+ const CALENDAR_ICON_SVG = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none"
384
+ stroke="currentColor" stroke-width="2" stroke-linecap="round"
385
+ stroke-linejoin="round" aria-hidden="true">
386
+ <rect x="3" y="4" width="18" height="18" rx="2"/>
387
+ <line x1="16" y1="2" x2="16" y2="6"/>
388
+ <line x1="8" y1="2" x2="8" y2="6"/>
389
+ <line x1="3" y1="10" x2="21" y2="10"/>
390
+ </svg>`;
391
+ const CLOCK_ICON_SVG = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none"
392
+ stroke="currentColor" stroke-width="2" stroke-linecap="round"
393
+ stroke-linejoin="round" aria-hidden="true">
394
+ <circle cx="12" cy="12" r="10"/>
395
+ <polyline points="12 6 12 12 16 14"/>
396
+ </svg>`;
397
+ /**
398
+ * Wrap `input` in a `.superdate-wrapper`, inject the overlay element and
399
+ * segment spans. Returns references to every created element.
400
+ */
401
+ function buildOverlay(input, segments, onSegmentClick, onIconClick, kind = 'date') {
402
+ // ── Wrapper ─────────────────────────────────────────────────────────────────
403
+ const wrapper = document.createElement('div');
404
+ wrapper.className = 'superdate-wrapper';
405
+ wrapper.style.width = input.style.width || '';
406
+ input.parentNode.insertBefore(wrapper, input);
407
+ wrapper.appendChild(input);
408
+ input.classList.add('superdate-input');
409
+ input.style.display = 'block';
410
+ input.style.boxSizing = 'border-box';
411
+ // ── Overlay ──────────────────────────────────────────────────────────────────
412
+ const overlay = document.createElement('div');
413
+ overlay.className = 'superdate-overlay';
414
+ const segEls = [];
415
+ segments.forEach((seg, i) => {
416
+ const el = document.createElement('span');
417
+ el.className = 'superdate-seg';
418
+ if (seg.token) {
419
+ el.dataset.token = seg.token;
420
+ el.dataset.idx = String(i);
421
+ el.setAttribute('tabindex', '-1');
422
+ el.addEventListener('click', (e) => {
423
+ e.stopPropagation();
424
+ onSegmentClick(i);
425
+ });
426
+ }
427
+ overlay.appendChild(el);
428
+ segEls.push(el);
429
+ });
430
+ // ── Icon (calendar for date/datetime-local, clock for time) ─────────────────
431
+ const icon = document.createElement('span');
432
+ icon.className = 'superdate-icon';
433
+ icon.innerHTML = kind === 'time' ? CLOCK_ICON_SVG : CALENDAR_ICON_SVG;
434
+ icon.addEventListener('click', (e) => {
435
+ e.stopPropagation();
436
+ onIconClick();
437
+ });
438
+ overlay.appendChild(icon);
439
+ wrapper.appendChild(overlay);
440
+ return { wrapper, overlay, segEls };
441
+ }
442
+ /**
443
+ * Re-render all segment spans from the current date/time value.
444
+ */
445
+ function renderSegments(segments, segEls, date) {
446
+ segments.forEach((seg, i) => {
447
+ const el = segEls[i];
448
+ if (!seg.token) {
449
+ el.textContent = seg.text;
450
+ return;
451
+ }
452
+ const val = tokenValue(seg.token, date);
453
+ if (val) {
454
+ el.textContent = val;
455
+ el.classList.remove('empty');
456
+ }
457
+ else {
458
+ el.textContent = tokenPlaceholder(seg.token);
459
+ el.classList.add('empty');
460
+ }
461
+ });
462
+ }
463
+ /**
464
+ * Mark one segment element as active (highlighted), deactivate all others.
465
+ */
466
+ function activateSegmentEl(segEls, idx) {
467
+ segEls.forEach((el, i) => el.classList.toggle('active', i === idx));
468
+ }
469
+ /** Remove active highlight from all segment elements. */
470
+ function deactivateAll(segEls) {
471
+ segEls.forEach(el => el.classList.remove('active'));
472
+ }
473
+
474
+ /**
475
+ * instance.ts — SuperDateInstance manages one enhanced date/time input:
476
+ * overlay rendering, keyboard/mouse/paste interaction, and lifecycle.
477
+ * Supports input[type="date"], input[type="time"], and input[type="datetime-local"].
478
+ */
479
+ const INSTANCE_KEY = '__superdate__';
480
+ class SuperDateInstance {
481
+ constructor(input, globalDateFormat, globalTimeFormat = 'HH:mm', globalDelimiter = ' ') {
482
+ this.activeTokenIdx = -1;
483
+ this.typingBuffer = '';
484
+ // ── Selection state ────────────────────────────────────────────────────────
485
+ this.selAnchor = -1;
486
+ this.selEnd = -1;
487
+ this._justDragged = false;
488
+ this._destroyed = false;
489
+ this.input = input;
490
+ this.kind = getInputKind(input);
491
+ const dateFormat = input.dataset.dateFormat ?? globalDateFormat;
492
+ const timeFormat = input.dataset.timeFormat ?? globalTimeFormat;
493
+ const delimiter = input.dataset.dateTimeDelimiter ?? globalDelimiter;
494
+ this.format =
495
+ this.kind === 'datetime-local' ? buildDateTimeFormat(dateFormat, timeFormat, delimiter) :
496
+ this.kind === 'time' ? timeFormat :
497
+ dateFormat;
498
+ this.segments = parseFormat(this.format);
499
+ const elements = buildOverlay(input, this.segments, (idx) => this.activateSegment(idx), () => this.input.showPicker?.(), this.kind);
500
+ this.wrapper = elements.wrapper;
501
+ this.overlay = elements.overlay;
502
+ this.segEls = elements.segEls;
503
+ this.render();
504
+ this._onKeyDown = (e) => this.handleKeyDown(e);
505
+ this._onPaste = (e) => this.handlePaste(e);
506
+ this._onChange = () => this.render();
507
+ this._onBlur = (e) => this.handleBlur(e);
508
+ this._onFocus = (e) => this.handleFocus(e);
509
+ this._onWrapClick = (e) => this.handleWrapperClick(e);
510
+ this._onWrapDblClick = (e) => this.handleWrapperDblClick(e);
511
+ this._onMouseDown = (e) => this.handleMouseDown(e);
512
+ this._onMouseMove = (e) => this.handleMouseMove(e);
513
+ this._onMouseUp = (e) => this.handleMouseUp(e);
514
+ this._mutObs = new MutationObserver(() => { if (!this._destroyed)
515
+ this.render(); });
516
+ this.attachEvents();
517
+ }
518
+ // ── Rendering ───────────────────────────────────────────────────────────────
519
+ render() {
520
+ renderSegments(this.segments, this.segEls, readInputDate(this.input));
521
+ }
522
+ // ── Helpers ──────────────────────────────────────────────────────────────────
523
+ firstTokenIdx() {
524
+ return this.segments.findIndex(s => s.token !== null);
525
+ }
526
+ lastTokenIdx() {
527
+ for (let i = this.segments.length - 1; i >= 0; i--) {
528
+ if (this.segments[i].token)
529
+ return i;
530
+ }
531
+ return -1;
532
+ }
533
+ nextTokenIdx(from, dir = 1) {
534
+ let i = from + dir;
535
+ while (i >= 0 && i < this.segments.length) {
536
+ if (this.segments[i].token)
537
+ return i;
538
+ i += dir;
539
+ }
540
+ return -1;
541
+ }
542
+ // ── Segment activation ───────────────────────────────────────────────────────
543
+ /**
544
+ * Validate and normalise the buffer for the given segment index.
545
+ * - Empty buffer ("0" or no input) → fills with today's value.
546
+ * - Partial numeric buffer → left-pads to maxDigits.
547
+ * - Year buffer shorter than 4 digits → prepend century from today.
548
+ */
549
+ verifySegment(idx) {
550
+ const token = this.segments[idx].token;
551
+ if (!token)
552
+ return;
553
+ let bufferVal = this.getBufferValue(idx);
554
+ const maxDigits = tokenMaxDigits(token);
555
+ const nowValue = pad(tokenCurrentValue(token, new Date()), maxDigits);
556
+ if (bufferVal === '0') {
557
+ this.typingBuffer = '';
558
+ this.typeDigit(token, nowValue, true);
559
+ }
560
+ else if (bufferVal !== '') {
561
+ if (maxDigits <= 2) {
562
+ if (bufferVal.length < maxDigits) {
563
+ bufferVal = bufferVal.padStart(maxDigits, '0');
564
+ this.typingBuffer = '';
565
+ this.typeDigit(token, bufferVal, true);
566
+ }
567
+ }
568
+ else {
569
+ // Year: prepend leading digits from today
570
+ this.typingBuffer = '';
571
+ const needle = maxDigits - bufferVal.length;
572
+ bufferVal = `${nowValue.substring(0, needle)}${bufferVal}`;
573
+ this.typeDigit(token, bufferVal, true);
574
+ }
575
+ }
576
+ }
577
+ activateSegment(idx) {
578
+ if (idx < 0 || idx >= this.segments.length)
579
+ return;
580
+ if (!this.segments[idx].token)
581
+ return;
582
+ this.endSelection();
583
+ this.typingBuffer = '';
584
+ this.activeTokenIdx = idx;
585
+ activateSegmentEl(this.segEls, idx);
586
+ this.input.focus({ preventScroll: true });
587
+ }
588
+ deactivate() {
589
+ if (this.activeTokenIdx !== -1) {
590
+ const token = this.segments[this.activeTokenIdx].token;
591
+ if (token) {
592
+ this.verifySegment(this.activeTokenIdx);
593
+ const date = readInputDate(this.input);
594
+ if (date) {
595
+ const current = tokenCurrentValue(token, date);
596
+ if (current < tokenMinValue(token)) {
597
+ this.commitTokenValue(token, tokenMinValue(token));
598
+ }
599
+ }
600
+ }
601
+ }
602
+ this.activeTokenIdx = -1;
603
+ this.typingBuffer = '';
604
+ this.endSelection();
605
+ deactivateAll(this.segEls);
606
+ }
607
+ // ── Selection via active class ────────────────────────────────────────────
608
+ hasSelection() {
609
+ return this.selAnchor !== -1;
610
+ }
611
+ paintSelection(anchor, end) {
612
+ const from = Math.min(anchor, end);
613
+ const to = Math.max(anchor, end);
614
+ this.segEls.forEach((el, i) => {
615
+ el.classList.toggle('active', this.segments[i].token !== null && i >= from && i <= to);
616
+ });
617
+ }
618
+ extendSelectionTo(idx) {
619
+ this.selEnd = idx;
620
+ this.paintSelection(this.selAnchor, this.selEnd);
621
+ }
622
+ selectAll() {
623
+ const first = this.firstTokenIdx();
624
+ const last = this.lastTokenIdx();
625
+ if (first === -1)
626
+ return;
627
+ this.activeTokenIdx = -1;
628
+ this.selAnchor = first;
629
+ this.selEnd = last;
630
+ this.paintSelection(first, last);
631
+ }
632
+ endSelection() {
633
+ if (this.activeTokenIdx !== -1) {
634
+ this.verifySegment(this.activeTokenIdx);
635
+ }
636
+ this.selAnchor = -1;
637
+ this.selEnd = -1;
638
+ deactivateAll(this.segEls);
639
+ }
640
+ // ── Copy / delete selected range ─────────────────────────────────────────
641
+ copySelection() {
642
+ const date = readInputDate(this.input);
643
+ if (!date || !this.hasSelection())
644
+ return;
645
+ const from = Math.min(this.selAnchor, this.selEnd);
646
+ const to = Math.max(this.selAnchor, this.selEnd);
647
+ let text = '';
648
+ for (let i = from; i <= to; i++) {
649
+ const seg = this.segments[i];
650
+ if (!seg)
651
+ continue;
652
+ text += seg.token ? tokenValue(seg.token, date) : seg.text;
653
+ }
654
+ navigator.clipboard.writeText(text).catch(() => {
655
+ const ta = document.createElement('textarea');
656
+ ta.value = text;
657
+ ta.style.cssText = 'position:fixed;opacity:0;pointer-events:none';
658
+ document.body.appendChild(ta);
659
+ ta.select();
660
+ document.execCommand('copy');
661
+ document.body.removeChild(ta);
662
+ });
663
+ }
664
+ deleteSelection() {
665
+ // Check if every token segment is already empty
666
+ const isEmpty = this.segments.every((seg, i) => !seg.token || this.getBufferValue(i) === '');
667
+ if (isEmpty) {
668
+ this.input.value = '';
669
+ this.input.dispatchEvent(new Event('input', { bubbles: true }));
670
+ this.input.dispatchEvent(new Event('change', { bubbles: true }));
671
+ this.render();
672
+ this.activeTokenIdx = -1;
673
+ }
674
+ }
675
+ // ── Mouse drag ───────────────────────────────────────────────────────────
676
+ tokenIdxFromEvent(e) {
677
+ const el = e.target.closest('[data-idx]');
678
+ if (!el)
679
+ return -1;
680
+ const idx = parseInt(el.dataset.idx ?? '-1', 10);
681
+ return this.segments[idx]?.token ? idx : -1;
682
+ }
683
+ handleMouseDown(e) {
684
+ if (e.detail >= 2)
685
+ return;
686
+ const idx = this.tokenIdxFromEvent(e);
687
+ if (idx === -1)
688
+ return;
689
+ this.selAnchor = idx;
690
+ this.selEnd = idx;
691
+ }
692
+ handleMouseMove(e) {
693
+ if (this.selAnchor === -1 || !(e.buttons & 1))
694
+ return;
695
+ const idx = this.tokenIdxFromEvent(e);
696
+ if (idx === -1 || idx === this.selEnd)
697
+ return;
698
+ if (this.activeTokenIdx !== -1) {
699
+ this.activeTokenIdx = -1;
700
+ this.typingBuffer = '';
701
+ }
702
+ this.extendSelectionTo(idx);
703
+ }
704
+ handleMouseUp(_e) {
705
+ if (this.selAnchor !== -1 && this.selAnchor === this.selEnd) {
706
+ const idx = this.selAnchor;
707
+ this.selAnchor = -1;
708
+ this.selEnd = -1;
709
+ this.activateSegment(idx);
710
+ }
711
+ else if (this.selAnchor !== this.selEnd) {
712
+ this._justDragged = true;
713
+ this.input.focus({ preventScroll: true });
714
+ }
715
+ }
716
+ // ── Double-click ─────────────────────────────────────────────────────────
717
+ handleWrapperDblClick(e) {
718
+ e.preventDefault();
719
+ this.activeTokenIdx = -1;
720
+ this.typingBuffer = '';
721
+ this.selectAll();
722
+ this.input.focus({ preventScroll: true });
723
+ }
724
+ // ── Blur / Focus / wrapper click ─────────────────────────────────────────────────
725
+ handleBlur(e) {
726
+ const related = e.relatedTarget;
727
+ if (!related || !this.wrapper.contains(related)) {
728
+ this.deactivate();
729
+ }
730
+ }
731
+ handleFocus(e) {
732
+ this.activateSegment(0);
733
+ }
734
+ handleWrapperClick(e) {
735
+ if (this._justDragged) {
736
+ this._justDragged = false;
737
+ return;
738
+ }
739
+ const target = e.target;
740
+ if (target === this.wrapper || target === this.input || target === this.overlay) {
741
+ this.endSelection();
742
+ this.activateSegment(this.firstTokenIdx());
743
+ }
744
+ }
745
+ // ── Keyboard ──────────────────────────────────────────────────────────────
746
+ handleKeyDown(e) {
747
+ const mod = e.ctrlKey || e.metaKey;
748
+ if (mod && e.key === 'a') {
749
+ e.preventDefault();
750
+ this.selectAll();
751
+ return;
752
+ }
753
+ if (mod && e.key === 'c') {
754
+ if (this.hasSelection()) {
755
+ e.preventDefault();
756
+ this.copySelection();
757
+ }
758
+ return;
759
+ }
760
+ if (e.key === 'Escape') {
761
+ this.hasSelection() ? this.endSelection() : this.deactivate();
762
+ return;
763
+ }
764
+ if (e.key === 'Backspace' || e.key === 'Delete') {
765
+ e.preventDefault();
766
+ this.typingBuffer = '';
767
+ let selAnchor = -1;
768
+ if (this.hasSelection()) {
769
+ selAnchor = this.selAnchor;
770
+ for (let i = selAnchor; i <= this.selEnd; i++) {
771
+ if (this.segments[i]?.token) {
772
+ this.renderBuffer(i);
773
+ }
774
+ }
775
+ this.endSelection();
776
+ }
777
+ else if (this.activeTokenIdx !== -1) {
778
+ if (this.getBufferValue(this.activeTokenIdx) === '')
779
+ selAnchor = this.activeTokenIdx;
780
+ this.renderBuffer();
781
+ }
782
+ if (selAnchor > 0) {
783
+ let s = selAnchor - 1;
784
+ while (s >= 0) {
785
+ if (this.segments[s]?.token) {
786
+ this.activateSegment(s);
787
+ break;
788
+ }
789
+ s--;
790
+ }
791
+ }
792
+ else if (selAnchor == 0) {
793
+ this.activateSegment(selAnchor);
794
+ }
795
+ this.deleteSelection();
796
+ return;
797
+ }
798
+ if (this.activeTokenIdx === -1)
799
+ return;
800
+ const token = this.segments[this.activeTokenIdx].token;
801
+ switch (e.key) {
802
+ case 'Tab':
803
+ this.handleTab(e);
804
+ break;
805
+ case 'ArrowRight': {
806
+ e.preventDefault();
807
+ const n = this.nextTokenIdx(this.activeTokenIdx, 1);
808
+ if (n !== -1)
809
+ this.activateSegment(n);
810
+ break;
811
+ }
812
+ case 'ArrowLeft': {
813
+ e.preventDefault();
814
+ const p = this.nextTokenIdx(this.activeTokenIdx, -1);
815
+ if (p !== -1)
816
+ this.activateSegment(p);
817
+ break;
818
+ }
819
+ case 'ArrowUp':
820
+ e.preventDefault();
821
+ this.stepValue(token, 1);
822
+ break;
823
+ case 'ArrowDown':
824
+ e.preventDefault();
825
+ this.stepValue(token, -1);
826
+ break;
827
+ default:
828
+ if (/^\d$/.test(e.key)) {
829
+ e.preventDefault();
830
+ this.typeDigit(token, e.key);
831
+ }
832
+ break;
833
+ }
834
+ }
835
+ handleTab(e) {
836
+ const next = this.nextTokenIdx(this.activeTokenIdx, e.shiftKey ? -1 : 1);
837
+ if (next !== -1) {
838
+ e.preventDefault();
839
+ this.activateSegment(next);
840
+ }
841
+ else {
842
+ this.deactivate();
843
+ }
844
+ }
845
+ // ── Digit typing ──────────────────────────────────────────────────────────
846
+ typeDigit(token, digit, skipNext = false) {
847
+ this.typingBuffer += digit;
848
+ const num = parseInt(this.typingBuffer, 10);
849
+ const maxVal = tokenMaxValue(token);
850
+ const maxDigit = tokenMaxDigits(token);
851
+ if (this.typingBuffer.length >= maxDigit) {
852
+ const minVal = tokenMinValue(token);
853
+ const clamped = Math.max(minVal, Math.min(maxVal, num));
854
+ this.commitTokenValue(token, clamped);
855
+ this.typingBuffer = '';
856
+ if (!skipNext) {
857
+ const next = this.nextTokenIdx(this.activeTokenIdx, 1);
858
+ if (next !== -1)
859
+ setTimeout(() => this.activateSegment(next), 0);
860
+ }
861
+ }
862
+ else {
863
+ this.renderBuffer();
864
+ }
865
+ }
866
+ renderBuffer(activeTokenIdx = this.activeTokenIdx) {
867
+ const el = this.segEls[activeTokenIdx];
868
+ const token = this.segments[activeTokenIdx].token;
869
+ if (this.typingBuffer) {
870
+ el.textContent = this.typingBuffer;
871
+ el.classList.remove('empty');
872
+ }
873
+ else {
874
+ el.textContent = tokenPlaceholder(token);
875
+ el.classList.add('empty');
876
+ }
877
+ }
878
+ getBufferValue(activeTokenIdx) {
879
+ if (this.typingBuffer)
880
+ return this.typingBuffer;
881
+ const el = this.segEls[activeTokenIdx];
882
+ return el.classList.contains('empty') ? '' : (el.textContent ?? '');
883
+ }
884
+ // ── Step (↑ ↓) ────────────────────────────────────────────────────────────
885
+ stepValue(token, delta) {
886
+ const date = readInputDate(this.input);
887
+ const base = date ?? new Date();
888
+ const current = tokenCurrentValue(token, base);
889
+ const minV = tokenMinValue(token);
890
+ const maxV = tokenMaxValue(token);
891
+ let next = current + delta;
892
+ if (next < minV)
893
+ next = maxV;
894
+ if (next > maxV)
895
+ next = minV;
896
+ this.commitTokenValue(token, next);
897
+ }
898
+ commitTokenValue(token, value) {
899
+ const date = buildDate(readInputDate(this.input), token, value);
900
+ writeInputDate(this.input, date);
901
+ this.render();
902
+ }
903
+ // ── Paste ─────────────────────────────────────────────────────────────────
904
+ handlePaste(e) {
905
+ e.preventDefault();
906
+ const text = e.clipboardData?.getData('text/plain') ?? '';
907
+ const date = parsePastedDate(text.trim(), this.format, this.kind);
908
+ if (date) {
909
+ writeInputDate(this.input, date);
910
+ this.render();
911
+ }
912
+ }
913
+ // ── Event wiring ──────────────────────────────────────────────────────────
914
+ attachEvents() {
915
+ this.input.addEventListener('keydown', this._onKeyDown);
916
+ this.input.addEventListener('paste', this._onPaste);
917
+ this.input.addEventListener('change', this._onChange);
918
+ this.input.addEventListener('blur', this._onBlur);
919
+ this.input.addEventListener('focus', this._onFocus);
920
+ this.wrapper.addEventListener('click', this._onWrapClick);
921
+ this.wrapper.addEventListener('dblclick', this._onWrapDblClick);
922
+ this.overlay.addEventListener('mousedown', this._onMouseDown);
923
+ this.overlay.addEventListener('mousemove', this._onMouseMove);
924
+ this.overlay.addEventListener('mouseup', this._onMouseUp);
925
+ this._mutObs.observe(this.input, {
926
+ attributes: true,
927
+ attributeFilter: ['value', 'min', 'max'],
928
+ });
929
+ }
930
+ // ── Public API ────────────────────────────────────────────────────────────
931
+ update() {
932
+ this.render();
933
+ }
934
+ destroy() {
935
+ if (this._destroyed)
936
+ return;
937
+ this._destroyed = true;
938
+ this._mutObs.disconnect();
939
+ this.input.removeEventListener('keydown', this._onKeyDown);
940
+ this.input.removeEventListener('paste', this._onPaste);
941
+ this.input.removeEventListener('change', this._onChange);
942
+ this.input.removeEventListener('blur', this._onBlur);
943
+ this.wrapper.removeEventListener('click', this._onWrapClick);
944
+ this.wrapper.removeEventListener('dblclick', this._onWrapDblClick);
945
+ this.overlay.removeEventListener('mousedown', this._onMouseDown);
946
+ this.overlay.removeEventListener('mousemove', this._onMouseMove);
947
+ this.overlay.removeEventListener('mouseup', this._onMouseUp);
948
+ this.input.classList.remove('superdate-input');
949
+ this.input.style.display = '';
950
+ this.input.style.width = '';
951
+ this.input.style.color = '';
952
+ this.wrapper.replaceWith(this.input);
953
+ }
954
+ }
955
+
956
+ /**
957
+ * registry.ts — SuperDateRegistry tracks bound selectors and uses a single
958
+ * MutationObserver to auto-initialise matching inputs added to the DOM later.
959
+ * Supports input[type="date"], input[type="time"], and input[type="datetime-local"].
960
+ */
961
+ let observer = null;
962
+ let bindings = [];
963
+ const DESTROYED_ATTR = 'data-superdate-destroyed';
964
+ const SUPPORTED_TYPES = new Set(['date', 'time', 'datetime-local']);
965
+ // ── Destroyed-marker helpers ──────────────────────────────────────────────────
966
+ function isDestroyed(el) {
967
+ return el.hasAttribute(DESTROYED_ATTR);
968
+ }
969
+ function markDestroyed(el) {
970
+ el.setAttribute(DESTROYED_ATTR, '');
971
+ }
972
+ function clearDestroyedBySelector(selector) {
973
+ document.querySelectorAll(selector).forEach(el => {
974
+ el.removeAttribute(DESTROYED_ATTR);
975
+ });
976
+ }
977
+ // ── Options normalisation ─────────────────────────────────────────────────────
978
+ function defaultOpts(options = {}) {
979
+ return {
980
+ format: options.format ?? 'dd/MM/yyyy',
981
+ timeFormat: options.timeFormat ?? 'HH:mm',
982
+ dateTimeDelimiter: options.dateTimeDelimiter ?? ' ',
983
+ locale: options.locale ?? (typeof navigator !== 'undefined' ? navigator.language : 'en'),
984
+ };
985
+ }
986
+ // ── Initialisation helpers ────────────────────────────────────────────────────
987
+ function createInstance(el, opts) {
988
+ el[INSTANCE_KEY] = new SuperDateInstance(el, opts.format, opts.timeFormat, opts.dateTimeDelimiter);
989
+ }
990
+ function canInit(el) {
991
+ return SUPPORTED_TYPES.has(el.type) && !el[INSTANCE_KEY] && !isDestroyed(el);
992
+ }
993
+ function initAll(selector, opts) {
994
+ document.querySelectorAll(selector).forEach(el => {
995
+ if (canInit(el))
996
+ createInstance(el, opts);
997
+ });
998
+ }
999
+ /**
1000
+ * Try to initialise any input in `node` (or node itself) against all
1001
+ * registered bindings. Called from the MutationObserver callback.
1002
+ */
1003
+ function tryInit(node) {
1004
+ for (const binding of bindings) {
1005
+ // Direct match
1006
+ if (node instanceof HTMLInputElement && node.matches(binding.selector) && canInit(node)) {
1007
+ createInstance(node, binding.options);
1008
+ }
1009
+ // Descendant matches
1010
+ node.querySelectorAll(binding.selector).forEach(el => {
1011
+ if (canInit(el))
1012
+ createInstance(el, binding.options);
1013
+ });
1014
+ }
1015
+ }
1016
+ function startObserver() {
1017
+ observer = new MutationObserver(mutations => {
1018
+ for (const mutation of mutations) {
1019
+ for (const node of Array.from(mutation.addedNodes)) {
1020
+ if (node instanceof Element)
1021
+ tryInit(node);
1022
+ }
1023
+ }
1024
+ });
1025
+ observer.observe(document.body, { childList: true, subtree: true });
1026
+ }
1027
+ // ── Public registry ───────────────────────────────────────────────────────────
1028
+ class SuperDateRegistry {
1029
+ constructor() {
1030
+ this.version = '';
1031
+ this.name = '';
1032
+ }
1033
+ /**
1034
+ * Bind SuperDate to all current **and future** matching inputs.
1035
+ * Safe to call multiple times with different selectors.
1036
+ */
1037
+ bind(selector, options = {}) {
1038
+ const opts = defaultOpts(options);
1039
+ bindings.push({ selector, options: opts });
1040
+ clearDestroyedBySelector(selector);
1041
+ initAll(selector, opts);
1042
+ if (!observer)
1043
+ startObserver();
1044
+ return this;
1045
+ }
1046
+ /**
1047
+ * Manually enhance a single element.
1048
+ * Returns the existing instance if one is already attached.
1049
+ */
1050
+ init(el, options = {}) {
1051
+ if (el[INSTANCE_KEY])
1052
+ return el[INSTANCE_KEY];
1053
+ el.removeAttribute(DESTROYED_ATTR);
1054
+ const opts = defaultOpts(options);
1055
+ const instance = new SuperDateInstance(el, opts.format, opts.timeFormat, opts.dateTimeDelimiter);
1056
+ el[INSTANCE_KEY] = instance;
1057
+ return instance;
1058
+ }
1059
+ /**
1060
+ * Remove the enhancement from a single element and mark it with
1061
+ * [data-superdate-destroyed] so the MutationObserver won't re-bind it.
1062
+ */
1063
+ destroy(el) {
1064
+ if (el[INSTANCE_KEY]) {
1065
+ el[INSTANCE_KEY].destroy();
1066
+ delete el[INSTANCE_KEY];
1067
+ }
1068
+ markDestroyed(el);
1069
+ }
1070
+ }
1071
+
1072
+ /**
1073
+ * SuperDate — lightweight TypeScript date-input enhancer.
1074
+ *
1075
+ * Hides the native browser chrome, renders a fully custom overlay,
1076
+ * and supports keyboard editing, copy/paste, and custom date formats.
1077
+ *
1078
+ * Usage:
1079
+ * import SuperDate from 'superdate';
1080
+ * SuperDate.bind('.date-field');
1081
+ * SuperDate.bind('[data-datepicker]', { format: 'MM/dd/yyyy' });
1082
+ */
1083
+ /** Singleton registry — the default export used in most projects. */
1084
+ if (typeof globalThis.GLOBAL_SDATE == "undefined") {
1085
+ globalThis.GLOBAL_SDATE = new SuperDateRegistry();
1086
+ }
1087
+ var SuperDate = globalThis.GLOBAL_SDATE;
1088
+ SuperDate.version = "0.3.0";
1089
+ SuperDate.name = "SuperDate";
1090
+
1091
+ return SuperDate;
1092
+
1093
+ }));
1094
+ //# sourceMappingURL=super-date.umd.js.map