View | Details | Raw Unified | Return to bug 33703
Collapse All | Expand All

(-)a/koha-tmpl/intranet-tmpl/lib/maskito/maskito.core.js (+663 lines)
Line 0 Link Here
1
const MASKITO_DEFAULT_ELEMENT_PREDICATE = e => e.querySelector('input,textarea') ||
2
    e;
3
4
const MASKITO_DEFAULT_OPTIONS = {
5
    mask: /^.*$/,
6
    preprocessors: [],
7
    postprocessors: [],
8
    plugins: [],
9
    overwriteMode: 'shift',
10
};
11
12
class MaskHistory {
13
    constructor() {
14
        this.now = null;
15
        this.past = [];
16
        this.future = [];
17
    }
18
    undo() {
19
        const state = this.past.pop();
20
        if (state && this.now) {
21
            this.future.push(this.now);
22
            this.updateElement(state, 'historyUndo');
23
        }
24
    }
25
    redo() {
26
        const state = this.future.pop();
27
        if (state && this.now) {
28
            this.past.push(this.now);
29
            this.updateElement(state, 'historyRedo');
30
        }
31
    }
32
    updateHistory(state) {
33
        if (!this.now) {
34
            this.now = state;
35
            return;
36
        }
37
        const isValueChanged = this.now.value !== state.value;
38
        const isSelectionChanged = this.now.selection.some((item, index) => item !== state.selection[index]);
39
        if (!isValueChanged && !isSelectionChanged) {
40
            return;
41
        }
42
        if (isValueChanged) {
43
            this.past.push(this.now);
44
            this.future = [];
45
        }
46
        this.now = state;
47
    }
48
    updateElement(state, inputType) {
49
        this.now = state;
50
        this.updateElementState(state, { inputType, data: null });
51
    }
52
}
53
54
function areElementValuesEqual(sampleState, ...states) {
55
    return states.every(({ value }) => value === sampleState.value);
56
}
57
function areElementStatesEqual(sampleState, ...states) {
58
    return states.every(({ value, selection }) => value === sampleState.value &&
59
        selection[0] === sampleState.selection[0] &&
60
        selection[1] === sampleState.selection[1]);
61
}
62
63
function applyOverwriteMode({ value, selection }, newCharacters, mode) {
64
    const [from, to] = selection;
65
    const computedMode = typeof mode === 'function' ? mode({ value, selection }) : mode;
66
    return {
67
        value,
68
        selection: computedMode === 'replace' ? [from, from + newCharacters.length] : [from, to],
69
    };
70
}
71
72
function isFixedCharacter(char) {
73
    return typeof char === 'string';
74
}
75
76
function getLeadingFixedCharacters(mask, validatedValuePart, newCharacter, initialElementState) {
77
    let leadingFixedCharacters = '';
78
    for (let i = validatedValuePart.length; i < mask.length; i++) {
79
        const charConstraint = mask[i];
80
        const isInitiallyExisted = (initialElementState === null || initialElementState === void 0 ? void 0 : initialElementState.value[i]) === charConstraint;
81
        if (!isFixedCharacter(charConstraint) ||
82
            (charConstraint === newCharacter && !isInitiallyExisted)) {
83
            return leadingFixedCharacters;
84
        }
85
        leadingFixedCharacters += charConstraint;
86
    }
87
    return leadingFixedCharacters;
88
}
89
90
function validateValueWithMask(value, maskExpression) {
91
    if (Array.isArray(maskExpression)) {
92
        return (value.length === maskExpression.length &&
93
            Array.from(value).every((char, i) => {
94
                const charConstraint = maskExpression[i];
95
                return isFixedCharacter(charConstraint)
96
                    ? char === charConstraint
97
                    : char.match(charConstraint);
98
            }));
99
    }
100
    return maskExpression.test(value);
101
}
102
103
function guessValidValueByPattern(elementState, mask, initialElementState) {
104
    let maskedFrom = null;
105
    let maskedTo = null;
106
    const maskedValue = Array.from(elementState.value).reduce((validatedCharacters, char, charIndex) => {
107
        const leadingCharacters = getLeadingFixedCharacters(mask, validatedCharacters, char, initialElementState);
108
        const newValidatedChars = validatedCharacters + leadingCharacters;
109
        const charConstraint = mask[newValidatedChars.length];
110
        if (isFixedCharacter(charConstraint)) {
111
            return newValidatedChars + charConstraint;
112
        }
113
        if (!char.match(charConstraint)) {
114
            return newValidatedChars;
115
        }
116
        if (maskedFrom === null && charIndex >= elementState.selection[0]) {
117
            maskedFrom = newValidatedChars.length;
118
        }
119
        if (maskedTo === null && charIndex >= elementState.selection[1]) {
120
            maskedTo = newValidatedChars.length;
121
        }
122
        return newValidatedChars + char;
123
    }, '');
124
    const trailingFixedCharacters = getLeadingFixedCharacters(mask, maskedValue, '', initialElementState);
125
    return {
126
        value: validateValueWithMask(maskedValue + trailingFixedCharacters, mask)
127
            ? maskedValue + trailingFixedCharacters
128
            : maskedValue,
129
        selection: [maskedFrom !== null && maskedFrom !== void 0 ? maskedFrom : maskedValue.length, maskedTo !== null && maskedTo !== void 0 ? maskedTo : maskedValue.length],
130
    };
131
}
132
133
function guessValidValueByRegExp({ value, selection }, maskRegExp) {
134
    const [from, to] = selection;
135
    let newFrom = from;
136
    let newTo = to;
137
    const validatedValue = Array.from(value).reduce((validatedValuePart, char, i) => {
138
        const newPossibleValue = validatedValuePart + char;
139
        if (from === i) {
140
            newFrom = validatedValuePart.length;
141
        }
142
        if (to === i) {
143
            newTo = validatedValuePart.length;
144
        }
145
        return newPossibleValue.match(maskRegExp) ? newPossibleValue : validatedValuePart;
146
    }, '');
147
    return { value: validatedValue, selection: [newFrom, newTo] };
148
}
149
150
function calibrateValueByMask(elementState, mask, initialElementState = null) {
151
    if (validateValueWithMask(elementState.value, mask)) {
152
        return elementState;
153
    }
154
    const { value, selection } = Array.isArray(mask)
155
        ? guessValidValueByPattern(elementState, mask, initialElementState)
156
        : guessValidValueByRegExp(elementState, mask);
157
    return {
158
        selection,
159
        value: Array.isArray(mask) ? value.slice(0, mask.length) : value,
160
    };
161
}
162
163
function removeFixedMaskCharacters(initialElementState, mask) {
164
    if (!Array.isArray(mask)) {
165
        return initialElementState;
166
    }
167
    const [from, to] = initialElementState.selection;
168
    const selection = [];
169
    const unmaskedValue = Array.from(initialElementState.value).reduce((rawValue, char, i) => {
170
        const charConstraint = mask[i];
171
        if (i === from) {
172
            selection.push(rawValue.length);
173
        }
174
        if (i === to) {
175
            selection.push(rawValue.length);
176
        }
177
        return isFixedCharacter(charConstraint) && charConstraint === char
178
            ? rawValue
179
            : rawValue + char;
180
    }, '');
181
    if (selection.length < 2) {
182
        selection.push(...new Array(2 - selection.length).fill(unmaskedValue.length));
183
    }
184
    return {
185
        value: unmaskedValue,
186
        selection: [selection[0], selection[1]],
187
    };
188
}
189
190
class MaskModel {
191
    constructor(initialElementState, maskOptions) {
192
        this.initialElementState = initialElementState;
193
        this.maskOptions = maskOptions;
194
        this.value = '';
195
        this.selection = [0, 0];
196
        const { value, selection } = calibrateValueByMask(initialElementState, this.getMaskExpression(initialElementState));
197
        this.value = value;
198
        this.selection = selection;
199
    }
200
    addCharacters([from, to], newCharacters) {
201
        const { value } = this;
202
        const maskExpression = this.getMaskExpression({
203
            value: value.slice(0, from) + newCharacters + value.slice(to),
204
            selection: [from + newCharacters.length, from + newCharacters.length],
205
        });
206
        const initialElementState = { value, selection: [from, to] };
207
        const unmaskedElementState = removeFixedMaskCharacters(initialElementState, maskExpression);
208
        const [unmaskedFrom, unmaskedTo] = applyOverwriteMode(unmaskedElementState, newCharacters, this.maskOptions.overwriteMode).selection;
209
        const newUnmaskedLeadingValuePart = unmaskedElementState.value.slice(0, unmaskedFrom) + newCharacters;
210
        const newCaretIndex = newUnmaskedLeadingValuePart.length;
211
        const maskedElementState = calibrateValueByMask({
212
            value: newUnmaskedLeadingValuePart +
213
                unmaskedElementState.value.slice(unmaskedTo),
214
            selection: [newCaretIndex, newCaretIndex],
215
        }, maskExpression, initialElementState);
216
        const isInvalidCharsInsertion = 
217
        // eslint-disable-next-line @typescript-eslint/prefer-string-starts-ends-with
218
        value.slice(0, unmaskedFrom) ===
219
            calibrateValueByMask({
220
                value: newUnmaskedLeadingValuePart,
221
                selection: [newCaretIndex, newCaretIndex],
222
            }, maskExpression, initialElementState).value;
223
        if (isInvalidCharsInsertion ||
224
            areElementStatesEqual(this, maskedElementState) // If typing new characters does not change value
225
        ) {
226
            throw new Error('Invalid mask value');
227
        }
228
        this.value = maskedElementState.value;
229
        this.selection = maskedElementState.selection;
230
    }
231
    deleteCharacters([from, to]) {
232
        if (from === to || !to) {
233
            return;
234
        }
235
        const { value } = this;
236
        const maskExpression = this.getMaskExpression({
237
            value: value.slice(0, from) + value.slice(to),
238
            selection: [from, from],
239
        });
240
        const initialElementState = { value, selection: [from, to] };
241
        const unmaskedElementState = removeFixedMaskCharacters(initialElementState, maskExpression);
242
        const [unmaskedFrom, unmaskedTo] = unmaskedElementState.selection;
243
        const newUnmaskedValue = unmaskedElementState.value.slice(0, unmaskedFrom) +
244
            unmaskedElementState.value.slice(unmaskedTo);
245
        const maskedElementState = calibrateValueByMask({ value: newUnmaskedValue, selection: [unmaskedFrom, unmaskedFrom] }, maskExpression, initialElementState);
246
        this.value = maskedElementState.value;
247
        this.selection = maskedElementState.selection;
248
    }
249
    getMaskExpression(elementState) {
250
        const { mask } = this.maskOptions;
251
        return typeof mask === 'function' ? mask(elementState) : mask;
252
    }
253
}
254
255
class EventListener {
256
    constructor(element) {
257
        this.element = element;
258
        this.listeners = [];
259
    }
260
    listen(eventType, fn, options) {
261
        const untypedFn = fn;
262
        this.element.addEventListener(eventType, untypedFn, options);
263
        this.listeners.push(() => this.element.removeEventListener(eventType, untypedFn));
264
    }
265
    destroy() {
266
        this.listeners.forEach(stopListen => stopListen());
267
    }
268
}
269
270
const HotkeyModifier = {
271
    CTRL: 1 << 0,
272
    ALT: 1 << 1,
273
    SHIFT: 1 << 2,
274
    META: 1 << 3,
275
};
276
// TODO add variants that can be processed correctly
277
const HotkeyCode = {
278
    Y: 89,
279
    Z: 90,
280
};
281
/**
282
 * Checks if the passed keyboard event match the required hotkey.
283
 *
284
 * @example
285
 * input.addEventListener('keydown', (event) => {
286
 *     if (isHotkey(event, HotkeyModifier.CTRL | HotkeyModifier.SHIFT, HotkeyCode.Z)) {
287
 *         // redo hotkey pressed
288
 *     }
289
 * })
290
 *
291
 * @return will return `true` only if the {@link HotkeyCode} matches and only the necessary
292
 * {@link HotkeyModifier modifiers} have been pressed
293
 */
