UNPKG

@maskito/kit

Version:

The optional framework-agnostic Maskito's package with ready-to-use masks

1,208 lines (1,142 loc) 92.7 kB
import { maskitoUpdateElement, MASKITO_DEFAULT_OPTIONS, maskitoTransform } from '@maskito/core'; /** * 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 countDigits(str) { return str.replaceAll(/\W/g, '').length; } 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, _d, _e, _f; return ({ day: (_b = (_a = dateString.match(/d/g)) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0, month: (_d = (_c = dateString.match(/m/g)) === null || _c === void 0 ? void 0 : _c.length) !== null && _d !== void 0 ? _d : 0, year: (_f = (_e = dateString.match(/y/g)) === null || _e === void 0 ? void 0 : _e.length) !== null && _f !== void 0 ? _f : 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 getFirstCompleteDate(dateString, dateModeTemplate) { const digitsInDate = countDigits(dateModeTemplate); const [completeDate = ''] = new RegExp(`(\\D*\\d){${digitsInDate}}`).exec(dateString) || []; return completeDate; } function isDateStringComplete(dateString, dateModeTemplate) { if (dateString.length < dateModeTemplate.length) { return false; } return dateString.split(/\D/).every((segment) => !/^0+$/.exec(segment)); } function parseDateRangeString(dateRange, dateModeTemplate, rangeSeparator) { const digitsInDate = countDigits(dateModeTemplate); return (dateRange .replace(rangeSeparator, '') .match(new RegExp(`(\\D*\\d[^\\d\\s]*){1,${digitsInDate}}`, 'g')) || []); } function parseDateString(dateString, fullMode) { const cleanMode = fullMode.replaceAll(/[^dmy]/g, ''); const onlyDigitsDate = dateString.replaceAll(/\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.slice(0, 1)) > fullMode.toLowerCase().indexOf(b.slice(0, 1)) ? 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, dateTimeSeparator = DATE_TIME_SEPARATOR, 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 ? dateTimeSeparator + timeMode : ''); return fullMode .replaceAll(/d+/g, day !== null && day !== void 0 ? day : '') .replaceAll(/m+/g, month !== null && month !== void 0 ? month : '') .replaceAll(/y+/g, safeYear !== null && safeYear !== void 0 ? safeYear : '') .replaceAll(/H+/g, hours !== null && hours !== void 0 ? hours : '') .replaceAll('MSS', milliseconds !== null && milliseconds !== void 0 ? milliseconds : '') .replaceAll(/M+/g, minutes !== null && minutes !== void 0 ? minutes : '') .replaceAll(/S+/g, seconds !== null && seconds !== void 0 ? seconds : '') .replaceAll(/^\D+/g, '') .replaceAll(/\D+$/g, ''); } const DATE_SEGMENTS_MAX_VALUES = { day: 31, month: 12, year: 9999, }; // eslint-disable-next-line i18n/no-russian-character const DEFAULT_DECIMAL_PSEUDO_SEPARATORS = ['.', ',', 'б', 'ю']; const DEFAULT_MIN_DATE = new Date('0001-01-01T00:00'); const DEFAULT_MAX_DATE = new Date('9999-12-31T23:59:59.999'); const DEFAULT_TIME_SEGMENT_MAX_VALUES = { hours: 23, minutes: 59, seconds: 59, milliseconds: 999, }; const DEFAULT_TIME_SEGMENT_MIN_VALUES = { hours: 0, minutes: 0, seconds: 0, milliseconds: 0, }; /** * {@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 `&minus;`. 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'; /** * {@link https://symbl.cc/en/003A/ Colon} * is a punctuation mark that connects parts of a text logically. * --- * is also used as separator in time. */ const CHAR_COLON = '\u003A'; /** * {@link https://symbl.cc/en/FF1A/ Full-width colon} * is a full-width punctuation mark used to separate parts of a text commonly in Japanese. */ const CHAR_JP_COLON = '\uFF1A'; const ANY_MERIDIEM_CHARACTER_RE = new RegExp(`[${CHAR_NO_BREAK_SPACE}APM]+$`, 'g'); const ALL_MERIDIEM_CHARACTERS_RE = new RegExp(`${CHAR_NO_BREAK_SPACE}[AP]M$`, 'g'); const TIME_FIXED_CHARACTERS = [':', '.']; const TIME_SEGMENT_VALUE_LENGTHS = { hours: 2, minutes: 2, seconds: 2, milliseconds: 3, }; function validateDateString({ dateString, dateModeTemplate, dateSegmentsSeparator, offset, selection: [from, to], }) { const parsedDate = parseDateString(dateString, dateModeTemplate); const dateSegments = Object.entries(parsedDate); const validatedDateSegments = {}; for (const [segmentName, segmentValue] of dateSegments) { const validatedDate = toDateString(validatedDateSegments, { dateMode: dateModeTemplate, }); const maxSegmentValue = DATE_SEGMENTS_MAX_VALUES[segmentName]; const fantomSeparator = validatedDate.length && dateSegmentsSeparator.length; 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 } validatedDateSegments[segmentName] = segmentValue; } const validatedDateString = toDateString(validatedDateSegments, { dateMode: dateModeTemplate, }); const addedDateSegmentSeparators = validatedDateString.length - dateString.length; return { validatedDateString, updatedSelection: [ from + addedDateSegmentSeparators, to + addedDateSegmentSeparators, ], }; } function identity(x) { return x; } // eslint-disable-next-line @typescript-eslint/no-empty-function function noop() { } /** * 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.replaceAll(reRegExpChar, String.raw `\$&`) : 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 : []; return { extractedPrefix, extractedPostfix, cleanValue: extractedPrefix || extractedPostfix ? value.slice(extractedPrefix.length, extractedPostfix.length ? -extractedPostfix.length : Infinity) : value, }; } 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; } function isEmpty(entity) { return !entity || (typeof entity === 'object' && Object.keys(entity).length === 0); } const ALL_ZEROES_RE = /^0+$/; function padWithZeroesUntilValid(segmentValue, paddedMaxValue, prefixedZeroesCount = 0) { const paddedSegmentValue = segmentValue.padEnd(paddedMaxValue.length, '0'); if (Number(paddedSegmentValue) <= Number(paddedMaxValue)) { return { validatedSegmentValue: segmentValue, prefixedZeroesCount }; } if (paddedSegmentValue.endsWith('0')) { // 00:|00 => Type 9 => 00:09| return padWithZeroesUntilValid(`0${segmentValue.slice(0, paddedMaxValue.length - 1)}`, paddedMaxValue, prefixedZeroesCount + 1); } const valueWithoutLastChar = segmentValue.slice(0, paddedMaxValue.length - 1); if (ALL_ZEROES_RE.exec(valueWithoutLastChar)) { return { validatedSegmentValue: '', prefixedZeroesCount }; } // |19:00 => Type 2 => 2|0:00 return padWithZeroesUntilValid(`${valueWithoutLastChar}0`, paddedMaxValue, prefixedZeroesCount); } /** * Replace fullwidth colon with half width colon * @param fullWidthColon full width colon * @returns processed half width colon */ function toHalfWidthColon(fullWidthColon) { return fullWidthColon.replaceAll(new RegExp(CHAR_JP_COLON, 'g'), CHAR_COLON); } /** * Replace fullwidth numbers with half width number * @param fullWidthNumber full width number * @returns processed half width number */ function toHalfWidthNumber(fullWidthNumber) { return fullWidthNumber.replaceAll(/[0-9]/g, (s) => String.fromCharCode(s.charCodeAt(0) - 0xfee0)); } /** * Convert full width colon (:) to half width one (:) */ function createColonConvertPreprocessor() { return ({ elementState, data }) => { const { value, selection } = elementState; return { elementState: { selection, value: toHalfWidthColon(value), }, data: toHalfWidthColon(data), }; }; } function createDateSegmentsZeroPaddingPostprocessor({ dateModeTemplate, dateSegmentSeparator, splitFn, uniteFn, }) { return ({ value, selection }) => { var _a; const [from, to] = selection; const { dateStrings, restPart = '' } = splitFn(value); const validatedDateStrings = []; let caretShift = 0; dateStrings.forEach((dateString) => { const parsedDate = parseDateString(dateString, dateModeTemplate); const dateSegments = Object.entries(parsedDate); const validatedDateSegments = dateSegments.reduce((acc, [segmentName, segmentValue]) => { const { validatedSegmentValue, prefixedZeroesCount } = padWithZeroesUntilValid(segmentValue, `${DATE_SEGMENTS_MAX_VALUES[segmentName]}`); caretShift += prefixedZeroesCount; return Object.assign(Object.assign({}, acc), { [segmentName]: validatedSegmentValue }); }, {}); validatedDateStrings.push(toDateString(validatedDateSegments, { dateMode: dateModeTemplate })); }); const validatedValue = uniteFn(validatedDateStrings, value) + (((_a = dateStrings[dateStrings.length - 1]) === null || _a === void 0 ? void 0 : _a.endsWith(dateSegmentSeparator)) ? dateSegmentSeparator : '') + restPart; if (caretShift && validatedValue.slice(to + caretShift, to + caretShift + dateSegmentSeparator.length) === dateSegmentSeparator) { /** * If `caretShift` > 0, it means that time segment was padded with zero. * It is only possible if any character insertion happens. * If caret is before `dateSegmentSeparator` => it should be moved after `dateSegmentSeparator`. */ caretShift += dateSegmentSeparator.length; } return { selection: [from + caretShift, to + caretShift], value: validatedValue, }; }; } /** * It replaces pseudo range separators with valid one. * @example '01.01.2000_11.11.2000' -> '01.01.2000 - 01.01.2000' * @example '01.01.2000_23:59' -> '01.01.2000, 23:59' */ function createFirstDateEndSeparatorPreprocessor({ dateModeTemplate, firstDateEndSeparator, dateSegmentSeparator, pseudoFirstDateEndSeparators, }) { return ({ elementState, data }) => { const { value, selection } = elementState; const [from, to] = selection; const firstCompleteDate = getFirstCompleteDate(value, dateModeTemplate); const pseudoSeparators = pseudoFirstDateEndSeparators.filter((x) => !firstDateEndSeparator.includes(x) && x !== dateSegmentSeparator); const pseudoSeparatorsRE = new RegExp(`[${pseudoSeparators.join('')}]`, 'gi'); const newValue = firstCompleteDate && value.length > firstCompleteDate.length ? firstCompleteDate + value .slice(firstCompleteDate.length) .replace(/^[\D\s]*/, firstDateEndSeparator) : value; const caretShift = newValue.length - value.length; return { elementState: { selection: [from + caretShift, to + caretShift], value: newValue, }, data: data.replace(pseudoSeparatorsRE, firstDateEndSeparator), }; }; } /** * Convert full width numbers like 1, 2 to half width numbers 1, 2 */ function createFullWidthToHalfWidthPreprocessor() { return ({ elementState, data }) => { const { value, selection } = elementState; return { elementState: { selection, value: toHalfWidthNumber(value), }, data: toHalfWidthNumber(data), }; }; } function createTimeMaskExpression(mode) { return Array.from(mode.replace(' AA', '')) .map((char) => (TIME_FIXED_CHARACTERS.includes(char) ? char : /\d/)) .concat(mode.includes('AA') ? [CHAR_NO_BREAK_SPACE, /[AP]/i, /M/i] : []); } function padTimeSegments(timeSegments, pad) { return Object.fromEntries(Object.entries(timeSegments).map(([segmentName, segmentValue]) => [ segmentName, pad(String(segmentValue), TIME_SEGMENT_VALUE_LENGTHS[segmentName]), ])); } function padStartTimeSegments(timeSegments) { return padTimeSegments(timeSegments, (value, length) => value.padStart(length, '0')); } const SEGMENT_FULL_NAME = { HH: 'hours', MM: 'minutes', SS: 'seconds', MSS: 'milliseconds', }; /** * @param timeString can be with/without fixed characters */ function parseTimeString(timeString, timeMode) { const onlyDigits = timeString.replaceAll(/\D+/g, ''); let offset = 0; return Object.fromEntries(timeMode .split(/\W/) .filter((segmentAbbr) => SEGMENT_FULL_NAME[segmentAbbr]) .map((segmentAbbr) => { const segmentValue = onlyDigits.slice(offset, offset + segmentAbbr.length); offset += segmentAbbr.length; return [SEGMENT_FULL_NAME[segmentAbbr], segmentValue]; })); } const LEADING_NON_DIGITS = /^\D*/; const TRAILING_NON_DIGITS = /\D*$/; function toTimeString({ hours = '', minutes = '', seconds = '', milliseconds = '', }) { return `${hours}:${minutes}:${seconds}.${milliseconds}` .replace(LEADING_NON_DIGITS, '') .replace(TRAILING_NON_DIGITS, ''); } const TRAILING_TIME_SEGMENT_SEPARATOR_REG = new RegExp(`[${TIME_FIXED_CHARACTERS.map(escapeRegExp).join('')}]$`); /** * Pads invalid time segment with zero to make it valid. * @example 00:|00 => Type 9 (too much for the first digit of minutes) => 00:09| * @example |19:00 => Type 2 (29 - invalid value for hours) => 2|0:00 */ function enrichTimeSegmentsWithZeroes({ value, selection }, { mode, timeSegmentMaxValues = DEFAULT_TIME_SEGMENT_MAX_VALUES, }) { const [from, to] = selection; const parsedTime = parseTimeString(value, mode); const possibleTimeSegments = Object.entries(parsedTime); const paddedMaxValues = padStartTimeSegments(timeSegmentMaxValues); const validatedTimeSegments = {}; let paddedZeroes = 0; for (const [segmentName, segmentValue] of possibleTimeSegments) { const maxSegmentValue = paddedMaxValues[segmentName]; const { validatedSegmentValue, prefixedZeroesCount } = padWithZeroesUntilValid(segmentValue, String(maxSegmentValue)); paddedZeroes += prefixedZeroesCount; validatedTimeSegments[segmentName] = validatedSegmentValue; } // trailing segment separators or meridiem characters const [trailingNonDigitCharacters = ''] = value.match(/\D+$/g) || []; const validatedTimeString = toTimeString(validatedTimeSegments) + trailingNonDigitCharacters; const addedDateSegmentSeparators = Math.max(validatedTimeString.length - value.length, 0); let newFrom = from + paddedZeroes + addedDateSegmentSeparators; let newTo = to + paddedZeroes + addedDateSegmentSeparators; if (newFrom === newTo && paddedZeroes && // if next character after cursor is time segment separator validatedTimeString.slice(0, newTo + 1).match(TRAILING_TIME_SEGMENT_SEPARATOR_REG)) { newFrom++; newTo++; } return { value: validatedTimeString, selection: [newFrom, newTo], }; } function padEndTimeSegments(timeSegments) { return padTimeSegments(timeSegments, (value, length) => value.padEnd(length, '0')); } /** * Prevent insertion if any time segment will become invalid * (and even zero padding won't help with it). * @example 2|0:00 => Type 9 => 2|0:00 */ function createInvalidTimeSegmentInsertionPreprocessor({ timeMode, timeSegmentMinValues = DEFAULT_TIME_SEGMENT_MIN_VALUES, timeSegmentMaxValues = DEFAULT_TIME_SEGMENT_MAX_VALUES, parseValue = (x) => ({ timeString: x }), }) { const invalidCharsRegExp = new RegExp(`[^\\d${TIME_FIXED_CHARACTERS.map(escapeRegExp).join('')}]+`); return ({ elementState, data }, actionType) => { if (actionType !== 'insert') { return { elementState, data }; } const { value, selection } = elementState; const [from, rawTo] = selection; const newCharacters = data.replace(invalidCharsRegExp, ''); const to = rawTo + newCharacters.length; // to be conformed with `overwriteMode: replace` const newPossibleValue = value.slice(0, from) + newCharacters + value.slice(to); const { timeString, restValue = '' } = parseValue(newPossibleValue); const timeSegments = Object.entries(parseTimeString(timeString, timeMode)); let offset = restValue.length; for (const [segmentName, stringifiedSegmentValue] of timeSegments) { const minSegmentValue = timeSegmentMinValues[segmentName]; const maxSegmentValue = timeSegmentMaxValues[segmentName]; const segmentValue = Number(stringifiedSegmentValue); const lastSegmentDigitIndex = offset + TIME_SEGMENT_VALUE_LENGTHS[segmentName]; if (lastSegmentDigitIndex >= from && lastSegmentDigitIndex <= to && segmentValue !== clamp(segmentValue, minSegmentValue, maxSegmentValue)) { return { elementState, data: '' }; // prevent insertion } offset += stringifiedSegmentValue.length + // any time segment separator 1; } return { elementState, data }; }; } function createMeridiemPreprocessor(timeMode) { if (!timeMode.includes('AA')) { return identity; } const mainMeridiemCharRE = /^[AP]$/gi; return ({ elementState, data }) => { const { value, selection } = elementState; const newValue = value.toUpperCase(); const newData = data.toUpperCase(); if (newValue.match(ALL_MERIDIEM_CHARACTERS_RE) && newData.match(mainMeridiemCharRE)) { return { elementState: { value: newValue.replaceAll(ALL_MERIDIEM_CHARACTERS_RE, ''), selection, }, data: `${newData}M`, }; } return { elementState: { selection, value: newValue }, data: newData }; }; } function createMeridiemPostprocessor(timeMode) { if (!timeMode.includes('AA')) { return identity; } return ({ value, selection }, initialElementState) => { if (!value.match(ANY_MERIDIEM_CHARACTER_RE) || value.match(ALL_MERIDIEM_CHARACTERS_RE)) { return { value, selection }; } const [from, to] = selection; // any meridiem character was deleted if (initialElementState.value.match(ALL_MERIDIEM_CHARACTERS_RE)) { const newValue = value.replace(ANY_MERIDIEM_CHARACTER_RE, ''); return { value: newValue, selection: [ Math.min(from, newValue.length), Math.min(to, newValue.length), ], }; } const fullMeridiem = `${CHAR_NO_BREAK_SPACE}${value.includes('P') ? 'P' : 'A'}M`; const newValue = value.replace(ANY_MERIDIEM_CHARACTER_RE, (x) => x !== CHAR_NO_BREAK_SPACE ? fullMeridiem : x); return { value: newValue, selection: to >= newValue.indexOf(fullMeridiem) ? [newValue.length, newValue.length] : selection, }; }; } 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 && /^0+$/.exec(value) ? '1'.padStart(segmentLength, '0') : value, ]; })); } const LEAP_YEAR = '1972'; 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, { dateMode: dateModeTemplate }); const tail = dateString.endsWith(dateSegmentSeparator) ? dateSegmentSeparator : ''; validatedValue += fixedValue + tail; continue; } const date = segmentsToDate(Object.assign({ year: LEAP_YEAR }, parsedDate)); const clampedDate = clamp(date, min, max); validatedValue += toDateString(dateToSegments(clampedDate), { dateMode: dateModeTemplate, }); } return { selection, value: validatedValue + (endsWithRangeSeparator ? rangeSeparator : ''), }; }; } function normalizeDatePreprocessor({ dateModeTemplate, dateSegmentsSeparator, rangeSeparator = '', dateTimeSeparator = DATE_TIME_SEPARATOR, }) { return ({ elementState, data }) => { const templateSegments = dateModeTemplate.split(dateSegmentsSeparator); const includesTime = data.includes(dateTimeSeparator); const dateSegments = data .slice(0, includesTime ? data.indexOf(dateTimeSeparator) : Infinity) .split(/\D/) .filter(Boolean); if (!dateSegments.length || dateSegments.length % templateSegments.length !== 0) { return { elementState, data }; } const dates = dateSegments.reduce((dates, segment, index) => { var _a; const template = (_a = templateSegments[index % templateSegments.length]) !== null && _a !== void 0 ? _a : ''; const dateIndex = Math.trunc(index / templateSegments.length); const isLastDateSegment = index % templateSegments.length === templateSegments.length - 1; if (!dates[dateIndex]) { dates[dateIndex] = ''; } dates[dateIndex] += isLastDateSegment ? segment : `${segment.padStart(template.length, '0')}${dateSegmentsSeparator}`; return dates; }, []); return { elementState, data: includesTime ? `${dates[0]}${data.slice(data.indexOf(dateTimeSeparator))}` : dates.join(rangeSeparator), }; }; } 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 : '', }; } const newCharacters = data.replaceAll(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, dateSegmentsSeparator, offset: validatedValue.length, selection: [from, to], }); if (dateString && !validatedDateString) { return { elementState, data: '' }; // prevent changes } to = updatedSelection[1]; validatedValue += hasRangeSeparator && !validatedValue ? validatedDateString + rangeSeparator : 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 maskitoSelectionChangeHandler(handler) { return (element, options) => { const document = element.ownerDocument; let isPointerDown = 0; const onPointerDown = () => isPointerDown++; const onPointerUp = () => { isPointerDown = Math.max(--isPointerDown, 0); }; const listener = () => { if (!element.matches(':focus')) { return; } if (isPointerDown) { return document.addEventListener('mouseup', listener, { once: true, passive: true, }); } handler(element, options); }; document.addEventListener('selectionchange', listener, { passive: true }); // Safari does not fire `selectionchange` on focus after programmatic update of textfield value element.addEventListener('focus', listener, { passive: true }); element.addEventListener('mousedown', onPointerDown, { passive: true }); document.addEventListener('mouseup', onPointerUp, { passive: true }); return () => { document.removeEventListener('selectionchange', listener); element.removeEventListener('focus', listener); element.removeEventListener('mousedown', onPointerDown); document.removeEventListener('mouseup', onPointerUp); }; }; } function maskitoCaretGuard(guard) { return maskitoSelectionChangeHandler((element) => { var _a, _b; const start = (_a = element.selectionStart) !== null && _a !== void 0 ? _a : 0; const end = (_b = element.selectionEnd) !== null && _b !== void 0 ? _b : 0; const [fromLimit, toLimit] = guard(element.value, [start, end]); if (fromLimit > start || toLimit < end) { element.setSelectionRange(clamp(start, fromLimit, toLimit), clamp(end, fromLimit, toLimit)); } }); } const 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 createMeridiemSteppingPlugin(meridiemStartIndex) { if (meridiemStartIndex < 0) { return noop; } return (element) => { const listener = (event) => { const caretIndex = Number(element.selectionStart); const value = element.value.toUpperCase(); if ((event.key !== 'ArrowUp' && event.key !== 'ArrowDown') || caretIndex < meridiemStartIndex) { return; } event.preventDefault(); // eslint-disable-next-line no-nested-ternary const meridiemMainCharacter = value.includes('A') ? 'P' : value.includes('P') || event.key === 'ArrowUp' ? 'A' : 'P'; const newMeridiem = `${CHAR_NO_BREAK_SPACE}${meridiemMainCharacter}M`; maskitoUpdateElement(element, { value: value.length === meridiemStartIndex ? value + newMeridiem : value.replace(ANY_MERIDIEM_CHARACTER_RE, newMeridiem), selection: [caretIndex, caretIndex], }); }; element.addEventListener('keydown', listener); return () => element.removeEventListener('keydown', listener); }; } function createTimeSegmentsSteppingPlugin({ step, fullMode, timeSegmentMaxValues, }) { const segmentsIndexes = createTimeSegmentsIndexes(fullMode); return step <= 0 ? noop : (element) => { const listener = (event) => { var _a; if (event.key !== 'ArrowUp' && event.key !== 'ArrowDown') { return; } event.preventDefault(); const selectionStart = (_a = element.selectionStart) !== null && _a !== void 0 ? _a : 0; const activeSegment = getActiveSegment({ segmentsIndexes, selectionStart, }); if (!activeSegment) { return; } const updatedValue = updateSegmentValue({ selection: segmentsIndexes.get(activeSegment), value: element.value, toAdd: event.key === 'ArrowUp' ? step : -step, max: timeSegmentMaxValues[activeSegment], }); maskitoUpdateElement(element, { value: updatedValue, selection: [selectionStart, selectionStart], }); }; element.addEventListener('keydown', listener); return () => element.removeEventListener('keydown', listener); }; } function createTimeSegmentsIndexes(fullMode) { return new Map([ ['hours', getSegmentRange(fullMode, 'HH')], ['milliseconds', getSegmentRange(fullMode, 'MSS')], ['minutes', getSegmentRange(fullMode, 'MM')], ['seconds', getSegmentRange(fullMode, 'SS')], ]); } function getSegmentRange(mode, segment) { const index = mode.indexOf(segment); return index === -1 ? [-1, -1] : [index, index + segment.length]; } function getActiveSegment({ segmentsIndexes, selectionStart, }) { for (const [segmentName, segmentRange] of segmentsIndexes.entries()) { const [from, to] = segmentRange; if (from <= selectionStart && selectionStart <= to) { return segmentName; } } return null; } function updateSegmentValue({ selection, value, toAdd, max, }) { const [from, to] = selection; const segmentValue = Number(value.slice(from, to).padEnd(to - from, '0')); const newSegmentValue = mod(segmentValue + toAdd, max + 1); return (value.slice(0, from) + String(newSegmentValue).padStart(to - from, '0') + value.slice(to, value.length)); } function mod(value, max) { if (value < 0) { value += Math.floor(Math.abs(value) / max + 1) * max; } return value % max; } function maskitoWithPlaceholder(placeholder, focusedOnly = false) { let lastClearValue = ''; let action = 'validation'; const removePlaceholder = (value) => { for (let i = value.length - 1; i >= lastClearValue.length; i--) { if (value[i] !== placeholder[i]) { return value.slice(0, i + 1); } } return value.slice(0, lastClearValue.length); }; 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 }, actionType) => { action = actionType; const { value, selection } = elementState; return { elementState: { selection, value: removePlaceholder(value), }, data, }; }, ], postprocessors: [ ({ value, selection }, initialElementState) => { lastClearValue = value; const justPlaceholderRemoval = value + placeholder.slice(value.length, initialElementState.value.length) === initialElementState.value; if (action === 'validation' && justPlaceholderRemoval) { /** * 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/remove 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. */ return { selection, value: initialElementState.value }; } const newValue = focused || !focusedOnly ? value + placeholder.slice(value.length) : value; if (newValue === initialElementState.value && action === 'deleteBackward') { const [caretIndex] = initialElementState.selection; return { value: newValue, selection: [caretIndex, caretIndex], }; } return { value: newValue, 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).replaceAll(/\d/g, '0'); const newValue = value.slice(0, from) + zeroes + value.slice(to); if (!zeroes.replaceAll(/\D/g, '')) { return { elementState }; } 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) => separator.includes(char) ? char : /\d/), overwriteMode: 'replace', preprocessors: [ createFullWidthToHalfWidthPreprocessor(), createZeroPlaceholdersPreprocessor(), normalizeDatePreprocessor({ dateModeTemplate, dateSegmentsSeparator: separator, }), createValidDatePreprocessor({ dateModeTemplate, dateSegmentsSeparator: separator, }), ], postprocessors: [ createDateSegmentsZeroPaddingPostprocessor({ dateModeTemplate, dateSegmentSeparator: separator, splitFn: (value) => ({ dateStrings: [value] }), uniteFn: ([dateString = '']) => dateString, }), createMinMaxDatePostprocessor({ min, max, dateModeTemplate, dateSegmentSeparator: separator, }), ] }); } function maskitoParseDate(value, { mode, min = DEFAULT_MIN_DATE, max = DEFAULT_MAX_DATE }) { if (value.length < mode.length) { return null; } const dateSegments = parseDateString(value, mode); const parsedDate = segmentsToDate(dateSegments); return clamp(parsedDate, min, max); } const formatter = Intl.DateTimeFormat('en-US', { month: '2-digit', day: '2-digit', year: 'numeric', }); function toDateSegments(date) { return formatter .formatToParts(date) .reduce((acc, part) => (Object.assign(Object.assign({}, acc), { [part.type]: part.value })), {}); } function maskitoStringifyDate(date, { mode, separator = '.', min = DEFAULT_MIN_DATE, max = DEFAULT_MAX_DATE, }) { const validatedDate = clamp(date, min, max); const segments = toDateSegments(validatedDate); return toDateString(segments, { dateMode: mode.replaceAll('/', separator), }); } const POSSIBLE_DATE_RANGE_SEPARATOR = [ CHAR_HYPHEN, CHAR_EN_DASH, CHAR_EM_DASH, CHAR_MINUS, CHAR_JP_HYPHEN, ]; 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))); if (!fromDate || !toDate) { return { value, selection }; } 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), { dateMode: 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 && 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) => dateSeparator.includes(char) ? char : /\d/); r