Bugzilla – Attachment 163982 Details for
Bug 33703
Entering dates should be more flexible accepting different entry formats
Home
|
New
|
Browse
|
Search
|
[?]
|
Reports
|
Help
|
New Account
|
Log In
[x]
|
Forgot Password
Login:
[x]
[patch]
Bug 33703: Add Maskito support for OPAC
Bug-33703-Add-Maskito-support-for-OPAC.patch (text/plain), 95.42 KB, created by
Owen Leonard
on 2024-03-27 12:03:01 UTC
(
hide
)
Description:
Bug 33703: Add Maskito support for OPAC
Filename:
MIME Type:
Creator:
Owen Leonard
Created:
2024-03-27 12:03:01 UTC
Size:
95.42 KB
patch
obsolete
>From 0ccc06e08eddeb32734f582761abe6061157084f Mon Sep 17 00:00:00 2001 >From: Lucas Gass <lucas@bywatersolutions.com> >Date: Mon, 4 Mar 2024 18:39:35 +0000 >Subject: [PATCH] Bug 33703: Add Maskito support for OPAC > >Signed-off-by: Martin Renvoize <martin.renvoize@ptfs-europe.com> >Signed-off-by: Owen Leonard <oleonard@myacpl.org> >--- > .../bootstrap/en/includes/calendar.inc | 21 + > .../opac-tmpl/lib/maskito/maskito.core.js | 662 +++++++ > .../opac-tmpl/lib/maskito/maskito.kit.js | 1608 +++++++++++++++++ > 3 files changed, 2291 insertions(+) > create mode 100644 koha-tmpl/opac-tmpl/lib/maskito/maskito.core.js > create mode 100644 koha-tmpl/opac-tmpl/lib/maskito/maskito.kit.js > >diff --git a/koha-tmpl/opac-tmpl/bootstrap/en/includes/calendar.inc b/koha-tmpl/opac-tmpl/bootstrap/en/includes/calendar.inc >index e35701b81a..f2fafa95d1 100644 >--- a/koha-tmpl/opac-tmpl/bootstrap/en/includes/calendar.inc >+++ b/koha-tmpl/opac-tmpl/bootstrap/en/includes/calendar.inc >@@ -19,18 +19,28 @@ > var flatpickr_timeformat_string = [% IF Koha.Preference('TimeFormat') == '12hr' %]"G:i K"[% ELSE %]"H:i"[% END %]; > var flatpickr_timeformat = [% IF Koha.Preference('TimeFormat') == '12hr' %]false[% ELSE %]true[% END %]; > var flatpickr_dateformat_string = ""; >+ var delimter = ""; >+ var altinput_dateformat = ""; > switch ( dateformat_pref ){ > case "us": > flatpickr_dateformat_string = "m/d/Y"; >+ altinput_dateformat = 'mm/dd/yyyy'; >+ delimiter = "/"; > break; > case "metric": > flatpickr_dateformat_string = "d/m/Y"; >+ altinput_dateformat = 'dd/mm/yyyy'; >+ delimiter = "/"; > break; > case "dmydot": > flatpickr_dateformat_string = "d.m.Y"; >+ altinput_dateformat = 'dd/mm/yyyy'; >+ delimiter = "."; > break; > default: > flatpickr_dateformat_string = "Y-m-d"; >+ altinput_dateformat = 'yyyy/mm/dd'; >+ delimiter = "-"; > } > > function get_dateformat_str(dateformat) { >@@ -48,6 +58,8 @@ > } > </script> > [% Asset.js("lib/flatpickr/flatpickr.min.js") | $raw %] >+[% Asset.js("lib/maskito/maskito.core.js") | $raw %] >+[% Asset.js("lib/maskito/maskito.kit.js") | $raw %] > <script> > flatpickr.l10ns.default.weekdays = flatpickr_weekdays; > flatpickr.l10ns.default.months = flatpickr_months; >@@ -91,6 +103,15 @@ > } > }); > }, >+ onOpen: function( selectedDates, dateText, instance ) { >+ var thisInput = instance.input; >+ let options = maskitoDateOptionsGenerator({ >+ mode: altinput_dateformat, >+ separator: delimiter, >+ }); >+ >+ new Maskito( instance.altInput, options ); >+ }, > onClose: function( selectedDates, dateText, instance) { > validate_date( dateText, instance ); > var thisInput = instance.input; >diff --git a/koha-tmpl/opac-tmpl/lib/maskito/maskito.core.js b/koha-tmpl/opac-tmpl/lib/maskito/maskito.core.js >new file mode 100644 >index 0000000000..7eaf451c0f >--- /dev/null >+++ b/koha-tmpl/opac-tmpl/lib/maskito/maskito.core.js >@@ -0,0 +1,662 @@ >+const MASKITO_DEFAULT_ELEMENT_PREDICATE = e => e.querySelector('input,textarea') || >+ e; >+ >+const MASKITO_DEFAULT_OPTIONS = { >+ mask: /^.*$/, >+ preprocessors: [], >+ postprocessors: [], >+ plugins: [], >+ overwriteMode: 'shift', >+}; >+ >+class MaskHistory { >+ constructor() { >+ this.now = null; >+ this.past = []; >+ this.future = []; >+ } >+ undo() { >+ const state = this.past.pop(); >+ if (state && this.now) { >+ this.future.push(this.now); >+ this.updateElement(state, 'historyUndo'); >+ } >+ } >+ redo() { >+ const state = this.future.pop(); >+ if (state && this.now) { >+ this.past.push(this.now); >+ this.updateElement(state, 'historyRedo'); >+ } >+ } >+ updateHistory(state) { >+ if (!this.now) { >+ this.now = state; >+ return; >+ } >+ const isValueChanged = this.now.value !== state.value; >+ const isSelectionChanged = this.now.selection.some((item, index) => item !== state.selection[index]); >+ if (!isValueChanged && !isSelectionChanged) { >+ return; >+ } >+ if (isValueChanged) { >+ this.past.push(this.now); >+ this.future = []; >+ } >+ this.now = state; >+ } >+ updateElement(state, inputType) { >+ this.now = state; >+ this.updateElementState(state, { inputType, data: null }); >+ } >+} >+ >+function areElementValuesEqual(sampleState, ...states) { >+ return states.every(({ value }) => value === sampleState.value); >+} >+function areElementStatesEqual(sampleState, ...states) { >+ return states.every(({ value, selection }) => value === sampleState.value && >+ selection[0] === sampleState.selection[0] && >+ selection[1] === sampleState.selection[1]); >+} >+ >+function applyOverwriteMode({ value, selection }, newCharacters, mode) { >+ const [from, to] = selection; >+ const computedMode = typeof mode === 'function' ? mode({ value, selection }) : mode; >+ return { >+ value, >+ selection: computedMode === 'replace' ? [from, from + newCharacters.length] : [from, to], >+ }; >+} >+ >+function isFixedCharacter(char) { >+ return typeof char === 'string'; >+} >+ >+function getLeadingFixedCharacters(mask, validatedValuePart, newCharacter, initialElementState) { >+ let leadingFixedCharacters = ''; >+ for (let i = validatedValuePart.length; i < mask.length; i++) { >+ const charConstraint = mask[i]; >+ const isInitiallyExisted = (initialElementState === null || initialElementState === void 0 ? void 0 : initialElementState.value[i]) === charConstraint; >+ if (!isFixedCharacter(charConstraint) || >+ (charConstraint === newCharacter && !isInitiallyExisted)) { >+ return leadingFixedCharacters; >+ } >+ leadingFixedCharacters += charConstraint; >+ } >+ return leadingFixedCharacters; >+} >+ >+function validateValueWithMask(value, maskExpression) { >+ if (Array.isArray(maskExpression)) { >+ return (value.length === maskExpression.length && >+ Array.from(value).every((char, i) => { >+ const charConstraint = maskExpression[i]; >+ return isFixedCharacter(charConstraint) >+ ? char === charConstraint >+ : char.match(charConstraint); >+ })); >+ } >+ return maskExpression.test(value); >+} >+ >+function guessValidValueByPattern(elementState, mask, initialElementState) { >+ let maskedFrom = null; >+ let maskedTo = null; >+ const maskedValue = Array.from(elementState.value).reduce((validatedCharacters, char, charIndex) => { >+ const leadingCharacters = getLeadingFixedCharacters(mask, validatedCharacters, char, initialElementState); >+ const newValidatedChars = validatedCharacters + leadingCharacters; >+ const charConstraint = mask[newValidatedChars.length]; >+ if (isFixedCharacter(charConstraint)) { >+ return newValidatedChars + charConstraint; >+ } >+ if (!char.match(charConstraint)) { >+ return newValidatedChars; >+ } >+ if (maskedFrom === null && charIndex >= elementState.selection[0]) { >+ maskedFrom = newValidatedChars.length; >+ } >+ if (maskedTo === null && charIndex >= elementState.selection[1]) { >+ maskedTo = newValidatedChars.length; >+ } >+ return newValidatedChars + char; >+ }, ''); >+ const trailingFixedCharacters = getLeadingFixedCharacters(mask, maskedValue, '', initialElementState); >+ return { >+ value: validateValueWithMask(maskedValue + trailingFixedCharacters, mask) >+ ? maskedValue + trailingFixedCharacters >+ : maskedValue, >+ selection: [maskedFrom !== null && maskedFrom !== void 0 ? maskedFrom : maskedValue.length, maskedTo !== null && maskedTo !== void 0 ? maskedTo : maskedValue.length], >+ }; >+} >+ >+function guessValidValueByRegExp({ value, selection }, maskRegExp) { >+ const [from, to] = selection; >+ let newFrom = from; >+ let newTo = to; >+ const validatedValue = Array.from(value).reduce((validatedValuePart, char, i) => { >+ const newPossibleValue = validatedValuePart + char; >+ if (from === i) { >+ newFrom = validatedValuePart.length; >+ } >+ if (to === i) { >+ newTo = validatedValuePart.length; >+ } >+ return newPossibleValue.match(maskRegExp) ? newPossibleValue : validatedValuePart; >+ }, ''); >+ return { value: validatedValue, selection: [newFrom, newTo] }; >+} >+ >+function calibrateValueByMask(elementState, mask, initialElementState = null) { >+ if (validateValueWithMask(elementState.value, mask)) { >+ return elementState; >+ } >+ const { value, selection } = Array.isArray(mask) >+ ? guessValidValueByPattern(elementState, mask, initialElementState) >+ : guessValidValueByRegExp(elementState, mask); >+ return { >+ selection, >+ value: Array.isArray(mask) ? value.slice(0, mask.length) : value, >+ }; >+} >+ >+function removeFixedMaskCharacters(initialElementState, mask) { >+ if (!Array.isArray(mask)) { >+ return initialElementState; >+ } >+ const [from, to] = initialElementState.selection; >+ const selection = []; >+ const unmaskedValue = Array.from(initialElementState.value).reduce((rawValue, char, i) => { >+ const charConstraint = mask[i]; >+ if (i === from) { >+ selection.push(rawValue.length); >+ } >+ if (i === to) { >+ selection.push(rawValue.length); >+ } >+ return isFixedCharacter(charConstraint) && charConstraint === char >+ ? rawValue >+ : rawValue + char; >+ }, ''); >+ if (selection.length < 2) { >+ selection.push(...new Array(2 - selection.length).fill(unmaskedValue.length)); >+ } >+ return { >+ value: unmaskedValue, >+ selection: [selection[0], selection[1]], >+ }; >+} >+ >+class MaskModel { >+ constructor(initialElementState, maskOptions) { >+ this.initialElementState = initialElementState; >+ this.maskOptions = maskOptions; >+ this.value = ''; >+ this.selection = [0, 0]; >+ const { value, selection } = calibrateValueByMask(initialElementState, this.getMaskExpression(initialElementState)); >+ this.value = value; >+ this.selection = selection; >+ } >+ addCharacters([from, to], newCharacters) { >+ const { value } = this; >+ const maskExpression = this.getMaskExpression({ >+ value: value.slice(0, from) + newCharacters + value.slice(to), >+ selection: [from + newCharacters.length, from + newCharacters.length], >+ }); >+ const initialElementState = { value, selection: [from, to] }; >+ const unmaskedElementState = removeFixedMaskCharacters(initialElementState, maskExpression); >+ const [unmaskedFrom, unmaskedTo] = applyOverwriteMode(unmaskedElementState, newCharacters, this.maskOptions.overwriteMode).selection; >+ const newUnmaskedLeadingValuePart = unmaskedElementState.value.slice(0, unmaskedFrom) + newCharacters; >+ const newCaretIndex = newUnmaskedLeadingValuePart.length; >+ const maskedElementState = calibrateValueByMask({ >+ value: newUnmaskedLeadingValuePart + >+ unmaskedElementState.value.slice(unmaskedTo), >+ selection: [newCaretIndex, newCaretIndex], >+ }, maskExpression, initialElementState); >+ const isInvalidCharsInsertion = >+ // eslint-disable-next-line @typescript-eslint/prefer-string-starts-ends-with >+ value.slice(0, unmaskedFrom) === >+ calibrateValueByMask({ >+ value: newUnmaskedLeadingValuePart, >+ selection: [newCaretIndex, newCaretIndex], >+ }, maskExpression, initialElementState).value; >+ if (isInvalidCharsInsertion || >+ areElementStatesEqual(this, maskedElementState) // If typing new characters does not change value >+ ) { >+ throw new Error('Invalid mask value'); >+ } >+ this.value = maskedElementState.value; >+ this.selection = maskedElementState.selection; >+ } >+ deleteCharacters([from, to]) { >+ if (from === to || !to) { >+ return; >+ } >+ const { value } = this; >+ const maskExpression = this.getMaskExpression({ >+ value: value.slice(0, from) + value.slice(to), >+ selection: [from, from], >+ }); >+ const initialElementState = { value, selection: [from, to] }; >+ const unmaskedElementState = removeFixedMaskCharacters(initialElementState, maskExpression); >+ const [unmaskedFrom, unmaskedTo] = unmaskedElementState.selection; >+ const newUnmaskedValue = unmaskedElementState.value.slice(0, unmaskedFrom) + >+ unmaskedElementState.value.slice(unmaskedTo); >+ const maskedElementState = calibrateValueByMask({ value: newUnmaskedValue, selection: [unmaskedFrom, unmaskedFrom] }, maskExpression, initialElementState); >+ this.value = maskedElementState.value; >+ this.selection = maskedElementState.selection; >+ } >+ getMaskExpression(elementState) { >+ const { mask } = this.maskOptions; >+ return typeof mask === 'function' ? mask(elementState) : mask; >+ } >+} >+ >+class EventListener { >+ constructor(element) { >+ this.element = element; >+ this.listeners = []; >+ } >+ listen(eventType, fn, options) { >+ const untypedFn = fn; >+ this.element.addEventListener(eventType, untypedFn, options); >+ this.listeners.push(() => this.element.removeEventListener(eventType, untypedFn)); >+ } >+ destroy() { >+ this.listeners.forEach(stopListen => stopListen()); >+ } >+} >+ >+const HotkeyModifier = { >+ CTRL: 1 << 0, >+ ALT: 1 << 1, >+ SHIFT: 1 << 2, >+ META: 1 << 3, >+}; >+// TODO add variants that can be processed correctly >+const HotkeyCode = { >+ Y: 89, >+ Z: 90, >+}; >+/** >+ * Checks if the passed keyboard event match the required hotkey. >+ * >+ * @example >+ * input.addEventListener('keydown', (event) => { >+ * if (isHotkey(event, HotkeyModifier.CTRL | HotkeyModifier.SHIFT, HotkeyCode.Z)) { >+ * // redo hotkey pressed >+ * } >+ * }) >+ * >+ * @return will return `true` only if the {@link HotkeyCode} matches and only the necessary >+ * {@link HotkeyModifier modifiers} have been pressed >+ */ >+function isHotkey(event, modifiers, hotkeyCode) { >+ return (event.ctrlKey === !!(modifiers & HotkeyModifier.CTRL) && >+ event.altKey === !!(modifiers & HotkeyModifier.ALT) && >+ event.shiftKey === !!(modifiers & HotkeyModifier.SHIFT) && >+ event.metaKey === !!(modifiers & HotkeyModifier.META) && >+ /** >+ * We intentionally use legacy {@link KeyboardEvent#keyCode `keyCode`} property. It is more >+ * "keyboard-layout"-independent than {@link KeyboardEvent#key `key`} or {@link KeyboardEvent#code `code`} properties. >+ * @see {@link https://github.com/taiga-family/maskito/issues/315 `KeyboardEvent#code` issue} >+ */ >+ // eslint-disable-next-line sonar/deprecation >+ event.keyCode === hotkeyCode); >+} >+ >+function isRedo(event) { >+ return (isHotkey(event, HotkeyModifier.CTRL, HotkeyCode.Y) || // Windows >+ isHotkey(event, HotkeyModifier.CTRL | HotkeyModifier.SHIFT, HotkeyCode.Z) || // Windows & Android >+ isHotkey(event, HotkeyModifier.META | HotkeyModifier.SHIFT, HotkeyCode.Z) // macOS & iOS >+ ); >+} >+function isUndo(event) { >+ return (isHotkey(event, HotkeyModifier.CTRL, HotkeyCode.Z) || // Windows & Android >+ isHotkey(event, HotkeyModifier.META, HotkeyCode.Z) // macOS & iOS >+ ); >+} >+ >+/** >+ * Sets value to element, and dispatches input event >+ * if you passed ELementState, it also sets selection range >+ * >+ * @example >+ * maskitoUpdateElement(input, newValue); >+ * maskitoUpdateElement(input, elementState); >+ * >+ * @see {@link https://github.com/taiga-family/maskito/issues/804 issue} >+ * >+ * @return void >+ */ >+function maskitoUpdateElement(element, valueOrElementState) { >+ var _a; >+ if (typeof valueOrElementState === 'string') { >+ element.value = valueOrElementState; >+ } >+ else { >+ const [from, to] = valueOrElementState.selection; >+ element.value = valueOrElementState.value; >+ (_a = element.setSelectionRange) === null || _a === void 0 ? void 0 : _a.call(element, from, to); >+ } >+ element.dispatchEvent(new Event('input', >+ /** >+ * React handles this event only on bubbling phase >+ * >+ * here is the list of events that are processed in the capture stage, others are processed in the bubbling stage >+ * https://github.com/facebook/react/blob/cb2439624f43c510007f65aea5c50a8bb97917e4/packages/react-dom-bindings/src/events/DOMPluginEventSystem.js#L222 >+ */ >+ { bubbles: true })); >+} >+ >+function getLineSelection({ value, selection }, isForward) { >+ const [from, to] = selection; >+ if (from !== to) { >+ return [from, to]; >+ } >+ const nearestBreak = isForward >+ ? value.slice(from).indexOf('\n') + 1 || value.length >+ : value.slice(0, to).lastIndexOf('\n') + 1; >+ const selectFrom = isForward ? from : nearestBreak; >+ const selectTo = isForward ? nearestBreak : to; >+ return [selectFrom, selectTo]; >+} >+ >+function getNotEmptySelection({ value, selection }, isForward) { >+ const [from, to] = selection; >+ if (from !== to) { >+ return [from, to]; >+ } >+ const notEmptySelection = isForward ? [from, to + 1] : [from - 1, to]; >+ return notEmptySelection.map(x => Math.min(Math.max(x, 0), value.length)); >+} >+ >+const TRAILING_SPACES_REG = /\s+$/g; >+const LEADING_SPACES_REG = /^\s+/g; >+const SPACE_REG = /\s/; >+function getWordSelection({ value, selection }, isForward) { >+ const [from, to] = selection; >+ if (from !== to) { >+ return [from, to]; >+ } >+ if (isForward) { >+ const valueAfterSelectionStart = value.slice(from); >+ const [leadingSpaces] = valueAfterSelectionStart.match(LEADING_SPACES_REG) || [ >+ '', >+ ]; >+ const nearestWordEndIndex = valueAfterSelectionStart >+ .trimStart() >+ .search(SPACE_REG); >+ return [ >+ from, >+ nearestWordEndIndex !== -1 >+ ? from + leadingSpaces.length + nearestWordEndIndex >+ : value.length, >+ ]; >+ } >+ const valueBeforeSelectionEnd = value.slice(0, to); >+ const [trailingSpaces] = valueBeforeSelectionEnd.match(TRAILING_SPACES_REG) || ['']; >+ const selectedWordLength = valueBeforeSelectionEnd >+ .trimEnd() >+ .split('') >+ .reverse() >+ .findIndex(char => char.match(SPACE_REG)); >+ return [ >+ selectedWordLength !== -1 ? to - trailingSpaces.length - selectedWordLength : 0, >+ to, >+ ]; >+} >+ >+/* eslint-disable @typescript-eslint/ban-types */ >+/** >+ * @internal >+ */ >+function maskitoPipe(processors = []) { >+ return (initialData, ...readonlyArgs) => processors.reduce((data, fn) => (Object.assign(Object.assign({}, data), fn(data, ...readonlyArgs))), initialData); >+} >+ >+function maskitoTransform(valueOrState, maskitoOptions) { >+ const options = Object.assign(Object.assign({}, MASKITO_DEFAULT_OPTIONS), maskitoOptions); >+ const preprocessor = maskitoPipe(options.preprocessors); >+ const postprocessor = maskitoPipe(options.postprocessors); >+ const initialElementState = typeof valueOrState === 'string' >+ ? { value: valueOrState, selection: [0, 0] } >+ : valueOrState; >+ const { elementState } = preprocessor({ elementState: initialElementState, data: '' }, 'validation'); >+ const maskModel = new MaskModel(elementState, options); >+ const { value, selection } = postprocessor(maskModel, initialElementState); >+ return typeof valueOrState === 'string' ? value : { value, selection }; >+} >+ >+function maskitoInitialCalibrationPlugin(customOptions) { >+ return (element, options) => { >+ const from = element.selectionStart || 0; >+ const to = element.selectionEnd || 0; >+ maskitoUpdateElement(element, { >+ value: maskitoTransform(element.value, customOptions || options), >+ selection: [from, to], >+ }); >+ }; >+} >+ >+function maskitoStrictCompositionPlugin() { >+ return (element, maskitoOptions) => { >+ const listener = (event) => { >+ if (event.inputType !== 'insertCompositionText') { >+ return; >+ } >+ const selection = [ >+ element.selectionStart || 0, >+ element.selectionEnd || 0, >+ ]; >+ const elementState = { >+ selection, >+ value: element.value, >+ }; >+ const validatedState = maskitoTransform(elementState, maskitoOptions); >+ if (!areElementStatesEqual(elementState, validatedState)) { >+ event.preventDefault(); >+ maskitoUpdateElement(element, validatedState); >+ } >+ }; >+ element.addEventListener('input', listener); >+ return () => element.removeEventListener('input', listener); >+ }; >+} >+ >+class Maskito extends MaskHistory { >+ constructor(element, maskitoOptions) { >+ super(); >+ this.element = element; >+ this.maskitoOptions = maskitoOptions; >+ this.isTextArea = this.element.nodeName === 'TEXTAREA'; >+ this.eventListener = new EventListener(this.element); >+ this.options = Object.assign(Object.assign({}, MASKITO_DEFAULT_OPTIONS), this.maskitoOptions); >+ this.preprocessor = maskitoPipe(this.options.preprocessors); >+ this.postprocessor = maskitoPipe(this.options.postprocessors); >+ this.teardowns = this.options.plugins.map(plugin => plugin(this.element, this.options)); >+ this.updateHistory(this.elementState); >+ this.eventListener.listen('keydown', event => { >+ if (isRedo(event)) { >+ event.preventDefault(); >+ return this.redo(); >+ } >+ if (isUndo(event)) { >+ event.preventDefault(); >+ return this.undo(); >+ } >+ }); >+ this.eventListener.listen('beforeinput', event => { >+ const isForward = event.inputType.includes('Forward'); >+ this.updateHistory(this.elementState); >+ switch (event.inputType) { >+ // historyUndo/historyRedo will not be triggered if value was modified programmatically >+ case 'historyUndo': >+ event.preventDefault(); >+ return this.undo(); >+ case 'historyRedo': >+ event.preventDefault(); >+ return this.redo(); >+ case 'deleteByCut': >+ case 'deleteContentBackward': >+ case 'deleteContentForward': >+ return this.handleDelete({ >+ event, >+ isForward, >+ selection: getNotEmptySelection(this.elementState, isForward), >+ }); >+ case 'deleteWordForward': >+ case 'deleteWordBackward': >+ return this.handleDelete({ >+ event, >+ isForward, >+ selection: getWordSelection(this.elementState, isForward), >+ force: true, >+ }); >+ case 'deleteSoftLineBackward': >+ case 'deleteSoftLineForward': >+ case 'deleteHardLineBackward': >+ case 'deleteHardLineForward': >+ return this.handleDelete({ >+ event, >+ isForward, >+ selection: getLineSelection(this.elementState, isForward), >+ force: true, >+ }); >+ case 'insertCompositionText': >+ return; // will be handled inside `compositionend` event >+ case 'insertLineBreak': >+ return this.handleEnter(event); >+ case 'insertFromPaste': >+ case 'insertText': >+ case 'insertFromDrop': >+ default: >+ return this.handleInsert(event, event.data || ''); >+ } >+ }); >+ this.eventListener.listen('input', ({ inputType }) => { >+ if (inputType === 'insertCompositionText') { >+ return; // will be handled inside `compositionend` event >+ } >+ this.ensureValueFitsMask(); >+ this.updateHistory(this.elementState); >+ }); >+ this.eventListener.listen('compositionend', () => { >+ this.ensureValueFitsMask(); >+ this.updateHistory(this.elementState); >+ }); >+ } >+ get elementState() { >+ const { value, selectionStart, selectionEnd } = this.element; >+ return { >+ value, >+ selection: [selectionStart || 0, selectionEnd || 0], >+ }; >+ } >+ get maxLength() { >+ const { maxLength } = this.element; >+ return maxLength === -1 ? Infinity : maxLength; >+ } >+ destroy() { >+ this.eventListener.destroy(); >+ this.teardowns.forEach(teardown => teardown === null || teardown === void 0 ? void 0 : teardown()); >+ } >+ updateElementState({ value, selection }, eventInit = { >+ inputType: 'insertText', >+ data: null, >+ }) { >+ const initialValue = this.elementState.value; >+ this.updateValue(value); >+ this.updateSelectionRange(selection); >+ if (initialValue !== value) { >+ this.dispatchInputEvent(eventInit); >+ } >+ } >+ updateSelectionRange([from, to]) { >+ var _a, _b; >+ if (this.element.selectionStart !== from || this.element.selectionEnd !== to) { >+ (_b = (_a = this.element).setSelectionRange) === null || _b === void 0 ? void 0 : _b.call(_a, from, to); >+ } >+ } >+ updateValue(value) { >+ this.element.value = value; >+ } >+ ensureValueFitsMask() { >+ this.updateElementState(maskitoTransform(this.elementState, this.options)); >+ } >+ dispatchInputEvent(eventInit = { >+ inputType: 'insertText', >+ data: null, >+ }) { >+ if (globalThis.InputEvent) { >+ this.element.dispatchEvent(new InputEvent('input', Object.assign(Object.assign({}, eventInit), { bubbles: true, cancelable: false }))); >+ } >+ } >+ handleDelete({ event, selection, isForward, force = false, }) { >+ const initialState = { >+ value: this.elementState.value, >+ selection, >+ }; >+ const [initialFrom, initialTo] = initialState.selection; >+ const { elementState } = this.preprocessor({ >+ elementState: initialState, >+ data: '', >+ }, isForward ? 'deleteForward' : 'deleteBackward'); >+ const maskModel = new MaskModel(elementState, this.options); >+ const [from, to] = elementState.selection; >+ maskModel.deleteCharacters([from, to]); >+ const newElementState = this.postprocessor(maskModel, initialState); >+ const newPossibleValue = initialState.value.slice(0, initialFrom) + >+ initialState.value.slice(initialTo); >+ if (newPossibleValue === newElementState.value && !force) { >+ return; >+ } >+ event.preventDefault(); >+ if (areElementValuesEqual(initialState, elementState, maskModel, newElementState)) { >+ // User presses Backspace/Delete for the fixed value >+ return this.updateSelectionRange(isForward ? [to, to] : [from, from]); >+ } >+ this.updateElementState(newElementState, { >+ inputType: event.inputType, >+ data: null, >+ }); >+ this.updateHistory(newElementState); >+ } >+ handleInsert(event, data) { >+ const initialElementState = this.elementState; >+ const { elementState, data: insertedText = data } = this.preprocessor({ >+ data, >+ elementState: initialElementState, >+ }, 'insert'); >+ const maskModel = new MaskModel(elementState, this.options); >+ try { >+ maskModel.addCharacters(elementState.selection, insertedText); >+ } >+ catch (_a) { >+ return event.preventDefault(); >+ } >+ const [from, to] = elementState.selection; >+ const newPossibleValue = initialElementState.value.slice(0, from) + >+ data + >+ initialElementState.value.slice(to); >+ const newElementState = this.postprocessor(maskModel, initialElementState); >+ if (newElementState.value.length > this.maxLength) { >+ return event.preventDefault(); >+ } >+ if (newPossibleValue !== newElementState.value) { >+ event.preventDefault(); >+ this.updateElementState(newElementState, { >+ data, >+ inputType: event.inputType, >+ }); >+ this.updateHistory(newElementState); >+ } >+ } >+ handleEnter(event) { >+ if (this.isTextArea) { >+ this.handleInsert(event, '\n'); >+ } >+ } >+} >+ >+//export { MASKITO_DEFAULT_ELEMENT_PREDICATE, MASKITO_DEFAULT_OPTIONS, Maskito, maskitoInitialCalibrationPlugin, maskitoPipe, maskitoStrictCompositionPlugin, maskitoTransform, maskitoUpdateElement }; >diff --git a/koha-tmpl/opac-tmpl/lib/maskito/maskito.kit.js b/koha-tmpl/opac-tmpl/lib/maskito/maskito.kit.js >new file mode 100644 >index 0000000000..2c01dd271f >--- /dev/null >+++ b/koha-tmpl/opac-tmpl/lib/maskito/maskito.kit.js >@@ -0,0 +1,1608 @@ >+//import { maskitoUpdateElement, MASKITO_DEFAULT_OPTIONS, maskitoTransform } from '@maskito/core'; >+ >+const DEFAULT_DECIMAL_PSEUDO_SEPARATORS = ['.', ',', 'б', 'Ñ']; >+ >+const DEFAULT_MIN_DATE = new Date('0001-01-01'); >+const DEFAULT_MAX_DATE = new Date('9999-12-31'); >+ >+const DEFAULT_TIME_SEGMENT_MAX_VALUES = { >+ hours: 23, >+ minutes: 59, >+ seconds: 59, >+ milliseconds: 999, >+}; >+ >+/** >+ * {@link https://unicode-table.com/en/00A0/ Non-breaking space}. >+ */ >+const CHAR_NO_BREAK_SPACE = '\u00A0'; >+/** >+ * {@link https://symbl.cc/en/200B/ Zero width space}. >+ */ >+const CHAR_ZERO_WIDTH_SPACE = '\u200B'; >+/** >+ * {@link https://unicode-table.com/en/2013/ EN dash} >+ * is used to indicate a range of numbers or a span of time. >+ * @example 2006â2022 >+ */ >+const CHAR_EN_DASH = '\u2013'; >+/** >+ * {@link https://unicode-table.com/en/2014/ EM dash} >+ * is used to mark a break in a sentence. >+ * @example Taiga UI â powerful set of open source components for Angular >+ * ___ >+ * Don't confuse with {@link CHAR_EN_DASH} or {@link CHAR_HYPHEN}! >+ */ >+const CHAR_EM_DASH = '\u2014'; >+/** >+ * {@link https://unicode-table.com/en/002D/ Hyphen (minus sign)} >+ * is used to combine words. >+ * @example well-behaved >+ * ___ >+ * Don't confuse with {@link CHAR_EN_DASH} or {@link CHAR_EM_DASH}! >+ */ >+const CHAR_HYPHEN = '\u002D'; >+/** >+ * {@link https://unicode-table.com/en/2212/ Minus} >+ * is used as math operator symbol or before negative digits. >+ * --- >+ * Can be used as `−`. Don't confuse with {@link CHAR_HYPHEN} >+ */ >+const CHAR_MINUS = '\u2212'; >+/** >+ * {@link https://symbl.cc/en/30FC/ Katakana-Hiragana Prolonged Sound Mark} >+ * is used as prolonged sounds in Japanese. >+ */ >+const CHAR_JP_HYPHEN = '\u30FC'; >+ >+const POSSIBLE_DATE_RANGE_SEPARATOR = [ >+ CHAR_HYPHEN, >+ CHAR_EN_DASH, >+ CHAR_EM_DASH, >+ CHAR_MINUS, >+]; >+const POSSIBLE_DATE_TIME_SEPARATOR = [',', ' ']; >+ >+const TIME_FIXED_CHARACTERS = [':', '.']; >+ >+const TIME_SEGMENT_VALUE_LENGTHS = { >+ hours: 2, >+ minutes: 2, >+ seconds: 2, >+ milliseconds: 3, >+}; >+ >+/** >+ * Clamps a value between two inclusive limits >+ * >+ * @param value >+ * @param min lower limit >+ * @param max upper limit >+ */ >+function clamp(value, min, max) { >+ const clampedValue = Math.min(Number(max), Math.max(Number(min), Number(value))); >+ return (value instanceof Date ? new Date(clampedValue) : clampedValue); >+} >+ >+function appendDate(initialDate, { day, month, year } = {}) { >+ const date = new Date(initialDate); >+ if (day) { >+ date.setDate(date.getDate() + day); >+ } >+ if (month) { >+ date.setMonth(date.getMonth() + month); >+ } >+ if (year) { >+ date.setFullYear(date.getFullYear() + year); >+ } >+ return date; >+} >+ >+const getDateSegmentValueLength = (dateString) => { >+ var _a, _b, _c; >+ return ({ >+ day: ((_a = dateString.match(/d/g)) === null || _a === void 0 ? void 0 : _a.length) || 0, >+ month: ((_b = dateString.match(/m/g)) === null || _b === void 0 ? void 0 : _b.length) || 0, >+ year: ((_c = dateString.match(/y/g)) === null || _c === void 0 ? void 0 : _c.length) || 0, >+ }); >+}; >+ >+function dateToSegments(date) { >+ return { >+ day: String(date.getDate()).padStart(2, '0'), >+ month: String(date.getMonth() + 1).padStart(2, '0'), >+ year: String(date.getFullYear()).padStart(4, '0'), >+ hours: String(date.getHours()).padStart(2, '0'), >+ minutes: String(date.getMinutes()).padStart(2, '0'), >+ seconds: String(date.getSeconds()).padStart(2, '0'), >+ milliseconds: String(date.getMilliseconds()).padStart(3, '0'), >+ }; >+} >+ >+function isDateStringComplete(dateString, dateModeTemplate) { >+ if (dateString.length < dateModeTemplate.length) { >+ return false; >+ } >+ return dateString.split(/\D/).every(segment => !segment.match(/^0+$/)); >+} >+ >+function parseDateRangeString(dateRange, dateModeTemplate, rangeSeparator) { >+ const digitsInDate = dateModeTemplate.replace(/\W/g, '').length; >+ return (dateRange >+ .replace(rangeSeparator, '') >+ .match(new RegExp(`(\\D*\\d[^\\d\\s]*){1,${digitsInDate}}`, 'g')) || []); >+} >+ >+function parseDateString(dateString, fullMode) { >+ const cleanMode = fullMode.replace(/[^dmy]/g, ''); >+ const onlyDigitsDate = dateString.replace(/\D+/g, ''); >+ const dateSegments = { >+ day: onlyDigitsDate.slice(cleanMode.indexOf('d'), cleanMode.lastIndexOf('d') + 1), >+ month: onlyDigitsDate.slice(cleanMode.indexOf('m'), cleanMode.lastIndexOf('m') + 1), >+ year: onlyDigitsDate.slice(cleanMode.indexOf('y'), cleanMode.lastIndexOf('y') + 1), >+ }; >+ return Object.fromEntries(Object.entries(dateSegments) >+ .filter(([_, value]) => Boolean(value)) >+ .sort(([a], [b]) => fullMode.toLowerCase().indexOf(a[0]) > >+ fullMode.toLowerCase().indexOf(b[0]) >+ ? 1 >+ : -1)); >+} >+ >+function segmentsToDate(parsedDate, parsedTime) { >+ var _a, _b, _c, _d, _e, _f, _g; >+ const year = ((_a = parsedDate.year) === null || _a === void 0 ? void 0 : _a.length) === 2 ? `20${parsedDate.year}` : parsedDate.year; >+ 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')); >+ // needed for years less than 1900 >+ date.setFullYear(Number(year !== null && year !== void 0 ? year : '0')); >+ return date; >+} >+ >+const DATE_TIME_SEPARATOR = ', '; >+ >+function toDateString({ day, month, year, hours, minutes, seconds, milliseconds, }, dateMode, timeMode) { >+ var _a; >+ 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; >+ const fullMode = dateMode + (timeMode ? DATE_TIME_SEPARATOR + timeMode : ''); >+ return fullMode >+ .replace(/d+/g, day !== null && day !== void 0 ? day : '') >+ .replace(/m+/g, month !== null && month !== void 0 ? month : '') >+ .replace(/y+/g, safeYear !== null && safeYear !== void 0 ? safeYear : '') >+ .replace(/H+/g, hours !== null && hours !== void 0 ? hours : '') >+ .replace(/MSS/g, milliseconds !== null && milliseconds !== void 0 ? milliseconds : '') >+ .replace(/M+/g, minutes !== null && minutes !== void 0 ? minutes : '') >+ .replace(/S+/g, seconds !== null && seconds !== void 0 ? seconds : '') >+ .replace(/^\D+/g, '') >+ .replace(/\D+$/g, ''); >+} >+ >+function padWithZeroesUntilValid(segmentValue, paddedMaxValue, prefixedZeroesCount = 0) { >+ if (Number(segmentValue.padEnd(paddedMaxValue.length, '0')) <= Number(paddedMaxValue)) { >+ return { validatedSegmentValue: segmentValue, prefixedZeroesCount }; >+ } >+ if (segmentValue.endsWith('0')) { >+ // 00:|00 => Type 9 => 00:09| >+ return padWithZeroesUntilValid(`0${segmentValue.slice(0, paddedMaxValue.length - 1)}`, paddedMaxValue, prefixedZeroesCount + 1); >+ } >+ // |19:00 => Type 2 => 2|0:00 >+ return padWithZeroesUntilValid(`${segmentValue.slice(0, paddedMaxValue.length - 1)}0`, paddedMaxValue, prefixedZeroesCount); >+} >+ >+const dateMaxValues = { >+ day: 31, >+ month: 12, >+ year: 9999, >+}; >+function validateDateString({ dateString, dateModeTemplate, offset, selection: [from, to], }) { >+ const parsedDate = parseDateString(dateString, dateModeTemplate); >+ const dateSegments = Object.entries(parsedDate); >+ const validatedDateSegments = {}; >+ let paddedZeroes = 0; >+ for (const [segmentName, segmentValue] of dateSegments) { >+ const validatedDate = toDateString(validatedDateSegments, dateModeTemplate); >+ const maxSegmentValue = dateMaxValues[segmentName]; >+ const fantomSeparator = validatedDate.length && 1; >+ const lastSegmentDigitIndex = offset + >+ validatedDate.length + >+ fantomSeparator + >+ getDateSegmentValueLength(dateModeTemplate)[segmentName]; >+ const isLastSegmentDigitAdded = lastSegmentDigitIndex >= from && lastSegmentDigitIndex === to; >+ if (isLastSegmentDigitAdded && Number(segmentValue) > Number(maxSegmentValue)) { >+ // 3|1.10.2010 => Type 9 => 3|1.10.2010 >+ return { validatedDateString: '', updatedSelection: [from, to] }; // prevent changes >+ } >+ if (isLastSegmentDigitAdded && Number(segmentValue) < 1) { >+ // 31.0|1.2010 => Type 0 => 31.0|1.2010 >+ return { validatedDateString: '', updatedSelection: [from, to] }; // prevent changes >+ } >+ const { validatedSegmentValue, prefixedZeroesCount } = padWithZeroesUntilValid(segmentValue, `${maxSegmentValue}`); >+ paddedZeroes += prefixedZeroesCount; >+ validatedDateSegments[segmentName] = validatedSegmentValue; >+ } >+ const validatedDateString = toDateString(validatedDateSegments, dateModeTemplate); >+ const addedDateSegmentSeparators = validatedDateString.length - dateString.length; >+ return { >+ validatedDateString, >+ updatedSelection: [ >+ from + paddedZeroes + addedDateSegmentSeparators, >+ to + paddedZeroes + addedDateSegmentSeparators, >+ ], >+ }; >+} >+ >+/** >+ * Copy-pasted solution from lodash >+ * @see https://lodash.com/docs/4.17.15#escapeRegExp >+ */ >+const reRegExpChar = /[\\^$.*+?()[\]{}|]/g; >+const reHasRegExpChar = new RegExp(reRegExpChar.source); >+function escapeRegExp(str) { >+ return str && reHasRegExpChar.test(str) ? str.replace(reRegExpChar, '\\$&') : str; >+} >+ >+function extractAffixes(value, { prefix, postfix }) { >+ var _a, _b; >+ const prefixRegExp = new RegExp(`^${escapeRegExp(prefix)}`); >+ const postfixRegExp = new RegExp(`${escapeRegExp(postfix)}$`); >+ const [extractedPrefix = ''] = (_a = value.match(prefixRegExp)) !== null && _a !== void 0 ? _a : []; >+ const [extractedPostfix = ''] = (_b = value.match(postfixRegExp)) !== null && _b !== void 0 ? _b : []; >+ const cleanValue = value.replace(prefixRegExp, '').replace(postfixRegExp, ''); >+ return { extractedPrefix, extractedPostfix, cleanValue }; >+} >+ >+function findCommonBeginningSubstr(a, b) { >+ let res = ''; >+ for (let i = 0; i < a.length; i++) { >+ if (a[i] !== b[i]) { >+ return res; >+ } >+ res += a[i]; >+ } >+ return res; >+} >+ >+/** >+ * Returns current active element, including shadow dom >+ * >+ * @return element or null >+ */ >+function getFocused({ activeElement }) { >+ if (!(activeElement === null || activeElement === void 0 ? void 0 : activeElement.shadowRoot)) { >+ return activeElement; >+ } >+ let element = activeElement.shadowRoot.activeElement; >+ while (element === null || element === void 0 ? void 0 : element.shadowRoot) { >+ element = element.shadowRoot.activeElement; >+ } >+ return element; >+} >+ >+function identity(x) { >+ return x; >+} >+ >+function isEmpty(entity) { >+ return !entity || (typeof entity === 'object' && Object.keys(entity).length === 0); >+} >+ >+function raiseSegmentValueToMin(segments, fullMode) { >+ const segmentsLength = getDateSegmentValueLength(fullMode); >+ return Object.fromEntries(Object.entries(segments).map(([key, value]) => { >+ const segmentLength = segmentsLength[key]; >+ return [ >+ key, >+ value.length === segmentLength && value.match(/^0+$/) >+ ? '1'.padStart(segmentLength, '0') >+ : value, >+ ]; >+ })); >+} >+ >+function createMinMaxDatePostprocessor({ dateModeTemplate, min = DEFAULT_MIN_DATE, max = DEFAULT_MAX_DATE, rangeSeparator = '', dateSegmentSeparator = '.', }) { >+ return ({ value, selection }) => { >+ const endsWithRangeSeparator = rangeSeparator && value.endsWith(rangeSeparator); >+ const dateStrings = parseDateRangeString(value, dateModeTemplate, rangeSeparator); >+ let validatedValue = ''; >+ for (const dateString of dateStrings) { >+ validatedValue += validatedValue ? rangeSeparator : ''; >+ const parsedDate = parseDateString(dateString, dateModeTemplate); >+ if (!isDateStringComplete(dateString, dateModeTemplate)) { >+ const fixedDate = raiseSegmentValueToMin(parsedDate, dateModeTemplate); >+ const fixedValue = toDateString(fixedDate, dateModeTemplate); >+ const tail = dateString.endsWith(dateSegmentSeparator) >+ ? dateSegmentSeparator >+ : ''; >+ validatedValue += fixedValue + tail; >+ continue; >+ } >+ const date = segmentsToDate(parsedDate); >+ const clampedDate = clamp(date, min, max); >+ validatedValue += toDateString(dateToSegments(clampedDate), dateModeTemplate); >+ } >+ return { >+ selection, >+ value: validatedValue + (endsWithRangeSeparator ? rangeSeparator : ''), >+ }; >+ }; >+} >+ >+function normalizeDatePreprocessor({ dateModeTemplate, dateSegmentsSeparator, rangeSeparator = '', }) { >+ return ({ elementState, data }) => { >+ const separator = rangeSeparator >+ ? new RegExp(`${rangeSeparator}|-`) >+ : DATE_TIME_SEPARATOR; >+ const possibleDates = data.split(separator); >+ const dates = data.includes(DATE_TIME_SEPARATOR) >+ ? [possibleDates[0]] >+ : possibleDates; >+ if (dates.every(date => date.trim().split(/\D/).length === >+ dateModeTemplate.split(dateSegmentsSeparator).length)) { >+ const newData = dates >+ .map(date => normalizeDateString(date, dateModeTemplate, dateSegmentsSeparator)) >+ .join(rangeSeparator); >+ return { >+ elementState, >+ data: `${newData}${data.includes(DATE_TIME_SEPARATOR) >+ ? DATE_TIME_SEPARATOR + possibleDates[1] || '' >+ : ''}`, >+ }; >+ } >+ return { elementState, data }; >+ }; >+} >+function normalizeDateString(dateString, template, separator) { >+ const dateSegments = dateString.split(/\D/); >+ const templateSegments = template.split(separator); >+ const normalizedSegments = dateSegments.map((segment, index) => index === templateSegments.length - 1 >+ ? segment >+ : segment.padStart(templateSegments[index].length, '0')); >+ return normalizedSegments.join(separator); >+} >+ >+function maskitoPostfixPostprocessorGenerator(postfix) { >+ const postfixRE = new RegExp(`${escapeRegExp(postfix)}$`); >+ return postfix >+ ? ({ value, selection }, initialElementState) => { >+ if (!value && !initialElementState.value.endsWith(postfix)) { >+ // cases when developer wants input to be empty (programmatically) >+ return { value, selection }; >+ } >+ if (!value.endsWith(postfix) && >+ !initialElementState.value.endsWith(postfix)) { >+ return { selection, value: value + postfix }; >+ } >+ const initialValueBeforePostfix = initialElementState.value.replace(postfixRE, ''); >+ const postfixWasModified = initialElementState.selection[1] >= initialValueBeforePostfix.length; >+ const alreadyExistedValueBeforePostfix = findCommonBeginningSubstr(initialValueBeforePostfix, value); >+ return { >+ selection, >+ value: Array.from(postfix) >+ .reverse() >+ .reduce((newValue, char, index) => { >+ const i = newValue.length - 1 - index; >+ const isInitiallyMirroredChar = alreadyExistedValueBeforePostfix[i] === char && >+ postfixWasModified; >+ return newValue[i] !== char || isInitiallyMirroredChar >+ ? newValue.slice(0, i + 1) + char + newValue.slice(i + 1) >+ : newValue; >+ }, value), >+ }; >+ } >+ : identity; >+} >+ >+function maskitoPrefixPostprocessorGenerator(prefix) { >+ return prefix >+ ? ({ value, selection }, initialElementState) => { >+ if (value.startsWith(prefix) || // already valid >+ (!value && !initialElementState.value.startsWith(prefix)) // cases when developer wants input to be empty >+ ) { >+ return { value, selection }; >+ } >+ const [from, to] = selection; >+ const prefixedValue = Array.from(prefix).reduce((modifiedValue, char, i) => modifiedValue[i] === char >+ ? modifiedValue >+ : modifiedValue.slice(0, i) + char + modifiedValue.slice(i), value); >+ const addedCharsCount = prefixedValue.length - value.length; >+ return { >+ selection: [from + addedCharsCount, to + addedCharsCount], >+ value: prefixedValue, >+ }; >+ } >+ : identity; >+} >+ >+function createValidDatePreprocessor({ dateModeTemplate, dateSegmentsSeparator, rangeSeparator = '', }) { >+ return ({ elementState, data }) => { >+ const { value, selection } = elementState; >+ if (data === dateSegmentsSeparator) { >+ return { >+ elementState, >+ data: selection[0] === value.length ? data : '', >+ }; >+ } >+ if (POSSIBLE_DATE_RANGE_SEPARATOR.includes(data)) { >+ return { elementState, data: rangeSeparator }; >+ } >+ const newCharacters = data.replace(new RegExp(`[^\\d${escapeRegExp(dateSegmentsSeparator)}${rangeSeparator}]`, 'g'), ''); >+ if (!newCharacters) { >+ return { elementState, data: '' }; >+ } >+ const [from, rawTo] = selection; >+ let to = rawTo + data.length; >+ const newPossibleValue = value.slice(0, from) + newCharacters + value.slice(to); >+ const dateStrings = parseDateRangeString(newPossibleValue, dateModeTemplate, rangeSeparator); >+ let validatedValue = ''; >+ const hasRangeSeparator = Boolean(rangeSeparator) && newPossibleValue.includes(rangeSeparator); >+ for (const dateString of dateStrings) { >+ const { validatedDateString, updatedSelection } = validateDateString({ >+ dateString, >+ dateModeTemplate, >+ offset: validatedValue >+ ? validatedValue.length + rangeSeparator.length >+ : 0, >+ selection: [from, to], >+ }); >+ if (dateString && !validatedDateString) { >+ return { elementState, data: '' }; // prevent changes >+ } >+ to = updatedSelection[1]; >+ validatedValue += >+ hasRangeSeparator && validatedValue >+ ? rangeSeparator + validatedDateString >+ : validatedDateString; >+ } >+ const newData = validatedValue.slice(from, to); >+ return { >+ elementState: { >+ selection, >+ value: validatedValue.slice(0, from) + >+ newData >+ .split(dateSegmentsSeparator) >+ .map(segment => '0'.repeat(segment.length)) >+ .join(dateSegmentsSeparator) + >+ validatedValue.slice(to), >+ }, >+ data: newData, >+ }; >+ }; >+} >+ >+function maskitoEventHandler(name, handler, eventListenerOptions) { >+ return (element, maskitoOptions) => { >+ const listener = () => handler(element, maskitoOptions); >+ element.addEventListener(name, listener, eventListenerOptions); >+ return () => element.removeEventListener(name, listener, eventListenerOptions); >+ }; >+} >+ >+function maskitoAddOnFocusPlugin(value) { >+ return maskitoEventHandler('focus', element => { >+ if (!element.value) { >+ maskitoUpdateElement(element, value); >+ } >+ }); >+} >+ >+function maskitoCaretGuard(guard) { >+ return element => { >+ const document = element.ownerDocument; >+ let isPointerDown = 0; >+ const onPointerDown = () => isPointerDown++; >+ const onPointerUp = () => { >+ isPointerDown = Math.max(--isPointerDown, 0); >+ }; >+ const listener = () => { >+ if (getFocused(document) !== element) { >+ return; >+ } >+ if (isPointerDown) { >+ return document.addEventListener('mouseup', listener, { >+ once: true, >+ passive: true, >+ }); >+ } >+ const start = element.selectionStart || 0; >+ const end = element.selectionEnd || 0; >+ const [fromLimit, toLimit] = guard(element.value, [start, end]); >+ if (fromLimit > start || toLimit < end) { >+ element.setSelectionRange(clamp(start, fromLimit, toLimit), clamp(end, fromLimit, toLimit)); >+ } >+ }; >+ document.addEventListener('selectionchange', listener, { passive: true }); >+ element.addEventListener('mousedown', onPointerDown, { passive: true }); >+ document.addEventListener('mouseup', onPointerUp, { passive: true }); >+ return () => { >+ document.removeEventListener('selectionchange', listener); >+ document.removeEventListener('mousedown', onPointerDown); >+ document.removeEventListener('mouseup', onPointerUp); >+ }; >+ }; >+} >+ >+function maskitoRejectEvent(element) { >+ const listener = () => { >+ const value = element.value; >+ element.addEventListener('beforeinput', event => { >+ if (event.defaultPrevented && value === element.value) { >+ element.dispatchEvent(new CustomEvent('maskitoReject', { bubbles: true })); >+ } >+ }, { once: true }); >+ }; >+ element.addEventListener('beforeinput', listener, true); >+ return () => element.removeEventListener('beforeinput', listener, true); >+} >+ >+function maskitoRemoveOnBlurPlugin(value) { >+ return maskitoEventHandler('blur', element => { >+ if (element.value === value) { >+ maskitoUpdateElement(element, ''); >+ } >+ }); >+} >+ >+function maskitoWithPlaceholder(placeholder, focusedOnly = false) { >+ const removePlaceholder = (value) => { >+ for (let i = value.length - 1; i >= 0; i--) { >+ if (value[i] !== placeholder[i]) { >+ return value.slice(0, i + 1); >+ } >+ } >+ return ''; >+ }; >+ const plugins = [maskitoCaretGuard(value => [0, removePlaceholder(value).length])]; >+ let focused = false; >+ if (focusedOnly) { >+ const focus = maskitoEventHandler('focus', element => { >+ focused = true; >+ maskitoUpdateElement(element, element.value + placeholder.slice(element.value.length)); >+ }, { capture: true }); >+ const blur = maskitoEventHandler('blur', element => { >+ focused = false; >+ maskitoUpdateElement(element, removePlaceholder(element.value)); >+ }, { capture: true }); >+ plugins.push(focus, blur); >+ } >+ return { >+ plugins, >+ removePlaceholder, >+ preprocessors: [ >+ ({ elementState, data }) => { >+ const { value, selection } = elementState; >+ return { >+ elementState: { >+ selection, >+ value: removePlaceholder(value), >+ }, >+ data, >+ }; >+ }, >+ ], >+ postprocessors: [ >+ ({ value, selection }, initialElementState) => >+ /** >+ * If `value` still equals to `initialElementState.value`, >+ * then it means that value is patched programmatically (from Maskito's plugin or externally). >+ * In this case, we don't want to mutate value and automatically add placeholder. >+ * ___ >+ * For example, developer wants to remove manually placeholder (+ do something else with value) on blur. >+ * Without this condition, placeholder will be unexpectedly added again. >+ */ >+ value !== initialElementState.value && (focused || !focusedOnly) >+ ? { >+ value: value + placeholder.slice(value.length), >+ selection, >+ } >+ : { value, selection }, >+ ], >+ }; >+} >+ >+function createZeroPlaceholdersPreprocessor() { >+ return ({ elementState }, actionType) => { >+ const { value, selection } = elementState; >+ if (!value || isLastChar(value, selection)) { >+ return { elementState }; >+ } >+ const [from, to] = selection; >+ const zeroes = value.slice(from, to).replace(/\d/g, '0'); >+ const newValue = value.slice(0, from) + zeroes + value.slice(to); >+ if (actionType === 'validation' || (actionType === 'insert' && from === to)) { >+ return { >+ elementState: { selection, value: newValue }, >+ }; >+ } >+ return { >+ elementState: { >+ selection: actionType === 'deleteBackward' || actionType === 'insert' >+ ? [from, from] >+ : [to, to], >+ value: newValue, >+ }, >+ }; >+ }; >+} >+function isLastChar(value, [_, to]) { >+ return to === value.length; >+} >+ >+function maskitoDateOptionsGenerator({ mode, separator = '.', max, min, }) { >+ const dateModeTemplate = mode.split('/').join(separator); >+ return Object.assign(Object.assign({}, MASKITO_DEFAULT_OPTIONS), { mask: Array.from(dateModeTemplate).map(char => char === separator ? char : /\d/), overwriteMode: 'replace', preprocessors: [ >+ createZeroPlaceholdersPreprocessor(), >+ normalizeDatePreprocessor({ >+ dateModeTemplate, >+ dateSegmentsSeparator: separator, >+ }), >+ createValidDatePreprocessor({ >+ dateModeTemplate, >+ dateSegmentsSeparator: separator, >+ }), >+ ], postprocessors: [ >+ createMinMaxDatePostprocessor({ >+ min, >+ max, >+ dateModeTemplate, >+ dateSegmentSeparator: separator, >+ }), >+ ] }); >+} >+ >+function createMinMaxRangeLengthPostprocessor({ dateModeTemplate, rangeSeparator, minLength, maxLength, max = DEFAULT_MAX_DATE, }) { >+ if (isEmpty(minLength) && isEmpty(maxLength)) { >+ return identity; >+ } >+ return ({ value, selection }) => { >+ const dateStrings = parseDateRangeString(value, dateModeTemplate, rangeSeparator); >+ if (dateStrings.length !== 2 || >+ dateStrings.some(date => !isDateStringComplete(date, dateModeTemplate))) { >+ return { value, selection }; >+ } >+ const [fromDate, toDate] = dateStrings.map(dateString => segmentsToDate(parseDateString(dateString, dateModeTemplate))); >+ const minDistantToDate = appendDate(fromDate, Object.assign(Object.assign({}, minLength), { >+ // 06.02.2023 - 07.02.2023 => {minLength: {day: 3}} => 06.02.2023 - 08.02.2023 >+ // "from"-day is included in the range >+ day: (minLength === null || minLength === void 0 ? void 0 : minLength.day) && minLength.day - 1 })); >+ const maxDistantToDate = !isEmpty(maxLength) >+ ? appendDate(fromDate, Object.assign(Object.assign({}, maxLength), { day: (maxLength === null || maxLength === void 0 ? void 0 : maxLength.day) && maxLength.day - 1 })) >+ : max; >+ const minLengthClampedToDate = clamp(toDate, minDistantToDate, max); >+ const minMaxLengthClampedToDate = minLengthClampedToDate > maxDistantToDate >+ ? maxDistantToDate >+ : minLengthClampedToDate; >+ return { >+ selection, >+ value: dateStrings[0] + >+ rangeSeparator + >+ toDateString(dateToSegments(minMaxLengthClampedToDate), dateModeTemplate), >+ }; >+ }; >+} >+ >+function createSwapDatesPostprocessor({ dateModeTemplate, rangeSeparator, }) { >+ return ({ value, selection }) => { >+ const dateStrings = parseDateRangeString(value, dateModeTemplate, rangeSeparator); >+ const isDateRangeComplete = dateStrings.length === 2 && >+ dateStrings.every(date => isDateStringComplete(date, dateModeTemplate)); >+ const [from, to] = selection; >+ const caretAtTheEnd = from >= value.length; >+ const allValueSelected = from === 0 && to >= value.length; // dropping text inside with a pointer >+ if (!(caretAtTheEnd || allValueSelected) || !isDateRangeComplete) { >+ return { value, selection }; >+ } >+ const [fromDate, toDate] = dateStrings.map(dateString => segmentsToDate(parseDateString(dateString, dateModeTemplate))); >+ return { >+ selection, >+ value: fromDate > toDate ? dateStrings.reverse().join(rangeSeparator) : value, >+ }; >+ }; >+} >+ >+function maskitoDateRangeOptionsGenerator({ mode, min, max, minLength, maxLength, dateSeparator = '.', rangeSeparator = `${CHAR_NO_BREAK_SPACE}${CHAR_EN_DASH}${CHAR_NO_BREAK_SPACE}`, }) { >+ const dateModeTemplate = mode.split('/').join(dateSeparator); >+ const dateMask = Array.from(dateModeTemplate).map(char => char === dateSeparator ? char : /\d/); >+ return Object.assign(Object.assign({}, MASKITO_DEFAULT_OPTIONS), { mask: [...dateMask, ...Array.from(rangeSeparator), ...dateMask], overwriteMode: 'replace', preprocessors: [ >+ createZeroPlaceholdersPreprocessor(), >+ normalizeDatePreprocessor({ >+ dateModeTemplate, >+ rangeSeparator, >+ dateSegmentsSeparator: dateSeparator, >+ }), >+ createValidDatePreprocessor({ >+ dateModeTemplate, >+ rangeSeparator, >+ dateSegmentsSeparator: dateSeparator, >+ }), >+ ], postprocessors: [ >+ createMinMaxDatePostprocessor({ >+ min, >+ max, >+ dateModeTemplate, >+ rangeSeparator, >+ dateSegmentSeparator: dateSeparator, >+ }), >+ createMinMaxRangeLengthPostprocessor({ >+ dateModeTemplate, >+ minLength, >+ maxLength, >+ max, >+ rangeSeparator, >+ }), >+ createSwapDatesPostprocessor({ >+ dateModeTemplate, >+ rangeSeparator, >+ }), >+ ] }); >+} >+ >+function padTimeSegments(timeSegments) { >+ return Object.fromEntries(Object.entries(timeSegments).map(([segmentName, segmentValue]) => [ >+ segmentName, >+ `${segmentValue}`.padEnd(TIME_SEGMENT_VALUE_LENGTHS[segmentName], '0'), >+ ])); >+} >+ >+/** >+ * @param timeString can be with/without fixed characters >+ */ >+function parseTimeString(timeString) { >+ const onlyDigits = timeString.replace(/\D+/g, ''); >+ const timeSegments = { >+ hours: onlyDigits.slice(0, 2), >+ minutes: onlyDigits.slice(2, 4), >+ seconds: onlyDigits.slice(4, 6), >+ milliseconds: onlyDigits.slice(6, 9), >+ }; >+ return Object.fromEntries(Object.entries(timeSegments).filter(([_, value]) => Boolean(value))); >+} >+ >+function toTimeString({ hours = '', minutes = '', seconds = '', milliseconds = '', }) { >+ const mm = minutes && `:${minutes}`; >+ const ss = seconds && `:${seconds}`; >+ const ms = milliseconds && `.${milliseconds}`; >+ return `${hours}${mm}${ss}${ms}`; >+} >+ >+const TRAILING_TIME_SEGMENT_SEPARATOR_REG = new RegExp(`[${TIME_FIXED_CHARACTERS.map(escapeRegExp).join('')}]$`); >+function validateTimeString({ timeString, paddedMaxValues, offset, selection: [from, to], }) { >+ const parsedTime = parseTimeString(timeString); >+ const possibleTimeSegments = Object.entries(parsedTime); >+ const validatedTimeSegments = {}; >+ let paddedZeroes = 0; >+ for (const [segmentName, segmentValue] of possibleTimeSegments) { >+ const validatedTime = toTimeString(validatedTimeSegments); >+ const maxSegmentValue = paddedMaxValues[segmentName]; >+ const fantomSeparator = validatedTime.length && 1; >+ const lastSegmentDigitIndex = offset + >+ validatedTime.length + >+ fantomSeparator + >+ TIME_SEGMENT_VALUE_LENGTHS[segmentName]; >+ const isLastSegmentDigitAdded = lastSegmentDigitIndex >= from && lastSegmentDigitIndex <= to; >+ if (isLastSegmentDigitAdded && Number(segmentValue) > Number(maxSegmentValue)) { >+ // 2|0:00 => Type 9 => 2|0:00 >+ return { validatedTimeString: '', updatedTimeSelection: [from, to] }; // prevent changes >+ } >+ const { validatedSegmentValue, prefixedZeroesCount } = padWithZeroesUntilValid(segmentValue, `${maxSegmentValue}`); >+ paddedZeroes += prefixedZeroesCount; >+ validatedTimeSegments[segmentName] = validatedSegmentValue; >+ } >+ const [trailingSegmentSeparator = ''] = timeString.match(TRAILING_TIME_SEGMENT_SEPARATOR_REG) || []; >+ const validatedTimeString = toTimeString(validatedTimeSegments) + trailingSegmentSeparator; >+ const addedDateSegmentSeparators = Math.max(validatedTimeString.length - timeString.length, 0); >+ return { >+ validatedTimeString, >+ updatedTimeSelection: [ >+ from + paddedZeroes + addedDateSegmentSeparators, >+ to + paddedZeroes + addedDateSegmentSeparators, >+ ], >+ }; >+} >+ >+function isDateTimeStringComplete(dateTimeString, dateMode, timeMode) { >+ return (dateTimeString.length >= >+ dateMode.length + timeMode.length + DATE_TIME_SEPARATOR.length && >+ dateTimeString >+ .split(DATE_TIME_SEPARATOR)[0] >+ .split(/\D/) >+ .every(segment => !segment.match(/^0+$/))); >+} >+ >+function parseDateTimeString(dateTime, dateModeTemplate) { >+ const hasSeparator = dateTime.includes(DATE_TIME_SEPARATOR); >+ return [ >+ dateTime.slice(0, dateModeTemplate.length), >+ dateTime.slice(hasSeparator >+ ? dateModeTemplate.length + DATE_TIME_SEPARATOR.length >+ : dateModeTemplate.length), >+ ]; >+} >+ >+function createMinMaxDateTimePostprocessor({ dateModeTemplate, timeMode, min = DEFAULT_MIN_DATE, max = DEFAULT_MAX_DATE, }) { >+ return ({ value, selection }) => { >+ const [dateString, timeString] = parseDateTimeString(value, dateModeTemplate); >+ const parsedDate = parseDateString(dateString, dateModeTemplate); >+ const parsedTime = parseTimeString(timeString); >+ if (!isDateTimeStringComplete(value, dateModeTemplate, timeMode)) { >+ const fixedDate = raiseSegmentValueToMin(parsedDate, dateModeTemplate); >+ const { year, month, day } = isDateStringComplete(dateString, dateModeTemplate) >+ ? dateToSegments(clamp(segmentsToDate(fixedDate), min, max)) >+ : fixedDate; >+ const fixedValue = toDateString(Object.assign({ year, >+ month, >+ day }, parsedTime), dateModeTemplate, timeMode); >+ const tail = value.slice(fixedValue.length); >+ return { >+ selection, >+ value: fixedValue + tail, >+ }; >+ } >+ const date = segmentsToDate(parsedDate, parsedTime); >+ const clampedDate = clamp(date, min, max); >+ const validatedValue = toDateString(dateToSegments(clampedDate), dateModeTemplate, timeMode); >+ return { >+ selection, >+ value: validatedValue, >+ }; >+ }; >+} >+ >+function createValidDateTimePreprocessor({ dateModeTemplate, dateSegmentsSeparator, }) { >+ const invalidCharsRegExp = new RegExp(`[^\\d${TIME_FIXED_CHARACTERS.map(escapeRegExp).join('')}${escapeRegExp(dateSegmentsSeparator)}]+`); >+ return ({ elementState, data }) => { >+ const { value, selection } = elementState; >+ if (data === dateSegmentsSeparator) { >+ return { >+ elementState, >+ data: selection[0] === value.length ? data : '', >+ }; >+ } >+ if (POSSIBLE_DATE_TIME_SEPARATOR.includes(data)) { >+ return { elementState, data: DATE_TIME_SEPARATOR }; >+ } >+ const newCharacters = data.replace(invalidCharsRegExp, ''); >+ if (!newCharacters) { >+ return { elementState, data: '' }; >+ } >+ const [from, rawTo] = selection; >+ let to = rawTo + data.length; >+ const newPossibleValue = value.slice(0, from) + newCharacters + value.slice(to); >+ const [dateString, timeString] = parseDateTimeString(newPossibleValue, dateModeTemplate); >+ let validatedValue = ''; >+ const hasDateTimeSeparator = newPossibleValue.includes(DATE_TIME_SEPARATOR); >+ const { validatedDateString, updatedSelection } = validateDateString({ >+ dateString, >+ dateModeTemplate, >+ offset: 0, >+ selection: [from, to], >+ }); >+ if (dateString && !validatedDateString) { >+ return { elementState, data: '' }; // prevent changes >+ } >+ to = updatedSelection[1]; >+ validatedValue += validatedDateString; >+ const paddedMaxValues = padTimeSegments(DEFAULT_TIME_SEGMENT_MAX_VALUES); >+ const { validatedTimeString, updatedTimeSelection } = validateTimeString({ >+ timeString, >+ paddedMaxValues, >+ offset: validatedValue.length + DATE_TIME_SEPARATOR.length, >+ selection: [from, to], >+ }); >+ if (timeString && !validatedTimeString) { >+ return { elementState, data: '' }; // prevent changes >+ } >+ to = updatedTimeSelection[1]; >+ validatedValue += hasDateTimeSeparator >+ ? DATE_TIME_SEPARATOR + validatedTimeString >+ : validatedTimeString; >+ const newData = validatedValue.slice(from, to); >+ return { >+ elementState: { >+ selection, >+ value: validatedValue.slice(0, from) + >+ newData >+ .split(dateSegmentsSeparator) >+ .map(segment => '0'.repeat(segment.length)) >+ .join(dateSegmentsSeparator) + >+ validatedValue.slice(to), >+ }, >+ data: newData, >+ }; >+ }; >+} >+ >+function maskitoDateTimeOptionsGenerator({ dateMode, timeMode, dateSeparator = '.', min, max, }) { >+ const dateModeTemplate = dateMode.split('/').join(dateSeparator); >+ return Object.assign(Object.assign({}, MASKITO_DEFAULT_OPTIONS), { mask: [ >+ ...Array.from(dateModeTemplate).map(char => char === dateSeparator ? char : /\d/), >+ ...DATE_TIME_SEPARATOR.split(''), >+ ...Array.from(timeMode).map(char => TIME_FIXED_CHARACTERS.includes(char) ? char : /\d/), >+ ], overwriteMode: 'replace', preprocessors: [ >+ createZeroPlaceholdersPreprocessor(), >+ normalizeDatePreprocessor({ >+ dateModeTemplate, >+ dateSegmentsSeparator: dateSeparator, >+ }), >+ createValidDateTimePreprocessor({ >+ dateModeTemplate, >+ dateSegmentsSeparator: dateSeparator, >+ }), >+ ], postprocessors: [ >+ createMinMaxDateTimePostprocessor({ >+ min, >+ max, >+ dateModeTemplate, >+ timeMode, >+ }), >+ ] }); >+} >+ >+/** >+ * It drops prefix and postfix from data >+ * Needed for case, when prefix or postfix contain decimalSeparator, to ignore it in resulting number >+ * @example User pastes '{prefix}123.45{postfix}' => 123.45 >+ */ >+function createAffixesFilterPreprocessor({ prefix, postfix, }) { >+ return ({ elementState, data }) => { >+ const { cleanValue: cleanData } = extractAffixes(data, { >+ prefix, >+ postfix, >+ }); >+ return { >+ elementState, >+ data: cleanData, >+ }; >+ }; >+} >+ >+function generateMaskExpression({ decimalSeparator, isNegativeAllowed, precision, thousandSeparator, prefix, postfix, decimalPseudoSeparators = [], pseudoMinuses = [], }) { >+ const computedPrefix = computeAllOptionalCharsRegExp(prefix); >+ const digit = '\\d'; >+ const optionalMinus = isNegativeAllowed >+ ? `[${CHAR_MINUS}${pseudoMinuses.map(x => `\\${x}`).join('')}]?` >+ : ''; >+ const integerPart = thousandSeparator >+ ? `[${digit}${escapeRegExp(thousandSeparator).replace(/\s/g, '\\s')}]*` >+ : `[${digit}]*`; >+ const decimalPart = precision > 0 >+ ? `([${escapeRegExp(decimalSeparator)}${decimalPseudoSeparators >+ .map(escapeRegExp) >+ .join('')}]${digit}{0,${Number.isFinite(precision) ? precision : ''}})?` >+ : ''; >+ const computedPostfix = computeAllOptionalCharsRegExp(postfix); >+ return new RegExp(`^${computedPrefix}${optionalMinus}${integerPart}${decimalPart}${computedPostfix}$`); >+} >+function computeAllOptionalCharsRegExp(str) { >+ return str >+ ? `${str >+ .split('') >+ .map(char => `${escapeRegExp(char)}?`) >+ .join('')}` >+ : ''; >+} >+ >+function maskitoParseNumber(maskedNumber, decimalSeparator = '.') { >+ const hasNegativeSign = !!maskedNumber.match(new RegExp(`^\\D*[${CHAR_MINUS}\\${CHAR_HYPHEN}${CHAR_EN_DASH}${CHAR_EM_DASH}]`)); >+ const escapedDecimalSeparator = escapeRegExp(decimalSeparator); >+ const unmaskedNumber = maskedNumber >+ // drop all decimal separators not followed by a digit >+ .replace(new RegExp(`${escapedDecimalSeparator}(?!\\d)`, 'g'), '') >+ // drop all non-digit characters except decimal separator >+ .replace(new RegExp(`[^\\d${escapedDecimalSeparator}]`, 'g'), '') >+ .replace(decimalSeparator, '.'); >+ return unmaskedNumber >+ ? Number((hasNegativeSign ? CHAR_HYPHEN : '') + unmaskedNumber) >+ : NaN; >+} >+ >+/** >+ * Convert number to string with replacing exponent part on decimals >+ * >+ * @param value the number >+ * @return string representation of a number >+ */ >+function stringifyNumberWithoutExp(value) { >+ const valueAsString = String(value); >+ const [numberPart, expPart] = valueAsString.split('e-'); >+ let valueWithoutExp = valueAsString; >+ if (expPart) { >+ const [, fractionalPart] = numberPart.split('.'); >+ const decimalDigits = Number(expPart) + ((fractionalPart === null || fractionalPart === void 0 ? void 0 : fractionalPart.length) || 0); >+ valueWithoutExp = value.toFixed(decimalDigits); >+ } >+ return valueWithoutExp; >+} >+ >+function validateDecimalPseudoSeparators({ decimalSeparator, thousandSeparator, decimalPseudoSeparators = DEFAULT_DECIMAL_PSEUDO_SEPARATORS, }) { >+ return decimalPseudoSeparators.filter(char => char !== thousandSeparator && char !== decimalSeparator); >+} >+ >+/** >+ * If `decimalZeroPadding` is `true`, it pads decimal part with zeroes >+ * (until number of digits after decimalSeparator is equal to the `precision`). >+ * @example 1,42 => (`precision` is equal to 4) => 1,4200. >+ */ >+function createDecimalZeroPaddingPostprocessor({ decimalSeparator, precision, decimalZeroPadding, prefix, postfix, }) { >+ if (precision <= 0 || !decimalZeroPadding) { >+ return identity; >+ } >+ return ({ value, selection }) => { >+ const { cleanValue, extractedPrefix, extractedPostfix } = extractAffixes(value, { >+ prefix, >+ postfix, >+ }); >+ if (Number.isNaN(maskitoParseNumber(cleanValue, decimalSeparator))) { >+ return { value, selection }; >+ } >+ const [integerPart, decimalPart = ''] = cleanValue.split(decimalSeparator); >+ return { >+ value: extractedPrefix + >+ integerPart + >+ decimalSeparator + >+ decimalPart.padEnd(precision, '0') + >+ extractedPostfix, >+ selection, >+ }; >+ }; >+} >+ >+/** >+ * Replace fullwidth numbers with half width number >+ * @param fullWidthNumber full width number >+ * @returns processed half width number >+ */ >+function toHalfWidthNumber(fullWidthNumber) { >+ return fullWidthNumber.replace(/[ï¼-ï¼]/g, s => String.fromCharCode(s.charCodeAt(0) - 0xfee0)); >+} >+ >+/** >+ * Convert full width numbers like ï¼, ï¼ to half width numbers 1, 2 >+ */ >+function createFullWidthToHalfWidthPreprocessor() { >+ return ({ elementState, data }) => { >+ const { value, selection } = elementState; >+ return { >+ elementState: { >+ selection, >+ value: toHalfWidthNumber(value), >+ }, >+ data: toHalfWidthNumber(data), >+ }; >+ }; >+} >+ >+/** >+ * This preprocessor works only once at initialization phase (when `new Maskito(...)` is executed). >+ * This preprocessor helps to avoid conflicts during transition from one mask to another (for the same input). >+ * For example, the developer changes postfix (or other mask's props) during run-time. >+ * ``` >+ * let maskitoOptions = maskitoNumberOptionsGenerator({postfix: ' year'}); >+ * // [3 seconds later] >+ * maskitoOptions = maskitoNumberOptionsGenerator({postfix: ' years'}); >+ * ``` >+ */ >+function createInitializationOnlyPreprocessor({ decimalSeparator, decimalPseudoSeparators, pseudoMinuses, prefix, postfix, }) { >+ let isInitializationPhase = true; >+ const cleanNumberMask = generateMaskExpression({ >+ decimalSeparator, >+ decimalPseudoSeparators, >+ pseudoMinuses, >+ prefix: '', >+ postfix: '', >+ thousandSeparator: '', >+ precision: Infinity, >+ isNegativeAllowed: true, >+ }); >+ return ({ elementState, data }) => { >+ if (!isInitializationPhase) { >+ return { elementState, data }; >+ } >+ isInitializationPhase = false; >+ const { cleanValue } = extractAffixes(elementState.value, { prefix, postfix }); >+ return { >+ elementState: maskitoTransform(Object.assign(Object.assign({}, elementState), { value: cleanValue }), { >+ mask: cleanNumberMask, >+ }), >+ data, >+ }; >+ }; >+} >+ >+/** >+ * It removes repeated leading zeroes for integer part. >+ * @example 0,|00005 => Backspace => |5 >+ * @example -0,|00005 => Backspace => -|5 >+ * @example User types "000000" => 0| >+ * @example 0| => User types "5" => 5| >+ */ >+function createLeadingZeroesValidationPostprocessor({ decimalSeparator, thousandSeparator, prefix, postfix, }) { >+ const trimLeadingZeroes = (value) => { >+ const escapedThousandSeparator = escapeRegExp(thousandSeparator); >+ return value >+ .replace( >+ // all leading zeroes followed by another zero >+ new RegExp(`^(\\D+)?[0${escapedThousandSeparator}]+(?=0)`), '$1') >+ .replace( >+ // zero followed by not-zero digit >+ new RegExp(`^(\\D+)?[0${escapedThousandSeparator}]+(?=[1-9])`), '$1'); >+ }; >+ const countTrimmedZeroesBefore = (value, index) => { >+ const valueBefore = value.slice(0, index); >+ const followedByZero = value.slice(index).startsWith('0'); >+ return (valueBefore.length - >+ trimLeadingZeroes(valueBefore).length + >+ (followedByZero ? 1 : 0)); >+ }; >+ return ({ value, selection }) => { >+ const [from, to] = selection; >+ const { cleanValue, extractedPrefix, extractedPostfix } = extractAffixes(value, { >+ prefix, >+ postfix, >+ }); >+ const hasDecimalSeparator = cleanValue.includes(decimalSeparator); >+ const [integerPart, decimalPart = ''] = cleanValue.split(decimalSeparator); >+ const zeroTrimmedIntegerPart = trimLeadingZeroes(integerPart); >+ if (integerPart === zeroTrimmedIntegerPart) { >+ return { value, selection }; >+ } >+ const newFrom = from - countTrimmedZeroesBefore(value, from); >+ const newTo = to - countTrimmedZeroesBefore(value, to); >+ return { >+ value: extractedPrefix + >+ zeroTrimmedIntegerPart + >+ (hasDecimalSeparator ? decimalSeparator : '') + >+ decimalPart + >+ extractedPostfix, >+ selection: [Math.max(newFrom, 0), Math.max(newTo, 0)], >+ }; >+ }; >+} >+ >+/** >+ * This postprocessor is connected with {@link createMinMaxPlugin}: >+ * both validate `min`/`max` bounds of entered value (but at the different point of time). >+ */ >+function createMinMaxPostprocessor({ min, max, decimalSeparator, }) { >+ return ({ value, selection }) => { >+ const parsedNumber = maskitoParseNumber(value, decimalSeparator); >+ const limitedValue = >+ /** >+ * We cannot limit lower bound if user enters positive number. >+ * The same for upper bound and negative number. >+ * ___ >+ * @example (min = 5) >+ * Empty input => Without this condition user cannot type 42 (the first digit will be rejected) >+ * ___ >+ * @example (max = -10) >+ * Value is -10 => Without this condition user cannot delete 0 to enter another digit >+ */ >+ parsedNumber > 0 ? Math.min(parsedNumber, max) : Math.max(parsedNumber, min); >+ if (!Number.isNaN(parsedNumber) && limitedValue !== parsedNumber) { >+ const newValue = `${limitedValue}` >+ .replace('.', decimalSeparator) >+ .replace(CHAR_HYPHEN, CHAR_MINUS); >+ return { >+ value: newValue, >+ selection: [newValue.length, newValue.length], >+ }; >+ } >+ return { >+ value, >+ selection, >+ }; >+ }; >+} >+ >+/** >+ * Manage caret-navigation when user "deletes" non-removable digits or separators >+ * @example 1,|42 => Backspace => 1|,42 (only if `decimalZeroPadding` is `true`) >+ * @example 1|,42 => Delete => 1,|42 (only if `decimalZeroPadding` is `true`) >+ * @example 0,|00 => Delete => 0,0|0 (only if `decimalZeroPadding` is `true`) >+ * @example 1 |000 => Backspace => 1| 000 (always) >+ */ >+function createNonRemovableCharsDeletionPreprocessor({ decimalSeparator, thousandSeparator, decimalZeroPadding, }) { >+ return ({ elementState, data }, actionType) => { >+ const { value, selection } = elementState; >+ const [from, to] = selection; >+ const selectedCharacters = value.slice(from, to); >+ const nonRemovableSeparators = decimalZeroPadding >+ ? [decimalSeparator, thousandSeparator] >+ : [thousandSeparator]; >+ const areNonRemovableZeroesSelected = decimalZeroPadding && >+ from > value.indexOf(decimalSeparator) && >+ Boolean(selectedCharacters.match(/^0+$/gi)); >+ if ((actionType !== 'deleteBackward' && actionType !== 'deleteForward') || >+ (!nonRemovableSeparators.includes(selectedCharacters) && >+ !areNonRemovableZeroesSelected)) { >+ return { >+ elementState, >+ data, >+ }; >+ } >+ return { >+ elementState: { >+ value, >+ selection: actionType === 'deleteForward' ? [to, to] : [from, from], >+ }, >+ data, >+ }; >+ }; >+} >+ >+/** >+ * It pads integer part with zero if user types decimal separator (for empty input). >+ * @example Empty input => User types "," (decimal separator) => 0,| >+ */ >+function createNotEmptyIntegerPartPreprocessor({ decimalSeparator, precision, prefix, postfix, }) { >+ const startWithDecimalSepRegExp = new RegExp(`^\\D*${escapeRegExp(decimalSeparator)}`); >+ return ({ elementState, data }) => { >+ const { value, selection } = elementState; >+ const { cleanValue } = extractAffixes(value, { >+ prefix, >+ postfix, >+ }); >+ const [from] = selection; >+ if (precision <= 0 || >+ cleanValue.includes(decimalSeparator) || >+ !data.match(startWithDecimalSepRegExp)) { >+ return { elementState, data }; >+ } >+ const digitsBeforeCursor = cleanValue.slice(0, from).match(/\d+/); >+ return { >+ elementState, >+ data: digitsBeforeCursor ? data : `0${data}`, >+ }; >+ }; >+} >+ >+/** >+ * It replaces pseudo characters with valid one. >+ * @example User types '.' (but separator is equal to comma) => dot is replaced with comma. >+ * @example User types hyphen / en-dash / em-dash => it is replaced with minus. >+ */ >+function createPseudoCharactersPreprocessor({ validCharacter, pseudoCharacters, prefix, postfix, }) { >+ const pseudoCharactersRegExp = new RegExp(`[${pseudoCharacters.join('')}]`, 'gi'); >+ return ({ elementState, data }) => { >+ const { value, selection } = elementState; >+ const { cleanValue, extractedPostfix, extractedPrefix } = extractAffixes(value, { >+ prefix, >+ postfix, >+ }); >+ return { >+ elementState: { >+ selection, >+ value: extractedPrefix + >+ cleanValue.replace(pseudoCharactersRegExp, validCharacter) + >+ extractedPostfix, >+ }, >+ data: data.replace(pseudoCharactersRegExp, validCharacter), >+ }; >+ }; >+} >+ >+/** >+ * It rejects new typed decimal separator if it already exists in text field. >+ * Behaviour is similar to native <input type="number"> (Chrome). >+ * @example 1|23,45 => Press comma (decimal separator) => 1|23,45 (do nothing). >+ */ >+function createRepeatedDecimalSeparatorPreprocessor({ decimalSeparator, prefix, postfix, }) { >+ return ({ elementState, data }) => { >+ const { value, selection } = elementState; >+ const [from, to] = selection; >+ const { cleanValue } = extractAffixes(value, { prefix, postfix }); >+ return { >+ elementState, >+ data: !cleanValue.includes(decimalSeparator) || >+ value.slice(from, to + 1).includes(decimalSeparator) >+ ? data >+ : data.replace(new RegExp(escapeRegExp(decimalSeparator), 'gi'), ''), >+ }; >+ }; >+} >+ >+/** >+ * It adds symbol for separating thousands. >+ * @example 1000000 => (thousandSeparator is equal to space) => 1 000 000. >+ */ >+function createThousandSeparatorPostprocessor({ thousandSeparator, decimalSeparator, prefix, postfix, }) { >+ if (!thousandSeparator) { >+ return identity; >+ } >+ const isAllSpaces = (...chars) => chars.every(x => /\s/.test(x)); >+ return ({ value, selection }) => { >+ const { cleanValue, extractedPostfix, extractedPrefix } = extractAffixes(value, { >+ prefix, >+ postfix, >+ }); >+ const [integerPart, decimalPart = ''] = cleanValue >+ .replace(CHAR_MINUS, '') >+ .split(decimalSeparator); >+ const [initialFrom, initialTo] = selection; >+ let [from, to] = selection; >+ const processedIntegerPart = Array.from(integerPart).reduceRight((formattedValuePart, char, i) => { >+ const isLeadingThousandSeparator = !i && char === thousandSeparator; >+ const isPositionForSeparator = !isLeadingThousandSeparator && >+ formattedValuePart.length && >+ (formattedValuePart.length + 1) % 4 === 0; >+ if (isPositionForSeparator && >+ (char === thousandSeparator || isAllSpaces(char, thousandSeparator))) { >+ return thousandSeparator + formattedValuePart; >+ } >+ if (char === thousandSeparator && !isPositionForSeparator) { >+ if (i && i <= initialFrom) { >+ from--; >+ } >+ if (i && i <= initialTo) { >+ to--; >+ } >+ return formattedValuePart; >+ } >+ if (!isPositionForSeparator) { >+ return char + formattedValuePart; >+ } >+ if (i <= initialFrom) { >+ from++; >+ } >+ if (i <= initialTo) { >+ to++; >+ } >+ return char + thousandSeparator + formattedValuePart; >+ }, ''); >+ return { >+ value: extractedPrefix + >+ (cleanValue.includes(CHAR_MINUS) ? CHAR_MINUS : '') + >+ processedIntegerPart + >+ (cleanValue.includes(decimalSeparator) ? decimalSeparator : '') + >+ decimalPart + >+ extractedPostfix, >+ selection: [from, to], >+ }; >+ }; >+} >+ >+/** >+ * It drops decimal part if precision is zero. >+ * @example User pastes '123.45' (but precision is zero) => 123 >+ */ >+function createZeroPrecisionPreprocessor({ precision, decimalSeparator, prefix, postfix, }) { >+ if (precision > 0) { >+ return identity; >+ } >+ const decimalPartRegExp = new RegExp(`${escapeRegExp(decimalSeparator)}.*$`, 'g'); >+ return ({ elementState, data }) => { >+ const { value, selection } = elementState; >+ const { cleanValue, extractedPrefix, extractedPostfix } = extractAffixes(value, { >+ prefix, >+ postfix, >+ }); >+ const [from, to] = selection; >+ const newValue = extractedPrefix + >+ cleanValue.replace(decimalPartRegExp, '') + >+ extractedPostfix; >+ return { >+ elementState: { >+ selection: [ >+ Math.min(from, newValue.length), >+ Math.min(to, newValue.length), >+ ], >+ value: newValue, >+ }, >+ data: data.replace(decimalPartRegExp, ''), >+ }; >+ }; >+} >+ >+const DUMMY_SELECTION = [0, 0]; >+/** >+ * It removes repeated leading zeroes for integer part on blur-event. >+ * @example 000000 => blur => 0 >+ * @example 00005 => blur => 5 >+ */ >+function createLeadingZeroesValidationPlugin({ decimalSeparator, thousandSeparator, prefix, postfix, }) { >+ const dropRepeatedLeadingZeroes = createLeadingZeroesValidationPostprocessor({ >+ decimalSeparator, >+ thousandSeparator, >+ prefix, >+ postfix, >+ }); >+ return maskitoEventHandler('blur', element => { >+ const newValue = dropRepeatedLeadingZeroes({ >+ value: element.value, >+ selection: DUMMY_SELECTION, >+ }, { value: '', selection: DUMMY_SELECTION }).value; >+ if (element.value !== newValue) { >+ maskitoUpdateElement(element, newValue); >+ } >+ }, { capture: true }); >+} >+ >+/** >+ * This plugin is connected with {@link createMinMaxPostprocessor}: >+ * both validate `min`/`max` bounds of entered value (but at the different point of time). >+ */ >+function createMinMaxPlugin({ min, max, decimalSeparator, }) { >+ return maskitoEventHandler('blur', (element, options) => { >+ const parsedNumber = maskitoParseNumber(element.value, decimalSeparator); >+ const clampedNumber = clamp(parsedNumber, min, max); >+ if (!Number.isNaN(parsedNumber) && parsedNumber !== clampedNumber) { >+ maskitoUpdateElement(element, maskitoTransform(stringifyNumberWithoutExp(clampedNumber), options)); >+ } >+ }, { capture: true }); >+} >+ >+/** >+ * It pads EMPTY integer part with zero if decimal parts exists. >+ * It works on blur event only! >+ * @example 1|,23 => Backspace => Blur => 0,23 >+ */ >+function createNotEmptyIntegerPlugin({ decimalSeparator, prefix, postfix, }) { >+ return maskitoEventHandler('blur', element => { >+ const { cleanValue, extractedPostfix, extractedPrefix } = extractAffixes(element.value, { prefix, postfix }); >+ const newValue = extractedPrefix + >+ cleanValue.replace(new RegExp(`^(\\D+)?${escapeRegExp(decimalSeparator)}`), `$10${decimalSeparator}`) + >+ extractedPostfix; >+ if (newValue !== element.value) { >+ maskitoUpdateElement(element, newValue); >+ } >+ }, { capture: true }); >+} >+ >+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 = '', } = {}) { >+ const pseudoMinuses = [ >+ CHAR_HYPHEN, >+ CHAR_EN_DASH, >+ CHAR_EM_DASH, >+ CHAR_JP_HYPHEN, >+ ].filter(char => char !== thousandSeparator && char !== decimalSeparator); >+ const validatedDecimalPseudoSeparators = validateDecimalPseudoSeparators({ >+ decimalSeparator, >+ thousandSeparator, >+ decimalPseudoSeparators, >+ }); >+ const prefix = unsafePrefix.endsWith(decimalSeparator) && precision > 0 >+ ? `${unsafePrefix}${CHAR_ZERO_WIDTH_SPACE}` >+ : unsafePrefix; >+ return Object.assign(Object.assign({}, MASKITO_DEFAULT_OPTIONS), { mask: generateMaskExpression({ >+ decimalSeparator, >+ precision, >+ thousandSeparator, >+ prefix, >+ postfix, >+ isNegativeAllowed: min < 0, >+ }), preprocessors: [ >+ createInitializationOnlyPreprocessor({ >+ decimalSeparator, >+ decimalPseudoSeparators: validatedDecimalPseudoSeparators, >+ pseudoMinuses, >+ prefix, >+ postfix, >+ }), >+ createAffixesFilterPreprocessor({ prefix, postfix }), >+ createFullWidthToHalfWidthPreprocessor(), >+ createPseudoCharactersPreprocessor({ >+ validCharacter: CHAR_MINUS, >+ pseudoCharacters: pseudoMinuses, >+ prefix, >+ postfix, >+ }), >+ createPseudoCharactersPreprocessor({ >+ validCharacter: decimalSeparator, >+ pseudoCharacters: validatedDecimalPseudoSeparators, >+ prefix, >+ postfix, >+ }), >+ createNotEmptyIntegerPartPreprocessor({ >+ decimalSeparator, >+ precision, >+ prefix, >+ postfix, >+ }), >+ createNonRemovableCharsDeletionPreprocessor({ >+ decimalSeparator, >+ decimalZeroPadding, >+ thousandSeparator, >+ }), >+ createZeroPrecisionPreprocessor({ >+ precision, >+ decimalSeparator, >+ prefix, >+ postfix, >+ }), >+ createRepeatedDecimalSeparatorPreprocessor({ >+ decimalSeparator, >+ prefix, >+ postfix, >+ }), >+ ], postprocessors: [ >+ createMinMaxPostprocessor({ decimalSeparator, min, max }), >+ maskitoPrefixPostprocessorGenerator(prefix), >+ maskitoPostfixPostprocessorGenerator(postfix), >+ createThousandSeparatorPostprocessor({ >+ decimalSeparator, >+ thousandSeparator, >+ prefix, >+ postfix, >+ }), >+ createDecimalZeroPaddingPostprocessor({ >+ decimalSeparator, >+ decimalZeroPadding, >+ precision, >+ prefix, >+ postfix, >+ }), >+ ], plugins: [ >+ createLeadingZeroesValidationPlugin({ >+ decimalSeparator, >+ thousandSeparator, >+ prefix, >+ postfix, >+ }), >+ createNotEmptyIntegerPlugin({ >+ decimalSeparator, >+ prefix, >+ postfix, >+ }), >+ createMinMaxPlugin({ min, max, decimalSeparator }), >+ ], overwriteMode: decimalZeroPadding >+ ? ({ value, selection: [from] }) => from <= value.indexOf(decimalSeparator) ? 'shift' : 'replace' >+ : 'shift' }); >+} >+ >+function createMaxValidationPreprocessor(timeSegmentMaxValues) { >+ const paddedMaxValues = padTimeSegments(timeSegmentMaxValues); >+ const invalidCharsRegExp = new RegExp(`[^\\d${TIME_FIXED_CHARACTERS.map(escapeRegExp).join('')}]+`); >+ return ({ elementState, data }, actionType) => { >+ if (actionType === 'deleteBackward' || actionType === 'deleteForward') { >+ return { elementState, data }; >+ } >+ const { value, selection } = elementState; >+ if (actionType === 'validation') { >+ const { validatedTimeString, updatedTimeSelection } = validateTimeString({ >+ timeString: value, >+ paddedMaxValues, >+ offset: 0, >+ selection, >+ }); >+ return { >+ elementState: { >+ value: validatedTimeString, >+ selection: updatedTimeSelection, >+ }, >+ data, >+ }; >+ } >+ const newCharacters = data.replace(invalidCharsRegExp, ''); >+ const [from, rawTo] = selection; >+ let to = rawTo + newCharacters.length; // to be conformed with `overwriteMode: replace` >+ const newPossibleValue = value.slice(0, from) + newCharacters + value.slice(to); >+ const { validatedTimeString, updatedTimeSelection } = validateTimeString({ >+ timeString: newPossibleValue, >+ paddedMaxValues, >+ offset: 0, >+ selection: [from, to], >+ }); >+ if (newPossibleValue && !validatedTimeString) { >+ return { elementState, data: '' }; // prevent changes >+ } >+ to = updatedTimeSelection[1]; >+ const newData = validatedTimeString.slice(from, to); >+ return { >+ elementState: { >+ selection, >+ value: validatedTimeString.slice(0, from) + >+ '0'.repeat(newData.length) + >+ validatedTimeString.slice(to), >+ }, >+ data: newData, >+ }; >+ }; >+} >+ >+function maskitoTimeOptionsGenerator({ mode, timeSegmentMaxValues = {}, }) { >+ const enrichedTimeSegmentMaxValues = Object.assign(Object.assign({}, DEFAULT_TIME_SEGMENT_MAX_VALUES), timeSegmentMaxValues); >+ return Object.assign(Object.assign({}, MASKITO_DEFAULT_OPTIONS), { mask: Array.from(mode).map(char => TIME_FIXED_CHARACTERS.includes(char) ? char : /\d/), preprocessors: [ >+ createZeroPlaceholdersPreprocessor(), >+ createMaxValidationPreprocessor(enrichedTimeSegmentMaxValues), >+ ], overwriteMode: 'replace' }); >+} >+ >+//export { maskitoAddOnFocusPlugin, maskitoCaretGuard, maskitoDateOptionsGenerator, maskitoDateRangeOptionsGenerator, maskitoDateTimeOptionsGenerator, maskitoEventHandler, maskitoNumberOptionsGenerator, maskitoParseNumber, maskitoPostfixPostprocessorGenerator, maskitoPrefixPostprocessorGenerator, maskitoRejectEvent, maskitoRemoveOnBlurPlugin, maskitoTimeOptionsGenerator, maskitoWithPlaceholder }; >-- >2.30.2
You cannot view the attachment while viewing its details because your browser does not support IFRAMEs.
View the attachment on a separate page
.
View Attachment As Diff
View Attachment As Raw
Actions:
View
|
Diff
|
Splinter Review
Attachments on
bug 33703
:
150894
|
150895
|
155719
|
155720
|
155861
|
155862
|
156411
|
156412
|
156413
|
157132
|
157135
|
157206
|
157746
|
161619
|
161663
|
162722
|
162723
|
162724
|
162725
|
162726
|
162727
|
162728
|
162729
|
162730
|
162731
|
163078
|
163079
|
163080
|
163081
|
163082
|
163083
|
163319
|
163320
|
163321
|
163322
|
163323
|
163324
|
163978
|
163979
|
163980
|
163981
|
163982
|
163983
|
165307
|
165308
|
165309
|
165310
|
165311
|
165312
|
165399