294
function isHotkey(event, modifiers, hotkeyCode) {
295
    return (event.ctrlKey === !!(modifiers & HotkeyModifier.CTRL) &&
296
        event.altKey === !!(modifiers & HotkeyModifier.ALT) &&
297
        event.shiftKey === !!(modifiers & HotkeyModifier.SHIFT) &&
298
        event.metaKey === !!(modifiers & HotkeyModifier.META) &&
299
        /**
300
         * We intentionally use legacy {@link KeyboardEvent#keyCode `keyCode`} property. It is more
301
         * "keyboard-layout"-independent than {@link KeyboardEvent#key `key`} or {@link KeyboardEvent#code `code`} properties.
302
         * @see {@link https://github.com/taiga-family/maskito/issues/315 `KeyboardEvent#code` issue}
303
         */
304
        // eslint-disable-next-line sonar/deprecation
305
        event.keyCode === hotkeyCode);
306
}
307
308
function isRedo(event) {
309
    return (isHotkey(event, HotkeyModifier.CTRL, HotkeyCode.Y) || // Windows
310
        isHotkey(event, HotkeyModifier.CTRL | HotkeyModifier.SHIFT, HotkeyCode.Z) || // Windows & Android
311
        isHotkey(event, HotkeyModifier.META | HotkeyModifier.SHIFT, HotkeyCode.Z) // macOS & iOS
312
    );
313
}
314
function isUndo(event) {
315
    return (isHotkey(event, HotkeyModifier.CTRL, HotkeyCode.Z) || // Windows & Android
316
        isHotkey(event, HotkeyModifier.META, HotkeyCode.Z) // macOS & iOS
317
    );
318
}
319
320
/**
321
 * Sets value to element, and dispatches input event
322
 * if you passed ELementState, it also sets selection range
323
 *
324
 * @example
325
 * maskitoUpdateElement(input, newValue);
326
 * maskitoUpdateElement(input, elementState);
327
 *
328
 * @see {@link https://github.com/taiga-family/maskito/issues/804 issue}
329
 *
330
 * @return void
331
 */
332
function maskitoUpdateElement(element, valueOrElementState) {
333
    var _a;
334
    if (typeof valueOrElementState === 'string') {
335
        element.value = valueOrElementState;
336
    }
337
    else {
338
        const [from, to] = valueOrElementState.selection;
339
        element.value = valueOrElementState.value;
340
        (_a = element.setSelectionRange) === null || _a === void 0 ? void 0 : _a.call(element, from, to);
341
    }
342
    element.dispatchEvent(new Event('input', 
343
    /**
344
     * React handles this event only on bubbling phase
345
     *
346
     * here is the list of events that are processed in the capture stage, others are processed in the bubbling stage
347
     * https://github.com/facebook/react/blob/cb2439624f43c510007f65aea5c50a8bb97917e4/packages/react-dom-bindings/src/events/DOMPluginEventSystem.js#L222
348
     */
349
    { bubbles: true }));
350
}
351
352
function getLineSelection({ value, selection }, isForward) {
353
    const [from, to] = selection;
354
    if (from !== to) {
355
        return [from, to];
356
    }
357
    const nearestBreak = isForward
358
        ? value.slice(from).indexOf('\n') + 1 || value.length
359
        : value.slice(0, to).lastIndexOf('\n') + 1;
360
    const selectFrom = isForward ? from : nearestBreak;
361
    const selectTo = isForward ? nearestBreak : to;
362
    return [selectFrom, selectTo];
363
}
364
365
function getNotEmptySelection({ value, selection }, isForward) {
366
    const [from, to] = selection;
367
    if (from !== to) {
368
        return [from, to];
369
    }
370
    const notEmptySelection = isForward ? [from, to + 1] : [from - 1, to];
371
    return notEmptySelection.map(x => Math.min(Math.max(x, 0), value.length));
372
}
373
374
const TRAILING_SPACES_REG = /\s+$/g;
375
const LEADING_SPACES_REG = /^\s+/g;
376
const SPACE_REG = /\s/;
377
function getWordSelection({ value, selection }, isForward) {
378
    const [from, to] = selection;
379
    if (from !== to) {
380
        return [from, to];
381
    }
382
    if (isForward) {
383
        const valueAfterSelectionStart = value.slice(from);
384
        const [leadingSpaces] = valueAfterSelectionStart.match(LEADING_SPACES_REG) || [
385
            '',
386
        ];
387
        const nearestWordEndIndex = valueAfterSelectionStart
388
            .trimStart()
389
            .search(SPACE_REG);
390
        return [
391
            from,
392
            nearestWordEndIndex !== -1
393
                ? from + leadingSpaces.length + nearestWordEndIndex
394
                : value.length,
395
        ];
396
    }
397
    const valueBeforeSelectionEnd = value.slice(0, to);
398
    const [trailingSpaces] = valueBeforeSelectionEnd.match(TRAILING_SPACES_REG) || [''];
399
    const selectedWordLength = valueBeforeSelectionEnd
400
        .trimEnd()
401
        .split('')
402
        .reverse()
403
        .findIndex(char => char.match(SPACE_REG));
404
    return [
405
        selectedWordLength !== -1 ? to - trailingSpaces.length - selectedWordLength : 0,
406
        to,
407
    ];
408
}
409
410
/* eslint-disable @typescript-eslint/ban-types */
411
/**
412
 * @internal
413
 */
414
function maskitoPipe(processors = []) {
415
    return (initialData, ...readonlyArgs) => processors.reduce((data, fn) => (Object.assign(Object.assign({}, data), fn(data, ...readonlyArgs))), initialData);
416
}
417
418
function maskitoTransform(valueOrState, maskitoOptions) {
419
    const options = Object.assign(Object.assign({}, MASKITO_DEFAULT_OPTIONS), maskitoOptions);
420
    const preprocessor = maskitoPipe(options.preprocessors);
421
    const postprocessor = maskitoPipe(options.postprocessors);
422
    const initialElementState = typeof valueOrState === 'string'
423
        ? { value: valueOrState, selection: [0, 0] }
424
        : valueOrState;
425
    const { elementState } = preprocessor({ elementState: initialElementState, data: '' }, 'validation');
426
    const maskModel = new MaskModel(elementState, options);
427
    const { value, selection } = postprocessor(maskModel, initialElementState);
428
    return typeof valueOrState === 'string' ? value : { value, selection };
429
}
430
431
function maskitoInitialCalibrationPlugin(customOptions) {
432
    return (element, options) => {
433
        const from = element.selectionStart || 0;
434
        const to = element.selectionEnd || 0;
435
        maskitoUpdateElement(element, {
436
            value: maskitoTransform(element.value, customOptions || options),
437
            selection: [from, to],
438
        });
439
    };
440
}
441
442
function maskitoStrictCompositionPlugin() {
443
    return (element, maskitoOptions) => {
444
        const listener = (event) => {
445
            if (event.inputType !== 'insertCompositionText') {
446
                return;
447
            }
448
            const selection = [
449
                element.selectionStart || 0,
450
                element.selectionEnd || 0,
451
            ];
452
            const elementState = {
453
                selection,
454
                value: element.value,
455
            };
456
            const validatedState = maskitoTransform(elementState, maskitoOptions);
457
            if (!areElementStatesEqual(elementState, validatedState)) {
458
                event.preventDefault();
459
                maskitoUpdateElement(element, validatedState);
460
            }
461
        };
462
        element.addEventListener('input', listener);
463
        return () => element.removeEventListener('input', listener);
464
    };
465
}
466
467
class Maskito extends MaskHistory {
468
    constructor(element, maskitoOptions) {
469
        super();
470
        this.element = element;
471
        this.maskitoOptions = maskitoOptions;
472
        this.isTextArea = this.element.nodeName === 'TEXTAREA';
473
        this.eventListener = new EventListener(this.element);
474
        this.options = Object.assign(Object.assign({}, MASKITO_DEFAULT_OPTIONS), this.maskitoOptions);
475
        this.preprocessor = maskitoPipe(this.options.preprocessors);
476
        this.postprocessor = maskitoPipe(this.options.postprocessors);
477
        this.teardowns = this.options.plugins.map(plugin => plugin(this.element, this.options));
478
        this.updateHistory(this.elementState);
479
        this.eventListener.listen('keydown', event => {
480
            if (isRedo(event)) {
481
                event.preventDefault();
482
                return this.redo();
483
            }
484
            if (isUndo(event)) {
485
                event.preventDefault();
486
                return this.undo();
487
            }
488
        });
489
        this.eventListener.listen('beforeinput', event => {
490
            const isForward = event.inputType.includes('Forward');
491
            this.updateHistory(this.elementState);
492
            switch (event.inputType) {
493
                // historyUndo/historyRedo will not be triggered if value was modified programmatically
494
                case 'historyUndo':
495
                    event.preventDefault();
496
                    return this.undo();
497
                case 'historyRedo':
498
                    event.preventDefault();
499
                    return this.redo();
500
                case 'deleteByCut':
501
                case 'deleteContentBackward':
502
                case 'deleteContentForward':
503
                    return this.handleDelete({
504
                        event,
505
                        isForward,
506
                        selection: getNotEmptySelection(this.elementState, isForward),
507
                    });
508
                case 'deleteWordForward':
509
                case 'deleteWordBackward':
510
                    return this.handleDelete({
511
                        event,
512
                        isForward,
513
                        selection: getWordSelection(this.elementState, isForward),
514
                        force: true,
515
                    });
516
                case 'deleteSoftLineBackward':
517
                case 'deleteSoftLineForward':
518
                case 'deleteHardLineBackward':
519
                case 'deleteHardLineForward':
520
                    return this.handleDelete({
521
                        event,
522
                        isForward,
523
                        selection: getLineSelection(this.elementState, isForward),
524
                        force: true,
525
                    });
526
                case 'insertCompositionText':
527
                    return; // will be handled inside `compositionend` event
528
                case 'insertLineBreak':
529
                    return this.handleEnter(event);
530
                case 'insertFromPaste':
531
                case 'insertText':
532
                case 'insertFromDrop':
533
                default:
534
                    return this.handleInsert(event, event.data || '');
535
            }
536
        });
537
        this.eventListener.listen('input', ({ inputType }) => {
538
            if (inputType === 'insertCompositionText') {
539
                return; // will be handled inside `compositionend` event
540
            }
541
            this.ensureValueFitsMask();
542
            this.updateHistory(this.elementState);
543
        });
544
        this.eventListener.listen('compositionend', () => {
545
            this.ensureValueFitsMask();
546
            this.updateHistory(this.elementState);
547
        });
548
    }
549
    get elementState() {
550
        const { value, selectionStart, selectionEnd } = this.element;
551
        return {
552
            value,
553
            selection: [selectionStart || 0, selectionEnd || 0],
554
        };
555
    }
556
    get maxLength() {
557
        const { maxLength } = this.element;
558
        return maxLength === -1 ? Infinity : maxLength;
559
    }
560
    destroy() {
561
        this.eventListener.destroy();
562
        this.teardowns.forEach(teardown => teardown === null || teardown === void 0 ? void 0 : teardown());
563
    }
564
    updateElementState({ value, selection }, eventInit = {
565
        inputType: 'insertText',
566
        data: null,
567
    }) {
568
        const initialValue = this.elementState.value;
569
        this.updateValue(value);
570
        this.updateSelectionRange(selection);
571
        if (initialValue !== value) {
572
            this.dispatchInputEvent(eventInit);
573
        }
574
    }
575
    updateSelectionRange([from, to]) {
576
        var _a, _b;
577
        if (this.element.selectionStart !== from || this.element.selectionEnd !== to) {
578
            (_b = (_a = this.element).setSelectionRange) === null || _b === void 0 ? void 0 : _b.call(_a, from, to);
579
        }
580
    }
581
    updateValue(value) {
582
        this.element.value = value;
583
    }
584
    ensureValueFitsMask() {
585
        this.updateElementState(maskitoTransform(this.elementState, this.options));
586
    }
587
    dispatchInputEvent(eventInit = {
588
        inputType: 'insertText',
589
        data: null,
590
    }) {
591
        if (globalThis.InputEvent) {
592
            this.element.dispatchEvent(new InputEvent('input', Object.assign(Object.assign({}, eventInit), { bubbles: true, cancelable: false })));
593
        }
594
    }
595
    handleDelete({ event, selection, isForward, force = false, }) {
596
        const initialState = {
597
            value: this.elementState.value,
598
            selection,
599
        };
600
        const [initialFrom, initialTo] = initialState.selection;
601
        const { elementState } = this.preprocessor({
602
            elementState: initialState,
603
            data: '',
604
        }, isForward ? 'deleteForward' : 'deleteBackward');
605
        const maskModel = new MaskModel(elementState, this.options);
606
        const [from, to] = elementState.selection;
607
        maskModel.deleteCharacters([from, to]);
608
        const newElementState = this.postprocessor(maskModel, initialState);
609
        const newPossibleValue = initialState.value.slice(0, initialFrom) +
610
            initialState.value.slice(initialTo);
611
        if (newPossibleValue === newElementState.value && !force) {
612
            return;
613
        }
614
        event.preventDefault();
615
        if (areElementValuesEqual(initialState, elementState, maskModel, newElementState)) {
616
            // User presses Backspace/Delete for the fixed value
617
            return this.updateSelectionRange(isForward ? [to, to] : [from, from]);
618
        }
619
        this.updateElementState(newElementState, {
620
            inputType: event.inputType,
621
            data: null,
622
        });
623
        this.updateHistory(newElementState);
624
    }
625
    handleInsert(event, data) {
626
        const initialElementState = this.elementState;
627
        const { elementState, data: insertedText = data } = this.preprocessor({
628
            data,
629
            elementState: initialElementState,
630
        }, 'insert');
631
        const maskModel = new MaskModel(elementState, this.options);
632
        try {
633
            maskModel.addCharacters(elementState.selection, insertedText);
634
        }
635
        catch (_a) {
636
            return event.preventDefault();
637
        }
638
        const [from, to] = elementState.selection;
639
        const newPossibleValue = initialElementState.value.slice(0, from) +
640
            data +
641
            initialElementState.value.slice(to);
642
        const newElementState = this.postprocessor(maskModel, initialElementState);
643
        if (newElementState.value.length > this.maxLength) {
644
            return event.preventDefault();
645
        }
646
        if (newPossibleValue !== newElementState.value) {
647
            event.preventDefault();
648
            this.updateElementState(newElementState, {
649
                data,
650
                inputType: event.inputType,
651
            });
652
            this.updateHistory(newElementState);
653
        }
654
    }
655
    handleEnter(event) {
656
        if (this.isTextArea) {
657
            this.handleInsert(event, '\n');
658
        }
659
    }
660
}
661
662
//export { MASKITO_DEFAULT_ELEMENT_PREDICATE, MASKITO_DEFAULT_OPTIONS, Maskito, maskitoInitialCalibrationPlugin, maskitoPipe, maskitoStrictCompositionPlugin, maskitoTransform, maskitoUpdateElement };
663
(-)a/koha-tmpl/intranet-tmpl/lib/maskito/maskito.kit.js (+1609 lines)
Line 0 Link Here
1
//import { maskitoUpdateElement, MASKITO_DEFAULT_OPTIONS, maskitoTransform } from '@maskito/core';
2
3
const DEFAULT_DECIMAL_PSEUDO_SEPARATORS = ['.', ',', 'б', 'ю'];
4
5
const DEFAULT_MIN_DATE = new Date('0001-01-01');
6
const DEFAULT_MAX_DATE = new Date('9999-12-31');
7
8
const DEFAULT_TIME_SEGMENT_MAX_VALUES = {
9
    hours: 23,
10
    minutes: 59,
11
    seconds: 59,
12
    milliseconds: 999,
13
};
14
15
/**
16
 * {@link https://unicode-table.com/en/00A0/ Non-breaking space}.
17
 */
