@hh.ru/magritte-ui-input 5.0.21 → 5.0.23
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/BaseInput.js +3 -3
- package/BaseInput.js.map +1 -1
- package/DateTimeInput.js +1 -3
- package/DateTimeInput.js.map +1 -1
- package/UncontrolledDateTimeInput.js +1 -3
- package/UncontrolledDateTimeInput.js.map +1 -1
- package/index.css +308 -308
- package/index.esm-af1b5ff6.js +1650 -0
- package/index.esm-af1b5ff6.js.map +1 -0
- package/index.js +1 -3
- package/index.js.map +1 -1
- package/maskito-exports.d.ts +3 -0
- package/maskito-exports.js +4 -0
- package/maskito-exports.js.map +1 -0
- package/package.json +12 -12
- package/types.d.ts +1 -0
- package/useDateTimeMask.js +1 -3
- package/useDateTimeMask.js.map +1 -1
|
@@ -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-af1b5ff6.js.map
|