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.
@@ -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
- let multi = false;
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
- const isCpfCnpjAlpha = maskExpression === MaskExpression.CPF_CNPJ_ALPHA;
275
- if (maskExpression === MaskExpression.CPF_CNPJ || isCpfCnpjAlpha) {
276
- this.cpfCnpjError = arr.length !== 11 && arr.length !== 14;
277
- const valueHasAnyLetter = /[a-zA-Z]/.test(processedValue);
278
- if (valueHasAnyLetter && isCpfCnpjAlpha) {
279
- // eslint-disable-next-line no-param-reassign
280
- maskExpression = 'AA.AAA.AAA/AAAA-00';
281
- }
282
- else if (arr.length > 11) {
283
- // eslint-disable-next-line no-param-reassign
284
- maskExpression = isCpfCnpjAlpha ? 'AA.AAA.AAA/AAAA-00' : '00.000.000/0000-00';
285
- }
286
- else {
287
- // eslint-disable-next-line no-param-reassign
288
- maskExpression = '000.000.000-00';
289
- }
290
- }
291
- if (maskExpression.startsWith(MaskExpression.PERCENT)) {
292
- if (processedValue.match('[a-z]|[A-Z]') ||
293
- // eslint-disable-next-line no-useless-escape
294
- (processedValue.match(/[-!$%^&*()_+|~=`{}\[\]:";'<>?,\/.]/) && !backspaced)) {
295
- processedValue = this._stripToDecimal(processedValue);
296
- const precision = this.getPrecision(maskExpression);
297
- processedValue = this.checkInputPrecision(processedValue, precision, this.decimalMarker);
298
- }
299
- const decimalMarker = typeof this.decimalMarker === 'string' ? this.decimalMarker : MaskExpression.DOT;
300
- if (processedValue.indexOf(decimalMarker) > 0 &&
301
- !this.percentage(processedValue.substring(0, processedValue.indexOf(decimalMarker)))) {
302
- let base = processedValue.substring(0, processedValue.indexOf(decimalMarker) - 1);
303
- if (this.allowNegativeNumbers &&
304
- processedValue.slice(cursor, cursor + 1) === MaskExpression.MINUS &&
305
- !backspaced) {
306
- base = processedValue.substring(0, processedValue.indexOf(decimalMarker));
307
- }
308
- processedValue = `${base}${processedValue.substring(processedValue.indexOf(decimalMarker), processedValue.length)}`;
309
- }
310
- let value = '';
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
- // TODO need think about decimalMarker
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
- : // TODO (inepipenko) use utility type
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.5", ngImport: i0, type: NgxMaskApplierService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1175
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: NgxMaskApplierService });
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.5", ngImport: i0, type: NgxMaskApplierService, decorators: [{
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
- // TODO should think about helpers or separting decimal precision to own property
1941
- _retrieveSeparatorPrecision(maskExpretion) {
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.5", ngImport: i0, type: NgxMaskService, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
2043
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: NgxMaskService });
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.5", ngImport: i0, type: NgxMaskService, decorators: [{
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
- void this.writeValue(pendingValue);
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
- this.onChange(this._maskService.actualValue);
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
- async writeValue(controlValue) {
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
- /** It disables the input element */
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.5", ngImport: i0, type: NgxMaskDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
3637
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.5", 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: [
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.5", ngImport: i0, type: NgxMaskDirective, decorators: [{
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.5", ngImport: i0, type: NgxMaskPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
3790
- static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "22.0.5", ngImport: i0, type: NgxMaskPipe, isStandalone: true, name: "mask" });
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.5", ngImport: i0, type: NgxMaskPipe, decorators: [{
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',