18
const CHAR_NO_BREAK_SPACE = '\u00A0';
19
/**
20
 * {@link https://symbl.cc/en/200B/ Zero width space}.
21
 */
22
const CHAR_ZERO_WIDTH_SPACE = '\u200B';
23
/**
24
 * {@link https://unicode-table.com/en/2013/ EN dash}
25
 * is used to indicate a range of numbers or a span of time.
26
 * @example 2006–2022
27
 */
28
const CHAR_EN_DASH = '\u2013';
29
/**
30
 * {@link https://unicode-table.com/en/2014/ EM dash}
31
 * is used to mark a break in a sentence.
32
 * @example Taiga UI — powerful set of open source components for Angular
33
 * ___
34
 * Don't confuse with {@link CHAR_EN_DASH} or {@link CHAR_HYPHEN}!
35
 */
36
const CHAR_EM_DASH = '\u2014';
37
/**
38
 * {@link https://unicode-table.com/en/002D/ Hyphen (minus sign)}
39
 * is used to combine words.
40
 * @example well-behaved
41
 * ___
42
 * Don't confuse with {@link CHAR_EN_DASH} or {@link CHAR_EM_DASH}!
43
 */
44
const CHAR_HYPHEN = '\u002D';
45
/**
46
 * {@link https://unicode-table.com/en/2212/ Minus}
47
 * is used as math operator symbol or before negative digits.
48
 * ---
49
 * Can be used as `&minus;`. Don't confuse with {@link CHAR_HYPHEN}
50
 */
51
const CHAR_MINUS = '\u2212';
52
/**
53
 * {@link https://symbl.cc/en/30FC/ Katakana-Hiragana Prolonged Sound Mark}
54
 * is used as prolonged sounds in Japanese.
55
 */
56
const CHAR_JP_HYPHEN = '\u30FC';
57
58
const POSSIBLE_DATE_RANGE_SEPARATOR = [
59
    CHAR_HYPHEN,
60
    CHAR_EN_DASH,
61
    CHAR_EM_DASH,
62
    CHAR_MINUS,
63
];
64
const POSSIBLE_DATE_TIME_SEPARATOR = [',', ' '];
65
66
const TIME_FIXED_CHARACTERS = [':', '.'];
67
68
const TIME_SEGMENT_VALUE_LENGTHS = {
69
    hours: 2,
70
    minutes: 2,
71
    seconds: 2,
72
    milliseconds: 3,
73
};
74
75
/**
76
 * Clamps a value between two inclusive limits
77
 *
78
 * @param value
79
 * @param min lower limit
80
 * @param max upper limit
81
 */
82
function clamp(value, min, max) {
83
    const clampedValue = Math.min(Number(max), Math.max(Number(min), Number(value)));
84
    return (value instanceof Date ? new Date(clampedValue) : clampedValue);
85
}
86
87
function appendDate(initialDate, { day, month, year } = {}) {
88
    const date = new Date(initialDate);
89
    if (day) {
90
        date.setDate(date.getDate() + day);
91
    }
92
    if (month) {
93
        date.setMonth(date.getMonth() + month);
94
    }
95
    if (year) {
96
        date.setFullYear(date.getFullYear() + year);
97
    }
98
    return date;
99
}
100
101
const getDateSegmentValueLength = (dateString) => {
102
    var _a, _b, _c;
103
    return ({
104
        day: ((_a = dateString.match(/d/g)) === null || _a === void 0 ? void 0 : _a.length) || 0,
105
        month: ((_b = dateString.match(/m/g)) === null || _b === void 0 ? void 0 : _b.length) || 0,
106
        year: ((_c = dateString.match(/y/g)) === null || _c === void 0 ? void 0 : _c.length) || 0,
107
    });
108
};
109
110
function dateToSegments(date) {
111
    return {
112
        day: String(date.getDate()).padStart(2, '0'),
113
        month: String(date.getMonth() + 1).padStart(2, '0'),
114
        year: String(date.getFullYear()).padStart(4, '0'),
115
        hours: String(date.getHours()).padStart(2, '0'),
116
        minutes: String(date.getMinutes()).padStart(2, '0'),
117
        seconds: String(date.getSeconds()).padStart(2, '0'),
118
        milliseconds: String(date.getMilliseconds()).padStart(3, '0'),
119
    };
120
}
121
122
function isDateStringComplete(dateString, dateModeTemplate) {
123
    if (dateString.length < dateModeTemplate.length) {
124
        return false;
125
    }
126
    return dateString.split(/\D/).every(segment => !segment.match(/^0+$/));
127
}
128
129
function parseDateRangeString(dateRange, dateModeTemplate, rangeSeparator) {
130
    const digitsInDate = dateModeTemplate.replace(/\W/g, '').length;
131
    return (dateRange
132
        .replace(rangeSeparator, '')
133
        .match(new RegExp(`(\\D*\\d[^\\d\\s]*){1,${digitsInDate}}`, 'g')) || []);
134
}
135
136
function parseDateString(dateString, fullMode) {
137
    const cleanMode = fullMode.replace(/[^dmy]/g, '');
138
    const onlyDigitsDate = dateString.replace(/\D+/g, '');
139
    const dateSegments = {
140
        day: onlyDigitsDate.slice(cleanMode.indexOf('d'), cleanMode.lastIndexOf('d') + 1),
141
        month: onlyDigitsDate.slice(cleanMode.indexOf('m'), cleanMode.lastIndexOf('m') + 1),
142
        year: onlyDigitsDate.slice(cleanMode.indexOf('y'), cleanMode.lastIndexOf('y') + 1),
143
    };
144
    return Object.fromEntries(Object.entries(dateSegments)
145
        .filter(([_, value]) => Boolean(value))
146
        .sort(([a], [b]) => fullMode.toLowerCase().indexOf(a[0]) >
147
        fullMode.toLowerCase().indexOf(b[0])
148
        ? 1
149
        : -1));
150
}
151
152
function segmentsToDate(parsedDate, parsedTime) {
153
    var _a, _b, _c, _d, _e, _f, _g;
154
    const year = ((_a = parsedDate.year) === null || _a === void 0 ? void 0 : _a.length) === 2 ? `20${parsedDate.year}` : parsedDate.year;
155
    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'));
156
    // needed for years less than 1900
157
    date.setFullYear(Number(year !== null && year !== void 0 ? year : '0'));
158
    return date;
159
}
160
161
const DATE_TIME_SEPARATOR = ', ';
162
163
function toDateString({ day, month, year, hours, minutes, seconds, milliseconds, }, dateMode, timeMode) {
164
    var _a;
165
    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;
166
    const fullMode = dateMode + (timeMode ? DATE_TIME_SEPARATOR + timeMode : '');
167
    return fullMode
168
        .replace(/d+/g, day !== null && day !== void 0 ? day : '')
169
        .replace(/m+/g, month !== null && month !== void 0 ? month : '')
170
        .replace(/y+/g, safeYear !== null && safeYear !== void 0 ? safeYear : '')
171
        .replace(/H+/g, hours !== null && hours !== void 0 ? hours : '')
172
        .replace(/MSS/g, milliseconds !== null && milliseconds !== void 0 ? milliseconds : '')
173
        .replace(/M+/g, minutes !== null && minutes !== void 0 ? minutes : '')
174
        .replace(/S+/g, seconds !== null && seconds !== void 0 ? seconds : '')
175
        .replace(/^\D+/g, '')
176
        .replace(/\D+$/g, '');
177
}
178
179
function padWithZeroesUntilValid(segmentValue, paddedMaxValue, prefixedZeroesCount = 0) {
180
    if (Number(segmentValue.padEnd(paddedMaxValue.length, '0')) <= Number(paddedMaxValue)) {
181
        return { validatedSegmentValue: segmentValue, prefixedZeroesCount };
182
    }
183
    if (segmentValue.endsWith('0')) {
184
        // 00:|00 => Type 9 => 00:09|
185
        return padWithZeroesUntilValid(`0${segmentValue.slice(0, paddedMaxValue.length - 1)}`, paddedMaxValue, prefixedZeroesCount + 1);
186
    }
187
    // |19:00 => Type 2 => 2|0:00
188
    return padWithZeroesUntilValid(`${segmentValue.slice(0, paddedMaxValue.length - 1)}0`, paddedMaxValue, prefixedZeroesCount);
189
}
190
191
const dateMaxValues = {
192
    day: 31,
193
    month: 12,
194
    year: 9999,
195
};
196
function validateDateString({ dateString, dateModeTemplate, offset, selection: [from, to], }) {
197
    const parsedDate = parseDateString(dateString, dateModeTemplate);
198
    const dateSegments = Object.entries(parsedDate);
199
    const validatedDateSegments = {};
200
    let paddedZeroes = 0;
201
    for (const [segmentName, segmentValue] of dateSegments) {
202
        const validatedDate = toDateString(validatedDateSegments, dateModeTemplate);
203
        const maxSegmentValue = dateMaxValues[segmentName];
204
        const fantomSeparator = validatedDate.length && 1;
205
        const lastSegmentDigitIndex = offset +
206
            validatedDate.length +
207
            fantomSeparator +
208
            getDateSegmentValueLength(dateModeTemplate)[segmentName];
209
        const isLastSegmentDigitAdded = lastSegmentDigitIndex >= from && lastSegmentDigitIndex === to;
210
        if (isLastSegmentDigitAdded && Number(segmentValue) > Number(maxSegmentValue)) {
211
            // 3|1.10.2010 => Type 9 => 3|1.10.2010
212
            return { validatedDateString: '', updatedSelection: [from, to] }; // prevent changes
213
        }
214
        if (isLastSegmentDigitAdded && Number(segmentValue) < 1) {
215
            // 31.0|1.2010 => Type 0 => 31.0|1.2010
216
            return { validatedDateString: '', updatedSelection: [from, to] }; // prevent changes
217
        }
218
        const { validatedSegmentValue, prefixedZeroesCount } = padWithZeroesUntilValid(segmentValue, `${maxSegmentValue}`);
219
        paddedZeroes += prefixedZeroesCount;
220
        validatedDateSegments[segmentName] = validatedSegmentValue;
221
    }
222
    const validatedDateString = toDateString(validatedDateSegments, dateModeTemplate);
223
    const addedDateSegmentSeparators = validatedDateString.length - dateString.length;
224
    return {
225
        validatedDateString,
226
        updatedSelection: [
227
            from + paddedZeroes + addedDateSegmentSeparators,
228
            to + paddedZeroes + addedDateSegmentSeparators,
229
        ],
230
    };
231
}
232
233
/**
234
 * Copy-pasted solution from lodash
235
 * @see https://lodash.com/docs/4.17.15#escapeRegExp
236
 */
