@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.
- package/LICENSE +21 -0
- package/README.md +191 -0
- package/dist/super-date.css +86 -0
- package/dist/super-date.css.map +1 -0
- package/dist/super-date.esm.js +1083 -0
- package/dist/super-date.esm.js.map +1 -0
- package/dist/super-date.esm.min.js +2 -0
- package/dist/super-date.esm.min.js.br +0 -0
- package/dist/super-date.min.css +1 -0
- package/dist/super-date.min.css.br +0 -0
- package/dist/super-date.min.js +2 -0
- package/dist/super-date.min.js.br +0 -0
- package/dist/super-date.umd.js +1094 -0
- package/dist/super-date.umd.js.map +1 -0
- package/package.json +68 -0
- package/src/css/index.css +84 -0
- package/src/ts/core/format.ts +452 -0
- package/src/ts/core/instance.ts +605 -0
- package/src/ts/core/overlay.ts +127 -0
- package/src/ts/core/registry.ts +141 -0
- package/src/ts/global.ts +33 -0
- package/src/ts/index.ts +26 -0
- package/src/ts/types/bind-entry.type.ts +6 -0
- package/src/ts/types/css.d.ts +1 -0
- package/src/ts/types/date-token.type.ts +9 -0
- package/src/ts/types/segment.type.ts +8 -0
- package/src/ts/types/super-date.type.ts +14 -0
|
@@ -0,0 +1,605 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* instance.ts — SuperDateInstance manages one enhanced date/time input:
|
|
3
|
+
* overlay rendering, keyboard/mouse/paste interaction, and lifecycle.
|
|
4
|
+
* Supports input[type="date"], input[type="time"], and input[type="datetime-local"].
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { DateToken } from '../types/date-token.type';
|
|
8
|
+
import { Segment } from '../types/segment.type';
|
|
9
|
+
import {
|
|
10
|
+
parseFormat,
|
|
11
|
+
buildDateTimeFormat,
|
|
12
|
+
getInputKind,
|
|
13
|
+
InputKind,
|
|
14
|
+
isTimeToken,
|
|
15
|
+
tokenMaxDigits,
|
|
16
|
+
tokenMaxValue,
|
|
17
|
+
tokenMinValue,
|
|
18
|
+
tokenValue,
|
|
19
|
+
tokenPlaceholder,
|
|
20
|
+
tokenCurrentValue,
|
|
21
|
+
buildDate,
|
|
22
|
+
readInputDate,
|
|
23
|
+
writeInputDate,
|
|
24
|
+
parsePastedDate,
|
|
25
|
+
pad,
|
|
26
|
+
} from './format';
|
|
27
|
+
import {
|
|
28
|
+
buildOverlay,
|
|
29
|
+
renderSegments,
|
|
30
|
+
activateSegmentEl,
|
|
31
|
+
deactivateAll,
|
|
32
|
+
} from './overlay';
|
|
33
|
+
|
|
34
|
+
export const INSTANCE_KEY = '__superdate__';
|
|
35
|
+
|
|
36
|
+
declare global {
|
|
37
|
+
interface HTMLInputElement {
|
|
38
|
+
[INSTANCE_KEY]?: SuperDateInstance;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export class SuperDateInstance {
|
|
43
|
+
private input: HTMLInputElement;
|
|
44
|
+
private kind: InputKind;
|
|
45
|
+
|
|
46
|
+
/** Combined display format (for datetime-local: "dd/MM/yyyy HH:mm") */
|
|
47
|
+
private format: string;
|
|
48
|
+
private segments: Segment[];
|
|
49
|
+
|
|
50
|
+
private wrapper: HTMLElement;
|
|
51
|
+
private overlay: HTMLElement;
|
|
52
|
+
private segEls: HTMLElement[];
|
|
53
|
+
|
|
54
|
+
private activeTokenIdx: number = -1;
|
|
55
|
+
private typingBuffer: string = '';
|
|
56
|
+
|
|
57
|
+
// ── Selection state ────────────────────────────────────────────────────────
|
|
58
|
+
private selAnchor: number = -1;
|
|
59
|
+
private selEnd: number = -1;
|
|
60
|
+
private _justDragged = false;
|
|
61
|
+
|
|
62
|
+
// ── Lifecycle ──────────────────────────────────────────────────────────────
|
|
63
|
+
private _onKeyDown: (e: Event) => void;
|
|
64
|
+
private _onPaste: (e: Event) => void;
|
|
65
|
+
private _onChange: () => void;
|
|
66
|
+
private _onBlur: (e: Event) => void;
|
|
67
|
+
private _onFocus: (e: Event) => void;
|
|
68
|
+
private _onWrapClick: (e: Event) => void;
|
|
69
|
+
private _onWrapDblClick: (e: Event) => void;
|
|
70
|
+
private _onMouseDown: (e: Event) => void;
|
|
71
|
+
private _onMouseMove: (e: Event) => void;
|
|
72
|
+
private _onMouseUp: (e: Event) => void;
|
|
73
|
+
private _mutObs: MutationObserver;
|
|
74
|
+
private _destroyed = false;
|
|
75
|
+
|
|
76
|
+
constructor(
|
|
77
|
+
input: HTMLInputElement,
|
|
78
|
+
globalDateFormat: string,
|
|
79
|
+
globalTimeFormat: string = 'HH:mm',
|
|
80
|
+
globalDelimiter: string = ' ',
|
|
81
|
+
) {
|
|
82
|
+
this.input = input;
|
|
83
|
+
this.kind = getInputKind(input);
|
|
84
|
+
|
|
85
|
+
const dateFormat = input.dataset.dateFormat ?? globalDateFormat;
|
|
86
|
+
const timeFormat = input.dataset.timeFormat ?? globalTimeFormat;
|
|
87
|
+
const delimiter = input.dataset.dateTimeDelimiter ?? globalDelimiter;
|
|
88
|
+
|
|
89
|
+
this.format =
|
|
90
|
+
this.kind === 'datetime-local' ? buildDateTimeFormat(dateFormat, timeFormat, delimiter) :
|
|
91
|
+
this.kind === 'time' ? timeFormat :
|
|
92
|
+
dateFormat;
|
|
93
|
+
|
|
94
|
+
this.segments = parseFormat(this.format);
|
|
95
|
+
|
|
96
|
+
const elements = buildOverlay(
|
|
97
|
+
input,
|
|
98
|
+
this.segments,
|
|
99
|
+
(idx) => this.activateSegment(idx),
|
|
100
|
+
() => this.input.showPicker?.(),
|
|
101
|
+
this.kind,
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
this.wrapper = elements.wrapper;
|
|
105
|
+
this.overlay = elements.overlay;
|
|
106
|
+
this.segEls = elements.segEls;
|
|
107
|
+
|
|
108
|
+
this.render();
|
|
109
|
+
|
|
110
|
+
this._onKeyDown = (e) => this.handleKeyDown(e as KeyboardEvent);
|
|
111
|
+
this._onPaste = (e) => this.handlePaste(e as ClipboardEvent);
|
|
112
|
+
this._onChange = () => this.render();
|
|
113
|
+
this._onBlur = (e) => this.handleBlur(e as FocusEvent);
|
|
114
|
+
this._onFocus = (e) => this.handleFocus(e as FocusEvent);
|
|
115
|
+
this._onWrapClick = (e) => this.handleWrapperClick(e as MouseEvent);
|
|
116
|
+
this._onWrapDblClick = (e) => this.handleWrapperDblClick(e as MouseEvent);
|
|
117
|
+
this._onMouseDown = (e) => this.handleMouseDown(e as MouseEvent);
|
|
118
|
+
this._onMouseMove = (e) => this.handleMouseMove(e as MouseEvent);
|
|
119
|
+
this._onMouseUp = (e) => this.handleMouseUp(e as MouseEvent);
|
|
120
|
+
this._mutObs = new MutationObserver(() => { if (!this._destroyed) this.render(); });
|
|
121
|
+
|
|
122
|
+
this.attachEvents();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ── Rendering ───────────────────────────────────────────────────────────────
|
|
126
|
+
|
|
127
|
+
private render(): void {
|
|
128
|
+
renderSegments(this.segments, this.segEls, readInputDate(this.input));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
132
|
+
|
|
133
|
+
private firstTokenIdx(): number {
|
|
134
|
+
return this.segments.findIndex(s => s.token !== null);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private lastTokenIdx(): number {
|
|
138
|
+
for (let i = this.segments.length - 1; i >= 0; i--) {
|
|
139
|
+
if (this.segments[i].token) return i;
|
|
140
|
+
}
|
|
141
|
+
return -1;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
private nextTokenIdx(from: number, dir: 1 | -1 = 1): number {
|
|
145
|
+
let i = from + dir;
|
|
146
|
+
while (i >= 0 && i < this.segments.length) {
|
|
147
|
+
if (this.segments[i].token) return i;
|
|
148
|
+
i += dir;
|
|
149
|
+
}
|
|
150
|
+
return -1;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ── Segment activation ───────────────────────────────────────────────────────
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Validate and normalise the buffer for the given segment index.
|
|
157
|
+
* - Empty buffer ("0" or no input) → fills with today's value.
|
|
158
|
+
* - Partial numeric buffer → left-pads to maxDigits.
|
|
159
|
+
* - Year buffer shorter than 4 digits → prepend century from today.
|
|
160
|
+
*/
|
|
161
|
+
private verifySegment(idx: number): void {
|
|
162
|
+
const token = this.segments[idx].token;
|
|
163
|
+
if (!token) return;
|
|
164
|
+
|
|
165
|
+
let bufferVal = this.getBufferValue(idx);
|
|
166
|
+
const maxDigits = tokenMaxDigits(token);
|
|
167
|
+
const nowValue = pad(tokenCurrentValue(token, new Date()), maxDigits);
|
|
168
|
+
|
|
169
|
+
if (bufferVal === '0') {
|
|
170
|
+
this.typingBuffer = '';
|
|
171
|
+
this.typeDigit(token, nowValue, true);
|
|
172
|
+
} else if (bufferVal !== '') {
|
|
173
|
+
if (maxDigits <= 2) {
|
|
174
|
+
if (bufferVal.length < maxDigits) {
|
|
175
|
+
bufferVal = bufferVal.padStart(maxDigits, '0');
|
|
176
|
+
this.typingBuffer = '';
|
|
177
|
+
this.typeDigit(token, bufferVal, true);
|
|
178
|
+
}
|
|
179
|
+
} else {
|
|
180
|
+
// Year: prepend leading digits from today
|
|
181
|
+
this.typingBuffer = '';
|
|
182
|
+
const needle = maxDigits - bufferVal.length;
|
|
183
|
+
bufferVal = `${nowValue.substring(0, needle)}${bufferVal}`;
|
|
184
|
+
this.typeDigit(token, bufferVal, true);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
private activateSegment(idx: number): void {
|
|
190
|
+
if (idx < 0 || idx >= this.segments.length) return;
|
|
191
|
+
if (!this.segments[idx].token) return;
|
|
192
|
+
|
|
193
|
+
this.endSelection();
|
|
194
|
+
this.typingBuffer = '';
|
|
195
|
+
this.activeTokenIdx = idx;
|
|
196
|
+
activateSegmentEl(this.segEls, idx);
|
|
197
|
+
this.input.focus({ preventScroll: true });
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
private deactivate(): void {
|
|
201
|
+
if (this.activeTokenIdx !== -1) {
|
|
202
|
+
const token = this.segments[this.activeTokenIdx].token;
|
|
203
|
+
if (token) {
|
|
204
|
+
this.verifySegment(this.activeTokenIdx);
|
|
205
|
+
|
|
206
|
+
const date = readInputDate(this.input);
|
|
207
|
+
if (date) {
|
|
208
|
+
const current = tokenCurrentValue(token, date);
|
|
209
|
+
if (current < tokenMinValue(token)) {
|
|
210
|
+
this.commitTokenValue(token, tokenMinValue(token));
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
this.activeTokenIdx = -1;
|
|
217
|
+
this.typingBuffer = '';
|
|
218
|
+
this.endSelection();
|
|
219
|
+
deactivateAll(this.segEls);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ── Selection via active class ────────────────────────────────────────────
|
|
223
|
+
|
|
224
|
+
private hasSelection(): boolean {
|
|
225
|
+
return this.selAnchor !== -1;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
private paintSelection(anchor: number, end: number): void {
|
|
229
|
+
const from = Math.min(anchor, end);
|
|
230
|
+
const to = Math.max(anchor, end);
|
|
231
|
+
this.segEls.forEach((el, i) => {
|
|
232
|
+
el.classList.toggle('active', this.segments[i].token !== null && i >= from && i <= to);
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
private extendSelectionTo(idx: number): void {
|
|
237
|
+
this.selEnd = idx;
|
|
238
|
+
this.paintSelection(this.selAnchor, this.selEnd);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
private selectAll(): void {
|
|
242
|
+
const first = this.firstTokenIdx();
|
|
243
|
+
const last = this.lastTokenIdx();
|
|
244
|
+
if (first === -1) return;
|
|
245
|
+
this.activeTokenIdx = -1;
|
|
246
|
+
this.selAnchor = first;
|
|
247
|
+
this.selEnd = last;
|
|
248
|
+
this.paintSelection(first, last);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
private endSelection(): void {
|
|
252
|
+
if (this.activeTokenIdx !== -1) {
|
|
253
|
+
this.verifySegment(this.activeTokenIdx);
|
|
254
|
+
}
|
|
255
|
+
this.selAnchor = -1;
|
|
256
|
+
this.selEnd = -1;
|
|
257
|
+
deactivateAll(this.segEls);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// ── Copy / delete selected range ─────────────────────────────────────────
|
|
261
|
+
|
|
262
|
+
private copySelection(): void {
|
|
263
|
+
const date = readInputDate(this.input);
|
|
264
|
+
if (!date || !this.hasSelection()) return;
|
|
265
|
+
|
|
266
|
+
const from = Math.min(this.selAnchor, this.selEnd);
|
|
267
|
+
const to = Math.max(this.selAnchor, this.selEnd);
|
|
268
|
+
let text = '';
|
|
269
|
+
for (let i = from; i <= to; i++) {
|
|
270
|
+
const seg = this.segments[i];
|
|
271
|
+
if (!seg) continue;
|
|
272
|
+
text += seg.token ? tokenValue(seg.token, date) : seg.text;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
navigator.clipboard.writeText(text).catch(() => {
|
|
276
|
+
const ta = document.createElement('textarea');
|
|
277
|
+
ta.value = text;
|
|
278
|
+
ta.style.cssText = 'position:fixed;opacity:0;pointer-events:none';
|
|
279
|
+
document.body.appendChild(ta);
|
|
280
|
+
ta.select();
|
|
281
|
+
document.execCommand('copy');
|
|
282
|
+
document.body.removeChild(ta);
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
private deleteSelection(): void {
|
|
287
|
+
// Check if every token segment is already empty
|
|
288
|
+
const isEmpty = this.segments.every(
|
|
289
|
+
(seg, i) => !seg.token || this.getBufferValue(i) === '',
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
if (isEmpty) {
|
|
293
|
+
this.input.value = '';
|
|
294
|
+
this.input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
295
|
+
this.input.dispatchEvent(new Event('change', { bubbles: true }));
|
|
296
|
+
this.render();
|
|
297
|
+
this.activeTokenIdx = -1;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// ── Mouse drag ───────────────────────────────────────────────────────────
|
|
302
|
+
|
|
303
|
+
private tokenIdxFromEvent(e: MouseEvent): number {
|
|
304
|
+
const el = (e.target as HTMLElement).closest('[data-idx]') as HTMLElement | null;
|
|
305
|
+
if (!el) return -1;
|
|
306
|
+
const idx = parseInt(el.dataset.idx ?? '-1', 10);
|
|
307
|
+
return this.segments[idx]?.token ? idx : -1;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
private handleMouseDown(e: MouseEvent): void {
|
|
311
|
+
if (e.detail >= 2) return;
|
|
312
|
+
const idx = this.tokenIdxFromEvent(e);
|
|
313
|
+
if (idx === -1) return;
|
|
314
|
+
this.selAnchor = idx;
|
|
315
|
+
this.selEnd = idx;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
private handleMouseMove(e: MouseEvent): void {
|
|
319
|
+
if (this.selAnchor === -1 || !(e.buttons & 1)) return;
|
|
320
|
+
const idx = this.tokenIdxFromEvent(e);
|
|
321
|
+
if (idx === -1 || idx === this.selEnd) return;
|
|
322
|
+
|
|
323
|
+
if (this.activeTokenIdx !== -1) {
|
|
324
|
+
this.activeTokenIdx = -1;
|
|
325
|
+
this.typingBuffer = '';
|
|
326
|
+
}
|
|
327
|
+
this.extendSelectionTo(idx);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
private handleMouseUp(_e: MouseEvent): void {
|
|
331
|
+
if (this.selAnchor !== -1 && this.selAnchor === this.selEnd) {
|
|
332
|
+
const idx = this.selAnchor;
|
|
333
|
+
this.selAnchor = -1;
|
|
334
|
+
this.selEnd = -1;
|
|
335
|
+
this.activateSegment(idx);
|
|
336
|
+
} else if (this.selAnchor !== this.selEnd) {
|
|
337
|
+
this._justDragged = true;
|
|
338
|
+
this.input.focus({ preventScroll: true });
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// ── Double-click ─────────────────────────────────────────────────────────
|
|
343
|
+
|
|
344
|
+
private handleWrapperDblClick(e: MouseEvent): void {
|
|
345
|
+
e.preventDefault();
|
|
346
|
+
this.activeTokenIdx = -1;
|
|
347
|
+
this.typingBuffer = '';
|
|
348
|
+
this.selectAll();
|
|
349
|
+
this.input.focus({ preventScroll: true });
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// ── Blur / Focus / wrapper click ─────────────────────────────────────────────────
|
|
353
|
+
|
|
354
|
+
private handleBlur(e: FocusEvent): void {
|
|
355
|
+
const related = e.relatedTarget as Node | null;
|
|
356
|
+
if (!related || !this.wrapper.contains(related)) {
|
|
357
|
+
this.deactivate();
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
private handleFocus(e: FocusEvent): void {
|
|
362
|
+
this.activateSegment(0);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
private handleWrapperClick(e: MouseEvent): void {
|
|
366
|
+
if (this._justDragged) {
|
|
367
|
+
this._justDragged = false;
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
const target = e.target as HTMLElement;
|
|
371
|
+
if (target === this.wrapper || target === this.input || target === this.overlay) {
|
|
372
|
+
this.endSelection();
|
|
373
|
+
this.activateSegment(this.firstTokenIdx());
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// ── Keyboard ──────────────────────────────────────────────────────────────
|
|
378
|
+
|
|
379
|
+
private handleKeyDown(e: KeyboardEvent): void {
|
|
380
|
+
const mod = e.ctrlKey || e.metaKey;
|
|
381
|
+
|
|
382
|
+
if (mod && e.key === 'a') {
|
|
383
|
+
e.preventDefault();
|
|
384
|
+
this.selectAll();
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if (mod && e.key === 'c') {
|
|
389
|
+
if (this.hasSelection()) {
|
|
390
|
+
e.preventDefault();
|
|
391
|
+
this.copySelection();
|
|
392
|
+
}
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (e.key === 'Escape') {
|
|
397
|
+
this.hasSelection() ? this.endSelection() : this.deactivate();
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (e.key === 'Backspace' || e.key === 'Delete') {
|
|
402
|
+
e.preventDefault();
|
|
403
|
+
this.typingBuffer = '';
|
|
404
|
+
let selAnchor = -1;
|
|
405
|
+
|
|
406
|
+
if (this.hasSelection()) {
|
|
407
|
+
selAnchor = this.selAnchor;
|
|
408
|
+
for (let i = selAnchor; i <= this.selEnd; i++) {
|
|
409
|
+
if (this.segments[i]?.token) {
|
|
410
|
+
this.renderBuffer(i);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
this.endSelection();
|
|
414
|
+
} else if (this.activeTokenIdx !== -1) {
|
|
415
|
+
if (this.getBufferValue(this.activeTokenIdx) === '') selAnchor = this.activeTokenIdx;
|
|
416
|
+
this.renderBuffer();
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
if (selAnchor > 0) {
|
|
420
|
+
let s = selAnchor - 1;
|
|
421
|
+
while (s >= 0) {
|
|
422
|
+
if (this.segments[s]?.token) { this.activateSegment(s); break; }
|
|
423
|
+
s--;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
else if (selAnchor == 0) {
|
|
427
|
+
this.activateSegment(selAnchor);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
this.deleteSelection();
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
if (this.activeTokenIdx === -1) return;
|
|
435
|
+
|
|
436
|
+
const token = this.segments[this.activeTokenIdx].token!;
|
|
437
|
+
|
|
438
|
+
switch (e.key) {
|
|
439
|
+
case 'Tab':
|
|
440
|
+
this.handleTab(e);
|
|
441
|
+
break;
|
|
442
|
+
case 'ArrowRight': {
|
|
443
|
+
e.preventDefault();
|
|
444
|
+
const n = this.nextTokenIdx(this.activeTokenIdx, 1);
|
|
445
|
+
if (n !== -1) this.activateSegment(n);
|
|
446
|
+
break;
|
|
447
|
+
}
|
|
448
|
+
case 'ArrowLeft': {
|
|
449
|
+
e.preventDefault();
|
|
450
|
+
const p = this.nextTokenIdx(this.activeTokenIdx, -1);
|
|
451
|
+
if (p !== -1) this.activateSegment(p);
|
|
452
|
+
break;
|
|
453
|
+
}
|
|
454
|
+
case 'ArrowUp':
|
|
455
|
+
e.preventDefault();
|
|
456
|
+
this.stepValue(token, +1);
|
|
457
|
+
break;
|
|
458
|
+
case 'ArrowDown':
|
|
459
|
+
e.preventDefault();
|
|
460
|
+
this.stepValue(token, -1);
|
|
461
|
+
break;
|
|
462
|
+
default:
|
|
463
|
+
if (/^\d$/.test(e.key)) {
|
|
464
|
+
e.preventDefault();
|
|
465
|
+
this.typeDigit(token, e.key);
|
|
466
|
+
}
|
|
467
|
+
break;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
private handleTab(e: KeyboardEvent): void {
|
|
472
|
+
const next = this.nextTokenIdx(this.activeTokenIdx, e.shiftKey ? -1 : 1);
|
|
473
|
+
if (next !== -1) {
|
|
474
|
+
e.preventDefault();
|
|
475
|
+
this.activateSegment(next);
|
|
476
|
+
} else {
|
|
477
|
+
this.deactivate();
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// ── Digit typing ──────────────────────────────────────────────────────────
|
|
482
|
+
|
|
483
|
+
private typeDigit(token: DateToken, digit: string, skipNext: boolean = false): void {
|
|
484
|
+
this.typingBuffer += digit;
|
|
485
|
+
const num = parseInt(this.typingBuffer, 10);
|
|
486
|
+
const maxVal = tokenMaxValue(token);
|
|
487
|
+
const maxDigit = tokenMaxDigits(token);
|
|
488
|
+
|
|
489
|
+
if (this.typingBuffer.length >= maxDigit) {
|
|
490
|
+
const minVal = tokenMinValue(token);
|
|
491
|
+
const clamped = Math.max(minVal, Math.min(maxVal, num));
|
|
492
|
+
this.commitTokenValue(token, clamped);
|
|
493
|
+
this.typingBuffer = '';
|
|
494
|
+
if (!skipNext) {
|
|
495
|
+
const next = this.nextTokenIdx(this.activeTokenIdx, 1);
|
|
496
|
+
if (next !== -1) setTimeout(() => this.activateSegment(next), 0);
|
|
497
|
+
}
|
|
498
|
+
} else {
|
|
499
|
+
this.renderBuffer();
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
private renderBuffer(activeTokenIdx: number = this.activeTokenIdx): void {
|
|
504
|
+
const el = this.segEls[activeTokenIdx];
|
|
505
|
+
const token = this.segments[activeTokenIdx].token!;
|
|
506
|
+
if (this.typingBuffer) {
|
|
507
|
+
el.textContent = this.typingBuffer;
|
|
508
|
+
el.classList.remove('empty');
|
|
509
|
+
} else {
|
|
510
|
+
el.textContent = tokenPlaceholder(token);
|
|
511
|
+
el.classList.add('empty');
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
private getBufferValue(activeTokenIdx: number): string {
|
|
516
|
+
if (this.typingBuffer) return this.typingBuffer;
|
|
517
|
+
const el = this.segEls[activeTokenIdx];
|
|
518
|
+
return el.classList.contains('empty') ? '' : (el.textContent ?? '');
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// ── Step (↑ ↓) ────────────────────────────────────────────────────────────
|
|
522
|
+
|
|
523
|
+
private stepValue(token: DateToken, delta: number): void {
|
|
524
|
+
const date = readInputDate(this.input);
|
|
525
|
+
const base = date ?? new Date();
|
|
526
|
+
const current = tokenCurrentValue(token, base);
|
|
527
|
+
const minV = tokenMinValue(token);
|
|
528
|
+
const maxV = tokenMaxValue(token);
|
|
529
|
+
let next = current + delta;
|
|
530
|
+
|
|
531
|
+
if (next < minV) next = maxV;
|
|
532
|
+
if (next > maxV) next = minV;
|
|
533
|
+
|
|
534
|
+
this.commitTokenValue(token, next);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
private commitTokenValue(token: DateToken, value: number): void {
|
|
538
|
+
const date = buildDate(readInputDate(this.input), token, value);
|
|
539
|
+
writeInputDate(this.input, date);
|
|
540
|
+
this.render();
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// ── Paste ─────────────────────────────────────────────────────────────────
|
|
544
|
+
|
|
545
|
+
private handlePaste(e: ClipboardEvent): void {
|
|
546
|
+
e.preventDefault();
|
|
547
|
+
const text = e.clipboardData?.getData('text/plain') ?? '';
|
|
548
|
+
const date = parsePastedDate(text.trim(), this.format, this.kind);
|
|
549
|
+
if (date) {
|
|
550
|
+
writeInputDate(this.input, date);
|
|
551
|
+
this.render();
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// ── Event wiring ──────────────────────────────────────────────────────────
|
|
556
|
+
|
|
557
|
+
private attachEvents(): void {
|
|
558
|
+
this.input.addEventListener('keydown', this._onKeyDown);
|
|
559
|
+
this.input.addEventListener('paste', this._onPaste);
|
|
560
|
+
this.input.addEventListener('change', this._onChange);
|
|
561
|
+
this.input.addEventListener('blur', this._onBlur);
|
|
562
|
+
this.input.addEventListener('focus', this._onFocus);
|
|
563
|
+
|
|
564
|
+
this.wrapper.addEventListener('click', this._onWrapClick);
|
|
565
|
+
this.wrapper.addEventListener('dblclick', this._onWrapDblClick);
|
|
566
|
+
this.overlay.addEventListener('mousedown', this._onMouseDown);
|
|
567
|
+
this.overlay.addEventListener('mousemove', this._onMouseMove);
|
|
568
|
+
this.overlay.addEventListener('mouseup', this._onMouseUp);
|
|
569
|
+
|
|
570
|
+
this._mutObs.observe(this.input, {
|
|
571
|
+
attributes: true,
|
|
572
|
+
attributeFilter: ['value', 'min', 'max'],
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// ── Public API ────────────────────────────────────────────────────────────
|
|
577
|
+
|
|
578
|
+
public update(): void {
|
|
579
|
+
this.render();
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
public destroy(): void {
|
|
583
|
+
if (this._destroyed) return;
|
|
584
|
+
this._destroyed = true;
|
|
585
|
+
|
|
586
|
+
this._mutObs.disconnect();
|
|
587
|
+
|
|
588
|
+
this.input.removeEventListener('keydown', this._onKeyDown);
|
|
589
|
+
this.input.removeEventListener('paste', this._onPaste);
|
|
590
|
+
this.input.removeEventListener('change', this._onChange);
|
|
591
|
+
this.input.removeEventListener('blur', this._onBlur);
|
|
592
|
+
|
|
593
|
+
this.wrapper.removeEventListener('click', this._onWrapClick);
|
|
594
|
+
this.wrapper.removeEventListener('dblclick', this._onWrapDblClick);
|
|
595
|
+
this.overlay.removeEventListener('mousedown', this._onMouseDown);
|
|
596
|
+
this.overlay.removeEventListener('mousemove', this._onMouseMove);
|
|
597
|
+
this.overlay.removeEventListener('mouseup', this._onMouseUp);
|
|
598
|
+
|
|
599
|
+
this.input.classList.remove('superdate-input');
|
|
600
|
+
this.input.style.display = '';
|
|
601
|
+
this.input.style.width = '';
|
|
602
|
+
this.input.style.color = '';
|
|
603
|
+
this.wrapper.replaceWith(this.input);
|
|
604
|
+
}
|
|
605
|
+
}
|