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