237
const reRegExpChar = /[\\^$.*+?()[\]{}|]/g;
238
const reHasRegExpChar = new RegExp(reRegExpChar.source);
239
function escapeRegExp(str) {
240
    return str && reHasRegExpChar.test(str) ? str.replace(reRegExpChar, '\\$&') : str;
241
}
242
243
function extractAffixes(value, { prefix, postfix }) {
244
    var _a, _b;
245
    const prefixRegExp = new RegExp(`^${escapeRegExp(prefix)}`);
246
    const postfixRegExp = new RegExp(`${escapeRegExp(postfix)}$`);
247
    const [extractedPrefix = ''] = (_a = value.match(prefixRegExp)) !== null && _a !== void 0 ? _a : [];
248
    const [extractedPostfix = ''] = (_b = value.match(postfixRegExp)) !== null && _b !== void 0 ? _b : [];
249
    const cleanValue = value.replace(prefixRegExp, '').replace(postfixRegExp, '');
250
    return { extractedPrefix, extractedPostfix, cleanValue };
251
}
252
253
function findCommonBeginningSubstr(a, b) {
254
    let res = '';
255
    for (let i = 0; i < a.length; i++) {
256
        if (a[i] !== b[i]) {
257
            return res;
258
        }
259
        res += a[i];
260
    }
261
    return res;
262
}
263
264
/**
265
 * Returns current active element, including shadow dom
266
 *
267
 * @return element or null
268
 */
269
function getFocused({ activeElement }) {
270
    if (!(activeElement === null || activeElement === void 0 ? void 0 : activeElement.shadowRoot)) {
271
        return activeElement;
272
    }
273
    let element = activeElement.shadowRoot.activeElement;
274
    while (element === null || element === void 0 ? void 0 : element.shadowRoot) {
275
        element = element.shadowRoot.activeElement;
276
    }
277
    return element;
278
}
279
280
function identity(x) {
281
    return x;
282
}
283
284
function isEmpty(entity) {
285
    return !entity || (typeof entity === 'object' && Object.keys(entity).length === 0);
286
}
287
288
function raiseSegmentValueToMin(segments, fullMode) {
289
    const segmentsLength = getDateSegmentValueLength(fullMode);
290
    return Object.fromEntries(Object.entries(segments).map(([key, value]) => {
291
        const segmentLength = segmentsLength[key];
292
        return [
293
            key,
294
            value.length === segmentLength && value.match(/^0+$/)
295
                ? '1'.padStart(segmentLength, '0')
296
                : value,
297
        ];
298
    }));
299
}
300
301
function createMinMaxDatePostprocessor({ dateModeTemplate, min = DEFAULT_MIN_DATE, max = DEFAULT_MAX_DATE, rangeSeparator = '', dateSegmentSeparator = '.', }) {
302
    return ({ value, selection }) => {
303
        const endsWithRangeSeparator = rangeSeparator && value.endsWith(rangeSeparator);
304
        const dateStrings = parseDateRangeString(value, dateModeTemplate, rangeSeparator);
305
        let validatedValue = '';
306
        for (const dateString of dateStrings) {
307
            validatedValue += validatedValue ? rangeSeparator : '';
308
            const parsedDate = parseDateString(dateString, dateModeTemplate);
309
            if (!isDateStringComplete(dateString, dateModeTemplate)) {
310
                const fixedDate = raiseSegmentValueToMin(parsedDate, dateModeTemplate);
311
                const fixedValue = toDateString(fixedDate, dateModeTemplate);
312
                const tail = dateString.endsWith(dateSegmentSeparator)
313
                    ? dateSegmentSeparator
314
                    : '';
315
                validatedValue += fixedValue + tail;
316
                continue;
317
            }
318
            const date = segmentsToDate(parsedDate);
319
            const clampedDate = clamp(date, min, max);
320
            validatedValue += toDateString(dateToSegments(clampedDate), dateModeTemplate);
321
        }
322
        return {
323
            selection,
324
            value: validatedValue + (endsWithRangeSeparator ? rangeSeparator : ''),
325
        };
326
    };
327
}
328
329
function normalizeDatePreprocessor({ dateModeTemplate, dateSegmentsSeparator, rangeSeparator = '', }) {
330
    return ({ elementState, data }) => {
331
        const separator = rangeSeparator
332
            ? new RegExp(`${rangeSeparator}|-`)
333
            : DATE_TIME_SEPARATOR;
334
        const possibleDates = data.split(separator);
335
        const dates = data.includes(DATE_TIME_SEPARATOR)
336
            ? [possibleDates[0]]
337
            : possibleDates;
338
        if (dates.every(date => date.trim().split(/\D/).length ===
339
            dateModeTemplate.split(dateSegmentsSeparator).length)) {
340
            const newData = dates
341
                .map(date => normalizeDateString(date, dateModeTemplate, dateSegmentsSeparator))
342
                .join(rangeSeparator);
343
            return {
344
                elementState,
345
                data: `${newData}${data.includes(DATE_TIME_SEPARATOR)
346
                    ? DATE_TIME_SEPARATOR + possibleDates[1] || ''
347
                    : ''}`,
348
            };
349
        }
350
        return { elementState, data };
351
    };
352
}
353
function normalizeDateString(dateString, template, separator) {
354
    const dateSegments = dateString.split(/\D/);
355
    const templateSegments = template.split(separator);
356
    const normalizedSegments = dateSegments.map((segment, index) => index === templateSegments.length - 1
357
        ? segment
358
        : segment.padStart(templateSegments[index].length, '0'));
359
    return normalizedSegments.join(separator);
360
}
361
362
function maskitoPostfixPostprocessorGenerator(postfix) {
363
    const postfixRE = new RegExp(`${escapeRegExp(postfix)}$`);
364
    return postfix
365
        ? ({ value, selection }, initialElementState) => {
366
            if (!value && !initialElementState.value.endsWith(postfix)) {
367
                // cases when developer wants input to be empty (programmatically)
368
                return { value, selection };
369
            }
370
            if (!value.endsWith(postfix) &&
371
                !initialElementState.value.endsWith(postfix)) {
372
                return { selection, value: value + postfix };
373
            }
374
            const initialValueBeforePostfix = initialElementState.value.replace(postfixRE, '');
375
            const postfixWasModified = initialElementState.selection[1] >= initialValueBeforePostfix.length;
376
            const alreadyExistedValueBeforePostfix = findCommonBeginningSubstr(initialValueBeforePostfix, value);
377
            return {
378
                selection,
379
                value: Array.from(postfix)
380
                    .reverse()
381
                    .reduce((newValue, char, index) => {
382
                    const i = newValue.length - 1 - index;
383
                    const isInitiallyMirroredChar = alreadyExistedValueBeforePostfix[i] === char &&
384
                        postfixWasModified;
385
                    return newValue[i] !== char || isInitiallyMirroredChar
386
                        ? newValue.slice(0, i + 1) + char + newValue.slice(i + 1)
387
                        : newValue;
388
                }, value),
389
            };
390
        }
391
        : identity;
392
}
393
394
function maskitoPrefixPostprocessorGenerator(prefix) {
395
    return prefix
396
        ? ({ value, selection }, initialElementState) => {
397
            if (value.startsWith(prefix) || // already valid
398
                (!value && !initialElementState.value.startsWith(prefix)) // cases when developer wants input to be empty
399
            ) {
400
                return { value, selection };
401
            }
402
            const [from, to] = selection;
403
            const prefixedValue = Array.from(prefix).reduce((modifiedValue, char, i) => modifiedValue[i] === char
404
                ? modifiedValue
405
                : modifiedValue.slice(0, i) + char + modifiedValue.slice(i), value);
406
            const addedCharsCount = prefixedValue.length - value.length;
407
            return {
408
                selection: [from + addedCharsCount, to + addedCharsCount],
409
                value: prefixedValue,
410
            };
411
        }
412
        : identity;
413
}
414
415
function createValidDatePreprocessor({ dateModeTemplate, dateSegmentsSeparator, rangeSeparator = '', }) {
416
    return ({ elementState, data }) => {
417
        const { value, selection } = elementState;
418
        if (data === dateSegmentsSeparator) {
419
            return {
420
                elementState,
421
                data: selection[0] === value.length ? data : '',
422
            };
423
        }
424
        if (POSSIBLE_DATE_RANGE_SEPARATOR.includes(data)) {
425
            return { elementState, data: rangeSeparator };
426
        }
427
        const newCharacters = data.replace(new RegExp(`[^\\d${escapeRegExp(dateSegmentsSeparator)}${rangeSeparator}]`, 'g'), '');
428
        if (!newCharacters) {
429
            return { elementState, data: '' };
430
        }
431
        const [from, rawTo] = selection;
432
        let to = rawTo + data.length;
433
        const newPossibleValue = value.slice(0, from) + newCharacters + value.slice(to);
434
        const dateStrings = parseDateRangeString(newPossibleValue, dateModeTemplate, rangeSeparator);
435
        let validatedValue = '';
436
        const hasRangeSeparator = Boolean(rangeSeparator) && newPossibleValue.includes(rangeSeparator);
437
        for (const dateString of dateStrings) {
438
            const { validatedDateString, updatedSelection } = validateDateString({
439
                dateString,
440
                dateModeTemplate,
441
                offset: validatedValue
442
                    ? validatedValue.length + rangeSeparator.length
443
                    : 0,
444
                selection: [from, to],
445
            });
446
            if (dateString && !validatedDateString) {
447
                return { elementState, data: '' }; // prevent changes
448
            }
449
            to = updatedSelection[1];
450
            validatedValue +=
451
                hasRangeSeparator && validatedValue
452
                    ? rangeSeparator + validatedDateString
453
                    : validatedDateString;
454
        }
455
        const newData = validatedValue.slice(from, to);
456
        return {
457
            elementState: {
458
                selection,
459
                value: validatedValue.slice(0, from) +
460
                    newData
461
                        .split(dateSegmentsSeparator)
462
                        .map(segment => '0'.repeat(segment.length))
463
                        .join(dateSegmentsSeparator) +
464
                    validatedValue.slice(to),
465
            },
466
            data: newData,
467
        };
468
    };
469
}
470
471
function maskitoEventHandler(name, handler, eventListenerOptions) {
472
    return (element, maskitoOptions) => {
473
        const listener = () => handler(element, maskitoOptions);
474
        element.addEventListener(name, listener, eventListenerOptions);
475
        return () => element.removeEventListener(name, listener, eventListenerOptions);
476
    };
477
}
478
479
function maskitoAddOnFocusPlugin(value) {
480
    return maskitoEventHandler('focus', element => {
481
        if (!element.value) {
482
            maskitoUpdateElement(element, value);
483
        }
484
    });
485
}
486
487
function maskitoCaretGuard(guard) {
488
    return element => {
489
        const document = element.ownerDocument;
490
        let isPointerDown = 0;
491
        const onPointerDown = () => isPointerDown++;
492
        const onPointerUp = () => {
493
            isPointerDown = Math.max(--isPointerDown, 0);
494
        };
495
        const listener = () => {
496
            if (getFocused(document) !== element) {
497
                return;
498
            }
499
            if (isPointerDown) {
500
                return document.addEventListener('mouseup', listener, {
501
                    once: true,
502
                    passive: true,
503
                });
504
            }
505
            const start = element.selectionStart || 0;
506
            const end = element.selectionEnd || 0;
507
            const [fromLimit, toLimit] = guard(element.value, [start, end]);
508
            if (fromLimit > start || toLimit < end) {
509
                element.setSelectionRange(clamp(start, fromLimit, toLimit), clamp(end, fromLimit, toLimit));
510
            }
511
        };
512
        document.addEventListener('selectionchange', listener, { passive: true });
513
        element.addEventListener('mousedown', onPointerDown, { passive: true });
514
        document.addEventListener('mouseup', onPointerUp, { passive: true });
515
        return () => {
516
            document.removeEventListener('selectionchange', listener);
517
            document.removeEventListener('mousedown', onPointerDown);
518
            document.removeEventListener('mouseup', onPointerUp);
519
        };
520
    };
521
}
522
523
function maskitoRejectEvent(element) {
524
    const listener = () => {
525
        const value = element.value;
526
        element.addEventListener('beforeinput', event => {
527
            if (event.defaultPrevented && value === element.value) {
528
                element.dispatchEvent(new CustomEvent('maskitoReject', { bubbles: true }));
529
            }
530
        }, { once: true });
531
    };
532
    element.addEventListener('beforeinput', listener, true);
533
    return () => element.removeEventListener('beforeinput', listener, true);
534
}
535
536
function maskitoRemoveOnBlurPlugin(value) {
537
    return maskitoEventHandler('blur', element => {
538
        if (element.value === value) {
539
            maskitoUpdateElement(element, '');
540
        }
541
    });
542
}
543
544
function maskitoWithPlaceholder(placeholder, focusedOnly = false) {
545
    const removePlaceholder = (value) => {
546
        for (let i = value.length - 1; i >= 0; i--) {
547
            if (value[i] !== placeholder[i]) {
548
                return value.slice(0, i + 1);
549
            }
550
        }
551
        return '';
552
    };
553
    const plugins = [maskitoCaretGuard(value => [0, removePlaceholder(value).length])];
554
    let focused = false;
555
    if (focusedOnly) {
556
        const focus = maskitoEventHandler('focus', element => {
557
            focused = true;
558
            maskitoUpdateElement(element, element.value + placeholder.slice(element.value.length));
559
        }, { capture: true });
560
        const blur = maskitoEventHandler('blur', element => {
561
            focused = false;
562
            maskitoUpdateElement(element, removePlaceholder(element.value));
563
        }, { capture: true });
564
        plugins.push(focus, blur);
565
    }
566
    return {
567
        plugins,
568
        removePlaceholder,
569
        preprocessors: [
570
            ({ elementState, data }) => {
571
                const { value, selection } = elementState;
572
                return {
573
                    elementState: {
574
                        selection,
575
                        value: removePlaceholder(value),
576
                    },
577
                    data,
578
                };
579
            },
580
        ],
581
        postprocessors: [
582
            ({ value, selection }, initialElementState) => 
583
            /**
584
             * If `value` still equals to `initialElementState.value`,
585
             * then it means that value is patched programmatically (from Maskito's plugin or externally).
586
             * In this case, we don't want to mutate value and automatically add placeholder.
587
             * ___
588
             * For example, developer wants to remove manually placeholder (+ do something else with value) on blur.
589
             * Without this condition, placeholder will be unexpectedly added again.
590
             */
591
            value !== initialElementState.value && (focused || !focusedOnly)
592
                ? {
593
                    value: value + placeholder.slice(value.length),
594
                    selection,
595
                }
596
                : { value, selection },
597
        ],
598
    };
599
}
600
601
function createZeroPlaceholdersPreprocessor() {
602
    return ({ elementState }, actionType) => {
603
        const { value, selection } = elementState;
604
        if (!value || isLastChar(value, selection)) {
605
            return { elementState };
606
        }
607
        const [from, to] = selection;
608
        const zeroes = value.slice(from, to).replace(/\d/g, '0');
609
        const newValue = value.slice(0, from) + zeroes + value.slice(to);
610
        if (actionType === 'validation' || (actionType === 'insert' && from === to)) {
611
            return {
612
                elementState: { selection, value: newValue },
613
            };
614
        }
615
        return {
616
            elementState: {
617
                selection: actionType === 'deleteBackward' || actionType === 'insert'
618
                    ? [from, from]
619
                    : [to, to],
620
                value: newValue,
621
            },
622
        };
623
    };
624
}
625
function isLastChar(value, [_, to]) {
626
    return to === value.length;
627
}
628
629
function maskitoDateOptionsGenerator({ mode, separator = '.', max, min, }) {
630
    const dateModeTemplate = mode.split('/').join(separator);
631
    return Object.assign(Object.assign({}, MASKITO_DEFAULT_OPTIONS), { mask: Array.from(dateModeTemplate).map(char => char === separator ? char : /\d/), overwriteMode: 'replace', preprocessors: [
632
            createZeroPlaceholdersPreprocessor(),
633
            normalizeDatePreprocessor({
634
                dateModeTemplate,
635
                dateSegmentsSeparator: separator,
636
            }),
637
            createValidDatePreprocessor({
638
                dateModeTemplate,
639
                dateSegmentsSeparator: separator,
640
            }),
641
        ], postprocessors: [
642
            createMinMaxDatePostprocessor({
643
                min,
644
                max,
645
                dateModeTemplate,
646
                dateSegmentSeparator: separator,
647
            }),
648
        ] });
649
}
650
651
function createMinMaxRangeLengthPostprocessor({ dateModeTemplate, rangeSeparator, minLength, maxLength, max = DEFAULT_MAX_DATE, }) {
652
    if (isEmpty(minLength) && isEmpty(maxLength)) {
653
        return identity;
654
    }
655
    return ({ value, selection }) => {
656
        const dateStrings = parseDateRangeString(value, dateModeTemplate, rangeSeparator);
657
        if (dateStrings.length !== 2 ||
658
            dateStrings.some(date => !isDateStringComplete(date, dateModeTemplate))) {
659
            return { value, selection };
660
        }
661
        const [fromDate, toDate] = dateStrings.map(dateString => segmentsToDate(parseDateString(dateString, dateModeTemplate)));
662
        const minDistantToDate = appendDate(fromDate, Object.assign(Object.assign({}, minLength), { 
663
            // 06.02.2023 - 07.02.2023 => {minLength: {day: 3}} => 06.02.2023 - 08.02.2023
664
            // "from"-day is included in the range
665
            day: (minLength === null || minLength === void 0 ? void 0 : minLength.day) && minLength.day - 1 }));
666
        const maxDistantToDate = !isEmpty(maxLength)
667
            ? appendDate(fromDate, Object.assign(Object.assign({}, maxLength), { day: (maxLength === null || maxLength === void 0 ? void 0 : maxLength.day) && maxLength.day - 1 }))
668
            : max;
669
        const minLengthClampedToDate = clamp(toDate, minDistantToDate, max);
670
        const minMaxLengthClampedToDate = minLengthClampedToDate > maxDistantToDate
671
            ? maxDistantToDate
672
            : minLengthClampedToDate;
673
        return {
674
            selection,
675
            value: dateStrings[0] +
676
                rangeSeparator +
677
                toDateString(dateToSegments(minMaxLengthClampedToDate), dateModeTemplate),
678
        };
679
    };
680
}
681
682
function createSwapDatesPostprocessor({ dateModeTemplate, rangeSeparator, }) {
683
    return ({ value, selection }) => {
684
        const dateStrings = parseDateRangeString(value, dateModeTemplate, rangeSeparator);
685
        const isDateRangeComplete = dateStrings.length === 2 &&
686
            dateStrings.every(date => isDateStringComplete(date, dateModeTemplate));
687
        const [from, to] = selection;
688
        const caretAtTheEnd = from >= value.length;
689
        const allValueSelected = from === 0 && to >= value.length; // dropping text inside with a pointer
690
        if (!(caretAtTheEnd || allValueSelected) || !isDateRangeComplete) {
691
            return { value, selection };
692
        }
693
        const [fromDate, toDate] = dateStrings.map(dateString => segmentsToDate(parseDateString(dateString, dateModeTemplate)));
694
        return {
695
            selection,
696
            value: fromDate > toDate ? dateStrings.reverse().join(rangeSeparator) : value,
697
        };
698
    };
699
}
700
701
function maskitoDateRangeOptionsGenerator({ mode, min, max, minLength, maxLength, dateSeparator = '.', rangeSeparator = `${CHAR_NO_BREAK_SPACE}${CHAR_EN_DASH}${CHAR_NO_BREAK_SPACE}`, }) {
702
    const dateModeTemplate = mode.split('/').join(dateSeparator);
703
    const dateMask = Array.from(dateModeTemplate).map(char => char === dateSeparator ? char : /\d/);
704
    return Object.assign(Object.assign({}, MASKITO_DEFAULT_OPTIONS), { mask: [...dateMask, ...Array.from(rangeSeparator), ...dateMask], overwriteMode: 'replace', preprocessors: [
705
            createZeroPlaceholdersPreprocessor(),
706
            normalizeDatePreprocessor({
707
                dateModeTemplate,
708
                rangeSeparator,
709
                dateSegmentsSeparator: dateSeparator,
710
            }),
711
            createValidDatePreprocessor({
712
                dateModeTemplate,
713
                rangeSeparator,
714
                dateSegmentsSeparator: dateSeparator,
715
            }),
716
        ], postprocessors: [
717
            createMinMaxDatePostprocessor({
718
                min,
719
                max,
720
                dateModeTemplate,
721
                rangeSeparator,
722
                dateSegmentSeparator: dateSeparator,
723
            }),
724
            createMinMaxRangeLengthPostprocessor({
725
                dateModeTemplate,
726
                minLength,
727
                maxLength,
728
                max,
729
                rangeSeparator,
730
            }),
731
            createSwapDatesPostprocessor({
732
                dateModeTemplate,
733
                rangeSeparator,
734
            }),
735
        ] });
736
}
737
738
function padTimeSegments(timeSegments) {
739
    return Object.fromEntries(Object.entries(timeSegments).map(([segmentName, segmentValue]) => [
740
        segmentName,
741
        `${segmentValue}`.padEnd(TIME_SEGMENT_VALUE_LENGTHS[segmentName], '0'),
742
    ]));
743
}
744
745
/**
746
 * @param timeString can be with/without fixed characters
747
 */
