@hh.ru/magritte-ui-input 5.0.22 → 5.0.24

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,1650 @@
1
+ import './index.css';
2
+ import { useState, useCallback, useRef, useLayoutEffect, useEffect } from 'react';
3
+
4
+ function getContentEditableSelection(element) {
5
+ const { anchorOffset = 0, focusOffset = 0 } = element.ownerDocument.getSelection() || {};
6
+ const from = Math.min(anchorOffset, focusOffset);
7
+ const to = Math.max(anchorOffset, focusOffset);
8
+ return [from, to];
9
+ }
10
+
11
+ function setContentEditableSelection(element, [from, to]) {
12
+ var _a, _b;
13
+ const document = element.ownerDocument;
14
+ const range = document.createRange();
15
+ range.setStart(element.firstChild || element, Math.min(from, ((_a = element.textContent) === null || _a === void 0 ? void 0 : _a.length) || 0));
16
+ range.setEnd(element.lastChild || element, Math.min(to, ((_b = element.textContent) === null || _b === void 0 ? void 0 : _b.length) || 0));
17
+ const selection = document.getSelection();
18
+ if (selection) {
19
+ selection.removeAllRanges();
20
+ selection.addRange(range);
21
+ }
22
+ }
23
+
24
+ class ContentEditableAdapter {
25
+ constructor(element) {
26
+ this.element = element;
27
+ this.maxLength = Infinity;
28
+ }
29
+ get value() {
30
+ return this.element.innerText.replace(/\n\n$/, '\n');
31
+ }
32
+ set value(value) {
33
+ // Setting into innerHTML of element with `white-space: pre;` style
34
+ this.element.innerHTML = value.replace(/\n$/, '\n\n');
35
+ }
36
+ get selectionStart() {
37
+ return getContentEditableSelection(this.element)[0];
38
+ }
39
+ get selectionEnd() {
40
+ return getContentEditableSelection(this.element)[1];
41
+ }
42
+ setSelectionRange(from, to) {
43
+ setContentEditableSelection(this.element, [from || 0, to || 0]);
44
+ }
45
+ select() {
46
+ this.setSelectionRange(0, this.value.length);
47
+ }
48
+ }
49
+ function maskitoAdaptContentEditable(element) {
50
+ const adapter = new ContentEditableAdapter(element);
51
+ return new Proxy(element, {
52
+ get(target, prop) {
53
+ if (prop in adapter) {
54
+ return adapter[prop];
55
+ }
56
+ const nativeProperty = target[prop];
57
+ return typeof nativeProperty === 'function'
58
+ ? nativeProperty.bind(target)
59
+ : nativeProperty;
60
+ },
61
+ set(target, prop, val, receiver) {
62
+ return Reflect.set(prop in adapter ? adapter : target, prop, val, receiver);
63
+ },
64
+ });
65
+ }
66
+
67
+ const MASKITO_DEFAULT_ELEMENT_PREDICATE = e => e.isContentEditable
68
+ ? maskitoAdaptContentEditable(e)
69
+ : e.querySelector('input,textarea') ||
70
+ e;
71
+
72
+ const MASKITO_DEFAULT_OPTIONS = {
73
+ mask: /^.*$/,
74
+ preprocessors: [],
75
+ postprocessors: [],
76
+ plugins: [],
77
+ overwriteMode: 'shift',
78
+ };
79
+
80
+ class MaskHistory {
81
+ constructor() {
82
+ this.now = null;
83
+ this.past = [];
84
+ this.future = [];
85
+ }
86
+ undo() {
87
+ const state = this.past.pop();
88
+ if (state && this.now) {
89
+ this.future.push(this.now);
90
+ this.updateElement(state, 'historyUndo');
91
+ }
92
+ }
93
+ redo() {
94
+ const state = this.future.pop();
95
+ if (state && this.now) {
96
+ this.past.push(this.now);
97
+ this.updateElement(state, 'historyRedo');
98
+ }
99
+ }
100
+ updateHistory(state) {
101
+ if (!this.now) {
102
+ this.now = state;
103
+ return;
104
+ }
105
+ const isValueChanged = this.now.value !== state.value;
106
+ const isSelectionChanged = this.now.selection.some((item, index) => item !== state.selection[index]);
107
+ if (!isValueChanged && !isSelectionChanged) {
108
+ return;
109
+ }
110
+ if (isValueChanged) {
111
+ this.past.push(this.now);
112
+ this.future = [];
113
+ }
114
+ this.now = state;
115
+ }
116
+ updateElement(state, inputType) {
117
+ this.now = state;
118
+ this.updateElementState(state, { inputType, data: null });
119
+ }
120
+ }
121
+
122
+ function areElementValuesEqual(sampleState, ...states) {
123
+ return states.every(({ value }) => value === sampleState.value);
124
+ }
125
+ function areElementStatesEqual(sampleState, ...states) {
126
+ return states.every(({ value, selection }) => value === sampleState.value &&
127
+ selection[0] === sampleState.selection[0] &&
128
+ selection[1] === sampleState.selection[1]);
129
+ }
130
+
131
+ function applyOverwriteMode({ value, selection }, newCharacters, mode) {
132
+ const [from, to] = selection;
133
+ const computedMode = typeof mode === 'function' ? mode({ value, selection }) : mode;
134
+ return {
135
+ value,
136
+ selection: computedMode === 'replace' ? [from, from + newCharacters.length] : [from, to],
137
+ };
138
+ }
139
+
140
+ function isFixedCharacter(char) {
141
+ return typeof char === 'string';
142
+ }
143
+
144
+ function getLeadingFixedCharacters(mask, validatedValuePart, newCharacter, initialElementState) {
145
+ let leadingFixedCharacters = '';
146
+ for (let i = validatedValuePart.length; i < mask.length; i++) {
147
+ const charConstraint = mask[i];
148
+ const isInitiallyExisted = (initialElementState === null || initialElementState === void 0 ? void 0 : initialElementState.value[i]) === charConstraint;
149
+ if (!isFixedCharacter(charConstraint) ||
150
+ (charConstraint === newCharacter && !isInitiallyExisted)) {
151
+ return leadingFixedCharacters;
152
+ }
153
+ leadingFixedCharacters += charConstraint;
154
+ }
155
+ return leadingFixedCharacters;
156
+ }
157
+
158
+ function validateValueWithMask(value, maskExpression) {
159
+ if (Array.isArray(maskExpression)) {
160
+ return (value.length === maskExpression.length &&
161
+ Array.from(value).every((char, i) => {
162
+ const charConstraint = maskExpression[i];
163
+ return isFixedCharacter(charConstraint)
164
+ ? char === charConstraint
165
+ : char.match(charConstraint);
166
+ }));
167
+ }
168
+ return maskExpression.test(value);
169
+ }
170
+
171
+ function guessValidValueByPattern(elementState, mask, initialElementState) {
172
+ let maskedFrom = null;
173
+ let maskedTo = null;
174
+ const maskedValue = Array.from(elementState.value).reduce((validatedCharacters, char, charIndex) => {
175
+ const leadingCharacters = getLeadingFixedCharacters(mask, validatedCharacters, char, initialElementState);
176
+ const newValidatedChars = validatedCharacters + leadingCharacters;
177
+ const charConstraint = mask[newValidatedChars.length];
178
+ if (isFixedCharacter(charConstraint)) {
179
+ return newValidatedChars + charConstraint;
180
+ }
181
+ if (!char.match(charConstraint)) {
182
+ return newValidatedChars;
183
+ }
184
+ if (maskedFrom === null && charIndex >= elementState.selection[0]) {
185
+ maskedFrom = newValidatedChars.length;
186
+ }
187
+ if (maskedTo === null && charIndex >= elementState.selection[1]) {
188
+ maskedTo = newValidatedChars.length;
189
+ }
190
+ return newValidatedChars + char;
191
+ }, '');
192
+ const trailingFixedCharacters = getLeadingFixedCharacters(mask, maskedValue, '', initialElementState);
193
+ return {
194
+ value: validateValueWithMask(maskedValue + trailingFixedCharacters, mask)
195
+ ? maskedValue + trailingFixedCharacters
196
+ : maskedValue,
197
+ selection: [maskedFrom !== null && maskedFrom !== void 0 ? maskedFrom : maskedValue.length, maskedTo !== null && maskedTo !== void 0 ? maskedTo : maskedValue.length],
198
+ };
199
+ }
200
+
201
+ function guessValidValueByRegExp({ value, selection }, maskRegExp) {
202
+ const [from, to] = selection;
203
+ let newFrom = from;
204
+ let newTo = to;
205
+ const validatedValue = Array.from(value).reduce((validatedValuePart, char, i) => {
206
+ const newPossibleValue = validatedValuePart + char;
207
+ if (from === i) {
208
+ newFrom = validatedValuePart.length;
209
+ }
210
+ if (to === i) {
211
+ newTo = validatedValuePart.length;
212
+ }
213
+ return newPossibleValue.match(maskRegExp) ? newPossibleValue : validatedValuePart;
214
+ }, '');
215
+ return { value: validatedValue, selection: [newFrom, newTo] };
216
+ }
217
+
218
+ function calibrateValueByMask(elementState, mask, initialElementState = null) {
219
+ if (validateValueWithMask(elementState.value, mask)) {
220
+ return elementState;
221
+ }
222
+ const { value, selection } = Array.isArray(mask)
223
+ ? guessValidValueByPattern(elementState, mask, initialElementState)
224
+ : guessValidValueByRegExp(elementState, mask);
225
+ return {
226
+ selection,
227
+ value: Array.isArray(mask) ? value.slice(0, mask.length) : value,
228
+ };
229
+ }
230
+
231
+ function removeFixedMaskCharacters(initialElementState, mask) {
232
+ if (!Array.isArray(mask)) {
233
+ return initialElementState;
234
+ }
235
+ const [from, to] = initialElementState.selection;
236
+ const selection = [];
237
+ const unmaskedValue = Array.from(initialElementState.value).reduce((rawValue, char, i) => {
238
+ const charConstraint = mask[i];
239
+ if (i === from) {
240
+ selection.push(rawValue.length);
241
+ }
242
+ if (i === to) {
243
+ selection.push(rawValue.length);
244
+ }
245
+ return isFixedCharacter(charConstraint) && charConstraint === char
246
+ ? rawValue
247
+ : rawValue + char;
248
+ }, '');
249
+ if (selection.length < 2) {
250
+ selection.push(...new Array(2 - selection.length).fill(unmaskedValue.length));
251
+ }
252
+ return {
253
+ value: unmaskedValue,
254
+ selection: [selection[0], selection[1]],
255
+ };
256
+ }
257
+
258
+ class MaskModel {
259
+ constructor(initialElementState, maskOptions) {
260
+ this.initialElementState = initialElementState;
261
+ this.maskOptions = maskOptions;
262
+ this.value = '';
263
+ this.selection = [0, 0];
264
+ const { value, selection } = calibrateValueByMask(this.initialElementState, this.getMaskExpression(this.initialElementState));
265
+ this.value = value;
266
+ this.selection = selection;
267
+ }
268
+ addCharacters([from, to], newCharacters) {
269
+ const { value } = this;
270
+ const maskExpression = this.getMaskExpression({
271
+ value: value.slice(0, from) + newCharacters + value.slice(to),
272
+ selection: [from + newCharacters.length, from + newCharacters.length],
273
+ });
274
+ const initialElementState = { value, selection: [from, to] };
275
+ const unmaskedElementState = removeFixedMaskCharacters(initialElementState, maskExpression);
276
+ const [unmaskedFrom, unmaskedTo] = applyOverwriteMode(unmaskedElementState, newCharacters, this.maskOptions.overwriteMode).selection;
277
+ const newUnmaskedLeadingValuePart = unmaskedElementState.value.slice(0, unmaskedFrom) + newCharacters;
278
+ const newCaretIndex = newUnmaskedLeadingValuePart.length;
279
+ const maskedElementState = calibrateValueByMask({
280
+ value: newUnmaskedLeadingValuePart +
281
+ unmaskedElementState.value.slice(unmaskedTo),
282
+ selection: [newCaretIndex, newCaretIndex],
283
+ }, maskExpression, initialElementState);
284
+ const isInvalidCharsInsertion =
285
+ // eslint-disable-next-line @typescript-eslint/prefer-string-starts-ends-with
286
+ value.slice(0, unmaskedFrom) ===
287
+ calibrateValueByMask({
288
+ value: newUnmaskedLeadingValuePart,
289
+ selection: [newCaretIndex, newCaretIndex],
290
+ }, maskExpression, initialElementState).value;
291
+ if (isInvalidCharsInsertion ||
292
+ areElementStatesEqual(this, maskedElementState) // If typing new characters does not change value
293
+ ) {
294
+ throw new Error('Invalid mask value');
295
+ }
296
+ this.value = maskedElementState.value;
297
+ this.selection = maskedElementState.selection;
298
+ }
299
+ deleteCharacters([from, to]) {
300
+ if (from === to || !to) {
301
+ return;
302
+ }
303
+ const { value } = this;
304
+ const maskExpression = this.getMaskExpression({
305
+ value: value.slice(0, from) + value.slice(to),
306
+ selection: [from, from],
307
+ });
308
+ const initialElementState = { value, selection: [from, to] };
309
+ const unmaskedElementState = removeFixedMaskCharacters(initialElementState, maskExpression);
310
+ const [unmaskedFrom, unmaskedTo] = unmaskedElementState.selection;
311
+ const newUnmaskedValue = unmaskedElementState.value.slice(0, unmaskedFrom) +
312
+ unmaskedElementState.value.slice(unmaskedTo);
313
+ const maskedElementState = calibrateValueByMask({ value: newUnmaskedValue, selection: [unmaskedFrom, unmaskedFrom] }, maskExpression, initialElementState);
314
+ this.value = maskedElementState.value;
315
+ this.selection = maskedElementState.selection;
316
+ }
317
+ getMaskExpression(elementState) {
318
+ const { mask } = this.maskOptions;
319
+ return typeof mask === 'function' ? mask(elementState) : mask;
320
+ }
321
+ }
322
+
323
+ class EventListener {
324
+ constructor(element) {
325
+ this.element = element;
326
+ this.listeners = [];
327
+ }
328
+ listen(eventType, fn, options) {
329
+ const untypedFn = fn;
330
+ this.element.addEventListener(eventType, untypedFn, options);
331
+ this.listeners.push(() => this.element.removeEventListener(eventType, untypedFn));
332
+ }
333
+ destroy() {
334
+ this.listeners.forEach(stopListen => stopListen());
335
+ }
336
+ }
337
+
338
+ const HotkeyModifier = {
339
+ CTRL: 1 << 0,
340
+ ALT: 1 << 1,
341
+ SHIFT: 1 << 2,
342
+ META: 1 << 3,
343
+ };
344
+ // TODO add variants that can be processed correctly
345
+ const HotkeyCode = {
346
+ Y: 89,
347
+ Z: 90,
348
+ };
349
+ /**
350
+ * Checks if the passed keyboard event match the required hotkey.
351
+ *
352
+ * @example
353
+ * input.addEventListener('keydown', (event) => {
354
+ * if (isHotkey(event, HotkeyModifier.CTRL | HotkeyModifier.SHIFT, HotkeyCode.Z)) {
355
+ * // redo hotkey pressed
356
+ * }
357
+ * })
358
+ *
359
+ * @return will return `true` only if the {@link HotkeyCode} matches and only the necessary
360
+ * {@link HotkeyModifier modifiers} have been pressed
361
+ */
362
+ function isHotkey(event, modifiers, hotkeyCode) {
363
+ return (event.ctrlKey === !!(modifiers & HotkeyModifier.CTRL) &&
364
+ event.altKey === !!(modifiers & HotkeyModifier.ALT) &&
365
+ event.shiftKey === !!(modifiers & HotkeyModifier.SHIFT) &&
366
+ event.metaKey === !!(modifiers & HotkeyModifier.META) &&
367
+ /**
368
+ * We intentionally use legacy {@link KeyboardEvent#keyCode `keyCode`} property. It is more
369
+ * "keyboard-layout"-independent than {@link KeyboardEvent#key `key`} or {@link KeyboardEvent#code `code`} properties.
370
+ * @see {@link https://github.com/taiga-family/maskito/issues/315 `KeyboardEvent#code` issue}
371
+ */
372
+ event.keyCode === hotkeyCode);
373
+ }
374
+
375
+ function isRedo(event) {
376
+ return (isHotkey(event, HotkeyModifier.CTRL, HotkeyCode.Y) || // Windows
377
+ isHotkey(event, HotkeyModifier.CTRL | HotkeyModifier.SHIFT, HotkeyCode.Z) || // Windows & Android
378
+ isHotkey(event, HotkeyModifier.META | HotkeyModifier.SHIFT, HotkeyCode.Z) // macOS & iOS
379
+ );
380
+ }
381
+ function isUndo(event) {
382
+ return (isHotkey(event, HotkeyModifier.CTRL, HotkeyCode.Z) || // Windows & Android
383
+ isHotkey(event, HotkeyModifier.META, HotkeyCode.Z) // macOS & iOS
384
+ );
385
+ }
386
+
387
+ /**
388
+ * Sets value to element, and dispatches input event
389
+ * if you passed ELementState, it also sets selection range
390
+ *
391
+ * @example
392
+ * maskitoUpdateElement(input, newValue);
393
+ * maskitoUpdateElement(input, elementState);
394
+ *
395
+ * @see {@link https://github.com/taiga-family/maskito/issues/804 issue}
396
+ *
397
+ * @return void
398
+ */
399
+ function maskitoUpdateElement(element, valueOrElementState) {
400
+ var _a;
401
+ const initialValue = element.value;
402
+ if (typeof valueOrElementState === 'string') {
403
+ element.value = valueOrElementState;
404
+ }
405
+ else {
406
+ const [from, to] = valueOrElementState.selection;
407
+ element.value = valueOrElementState.value;
408
+ if (element.matches(':focus')) {
409
+ (_a = element.setSelectionRange) === null || _a === void 0 ? void 0 : _a.call(element, from, to);
410
+ }
411
+ }
412
+ if (element.value !== initialValue) {
413
+ element.dispatchEvent(new Event('input',
414
+ /**
415
+ * React handles this event only on bubbling phase
416
+ *
417
+ * here is the list of events that are processed in the capture stage, others are processed in the bubbling stage
418
+ * https://github.com/facebook/react/blob/cb2439624f43c510007f65aea5c50a8bb97917e4/packages/react-dom-bindings/src/events/DOMPluginEventSystem.js#L222
419
+ */
420
+ { bubbles: true }));
421
+ }
422
+ }
423
+
424
+ function getLineSelection({ value, selection }, isForward) {
425
+ const [from, to] = selection;
426
+ if (from !== to) {
427
+ return [from, to];
428
+ }
429
+ const nearestBreak = isForward
430
+ ? value.slice(from).indexOf('\n') + 1 || value.length
431
+ : value.slice(0, to).lastIndexOf('\n') + 1;
432
+ const selectFrom = isForward ? from : nearestBreak;
433
+ const selectTo = isForward ? nearestBreak : to;
434
+ return [selectFrom, selectTo];
435
+ }
436
+
437
+ function getNotEmptySelection({ value, selection }, isForward) {
438
+ const [from, to] = selection;
439
+ if (from !== to) {
440
+ return [from, to];
441
+ }
442
+ const notEmptySelection = isForward ? [from, to + 1] : [from - 1, to];
443
+ return notEmptySelection.map(x => Math.min(Math.max(x, 0), value.length));
444
+ }
445
+
446
+ const TRAILING_SPACES_REG = /\s+$/g;
447
+ const LEADING_SPACES_REG = /^\s+/g;
448
+ const SPACE_REG = /\s/;
449
+ function getWordSelection({ value, selection }, isForward) {
450
+ const [from, to] = selection;
451
+ if (from !== to) {
452
+ return [from, to];
453
+ }
454
+ if (isForward) {
455
+ const valueAfterSelectionStart = value.slice(from);
456
+ const [leadingSpaces] = valueAfterSelectionStart.match(LEADING_SPACES_REG) || [
457
+ '',
458
+ ];
459
+ const nearestWordEndIndex = valueAfterSelectionStart
460
+ .trimStart()
461
+ .search(SPACE_REG);
462
+ return [
463
+ from,
464
+ nearestWordEndIndex !== -1
465
+ ? from + leadingSpaces.length + nearestWordEndIndex
466
+ : value.length,
467
+ ];
468
+ }
469
+ const valueBeforeSelectionEnd = value.slice(0, to);
470
+ const [trailingSpaces] = valueBeforeSelectionEnd.match(TRAILING_SPACES_REG) || [''];
471
+ const selectedWordLength = valueBeforeSelectionEnd
472
+ .trimEnd()
473
+ .split('')
474
+ .reverse()
475
+ .findIndex(char => char.match(SPACE_REG));
476
+ return [
477
+ selectedWordLength !== -1 ? to - trailingSpaces.length - selectedWordLength : 0,
478
+ to,
479
+ ];
480
+ }
481
+
482
+ /* eslint-disable @typescript-eslint/ban-types */
483
+ /**
484
+ * @internal
485
+ */
486
+ function maskitoPipe(processors = []) {
487
+ return (initialData, ...readonlyArgs) => processors.reduce((data, fn) => (Object.assign(Object.assign({}, data), fn(data, ...readonlyArgs))), initialData);
488
+ }
489
+
490
+ function maskitoTransform(valueOrState, maskitoOptions) {
491
+ const options = Object.assign(Object.assign({}, MASKITO_DEFAULT_OPTIONS), maskitoOptions);
492
+ const preprocessor = maskitoPipe(options.preprocessors);
493
+ const postprocessor = maskitoPipe(options.postprocessors);
494
+ const initialElementState = typeof valueOrState === 'string'
495
+ ? { value: valueOrState, selection: [0, 0] }
496
+ : valueOrState;
497
+ const { elementState } = preprocessor({ elementState: initialElementState, data: '' }, 'validation');
498
+ const maskModel = new MaskModel(elementState, options);
499
+ const { value, selection } = postprocessor(maskModel, initialElementState);
500
+ return typeof valueOrState === 'string' ? value : { value, selection };
501
+ }
502
+
503
+ class Maskito extends MaskHistory {
504
+ constructor(element, maskitoOptions) {
505
+ super();
506
+ this.element = element;
507
+ this.maskitoOptions = maskitoOptions;
508
+ this.isTextArea = this.element.nodeName === 'TEXTAREA';
509
+ this.eventListener = new EventListener(this.element);
510
+ this.options = Object.assign(Object.assign({}, MASKITO_DEFAULT_OPTIONS), this.maskitoOptions);
511
+ this.preprocessor = maskitoPipe(this.options.preprocessors);
512
+ this.postprocessor = maskitoPipe(this.options.postprocessors);
513
+ this.teardowns = this.options.plugins.map(plugin => plugin(this.element, this.options));
514
+ this.updateHistory(this.elementState);
515
+ this.eventListener.listen('keydown', event => {
516
+ if (isRedo(event)) {
517
+ event.preventDefault();
518
+ return this.redo();
519
+ }
520
+ if (isUndo(event)) {
521
+ event.preventDefault();
522
+ return this.undo();
523
+ }
524
+ });
525
+ this.eventListener.listen('beforeinput', event => {
526
+ var _a;
527
+ const isForward = event.inputType.includes('Forward');
528
+ this.updateHistory(this.elementState);
529
+ switch (event.inputType) {
530
+ // historyUndo/historyRedo will not be triggered if value was modified programmatically
531
+ case 'historyUndo':
532
+ event.preventDefault();
533
+ return this.undo();
534
+ case 'historyRedo':
535
+ event.preventDefault();
536
+ return this.redo();
537
+ case 'deleteByCut':
538
+ case 'deleteContentBackward':
539
+ case 'deleteContentForward':
540
+ return this.handleDelete({
541
+ event,
542
+ isForward,
543
+ selection: getNotEmptySelection(this.elementState, isForward),
544
+ });
545
+ case 'deleteWordForward':
546
+ case 'deleteWordBackward':
547
+ return this.handleDelete({
548
+ event,
549
+ isForward,
550
+ selection: getWordSelection(this.elementState, isForward),
551
+ force: true,
552
+ });
553
+ case 'deleteSoftLineBackward':
554
+ case 'deleteSoftLineForward':
555
+ case 'deleteHardLineBackward':
556
+ case 'deleteHardLineForward':
557
+ return this.handleDelete({
558
+ event,
559
+ isForward,
560
+ selection: getLineSelection(this.elementState, isForward),
561
+ force: true,
562
+ });
563
+ case 'insertCompositionText':
564
+ return; // will be handled inside `compositionend` event
565
+ case 'insertLineBreak':
566
+ case 'insertParagraph':
567
+ return this.handleEnter(event);
568
+ case 'insertFromPaste':
569
+ case 'insertText':
570
+ case 'insertFromDrop':
571
+ default:
572
+ return this.handleInsert(event, event.data ||
573
+ (
574
+ // `event.data` for `contentEditable` is always `null` for paste/drop events
575
+ (_a = event.dataTransfer) === null || _a === void 0 ? void 0 : _a.getData('text/plain')) ||
576
+ '');
577
+ }
578
+ });
579
+ this.eventListener.listen('input', ({ inputType }) => {
580
+ if (inputType === 'insertCompositionText') {
581
+ return; // will be handled inside `compositionend` event
582
+ }
583
+ this.ensureValueFitsMask();
584
+ this.updateHistory(this.elementState);
585
+ });
586
+ this.eventListener.listen('compositionend', () => {
587
+ this.ensureValueFitsMask();
588
+ this.updateHistory(this.elementState);
589
+ });
590
+ }
591
+ get elementState() {
592
+ const { value, selectionStart, selectionEnd } = this.element;
593
+ return {
594
+ value,
595
+ selection: [selectionStart || 0, selectionEnd || 0],
596
+ };
597
+ }
598
+ get maxLength() {
599
+ const { maxLength } = this.element;
600
+ return maxLength === -1 ? Infinity : maxLength;
601
+ }
602
+ destroy() {
603
+ this.eventListener.destroy();
604
+ this.teardowns.forEach(teardown => teardown === null || teardown === void 0 ? void 0 : teardown());
605
+ }
606
+ updateElementState({ value, selection }, eventInit = {
607
+ inputType: 'insertText',
608
+ data: null,
609
+ }) {
610
+ const initialValue = this.elementState.value;
611
+ this.updateValue(value);
612
+ this.updateSelectionRange(selection);
613
+ if (initialValue !== value) {
614
+ this.dispatchInputEvent(eventInit);
615
+ }
616
+ }
617
+ updateSelectionRange([from, to]) {
618
+ var _a;
619
+ const { element } = this;
620
+ if (element.matches(':focus') &&
621
+ (element.selectionStart !== from || element.selectionEnd !== to)) {
622
+ (_a = element.setSelectionRange) === null || _a === void 0 ? void 0 : _a.call(element, from, to);
623
+ }
624
+ }
625
+ updateValue(value) {
626
+ this.element.value = value;
627
+ }
628
+ ensureValueFitsMask() {
629
+ this.updateElementState(maskitoTransform(this.elementState, this.options));
630
+ }
631
+ dispatchInputEvent(eventInit = {
632
+ inputType: 'insertText',
633
+ data: null,
634
+ }) {
635
+ if (globalThis.InputEvent) {
636
+ this.element.dispatchEvent(new InputEvent('input', Object.assign(Object.assign({}, eventInit), { bubbles: true, cancelable: false })));
637
+ }
638
+ }
639
+ handleDelete({ event, selection, isForward, force = false, }) {
640
+ const initialState = {
641
+ value: this.elementState.value,
642
+ selection,
643
+ };
644
+ const [initialFrom, initialTo] = initialState.selection;
645
+ const { elementState } = this.preprocessor({
646
+ elementState: initialState,
647
+ data: '',
648
+ }, isForward ? 'deleteForward' : 'deleteBackward');
649
+ const maskModel = new MaskModel(elementState, this.options);
650
+ const [from, to] = elementState.selection;
651
+ maskModel.deleteCharacters([from, to]);
652
+ const newElementState = this.postprocessor(maskModel, initialState);
653
+ const newPossibleValue = initialState.value.slice(0, initialFrom) +
654
+ initialState.value.slice(initialTo);
655
+ if (newPossibleValue === newElementState.value &&
656
+ !force &&
657
+ !this.element.isContentEditable) {
658
+ return;
659
+ }
660
+ event.preventDefault();
661
+ if (areElementValuesEqual(initialState, elementState, maskModel, newElementState)) {
662
+ // User presses Backspace/Delete for the fixed value
663
+ return this.updateSelectionRange(isForward ? [to, to] : [from, from]);
664
+ }
665
+ this.updateElementState(newElementState, {
666
+ inputType: event.inputType,
667
+ data: null,
668
+ });
669
+ this.updateHistory(newElementState);
670
+ }
671
+ handleInsert(event, data) {
672
+ const initialElementState = this.elementState;
673
+ const { elementState, data: insertedText = data } = this.preprocessor({
674
+ data,
675
+ elementState: initialElementState,
676
+ }, 'insert');
677
+ const maskModel = new MaskModel(elementState, this.options);
678
+ try {
679
+ maskModel.addCharacters(elementState.selection, insertedText);
680
+ }
681
+ catch (_a) {
682
+ return event.preventDefault();
683
+ }
684
+ const [from, to] = elementState.selection;
685
+ const newPossibleValue = initialElementState.value.slice(0, from) +
686
+ data +
687
+ initialElementState.value.slice(to);
688
+ const newElementState = this.postprocessor(maskModel, initialElementState);
689
+ if (newElementState.value.length > this.maxLength) {
690
+ return event.preventDefault();
691
+ }
692
+ if (newPossibleValue !== newElementState.value ||
693
+ this.element.isContentEditable) {
694
+ event.preventDefault();
695
+ this.updateElementState(newElementState, {
696
+ data,
697
+ inputType: event.inputType,
698
+ });
699
+ this.updateHistory(newElementState);
700
+ }
701
+ }
702
+ handleEnter(event) {
703
+ if (this.isTextArea || this.element.isContentEditable) {
704
+ this.handleInsert(event, '\n');
705
+ }
706
+ }
707
+ }
708
+
709
+ /**
710
+ * Clamps a value between two inclusive limits
711
+ *
712
+ * @param value
713
+ * @param min lower limit
714
+ * @param max upper limit
715
+ */
716
+ function clamp(value, min, max) {
717
+ const clampedValue = Math.min(Number(max), Math.max(Number(min), Number(value)));
718
+ return (value instanceof Date ? new Date(clampedValue) : clampedValue);
719
+ }
720
+
721
+ function countDigits(str) {
722
+ return str.replaceAll(/\W/g, '').length;
723
+ }
724
+
725
+ const getDateSegmentValueLength = (dateString) => {
726
+ var _a, _b, _c;
727
+ return ({
728
+ day: ((_a = dateString.match(/d/g)) === null || _a === void 0 ? void 0 : _a.length) || 0,
729
+ month: ((_b = dateString.match(/m/g)) === null || _b === void 0 ? void 0 : _b.length) || 0,
730
+ year: ((_c = dateString.match(/y/g)) === null || _c === void 0 ? void 0 : _c.length) || 0,
731
+ });
732
+ };
733
+
734
+ function dateToSegments(date) {
735
+ return {
736
+ day: String(date.getDate()).padStart(2, '0'),
737
+ month: String(date.getMonth() + 1).padStart(2, '0'),
738
+ year: String(date.getFullYear()).padStart(4, '0'),
739
+ hours: String(date.getHours()).padStart(2, '0'),
740
+ minutes: String(date.getMinutes()).padStart(2, '0'),
741
+ seconds: String(date.getSeconds()).padStart(2, '0'),
742
+ milliseconds: String(date.getMilliseconds()).padStart(3, '0'),
743
+ };
744
+ }
745
+
746
+ function getFirstCompleteDate(dateString, dateModeTemplate) {
747
+ const digitsInDate = countDigits(dateModeTemplate);
748
+ const [completeDate = ''] = dateString.match(new RegExp(`(\\D*\\d){${digitsInDate}}`)) || [];
749
+ return completeDate;
750
+ }
751
+
752
+ function isDateStringComplete(dateString, dateModeTemplate) {
753
+ if (dateString.length < dateModeTemplate.length) {
754
+ return false;
755
+ }
756
+ return dateString.split(/\D/).every(segment => !segment.match(/^0+$/));
757
+ }
758
+
759
+ function parseDateRangeString(dateRange, dateModeTemplate, rangeSeparator) {
760
+ const digitsInDate = countDigits(dateModeTemplate);
761
+ return (dateRange
762
+ .replace(rangeSeparator, '')
763
+ .match(new RegExp(`(\\D*\\d[^\\d\\s]*){1,${digitsInDate}}`, 'g')) || []);
764
+ }
765
+
766
+ function parseDateString(dateString, fullMode) {
767
+ const cleanMode = fullMode.replaceAll(/[^dmy]/g, '');
768
+ const onlyDigitsDate = dateString.replaceAll(/\D+/g, '');
769
+ const dateSegments = {
770
+ day: onlyDigitsDate.slice(cleanMode.indexOf('d'), cleanMode.lastIndexOf('d') + 1),
771
+ month: onlyDigitsDate.slice(cleanMode.indexOf('m'), cleanMode.lastIndexOf('m') + 1),
772
+ year: onlyDigitsDate.slice(cleanMode.indexOf('y'), cleanMode.lastIndexOf('y') + 1),
773
+ };
774
+ return Object.fromEntries(Object.entries(dateSegments)
775
+ .filter(([_, value]) => Boolean(value))
776
+ .sort(([a], [b]) => fullMode.toLowerCase().indexOf(a[0]) >
777
+ fullMode.toLowerCase().indexOf(b[0])
778
+ ? 1
779
+ : -1));
780
+ }
781
+
782
+ function segmentsToDate(parsedDate, parsedTime) {
783
+ var _a, _b, _c, _d, _e, _f, _g;
784
+ const year = ((_a = parsedDate.year) === null || _a === void 0 ? void 0 : _a.length) === 2 ? `20${parsedDate.year}` : parsedDate.year;
785
+ const date = new Date(Number(year !== null && year !== void 0 ? year : '0'), Number((_b = parsedDate.month) !== null && _b !== void 0 ? _b : '1') - 1, Number((_c = parsedDate.day) !== null && _c !== void 0 ? _c : '1'), Number((_d = parsedTime === null || parsedTime === void 0 ? void 0 : parsedTime.hours) !== null && _d !== void 0 ? _d : '0'), Number((_e = parsedTime === null || parsedTime === void 0 ? void 0 : parsedTime.minutes) !== null && _e !== void 0 ? _e : '0'), Number((_f = parsedTime === null || parsedTime === void 0 ? void 0 : parsedTime.seconds) !== null && _f !== void 0 ? _f : '0'), Number((_g = parsedTime === null || parsedTime === void 0 ? void 0 : parsedTime.milliseconds) !== null && _g !== void 0 ? _g : '0'));
786
+ // needed for years less than 1900
787
+ date.setFullYear(Number(year !== null && year !== void 0 ? year : '0'));
788
+ return date;
789
+ }
790
+
791
+ const DATE_TIME_SEPARATOR = ', ';
792
+
793
+ function toDateString({ day, month, year, hours, minutes, seconds, milliseconds, }, { dateMode, dateTimeSeparator = DATE_TIME_SEPARATOR, timeMode, }) {
794
+ var _a;
795
+ const safeYear = ((_a = dateMode.match(/y/g)) === null || _a === void 0 ? void 0 : _a.length) === 2 ? year === null || year === void 0 ? void 0 : year.slice(-2) : year;
796
+ const fullMode = dateMode + (timeMode ? dateTimeSeparator + timeMode : '');
797
+ return fullMode
798
+ .replaceAll(/d+/g, day !== null && day !== void 0 ? day : '')
799
+ .replaceAll(/m+/g, month !== null && month !== void 0 ? month : '')
800
+ .replaceAll(/y+/g, safeYear !== null && safeYear !== void 0 ? safeYear : '')
801
+ .replaceAll(/H+/g, hours !== null && hours !== void 0 ? hours : '')
802
+ .replaceAll('MSS', milliseconds !== null && milliseconds !== void 0 ? milliseconds : '')
803
+ .replaceAll(/M+/g, minutes !== null && minutes !== void 0 ? minutes : '')
804
+ .replaceAll(/S+/g, seconds !== null && seconds !== void 0 ? seconds : '')
805
+ .replaceAll(/^\D+/g, '')
806
+ .replaceAll(/\D+$/g, '');
807
+ }
808
+
809
+ const DATE_SEGMENTS_MAX_VALUES = {
810
+ day: 31,
811
+ month: 12,
812
+ year: 9999,
813
+ };
814
+
815
+ const DEFAULT_MIN_DATE = new Date('0001-01-01');
816
+ const DEFAULT_MAX_DATE = new Date('9999-12-31');
817
+
818
+ const DEFAULT_TIME_SEGMENT_MAX_VALUES = {
819
+ hours: 23,
820
+ minutes: 59,
821
+ seconds: 59,
822
+ milliseconds: 999,
823
+ };
824
+
825
+ const TIME_FIXED_CHARACTERS = [':', '.'];
826
+
827
+ const TIME_SEGMENT_VALUE_LENGTHS = {
828
+ hours: 2,
829
+ minutes: 2,
830
+ seconds: 2,
831
+ milliseconds: 3,
832
+ };
833
+ /**
834
+ * {@link https://symbl.cc/en/003A/ Colon}
835
+ * is a punctuation mark that connects parts of a text logically.
836
+ * ---
837
+ * is also used as separator in time.
838
+ */
839
+ const CHAR_COLON = '\u003A';
840
+ /**
841
+ * {@link https://symbl.cc/en/FF1A/ Full-width colon}
842
+ * is a full-width punctuation mark used to separate parts of a text commonly in Japanese.
843
+ */
844
+ const CHAR_JP_COLON = '\uFF1A';
845
+
846
+ function validateDateString({ dateString, dateModeTemplate, offset, selection: [from, to], }) {
847
+ const parsedDate = parseDateString(dateString, dateModeTemplate);
848
+ const dateSegments = Object.entries(parsedDate);
849
+ const validatedDateSegments = {};
850
+ for (const [segmentName, segmentValue] of dateSegments) {
851
+ const validatedDate = toDateString(validatedDateSegments, {
852
+ dateMode: dateModeTemplate,
853
+ });
854
+ const maxSegmentValue = DATE_SEGMENTS_MAX_VALUES[segmentName];
855
+ const fantomSeparator = validatedDate.length && 1;
856
+ const lastSegmentDigitIndex = offset +
857
+ validatedDate.length +
858
+ fantomSeparator +
859
+ getDateSegmentValueLength(dateModeTemplate)[segmentName];
860
+ const isLastSegmentDigitAdded = lastSegmentDigitIndex >= from && lastSegmentDigitIndex === to;
861
+ if (isLastSegmentDigitAdded && Number(segmentValue) > Number(maxSegmentValue)) {
862
+ // 3|1.10.2010 => Type 9 => 3|1.10.2010
863
+ return { validatedDateString: '', updatedSelection: [from, to] }; // prevent changes
864
+ }
865
+ if (isLastSegmentDigitAdded && Number(segmentValue) < 1) {
866
+ // 31.0|1.2010 => Type 0 => 31.0|1.2010
867
+ return { validatedDateString: '', updatedSelection: [from, to] }; // prevent changes
868
+ }
869
+ validatedDateSegments[segmentName] = segmentValue;
870
+ }
871
+ const validatedDateString = toDateString(validatedDateSegments, {
872
+ dateMode: dateModeTemplate,
873
+ });
874
+ const addedDateSegmentSeparators = validatedDateString.length - dateString.length;
875
+ return {
876
+ validatedDateString,
877
+ updatedSelection: [
878
+ from + addedDateSegmentSeparators,
879
+ to + addedDateSegmentSeparators,
880
+ ],
881
+ };
882
+ }
883
+
884
+ /**
885
+ * Copy-pasted solution from lodash
886
+ * @see https://lodash.com/docs/4.17.15#escapeRegExp
887
+ */
888
+ const reRegExpChar = /[\\^$.*+?()[\]{}|]/g;
889
+ const reHasRegExpChar = new RegExp(reRegExpChar.source);
890
+ function escapeRegExp(str) {
891
+ return str && reHasRegExpChar.test(str) ? str.replaceAll(reRegExpChar, '\\$&') : str;
892
+ }
893
+
894
+ function padWithZeroesUntilValid(segmentValue, paddedMaxValue, prefixedZeroesCount = 0) {
895
+ if (Number(segmentValue.padEnd(paddedMaxValue.length, '0')) <= Number(paddedMaxValue)) {
896
+ return { validatedSegmentValue: segmentValue, prefixedZeroesCount };
897
+ }
898
+ if (segmentValue.endsWith('0')) {
899
+ // 00:|00 => Type 9 => 00:09|
900
+ return padWithZeroesUntilValid(`0${segmentValue.slice(0, paddedMaxValue.length - 1)}`, paddedMaxValue, prefixedZeroesCount + 1);
901
+ }
902
+ // |19:00 => Type 2 => 2|0:00
903
+ return padWithZeroesUntilValid(`${segmentValue.slice(0, paddedMaxValue.length - 1)}0`, paddedMaxValue, prefixedZeroesCount);
904
+ }
905
+
906
+ /**
907
+ * Replace fullwidth colon with half width colon
908
+ * @param fullWidthColon full width colon
909
+ * @returns processed half width colon
910
+ */
911
+ function toHalfWidthColon(fullWidthColon) {
912
+ return fullWidthColon.replaceAll(new RegExp(CHAR_JP_COLON, 'g'), CHAR_COLON);
913
+ }
914
+
915
+ /**
916
+ * Replace fullwidth numbers with half width number
917
+ * @param fullWidthNumber full width number
918
+ * @returns processed half width number
919
+ */
920
+ function toHalfWidthNumber(fullWidthNumber) {
921
+ return fullWidthNumber.replaceAll(/[0-9]/g, s => String.fromCharCode(s.charCodeAt(0) - 0xfee0));
922
+ }
923
+
924
+ /**
925
+ * Convert full width colon (:) to half width one (:)
926
+ */
927
+ function createColonConvertPreprocessor() {
928
+ return ({ elementState, data }) => {
929
+ const { value, selection } = elementState;
930
+ return {
931
+ elementState: {
932
+ selection,
933
+ value: toHalfWidthColon(value),
934
+ },
935
+ data: toHalfWidthColon(data),
936
+ };
937
+ };
938
+ }
939
+
940
+ function createDateSegmentsZeroPaddingPostprocessor({ dateModeTemplate, dateSegmentSeparator, splitFn, uniteFn, }) {
941
+ return ({ value, selection }) => {
942
+ var _a;
943
+ const [from, to] = selection;
944
+ const { dateStrings, restPart = '' } = splitFn(value);
945
+ const validatedDateStrings = [];
946
+ let caretShift = 0;
947
+ dateStrings.forEach(dateString => {
948
+ const parsedDate = parseDateString(dateString, dateModeTemplate);
949
+ const dateSegments = Object.entries(parsedDate);
950
+ const validatedDateSegments = dateSegments.reduce((acc, [segmentName, segmentValue]) => {
951
+ const { validatedSegmentValue, prefixedZeroesCount } = padWithZeroesUntilValid(segmentValue, `${DATE_SEGMENTS_MAX_VALUES[segmentName]}`);
952
+ caretShift += prefixedZeroesCount;
953
+ return Object.assign(Object.assign({}, acc), { [segmentName]: validatedSegmentValue });
954
+ }, {});
955
+ validatedDateStrings.push(toDateString(validatedDateSegments, { dateMode: dateModeTemplate }));
956
+ });
957
+ const validatedValue = uniteFn(validatedDateStrings, value) +
958
+ (((_a = dateStrings[dateStrings.length - 1]) === null || _a === void 0 ? void 0 : _a.endsWith(dateSegmentSeparator))
959
+ ? dateSegmentSeparator
960
+ : '') +
961
+ restPart;
962
+ if (caretShift && validatedValue[to + 1] === dateSegmentSeparator) {
963
+ /**
964
+ * If `caretShift` > 0, it means that time segment was padded with zero.
965
+ * It is only possible if any character insertion happens.
966
+ * If caret is before `dateSegmentSeparator` => it should be moved after `dateSegmentSeparator`.
967
+ */
968
+ caretShift++;
969
+ }
970
+ return {
971
+ selection: [from + caretShift, to + caretShift],
972
+ value: validatedValue,
973
+ };
974
+ };
975
+ }
976
+
977
+ /**
978
+ * It replaces pseudo range separators with valid one.
979
+ * @example '01.01.2000_11.11.2000' -> '01.01.2000 - 01.01.2000'
980
+ * @example '01.01.2000_23:59' -> '01.01.2000, 23:59'
981
+ */
982
+ function createFirstDateEndSeparatorPreprocessor({ dateModeTemplate, firstDateEndSeparator, dateSegmentSeparator, pseudoFirstDateEndSeparators, }) {
983
+ return ({ elementState, data }) => {
984
+ const { value, selection } = elementState;
985
+ const firstCompleteDate = getFirstCompleteDate(value, dateModeTemplate);
986
+ const pseudoSeparators = pseudoFirstDateEndSeparators.filter(x => !firstDateEndSeparator.includes(x) && x !== dateSegmentSeparator);
987
+ const pseudoSeparatorsRE = new RegExp(`[${pseudoSeparators.join('')}]`, 'gi');
988
+ return {
989
+ elementState: {
990
+ selection,
991
+ value: firstCompleteDate && value.length > firstCompleteDate.length
992
+ ? firstCompleteDate +
993
+ value
994
+ .slice(firstCompleteDate.length)
995
+ .replace(/^[\D\s]*/, firstDateEndSeparator)
996
+ : value,
997
+ },
998
+ data: data.replace(pseudoSeparatorsRE, firstDateEndSeparator),
999
+ };
1000
+ };
1001
+ }
1002
+
1003
+ /**
1004
+ * Convert full width numbers like 1, 2 to half width numbers 1, 2
1005
+ */
1006
+ function createFullWidthToHalfWidthPreprocessor() {
1007
+ return ({ elementState, data }) => {
1008
+ const { value, selection } = elementState;
1009
+ return {
1010
+ elementState: {
1011
+ selection,
1012
+ value: toHalfWidthNumber(value),
1013
+ },
1014
+ data: toHalfWidthNumber(data),
1015
+ };
1016
+ };
1017
+ }
1018
+
1019
+ function raiseSegmentValueToMin(segments, fullMode) {
1020
+ const segmentsLength = getDateSegmentValueLength(fullMode);
1021
+ return Object.fromEntries(Object.entries(segments).map(([key, value]) => {
1022
+ const segmentLength = segmentsLength[key];
1023
+ return [
1024
+ key,
1025
+ value.length === segmentLength && value.match(/^0+$/)
1026
+ ? '1'.padStart(segmentLength, '0')
1027
+ : value,
1028
+ ];
1029
+ }));
1030
+ }
1031
+
1032
+ function createMinMaxDatePostprocessor({ dateModeTemplate, min = DEFAULT_MIN_DATE, max = DEFAULT_MAX_DATE, rangeSeparator = '', dateSegmentSeparator = '.', }) {
1033
+ return ({ value, selection }) => {
1034
+ const endsWithRangeSeparator = rangeSeparator && value.endsWith(rangeSeparator);
1035
+ const dateStrings = parseDateRangeString(value, dateModeTemplate, rangeSeparator);
1036
+ let validatedValue = '';
1037
+ for (const dateString of dateStrings) {
1038
+ validatedValue += validatedValue ? rangeSeparator : '';
1039
+ const parsedDate = parseDateString(dateString, dateModeTemplate);
1040
+ if (!isDateStringComplete(dateString, dateModeTemplate)) {
1041
+ const fixedDate = raiseSegmentValueToMin(parsedDate, dateModeTemplate);
1042
+ const fixedValue = toDateString(fixedDate, { dateMode: dateModeTemplate });
1043
+ const tail = dateString.endsWith(dateSegmentSeparator)
1044
+ ? dateSegmentSeparator
1045
+ : '';
1046
+ validatedValue += fixedValue + tail;
1047
+ continue;
1048
+ }
1049
+ const date = segmentsToDate(parsedDate);
1050
+ const clampedDate = clamp(date, min, max);
1051
+ validatedValue += toDateString(dateToSegments(clampedDate), {
1052
+ dateMode: dateModeTemplate,
1053
+ });
1054
+ }
1055
+ return {
1056
+ selection,
1057
+ value: validatedValue + (endsWithRangeSeparator ? rangeSeparator : ''),
1058
+ };
1059
+ };
1060
+ }
1061
+
1062
+ function normalizeDatePreprocessor({ dateModeTemplate, dateSegmentsSeparator, rangeSeparator = '', dateTimeSeparator = DATE_TIME_SEPARATOR, }) {
1063
+ return ({ elementState, data }) => {
1064
+ const separator = rangeSeparator
1065
+ ? new RegExp(`${rangeSeparator}|-`)
1066
+ : dateTimeSeparator;
1067
+ const possibleDates = data.split(separator);
1068
+ const dates = data.includes(dateTimeSeparator)
1069
+ ? [possibleDates[0]]
1070
+ : possibleDates;
1071
+ if (dates.every(date => date.trim().split(/\D/).filter(Boolean).length ===
1072
+ dateModeTemplate.split(dateSegmentsSeparator).length)) {
1073
+ const newData = dates
1074
+ .map(date => normalizeDateString(date, dateModeTemplate, dateSegmentsSeparator))
1075
+ .join(rangeSeparator);
1076
+ return {
1077
+ elementState,
1078
+ data: `${newData}${data.includes(dateTimeSeparator)
1079
+ ? dateTimeSeparator + possibleDates[1] || ''
1080
+ : ''}`,
1081
+ };
1082
+ }
1083
+ return { elementState, data };
1084
+ };
1085
+ }
1086
+ function normalizeDateString(dateString, template, separator) {
1087
+ const dateSegments = dateString.split(/\D/).filter(Boolean);
1088
+ const templateSegments = template.split(separator);
1089
+ const normalizedSegments = dateSegments.map((segment, index) => index === templateSegments.length - 1
1090
+ ? segment
1091
+ : segment.padStart(templateSegments[index].length, '0'));
1092
+ return normalizedSegments.join(separator);
1093
+ }
1094
+
1095
+ function createValidDatePreprocessor({ dateModeTemplate, dateSegmentsSeparator, rangeSeparator = '', }) {
1096
+ return ({ elementState, data }) => {
1097
+ const { value, selection } = elementState;
1098
+ if (data === dateSegmentsSeparator) {
1099
+ return {
1100
+ elementState,
1101
+ data: selection[0] === value.length ? data : '',
1102
+ };
1103
+ }
1104
+ const newCharacters = data.replaceAll(new RegExp(`[^\\d${escapeRegExp(dateSegmentsSeparator)}${rangeSeparator}]`, 'g'), '');
1105
+ if (!newCharacters) {
1106
+ return { elementState, data: '' };
1107
+ }
1108
+ const [from, rawTo] = selection;
1109
+ let to = rawTo + data.length;
1110
+ const newPossibleValue = value.slice(0, from) + newCharacters + value.slice(to);
1111
+ const dateStrings = parseDateRangeString(newPossibleValue, dateModeTemplate, rangeSeparator);
1112
+ let validatedValue = '';
1113
+ const hasRangeSeparator = Boolean(rangeSeparator) && newPossibleValue.includes(rangeSeparator);
1114
+ for (const dateString of dateStrings) {
1115
+ const { validatedDateString, updatedSelection } = validateDateString({
1116
+ dateString,
1117
+ dateModeTemplate,
1118
+ offset: validatedValue.length,
1119
+ selection: [from, to],
1120
+ });
1121
+ if (dateString && !validatedDateString) {
1122
+ return { elementState, data: '' }; // prevent changes
1123
+ }
1124
+ to = updatedSelection[1];
1125
+ validatedValue +=
1126
+ hasRangeSeparator && !validatedValue
1127
+ ? validatedDateString + rangeSeparator
1128
+ : validatedDateString;
1129
+ }
1130
+ const newData = validatedValue.slice(from, to);
1131
+ return {
1132
+ elementState: {
1133
+ selection,
1134
+ value: validatedValue.slice(0, from) +
1135
+ newData
1136
+ .split(dateSegmentsSeparator)
1137
+ .map(segment => '0'.repeat(segment.length))
1138
+ .join(dateSegmentsSeparator) +
1139
+ validatedValue.slice(to),
1140
+ },
1141
+ data: newData,
1142
+ };
1143
+ };
1144
+ }
1145
+
1146
+ const noop = () => { };
1147
+ function createTimeSegmentsSteppingPlugin({ step, fullMode, timeSegmentMaxValues, }) {
1148
+ const segmentsIndexes = createTimeSegmentsIndexes(fullMode);
1149
+ return step <= 0
1150
+ ? noop
1151
+ : element => {
1152
+ const listener = (event) => {
1153
+ if (event.key !== 'ArrowUp' && event.key !== 'ArrowDown') {
1154
+ return;
1155
+ }
1156
+ event.preventDefault();
1157
+ const selectionStart = element.selectionStart || 0;
1158
+ const activeSegment = getActiveSegment({
1159
+ segmentsIndexes,
1160
+ selectionStart,
1161
+ });
1162
+ if (!activeSegment) {
1163
+ return;
1164
+ }
1165
+ const updatedValue = updateSegmentValue({
1166
+ selection: segmentsIndexes.get(activeSegment),
1167
+ value: element.value,
1168
+ toAdd: event.key === 'ArrowUp' ? step : -step,
1169
+ max: timeSegmentMaxValues[activeSegment],
1170
+ });
1171
+ maskitoUpdateElement(element, {
1172
+ value: updatedValue,
1173
+ selection: [selectionStart, selectionStart],
1174
+ });
1175
+ };
1176
+ element.addEventListener('keydown', listener);
1177
+ return () => element.removeEventListener('keydown', listener);
1178
+ };
1179
+ }
1180
+ function createTimeSegmentsIndexes(fullMode) {
1181
+ return new Map([
1182
+ ['hours', getSegmentRange(fullMode, 'HH')],
1183
+ ['minutes', getSegmentRange(fullMode, 'MM')],
1184
+ ['seconds', getSegmentRange(fullMode, 'SS')],
1185
+ ['milliseconds', getSegmentRange(fullMode, 'MSS')],
1186
+ ]);
1187
+ }
1188
+ function getSegmentRange(mode, segment) {
1189
+ const index = mode.indexOf(segment);
1190
+ return index === -1 ? [-1, -1] : [index, index + segment.length];
1191
+ }
1192
+ function getActiveSegment({ segmentsIndexes, selectionStart, }) {
1193
+ for (const [segmentName, segmentRange] of segmentsIndexes.entries()) {
1194
+ const [from, to] = segmentRange;
1195
+ if (from <= selectionStart && selectionStart <= to) {
1196
+ return segmentName;
1197
+ }
1198
+ }
1199
+ return null;
1200
+ }
1201
+ function updateSegmentValue({ selection, value, toAdd, max, }) {
1202
+ const [from, to] = selection;
1203
+ const segmentValue = Number(value.slice(from, to).padEnd(to - from, '0'));
1204
+ const newSegmentValue = mod(segmentValue + toAdd, max + 1);
1205
+ return (value.slice(0, from) +
1206
+ String(newSegmentValue).padStart(to - from, '0') +
1207
+ value.slice(to, value.length));
1208
+ }
1209
+ function mod(value, max) {
1210
+ if (value < 0) {
1211
+ value += Math.floor(Math.abs(value) / max + 1) * max;
1212
+ }
1213
+ return value % max;
1214
+ }
1215
+
1216
+ function createZeroPlaceholdersPreprocessor() {
1217
+ return ({ elementState }, actionType) => {
1218
+ const { value, selection } = elementState;
1219
+ if (!value || isLastChar(value, selection)) {
1220
+ return { elementState };
1221
+ }
1222
+ const [from, to] = selection;
1223
+ const zeroes = value.slice(from, to).replaceAll(/\d/g, '0');
1224
+ const newValue = value.slice(0, from) + zeroes + value.slice(to);
1225
+ if (actionType === 'validation' || (actionType === 'insert' && from === to)) {
1226
+ return {
1227
+ elementState: { selection, value: newValue },
1228
+ };
1229
+ }
1230
+ return {
1231
+ elementState: {
1232
+ selection: actionType === 'deleteBackward' || actionType === 'insert'
1233
+ ? [from, from]
1234
+ : [to, to],
1235
+ value: newValue,
1236
+ },
1237
+ };
1238
+ };
1239
+ }
1240
+ function isLastChar(value, [_, to]) {
1241
+ return to === value.length;
1242
+ }
1243
+
1244
+ function maskitoDateOptionsGenerator({ mode, separator = '.', max, min, }) {
1245
+ const dateModeTemplate = mode.split('/').join(separator);
1246
+ return Object.assign(Object.assign({}, MASKITO_DEFAULT_OPTIONS), { mask: Array.from(dateModeTemplate).map(char => char === separator ? char : /\d/), overwriteMode: 'replace', preprocessors: [
1247
+ createFullWidthToHalfWidthPreprocessor(),
1248
+ createZeroPlaceholdersPreprocessor(),
1249
+ normalizeDatePreprocessor({
1250
+ dateModeTemplate,
1251
+ dateSegmentsSeparator: separator,
1252
+ }),
1253
+ createValidDatePreprocessor({
1254
+ dateModeTemplate,
1255
+ dateSegmentsSeparator: separator,
1256
+ }),
1257
+ ], postprocessors: [
1258
+ createDateSegmentsZeroPaddingPostprocessor({
1259
+ dateModeTemplate,
1260
+ dateSegmentSeparator: separator,
1261
+ splitFn: value => ({ dateStrings: [value] }),
1262
+ uniteFn: ([dateString]) => dateString,
1263
+ }),
1264
+ createMinMaxDatePostprocessor({
1265
+ min,
1266
+ max,
1267
+ dateModeTemplate,
1268
+ dateSegmentSeparator: separator,
1269
+ }),
1270
+ ] });
1271
+ }
1272
+
1273
+ function padTimeSegments(timeSegments) {
1274
+ return Object.fromEntries(Object.entries(timeSegments).map(([segmentName, segmentValue]) => [
1275
+ segmentName,
1276
+ `${segmentValue}`.padEnd(TIME_SEGMENT_VALUE_LENGTHS[segmentName], '0'),
1277
+ ]));
1278
+ }
1279
+
1280
+ const SEGMENT_FULL_NAME = {
1281
+ HH: 'hours',
1282
+ MM: 'minutes',
1283
+ SS: 'seconds',
1284
+ MSS: 'milliseconds',
1285
+ };
1286
+ /**
1287
+ * @param timeString can be with/without fixed characters
1288
+ */
1289
+ function parseTimeString(timeString, timeMode) {
1290
+ const onlyDigits = timeString.replaceAll(/\D+/g, '');
1291
+ let offset = 0;
1292
+ return Object.fromEntries(timeMode.split(/\W/).map(segmentAbbr => {
1293
+ const segmentValue = onlyDigits.slice(offset, offset + segmentAbbr.length);
1294
+ offset += segmentAbbr.length;
1295
+ return [SEGMENT_FULL_NAME[segmentAbbr], segmentValue];
1296
+ }));
1297
+ }
1298
+
1299
+ const LEADING_NON_DIGITS = /^\D*/;
1300
+ const TRAILING_NON_DIGITS = /\D*$/;
1301
+ function toTimeString({ hours = '', minutes = '', seconds = '', milliseconds = '', }) {
1302
+ return `${hours}:${minutes}:${seconds}.${milliseconds}`
1303
+ .replace(LEADING_NON_DIGITS, '')
1304
+ .replace(TRAILING_NON_DIGITS, '');
1305
+ }
1306
+
1307
+ const TRAILING_TIME_SEGMENT_SEPARATOR_REG = new RegExp(`[${TIME_FIXED_CHARACTERS.map(escapeRegExp).join('')}]$`);
1308
+ function validateTimeString({ timeString, paddedMaxValues, offset, selection: [from, to], timeMode, }) {
1309
+ const parsedTime = parseTimeString(timeString, timeMode);
1310
+ const possibleTimeSegments = Object.entries(parsedTime);
1311
+ const validatedTimeSegments = {};
1312
+ let paddedZeroes = 0;
1313
+ for (const [segmentName, segmentValue] of possibleTimeSegments) {
1314
+ const validatedTime = toTimeString(validatedTimeSegments);
1315
+ const maxSegmentValue = paddedMaxValues[segmentName];
1316
+ const fantomSeparator = validatedTime.length && 1;
1317
+ const lastSegmentDigitIndex = offset +
1318
+ validatedTime.length +
1319
+ fantomSeparator +
1320
+ TIME_SEGMENT_VALUE_LENGTHS[segmentName];
1321
+ const isLastSegmentDigitAdded = lastSegmentDigitIndex >= from && lastSegmentDigitIndex <= to;
1322
+ if (isLastSegmentDigitAdded && Number(segmentValue) > Number(maxSegmentValue)) {
1323
+ // 2|0:00 => Type 9 => 2|0:00
1324
+ return { validatedTimeString: '', updatedTimeSelection: [from, to] }; // prevent changes
1325
+ }
1326
+ const { validatedSegmentValue, prefixedZeroesCount } = padWithZeroesUntilValid(segmentValue, `${maxSegmentValue}`);
1327
+ paddedZeroes += prefixedZeroesCount;
1328
+ validatedTimeSegments[segmentName] = validatedSegmentValue;
1329
+ }
1330
+ const [trailingSegmentSeparator = ''] = timeString.match(TRAILING_TIME_SEGMENT_SEPARATOR_REG) || [];
1331
+ const validatedTimeString = toTimeString(validatedTimeSegments) + trailingSegmentSeparator;
1332
+ const addedDateSegmentSeparators = Math.max(validatedTimeString.length - timeString.length, 0);
1333
+ return {
1334
+ validatedTimeString,
1335
+ updatedTimeSelection: [
1336
+ from + paddedZeroes + addedDateSegmentSeparators,
1337
+ to + paddedZeroes + addedDateSegmentSeparators,
1338
+ ],
1339
+ };
1340
+ }
1341
+
1342
+ function isDateTimeStringComplete(dateTimeString, { dateMode, timeMode, dateTimeSeparator = DATE_TIME_SEPARATOR, }) {
1343
+ return (dateTimeString.length >=
1344
+ dateMode.length + timeMode.length + dateTimeSeparator.length &&
1345
+ dateTimeString
1346
+ .split(dateTimeSeparator)[0]
1347
+ .split(/\D/)
1348
+ .every(segment => !segment.match(/^0+$/)));
1349
+ }
1350
+
1351
+ function parseDateTimeString(dateTime, { dateModeTemplate, dateTimeSeparator, }) {
1352
+ const hasSeparator = dateTime.includes(dateTimeSeparator);
1353
+ return [
1354
+ dateTime.slice(0, dateModeTemplate.length),
1355
+ dateTime.slice(hasSeparator
1356
+ ? dateModeTemplate.length + dateTimeSeparator.length
1357
+ : dateModeTemplate.length),
1358
+ ];
1359
+ }
1360
+
1361
+ function createMinMaxDateTimePostprocessor({ dateModeTemplate, timeMode, min = DEFAULT_MIN_DATE, max = DEFAULT_MAX_DATE, dateTimeSeparator, }) {
1362
+ return ({ value, selection }) => {
1363
+ const [dateString, timeString] = parseDateTimeString(value, {
1364
+ dateModeTemplate,
1365
+ dateTimeSeparator,
1366
+ });
1367
+ const parsedDate = parseDateString(dateString, dateModeTemplate);
1368
+ const parsedTime = parseTimeString(timeString, timeMode);
1369
+ if (!isDateTimeStringComplete(value, {
1370
+ dateMode: dateModeTemplate,
1371
+ timeMode,
1372
+ dateTimeSeparator,
1373
+ })) {
1374
+ const fixedDate = raiseSegmentValueToMin(parsedDate, dateModeTemplate);
1375
+ const { year, month, day } = isDateStringComplete(dateString, dateModeTemplate)
1376
+ ? dateToSegments(clamp(segmentsToDate(fixedDate), min, max))
1377
+ : fixedDate;
1378
+ const fixedValue = toDateString(Object.assign({ year,
1379
+ month,
1380
+ day }, parsedTime), { dateMode: dateModeTemplate, dateTimeSeparator, timeMode });
1381
+ const tail = value.slice(fixedValue.length);
1382
+ return {
1383
+ selection,
1384
+ value: fixedValue + tail,
1385
+ };
1386
+ }
1387
+ const date = segmentsToDate(parsedDate, parsedTime);
1388
+ const clampedDate = clamp(date, min, max);
1389
+ const validatedValue = toDateString(dateToSegments(clampedDate), {
1390
+ dateMode: dateModeTemplate,
1391
+ dateTimeSeparator,
1392
+ timeMode,
1393
+ });
1394
+ return {
1395
+ selection,
1396
+ value: validatedValue,
1397
+ };
1398
+ };
1399
+ }
1400
+
1401
+ function createValidDateTimePreprocessor({ dateModeTemplate, dateSegmentsSeparator, dateTimeSeparator, timeMode, }) {
1402
+ const invalidCharsRegExp = new RegExp(`[^\\d${TIME_FIXED_CHARACTERS.map(escapeRegExp).join('')}${escapeRegExp(dateSegmentsSeparator)}]+`);
1403
+ return ({ elementState, data }) => {
1404
+ const { value, selection } = elementState;
1405
+ if (data === dateSegmentsSeparator) {
1406
+ return {
1407
+ elementState,
1408
+ data: selection[0] === value.length ? data : '',
1409
+ };
1410
+ }
1411
+ const newCharacters = data.replace(invalidCharsRegExp, '');
1412
+ if (!newCharacters) {
1413
+ return { elementState, data: '' };
1414
+ }
1415
+ const [from, rawTo] = selection;
1416
+ let to = rawTo + data.length;
1417
+ const newPossibleValue = value.slice(0, from) + newCharacters + value.slice(to);
1418
+ const [dateString, timeString] = parseDateTimeString(newPossibleValue, {
1419
+ dateModeTemplate,
1420
+ dateTimeSeparator,
1421
+ });
1422
+ let validatedValue = '';
1423
+ const hasDateTimeSeparator = newPossibleValue.includes(dateTimeSeparator);
1424
+ const { validatedDateString, updatedSelection } = validateDateString({
1425
+ dateString,
1426
+ dateModeTemplate,
1427
+ offset: 0,
1428
+ selection: [from, to],
1429
+ });
1430
+ if (dateString && !validatedDateString) {
1431
+ return { elementState, data: '' }; // prevent changes
1432
+ }
1433
+ to = updatedSelection[1];
1434
+ validatedValue += validatedDateString;
1435
+ const paddedMaxValues = padTimeSegments(DEFAULT_TIME_SEGMENT_MAX_VALUES);
1436
+ const { validatedTimeString, updatedTimeSelection } = validateTimeString({
1437
+ timeString,
1438
+ paddedMaxValues,
1439
+ offset: validatedValue.length + dateTimeSeparator.length,
1440
+ selection: [from, to],
1441
+ timeMode,
1442
+ });
1443
+ if (timeString && !validatedTimeString) {
1444
+ return { elementState, data: '' }; // prevent changes
1445
+ }
1446
+ to = updatedTimeSelection[1];
1447
+ validatedValue += hasDateTimeSeparator
1448
+ ? dateTimeSeparator + validatedTimeString
1449
+ : validatedTimeString;
1450
+ const newData = validatedValue.slice(from, to);
1451
+ return {
1452
+ elementState: {
1453
+ selection,
1454
+ value: validatedValue.slice(0, from) +
1455
+ newData
1456
+ .split(dateSegmentsSeparator)
1457
+ .map(segment => '0'.repeat(segment.length))
1458
+ .join(dateSegmentsSeparator) +
1459
+ validatedValue.slice(to),
1460
+ },
1461
+ data: newData,
1462
+ };
1463
+ };
1464
+ }
1465
+
1466
+ function maskitoDateTimeOptionsGenerator({ dateMode, timeMode, dateSeparator = '.', min, max, dateTimeSeparator = DATE_TIME_SEPARATOR, timeStep = 0, }) {
1467
+ const dateModeTemplate = dateMode.split('/').join(dateSeparator);
1468
+ return Object.assign(Object.assign({}, MASKITO_DEFAULT_OPTIONS), { mask: [
1469
+ ...Array.from(dateModeTemplate).map(char => char === dateSeparator ? char : /\d/),
1470
+ ...dateTimeSeparator.split(''),
1471
+ ...Array.from(timeMode).map(char => TIME_FIXED_CHARACTERS.includes(char) ? char : /\d/),
1472
+ ], overwriteMode: 'replace', preprocessors: [
1473
+ createFullWidthToHalfWidthPreprocessor(),
1474
+ createColonConvertPreprocessor(),
1475
+ createFirstDateEndSeparatorPreprocessor({
1476
+ dateModeTemplate,
1477
+ dateSegmentSeparator: dateSeparator,
1478
+ firstDateEndSeparator: dateTimeSeparator,
1479
+ pseudoFirstDateEndSeparators: dateTimeSeparator.split(''),
1480
+ }),
1481
+ createZeroPlaceholdersPreprocessor(),
1482
+ normalizeDatePreprocessor({
1483
+ dateModeTemplate,
1484
+ dateSegmentsSeparator: dateSeparator,
1485
+ dateTimeSeparator,
1486
+ }),
1487
+ createValidDateTimePreprocessor({
1488
+ dateModeTemplate,
1489
+ dateSegmentsSeparator: dateSeparator,
1490
+ dateTimeSeparator,
1491
+ timeMode,
1492
+ }),
1493
+ ], postprocessors: [
1494
+ createDateSegmentsZeroPaddingPostprocessor({
1495
+ dateModeTemplate,
1496
+ dateSegmentSeparator: dateSeparator,
1497
+ splitFn: value => {
1498
+ const [dateString, timeString] = parseDateTimeString(value, {
1499
+ dateModeTemplate,
1500
+ dateTimeSeparator,
1501
+ });
1502
+ return { dateStrings: [dateString], restPart: timeString };
1503
+ },
1504
+ uniteFn: ([validatedDateString], initialValue) => validatedDateString +
1505
+ (initialValue.includes(dateTimeSeparator) ? dateTimeSeparator : ''),
1506
+ }),
1507
+ createMinMaxDateTimePostprocessor({
1508
+ min,
1509
+ max,
1510
+ dateModeTemplate,
1511
+ timeMode,
1512
+ dateTimeSeparator,
1513
+ }),
1514
+ ], plugins: [
1515
+ createTimeSegmentsSteppingPlugin({
1516
+ step: timeStep,
1517
+ fullMode: `${dateModeTemplate}${dateTimeSeparator}${timeMode}`,
1518
+ timeSegmentMaxValues: DEFAULT_TIME_SEGMENT_MAX_VALUES,
1519
+ }),
1520
+ ] });
1521
+ }
1522
+
1523
+ function createMaxValidationPreprocessor(timeSegmentMaxValues, timeMode) {
1524
+ const paddedMaxValues = padTimeSegments(timeSegmentMaxValues);
1525
+ const invalidCharsRegExp = new RegExp(`[^\\d${TIME_FIXED_CHARACTERS.map(escapeRegExp).join('')}]+`);
1526
+ return ({ elementState, data }, actionType) => {
1527
+ if (actionType === 'deleteBackward' || actionType === 'deleteForward') {
1528
+ return { elementState, data };
1529
+ }
1530
+ const { value, selection } = elementState;
1531
+ if (actionType === 'validation') {
1532
+ const { validatedTimeString, updatedTimeSelection } = validateTimeString({
1533
+ timeString: value,
1534
+ paddedMaxValues,
1535
+ offset: 0,
1536
+ selection,
1537
+ timeMode,
1538
+ });
1539
+ return {
1540
+ elementState: {
1541
+ value: validatedTimeString,
1542
+ selection: updatedTimeSelection,
1543
+ },
1544
+ data,
1545
+ };
1546
+ }
1547
+ const newCharacters = data.replace(invalidCharsRegExp, '');
1548
+ const [from, rawTo] = selection;
1549
+ let to = rawTo + newCharacters.length; // to be conformed with `overwriteMode: replace`
1550
+ const newPossibleValue = value.slice(0, from) + newCharacters + value.slice(to);
1551
+ const { validatedTimeString, updatedTimeSelection } = validateTimeString({
1552
+ timeString: newPossibleValue,
1553
+ paddedMaxValues,
1554
+ offset: 0,
1555
+ selection: [from, to],
1556
+ timeMode,
1557
+ });
1558
+ if (newPossibleValue && !validatedTimeString) {
1559
+ return { elementState, data: '' }; // prevent changes
1560
+ }
1561
+ to = updatedTimeSelection[1];
1562
+ const newData = validatedTimeString.slice(from, to);
1563
+ return {
1564
+ elementState: {
1565
+ selection,
1566
+ value: validatedTimeString.slice(0, from) +
1567
+ '0'.repeat(newData.length) +
1568
+ validatedTimeString.slice(to),
1569
+ },
1570
+ data: newData,
1571
+ };
1572
+ };
1573
+ }
1574
+
1575
+ function maskitoTimeOptionsGenerator({ mode, timeSegmentMaxValues = {}, step = 0, }) {
1576
+ const enrichedTimeSegmentMaxValues = Object.assign(Object.assign({}, DEFAULT_TIME_SEGMENT_MAX_VALUES), timeSegmentMaxValues);
1577
+ return Object.assign(Object.assign({}, MASKITO_DEFAULT_OPTIONS), { mask: Array.from(mode).map(char => TIME_FIXED_CHARACTERS.includes(char) ? char : /\d/), preprocessors: [
1578
+ createFullWidthToHalfWidthPreprocessor(),
1579
+ createColonConvertPreprocessor(),
1580
+ createZeroPlaceholdersPreprocessor(),
1581
+ createMaxValidationPreprocessor(enrichedTimeSegmentMaxValues, mode),
1582
+ ], plugins: [
1583
+ createTimeSegmentsSteppingPlugin({
1584
+ fullMode: mode,
1585
+ step,
1586
+ timeSegmentMaxValues: enrichedTimeSegmentMaxValues,
1587
+ }),
1588
+ ], overwriteMode: 'replace' });
1589
+ }
1590
+
1591
+ const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect;
1592
+
1593
+ function isThenable(x) {
1594
+ return x && typeof x === 'object' && 'then' in x;
1595
+ }
1596
+ /**
1597
+ * Hook for convenient use of Maskito in React
1598
+ * @description For controlled inputs use `onInput` event
1599
+ * @param options options used for creating Maskito
1600
+ * @param elementPredicate function that can help find nested Input or TextArea
1601
+ * @returns ref callback to pass it in React Element
1602
+ * @example
1603
+ * // To avoid unnecessary hook runs with Maskito recreation pass named variables
1604
+ * // good example ✅
1605
+ * useMaskito({ options: maskitoOptions, elementPredicate: maskitoPredicate })
1606
+ *
1607
+ * // bad example ❌
1608
+ * useMaskito({ options: { mask: /^.*$/ }, elementPredicate: () => e.querySelector('input') })
1609
+ */
1610
+ const useMaskito = ({
1611
+ options = null,
1612
+ elementPredicate = MASKITO_DEFAULT_ELEMENT_PREDICATE
1613
+ } = {}) => {
1614
+ const [hostElement, setHostElement] = useState(null);
1615
+ const [element, setElement] = useState(null);
1616
+ const onRefChange = useCallback(node => {
1617
+ setHostElement(node);
1618
+ }, []);
1619
+ const latestPredicateRef = useRef(elementPredicate);
1620
+ latestPredicateRef.current = elementPredicate;
1621
+ useIsomorphicLayoutEffect(() => {
1622
+ if (!hostElement) {
1623
+ return;
1624
+ }
1625
+ const predicate = elementPredicate;
1626
+ const elementOrPromise = predicate(hostElement);
1627
+ if (isThenable(elementOrPromise)) {
1628
+ void elementOrPromise.then(el => {
1629
+ if (latestPredicateRef.current === predicate) {
1630
+ setElement(el);
1631
+ }
1632
+ });
1633
+ } else {
1634
+ setElement(elementOrPromise);
1635
+ }
1636
+ }, [hostElement, elementPredicate, latestPredicateRef]);
1637
+ useIsomorphicLayoutEffect(() => {
1638
+ if (!element || !options) {
1639
+ return;
1640
+ }
1641
+ const maskedElement = new Maskito(element, options);
1642
+ return () => {
1643
+ maskedElement.destroy();
1644
+ };
1645
+ }, [options, element]);
1646
+ return onRefChange;
1647
+ };
1648
+
1649
+ export { MASKITO_DEFAULT_OPTIONS as M, maskitoDateTimeOptionsGenerator as a, maskitoTimeOptionsGenerator as b, maskitoDateOptionsGenerator as m, useMaskito as u };
1650
+ //# sourceMappingURL=index.esm-fa75fa26.js.map