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 }; |
1609 |
|