748
function parseTimeString(timeString) {
749
    const onlyDigits = timeString.replace(/\D+/g, '');
750
    const timeSegments = {
751
        hours: onlyDigits.slice(0, 2),
752
        minutes: onlyDigits.slice(2, 4),
753
        seconds: onlyDigits.slice(4, 6),
754
        milliseconds: onlyDigits.slice(6, 9),
755
    };
756
    return Object.fromEntries(Object.entries(timeSegments).filter(([_, value]) => Boolean(value)));
757
}
758
759
function toTimeString({ hours = '', minutes = '', seconds = '', milliseconds = '', }) {
760
    const mm = minutes && `:${minutes}`;
761
    const ss = seconds && `:${seconds}`;
762
    const ms = milliseconds && `.${milliseconds}`;
763
    return `${hours}${mm}${ss}${ms}`;
764
}
765
766
const TRAILING_TIME_SEGMENT_SEPARATOR_REG = new RegExp(`[${TIME_FIXED_CHARACTERS.map(escapeRegExp).join('')}]$`);
767
function validateTimeString({ timeString, paddedMaxValues, offset, selection: [from, to], }) {
768
    const parsedTime = parseTimeString(timeString);
769
    const possibleTimeSegments = Object.entries(parsedTime);
770
    const validatedTimeSegments = {};
771
    let paddedZeroes = 0;
772
    for (const [segmentName, segmentValue] of possibleTimeSegments) {
773
        const validatedTime = toTimeString(validatedTimeSegments);
774
        const maxSegmentValue = paddedMaxValues[segmentName];
775
        const fantomSeparator = validatedTime.length && 1;
776
        const lastSegmentDigitIndex = offset +
777
            validatedTime.length +
778
            fantomSeparator +
779
            TIME_SEGMENT_VALUE_LENGTHS[segmentName];
780
        const isLastSegmentDigitAdded = lastSegmentDigitIndex >= from && lastSegmentDigitIndex <= to;
781
        if (isLastSegmentDigitAdded && Number(segmentValue) > Number(maxSegmentValue)) {
782
            // 2|0:00 => Type 9 => 2|0:00
783
            return { validatedTimeString: '', updatedTimeSelection: [from, to] }; // prevent changes
784
        }
785
        const { validatedSegmentValue, prefixedZeroesCount } = padWithZeroesUntilValid(segmentValue, `${maxSegmentValue}`);
786
        paddedZeroes += prefixedZeroesCount;
787
        validatedTimeSegments[segmentName] = validatedSegmentValue;
788
    }
789
    const [trailingSegmentSeparator = ''] = timeString.match(TRAILING_TIME_SEGMENT_SEPARATOR_REG) || [];
790
    const validatedTimeString = toTimeString(validatedTimeSegments) + trailingSegmentSeparator;
791
    const addedDateSegmentSeparators = Math.max(validatedTimeString.length - timeString.length, 0);
792
    return {
793
        validatedTimeString,
794
        updatedTimeSelection: [
795
            from + paddedZeroes + addedDateSegmentSeparators,
796
            to + paddedZeroes + addedDateSegmentSeparators,
797
        ],
798
    };
799
}
800
801
function isDateTimeStringComplete(dateTimeString, dateMode, timeMode) {
802
    return (dateTimeString.length >=
803
        dateMode.length + timeMode.length + DATE_TIME_SEPARATOR.length &&
804
        dateTimeString
805
            .split(DATE_TIME_SEPARATOR)[0]
806
            .split(/\D/)
807
            .every(segment => !segment.match(/^0+$/)));
808
}
809
810
function parseDateTimeString(dateTime, dateModeTemplate) {
811
    const hasSeparator = dateTime.includes(DATE_TIME_SEPARATOR);
812
    return [
813
        dateTime.slice(0, dateModeTemplate.length),
814
        dateTime.slice(hasSeparator
815
            ? dateModeTemplate.length + DATE_TIME_SEPARATOR.length
816
            : dateModeTemplate.length),
817
    ];
818
}
819
820
function createMinMaxDateTimePostprocessor({ dateModeTemplate, timeMode, min = DEFAULT_MIN_DATE, max = DEFAULT_MAX_DATE, }) {
821
    return ({ value, selection }) => {
822
        const [dateString, timeString] = parseDateTimeString(value, dateModeTemplate);
823
        const parsedDate = parseDateString(dateString, dateModeTemplate);
824
        const parsedTime = parseTimeString(timeString);
825
        if (!isDateTimeStringComplete(value, dateModeTemplate, timeMode)) {
826
            const fixedDate = raiseSegmentValueToMin(parsedDate, dateModeTemplate);
827
            const { year, month, day } = isDateStringComplete(dateString, dateModeTemplate)
828
                ? dateToSegments(clamp(segmentsToDate(fixedDate), min, max))
829
                : fixedDate;
830
            const fixedValue = toDateString(Object.assign({ year,
831
                month,
832
                day }, parsedTime), dateModeTemplate, timeMode);
833
            const tail = value.slice(fixedValue.length);
834
            return {
835
                selection,
836
                value: fixedValue + tail,
837
            };
838
        }
839
        const date = segmentsToDate(parsedDate, parsedTime);
840
        const clampedDate = clamp(date, min, max);
841
        const validatedValue = toDateString(dateToSegments(clampedDate), dateModeTemplate, timeMode);
842
        return {
843
            selection,
844
            value: validatedValue,
845
        };
846
    };
847
}
848
849
function createValidDateTimePreprocessor({ dateModeTemplate, dateSegmentsSeparator, }) {
850
    const invalidCharsRegExp = new RegExp(`[^\\d${TIME_FIXED_CHARACTERS.map(escapeRegExp).join('')}${escapeRegExp(dateSegmentsSeparator)}]+`);
851
    return ({ elementState, data }) => {
852
        const { value, selection } = elementState;
853
        if (data === dateSegmentsSeparator) {
854
            return {
855
                elementState,
856
                data: selection[0] === value.length ? data : '',
857
            };
858
        }
859
        if (POSSIBLE_DATE_TIME_SEPARATOR.includes(data)) {
860
            return { elementState, data: DATE_TIME_SEPARATOR };
861
        }
862
        const newCharacters = data.replace(invalidCharsRegExp, '');
863
        if (!newCharacters) {
864
            return { elementState, data: '' };
865
        }
866
        const [from, rawTo] = selection;
867
        let to = rawTo + data.length;
868
        const newPossibleValue = value.slice(0, from) + newCharacters + value.slice(to);
869
        const [dateString, timeString] = parseDateTimeString(newPossibleValue, dateModeTemplate);
870
        let validatedValue = '';
871
        const hasDateTimeSeparator = newPossibleValue.includes(DATE_TIME_SEPARATOR);
872
        const { validatedDateString, updatedSelection } = validateDateString({
873
            dateString,
874
            dateModeTemplate,
875
            offset: 0,
876
            selection: [from, to],
877
        });
878
        if (dateString && !validatedDateString) {
879
            return { elementState, data: '' }; // prevent changes
880
        }
881
        to = updatedSelection[1];
882
        validatedValue += validatedDateString;
883
        const paddedMaxValues = padTimeSegments(DEFAULT_TIME_SEGMENT_MAX_VALUES);
884
        const { validatedTimeString, updatedTimeSelection } = validateTimeString({
885
            timeString,
886
            paddedMaxValues,
887
            offset: validatedValue.length + DATE_TIME_SEPARATOR.length,
888
            selection: [from, to],
889
        });
890
        if (timeString && !validatedTimeString) {
891
            return { elementState, data: '' }; // prevent changes
892
        }
893
        to = updatedTimeSelection[1];
894
        validatedValue += hasDateTimeSeparator
895
            ? DATE_TIME_SEPARATOR + validatedTimeString
896
            : validatedTimeString;
897
        const newData = validatedValue.slice(from, to);
898
        return {
899
            elementState: {
900
                selection,
901
                value: validatedValue.slice(0, from) +
902
                    newData
903
                        .split(dateSegmentsSeparator)
904
                        .map(segment => '0'.repeat(segment.length))
905
                        .join(dateSegmentsSeparator) +
906
                    validatedValue.slice(to),
907
            },
908
            data: newData,
909
        };
910
    };
911
}
912
913
function maskitoDateTimeOptionsGenerator({ dateMode, timeMode, dateSeparator = '.', min, max, }) {
914
    const dateModeTemplate = dateMode.split('/').join(dateSeparator);
915
    return Object.assign(Object.assign({}, MASKITO_DEFAULT_OPTIONS), { mask: [
916
            ...Array.from(dateModeTemplate).map(char => char === dateSeparator ? char : /\d/),
917
            ...DATE_TIME_SEPARATOR.split(''),
918
            ...Array.from(timeMode).map(char => TIME_FIXED_CHARACTERS.includes(char) ? char : /\d/),
919
        ], overwriteMode: 'replace', preprocessors: [
920
            createZeroPlaceholdersPreprocessor(),
921
            normalizeDatePreprocessor({
922
                dateModeTemplate,
923
                dateSegmentsSeparator: dateSeparator,
924
            }),
925
            createValidDateTimePreprocessor({
926
                dateModeTemplate,
927
                dateSegmentsSeparator: dateSeparator,
928
            }),
929
        ], postprocessors: [
930
            createMinMaxDateTimePostprocessor({
931
                min,
932
                max,
933
                dateModeTemplate,
934
                timeMode,
935
            }),
936
        ] });
937
}
938
939
/**
940
 * It drops prefix and postfix from data
941
 * Needed for case, when prefix or postfix contain decimalSeparator, to ignore it in resulting number
942
 * @example User pastes '{prefix}123.45{postfix}' => 123.45
943
 */
