@maskito/kit
Version:
The optional framework-agnostic Maskito's package with ready-to-use masks
1,208 lines (1,142 loc) • 92.7 kB
JavaScript
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 `−`. 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