ngx-mask 22.0.0 → 22.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +25 -28
- package/fesm2022/ngx-mask.mjs +818 -652
- package/package.json +13 -7
- package/types/ngx-mask.d.ts +41 -22
- package/fesm2022/ngx-mask.mjs.map +0 -1
package/fesm2022/ngx-mask.mjs
CHANGED
|
@@ -180,6 +180,721 @@ const withoutValidation = [
|
|
|
180
180
|
MaskExpression.MONTHS,
|
|
181
181
|
];
|
|
182
182
|
|
|
183
|
+
/** CPF_CNPJ / CPF_CNPJ_ALPHA pre-processor (moved verbatim from applyMask). Sets
|
|
184
|
+
* `this.cpfCnpjError`, rewrites the mask, then falls through to the generic loop.
|
|
185
|
+
* Discriminated by exact equality BEFORE any startsWith handler — the 'H' of HOURS
|
|
186
|
+
* would otherwise catch CPF_CNPJ_ALPHA by substring (CODEBASE_NOTES #3/#4). */
|
|
187
|
+
const cpfCnpjHandler = function (state, params) {
|
|
188
|
+
const isCpfCnpjAlpha = state.maskExpression === MaskExpression.CPF_CNPJ_ALPHA;
|
|
189
|
+
this.cpfCnpjError = params.arr.length !== 11 && params.arr.length !== 14;
|
|
190
|
+
const valueHasAnyLetter = /[a-zA-Z]/.test(state.processedValue);
|
|
191
|
+
if (valueHasAnyLetter && isCpfCnpjAlpha) {
|
|
192
|
+
state.maskExpression = 'AA.AAA.AAA/AAAA-00';
|
|
193
|
+
}
|
|
194
|
+
else if (params.arr.length > 11) {
|
|
195
|
+
state.maskExpression = isCpfCnpjAlpha ? 'AA.AAA.AAA/AAAA-00' : '00.000.000/0000-00';
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
state.maskExpression = '000.000.000-00';
|
|
199
|
+
}
|
|
200
|
+
return state;
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Generic character-by-character pattern loop (moved verbatim from applyMask,
|
|
205
|
+
* original lines 581-968). This is the default/fallback handler — always runs when
|
|
206
|
+
* no exact/startsWith handler matched. IP and CPF_CNPJ pre-processors rewrite
|
|
207
|
+
* `state.maskExpression` and then fall through into this loop.
|
|
208
|
+
*
|
|
209
|
+
* The inline HOURS/HOUR/MINUTE/SECOND/DAY/MONTH sub-branches (including the day-window
|
|
210
|
+
* anchor flow-gating of #1611/#1513, CODEBASE_NOTES.md #20) are preserved unmodified —
|
|
211
|
+
* they are explicitly out of scope for the phase-1 extraction boundary.
|
|
212
|
+
*
|
|
213
|
+
* Note: several branches read `this.maskExpression` (the instance field) rather than
|
|
214
|
+
* the local `maskExpression` — this distinction is preserved exactly as in the
|
|
215
|
+
* original source.
|
|
216
|
+
*/
|
|
217
|
+
const genericPatternHandler = function (state, params) {
|
|
218
|
+
const { inputArray } = params;
|
|
219
|
+
const maskExpression = state.maskExpression;
|
|
220
|
+
const processedValue = state.processedValue;
|
|
221
|
+
let { cursor, result, multi, processedPosition, stepBack } = state;
|
|
222
|
+
for (
|
|
223
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
224
|
+
let i = 0, inputSymbol = inputArray[0]; i < inputArray.length; i++, inputSymbol = inputArray[i] ?? MaskExpression.EMPTY_STRING) {
|
|
225
|
+
if (cursor === maskExpression.length) {
|
|
226
|
+
break;
|
|
227
|
+
}
|
|
228
|
+
const symbolStarInPattern = MaskExpression.SYMBOL_STAR in this.patterns;
|
|
229
|
+
if (this._checkSymbolMask(inputSymbol, maskExpression[cursor] ?? MaskExpression.EMPTY_STRING) &&
|
|
230
|
+
maskExpression[cursor + 1] === MaskExpression.SYMBOL_QUESTION) {
|
|
231
|
+
result += inputSymbol;
|
|
232
|
+
cursor += 2;
|
|
233
|
+
}
|
|
234
|
+
else if (maskExpression[cursor + 1] === MaskExpression.SYMBOL_STAR &&
|
|
235
|
+
multi &&
|
|
236
|
+
this._checkSymbolMask(inputSymbol, maskExpression[cursor + 2] ?? MaskExpression.EMPTY_STRING)) {
|
|
237
|
+
result += inputSymbol;
|
|
238
|
+
cursor += 3;
|
|
239
|
+
multi = false;
|
|
240
|
+
}
|
|
241
|
+
else if (this._checkSymbolMask(inputSymbol, maskExpression[cursor] ?? MaskExpression.EMPTY_STRING) &&
|
|
242
|
+
maskExpression[cursor + 1] === MaskExpression.SYMBOL_STAR &&
|
|
243
|
+
!symbolStarInPattern) {
|
|
244
|
+
result += inputSymbol;
|
|
245
|
+
multi = true;
|
|
246
|
+
}
|
|
247
|
+
else if (maskExpression[cursor + 1] === MaskExpression.SYMBOL_QUESTION &&
|
|
248
|
+
this._checkSymbolMask(inputSymbol, maskExpression[cursor + 2] ?? MaskExpression.EMPTY_STRING)) {
|
|
249
|
+
result += inputSymbol;
|
|
250
|
+
cursor += 3;
|
|
251
|
+
}
|
|
252
|
+
else if (this._checkSymbolMask(inputSymbol, maskExpression[cursor] ?? MaskExpression.EMPTY_STRING)) {
|
|
253
|
+
if (maskExpression[cursor] === MaskExpression.HOURS) {
|
|
254
|
+
if (this.apm ? Number(inputSymbol) > 9 : Number(inputSymbol) > 2) {
|
|
255
|
+
processedPosition = !this.leadZeroDateTime
|
|
256
|
+
? processedPosition + 1
|
|
257
|
+
: processedPosition;
|
|
258
|
+
cursor += 1;
|
|
259
|
+
this._shiftStep(cursor);
|
|
260
|
+
i--;
|
|
261
|
+
if (this.leadZeroDateTime) {
|
|
262
|
+
result += '0';
|
|
263
|
+
}
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
if (maskExpression[cursor] === MaskExpression.HOUR) {
|
|
268
|
+
if (this.apm
|
|
269
|
+
? (result.length === 1 && Number(result) > 1) ||
|
|
270
|
+
(result === '1' && Number(inputSymbol) > 2) ||
|
|
271
|
+
(processedValue.slice(cursor - 1, cursor).length === 1 &&
|
|
272
|
+
Number(processedValue.slice(cursor - 1, cursor)) > 2) ||
|
|
273
|
+
(processedValue.slice(cursor - 1, cursor) === '1' &&
|
|
274
|
+
Number(inputSymbol) > 2)
|
|
275
|
+
: (result === '2' && Number(inputSymbol) > 3) ||
|
|
276
|
+
((result.slice(cursor - 2, cursor) === '2' ||
|
|
277
|
+
result.slice(cursor - 3, cursor) === '2' ||
|
|
278
|
+
result.slice(cursor - 4, cursor) === '2' ||
|
|
279
|
+
result.slice(cursor - 1, cursor) === '2') &&
|
|
280
|
+
Number(inputSymbol) > 3 &&
|
|
281
|
+
cursor > 10)) {
|
|
282
|
+
processedPosition = processedPosition + 1;
|
|
283
|
+
cursor += 1;
|
|
284
|
+
i--;
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
if (maskExpression[cursor] === MaskExpression.MINUTE ||
|
|
289
|
+
maskExpression[cursor] === MaskExpression.SECOND) {
|
|
290
|
+
if (Number(inputSymbol) > 5) {
|
|
291
|
+
processedPosition = !this.leadZeroDateTime
|
|
292
|
+
? processedPosition + 1
|
|
293
|
+
: processedPosition;
|
|
294
|
+
cursor += 1;
|
|
295
|
+
this._shiftStep(cursor);
|
|
296
|
+
i--;
|
|
297
|
+
if (this.leadZeroDateTime) {
|
|
298
|
+
result += '0';
|
|
299
|
+
}
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
const daysCount = 31;
|
|
304
|
+
const inputValueCursor = processedValue[cursor];
|
|
305
|
+
const inputValueCursorPlusOne = processedValue[cursor + 1];
|
|
306
|
+
const inputValueCursorPlusTwo = processedValue[cursor + 2];
|
|
307
|
+
const inputValueCursorMinusOne = processedValue[cursor - 1];
|
|
308
|
+
const inputValueCursorMinusTwo = processedValue[cursor - 2];
|
|
309
|
+
const inputValueSliceMinusThreeMinusOne = processedValue.slice(cursor - 3, cursor - 1);
|
|
310
|
+
const inputValueSliceMinusOnePlusOne = processedValue.slice(cursor - 1, cursor + 1);
|
|
311
|
+
const inputValueSliceCursorPlusTwo = processedValue.slice(cursor, cursor + 2);
|
|
312
|
+
const inputValueSliceMinusTwoCursor = processedValue.slice(cursor - 2, cursor);
|
|
313
|
+
// Issue #1523: when the date token directly abuts a plain digit
|
|
314
|
+
// token (year-first masks without separators like 00M0d0 or
|
|
315
|
+
// 0000M0d0), the backward-looking windows below read the previous
|
|
316
|
+
// field's digits (year) as day/month digits and skip valid input.
|
|
317
|
+
// Disable only those backward heuristics in that layout.
|
|
318
|
+
const tokenAbutsDigitField = maskExpression[cursor - 1] === MaskExpression.NUMBER_ZERO;
|
|
319
|
+
if (maskExpression[cursor] === MaskExpression.DAY) {
|
|
320
|
+
const maskStartWithMonth = maskExpression.slice(0, 2) === MaskExpression.MONTHS;
|
|
321
|
+
const startWithMonthInput = maskExpression.slice(0, 2) === MaskExpression.MONTHS &&
|
|
322
|
+
this.specialCharacters.includes(inputValueCursorMinusTwo);
|
|
323
|
+
// Issue #1611: `cursor` indexes the mask, `i` indexes the input.
|
|
324
|
+
// On paste/writeValue of a bare digit string the cursor runs
|
|
325
|
+
// ahead of the input by every emitted separator, so cursor-based
|
|
326
|
+
// slices read year digits instead of the day — anchor the day
|
|
327
|
+
// window on the input index in those flows. Keystroke flows keep
|
|
328
|
+
// the historical cursor anchor (with showMaskTyped the value also
|
|
329
|
+
// carries placeholder chars, where the input anchor misreads).
|
|
330
|
+
const dayWindowStart = params.justPasted || this.writingValue ? i : cursor;
|
|
331
|
+
const dayWindowSlice = processedValue.slice(dayWindowStart, dayWindowStart + 2);
|
|
332
|
+
const dayWindowNext = processedValue[dayWindowStart + 1];
|
|
333
|
+
if ((Number(inputSymbol) > 3 && this.leadZeroDateTime) ||
|
|
334
|
+
(!maskStartWithMonth &&
|
|
335
|
+
(Number(inputValueSliceCursorPlusTwo) > daysCount ||
|
|
336
|
+
(!tokenAbutsDigitField &&
|
|
337
|
+
Number(inputValueSliceMinusOnePlusOne) > daysCount) ||
|
|
338
|
+
this.specialCharacters.includes(inputValueCursorPlusOne))) ||
|
|
339
|
+
(startWithMonthInput
|
|
340
|
+
? Number(inputValueSliceMinusOnePlusOne) > daysCount ||
|
|
341
|
+
(!this.specialCharacters.includes(inputValueCursor) &&
|
|
342
|
+
this.specialCharacters.includes(inputValueCursorPlusTwo)) ||
|
|
343
|
+
this.specialCharacters.includes(inputValueCursor)
|
|
344
|
+
: Number(dayWindowSlice) > daysCount ||
|
|
345
|
+
(this.specialCharacters.includes(dayWindowNext) && !params.backspaced))) {
|
|
346
|
+
processedPosition = !this.leadZeroDateTime
|
|
347
|
+
? processedPosition + 1
|
|
348
|
+
: processedPosition;
|
|
349
|
+
cursor += 1;
|
|
350
|
+
this._shiftStep(cursor);
|
|
351
|
+
i--;
|
|
352
|
+
if (this.leadZeroDateTime) {
|
|
353
|
+
result += '0';
|
|
354
|
+
}
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
if (maskExpression[cursor] === MaskExpression.MONTH) {
|
|
359
|
+
const monthsCount = 12;
|
|
360
|
+
// Issue #1513: the backward-looking day/month windows below
|
|
361
|
+
// assume the digits before the MONTH token belong to a DAY
|
|
362
|
+
// field. In year-first masks with separators (e.g. 0000-M0-d0)
|
|
363
|
+
// they read YEAR digits through the separator and shove a
|
|
364
|
+
// spurious leading zero into the month. tokenAbutsDigitField
|
|
365
|
+
// (#1523) only covers separator-less layouts, so derive field
|
|
366
|
+
// ownership from the mask itself: locate the field (maximal
|
|
367
|
+
// run of non-special tokens) immediately preceding this MONTH
|
|
368
|
+
// token — a plain digit run of 3+ characters is a year, not a
|
|
369
|
+
// day, and the day-based heuristics must not fire.
|
|
370
|
+
let precedingFieldEnd = cursor - 1;
|
|
371
|
+
while (precedingFieldEnd >= 0 &&
|
|
372
|
+
this.specialCharacters.includes(maskExpression[precedingFieldEnd])) {
|
|
373
|
+
precedingFieldEnd--;
|
|
374
|
+
}
|
|
375
|
+
let precedingFieldStart = precedingFieldEnd;
|
|
376
|
+
while (precedingFieldStart >= 0 &&
|
|
377
|
+
!this.specialCharacters.includes(maskExpression[precedingFieldStart])) {
|
|
378
|
+
precedingFieldStart--;
|
|
379
|
+
}
|
|
380
|
+
const precedingField = maskExpression.slice(precedingFieldStart + 1, precedingFieldEnd + 1);
|
|
381
|
+
const yearFieldPrecedesMonth = precedingField.length > 2 &&
|
|
382
|
+
precedingField
|
|
383
|
+
.split(MaskExpression.EMPTY_STRING)
|
|
384
|
+
.every((token) => token === MaskExpression.NUMBER_ZERO);
|
|
385
|
+
// mask without day
|
|
386
|
+
const withoutDays = cursor === 0 &&
|
|
387
|
+
(Number(inputSymbol) > 2 ||
|
|
388
|
+
Number(inputValueSliceCursorPlusTwo) > monthsCount ||
|
|
389
|
+
(this.specialCharacters.includes(inputValueCursorPlusOne) &&
|
|
390
|
+
!params.backspaced));
|
|
391
|
+
// day<10 && month<12 for input
|
|
392
|
+
const specialChart = maskExpression.slice(cursor + 2, cursor + 3);
|
|
393
|
+
const day1monthInput = inputValueSliceMinusThreeMinusOne.includes(specialChart) &&
|
|
394
|
+
maskExpression.includes('d0') &&
|
|
395
|
+
((this.specialCharacters.includes(inputValueCursorMinusTwo) &&
|
|
396
|
+
Number(inputValueSliceMinusOnePlusOne) > monthsCount &&
|
|
397
|
+
!this.specialCharacters.includes(inputValueCursor)) ||
|
|
398
|
+
this.specialCharacters.includes(inputValueCursor));
|
|
399
|
+
// month<12 && day<10 for input
|
|
400
|
+
const day2monthInput = !tokenAbutsDigitField &&
|
|
401
|
+
!yearFieldPrecedesMonth &&
|
|
402
|
+
Number(inputValueSliceMinusThreeMinusOne) <= daysCount &&
|
|
403
|
+
!this.specialCharacters.includes(inputValueSliceMinusThreeMinusOne) &&
|
|
404
|
+
this.specialCharacters.includes(inputValueCursorMinusOne) &&
|
|
405
|
+
(Number(inputValueSliceCursorPlusTwo) > monthsCount ||
|
|
406
|
+
this.specialCharacters.includes(inputValueCursorPlusOne));
|
|
407
|
+
// cursor === 5 && without days
|
|
408
|
+
const day2monthInputDot = (Number(inputValueSliceCursorPlusTwo) > monthsCount && cursor === 5) ||
|
|
409
|
+
(this.specialCharacters.includes(inputValueCursorPlusOne) && cursor === 5);
|
|
410
|
+
// // day<10 && month<12 for paste whole data
|
|
411
|
+
const day1monthPaste = !tokenAbutsDigitField &&
|
|
412
|
+
!yearFieldPrecedesMonth &&
|
|
413
|
+
Number(inputValueSliceMinusThreeMinusOne) > daysCount &&
|
|
414
|
+
!this.specialCharacters.includes(inputValueSliceMinusThreeMinusOne) &&
|
|
415
|
+
!this.specialCharacters.includes(inputValueSliceMinusTwoCursor) &&
|
|
416
|
+
Number(inputValueSliceMinusTwoCursor) > monthsCount &&
|
|
417
|
+
maskExpression.includes('d0');
|
|
418
|
+
// 10<day<31 && month<12 for paste whole data
|
|
419
|
+
const day2monthPaste = !tokenAbutsDigitField &&
|
|
420
|
+
!yearFieldPrecedesMonth &&
|
|
421
|
+
Number(inputValueSliceMinusThreeMinusOne) <= daysCount &&
|
|
422
|
+
!this.specialCharacters.includes(inputValueSliceMinusThreeMinusOne) &&
|
|
423
|
+
!this.specialCharacters.includes(inputValueCursorMinusOne) &&
|
|
424
|
+
Number(inputValueSliceMinusOnePlusOne) > monthsCount;
|
|
425
|
+
if ((Number(inputSymbol) > 1 && this.leadZeroDateTime) ||
|
|
426
|
+
withoutDays ||
|
|
427
|
+
day1monthInput ||
|
|
428
|
+
day2monthPaste ||
|
|
429
|
+
day1monthPaste ||
|
|
430
|
+
day2monthInput ||
|
|
431
|
+
(day2monthInputDot && !this.leadZeroDateTime)) {
|
|
432
|
+
processedPosition = !this.leadZeroDateTime
|
|
433
|
+
? processedPosition + 1
|
|
434
|
+
: processedPosition;
|
|
435
|
+
cursor += 1;
|
|
436
|
+
this._shiftStep(cursor);
|
|
437
|
+
i--;
|
|
438
|
+
if (this.leadZeroDateTime) {
|
|
439
|
+
result += '0';
|
|
440
|
+
}
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
result += inputSymbol;
|
|
445
|
+
cursor++;
|
|
446
|
+
}
|
|
447
|
+
else if (this.specialCharacters.includes(inputSymbol) &&
|
|
448
|
+
maskExpression[cursor] === inputSymbol) {
|
|
449
|
+
result += inputSymbol;
|
|
450
|
+
cursor++;
|
|
451
|
+
}
|
|
452
|
+
else if (this.specialCharacters.indexOf(maskExpression[cursor] ?? MaskExpression.EMPTY_STRING) !== -1) {
|
|
453
|
+
result += maskExpression[cursor];
|
|
454
|
+
cursor++;
|
|
455
|
+
this._shiftStep(cursor);
|
|
456
|
+
i--;
|
|
457
|
+
}
|
|
458
|
+
else if (maskExpression[cursor] === MaskExpression.NUMBER_NINE && this.showMaskTyped) {
|
|
459
|
+
this._shiftStep(cursor);
|
|
460
|
+
}
|
|
461
|
+
else if (this.patterns[maskExpression[cursor] ?? MaskExpression.EMPTY_STRING] &&
|
|
462
|
+
this.patterns[maskExpression[cursor] ?? MaskExpression.EMPTY_STRING]?.optional) {
|
|
463
|
+
// If the input symbol is whitespace or doesn't match the pattern,
|
|
464
|
+
// skip it without consuming the mask position
|
|
465
|
+
if (inputSymbol.trim() === MaskExpression.EMPTY_STRING) {
|
|
466
|
+
// Skip whitespace input, don't advance mask cursor
|
|
467
|
+
continue;
|
|
468
|
+
}
|
|
469
|
+
if (!!inputArray[cursor] &&
|
|
470
|
+
maskExpression !== '099.099.099.099' &&
|
|
471
|
+
maskExpression !== '000.000.000-00' &&
|
|
472
|
+
maskExpression !== '00.000.000/0000-00' &&
|
|
473
|
+
!maskExpression.match(/^9+\.0+$/) &&
|
|
474
|
+
!this.patterns[maskExpression[cursor] ?? MaskExpression.EMPTY_STRING]?.optional) {
|
|
475
|
+
result += inputArray[cursor];
|
|
476
|
+
}
|
|
477
|
+
if (maskExpression.includes(MaskExpression.NUMBER_NINE + MaskExpression.SYMBOL_STAR) &&
|
|
478
|
+
maskExpression.includes(MaskExpression.NUMBER_ZERO + MaskExpression.SYMBOL_STAR)) {
|
|
479
|
+
cursor++;
|
|
480
|
+
}
|
|
481
|
+
cursor++;
|
|
482
|
+
i--;
|
|
483
|
+
}
|
|
484
|
+
else if (this.maskExpression[cursor + 1] === MaskExpression.SYMBOL_STAR &&
|
|
485
|
+
// A typed char that exactly matches the mask's literal char at cursor+2
|
|
486
|
+
// (e.g. the '@' in 'A*@A*.A*') always terminates the 'A*' run, regardless
|
|
487
|
+
// of whether that literal is registered in `specialCharacters` — an
|
|
488
|
+
// explicitly empty `specialCharacters` list must not make mask literals
|
|
489
|
+
// unmatchable (#1512).
|
|
490
|
+
(this._findSpecialChar(this.maskExpression[cursor + 2] ?? MaskExpression.EMPTY_STRING)
|
|
491
|
+
? this._findSpecialChar(inputSymbol) === this.maskExpression[cursor + 2]
|
|
492
|
+
: inputSymbol === this.maskExpression[cursor + 2]) &&
|
|
493
|
+
multi) {
|
|
494
|
+
cursor += 3;
|
|
495
|
+
result += inputSymbol;
|
|
496
|
+
}
|
|
497
|
+
else if (this.maskExpression[cursor + 1] === MaskExpression.SYMBOL_QUESTION &&
|
|
498
|
+
(this._findSpecialChar(this.maskExpression[cursor + 2] ?? MaskExpression.EMPTY_STRING)
|
|
499
|
+
? this._findSpecialChar(inputSymbol) === this.maskExpression[cursor + 2]
|
|
500
|
+
: inputSymbol === this.maskExpression[cursor + 2]) &&
|
|
501
|
+
multi) {
|
|
502
|
+
cursor += 3;
|
|
503
|
+
result += inputSymbol;
|
|
504
|
+
}
|
|
505
|
+
else if (this.showMaskTyped &&
|
|
506
|
+
this.specialCharacters.indexOf(inputSymbol) < 0 &&
|
|
507
|
+
inputSymbol !== this.placeHolderCharacter &&
|
|
508
|
+
this.placeHolderCharacter.length === 1) {
|
|
509
|
+
stepBack = true;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
state.cursor = cursor;
|
|
513
|
+
state.result = result;
|
|
514
|
+
state.multi = multi;
|
|
515
|
+
state.processedPosition = processedPosition;
|
|
516
|
+
state.stepBack = stepBack;
|
|
517
|
+
return state;
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
/** IP pre-processor (moved verbatim from applyMask). Sets `this.ipError`, rewrites
|
|
521
|
+
* the mask to `099.099.099.099`, then falls through to the generic loop. */
|
|
522
|
+
const ipHandler = function (state) {
|
|
523
|
+
const valuesIP = state.processedValue.split(MaskExpression.DOT);
|
|
524
|
+
this.ipError = this._validIP(valuesIP);
|
|
525
|
+
state.maskExpression = '099.099.099.099';
|
|
526
|
+
return state;
|
|
527
|
+
};
|
|
528
|
+
|
|
529
|
+
/** PERCENT handler (moved verbatim from applyMask). startsWith(PERCENT); terminal —
|
|
530
|
+
* produces the final `result`, no fallthrough. */
|
|
531
|
+
const percentHandler = function (state, params) {
|
|
532
|
+
const { backspaced } = params;
|
|
533
|
+
const { cursor } = state;
|
|
534
|
+
let processedValue = state.processedValue;
|
|
535
|
+
if (processedValue.match('[a-z]|[A-Z]') ||
|
|
536
|
+
// eslint-disable-next-line no-useless-escape
|
|
537
|
+
(processedValue.match(/[-!$%^&*()_+|~=`{}\[\]:";'<>?,\/.]/) && !backspaced)) {
|
|
538
|
+
processedValue = this._stripToDecimal(processedValue);
|
|
539
|
+
const precision = this.getPrecision(state.maskExpression);
|
|
540
|
+
processedValue = this.checkInputPrecision(processedValue, precision, this.decimalMarker);
|
|
541
|
+
}
|
|
542
|
+
const decimalMarker = typeof this.decimalMarker === 'string' ? this.decimalMarker : MaskExpression.DOT;
|
|
543
|
+
if (processedValue.indexOf(decimalMarker) > 0 &&
|
|
544
|
+
!this.percentage(processedValue.substring(0, processedValue.indexOf(decimalMarker)))) {
|
|
545
|
+
let base = processedValue.substring(0, processedValue.indexOf(decimalMarker) - 1);
|
|
546
|
+
if (this.allowNegativeNumbers &&
|
|
547
|
+
processedValue.slice(cursor, cursor + 1) === MaskExpression.MINUS &&
|
|
548
|
+
!backspaced) {
|
|
549
|
+
base = processedValue.substring(0, processedValue.indexOf(decimalMarker));
|
|
550
|
+
}
|
|
551
|
+
processedValue = `${base}${processedValue.substring(processedValue.indexOf(decimalMarker), processedValue.length)}`;
|
|
552
|
+
}
|
|
553
|
+
let value;
|
|
554
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
|
555
|
+
this.allowNegativeNumbers && processedValue.slice(cursor, cursor + 1) === MaskExpression.MINUS
|
|
556
|
+
? (value = `${MaskExpression.MINUS}${processedValue.slice(cursor + 1, cursor + processedValue.length)}`)
|
|
557
|
+
: (value = processedValue);
|
|
558
|
+
if (this.percentage(value)) {
|
|
559
|
+
state.result = this._splitPercentZero(processedValue);
|
|
560
|
+
}
|
|
561
|
+
else {
|
|
562
|
+
state.result = this._splitPercentZero(processedValue.substring(0, processedValue.length - 1));
|
|
563
|
+
}
|
|
564
|
+
state.processedValue = processedValue;
|
|
565
|
+
return state;
|
|
566
|
+
};
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* SEPARATOR handler (moved verbatim from applyMask, original lines 219-580).
|
|
570
|
+
* Discriminator: `maskExpression.startsWith(MaskExpression.SEPARATOR)`. Produces the
|
|
571
|
+
* final `result` for this call.
|
|
572
|
+
*
|
|
573
|
+
* Two sequential zero-handling stages are preserved together, in original relative
|
|
574
|
+
* order (CODEBASE_NOTES.md #13): the `if (backspaced)` guard block and the
|
|
575
|
+
* precision-0 leading-zero stripper.
|
|
576
|
+
*
|
|
577
|
+
* The `typeFromDecimals` sub-case (#733/#1414/#1315) early-returns the final string
|
|
578
|
+
* via `state.earlyReturn` after calling `cb()` — this must bypass the shared
|
|
579
|
+
* post-processing in `applyMask` exactly, or the caret-pin `cb()` contract breaks.
|
|
580
|
+
*/
|
|
581
|
+
const separatorHandler = function (state, params) {
|
|
582
|
+
const { backspaced, justPasted, startsWithPrefix, prefixAlreadyRemovedByCaller, cb, inputValue, } = params;
|
|
583
|
+
let { processedValue, processedPosition, backspaceShift, shift, stepBack } = state;
|
|
584
|
+
let result;
|
|
585
|
+
if (processedValue.match('[wа-яА-Я]') ||
|
|
586
|
+
processedValue.match('[ЁёА-я]') ||
|
|
587
|
+
processedValue.match('[a-z]|[A-Z]') ||
|
|
588
|
+
processedValue.match(/[-@#!$%\\^&*()_£¬'+|~=`{}\]:";<>.?/]/) ||
|
|
589
|
+
processedValue.match('[^A-Za-z0-9,]')) {
|
|
590
|
+
processedValue = this._stripToDecimal(processedValue);
|
|
591
|
+
}
|
|
592
|
+
const precision = this.getPrecision(state.maskExpression);
|
|
593
|
+
let decimalMarker = this.decimalMarker;
|
|
594
|
+
if (Array.isArray(this.decimalMarker)) {
|
|
595
|
+
if (this.actualValue.includes(this.decimalMarker[0]) ||
|
|
596
|
+
this.actualValue.includes(this.decimalMarker[1])) {
|
|
597
|
+
decimalMarker = this.actualValue.includes(this.decimalMarker[0])
|
|
598
|
+
? this.decimalMarker[0]
|
|
599
|
+
: this.decimalMarker[1];
|
|
600
|
+
}
|
|
601
|
+
else {
|
|
602
|
+
decimalMarker = this.decimalMarker.find((dm) => dm !== this.thousandSeparator);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
// Issues #733/#1414/#1315: opt-in "banking" typing mode — typed digits fill
|
|
606
|
+
// the value from the decimal end, ATM/calculator style (5 -> 0.05 -> 0.57
|
|
607
|
+
// -> 5.73); backspace shifts digits back to the right. Only keystroke and
|
|
608
|
+
// backspace flows are reinterpreted; paste and writeValue keep the regular
|
|
609
|
+
// separator formatting. The early return leaves every existing separator
|
|
610
|
+
// path byte-identical when the option is off.
|
|
611
|
+
if (this.typeFromDecimals &&
|
|
612
|
+
Number.isFinite(precision) &&
|
|
613
|
+
precision > 0 &&
|
|
614
|
+
!justPasted &&
|
|
615
|
+
!this.writingValue) {
|
|
616
|
+
result = this._formatFromDecimals(processedValue, precision, decimalMarker);
|
|
617
|
+
this._shift.clear();
|
|
618
|
+
const res = result.includes(MaskExpression.MINUS) && this.prefix && this.allowNegativeNumbers
|
|
619
|
+
? `${MaskExpression.MINUS}${this.prefix}${result
|
|
620
|
+
.split(MaskExpression.MINUS)
|
|
621
|
+
.join(MaskExpression.EMPTY_STRING)}${this.suffix}`
|
|
622
|
+
: result.length
|
|
623
|
+
? `${this.prefix}${result}${this.suffix}`
|
|
624
|
+
: this.instantPrefix
|
|
625
|
+
? this.prefix
|
|
626
|
+
: MaskExpression.EMPTY_STRING;
|
|
627
|
+
// Pin the caret to the end of the value part: every keystroke reshapes
|
|
628
|
+
// the whole string, so the in-place caret position is meaningless here.
|
|
629
|
+
cb(res.length - this.suffix.length - processedPosition, true);
|
|
630
|
+
state.earlyReturn = res;
|
|
631
|
+
return state;
|
|
632
|
+
}
|
|
633
|
+
// Issue #1250: typing an additional decimal marker into a value that
|
|
634
|
+
// already contains one must be a no-op — otherwise everything after the
|
|
635
|
+
// new marker is re-parsed as a fresh decimal part and the value is
|
|
636
|
+
// mangled (15.000,53 + ',' typed after the '1' -> 1,5). Remove the newly
|
|
637
|
+
// typed marker (the char right before the caret) and step the caret back.
|
|
638
|
+
// Paste keeps its own semantics (#1547 below: last marker wins), and
|
|
639
|
+
// backspace/writeValue flows cannot introduce a new marker.
|
|
640
|
+
if (!justPasted && !backspaced && !this.writingValue) {
|
|
641
|
+
const isDecimalMarkerChar = (char) => !!char &&
|
|
642
|
+
char !== this.thousandSeparator &&
|
|
643
|
+
(Array.isArray(this.decimalMarker)
|
|
644
|
+
? this.decimalMarker.includes(char)
|
|
645
|
+
: char === this.decimalMarker);
|
|
646
|
+
const prefixOffset = startsWithPrefix && !prefixAlreadyRemovedByCaller ? this.prefix.length : 0;
|
|
647
|
+
const typedMarkerIndex = processedPosition - prefixOffset - 1;
|
|
648
|
+
if (typedMarkerIndex >= 0 && isDecimalMarkerChar(processedValue[typedMarkerIndex])) {
|
|
649
|
+
let markerCount = 0;
|
|
650
|
+
for (const char of processedValue) {
|
|
651
|
+
if (isDecimalMarkerChar(char)) {
|
|
652
|
+
markerCount++;
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
if (markerCount > 1) {
|
|
656
|
+
processedValue =
|
|
657
|
+
processedValue.slice(0, typedMarkerIndex) +
|
|
658
|
+
processedValue.slice(typedMarkerIndex + 1);
|
|
659
|
+
stepBack = true;
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
// Issue #1547: a pasted value may contain grouping separators that are
|
|
664
|
+
// also configured decimal markers (default decimalMarker is ['.', ',']),
|
|
665
|
+
// e.g. '1,234.56'. Only the last marker character can actually be the
|
|
666
|
+
// decimal marker — treat the earlier ones as thousand separators and
|
|
667
|
+
// strip them, otherwise formatting cuts the value off at the first one.
|
|
668
|
+
if (justPasted && Array.isArray(this.decimalMarker)) {
|
|
669
|
+
const markerPositions = [];
|
|
670
|
+
for (let i = 0; i < processedValue.length; i++) {
|
|
671
|
+
const char = processedValue[i];
|
|
672
|
+
if (char !== this.thousandSeparator &&
|
|
673
|
+
this.decimalMarker.includes(char)) {
|
|
674
|
+
markerPositions.push(i);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
if (markerPositions.length > 1) {
|
|
678
|
+
const lastMarkerPosition = markerPositions[markerPositions.length - 1];
|
|
679
|
+
processedValue = processedValue
|
|
680
|
+
.split(MaskExpression.EMPTY_STRING)
|
|
681
|
+
.filter((_, index) => index === lastMarkerPosition || !markerPositions.includes(index))
|
|
682
|
+
.join(MaskExpression.EMPTY_STRING);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
if (backspaced) {
|
|
686
|
+
const { decimalMarkerIndex, nonZeroIndex } = this._findFirstNonZeroAndDecimalIndex(processedValue, decimalMarker);
|
|
687
|
+
const zeroIndexMinus = processedValue[0] === MaskExpression.MINUS;
|
|
688
|
+
const zeroIndexDecimalMarker = processedValue[0] === decimalMarker;
|
|
689
|
+
const firstIndexDecimalMarker = processedValue[1] === decimalMarker;
|
|
690
|
+
// Issues #1355/#1578: an all-zero remainder (e.g. '00' from '500', '00,000'
|
|
691
|
+
// from '100,000') must keep its zeros, matching the ',000,000' case.
|
|
692
|
+
// Only collapse when nothing but a bare decimal marker (or '-.') remains.
|
|
693
|
+
if ((zeroIndexDecimalMarker && !nonZeroIndex) ||
|
|
694
|
+
(zeroIndexMinus && firstIndexDecimalMarker && !nonZeroIndex)) {
|
|
695
|
+
processedValue = MaskExpression.NUMBER_ZERO;
|
|
696
|
+
}
|
|
697
|
+
if (decimalMarkerIndex && nonZeroIndex && zeroIndexMinus && processedPosition === 1) {
|
|
698
|
+
if (decimalMarkerIndex < nonZeroIndex || decimalMarkerIndex > nonZeroIndex) {
|
|
699
|
+
processedValue = MaskExpression.MINUS + processedValue.slice(nonZeroIndex);
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
// Issue #1516: decimalMarkerIndex is 0 when the remainder starts with
|
|
703
|
+
// the decimal marker (',34' after deleting the integer part) — a truthy
|
|
704
|
+
// check would treat it as "no decimal marker" and slice the marker off.
|
|
705
|
+
if (decimalMarkerIndex === null && nonZeroIndex && processedValue.length > nonZeroIndex) {
|
|
706
|
+
processedValue = zeroIndexMinus
|
|
707
|
+
? MaskExpression.MINUS + processedValue.slice(nonZeroIndex)
|
|
708
|
+
: processedValue.slice(nonZeroIndex);
|
|
709
|
+
}
|
|
710
|
+
if (decimalMarkerIndex && nonZeroIndex && processedPosition === 0) {
|
|
711
|
+
if (decimalMarkerIndex < nonZeroIndex) {
|
|
712
|
+
processedValue = processedValue.slice(decimalMarkerIndex - 1);
|
|
713
|
+
}
|
|
714
|
+
if (decimalMarkerIndex > nonZeroIndex) {
|
|
715
|
+
processedValue = processedValue.slice(nonZeroIndex);
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
// Leading-zero stripping is a typing-time rule ('05' -> '5'). When backspaced,
|
|
720
|
+
// leading zeros before a non-zero digit were already removed above, and an
|
|
721
|
+
// all-zero remainder must keep its zeros (issues #1355/#1578).
|
|
722
|
+
if (precision === 0 && !backspaced) {
|
|
723
|
+
processedValue = this.allowNegativeNumbers
|
|
724
|
+
? processedValue.length > 2 &&
|
|
725
|
+
processedValue[0] === MaskExpression.MINUS &&
|
|
726
|
+
processedValue[1] === MaskExpression.NUMBER_ZERO &&
|
|
727
|
+
processedValue[2] !== this.thousandSeparator &&
|
|
728
|
+
processedValue[2] !== MaskExpression.COMMA &&
|
|
729
|
+
processedValue[2] !== MaskExpression.DOT
|
|
730
|
+
? '-' + processedValue.slice(2, processedValue.length)
|
|
731
|
+
: processedValue[0] === MaskExpression.NUMBER_ZERO &&
|
|
732
|
+
processedValue.length > 1 &&
|
|
733
|
+
processedValue[1] !== this.thousandSeparator &&
|
|
734
|
+
processedValue[1] !== MaskExpression.COMMA &&
|
|
735
|
+
processedValue[1] !== MaskExpression.DOT
|
|
736
|
+
? processedValue.slice(1, processedValue.length)
|
|
737
|
+
: processedValue
|
|
738
|
+
: processedValue.length > 1 &&
|
|
739
|
+
processedValue[0] === MaskExpression.NUMBER_ZERO &&
|
|
740
|
+
processedValue[1] !== this.thousandSeparator &&
|
|
741
|
+
processedValue[1] !== MaskExpression.COMMA &&
|
|
742
|
+
processedValue[1] !== MaskExpression.DOT
|
|
743
|
+
? processedValue.slice(1, processedValue.length)
|
|
744
|
+
: processedValue;
|
|
745
|
+
}
|
|
746
|
+
else {
|
|
747
|
+
if (processedValue[0] === decimalMarker && processedValue.length > 1 && !backspaced) {
|
|
748
|
+
processedValue =
|
|
749
|
+
MaskExpression.NUMBER_ZERO + processedValue.slice(0, processedValue.length + 1);
|
|
750
|
+
this.plusOnePosition = true;
|
|
751
|
+
}
|
|
752
|
+
if (processedValue[0] === MaskExpression.NUMBER_ZERO &&
|
|
753
|
+
processedValue[1] !== decimalMarker &&
|
|
754
|
+
processedValue[1] !== this.thousandSeparator &&
|
|
755
|
+
!backspaced) {
|
|
756
|
+
processedValue =
|
|
757
|
+
processedValue.length > 1
|
|
758
|
+
? processedValue.slice(0, 1) +
|
|
759
|
+
decimalMarker +
|
|
760
|
+
processedValue.slice(1, processedValue.length + 1)
|
|
761
|
+
: processedValue;
|
|
762
|
+
this.plusOnePosition = true;
|
|
763
|
+
}
|
|
764
|
+
if (this.allowNegativeNumbers &&
|
|
765
|
+
!backspaced &&
|
|
766
|
+
processedValue[0] === MaskExpression.MINUS &&
|
|
767
|
+
(processedValue[1] === decimalMarker ||
|
|
768
|
+
processedValue[1] === MaskExpression.NUMBER_ZERO)) {
|
|
769
|
+
processedValue =
|
|
770
|
+
processedValue[1] === decimalMarker && processedValue.length > 2
|
|
771
|
+
? processedValue.slice(0, 1) +
|
|
772
|
+
MaskExpression.NUMBER_ZERO +
|
|
773
|
+
processedValue.slice(1, processedValue.length)
|
|
774
|
+
: processedValue[1] === MaskExpression.NUMBER_ZERO &&
|
|
775
|
+
processedValue.length > 2 &&
|
|
776
|
+
processedValue[2] !== decimalMarker
|
|
777
|
+
? processedValue.slice(0, 2) +
|
|
778
|
+
decimalMarker +
|
|
779
|
+
processedValue.slice(2, processedValue.length)
|
|
780
|
+
: processedValue;
|
|
781
|
+
this.plusOnePosition = true;
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
// Historical note: pre-refactor code used different allowed-character regexes
|
|
785
|
+
// per config (plain separator: no COMMA; dot thousand-sep: no SPACE, COMMA OK;
|
|
786
|
+
// comma thousand-sep: no SPACE, COMMA OK). The unified regex below removes
|
|
787
|
+
// exactly the active thousandSeparator + decimalMarker(s) from the invalid set,
|
|
788
|
+
// so each config already accepts its own chars and rejects the others —
|
|
789
|
+
// verified by the "unified invalidChars regex" cases in separator.spec.ts.
|
|
790
|
+
const thousandSeparatorCharEscaped = this._charToRegExpExpression(this.thousandSeparator);
|
|
791
|
+
let invalidChars = '@#!$%^&*()_+|~=`{}\\[\\]:\\s,\\.";<>?\\/'.replace(thousandSeparatorCharEscaped, '');
|
|
792
|
+
//.replace(decimalMarkerEscaped, '');
|
|
793
|
+
if (Array.isArray(this.decimalMarker)) {
|
|
794
|
+
for (const marker of this.decimalMarker) {
|
|
795
|
+
invalidChars = invalidChars.replace(this._charToRegExpExpression(marker), MaskExpression.EMPTY_STRING);
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
else {
|
|
799
|
+
invalidChars = invalidChars.replace(this._charToRegExpExpression(this.decimalMarker), '');
|
|
800
|
+
}
|
|
801
|
+
const invalidCharRegexp = new RegExp('[' + invalidChars + ']');
|
|
802
|
+
if (processedValue.match(invalidCharRegexp)) {
|
|
803
|
+
processedValue = processedValue.substring(0, processedValue.length - 1);
|
|
804
|
+
}
|
|
805
|
+
processedValue = this.checkInputPrecision(processedValue, precision, this.decimalMarker);
|
|
806
|
+
const strForSep = processedValue.replace(new RegExp(thousandSeparatorCharEscaped, 'g'), '');
|
|
807
|
+
result = this._formatWithSeparators(strForSep, this.thousandSeparator, this.decimalMarker, precision);
|
|
808
|
+
const commaShift = result.indexOf(MaskExpression.COMMA) - processedValue.indexOf(MaskExpression.COMMA);
|
|
809
|
+
const shiftStep = result.length - processedValue.length;
|
|
810
|
+
const backspacedDecimalMarkerWithSeparatorLimit = backspaced && result.length < inputValue.length - this.suffix.length && this.separatorLimit;
|
|
811
|
+
if ((result[processedPosition - 1] === this.thousandSeparator ||
|
|
812
|
+
result[processedPosition - this.prefix.length]) &&
|
|
813
|
+
this.prefix &&
|
|
814
|
+
backspaced) {
|
|
815
|
+
processedPosition = processedPosition - 1;
|
|
816
|
+
}
|
|
817
|
+
else if ((shiftStep > 0 && result[processedPosition] !== this.thousandSeparator) ||
|
|
818
|
+
backspacedDecimalMarkerWithSeparatorLimit) {
|
|
819
|
+
backspaceShift = true;
|
|
820
|
+
let _shift = 0;
|
|
821
|
+
do {
|
|
822
|
+
this._shift.add(processedPosition + _shift);
|
|
823
|
+
_shift++;
|
|
824
|
+
} while (_shift < shiftStep);
|
|
825
|
+
}
|
|
826
|
+
else if (result[processedPosition - 1] === this.thousandSeparator ||
|
|
827
|
+
shiftStep === -4 ||
|
|
828
|
+
shiftStep === -3 ||
|
|
829
|
+
result[processedPosition] === this.thousandSeparator) {
|
|
830
|
+
this._shift.clear();
|
|
831
|
+
this._shift.add(processedPosition - 1);
|
|
832
|
+
}
|
|
833
|
+
else if ((commaShift !== 0 &&
|
|
834
|
+
processedPosition > 0 &&
|
|
835
|
+
!(result.indexOf(MaskExpression.COMMA) >= processedPosition && processedPosition > 3)) ||
|
|
836
|
+
(!(result.indexOf(MaskExpression.DOT) >= processedPosition && processedPosition > 3) &&
|
|
837
|
+
shiftStep <= 0)) {
|
|
838
|
+
this._shift.clear();
|
|
839
|
+
backspaceShift = true;
|
|
840
|
+
shift = shiftStep;
|
|
841
|
+
processedPosition += shiftStep;
|
|
842
|
+
this._shift.add(processedPosition);
|
|
843
|
+
}
|
|
844
|
+
else {
|
|
845
|
+
this._shift.clear();
|
|
846
|
+
}
|
|
847
|
+
state.processedValue = processedValue;
|
|
848
|
+
state.processedPosition = processedPosition;
|
|
849
|
+
state.result = result;
|
|
850
|
+
state.backspaceShift = backspaceShift;
|
|
851
|
+
state.shift = shift;
|
|
852
|
+
state.stepBack = stepBack;
|
|
853
|
+
return state;
|
|
854
|
+
};
|
|
855
|
+
|
|
856
|
+
const MASK_HANDLERS = [
|
|
857
|
+
{
|
|
858
|
+
id: 'ip',
|
|
859
|
+
terminal: false,
|
|
860
|
+
match: (maskExpression) => maskExpression === MaskExpression.IP,
|
|
861
|
+
handle: ipHandler,
|
|
862
|
+
},
|
|
863
|
+
{
|
|
864
|
+
id: 'cpf-cnpj',
|
|
865
|
+
terminal: false,
|
|
866
|
+
match: (maskExpression) => maskExpression === MaskExpression.CPF_CNPJ ||
|
|
867
|
+
maskExpression === MaskExpression.CPF_CNPJ_ALPHA,
|
|
868
|
+
handle: cpfCnpjHandler,
|
|
869
|
+
},
|
|
870
|
+
{
|
|
871
|
+
id: 'percent',
|
|
872
|
+
terminal: true,
|
|
873
|
+
match: (maskExpression) => maskExpression.startsWith(MaskExpression.PERCENT),
|
|
874
|
+
handle: percentHandler,
|
|
875
|
+
},
|
|
876
|
+
{
|
|
877
|
+
id: 'separator',
|
|
878
|
+
terminal: true,
|
|
879
|
+
match: (maskExpression) => maskExpression.startsWith(MaskExpression.SEPARATOR),
|
|
880
|
+
handle: separatorHandler,
|
|
881
|
+
},
|
|
882
|
+
];
|
|
883
|
+
/** Runs mask-type dispatch: first matching entry wins. IP/CPF_CNPJ pre-process then
|
|
884
|
+
* fall through to the generic loop; PERCENT/SEPARATOR resolve terminally; no match →
|
|
885
|
+
* generic loop directly (the original `else`). IP and CPF_CNPJ are mutually exclusive
|
|
886
|
+
* exact matches, so scanning the ordered table is behavior-identical to the original
|
|
887
|
+
* IP → CPF_CNPJ → PERCENT → SEPARATOR → else chain. */
|
|
888
|
+
function dispatchMaskHandler(self, state, params) {
|
|
889
|
+
for (const entry of MASK_HANDLERS) {
|
|
890
|
+
if (entry.match(state.maskExpression)) {
|
|
891
|
+
const next = entry.handle.call(self, state, params);
|
|
892
|
+
return entry.terminal ? next : genericPatternHandler.call(self, next, params);
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
return genericPatternHandler.call(self, state, params);
|
|
896
|
+
}
|
|
897
|
+
|
|
183
898
|
class NgxMaskApplierService {
|
|
184
899
|
_config = inject(NGX_MASK_CONFIG);
|
|
185
900
|
dropSpecialCharacters = this._config.dropSpecialCharacters;
|
|
@@ -228,7 +943,7 @@ class NgxMaskApplierService {
|
|
|
228
943
|
}
|
|
229
944
|
let cursor = 0;
|
|
230
945
|
let result = '';
|
|
231
|
-
|
|
946
|
+
const multi = false;
|
|
232
947
|
let backspaceShift = false;
|
|
233
948
|
let shift = 1;
|
|
234
949
|
let stepBack = false;
|
|
@@ -258,12 +973,6 @@ class NgxMaskApplierService {
|
|
|
258
973
|
processedValue.slice(cursor, cursor + 1) === MaskExpression.MINUS) {
|
|
259
974
|
result += processedValue.slice(cursor, cursor + 1);
|
|
260
975
|
}
|
|
261
|
-
if (maskExpression === MaskExpression.IP) {
|
|
262
|
-
const valuesIP = processedValue.split(MaskExpression.DOT);
|
|
263
|
-
this.ipError = this._validIP(valuesIP);
|
|
264
|
-
// eslint-disable-next-line no-param-reassign
|
|
265
|
-
maskExpression = '099.099.099.099';
|
|
266
|
-
}
|
|
267
976
|
const arr = [];
|
|
268
977
|
// eslint-disable-next-line @typescript-eslint/prefer-for-of
|
|
269
978
|
for (let i = 0; i < processedValue.length; i++) {
|
|
@@ -271,623 +980,43 @@ class NgxMaskApplierService {
|
|
|
271
980
|
arr.push(processedValue[i] ?? MaskExpression.EMPTY_STRING);
|
|
272
981
|
}
|
|
273
982
|
}
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
|
312
|
-
this.allowNegativeNumbers &&
|
|
313
|
-
processedValue.slice(cursor, cursor + 1) === MaskExpression.MINUS
|
|
314
|
-
? (value = `${MaskExpression.MINUS}${processedValue.slice(cursor + 1, cursor + processedValue.length)}`)
|
|
315
|
-
: (value = processedValue);
|
|
316
|
-
if (this.percentage(value)) {
|
|
317
|
-
result = this._splitPercentZero(processedValue);
|
|
318
|
-
}
|
|
319
|
-
else {
|
|
320
|
-
result = this._splitPercentZero(processedValue.substring(0, processedValue.length - 1));
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
else if (maskExpression.startsWith(MaskExpression.SEPARATOR)) {
|
|
324
|
-
if (processedValue.match('[wа-яА-Я]') ||
|
|
325
|
-
processedValue.match('[ЁёА-я]') ||
|
|
326
|
-
processedValue.match('[a-z]|[A-Z]') ||
|
|
327
|
-
processedValue.match(/[-@#!$%\\^&*()_£¬'+|~=`{}\]:";<>.?/]/) ||
|
|
328
|
-
processedValue.match('[^A-Za-z0-9,]')) {
|
|
329
|
-
processedValue = this._stripToDecimal(processedValue);
|
|
330
|
-
}
|
|
331
|
-
const precision = this.getPrecision(maskExpression);
|
|
332
|
-
let decimalMarker = this.decimalMarker;
|
|
333
|
-
if (Array.isArray(this.decimalMarker)) {
|
|
334
|
-
if (this.actualValue.includes(this.decimalMarker[0]) ||
|
|
335
|
-
this.actualValue.includes(this.decimalMarker[1])) {
|
|
336
|
-
decimalMarker = this.actualValue.includes(this.decimalMarker[0])
|
|
337
|
-
? this.decimalMarker[0]
|
|
338
|
-
: this.decimalMarker[1];
|
|
339
|
-
}
|
|
340
|
-
else {
|
|
341
|
-
decimalMarker = this.decimalMarker.find((dm) => dm !== this.thousandSeparator);
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
// Issues #733/#1414/#1315: opt-in "banking" typing mode — typed digits fill
|
|
345
|
-
// the value from the decimal end, ATM/calculator style (5 -> 0.05 -> 0.57
|
|
346
|
-
// -> 5.73); backspace shifts digits back to the right. Only keystroke and
|
|
347
|
-
// backspace flows are reinterpreted; paste and writeValue keep the regular
|
|
348
|
-
// separator formatting. The early return leaves every existing separator
|
|
349
|
-
// path byte-identical when the option is off.
|
|
350
|
-
if (this.typeFromDecimals &&
|
|
351
|
-
Number.isFinite(precision) &&
|
|
352
|
-
precision > 0 &&
|
|
353
|
-
!justPasted &&
|
|
354
|
-
!this.writingValue) {
|
|
355
|
-
result = this._formatFromDecimals(processedValue, precision, decimalMarker);
|
|
356
|
-
this._shift.clear();
|
|
357
|
-
const res = result.includes(MaskExpression.MINUS) &&
|
|
358
|
-
this.prefix &&
|
|
359
|
-
this.allowNegativeNumbers
|
|
360
|
-
? `${MaskExpression.MINUS}${this.prefix}${result
|
|
361
|
-
.split(MaskExpression.MINUS)
|
|
362
|
-
.join(MaskExpression.EMPTY_STRING)}${this.suffix}`
|
|
363
|
-
: result.length
|
|
364
|
-
? `${this.prefix}${result}${this.suffix}`
|
|
365
|
-
: this.instantPrefix
|
|
366
|
-
? this.prefix
|
|
367
|
-
: MaskExpression.EMPTY_STRING;
|
|
368
|
-
// Pin the caret to the end of the value part: every keystroke reshapes
|
|
369
|
-
// the whole string, so the in-place caret position is meaningless here.
|
|
370
|
-
cb(res.length - this.suffix.length - processedPosition, true);
|
|
371
|
-
return res;
|
|
372
|
-
}
|
|
373
|
-
// Issue #1250: typing an additional decimal marker into a value that
|
|
374
|
-
// already contains one must be a no-op — otherwise everything after the
|
|
375
|
-
// new marker is re-parsed as a fresh decimal part and the value is
|
|
376
|
-
// mangled (15.000,53 + ',' typed after the '1' -> 1,5). Remove the newly
|
|
377
|
-
// typed marker (the char right before the caret) and step the caret back.
|
|
378
|
-
// Paste keeps its own semantics (#1547 below: last marker wins), and
|
|
379
|
-
// backspace/writeValue flows cannot introduce a new marker.
|
|
380
|
-
if (!justPasted && !backspaced && !this.writingValue) {
|
|
381
|
-
const isDecimalMarkerChar = (char) => !!char &&
|
|
382
|
-
char !== this.thousandSeparator &&
|
|
383
|
-
(Array.isArray(this.decimalMarker)
|
|
384
|
-
? this.decimalMarker.includes(char)
|
|
385
|
-
: char === this.decimalMarker);
|
|
386
|
-
const prefixOffset = startsWithPrefix && !prefixAlreadyRemovedByCaller ? this.prefix.length : 0;
|
|
387
|
-
const typedMarkerIndex = processedPosition - prefixOffset - 1;
|
|
388
|
-
if (typedMarkerIndex >= 0 &&
|
|
389
|
-
isDecimalMarkerChar(processedValue[typedMarkerIndex])) {
|
|
390
|
-
let markerCount = 0;
|
|
391
|
-
for (const char of processedValue) {
|
|
392
|
-
if (isDecimalMarkerChar(char)) {
|
|
393
|
-
markerCount++;
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
if (markerCount > 1) {
|
|
397
|
-
processedValue =
|
|
398
|
-
processedValue.slice(0, typedMarkerIndex) +
|
|
399
|
-
processedValue.slice(typedMarkerIndex + 1);
|
|
400
|
-
stepBack = true;
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
// Issue #1547: a pasted value may contain grouping separators that are
|
|
405
|
-
// also configured decimal markers (default decimalMarker is ['.', ',']),
|
|
406
|
-
// e.g. '1,234.56'. Only the last marker character can actually be the
|
|
407
|
-
// decimal marker — treat the earlier ones as thousand separators and
|
|
408
|
-
// strip them, otherwise formatting cuts the value off at the first one.
|
|
409
|
-
if (justPasted && Array.isArray(this.decimalMarker)) {
|
|
410
|
-
const markerPositions = [];
|
|
411
|
-
for (let i = 0; i < processedValue.length; i++) {
|
|
412
|
-
const char = processedValue[i];
|
|
413
|
-
if (char !== this.thousandSeparator &&
|
|
414
|
-
this.decimalMarker.includes(char)) {
|
|
415
|
-
markerPositions.push(i);
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
if (markerPositions.length > 1) {
|
|
419
|
-
const lastMarkerPosition = markerPositions[markerPositions.length - 1];
|
|
420
|
-
processedValue = processedValue
|
|
421
|
-
.split(MaskExpression.EMPTY_STRING)
|
|
422
|
-
.filter((_, index) => index === lastMarkerPosition || !markerPositions.includes(index))
|
|
423
|
-
.join(MaskExpression.EMPTY_STRING);
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
if (backspaced) {
|
|
427
|
-
const { decimalMarkerIndex, nonZeroIndex } = this._findFirstNonZeroAndDecimalIndex(processedValue, decimalMarker);
|
|
428
|
-
const zeroIndexMinus = processedValue[0] === MaskExpression.MINUS;
|
|
429
|
-
const zeroIndexDecimalMarker = processedValue[0] === decimalMarker;
|
|
430
|
-
const firstIndexDecimalMarker = processedValue[1] === decimalMarker;
|
|
431
|
-
// Issues #1355/#1578: an all-zero remainder (e.g. '00' from '500', '00,000'
|
|
432
|
-
// from '100,000') must keep its zeros, matching the ',000,000' case.
|
|
433
|
-
// Only collapse when nothing but a bare decimal marker (or '-.') remains.
|
|
434
|
-
if ((zeroIndexDecimalMarker && !nonZeroIndex) ||
|
|
435
|
-
(zeroIndexMinus && firstIndexDecimalMarker && !nonZeroIndex)) {
|
|
436
|
-
processedValue = MaskExpression.NUMBER_ZERO;
|
|
437
|
-
}
|
|
438
|
-
if (decimalMarkerIndex &&
|
|
439
|
-
nonZeroIndex &&
|
|
440
|
-
zeroIndexMinus &&
|
|
441
|
-
processedPosition === 1) {
|
|
442
|
-
if (decimalMarkerIndex < nonZeroIndex || decimalMarkerIndex > nonZeroIndex) {
|
|
443
|
-
processedValue = MaskExpression.MINUS + processedValue.slice(nonZeroIndex);
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
// Issue #1516: decimalMarkerIndex is 0 when the remainder starts with
|
|
447
|
-
// the decimal marker (',34' after deleting the integer part) — a truthy
|
|
448
|
-
// check would treat it as "no decimal marker" and slice the marker off.
|
|
449
|
-
if (decimalMarkerIndex === null &&
|
|
450
|
-
nonZeroIndex &&
|
|
451
|
-
processedValue.length > nonZeroIndex) {
|
|
452
|
-
processedValue = zeroIndexMinus
|
|
453
|
-
? MaskExpression.MINUS + processedValue.slice(nonZeroIndex)
|
|
454
|
-
: processedValue.slice(nonZeroIndex);
|
|
455
|
-
}
|
|
456
|
-
if (decimalMarkerIndex && nonZeroIndex && processedPosition === 0) {
|
|
457
|
-
if (decimalMarkerIndex < nonZeroIndex) {
|
|
458
|
-
processedValue = processedValue.slice(decimalMarkerIndex - 1);
|
|
459
|
-
}
|
|
460
|
-
if (decimalMarkerIndex > nonZeroIndex) {
|
|
461
|
-
processedValue = processedValue.slice(nonZeroIndex);
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
// Leading-zero stripping is a typing-time rule ('05' -> '5'). When backspaced,
|
|
466
|
-
// leading zeros before a non-zero digit were already removed above, and an
|
|
467
|
-
// all-zero remainder must keep its zeros (issues #1355/#1578).
|
|
468
|
-
if (precision === 0 && !backspaced) {
|
|
469
|
-
processedValue = this.allowNegativeNumbers
|
|
470
|
-
? processedValue.length > 2 &&
|
|
471
|
-
processedValue[0] === MaskExpression.MINUS &&
|
|
472
|
-
processedValue[1] === MaskExpression.NUMBER_ZERO &&
|
|
473
|
-
processedValue[2] !== this.thousandSeparator &&
|
|
474
|
-
processedValue[2] !== MaskExpression.COMMA &&
|
|
475
|
-
processedValue[2] !== MaskExpression.DOT
|
|
476
|
-
? '-' + processedValue.slice(2, processedValue.length)
|
|
477
|
-
: processedValue[0] === MaskExpression.NUMBER_ZERO &&
|
|
478
|
-
processedValue.length > 1 &&
|
|
479
|
-
processedValue[1] !== this.thousandSeparator &&
|
|
480
|
-
processedValue[1] !== MaskExpression.COMMA &&
|
|
481
|
-
processedValue[1] !== MaskExpression.DOT
|
|
482
|
-
? processedValue.slice(1, processedValue.length)
|
|
483
|
-
: processedValue
|
|
484
|
-
: processedValue.length > 1 &&
|
|
485
|
-
processedValue[0] === MaskExpression.NUMBER_ZERO &&
|
|
486
|
-
processedValue[1] !== this.thousandSeparator &&
|
|
487
|
-
processedValue[1] !== MaskExpression.COMMA &&
|
|
488
|
-
processedValue[1] !== MaskExpression.DOT
|
|
489
|
-
? processedValue.slice(1, processedValue.length)
|
|
490
|
-
: processedValue;
|
|
491
|
-
}
|
|
492
|
-
else {
|
|
493
|
-
if (processedValue[0] === decimalMarker &&
|
|
494
|
-
processedValue.length > 1 &&
|
|
495
|
-
!backspaced) {
|
|
496
|
-
processedValue =
|
|
497
|
-
MaskExpression.NUMBER_ZERO +
|
|
498
|
-
processedValue.slice(0, processedValue.length + 1);
|
|
499
|
-
this.plusOnePosition = true;
|
|
500
|
-
}
|
|
501
|
-
if (processedValue[0] === MaskExpression.NUMBER_ZERO &&
|
|
502
|
-
processedValue[1] !== decimalMarker &&
|
|
503
|
-
processedValue[1] !== this.thousandSeparator &&
|
|
504
|
-
!backspaced) {
|
|
505
|
-
processedValue =
|
|
506
|
-
processedValue.length > 1
|
|
507
|
-
? processedValue.slice(0, 1) +
|
|
508
|
-
decimalMarker +
|
|
509
|
-
processedValue.slice(1, processedValue.length + 1)
|
|
510
|
-
: processedValue;
|
|
511
|
-
this.plusOnePosition = true;
|
|
512
|
-
}
|
|
513
|
-
if (this.allowNegativeNumbers &&
|
|
514
|
-
!backspaced &&
|
|
515
|
-
processedValue[0] === MaskExpression.MINUS &&
|
|
516
|
-
(processedValue[1] === decimalMarker ||
|
|
517
|
-
processedValue[1] === MaskExpression.NUMBER_ZERO)) {
|
|
518
|
-
processedValue =
|
|
519
|
-
processedValue[1] === decimalMarker && processedValue.length > 2
|
|
520
|
-
? processedValue.slice(0, 1) +
|
|
521
|
-
MaskExpression.NUMBER_ZERO +
|
|
522
|
-
processedValue.slice(1, processedValue.length)
|
|
523
|
-
: processedValue[1] === MaskExpression.NUMBER_ZERO &&
|
|
524
|
-
processedValue.length > 2 &&
|
|
525
|
-
processedValue[2] !== decimalMarker
|
|
526
|
-
? processedValue.slice(0, 2) +
|
|
527
|
-
decimalMarker +
|
|
528
|
-
processedValue.slice(2, processedValue.length)
|
|
529
|
-
: processedValue;
|
|
530
|
-
this.plusOnePosition = true;
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
// TODO: we had different rexexps here for the different cases... but tests dont seam to bother - check this
|
|
534
|
-
// separator: no COMMA, dot-sep: no SPACE, COMMA OK, comma-sep: no SPACE, COMMA OK
|
|
535
|
-
const thousandSeparatorCharEscaped = this._charToRegExpExpression(this.thousandSeparator);
|
|
536
|
-
let invalidChars = '@#!$%^&*()_+|~=`{}\\[\\]:\\s,\\.";<>?\\/'.replace(thousandSeparatorCharEscaped, '');
|
|
537
|
-
//.replace(decimalMarkerEscaped, '');
|
|
538
|
-
if (Array.isArray(this.decimalMarker)) {
|
|
539
|
-
for (const marker of this.decimalMarker) {
|
|
540
|
-
invalidChars = invalidChars.replace(this._charToRegExpExpression(marker), MaskExpression.EMPTY_STRING);
|
|
541
|
-
}
|
|
542
|
-
}
|
|
543
|
-
else {
|
|
544
|
-
invalidChars = invalidChars.replace(this._charToRegExpExpression(this.decimalMarker), '');
|
|
545
|
-
}
|
|
546
|
-
const invalidCharRegexp = new RegExp('[' + invalidChars + ']');
|
|
547
|
-
if (processedValue.match(invalidCharRegexp)) {
|
|
548
|
-
processedValue = processedValue.substring(0, processedValue.length - 1);
|
|
549
|
-
}
|
|
550
|
-
processedValue = this.checkInputPrecision(processedValue, precision, this.decimalMarker);
|
|
551
|
-
const strForSep = processedValue.replace(new RegExp(thousandSeparatorCharEscaped, 'g'), '');
|
|
552
|
-
result = this._formatWithSeparators(strForSep, this.thousandSeparator, this.decimalMarker, precision);
|
|
553
|
-
const commaShift = result.indexOf(MaskExpression.COMMA) - processedValue.indexOf(MaskExpression.COMMA);
|
|
554
|
-
const shiftStep = result.length - processedValue.length;
|
|
555
|
-
const backspacedDecimalMarkerWithSeparatorLimit = backspaced &&
|
|
556
|
-
result.length < inputValue.length - this.suffix.length &&
|
|
557
|
-
this.separatorLimit;
|
|
558
|
-
if ((result[processedPosition - 1] === this.thousandSeparator ||
|
|
559
|
-
result[processedPosition - this.prefix.length]) &&
|
|
560
|
-
this.prefix &&
|
|
561
|
-
backspaced) {
|
|
562
|
-
processedPosition = processedPosition - 1;
|
|
563
|
-
}
|
|
564
|
-
else if ((shiftStep > 0 && result[processedPosition] !== this.thousandSeparator) ||
|
|
565
|
-
backspacedDecimalMarkerWithSeparatorLimit) {
|
|
566
|
-
backspaceShift = true;
|
|
567
|
-
let _shift = 0;
|
|
568
|
-
do {
|
|
569
|
-
this._shift.add(processedPosition + _shift);
|
|
570
|
-
_shift++;
|
|
571
|
-
} while (_shift < shiftStep);
|
|
572
|
-
}
|
|
573
|
-
else if (result[processedPosition - 1] === this.thousandSeparator ||
|
|
574
|
-
shiftStep === -4 ||
|
|
575
|
-
shiftStep === -3 ||
|
|
576
|
-
result[processedPosition] === this.thousandSeparator) {
|
|
577
|
-
this._shift.clear();
|
|
578
|
-
this._shift.add(processedPosition - 1);
|
|
579
|
-
}
|
|
580
|
-
else if ((commaShift !== 0 &&
|
|
581
|
-
processedPosition > 0 &&
|
|
582
|
-
!(result.indexOf(MaskExpression.COMMA) >= processedPosition &&
|
|
583
|
-
processedPosition > 3)) ||
|
|
584
|
-
(!(result.indexOf(MaskExpression.DOT) >= processedPosition && processedPosition > 3) &&
|
|
585
|
-
shiftStep <= 0)) {
|
|
586
|
-
this._shift.clear();
|
|
587
|
-
backspaceShift = true;
|
|
588
|
-
shift = shiftStep;
|
|
589
|
-
processedPosition += shiftStep;
|
|
590
|
-
this._shift.add(processedPosition);
|
|
591
|
-
}
|
|
592
|
-
else {
|
|
593
|
-
this._shift.clear();
|
|
594
|
-
}
|
|
595
|
-
}
|
|
596
|
-
else {
|
|
597
|
-
for (
|
|
598
|
-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
599
|
-
let i = 0, inputSymbol = inputArray[0]; i < inputArray.length; i++, inputSymbol = inputArray[i] ?? MaskExpression.EMPTY_STRING) {
|
|
600
|
-
if (cursor === maskExpression.length) {
|
|
601
|
-
break;
|
|
602
|
-
}
|
|
603
|
-
const symbolStarInPattern = MaskExpression.SYMBOL_STAR in this.patterns;
|
|
604
|
-
if (this._checkSymbolMask(inputSymbol, maskExpression[cursor] ?? MaskExpression.EMPTY_STRING) &&
|
|
605
|
-
maskExpression[cursor + 1] === MaskExpression.SYMBOL_QUESTION) {
|
|
606
|
-
result += inputSymbol;
|
|
607
|
-
cursor += 2;
|
|
608
|
-
}
|
|
609
|
-
else if (maskExpression[cursor + 1] === MaskExpression.SYMBOL_STAR &&
|
|
610
|
-
multi &&
|
|
611
|
-
this._checkSymbolMask(inputSymbol, maskExpression[cursor + 2] ?? MaskExpression.EMPTY_STRING)) {
|
|
612
|
-
result += inputSymbol;
|
|
613
|
-
cursor += 3;
|
|
614
|
-
multi = false;
|
|
615
|
-
}
|
|
616
|
-
else if (this._checkSymbolMask(inputSymbol, maskExpression[cursor] ?? MaskExpression.EMPTY_STRING) &&
|
|
617
|
-
maskExpression[cursor + 1] === MaskExpression.SYMBOL_STAR &&
|
|
618
|
-
!symbolStarInPattern) {
|
|
619
|
-
result += inputSymbol;
|
|
620
|
-
multi = true;
|
|
621
|
-
}
|
|
622
|
-
else if (maskExpression[cursor + 1] === MaskExpression.SYMBOL_QUESTION &&
|
|
623
|
-
this._checkSymbolMask(inputSymbol, maskExpression[cursor + 2] ?? MaskExpression.EMPTY_STRING)) {
|
|
624
|
-
result += inputSymbol;
|
|
625
|
-
cursor += 3;
|
|
626
|
-
}
|
|
627
|
-
else if (this._checkSymbolMask(inputSymbol, maskExpression[cursor] ?? MaskExpression.EMPTY_STRING)) {
|
|
628
|
-
if (maskExpression[cursor] === MaskExpression.HOURS) {
|
|
629
|
-
if (this.apm ? Number(inputSymbol) > 9 : Number(inputSymbol) > 2) {
|
|
630
|
-
processedPosition = !this.leadZeroDateTime
|
|
631
|
-
? processedPosition + 1
|
|
632
|
-
: processedPosition;
|
|
633
|
-
cursor += 1;
|
|
634
|
-
this._shiftStep(cursor);
|
|
635
|
-
i--;
|
|
636
|
-
if (this.leadZeroDateTime) {
|
|
637
|
-
result += '0';
|
|
638
|
-
}
|
|
639
|
-
continue;
|
|
640
|
-
}
|
|
641
|
-
}
|
|
642
|
-
if (maskExpression[cursor] === MaskExpression.HOUR) {
|
|
643
|
-
if (this.apm
|
|
644
|
-
? (result.length === 1 && Number(result) > 1) ||
|
|
645
|
-
(result === '1' && Number(inputSymbol) > 2) ||
|
|
646
|
-
(processedValue.slice(cursor - 1, cursor).length === 1 &&
|
|
647
|
-
Number(processedValue.slice(cursor - 1, cursor)) > 2) ||
|
|
648
|
-
(processedValue.slice(cursor - 1, cursor) === '1' &&
|
|
649
|
-
Number(inputSymbol) > 2)
|
|
650
|
-
: (result === '2' && Number(inputSymbol) > 3) ||
|
|
651
|
-
((result.slice(cursor - 2, cursor) === '2' ||
|
|
652
|
-
result.slice(cursor - 3, cursor) === '2' ||
|
|
653
|
-
result.slice(cursor - 4, cursor) === '2' ||
|
|
654
|
-
result.slice(cursor - 1, cursor) === '2') &&
|
|
655
|
-
Number(inputSymbol) > 3 &&
|
|
656
|
-
cursor > 10)) {
|
|
657
|
-
processedPosition = processedPosition + 1;
|
|
658
|
-
cursor += 1;
|
|
659
|
-
i--;
|
|
660
|
-
continue;
|
|
661
|
-
}
|
|
662
|
-
}
|
|
663
|
-
if (maskExpression[cursor] === MaskExpression.MINUTE ||
|
|
664
|
-
maskExpression[cursor] === MaskExpression.SECOND) {
|
|
665
|
-
if (Number(inputSymbol) > 5) {
|
|
666
|
-
processedPosition = !this.leadZeroDateTime
|
|
667
|
-
? processedPosition + 1
|
|
668
|
-
: processedPosition;
|
|
669
|
-
cursor += 1;
|
|
670
|
-
this._shiftStep(cursor);
|
|
671
|
-
i--;
|
|
672
|
-
if (this.leadZeroDateTime) {
|
|
673
|
-
result += '0';
|
|
674
|
-
}
|
|
675
|
-
continue;
|
|
676
|
-
}
|
|
677
|
-
}
|
|
678
|
-
const daysCount = 31;
|
|
679
|
-
const inputValueCursor = processedValue[cursor];
|
|
680
|
-
const inputValueCursorPlusOne = processedValue[cursor + 1];
|
|
681
|
-
const inputValueCursorPlusTwo = processedValue[cursor + 2];
|
|
682
|
-
const inputValueCursorMinusOne = processedValue[cursor - 1];
|
|
683
|
-
const inputValueCursorMinusTwo = processedValue[cursor - 2];
|
|
684
|
-
const inputValueSliceMinusThreeMinusOne = processedValue.slice(cursor - 3, cursor - 1);
|
|
685
|
-
const inputValueSliceMinusOnePlusOne = processedValue.slice(cursor - 1, cursor + 1);
|
|
686
|
-
const inputValueSliceCursorPlusTwo = processedValue.slice(cursor, cursor + 2);
|
|
687
|
-
const inputValueSliceMinusTwoCursor = processedValue.slice(cursor - 2, cursor);
|
|
688
|
-
// Issue #1523: when the date token directly abuts a plain digit
|
|
689
|
-
// token (year-first masks without separators like 00M0d0 or
|
|
690
|
-
// 0000M0d0), the backward-looking windows below read the previous
|
|
691
|
-
// field's digits (year) as day/month digits and skip valid input.
|
|
692
|
-
// Disable only those backward heuristics in that layout.
|
|
693
|
-
const tokenAbutsDigitField = maskExpression[cursor - 1] === MaskExpression.NUMBER_ZERO;
|
|
694
|
-
if (maskExpression[cursor] === MaskExpression.DAY) {
|
|
695
|
-
const maskStartWithMonth = maskExpression.slice(0, 2) === MaskExpression.MONTHS;
|
|
696
|
-
const startWithMonthInput = maskExpression.slice(0, 2) === MaskExpression.MONTHS &&
|
|
697
|
-
this.specialCharacters.includes(inputValueCursorMinusTwo);
|
|
698
|
-
// Issue #1611: `cursor` indexes the mask, `i` indexes the input.
|
|
699
|
-
// On paste/writeValue of a bare digit string the cursor runs
|
|
700
|
-
// ahead of the input by every emitted separator, so cursor-based
|
|
701
|
-
// slices read year digits instead of the day — anchor the day
|
|
702
|
-
// window on the input index in those flows. Keystroke flows keep
|
|
703
|
-
// the historical cursor anchor (with showMaskTyped the value also
|
|
704
|
-
// carries placeholder chars, where the input anchor misreads).
|
|
705
|
-
const dayWindowStart = justPasted || this.writingValue ? i : cursor;
|
|
706
|
-
const dayWindowSlice = processedValue.slice(dayWindowStart, dayWindowStart + 2);
|
|
707
|
-
const dayWindowNext = processedValue[dayWindowStart + 1];
|
|
708
|
-
if ((Number(inputSymbol) > 3 && this.leadZeroDateTime) ||
|
|
709
|
-
(!maskStartWithMonth &&
|
|
710
|
-
(Number(inputValueSliceCursorPlusTwo) > daysCount ||
|
|
711
|
-
(!tokenAbutsDigitField &&
|
|
712
|
-
Number(inputValueSliceMinusOnePlusOne) > daysCount) ||
|
|
713
|
-
this.specialCharacters.includes(inputValueCursorPlusOne))) ||
|
|
714
|
-
(startWithMonthInput
|
|
715
|
-
? Number(inputValueSliceMinusOnePlusOne) > daysCount ||
|
|
716
|
-
(!this.specialCharacters.includes(inputValueCursor) &&
|
|
717
|
-
this.specialCharacters.includes(inputValueCursorPlusTwo)) ||
|
|
718
|
-
this.specialCharacters.includes(inputValueCursor)
|
|
719
|
-
: Number(dayWindowSlice) > daysCount ||
|
|
720
|
-
(this.specialCharacters.includes(dayWindowNext) && !backspaced))) {
|
|
721
|
-
processedPosition = !this.leadZeroDateTime
|
|
722
|
-
? processedPosition + 1
|
|
723
|
-
: processedPosition;
|
|
724
|
-
cursor += 1;
|
|
725
|
-
this._shiftStep(cursor);
|
|
726
|
-
i--;
|
|
727
|
-
if (this.leadZeroDateTime) {
|
|
728
|
-
result += '0';
|
|
729
|
-
}
|
|
730
|
-
continue;
|
|
731
|
-
}
|
|
732
|
-
}
|
|
733
|
-
if (maskExpression[cursor] === MaskExpression.MONTH) {
|
|
734
|
-
const monthsCount = 12;
|
|
735
|
-
// Issue #1513: the backward-looking day/month windows below
|
|
736
|
-
// assume the digits before the MONTH token belong to a DAY
|
|
737
|
-
// field. In year-first masks with separators (e.g. 0000-M0-d0)
|
|
738
|
-
// they read YEAR digits through the separator and shove a
|
|
739
|
-
// spurious leading zero into the month. tokenAbutsDigitField
|
|
740
|
-
// (#1523) only covers separator-less layouts, so derive field
|
|
741
|
-
// ownership from the mask itself: locate the field (maximal
|
|
742
|
-
// run of non-special tokens) immediately preceding this MONTH
|
|
743
|
-
// token — a plain digit run of 3+ characters is a year, not a
|
|
744
|
-
// day, and the day-based heuristics must not fire.
|
|
745
|
-
let precedingFieldEnd = cursor - 1;
|
|
746
|
-
while (precedingFieldEnd >= 0 &&
|
|
747
|
-
this.specialCharacters.includes(maskExpression[precedingFieldEnd])) {
|
|
748
|
-
precedingFieldEnd--;
|
|
749
|
-
}
|
|
750
|
-
let precedingFieldStart = precedingFieldEnd;
|
|
751
|
-
while (precedingFieldStart >= 0 &&
|
|
752
|
-
!this.specialCharacters.includes(maskExpression[precedingFieldStart])) {
|
|
753
|
-
precedingFieldStart--;
|
|
754
|
-
}
|
|
755
|
-
const precedingField = maskExpression.slice(precedingFieldStart + 1, precedingFieldEnd + 1);
|
|
756
|
-
const yearFieldPrecedesMonth = precedingField.length > 2 &&
|
|
757
|
-
precedingField
|
|
758
|
-
.split(MaskExpression.EMPTY_STRING)
|
|
759
|
-
.every((token) => token === MaskExpression.NUMBER_ZERO);
|
|
760
|
-
// mask without day
|
|
761
|
-
const withoutDays = cursor === 0 &&
|
|
762
|
-
(Number(inputSymbol) > 2 ||
|
|
763
|
-
Number(inputValueSliceCursorPlusTwo) > monthsCount ||
|
|
764
|
-
(this.specialCharacters.includes(inputValueCursorPlusOne) &&
|
|
765
|
-
!backspaced));
|
|
766
|
-
// day<10 && month<12 for input
|
|
767
|
-
const specialChart = maskExpression.slice(cursor + 2, cursor + 3);
|
|
768
|
-
const day1monthInput = inputValueSliceMinusThreeMinusOne.includes(specialChart) &&
|
|
769
|
-
maskExpression.includes('d0') &&
|
|
770
|
-
((this.specialCharacters.includes(inputValueCursorMinusTwo) &&
|
|
771
|
-
Number(inputValueSliceMinusOnePlusOne) > monthsCount &&
|
|
772
|
-
!this.specialCharacters.includes(inputValueCursor)) ||
|
|
773
|
-
this.specialCharacters.includes(inputValueCursor));
|
|
774
|
-
// month<12 && day<10 for input
|
|
775
|
-
const day2monthInput = !tokenAbutsDigitField &&
|
|
776
|
-
!yearFieldPrecedesMonth &&
|
|
777
|
-
Number(inputValueSliceMinusThreeMinusOne) <= daysCount &&
|
|
778
|
-
!this.specialCharacters.includes(inputValueSliceMinusThreeMinusOne) &&
|
|
779
|
-
this.specialCharacters.includes(inputValueCursorMinusOne) &&
|
|
780
|
-
(Number(inputValueSliceCursorPlusTwo) > monthsCount ||
|
|
781
|
-
this.specialCharacters.includes(inputValueCursorPlusOne));
|
|
782
|
-
// cursor === 5 && without days
|
|
783
|
-
const day2monthInputDot = (Number(inputValueSliceCursorPlusTwo) > monthsCount && cursor === 5) ||
|
|
784
|
-
(this.specialCharacters.includes(inputValueCursorPlusOne) &&
|
|
785
|
-
cursor === 5);
|
|
786
|
-
// // day<10 && month<12 for paste whole data
|
|
787
|
-
const day1monthPaste = !tokenAbutsDigitField &&
|
|
788
|
-
!yearFieldPrecedesMonth &&
|
|
789
|
-
Number(inputValueSliceMinusThreeMinusOne) > daysCount &&
|
|
790
|
-
!this.specialCharacters.includes(inputValueSliceMinusThreeMinusOne) &&
|
|
791
|
-
!this.specialCharacters.includes(inputValueSliceMinusTwoCursor) &&
|
|
792
|
-
Number(inputValueSliceMinusTwoCursor) > monthsCount &&
|
|
793
|
-
maskExpression.includes('d0');
|
|
794
|
-
// 10<day<31 && month<12 for paste whole data
|
|
795
|
-
const day2monthPaste = !tokenAbutsDigitField &&
|
|
796
|
-
!yearFieldPrecedesMonth &&
|
|
797
|
-
Number(inputValueSliceMinusThreeMinusOne) <= daysCount &&
|
|
798
|
-
!this.specialCharacters.includes(inputValueSliceMinusThreeMinusOne) &&
|
|
799
|
-
!this.specialCharacters.includes(inputValueCursorMinusOne) &&
|
|
800
|
-
Number(inputValueSliceMinusOnePlusOne) > monthsCount;
|
|
801
|
-
if ((Number(inputSymbol) > 1 && this.leadZeroDateTime) ||
|
|
802
|
-
withoutDays ||
|
|
803
|
-
day1monthInput ||
|
|
804
|
-
day2monthPaste ||
|
|
805
|
-
day1monthPaste ||
|
|
806
|
-
day2monthInput ||
|
|
807
|
-
(day2monthInputDot && !this.leadZeroDateTime)) {
|
|
808
|
-
processedPosition = !this.leadZeroDateTime
|
|
809
|
-
? processedPosition + 1
|
|
810
|
-
: processedPosition;
|
|
811
|
-
cursor += 1;
|
|
812
|
-
this._shiftStep(cursor);
|
|
813
|
-
i--;
|
|
814
|
-
if (this.leadZeroDateTime) {
|
|
815
|
-
result += '0';
|
|
816
|
-
}
|
|
817
|
-
continue;
|
|
818
|
-
}
|
|
819
|
-
}
|
|
820
|
-
result += inputSymbol;
|
|
821
|
-
cursor++;
|
|
822
|
-
}
|
|
823
|
-
else if (this.specialCharacters.includes(inputSymbol) &&
|
|
824
|
-
maskExpression[cursor] === inputSymbol) {
|
|
825
|
-
result += inputSymbol;
|
|
826
|
-
cursor++;
|
|
827
|
-
}
|
|
828
|
-
else if (this.specialCharacters.indexOf(maskExpression[cursor] ?? MaskExpression.EMPTY_STRING) !== -1) {
|
|
829
|
-
result += maskExpression[cursor];
|
|
830
|
-
cursor++;
|
|
831
|
-
this._shiftStep(cursor);
|
|
832
|
-
i--;
|
|
833
|
-
}
|
|
834
|
-
else if (maskExpression[cursor] === MaskExpression.NUMBER_NINE &&
|
|
835
|
-
this.showMaskTyped) {
|
|
836
|
-
this._shiftStep(cursor);
|
|
837
|
-
}
|
|
838
|
-
else if (this.patterns[maskExpression[cursor] ?? MaskExpression.EMPTY_STRING] &&
|
|
839
|
-
this.patterns[maskExpression[cursor] ?? MaskExpression.EMPTY_STRING]?.optional) {
|
|
840
|
-
// If the input symbol is whitespace or doesn't match the pattern,
|
|
841
|
-
// skip it without consuming the mask position
|
|
842
|
-
if (inputSymbol.trim() === MaskExpression.EMPTY_STRING) {
|
|
843
|
-
// Skip whitespace input, don't advance mask cursor
|
|
844
|
-
continue;
|
|
845
|
-
}
|
|
846
|
-
if (!!inputArray[cursor] &&
|
|
847
|
-
maskExpression !== '099.099.099.099' &&
|
|
848
|
-
maskExpression !== '000.000.000-00' &&
|
|
849
|
-
maskExpression !== '00.000.000/0000-00' &&
|
|
850
|
-
!maskExpression.match(/^9+\.0+$/) &&
|
|
851
|
-
!this.patterns[maskExpression[cursor] ?? MaskExpression.EMPTY_STRING]
|
|
852
|
-
?.optional) {
|
|
853
|
-
result += inputArray[cursor];
|
|
854
|
-
}
|
|
855
|
-
if (maskExpression.includes(MaskExpression.NUMBER_NINE + MaskExpression.SYMBOL_STAR) &&
|
|
856
|
-
maskExpression.includes(MaskExpression.NUMBER_ZERO + MaskExpression.SYMBOL_STAR)) {
|
|
857
|
-
cursor++;
|
|
858
|
-
}
|
|
859
|
-
cursor++;
|
|
860
|
-
i--;
|
|
861
|
-
}
|
|
862
|
-
else if (this.maskExpression[cursor + 1] === MaskExpression.SYMBOL_STAR &&
|
|
863
|
-
// A typed char that exactly matches the mask's literal char at cursor+2
|
|
864
|
-
// (e.g. the '@' in 'A*@A*.A*') always terminates the 'A*' run, regardless
|
|
865
|
-
// of whether that literal is registered in `specialCharacters` — an
|
|
866
|
-
// explicitly empty `specialCharacters` list must not make mask literals
|
|
867
|
-
// unmatchable (#1512).
|
|
868
|
-
(this._findSpecialChar(this.maskExpression[cursor + 2] ?? MaskExpression.EMPTY_STRING)
|
|
869
|
-
? this._findSpecialChar(inputSymbol) === this.maskExpression[cursor + 2]
|
|
870
|
-
: inputSymbol === this.maskExpression[cursor + 2]) &&
|
|
871
|
-
multi) {
|
|
872
|
-
cursor += 3;
|
|
873
|
-
result += inputSymbol;
|
|
874
|
-
}
|
|
875
|
-
else if (this.maskExpression[cursor + 1] === MaskExpression.SYMBOL_QUESTION &&
|
|
876
|
-
(this._findSpecialChar(this.maskExpression[cursor + 2] ?? MaskExpression.EMPTY_STRING)
|
|
877
|
-
? this._findSpecialChar(inputSymbol) === this.maskExpression[cursor + 2]
|
|
878
|
-
: inputSymbol === this.maskExpression[cursor + 2]) &&
|
|
879
|
-
multi) {
|
|
880
|
-
cursor += 3;
|
|
881
|
-
result += inputSymbol;
|
|
882
|
-
}
|
|
883
|
-
else if (this.showMaskTyped &&
|
|
884
|
-
this.specialCharacters.indexOf(inputSymbol) < 0 &&
|
|
885
|
-
inputSymbol !== this.placeHolderCharacter &&
|
|
886
|
-
this.placeHolderCharacter.length === 1) {
|
|
887
|
-
stepBack = true;
|
|
888
|
-
}
|
|
889
|
-
}
|
|
890
|
-
}
|
|
983
|
+
// Per-mask-type dispatch (IP/CPF_CNPJ pre-processors → generic loop; PERCENT and
|
|
984
|
+
// SEPARATOR terminal handlers; generic pattern loop as fallback). See
|
|
985
|
+
// ./mask-handlers/mask-handlers.registry.ts.
|
|
986
|
+
const state = {
|
|
987
|
+
processedValue,
|
|
988
|
+
processedPosition,
|
|
989
|
+
cursor,
|
|
990
|
+
result,
|
|
991
|
+
multi,
|
|
992
|
+
backspaceShift,
|
|
993
|
+
shift,
|
|
994
|
+
stepBack,
|
|
995
|
+
maskExpression,
|
|
996
|
+
};
|
|
997
|
+
const resolved = dispatchMaskHandler(this, state, {
|
|
998
|
+
inputValue,
|
|
999
|
+
position,
|
|
1000
|
+
justPasted,
|
|
1001
|
+
backspaced,
|
|
1002
|
+
cb,
|
|
1003
|
+
inputArray,
|
|
1004
|
+
arr,
|
|
1005
|
+
startsWithPrefix,
|
|
1006
|
+
prefixAlreadyRemovedByCaller,
|
|
1007
|
+
});
|
|
1008
|
+
if (typeof resolved.earlyReturn === 'string') {
|
|
1009
|
+
return resolved.earlyReturn;
|
|
1010
|
+
}
|
|
1011
|
+
processedValue = resolved.processedValue;
|
|
1012
|
+
processedPosition = resolved.processedPosition;
|
|
1013
|
+
cursor = resolved.cursor;
|
|
1014
|
+
result = resolved.result;
|
|
1015
|
+
backspaceShift = resolved.backspaceShift;
|
|
1016
|
+
shift = resolved.shift;
|
|
1017
|
+
stepBack = resolved.stepBack;
|
|
1018
|
+
// eslint-disable-next-line no-param-reassign
|
|
1019
|
+
maskExpression = resolved.maskExpression;
|
|
891
1020
|
if (result[processedPosition - 1] &&
|
|
892
1021
|
result.length + 1 === maskExpression.length &&
|
|
893
1022
|
this.specialCharacters.indexOf(maskExpression[maskExpression.length - 1] ?? MaskExpression.EMPTY_STRING) !== -1) {
|
|
@@ -962,8 +1091,8 @@ class NgxMaskApplierService {
|
|
|
962
1091
|
false);
|
|
963
1092
|
}
|
|
964
1093
|
_formatWithSeparators = (str, thousandSeparatorChar, decimalChars, precision) => {
|
|
965
|
-
let x
|
|
966
|
-
let decimalChar
|
|
1094
|
+
let x;
|
|
1095
|
+
let decimalChar;
|
|
967
1096
|
if (Array.isArray(decimalChars)) {
|
|
968
1097
|
const regExp = new RegExp(decimalChars.map((v) => ('[\\^$.|?*+()'.indexOf(v) >= 0 ? `\\${v}` : v)).join('|'));
|
|
969
1098
|
x = str.split(regExp);
|
|
@@ -1066,7 +1195,9 @@ class NgxMaskApplierService {
|
|
|
1066
1195
|
let processedInputValue = inputValue;
|
|
1067
1196
|
let processedDecimalMarker = decimalMarker;
|
|
1068
1197
|
if (precision < Infinity) {
|
|
1069
|
-
//
|
|
1198
|
+
// With an array decimalMarker (default ['.', ',']), pick the first configured
|
|
1199
|
+
// marker that isn't also the thousandSeparator — mirrors the same fallback
|
|
1200
|
+
// resolution used in separator.handler.ts (no marker typed yet in this value).
|
|
1070
1201
|
if (Array.isArray(processedDecimalMarker)) {
|
|
1071
1202
|
const marker = processedDecimalMarker.find((dm) => dm !== this.thousandSeparator);
|
|
1072
1203
|
processedDecimalMarker = marker ? marker : processedDecimalMarker[0];
|
|
@@ -1086,13 +1217,13 @@ class NgxMaskApplierService {
|
|
|
1086
1217
|
return processedInputValue;
|
|
1087
1218
|
};
|
|
1088
1219
|
_stripToDecimal(str) {
|
|
1220
|
+
const isDecimalMarkerChar = (char) => char === MaskExpression.DOT || char === MaskExpression.COMMA;
|
|
1089
1221
|
return str
|
|
1090
1222
|
.split(MaskExpression.EMPTY_STRING)
|
|
1091
1223
|
.filter((i, idx) => {
|
|
1092
1224
|
const isDecimalMarker = typeof this.decimalMarker === 'string'
|
|
1093
1225
|
? i === this.decimalMarker
|
|
1094
|
-
:
|
|
1095
|
-
this.decimalMarker.includes(i);
|
|
1226
|
+
: isDecimalMarkerChar(i) && this.decimalMarker.includes(i);
|
|
1096
1227
|
return (i.match('^-?\\d') ||
|
|
1097
1228
|
i === this.thousandSeparator ||
|
|
1098
1229
|
isDecimalMarker ||
|
|
@@ -1171,10 +1302,10 @@ class NgxMaskApplierService {
|
|
|
1171
1302
|
nonZeroIndex,
|
|
1172
1303
|
};
|
|
1173
1304
|
}
|
|
1174
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.
|
|
1175
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.
|
|
1305
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: NgxMaskApplierService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
1306
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: NgxMaskApplierService });
|
|
1176
1307
|
}
|
|
1177
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.
|
|
1308
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: NgxMaskApplierService, decorators: [{
|
|
1178
1309
|
type: Injectable
|
|
1179
1310
|
}] });
|
|
1180
1311
|
|
|
@@ -1231,6 +1362,27 @@ class NgxMaskService extends NgxMaskApplierService {
|
|
|
1231
1362
|
this.keepCharacterPositionsHandled = null;
|
|
1232
1363
|
// If no mask expression, return the input value or the actual value
|
|
1233
1364
|
if (!maskExpression) {
|
|
1365
|
+
// A RECONFIGURATION to an empty mask (mask was non-empty, now swapped to '') must
|
|
1366
|
+
// revert the display/model to the raw unmasked value, not the stale masked
|
|
1367
|
+
// `actualValue` from the mask that no longer applies (#1616: mask signal set to ''
|
|
1368
|
+
// on a populated control left the masked display/FormControl unchanged forever,
|
|
1369
|
+
// since this early return short-circuited before the `maskChanged`/emit logic
|
|
1370
|
+
// below ever ran). Only strip when a mask genuinely changed — a directive with no
|
|
1371
|
+
// mask ever configured must keep returning verbatim `actualValue`/`inputValue`.
|
|
1372
|
+
if (this.maskChanged) {
|
|
1373
|
+
const rawValue = this.removeMask(inputValue);
|
|
1374
|
+
this.previousValue = this.currentValue;
|
|
1375
|
+
this.currentValue = rawValue;
|
|
1376
|
+
this.actualValue = rawValue;
|
|
1377
|
+
this._emitValue = this.previousValue !== this.currentValue;
|
|
1378
|
+
if (this._emitValue && this.triggerOnMaskChange) {
|
|
1379
|
+
this.formControlResult(rawValue);
|
|
1380
|
+
}
|
|
1381
|
+
if (!this.triggerOnMaskChange) {
|
|
1382
|
+
this.maskChanged = false;
|
|
1383
|
+
}
|
|
1384
|
+
return rawValue;
|
|
1385
|
+
}
|
|
1234
1386
|
return inputValue !== this.actualValue ? this.actualValue : inputValue;
|
|
1235
1387
|
}
|
|
1236
1388
|
// #1492: a numeric FormControl value like 0.0000007 or 1e21 stringifies to
|
|
@@ -1336,10 +1488,6 @@ class NgxMaskService extends NgxMaskApplierService {
|
|
|
1336
1488
|
actualResult = [];
|
|
1337
1489
|
}
|
|
1338
1490
|
}
|
|
1339
|
-
// Remove mask if showMaskTyped is true and hiddenInput is false
|
|
1340
|
-
if (this.showMaskTyped && !this.hiddenInput) {
|
|
1341
|
-
newInputValue = this.removeMask(inputValue);
|
|
1342
|
-
}
|
|
1343
1491
|
// Handle actual value length
|
|
1344
1492
|
if (this.actualValue.length) {
|
|
1345
1493
|
if (actualResult.length < inputValue.length) {
|
|
@@ -1937,9 +2085,8 @@ class NgxMaskService extends NgxMaskApplierService {
|
|
|
1937
2085
|
}
|
|
1938
2086
|
return false;
|
|
1939
2087
|
}
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
const matcher = maskExpretion.match(new RegExp(`^separator\\.([^d]*)`));
|
|
2088
|
+
_retrieveSeparatorPrecision(maskExpression) {
|
|
2089
|
+
const matcher = maskExpression.match(new RegExp(`^separator\\.([^d]*)`));
|
|
1943
2090
|
return matcher ? Number(matcher[1]) : null;
|
|
1944
2091
|
}
|
|
1945
2092
|
_checkPrecision(separatorExpression, separatorValue) {
|
|
@@ -2039,10 +2186,10 @@ class NgxMaskService extends NgxMaskApplierService {
|
|
|
2039
2186
|
currentLocaleDecimalMarker() {
|
|
2040
2187
|
return MaskExpression.DOT;
|
|
2041
2188
|
}
|
|
2042
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.
|
|
2043
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.
|
|
2189
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: NgxMaskService, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
|
|
2190
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: NgxMaskService });
|
|
2044
2191
|
}
|
|
2045
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.
|
|
2192
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: NgxMaskService, decorators: [{
|
|
2046
2193
|
type: Injectable
|
|
2047
2194
|
}] });
|
|
2048
2195
|
|
|
@@ -2423,7 +2570,7 @@ class NgxMaskDirective {
|
|
|
2423
2570
|
this._hasPendingInitialValue = false;
|
|
2424
2571
|
const pendingValue = this._pendingInitialValue;
|
|
2425
2572
|
this._pendingInitialValue = null;
|
|
2426
|
-
|
|
2573
|
+
this.writeValue(pendingValue);
|
|
2427
2574
|
}
|
|
2428
2575
|
}
|
|
2429
2576
|
}
|
|
@@ -2993,7 +3140,11 @@ class NgxMaskDirective {
|
|
|
2993
3140
|
MaskExpression.NUMBER_ZERO.repeat(precision) +
|
|
2994
3141
|
suffix;
|
|
2995
3142
|
this._maskService.actualValue = el.value;
|
|
2996
|
-
|
|
3143
|
+
// #1634: propagate through the regular formControlResult pipeline (not a
|
|
3144
|
+
// raw onChange) so outputTransformFn runs on this blur-time reformat, same
|
|
3145
|
+
// as every typing-time emission. A raw onChange here sent the masked display
|
|
3146
|
+
// string straight to the model, bypassing outputTransformFn entirely.
|
|
3147
|
+
this._maskService.formControlResult(this._maskService.actualValue);
|
|
2997
3148
|
}
|
|
2998
3149
|
}
|
|
2999
3150
|
this._maskService.clearIfNotMatchFn();
|
|
@@ -3205,7 +3356,7 @@ class NgxMaskDirective {
|
|
|
3205
3356
|
}
|
|
3206
3357
|
}
|
|
3207
3358
|
/** It writes the value in the input */
|
|
3208
|
-
|
|
3359
|
+
writeValue(controlValue) {
|
|
3209
3360
|
if (!this._configApplied && this.mask()) {
|
|
3210
3361
|
// Called before the first ngOnChanges pass configured the mask service (happens with
|
|
3211
3362
|
// Signal Forms' [formField], whose control-sync instruction runs before sibling
|
|
@@ -3419,8 +3570,23 @@ class NgxMaskDirective {
|
|
|
3419
3570
|
el.selectionStart = Math.min(Math.max(prefixLength, el.selectionStart), inputValueLength - suffixLength);
|
|
3420
3571
|
el.selectionEnd = Math.min(Math.max(prefixLength, el.selectionEnd), inputValueLength - suffixLength);
|
|
3421
3572
|
}
|
|
3422
|
-
/**
|
|
3573
|
+
/**
|
|
3574
|
+
* It disables the input element.
|
|
3575
|
+
*
|
|
3576
|
+
* Mirrors `_writeElementValueSync` (#1305): the service's `formElementProperty` setter
|
|
3577
|
+
* defers ALL DOM writes via `queueMicrotask` to dodge ExpressionChangedAfterItHasBeenChecked
|
|
3578
|
+
* and keep FIFO ordering with config-driven re-renders. For `disabled` specifically, that
|
|
3579
|
+
* deferral leaves `nativeElement.disabled` stale for at least one microtask after Angular
|
|
3580
|
+
* Forms' `setUpControl` calls this method synchronously during init — long enough for a
|
|
3581
|
+
* consumer reading the DOM property in the same synchronous phase (or another deferred write
|
|
3582
|
+
* racing in FIFO order) to observe the wrong value, e.g. an initially-disabled FormControl
|
|
3583
|
+
* whose native input briefly (or, depending on interleaving, persistently) reports
|
|
3584
|
+
* `disabled === false` (#1633). Writing synchronously here closes that gap; the deferred
|
|
3585
|
+
* write below still runs afterwards and re-applies the same final value, preserving the
|
|
3586
|
+
* existing ordering guarantees other call sites rely on.
|
|
3587
|
+
*/
|
|
3423
3588
|
setDisabledState(isDisabled) {
|
|
3589
|
+
this._renderer.setProperty(this._elementRef.nativeElement, 'disabled', isDisabled);
|
|
3424
3590
|
this._maskService.formElementProperty = ['disabled', isDisabled];
|
|
3425
3591
|
}
|
|
3426
3592
|
_applyMask() {
|
|
@@ -3633,8 +3799,8 @@ class NgxMaskDirective {
|
|
|
3633
3799
|
return uniqueCharacters.size === 1;
|
|
3634
3800
|
});
|
|
3635
3801
|
}
|
|
3636
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.
|
|
3637
|
-
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.
|
|
3802
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: NgxMaskDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
3803
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.7", type: NgxMaskDirective, isStandalone: true, selector: "input[mask], textarea[mask]", inputs: { mask: { classPropertyName: "mask", publicName: "mask", isSignal: true, isRequired: false, transformFunction: null }, specialCharacters: { classPropertyName: "specialCharacters", publicName: "specialCharacters", isSignal: true, isRequired: false, transformFunction: null }, patterns: { classPropertyName: "patterns", publicName: "patterns", isSignal: true, isRequired: false, transformFunction: null }, prefix: { classPropertyName: "prefix", publicName: "prefix", isSignal: true, isRequired: false, transformFunction: null }, suffix: { classPropertyName: "suffix", publicName: "suffix", isSignal: true, isRequired: false, transformFunction: null }, thousandSeparator: { classPropertyName: "thousandSeparator", publicName: "thousandSeparator", isSignal: true, isRequired: false, transformFunction: null }, decimalMarker: { classPropertyName: "decimalMarker", publicName: "decimalMarker", isSignal: true, isRequired: false, transformFunction: null }, dropSpecialCharacters: { classPropertyName: "dropSpecialCharacters", publicName: "dropSpecialCharacters", isSignal: true, isRequired: false, transformFunction: null }, hiddenInput: { classPropertyName: "hiddenInput", publicName: "hiddenInput", isSignal: true, isRequired: false, transformFunction: null }, showMaskTyped: { classPropertyName: "showMaskTyped", publicName: "showMaskTyped", isSignal: true, isRequired: false, transformFunction: null }, placeHolderCharacter: { classPropertyName: "placeHolderCharacter", publicName: "placeHolderCharacter", isSignal: true, isRequired: false, transformFunction: null }, shownMaskExpression: { classPropertyName: "shownMaskExpression", publicName: "shownMaskExpression", isSignal: true, isRequired: false, transformFunction: null }, clearIfNotMatch: { classPropertyName: "clearIfNotMatch", publicName: "clearIfNotMatch", isSignal: true, isRequired: false, transformFunction: null }, validation: { classPropertyName: "validation", publicName: "validation", isSignal: true, isRequired: false, transformFunction: null }, separatorLimit: { classPropertyName: "separatorLimit", publicName: "separatorLimit", isSignal: true, isRequired: false, transformFunction: null }, typeFromDecimals: { classPropertyName: "typeFromDecimals", publicName: "typeFromDecimals", isSignal: true, isRequired: false, transformFunction: null }, allowNegativeNumbers: { classPropertyName: "allowNegativeNumbers", publicName: "allowNegativeNumbers", isSignal: true, isRequired: false, transformFunction: null }, leadZeroDateTime: { classPropertyName: "leadZeroDateTime", publicName: "leadZeroDateTime", isSignal: true, isRequired: false, transformFunction: null }, leadZero: { classPropertyName: "leadZero", publicName: "leadZero", isSignal: true, isRequired: false, transformFunction: null }, triggerOnMaskChange: { classPropertyName: "triggerOnMaskChange", publicName: "triggerOnMaskChange", isSignal: true, isRequired: false, transformFunction: null }, apm: { classPropertyName: "apm", publicName: "apm", isSignal: true, isRequired: false, transformFunction: null }, inputTransformFn: { classPropertyName: "inputTransformFn", publicName: "inputTransformFn", isSignal: true, isRequired: false, transformFunction: null }, outputTransformFn: { classPropertyName: "outputTransformFn", publicName: "outputTransformFn", isSignal: true, isRequired: false, transformFunction: null }, keepCharacterPositions: { classPropertyName: "keepCharacterPositions", publicName: "keepCharacterPositions", isSignal: true, isRequired: false, transformFunction: null }, instantPrefix: { classPropertyName: "instantPrefix", publicName: "instantPrefix", isSignal: true, isRequired: false, transformFunction: null }, defaultValueOnBlur: { classPropertyName: "defaultValueOnBlur", publicName: "defaultValueOnBlur", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, touched: { classPropertyName: "touched", publicName: "touched", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", touched: "touchedChange", maskFilled: "maskFilled" }, host: { listeners: { "paste": "onPaste()", "focus": "onFocus()", "ngModelChange": "onModelChange($event)", "input": "onInput($event)", "compositionstart": "onCompositionStart()", "compositionend": "onCompositionEnd($event)", "blur": "onBlur($event)", "click": "onClick($event)", "keydown": "onKeyDown($event)" } }, providers: [
|
|
3638
3804
|
{
|
|
3639
3805
|
provide: NG_VALUE_ACCESSOR,
|
|
3640
3806
|
useExisting: NgxMaskDirective,
|
|
@@ -3648,7 +3814,7 @@ class NgxMaskDirective {
|
|
|
3648
3814
|
NgxMaskService,
|
|
3649
3815
|
], exportAs: ["mask", "ngxMask"], usesOnChanges: true, ngImport: i0 });
|
|
3650
3816
|
}
|
|
3651
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.
|
|
3817
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: NgxMaskDirective, decorators: [{
|
|
3652
3818
|
type: Directive,
|
|
3653
3819
|
args: [{
|
|
3654
3820
|
selector: 'input[mask], textarea[mask]',
|
|
@@ -3786,10 +3952,10 @@ class NgxMaskPipe {
|
|
|
3786
3952
|
});
|
|
3787
3953
|
}
|
|
3788
3954
|
}
|
|
3789
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.
|
|
3790
|
-
static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "22.0.
|
|
3955
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: NgxMaskPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
|
|
3956
|
+
static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "22.0.7", ngImport: i0, type: NgxMaskPipe, isStandalone: true, name: "mask" });
|
|
3791
3957
|
}
|
|
3792
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.
|
|
3958
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: NgxMaskPipe, decorators: [{
|
|
3793
3959
|
type: Pipe,
|
|
3794
3960
|
args: [{
|
|
3795
3961
|
name: 'mask',
|