944
function createAffixesFilterPreprocessor({ prefix, postfix, }) {
945
    return ({ elementState, data }) => {
946
        const { cleanValue: cleanData } = extractAffixes(data, {
947
            prefix,
948
            postfix,
949
        });
950
        return {
951
            elementState,
952
            data: cleanData,
953
        };
954
    };
955
}
956
957
function generateMaskExpression({ decimalSeparator, isNegativeAllowed, precision, thousandSeparator, prefix, postfix, decimalPseudoSeparators = [], pseudoMinuses = [], }) {
958
    const computedPrefix = computeAllOptionalCharsRegExp(prefix);
959
    const digit = '\\d';
960
    const optionalMinus = isNegativeAllowed
961
        ? `[${CHAR_MINUS}${pseudoMinuses.map(x => `\\${x}`).join('')}]?`
962
        : '';
963
    const integerPart = thousandSeparator
964
        ? `[${digit}${escapeRegExp(thousandSeparator).replace(/\s/g, '\\s')}]*`
965
        : `[${digit}]*`;
966
    const decimalPart = precision > 0
967
        ? `([${escapeRegExp(decimalSeparator)}${decimalPseudoSeparators
968
            .map(escapeRegExp)
969
            .join('')}]${digit}{0,${Number.isFinite(precision) ? precision : ''}})?`
970
        : '';
971
    const computedPostfix = computeAllOptionalCharsRegExp(postfix);
972
    return new RegExp(`^${computedPrefix}${optionalMinus}${integerPart}${decimalPart}${computedPostfix}$`);
973
}
974
function computeAllOptionalCharsRegExp(str) {
975
    return str
976
        ? `${str
977
            .split('')
978
            .map(char => `${escapeRegExp(char)}?`)
979
            .join('')}`
980
        : '';
981
}
982
983
function maskitoParseNumber(maskedNumber, decimalSeparator = '.') {
984
    const hasNegativeSign = !!maskedNumber.match(new RegExp(`^\\D*[${CHAR_MINUS}\\${CHAR_HYPHEN}${CHAR_EN_DASH}${CHAR_EM_DASH}]`));
985
    const escapedDecimalSeparator = escapeRegExp(decimalSeparator);
986
    const unmaskedNumber = maskedNumber
987
        // drop all decimal separators not followed by a digit
988
        .replace(new RegExp(`${escapedDecimalSeparator}(?!\\d)`, 'g'), '')
989
        // drop all non-digit characters except decimal separator
990
        .replace(new RegExp(`[^\\d${escapedDecimalSeparator}]`, 'g'), '')
991
        .replace(decimalSeparator, '.');
992
    return unmaskedNumber
993
        ? Number((hasNegativeSign ? CHAR_HYPHEN : '') + unmaskedNumber)
994
        : NaN;
995
}
996
997
/**
998
 * Convert number to string with replacing exponent part on decimals
999
 *
1000
 * @param value the number
1001
 * @return string representation of a number
1002
 */
1003
function stringifyNumberWithoutExp(value) {
1004
    const valueAsString = String(value);
1005
    const [numberPart, expPart] = valueAsString.split('e-');
1006
    let valueWithoutExp = valueAsString;
1007
    if (expPart) {
1008
        const [, fractionalPart] = numberPart.split('.');
1009
        const decimalDigits = Number(expPart) + ((fractionalPart === null || fractionalPart === void 0 ? void 0 : fractionalPart.length) || 0);
1010
        valueWithoutExp = value.toFixed(decimalDigits);
1011
    }
1012
    return valueWithoutExp;
1013
}
1014
1015
function validateDecimalPseudoSeparators({ decimalSeparator, thousandSeparator, decimalPseudoSeparators = DEFAULT_DECIMAL_PSEUDO_SEPARATORS, }) {
1016
    return decimalPseudoSeparators.filter(char => char !== thousandSeparator && char !== decimalSeparator);
1017
}
1018
1019
/**
1020
 * If `decimalZeroPadding` is `true`, it pads decimal part with zeroes
1021
 * (until number of digits after decimalSeparator is equal to the `precision`).
1022
 * @example 1,42 => (`precision` is equal to 4) => 1,4200.
1023
 */
1024
function createDecimalZeroPaddingPostprocessor({ decimalSeparator, precision, decimalZeroPadding, prefix, postfix, }) {
1025
    if (precision <= 0 || !decimalZeroPadding) {
1026
        return identity;
1027
    }
1028
    return ({ value, selection }) => {
1029
        const { cleanValue, extractedPrefix, extractedPostfix } = extractAffixes(value, {
1030
            prefix,
1031
            postfix,
1032
        });
1033
        if (Number.isNaN(maskitoParseNumber(cleanValue, decimalSeparator))) {
1034
            return { value, selection };
1035
        }
1036
        const [integerPart, decimalPart = ''] = cleanValue.split(decimalSeparator);
1037
        return {
1038
            value: extractedPrefix +
1039
                integerPart +
1040
                decimalSeparator +
1041
                decimalPart.padEnd(precision, '0') +
1042
                extractedPostfix,
1043
            selection,
1044
        };
1045
    };
1046
}
1047
1048
/**
1049
 * Replace fullwidth numbers with half width number
1050
 * @param fullWidthNumber full width number
1051
 * @returns processed half width number
1052
 */
1053
function toHalfWidthNumber(fullWidthNumber) {
1054
    return fullWidthNumber.replace(/[0-9]/g, s => String.fromCharCode(s.charCodeAt(0) - 0xfee0));
1055
}
1056
1057
/**
1058
 * Convert full width numbers like 1, 2 to half width numbers 1, 2
1059
 */
1060
function createFullWidthToHalfWidthPreprocessor() {
1061
    return ({ elementState, data }) => {
1062
        const { value, selection } = elementState;
1063
        return {
1064
            elementState: {
1065
                selection,
1066
                value: toHalfWidthNumber(value),
1067
            },
1068
            data: toHalfWidthNumber(data),
1069
        };
1070
    };
1071
}
1072
1073
/**
1074
 * This preprocessor works only once at initialization phase (when `new Maskito(...)` is executed).
1075
 * This preprocessor helps to avoid conflicts during transition from one mask to another (for the same input).
1076
 * For example, the developer changes postfix (or other mask's props) during run-time.
1077
 * ```
1078
 * let maskitoOptions = maskitoNumberOptionsGenerator({postfix: ' year'});
1079
 * // [3 seconds later]
1080
 * maskitoOptions = maskitoNumberOptionsGenerator({postfix: ' years'});
1081
 * ```
1082
 */
1083
function createInitializationOnlyPreprocessor({ decimalSeparator, decimalPseudoSeparators, pseudoMinuses, prefix, postfix, }) {
1084
    let isInitializationPhase = true;
1085
    const cleanNumberMask = generateMaskExpression({
1086
        decimalSeparator,
1087
        decimalPseudoSeparators,
1088
        pseudoMinuses,
1089
        prefix: '',
1090
        postfix: '',
1091
        thousandSeparator: '',
1092
        precision: Infinity,
1093
        isNegativeAllowed: true,
1094
    });
1095
    return ({ elementState, data }) => {
1096
        if (!isInitializationPhase) {
1097
            return { elementState, data };
1098
        }
1099
        isInitializationPhase = false;
1100
        const { cleanValue } = extractAffixes(elementState.value, { prefix, postfix });
1101
        return {
1102
            elementState: maskitoTransform(Object.assign(Object.assign({}, elementState), { value: cleanValue }), {
1103
                mask: cleanNumberMask,
1104
            }),
1105
            data,
1106
        };
1107
    };
1108
}
1109
1110
/**
1111
 * It removes repeated leading zeroes for integer part.
1112
 * @example 0,|00005 => Backspace => |5
1113
 * @example -0,|00005 => Backspace => -|5
1114
 * @example User types "000000" => 0|
1115
 * @example 0| => User types "5" => 5|
1116
 */
1117
function createLeadingZeroesValidationPostprocessor({ decimalSeparator, thousandSeparator, prefix, postfix, }) {
1118
    const trimLeadingZeroes = (value) => {
1119
        const escapedThousandSeparator = escapeRegExp(thousandSeparator);
1120
        return value
1121
            .replace(
1122
        // all leading zeroes followed by another zero
1123
        new RegExp(`^(\\D+)?[0${escapedThousandSeparator}]+(?=0)`), '$1')
1124
            .replace(
1125
        // zero followed by not-zero digit
1126
        new RegExp(`^(\\D+)?[0${escapedThousandSeparator}]+(?=[1-9])`), '$1');
1127
    };
1128
    const countTrimmedZeroesBefore = (value, index) => {
1129
        const valueBefore = value.slice(0, index);
1130
        const followedByZero = value.slice(index).startsWith('0');
1131
        return (valueBefore.length -
1132
            trimLeadingZeroes(valueBefore).length +
1133
            (followedByZero ? 1 : 0));
1134
    };
1135
    return ({ value, selection }) => {
1136
        const [from, to] = selection;
1137
        const { cleanValue, extractedPrefix, extractedPostfix } = extractAffixes(value, {
1138
            prefix,
1139
            postfix,
1140
        });
1141
        const hasDecimalSeparator = cleanValue.includes(decimalSeparator);
1142
        const [integerPart, decimalPart = ''] = cleanValue.split(decimalSeparator);
1143
        const zeroTrimmedIntegerPart = trimLeadingZeroes(integerPart);
1144
        if (integerPart === zeroTrimmedIntegerPart) {
1145
            return { value, selection };
1146
        }
1147
        const newFrom = from - countTrimmedZeroesBefore(value, from);
1148
        const newTo = to - countTrimmedZeroesBefore(value, to);
1149
        return {
1150
            value: extractedPrefix +
1151
                zeroTrimmedIntegerPart +
1152
                (hasDecimalSeparator ? decimalSeparator : '') +
1153
                decimalPart +
1154
                extractedPostfix,
1155
            selection: [Math.max(newFrom, 0), Math.max(newTo, 0)],
1156
        };
1157
    };
1158
}
1159
1160
/**
1161
 * This postprocessor is connected with {@link createMinMaxPlugin}:
1162
 * both validate `min`/`max` bounds of entered value (but at the different point of time).
1163
 */
1164
function createMinMaxPostprocessor({ min, max, decimalSeparator, }) {
1165
    return ({ value, selection }) => {
1166
        const parsedNumber = maskitoParseNumber(value, decimalSeparator);
1167
        const limitedValue = 
1168
        /**
1169
         * We cannot limit lower bound if user enters positive number.
1170
         * The same for upper bound and negative number.
1171
         * ___
1172
         * @example (min = 5)
1173
         * Empty input => Without this condition user cannot type 42 (the first digit will be rejected)
1174
         * ___
1175
         * @example (max = -10)
1176
         * Value is -10 => Without this condition user cannot delete 0 to enter another digit
1177
         */
1178
        parsedNumber > 0 ? Math.min(parsedNumber, max) : Math.max(parsedNumber, min);
1179
        if (!Number.isNaN(parsedNumber) && limitedValue !== parsedNumber) {
1180
            const newValue = `${limitedValue}`
1181
                .replace('.', decimalSeparator)
1182
                .replace(CHAR_HYPHEN, CHAR_MINUS);
1183
            return {
1184
                value: newValue,
1185
                selection: [newValue.length, newValue.length],
1186
            };
1187
        }
1188
        return {
1189
            value,
1190
            selection,
1191
        };
1192
    };
1193
}
1194
1195
/**
1196
 * Manage caret-navigation when user "deletes" non-removable digits or separators
1197
 * @example 1,|42 => Backspace => 1|,42 (only if `decimalZeroPadding` is `true`)
1198
 * @example 1|,42 => Delete => 1,|42 (only if `decimalZeroPadding` is `true`)
1199
 * @example 0,|00 => Delete => 0,0|0 (only if `decimalZeroPadding` is `true`)
1200
 * @example 1 |000 => Backspace => 1| 000 (always)
1201
 */
1202
function createNonRemovableCharsDeletionPreprocessor({ decimalSeparator, thousandSeparator, decimalZeroPadding, }) {
1203
    return ({ elementState, data }, actionType) => {
1204
        const { value, selection } = elementState;
1205
        const [from, to] = selection;
1206
        const selectedCharacters = value.slice(from, to);
1207
        const nonRemovableSeparators = decimalZeroPadding
1208
            ? [decimalSeparator, thousandSeparator]
1209
            : [thousandSeparator];
1210
        const areNonRemovableZeroesSelected = decimalZeroPadding &&
1211
            from > value.indexOf(decimalSeparator) &&
1212
            Boolean(selectedCharacters.match(/^0+$/gi));
1213
        if ((actionType !== 'deleteBackward' && actionType !== 'deleteForward') ||
1214
            (!nonRemovableSeparators.includes(selectedCharacters) &&
1215
                !areNonRemovableZeroesSelected)) {
1216
            return {
1217
                elementState,
1218
                data,
1219
            };
1220
        }
1221
        return {
1222
            elementState: {
1223
                value,
1224
                selection: actionType === 'deleteForward' ? [to, to] : [from, from],
1225
            },
1226
            data,
1227
        };
1228
    };
1229
}
1230
1231
/**
1232
 * It pads integer part with zero if user types decimal separator (for empty input).
1233
 * @example Empty input => User types "," (decimal separator) => 0,|
1234
 */
1235
function createNotEmptyIntegerPartPreprocessor({ decimalSeparator, precision, prefix, postfix, }) {
1236
    const startWithDecimalSepRegExp = new RegExp(`^\\D*${escapeRegExp(decimalSeparator)}`);
1237
    return ({ elementState, data }) => {
1238
        const { value, selection } = elementState;
1239
        const { cleanValue } = extractAffixes(value, {
1240
            prefix,
1241
            postfix,
1242
        });
1243
        const [from] = selection;
1244
        if (precision <= 0 ||
1245
            cleanValue.includes(decimalSeparator) ||
1246
            !data.match(startWithDecimalSepRegExp)) {
1247
            return { elementState, data };
1248
        }
1249
        const digitsBeforeCursor = cleanValue.slice(0, from).match(/\d+/);
1250
        return {
1251
            elementState,
1252
            data: digitsBeforeCursor ? data : `0${data}`,
1253
        };
1254
    };
1255
}
1256
1257
/**
1258
 * It replaces pseudo characters with valid one.
1259
 * @example User types '.' (but separator is equal to comma) => dot is replaced with comma.
1260
 * @example User types hyphen / en-dash / em-dash => it is replaced with minus.
1261
 */
1262
function createPseudoCharactersPreprocessor({ validCharacter, pseudoCharacters, prefix, postfix, }) {
1263
    const pseudoCharactersRegExp = new RegExp(`[${pseudoCharacters.join('')}]`, 'gi');
1264
    return ({ elementState, data }) => {
1265
        const { value, selection } = elementState;
1266
        const { cleanValue, extractedPostfix, extractedPrefix } = extractAffixes(value, {
1267
            prefix,
1268
            postfix,
1269
        });
1270
        return {
1271
            elementState: {
1272
                selection,
1273
                value: extractedPrefix +
1274
                    cleanValue.replace(pseudoCharactersRegExp, validCharacter) +
1275
                    extractedPostfix,
1276
            },
1277
            data: data.replace(pseudoCharactersRegExp, validCharacter),
1278
        };
1279
    };
1280
}
1281
1282
/**
1283
 * It rejects new typed decimal separator if it already exists in text field.
1284
 * Behaviour is similar to native <input type="number"> (Chrome).
1285
 * @example 1|23,45 => Press comma (decimal separator) => 1|23,45 (do nothing).
1286
 */
1287
function createRepeatedDecimalSeparatorPreprocessor({ decimalSeparator, prefix, postfix, }) {
1288
    return ({ elementState, data }) => {
1289
        const { value, selection } = elementState;
1290
        const [from, to] = selection;
1291
        const { cleanValue } = extractAffixes(value, { prefix, postfix });
1292
        return {
1293
            elementState,
1294
            data: !cleanValue.includes(decimalSeparator) ||
1295
                value.slice(from, to + 1).includes(decimalSeparator)
1296
                ? data
1297
                : data.replace(new RegExp(escapeRegExp(decimalSeparator), 'gi'), ''),
1298
        };
1299
    };
1300
}
1301
1302
/**
1303
 * It adds symbol for separating thousands.
1304
 * @example 1000000 => (thousandSeparator is equal to space) => 1 000 000.
1305
 */
1306
function createThousandSeparatorPostprocessor({ thousandSeparator, decimalSeparator, prefix, postfix, }) {
1307
    if (!thousandSeparator) {
1308
        return identity;
1309
    }
1310
    const isAllSpaces = (...chars) => chars.every(x => /\s/.test(x));
1311
    return ({ value, selection }) => {
1312
        const { cleanValue, extractedPostfix, extractedPrefix } = extractAffixes(value, {
1313
            prefix,
1314
            postfix,
1315
        });
1316
        const [integerPart, decimalPart = ''] = cleanValue
1317
            .replace(CHAR_MINUS, '')
1318
            .split(decimalSeparator);
1319
        const [initialFrom, initialTo] = selection;
1320
        let [from, to] = selection;
1321
        const processedIntegerPart = Array.from(integerPart).reduceRight((formattedValuePart, char, i) => {
1322
            const isLeadingThousandSeparator = !i && char === thousandSeparator;
1323
            const isPositionForSeparator = !isLeadingThousandSeparator &&
1324
                formattedValuePart.length &&
1325
                (formattedValuePart.length + 1) % 4 === 0;
1326
            if (isPositionForSeparator &&
1327
                (char === thousandSeparator || isAllSpaces(char, thousandSeparator))) {
1328
                return thousandSeparator + formattedValuePart;
1329
            }
1330
            if (char === thousandSeparator && !isPositionForSeparator) {
1331
                if (i && i <= initialFrom) {
1332
                    from--;
1333
                }
1334
                if (i && i <= initialTo) {
1335
                    to--;
1336
                }
1337
                return formattedValuePart;
1338
            }
1339
            if (!isPositionForSeparator) {
1340
                return char + formattedValuePart;
1341
            }
1342
            if (i <= initialFrom) {
1343
                from++;
1344
            }
1345
            if (i <= initialTo) {
1346
                to++;
1347
            }
1348
            return char + thousandSeparator + formattedValuePart;
1349
        }, '');
1350
        return {
1351
            value: extractedPrefix +
1352
                (cleanValue.includes(CHAR_MINUS) ? CHAR_MINUS : '') +
1353
                processedIntegerPart +
1354
                (cleanValue.includes(decimalSeparator) ? decimalSeparator : '') +
1355
                decimalPart +
1356
                extractedPostfix,
1357
            selection: [from, to],
1358
        };
1359
    };
1360
}
1361
1362
/**
1363
 * It drops decimal part if precision is zero.
1364
 * @example User pastes '123.45' (but precision is zero) => 123
1365
 */
1366
function createZeroPrecisionPreprocessor({ precision, decimalSeparator, prefix, postfix, }) {
1367
    if (precision > 0) {
1368
        return identity;
1369
    }
1370
    const decimalPartRegExp = new RegExp(`${escapeRegExp(decimalSeparator)}.*$`, 'g');
1371
    return ({ elementState, data }) => {
1372
        const { value, selection } = elementState;
1373
        const { cleanValue, extractedPrefix, extractedPostfix } = extractAffixes(value, {
1374
            prefix,
1375
            postfix,
1376
        });
1377
        const [from, to] = selection;
1378
        const newValue = extractedPrefix +
1379
            cleanValue.replace(decimalPartRegExp, '') +
1380
            extractedPostfix;
1381
        return {
1382
            elementState: {
1383
                selection: [
1384
                    Math.min(from, newValue.length),
1385
                    Math.min(to, newValue.length),
1386
                ],
1387
                value: newValue,
1388
            },
1389
            data: data.replace(decimalPartRegExp, ''),
1390
        };
1391
    };
1392
}
1393
1394
const DUMMY_SELECTION = [0, 0];
1395
/**
1396
 * It removes repeated leading zeroes for integer part on blur-event.
1397
 * @example 000000 => blur => 0
1398
 * @example 00005 => blur => 5
1399
 */
1400
function createLeadingZeroesValidationPlugin({ decimalSeparator, thousandSeparator, prefix, postfix, }) {
1401
    const dropRepeatedLeadingZeroes = createLeadingZeroesValidationPostprocessor({
1402
        decimalSeparator,
1403
        thousandSeparator,
1404
        prefix,
1405
        postfix,
1406
    });
1407
    return maskitoEventHandler('blur', element => {
1408
        const newValue = dropRepeatedLeadingZeroes({
1409
            value: element.value,
1410
            selection: DUMMY_SELECTION,
1411
        }, { value: '', selection: DUMMY_SELECTION }).value;
1412
        if (element.value !== newValue) {
1413
            maskitoUpdateElement(element, newValue);
1414
        }
1415
    }, { capture: true });
1416
}
1417
1418
/**
1419
 * This plugin is connected with {@link createMinMaxPostprocessor}:
1420
 * both validate `min`/`max` bounds of entered value (but at the different point of time).
1421
 */
1422
function createMinMaxPlugin({ min, max, decimalSeparator, }) {
1423
    return maskitoEventHandler('blur', (element, options) => {
1424
        const parsedNumber = maskitoParseNumber(element.value, decimalSeparator);
1425
        const clampedNumber = clamp(parsedNumber, min, max);
1426
        if (!Number.isNaN(parsedNumber) && parsedNumber !== clampedNumber) {
1427
            maskitoUpdateElement(element, maskitoTransform(stringifyNumberWithoutExp(clampedNumber), options));
1428
        }
1429
    }, { capture: true });
1430
}
1431
1432
/**
1433
 * It pads EMPTY integer part with zero if decimal parts exists.
1434
 * It works on blur event only!
1435
 * @example 1|,23 => Backspace => Blur => 0,23
1436
 */
1437
function createNotEmptyIntegerPlugin({ decimalSeparator, prefix, postfix, }) {
1438
    return maskitoEventHandler('blur', element => {
1439
        const { cleanValue, extractedPostfix, extractedPrefix } = extractAffixes(element.value, { prefix, postfix });
1440
        const newValue = extractedPrefix +
1441
            cleanValue.replace(new RegExp(`^(\\D+)?${escapeRegExp(decimalSeparator)}`), `$10${decimalSeparator}`) +
1442
            extractedPostfix;
1443
        if (newValue !== element.value) {
1444
            maskitoUpdateElement(element, newValue);
1445
        }
1446
    }, { capture: true });
1447
}
1448
1449
function maskitoNumberOptionsGenerator({ max = Number.MAX_SAFE_INTEGER, min = Number.MIN_SAFE_INTEGER, precision = 0, thousandSeparator = CHAR_NO_BREAK_SPACE, decimalSeparator = '.', decimalPseudoSeparators, decimalZeroPadding = false, prefix: unsafePrefix = '', postfix = '', } = {}) {
1450
    const pseudoMinuses = [
1451
        CHAR_HYPHEN,
1452
        CHAR_EN_DASH,
1453
        CHAR_EM_DASH,
1454
        CHAR_JP_HYPHEN,
1455
    ].filter(char => char !== thousandSeparator && char !== decimalSeparator);
1456
    const validatedDecimalPseudoSeparators = validateDecimalPseudoSeparators({
1457
        decimalSeparator,
1458
        thousandSeparator,
1459
        decimalPseudoSeparators,
1460
    });
1461
    const prefix = unsafePrefix.endsWith(decimalSeparator) && precision > 0
1462
        ? `${unsafePrefix}${CHAR_ZERO_WIDTH_SPACE}`
1463
        : unsafePrefix;
1464
    return Object.assign(Object.assign({}, MASKITO_DEFAULT_OPTIONS), { mask: generateMaskExpression({
1465
            decimalSeparator,
1466
            precision,
1467
            thousandSeparator,
1468
            prefix,
1469
            postfix,
1470
            isNegativeAllowed: min < 0,
1471
        }), preprocessors: [
1472
            createInitializationOnlyPreprocessor({
1473
                decimalSeparator,
1474
                decimalPseudoSeparators: validatedDecimalPseudoSeparators,
1475
                pseudoMinuses,
1476
                prefix,
1477
                postfix,
1478
            }),
1479
            createAffixesFilterPreprocessor({ prefix, postfix }),
1480
            createFullWidthToHalfWidthPreprocessor(),
1481
            createPseudoCharactersPreprocessor({
1482
                validCharacter: CHAR_MINUS,
1483
                pseudoCharacters: pseudoMinuses,
1484
                prefix,
1485
                postfix,
1486
            }),
1487
            createPseudoCharactersPreprocessor({
1488
                validCharacter: decimalSeparator,
1489
                pseudoCharacters: validatedDecimalPseudoSeparators,
1490
                prefix,
1491
                postfix,
1492
            }),
1493
            createNotEmptyIntegerPartPreprocessor({
1494
                decimalSeparator,
1495
                precision,
1496
                prefix,
1497
                postfix,
1498
            }),
1499
            createNonRemovableCharsDeletionPreprocessor({
1500
                decimalSeparator,
1501
                decimalZeroPadding,
1502
                thousandSeparator,
1503
            }),
1504
            createZeroPrecisionPreprocessor({
1505
                precision,
1506
                decimalSeparator,
1507
                prefix,
1508
                postfix,
1509
            }),
1510
            createRepeatedDecimalSeparatorPreprocessor({
1511
                decimalSeparator,
1512
                prefix,
1513
                postfix,
1514
            }),
1515
        ], postprocessors: [
1516
            createMinMaxPostprocessor({ decimalSeparator, min, max }),
1517
            maskitoPrefixPostprocessorGenerator(prefix),
1518
            maskitoPostfixPostprocessorGenerator(postfix),
1519
            createThousandSeparatorPostprocessor({
1520
                decimalSeparator,
1521
                thousandSeparator,
1522
                prefix,
1523
                postfix,
1524
            }),
1525
            createDecimalZeroPaddingPostprocessor({
1526
                decimalSeparator,
1527
                decimalZeroPadding,
1528
                precision,
1529
                prefix,
1530
                postfix,
1531
            }),
1532
        ], plugins: [
1533
            createLeadingZeroesValidationPlugin({
1534
                decimalSeparator,
1535
                thousandSeparator,
1536
                prefix,
1537
                postfix,
1538
            }),
1539
            createNotEmptyIntegerPlugin({
1540
                decimalSeparator,
1541
                prefix,
1542
                postfix,
1543
            }),
1544
            createMinMaxPlugin({ min, max, decimalSeparator }),
1545
        ], overwriteMode: decimalZeroPadding
1546
            ? ({ value, selection: [from] }) => from <= value.indexOf(decimalSeparator) ? 'shift' : 'replace'
1547
            : 'shift' });
1548
}
1549
1550
function createMaxValidationPreprocessor(timeSegmentMaxValues) {
1551
    const paddedMaxValues = padTimeSegments(timeSegmentMaxValues);
1552
    const invalidCharsRegExp = new RegExp(`[^\\d${TIME_FIXED_CHARACTERS.map(escapeRegExp).join('')}]+`);
1553
    return ({ elementState, data }, actionType) => {
1554
        if (actionType === 'deleteBackward' || actionType === 'deleteForward') {
1555
            return { elementState, data };
1556
        }
1557
        const { value, selection } = elementState;
1558
        if (actionType === 'validation') {
1559
            const { validatedTimeString, updatedTimeSelection } = validateTimeString({
1560
                timeString: value,
1561
                paddedMaxValues,
1562
                offset: 0,
1563
                selection,
1564
            });
1565
            return {
1566
                elementState: {
1567
                    value: validatedTimeString,
1568
                    selection: updatedTimeSelection,
1569
                },
1570
                data,
1571
            };
1572
        }
1573
        const newCharacters = data.replace(invalidCharsRegExp, '');
1574
        const [from, rawTo] = selection;
1575
        let to = rawTo + newCharacters.length; // to be conformed with `overwriteMode: replace`
1576
        const newPossibleValue = value.slice(0, from) + newCharacters + value.slice(to);
1577
        const { validatedTimeString, updatedTimeSelection } = validateTimeString({
1578
            timeString: newPossibleValue,
1579
            paddedMaxValues,
1580
            offset: 0,
1581
            selection: [from, to],
1582
        });
1583
        if (newPossibleValue && !validatedTimeString) {
1584
            return { elementState, data: '' }; // prevent changes
1585
        }
1586
        to = updatedTimeSelection[1];
1587
        const newData = validatedTimeString.slice(from, to);
1588
        return {
1589
            elementState: {
1590
                selection,
1591
                value: validatedTimeString.slice(0, from) +
1592
                    '0'.repeat(newData.length) +
1593
                    validatedTimeString.slice(to),
1594
            },
1595
            data: newData,
1596
        };
1597
    };
1598
}
1599
1600
function maskitoTimeOptionsGenerator({ mode, timeSegmentMaxValues = {}, }) {
1601
    const enrichedTimeSegmentMaxValues = Object.assign(Object.assign({}, DEFAULT_TIME_SEGMENT_MAX_VALUES), timeSegmentMaxValues);
1602
    return Object.assign(Object.assign({}, MASKITO_DEFAULT_OPTIONS), { mask: Array.from(mode).map(char => TIME_FIXED_CHARACTERS.includes(char) ? char : /\d/), preprocessors: [
1603
            createZeroPlaceholdersPreprocessor(),
1604
            createMaxValidationPreprocessor(enrichedTimeSegmentMaxValues),
1605
        ], overwriteMode: 'replace' });
1606
}
1607
1608
//export { maskitoAddOnFocusPlugin, maskitoCaretGuard, maskitoDateOptionsGenerator, maskitoDateRangeOptionsGenerator, maskitoDateTimeOptionsGenerator, maskitoEventHandler, maskitoNumberOptionsGenerator, maskitoParseNumber, maskitoPostfixPostprocessorGenerator, maskitoPrefixPostprocessorGenerator, maskitoRejectEvent, maskitoRemoveOnBlurPlugin, maskitoTimeOptionsGenerator, maskitoWithPlaceholder };
1609
(-)a/koha-tmpl/intranet-tmpl/prog/en/includes/calendar.inc (-1 / +20 lines)
Lines 7-24 Link Here
7
    var debug    = "[% debug | html %]";
7
    var debug    = "[% debug | html %]";
8
    var dateformat_pref = "[% Koha.Preference('dateformat') | html %]";
8
    var dateformat_pref = "[% Koha.Preference('dateformat') | html %]";
9
    var flatpickr_dateformat_string = "";
9
    var flatpickr_dateformat_string = "";
10
    var delimter = "";
11
    var altinput_dateformat = "";
10
    switch ( dateformat_pref ){
12
    switch ( dateformat_pref ){
11
        case "us":
13
        case "us":
12
            flatpickr_dateformat_string = "m/d/Y";
14
            flatpickr_dateformat_string = "m/d/Y";
15
            altinput_dateformat = 'mm/dd/yyyy';
16
            delimiter = "/";
13
            break;
17
            break;
14
        case "metric":
18
        case "metric":
15
            flatpickr_dateformat_string = "d/m/Y";
19
            flatpickr_dateformat_string = "d/m/Y";
20
             altinput_dateformat = 'dd/mm/yyyy';
21
             delimiter = "/";
16
            break;
22
            break;
17
        case "dmydot":
23
        case "dmydot":
18
            flatpickr_dateformat_string = "d.m.Y";
24
            flatpickr_dateformat_string = "d.m.Y";
25
            altinput_dateformat = 'dd/mm/yyyy';
26
            delimiter = ".";
19
            break;
27
            break;
20
        default:
28
        default:
21
            flatpickr_dateformat_string = "Y-m-d";
29
            flatpickr_dateformat_string = "Y-m-d";
30
            altinput_dateformat = 'yyyy/mm/dd';
31
            delimiter = "-";
22
    }
32
    }
23
    var sentmsg = 0;
33
    var sentmsg = 0;
24
    var bidi = [% IF(bidi) %] true[% ELSE %] false[% END %];
34
    var bidi = [% IF(bidi) %] true[% ELSE %] false[% END %];
Lines 30-35 Link Here
30
[% Asset.js("js/calendar.js") | $raw %]
40
[% Asset.js("js/calendar.js") | $raw %]
31
[% Asset.js("lib/flatpickr/flatpickr.min.js") | $raw %]
41
[% Asset.js("lib/flatpickr/flatpickr.min.js") | $raw %]
32
[% Asset.js("lib/flatpickr/shortcut-buttons-flatpickr.min.js") | $raw %]
42
[% Asset.js("lib/flatpickr/shortcut-buttons-flatpickr.min.js") | $raw %]
43
[% Asset.js("lib/maskito/maskito.core.js") | $raw %]
44
[% Asset.js("lib/maskito/maskito.kit.js") | $raw %]
33
<script>
45
<script>
34
    flatpickr.l10ns.default.weekdays = flatpickr_weekdays;
46
    flatpickr.l10ns.default.weekdays = flatpickr_weekdays;
35
    flatpickr.l10ns.default.months   = flatpickr_months;
47
    flatpickr.l10ns.default.months   = flatpickr_months;
Lines 80-85 Link Here
80
                }
92
                }
81
            }
93
            }
82
        },
94
        },
95
        onOpen: function( selectedDates, dateText, instance ) {
96
            let element = instance.altInput;
97
            let options = maskitoDateOptionsGenerator({
98
                mode: altinput_dateformat, 
99
                separator: delimiter,
100
            });
101
            new Maskito( instance.altInput, options );
102
        },
83
        onClose: function( selectedDates, dateText, instance) {
103
        onClose: function( selectedDates, dateText, instance) {
84
            validate_date( dateText, instance );
104
            validate_date( dateText, instance );
85
            var thisInput = instance.input;
105
            var thisInput = instance.input;
86
- 

Return to bug 33703