ngx-mask 21.0.0 → 21.1.0

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.
@@ -1,7 +1,7 @@
1
1
  import * as i0 from '@angular/core';
2
- import { InjectionToken, EventEmitter, inject, Injectable, ElementRef, Renderer2, makeEnvironmentProviders, input, model, output, signal, effect, untracked, HostListener, Directive, Pipe } from '@angular/core';
2
+ import { InjectionToken, EventEmitter, inject, Injectable, signal, ElementRef, Renderer2, makeEnvironmentProviders, input, model, booleanAttribute, output, ChangeDetectorRef, Injector, effect, untracked, HostListener, Directive, Pipe } from '@angular/core';
3
3
  import { DOCUMENT } from '@angular/common';
4
- import { NG_VALUE_ACCESSOR, NG_VALIDATORS } from '@angular/forms';
4
+ import { NgControl, NG_VALUE_ACCESSOR, NG_VALIDATORS } from '@angular/forms';
5
5
 
6
6
  var MaskExpression;
7
7
  (function (MaskExpression) {
@@ -9,6 +9,7 @@ var MaskExpression;
9
9
  MaskExpression["PERCENT"] = "percent";
10
10
  MaskExpression["IP"] = "IP";
11
11
  MaskExpression["CPF_CNPJ"] = "CPF_CNPJ";
12
+ MaskExpression["CPF_CNPJ_ALPHA"] = "CPF_CNPJ_ALPHA";
12
13
  MaskExpression["MONTH"] = "M";
13
14
  MaskExpression["MONTHS"] = "M0";
14
15
  MaskExpression["MINUTE"] = "m";
@@ -168,6 +169,11 @@ class NgxMaskApplierService {
168
169
  showKeepCharacterExp = '';
169
170
  shownMaskExpression = this._config.shownMaskExpression;
170
171
  deletedSpecialCharacter = false;
172
+ /**
173
+ * Whether we are currently in writeValue function, in this case when applying the mask we don't want to trigger onChange function,
174
+ * since writeValue should be a one way only process of writing the DOM value based on the Angular model value.
175
+ */
176
+ writingValue = false;
171
177
  ipError;
172
178
  cpfCnpjError;
173
179
  applyMask(inputValue, maskExpression, position = 0, justPasted = false, backspaced = false,
@@ -185,9 +191,16 @@ class NgxMaskApplierService {
185
191
  let processedValue = inputValue;
186
192
  let processedPosition = position;
187
193
  const startsWithPrefix = processedValue.slice(0, this.prefix.length) === this.prefix;
188
- const pastedFullWithPrefix = justPasted && processedValue.length === this.prefix.length + maskExpression.length;
189
- const looksLikeFullPrefixPaste = processedValue === this.prefix + processedValue.slice(this.prefix.length);
190
- if (startsWithPrefix && (pastedFullWithPrefix || looksLikeFullPrefixPaste)) {
194
+ // On paste with showMaskTyped, NgxMaskService.applyMask has already removed the
195
+ // prefix via removeMask() before delegating here (unless the value consisted of
196
+ // the prefix alone, in which case the raw value falls through). Stripping again
197
+ // would eat leading characters that merely look like the prefix (#1551).
198
+ const prefixAlreadyRemovedByCaller = justPasted &&
199
+ this.showMaskTyped &&
200
+ this.placeHolderCharacter.length === 1 &&
201
+ !this.leadZeroDateTime &&
202
+ processedValue !== this.prefix;
203
+ if (startsWithPrefix && !prefixAlreadyRemovedByCaller) {
191
204
  processedValue = processedValue.slice(this.prefix.length);
192
205
  }
193
206
  if (!!this.suffix && processedValue.length > 0) {
@@ -214,11 +227,17 @@ class NgxMaskApplierService {
214
227
  arr.push(processedValue[i] ?? MaskExpression.EMPTY_STRING);
215
228
  }
216
229
  }
217
- if (maskExpression === MaskExpression.CPF_CNPJ) {
230
+ const isCpfCnpjAlpha = maskExpression === MaskExpression.CPF_CNPJ_ALPHA;
231
+ if (maskExpression === MaskExpression.CPF_CNPJ || isCpfCnpjAlpha) {
218
232
  this.cpfCnpjError = arr.length !== 11 && arr.length !== 14;
219
- if (arr.length > 11) {
233
+ const valueHasAnyLetter = /[a-zA-Z]/.test(processedValue);
234
+ if (valueHasAnyLetter && isCpfCnpjAlpha) {
220
235
  // eslint-disable-next-line no-param-reassign
221
- maskExpression = '00.000.000/0000-00';
236
+ maskExpression = 'AA.AAA.AAA/AAAA-00';
237
+ }
238
+ else if (arr.length > 11) {
239
+ // eslint-disable-next-line no-param-reassign
240
+ maskExpression = isCpfCnpjAlpha ? 'AA.AAA.AAA/AAAA-00' : '00.000.000/0000-00';
222
241
  }
223
242
  else {
224
243
  // eslint-disable-next-line no-param-reassign
@@ -278,15 +297,38 @@ class NgxMaskApplierService {
278
297
  decimalMarker = this.decimalMarker.find((dm) => dm !== this.thousandSeparator);
279
298
  }
280
299
  }
300
+ // Issue #1547: a pasted value may contain grouping separators that are
301
+ // also configured decimal markers (default decimalMarker is ['.', ',']),
302
+ // e.g. '1,234.56'. Only the last marker character can actually be the
303
+ // decimal marker — treat the earlier ones as thousand separators and
304
+ // strip them, otherwise formatting cuts the value off at the first one.
305
+ if (justPasted && Array.isArray(this.decimalMarker)) {
306
+ const markerPositions = [];
307
+ for (let i = 0; i < processedValue.length; i++) {
308
+ const char = processedValue[i];
309
+ if (char !== this.thousandSeparator &&
310
+ this.decimalMarker.includes(char)) {
311
+ markerPositions.push(i);
312
+ }
313
+ }
314
+ if (markerPositions.length > 1) {
315
+ const lastMarkerPosition = markerPositions[markerPositions.length - 1];
316
+ processedValue = processedValue
317
+ .split(MaskExpression.EMPTY_STRING)
318
+ .filter((_, index) => index === lastMarkerPosition || !markerPositions.includes(index))
319
+ .join(MaskExpression.EMPTY_STRING);
320
+ }
321
+ }
281
322
  if (backspaced) {
282
323
  const { decimalMarkerIndex, nonZeroIndex } = this._findFirstNonZeroAndDecimalIndex(processedValue, decimalMarker);
283
324
  const zeroIndexMinus = processedValue[0] === MaskExpression.MINUS;
284
- const zeroIndexNumberZero = processedValue[0] === MaskExpression.NUMBER_ZERO;
285
325
  const zeroIndexDecimalMarker = processedValue[0] === decimalMarker;
286
326
  const firstIndexDecimalMarker = processedValue[1] === decimalMarker;
327
+ // Issues #1355/#1578: an all-zero remainder (e.g. '00' from '500', '00,000'
328
+ // from '100,000') must keep its zeros, matching the ',000,000' case.
329
+ // Only collapse when nothing but a bare decimal marker (or '-.') remains.
287
330
  if ((zeroIndexDecimalMarker && !nonZeroIndex) ||
288
- (zeroIndexMinus && firstIndexDecimalMarker && !nonZeroIndex) ||
289
- (zeroIndexNumberZero && !decimalMarkerIndex && !nonZeroIndex)) {
331
+ (zeroIndexMinus && firstIndexDecimalMarker && !nonZeroIndex)) {
290
332
  processedValue = MaskExpression.NUMBER_ZERO;
291
333
  }
292
334
  if (decimalMarkerIndex &&
@@ -297,7 +339,12 @@ class NgxMaskApplierService {
297
339
  processedValue = MaskExpression.MINUS + processedValue.slice(nonZeroIndex);
298
340
  }
299
341
  }
300
- if (!decimalMarkerIndex && nonZeroIndex && processedValue.length > nonZeroIndex) {
342
+ // Issue #1516: decimalMarkerIndex is 0 when the remainder starts with
343
+ // the decimal marker (',34' after deleting the integer part) — a truthy
344
+ // check would treat it as "no decimal marker" and slice the marker off.
345
+ if (decimalMarkerIndex === null &&
346
+ nonZeroIndex &&
347
+ processedValue.length > nonZeroIndex) {
301
348
  processedValue = zeroIndexMinus
302
349
  ? MaskExpression.MINUS + processedValue.slice(nonZeroIndex)
303
350
  : processedValue.slice(nonZeroIndex);
@@ -311,7 +358,10 @@ class NgxMaskApplierService {
311
358
  }
312
359
  }
313
360
  }
314
- if (precision === 0) {
361
+ // Leading-zero stripping is a typing-time rule ('05' -> '5'). When backspaced,
362
+ // leading zeros before a non-zero digit were already removed above, and an
363
+ // all-zero remainder must keep its zeros (issues #1355/#1578).
364
+ if (precision === 0 && !backspaced) {
315
365
  processedValue = this.allowNegativeNumbers
316
366
  ? processedValue.length > 2 &&
317
367
  processedValue[0] === MaskExpression.MINUS &&
@@ -531,23 +581,39 @@ class NgxMaskApplierService {
531
581
  const inputValueSliceMinusOnePlusOne = processedValue.slice(cursor - 1, cursor + 1);
532
582
  const inputValueSliceCursorPlusTwo = processedValue.slice(cursor, cursor + 2);
533
583
  const inputValueSliceMinusTwoCursor = processedValue.slice(cursor - 2, cursor);
584
+ // Issue #1523: when the date token directly abuts a plain digit
585
+ // token (year-first masks without separators like 00M0d0 or
586
+ // 0000M0d0), the backward-looking windows below read the previous
587
+ // field's digits (year) as day/month digits and skip valid input.
588
+ // Disable only those backward heuristics in that layout.
589
+ const tokenAbutsDigitField = maskExpression[cursor - 1] === MaskExpression.NUMBER_ZERO;
534
590
  if (maskExpression[cursor] === MaskExpression.DAY) {
535
591
  const maskStartWithMonth = maskExpression.slice(0, 2) === MaskExpression.MONTHS;
536
592
  const startWithMonthInput = maskExpression.slice(0, 2) === MaskExpression.MONTHS &&
537
593
  this.specialCharacters.includes(inputValueCursorMinusTwo);
594
+ // Issue #1611: `cursor` indexes the mask, `i` indexes the input.
595
+ // On paste/writeValue of a bare digit string the cursor runs
596
+ // ahead of the input by every emitted separator, so cursor-based
597
+ // slices read year digits instead of the day — anchor the day
598
+ // window on the input index in those flows. Keystroke flows keep
599
+ // the historical cursor anchor (with showMaskTyped the value also
600
+ // carries placeholder chars, where the input anchor misreads).
601
+ const dayWindowStart = justPasted || this.writingValue ? i : cursor;
602
+ const dayWindowSlice = processedValue.slice(dayWindowStart, dayWindowStart + 2);
603
+ const dayWindowNext = processedValue[dayWindowStart + 1];
538
604
  if ((Number(inputSymbol) > 3 && this.leadZeroDateTime) ||
539
605
  (!maskStartWithMonth &&
540
606
  (Number(inputValueSliceCursorPlusTwo) > daysCount ||
541
- Number(inputValueSliceMinusOnePlusOne) > daysCount ||
607
+ (!tokenAbutsDigitField &&
608
+ Number(inputValueSliceMinusOnePlusOne) > daysCount) ||
542
609
  this.specialCharacters.includes(inputValueCursorPlusOne))) ||
543
610
  (startWithMonthInput
544
611
  ? Number(inputValueSliceMinusOnePlusOne) > daysCount ||
545
612
  (!this.specialCharacters.includes(inputValueCursor) &&
546
613
  this.specialCharacters.includes(inputValueCursorPlusTwo)) ||
547
614
  this.specialCharacters.includes(inputValueCursor)
548
- : Number(inputValueSliceCursorPlusTwo) > daysCount ||
549
- (this.specialCharacters.includes(inputValueCursorPlusOne) &&
550
- !backspaced))) {
615
+ : Number(dayWindowSlice) > daysCount ||
616
+ (this.specialCharacters.includes(dayWindowNext) && !backspaced))) {
551
617
  processedPosition = !this.leadZeroDateTime
552
618
  ? processedPosition + 1
553
619
  : processedPosition;
@@ -577,7 +643,8 @@ class NgxMaskApplierService {
577
643
  !this.specialCharacters.includes(inputValueCursor)) ||
578
644
  this.specialCharacters.includes(inputValueCursor));
579
645
  // month<12 && day<10 for input
580
- const day2monthInput = Number(inputValueSliceMinusThreeMinusOne) <= daysCount &&
646
+ const day2monthInput = !tokenAbutsDigitField &&
647
+ Number(inputValueSliceMinusThreeMinusOne) <= daysCount &&
581
648
  !this.specialCharacters.includes(inputValueSliceMinusThreeMinusOne) &&
582
649
  this.specialCharacters.includes(inputValueCursorMinusOne) &&
583
650
  (Number(inputValueSliceCursorPlusTwo) > monthsCount ||
@@ -587,13 +654,15 @@ class NgxMaskApplierService {
587
654
  (this.specialCharacters.includes(inputValueCursorPlusOne) &&
588
655
  cursor === 5);
589
656
  // // day<10 && month<12 for paste whole data
590
- const day1monthPaste = Number(inputValueSliceMinusThreeMinusOne) > daysCount &&
657
+ const day1monthPaste = !tokenAbutsDigitField &&
658
+ Number(inputValueSliceMinusThreeMinusOne) > daysCount &&
591
659
  !this.specialCharacters.includes(inputValueSliceMinusThreeMinusOne) &&
592
660
  !this.specialCharacters.includes(inputValueSliceMinusTwoCursor) &&
593
661
  Number(inputValueSliceMinusTwoCursor) > monthsCount &&
594
662
  maskExpression.includes('d0');
595
663
  // 10<day<31 && month<12 for paste whole data
596
- const day2monthPaste = Number(inputValueSliceMinusThreeMinusOne) <= daysCount &&
664
+ const day2monthPaste = !tokenAbutsDigitField &&
665
+ Number(inputValueSliceMinusThreeMinusOne) <= daysCount &&
597
666
  !this.specialCharacters.includes(inputValueSliceMinusThreeMinusOne) &&
598
667
  !this.specialCharacters.includes(inputValueCursorMinusOne) &&
599
668
  Number(inputValueSliceMinusOnePlusOne) > monthsCount;
@@ -636,6 +705,12 @@ class NgxMaskApplierService {
636
705
  }
637
706
  else if (this.patterns[maskExpression[cursor] ?? MaskExpression.EMPTY_STRING] &&
638
707
  this.patterns[maskExpression[cursor] ?? MaskExpression.EMPTY_STRING]?.optional) {
708
+ // If the input symbol is whitespace or doesn't match the pattern,
709
+ // skip it without consuming the mask position
710
+ if (inputSymbol.trim() === MaskExpression.EMPTY_STRING) {
711
+ // Skip whitespace input, don't advance mask cursor
712
+ continue;
713
+ }
639
714
  if (!!inputArray[cursor] &&
640
715
  maskExpression !== '099.099.099.099' &&
641
716
  maskExpression !== '000.000.000-00' &&
@@ -653,15 +728,22 @@ class NgxMaskApplierService {
653
728
  i--;
654
729
  }
655
730
  else if (this.maskExpression[cursor + 1] === MaskExpression.SYMBOL_STAR &&
656
- this._findSpecialChar(this.maskExpression[cursor + 2] ?? MaskExpression.EMPTY_STRING) &&
657
- this._findSpecialChar(inputSymbol) === this.maskExpression[cursor + 2] &&
731
+ // A typed char that exactly matches the mask's literal char at cursor+2
732
+ // (e.g. the '@' in 'A*@A*.A*') always terminates the 'A*' run, regardless
733
+ // of whether that literal is registered in `specialCharacters` — an
734
+ // explicitly empty `specialCharacters` list must not make mask literals
735
+ // unmatchable (#1512).
736
+ (this._findSpecialChar(this.maskExpression[cursor + 2] ?? MaskExpression.EMPTY_STRING)
737
+ ? this._findSpecialChar(inputSymbol) === this.maskExpression[cursor + 2]
738
+ : inputSymbol === this.maskExpression[cursor + 2]) &&
658
739
  multi) {
659
740
  cursor += 3;
660
741
  result += inputSymbol;
661
742
  }
662
743
  else if (this.maskExpression[cursor + 1] === MaskExpression.SYMBOL_QUESTION &&
663
- this._findSpecialChar(this.maskExpression[cursor + 2] ?? MaskExpression.EMPTY_STRING) &&
664
- this._findSpecialChar(inputSymbol) === this.maskExpression[cursor + 2] &&
744
+ (this._findSpecialChar(this.maskExpression[cursor + 2] ?? MaskExpression.EMPTY_STRING)
745
+ ? this._findSpecialChar(inputSymbol) === this.maskExpression[cursor + 2]
746
+ : inputSymbol === this.maskExpression[cursor + 2]) &&
665
747
  multi) {
666
748
  cursor += 3;
667
749
  result += inputSymbol;
@@ -707,9 +789,20 @@ class NgxMaskApplierService {
707
789
  const isSpecialCharacterMaskFirstSymbol = processedValue.length === 1 &&
708
790
  this.specialCharacters.includes(maskExpression[0]) &&
709
791
  processedValue !== maskExpression[0];
710
- if (!this._checkSymbolMask(processedValue, maskExpression[1]) &&
711
- isSpecialCharacterMaskFirstSymbol) {
712
- return '';
792
+ if (isSpecialCharacterMaskFirstSymbol) {
793
+ // The mask may start with several literal special characters in a row
794
+ // (e.g. '+(000) 000-0000'). A single typed character must be checked
795
+ // against the first PATTERN position of the mask, not literally against
796
+ // index 1 — otherwise a valid digit is rejected and the leading literals
797
+ // are never auto-filled (#1498).
798
+ let firstPatternIndex = 1;
799
+ while (firstPatternIndex < maskExpression.length &&
800
+ this.specialCharacters.includes(maskExpression[firstPatternIndex])) {
801
+ firstPatternIndex++;
802
+ }
803
+ if (!this._checkSymbolMask(processedValue, maskExpression[firstPatternIndex] ?? MaskExpression.EMPTY_STRING)) {
804
+ return '';
805
+ }
713
806
  }
714
807
  if (result.includes(MaskExpression.MINUS) && this.prefix && this.allowNegativeNumbers) {
715
808
  if (backspaced && result === MaskExpression.MINUS) {
@@ -788,11 +881,21 @@ class NgxMaskApplierService {
788
881
  checkAndRemoveSuffix = (inputValue) => {
789
882
  for (let i = this.suffix?.length - 1; i >= 0; i--) {
790
883
  const substr = this.suffix.substring(i, this.suffix?.length);
791
- if (inputValue.includes(substr) &&
884
+ if (inputValue.endsWith(substr) &&
792
885
  i !== this.suffix?.length - 1 &&
886
+ // A partial suffix tail (i > 0) that makes up the WHOLE value is a
887
+ // leftover of the displayed suffix only when the previous rendered
888
+ // value ended with the suffix AND the edit shrank the value to (or
889
+ // below) the old value-part length, i.e. it was a deletion of the
890
+ // suffix head. Otherwise it is fresh user input that merely collides
891
+ // with the suffix text and must be kept (#1495).
892
+ (i === 0 ||
893
+ inputValue.length > substr.length ||
894
+ (this.actualValue.endsWith(this.suffix) &&
895
+ inputValue.length <= this.actualValue.length - this.suffix.length)) &&
793
896
  (i - 1 < 0 ||
794
- !inputValue.includes(this.suffix.substring(i - 1, this.suffix?.length)))) {
795
- return inputValue.replace(substr, MaskExpression.EMPTY_STRING);
897
+ !inputValue.endsWith(this.suffix.substring(i - 1, this.suffix?.length)))) {
898
+ return inputValue.slice(0, inputValue.length - substr.length);
796
899
  }
797
900
  }
798
901
  return inputValue;
@@ -906,10 +1009,10 @@ class NgxMaskApplierService {
906
1009
  nonZeroIndex,
907
1010
  };
908
1011
  }
909
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.1", ngImport: i0, type: NgxMaskApplierService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
910
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.1", ngImport: i0, type: NgxMaskApplierService });
1012
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: NgxMaskApplierService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1013
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: NgxMaskApplierService });
911
1014
  }
912
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.1", ngImport: i0, type: NgxMaskApplierService, decorators: [{
1015
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: NgxMaskApplierService, decorators: [{
913
1016
  type: Injectable
914
1017
  }] });
915
1018
 
@@ -922,12 +1025,20 @@ class NgxMaskService extends NgxMaskApplierService {
922
1025
  maskExpressionArray = [];
923
1026
  previousValue = '';
924
1027
  currentValue = '';
1028
+ // `writingValue` is declared on NgxMaskApplierService: applyMask needs it to
1029
+ // distinguish the writeValue flow from keystroke flows (#1611).
1030
+ isInitialized = false;
925
1031
  /**
926
- * Whether we are currently in writeValue function, in this case when applying the mask we don't want to trigger onChange function,
927
- * since writeValue should be a one way only process of writing the DOM value based on the Angular model value.
1032
+ * Set by the directive's keepCharacterPositions handling for the current edit:
1033
+ * true — the directive fully resolved the resulting display value into actualValue,
1034
+ * so applyMask must short-circuit and render actualValue as-is;
1035
+ * false — the edit must flow through regular masking (no short-circuit);
1036
+ * null — the directive was not involved in this applyMask call (legacy behavior).
1037
+ * Consumed and reset by applyMask. This lets keepCharacterPositions work without
1038
+ * showMaskTyped (#1545, #1543).
928
1039
  */
929
- writingValue = false;
930
- isInitialized = false;
1040
+ keepCharacterPositionsHandled = null;
1041
+ _isFocused = signal(false, ...(ngDevMode ? [{ debugName: "_isFocused" }] : /* istanbul ignore next */ []));
931
1042
  _emitValue = false;
932
1043
  _start;
933
1044
  _end;
@@ -950,10 +1061,35 @@ class NgxMaskService extends NgxMaskApplierService {
950
1061
  applyMask(inputValue, maskExpression, position = 0, justPasted = false, backspaced = false,
951
1062
  // eslint-disable-next-line @typescript-eslint/no-empty-function
952
1063
  cb = () => { }) {
1064
+ // Consume the directive's keepCharacterPositions verdict for this edit (see the
1065
+ // keepCharacterPositionsHandled doc). Reset immediately so it never leaks into
1066
+ // applyMask calls that do not originate from the directive's input handling.
1067
+ const kcpHandled = this.keepCharacterPositionsHandled;
1068
+ this.keepCharacterPositionsHandled = null;
953
1069
  // If no mask expression, return the input value or the actual value
954
1070
  if (!maskExpression) {
955
1071
  return inputValue !== this.actualValue ? this.actualValue : inputValue;
956
1072
  }
1073
+ // #1492: a numeric FormControl value like 0.0000007 or 1e21 stringifies to
1074
+ // exponential notation ('7e-7'); separator masking would strip the 'e'/'-' and
1075
+ // render garbage ('77'). Expand scientific notation to plain decimal form before
1076
+ // masking. Covers writeValue-driven flows and the pipe; a keystroke value can
1077
+ // never match, since separator masking never lets a letter through.
1078
+ if (maskExpression.startsWith(MaskExpression.SEPARATOR) &&
1079
+ inputValue &&
1080
+ /\d[eE][+-]?\d/.test(inputValue)) {
1081
+ const expandedNumber = Number(this._replaceDecimalMarkerToDot(inputValue));
1082
+ if (!Number.isNaN(expandedNumber)) {
1083
+ // eslint-disable-next-line no-param-reassign
1084
+ inputValue = this._toPlainDecimalString(expandedNumber);
1085
+ if (this.decimalMarker === MaskExpression.COMMA ||
1086
+ (Array.isArray(this.decimalMarker) &&
1087
+ this.thousandSeparator === MaskExpression.DOT)) {
1088
+ // eslint-disable-next-line no-param-reassign
1089
+ inputValue = inputValue.replace(MaskExpression.DOT, MaskExpression.COMMA);
1090
+ }
1091
+ }
1092
+ }
957
1093
  // Show mask in input if required
958
1094
  this.maskIsShown = this.showMaskTyped
959
1095
  ? this.showMaskInInput()
@@ -962,7 +1098,9 @@ class NgxMaskService extends NgxMaskApplierService {
962
1098
  if (this.maskExpression === MaskExpression.IP && this.showMaskTyped) {
963
1099
  this.maskIsShown = this.showMaskInInput(inputValue || MaskExpression.HASH);
964
1100
  }
965
- if (this.maskExpression === MaskExpression.CPF_CNPJ && this.showMaskTyped) {
1101
+ const isCpfCnpjMask = this.maskExpression === MaskExpression.CPF_CNPJ ||
1102
+ this.maskExpression === MaskExpression.CPF_CNPJ_ALPHA;
1103
+ if (isCpfCnpjMask && this.showMaskTyped) {
966
1104
  this.maskIsShown = this.showMaskInInput(inputValue || MaskExpression.HASH);
967
1105
  }
968
1106
  // Handle empty input value with mask typed
@@ -979,7 +1117,11 @@ class NgxMaskService extends NgxMaskApplierService {
979
1117
  if ((this.hiddenInput ||
980
1118
  (inputValue && inputValue.indexOf(MaskExpression.SYMBOL_STAR) >= 0)) &&
981
1119
  !this.writingValue) {
982
- let actualResult = inputValue && inputValue.length === 1
1120
+ // Seed from the raw input only when the user typed the FIRST character (the
1121
+ // single char is the raw symbol, actualValue is empty/stale). On backspace a
1122
+ // length-1 value is the remaining masked display (e.g. '*'), so the hidden
1123
+ // characters must come from actualValue or the deletion destroys both (#1612).
1124
+ let actualResult = inputValue && inputValue.length === 1 && !backspaced
983
1125
  ? inputValue.split(MaskExpression.EMPTY_STRING)
984
1126
  : this.actualValue.split(MaskExpression.EMPTY_STRING);
985
1127
  // Handle backspace
@@ -1079,8 +1221,10 @@ class NgxMaskService extends NgxMaskApplierService {
1079
1221
  newInputValue =
1080
1222
  Boolean(newInputValue) && newInputValue.length ? newInputValue : inputValue;
1081
1223
  }
1082
- // Handle showMaskTyped and keepCharacterPositions
1083
- if (this.showMaskTyped &&
1224
+ // Handle keepCharacterPositions: when the directive resolved the display for this
1225
+ // edit (kcpHandled === true), or legacily whenever showMaskTyped is on and the
1226
+ // directive gave no explicit verdict, render actualValue as-is.
1227
+ if ((kcpHandled ?? this.showMaskTyped) &&
1084
1228
  this.keepCharacterPositions &&
1085
1229
  this.actualValue &&
1086
1230
  !justPasted &&
@@ -1095,6 +1239,27 @@ class NgxMaskService extends NgxMaskApplierService {
1095
1239
  }
1096
1240
  // Apply the mask using the parent class method
1097
1241
  const result = super.applyMask(newInputValue, maskExpression, newPosition, justPasted, backspaced, cb);
1242
+ // #1615: a value the mask cannot process AT ALL (masking leaves nothing of it) is
1243
+ // rendered verbatim instead of being destroyed, and must not reach formControlResult
1244
+ // — the empty remnant would clobber the model that holds the sentinel (e.g.
1245
+ // setValue('ONGOING') on a digits mask). Values that PARTIALLY match keep regular
1246
+ // masking. Two cases reach this:
1247
+ // 1. this.writingValue: the originating writeValue() call itself.
1248
+ // 2. currentValue === inputValue (and the mask didn't just change): a LATER re-render
1249
+ // replaying the exact same already-verbatim value outside of writeValue (e.g.
1250
+ // ngOnChanges re-running applyMask via the directive's _applyMask() when an
1251
+ // UNRELATED input like `disabled` changes) — without this, that pass would fall
1252
+ // through to regular masking, emit the empty result via onChange, and clobber the
1253
+ // model even though the DOM would still show the sentinel correctly.
1254
+ if ((this.writingValue || (!this.maskChanged && inputValue === this.currentValue)) &&
1255
+ inputValue &&
1256
+ !result &&
1257
+ this.removeMask(inputValue)) {
1258
+ this.actualValue = inputValue;
1259
+ this.previousValue = this.currentValue;
1260
+ this.currentValue = inputValue;
1261
+ return inputValue;
1262
+ }
1098
1263
  this.actualValue = this.getActualValue(result);
1099
1264
  // handle some separator implications:
1100
1265
  // a.) adjust decimalMarker default (. -> ,) if thousandSeparator is a dot
@@ -1138,14 +1303,19 @@ class NgxMaskService extends NgxMaskApplierService {
1138
1303
  const resLen = result.length;
1139
1304
  const prefNmask = `${this.prefix}${this.maskIsShown}${this.suffix}`;
1140
1305
  // Handle specific mask expressions
1141
- if (this.maskExpression.includes(MaskExpression.HOURS)) {
1306
+ // NOTE: IP/CPF_CNPJ/CPF_CNPJ_ALPHA are checked via exact equality BEFORE the HOURS
1307
+ // substring check below, because MaskExpression.HOURS is the single character 'H' and
1308
+ // 'CPF_CNPJ_ALPHA' contains 'H' (from "ALPHA"), which previously caused CPF_CNPJ_ALPHA
1309
+ // to be misrouted into the HOURS branch instead of its own branch.
1310
+ if (this.maskExpression === MaskExpression.IP ||
1311
+ this.maskExpression === MaskExpression.CPF_CNPJ ||
1312
+ this.maskExpression === MaskExpression.CPF_CNPJ_ALPHA) {
1313
+ return `${result}${prefNmask}`;
1314
+ }
1315
+ else if (this.maskExpression.includes(MaskExpression.HOURS)) {
1142
1316
  const countSkipedSymbol = this._numberSkipedSymbols(result);
1143
1317
  return `${result}${prefNmask.slice(resLen + countSkipedSymbol)}`;
1144
1318
  }
1145
- else if (this.maskExpression === MaskExpression.IP ||
1146
- this.maskExpression === MaskExpression.CPF_CNPJ) {
1147
- return `${result}${prefNmask}`;
1148
- }
1149
1319
  return `${result}${prefNmask.slice(resLen)}`;
1150
1320
  }
1151
1321
  // get the number of characters that were shifted
@@ -1235,12 +1405,34 @@ class NgxMaskService extends NgxMaskApplierService {
1235
1405
  String(value).length > 14)) {
1236
1406
  return String(value);
1237
1407
  }
1238
- return Number(value)
1239
- .toLocaleString('fullwide', {
1240
- useGrouping: false,
1241
- maximumFractionDigits: 20,
1242
- })
1243
- .replace(`/${MaskExpression.MINUS}/`, MaskExpression.MINUS);
1408
+ // #1573: toLocaleString('fullwide', ...) is NOT locale-independent — 'fullwide' is
1409
+ // not a real locale tag, so Intl silently falls back to the runtime DEFAULT locale
1410
+ // (e.g. de-AT on Edge with Austrian regional format emits '0,5'). Expand exponential
1411
+ // notation with pure string math instead; the decimal marker is always '.'.
1412
+ return this._toPlainDecimalString(Number(value));
1413
+ }
1414
+ /**
1415
+ * Locale-independent replacement for toLocaleString('fullwide', { useGrouping: false,
1416
+ * maximumFractionDigits: 20 }) (#1573): expands exponential notation ('7e-7', '1e+21')
1417
+ * to plain decimal form using '.' as decimal marker, regardless of the runtime locale.
1418
+ */
1419
+ _toPlainDecimalString(value) {
1420
+ const stringValue = String(value);
1421
+ const match = /^(-?)(\d+)(?:\.(\d+))?[eE]([+-]?\d+)$/.exec(stringValue);
1422
+ if (!match) {
1423
+ return stringValue;
1424
+ }
1425
+ const [, sign, integerPart, fractionPart = '', exponentPart] = match;
1426
+ const exponent = Number(exponentPart);
1427
+ const digits = `${integerPart}${fractionPart}`;
1428
+ const pointIndex = integerPart.length + exponent;
1429
+ if (pointIndex <= 0) {
1430
+ return `${sign}0.${'0'.repeat(-pointIndex)}${digits}`;
1431
+ }
1432
+ if (pointIndex >= digits.length) {
1433
+ return `${sign}${digits}${'0'.repeat(pointIndex - digits.length)}`;
1434
+ }
1435
+ return `${sign}${digits.slice(0, pointIndex)}.${digits.slice(pointIndex)}`;
1244
1436
  }
1245
1437
  showMaskInInput(inputVal) {
1246
1438
  if (this.showMaskTyped && !!this.shownMaskExpression) {
@@ -1256,7 +1448,8 @@ class NgxMaskService extends NgxMaskApplierService {
1256
1448
  if (this.maskExpression === MaskExpression.IP) {
1257
1449
  return this._checkForIp(inputVal);
1258
1450
  }
1259
- if (this.maskExpression === MaskExpression.CPF_CNPJ) {
1451
+ if (this.maskExpression === MaskExpression.CPF_CNPJ ||
1452
+ this.maskExpression === MaskExpression.CPF_CNPJ_ALPHA) {
1260
1453
  return this._checkForCpfCnpj(inputVal);
1261
1454
  }
1262
1455
  }
@@ -1342,28 +1535,41 @@ class NgxMaskService extends NgxMaskApplierService {
1342
1535
  if (inputVal === MaskExpression.HASH) {
1343
1536
  return cpf;
1344
1537
  }
1345
- const arr = [];
1346
- // eslint-disable-next-line @typescript-eslint/prefer-for-of
1347
- for (let i = 0; i < inputVal.length; i++) {
1348
- const value = inputVal[i] ?? MaskExpression.EMPTY_STRING;
1349
- if (!value) {
1350
- continue;
1538
+ const isCpfCnpjAlpha = this.maskExpression === MaskExpression.CPF_CNPJ_ALPHA;
1539
+ const hasAnyLetter = /[a-zA-Z]/.test(inputVal);
1540
+ const arr = this._countCpfCnpjTypedChars(inputVal, isCpfCnpjAlpha);
1541
+ if (isCpfCnpjAlpha && hasAnyLetter) {
1542
+ // CNPJ_ALPHA shape is "AA.AAA.AAA/AAAA-00": separators fall after the 2nd, 5th,
1543
+ // 8th and 12th typed character, so the placeholder slice offset must account for
1544
+ // however many separators `result` has already passed through, mirroring the
1545
+ // numeric CPF/CNPJ bucketed offsets below.
1546
+ if (arr.length <= 2) {
1547
+ return cnpj.slice(arr.length, cnpj.length);
1351
1548
  }
1352
- if (value.match('\\d')) {
1353
- arr.push(value);
1549
+ if (arr.length > 2 && arr.length <= 5) {
1550
+ return cnpj.slice(arr.length + 1, cnpj.length);
1354
1551
  }
1552
+ if (arr.length > 5 && arr.length <= 8) {
1553
+ return cnpj.slice(arr.length + 2, cnpj.length);
1554
+ }
1555
+ if (arr.length > 8 && arr.length <= 12) {
1556
+ return cnpj.slice(arr.length + 3, cnpj.length);
1557
+ }
1558
+ return cnpj.slice(arr.length + 4, cnpj.length);
1355
1559
  }
1356
- if (arr.length <= 3) {
1357
- return cpf.slice(arr.length, cpf.length);
1358
- }
1359
- if (arr.length > 3 && arr.length <= 6) {
1360
- return cpf.slice(arr.length + 1, cpf.length);
1361
- }
1362
- if (arr.length > 6 && arr.length <= 9) {
1363
- return cpf.slice(arr.length + 2, cpf.length);
1364
- }
1365
- if (arr.length > 9 && arr.length < 11) {
1366
- return cpf.slice(arr.length + 3, cpf.length);
1560
+ else {
1561
+ if (arr.length <= 3) {
1562
+ return cpf.slice(arr.length, cpf.length);
1563
+ }
1564
+ if (arr.length > 3 && arr.length <= 6) {
1565
+ return cpf.slice(arr.length + 1, cpf.length);
1566
+ }
1567
+ if (arr.length > 6 && arr.length <= 9) {
1568
+ return cpf.slice(arr.length + 2, cpf.length);
1569
+ }
1570
+ if (arr.length > 9 && arr.length < 11) {
1571
+ return cpf.slice(arr.length + 3, cpf.length);
1572
+ }
1367
1573
  }
1368
1574
  if (arr.length === 11) {
1369
1575
  return '';
@@ -1379,6 +1585,22 @@ class NgxMaskService extends NgxMaskApplierService {
1379
1585
  }
1380
1586
  return '';
1381
1587
  }
1588
+ /** Collects the characters counted as "typed" for CPF/CNPJ progress tracking: digits only
1589
+ * for the numeric mask, alphanumerics for CPF_CNPJ_ALPHA. */
1590
+ _countCpfCnpjTypedChars(inputVal, isCpfCnpjAlpha) {
1591
+ const arr = [];
1592
+ // eslint-disable-next-line @typescript-eslint/prefer-for-of
1593
+ for (let i = 0; i < inputVal.length; i++) {
1594
+ const value = inputVal[i] ?? MaskExpression.EMPTY_STRING;
1595
+ if (!value) {
1596
+ continue;
1597
+ }
1598
+ if (isCpfCnpjAlpha ? value.match('[a-zA-Z0-9]') : value.match('\\d')) {
1599
+ arr.push(value);
1600
+ }
1601
+ }
1602
+ return arr;
1603
+ }
1382
1604
  /**
1383
1605
  * Recursively determine the current active element by navigating the Shadow DOM until the Active Element is found.
1384
1606
  */
@@ -1407,6 +1629,20 @@ class NgxMaskService extends NgxMaskApplierService {
1407
1629
  if (!this.isInitialized && this._emitValue) {
1408
1630
  return;
1409
1631
  }
1632
+ // #1519: a non-special placeHolderCharacter (e.g. 'X') is never something the user
1633
+ // typed — it's the showMaskTyped fill character for unfilled slots — so it must never
1634
+ // reach the model, regardless of dropSpecialCharacters. The default '_' placeholder is
1635
+ // already covered by specialCharacters/removeMask; a custom single-char placeholder
1636
+ // that isn't a special character is not, since none of the branches below include it
1637
+ // in their removal set.
1638
+ if (this.showMaskTyped &&
1639
+ this.placeHolderCharacter.length === 1 &&
1640
+ this.specialCharacters.indexOf(this.placeHolderCharacter) === -1) {
1641
+ // eslint-disable-next-line no-param-reassign
1642
+ inputValue = inputValue
1643
+ .split(this.placeHolderCharacter)
1644
+ .join(MaskExpression.EMPTY_STRING);
1645
+ }
1410
1646
  if (Array.isArray(this.dropSpecialCharacters)) {
1411
1647
  this.onChange(outputTransformFn(this._toNumber(this._checkSymbols(this._removeMask(this._removeSuffix(this._removePrefix(inputValue)), this.dropSpecialCharacters)))));
1412
1648
  }
@@ -1539,17 +1775,59 @@ class NgxMaskService extends NgxMaskApplierService {
1539
1775
  _checkPrecision(separatorExpression, separatorValue) {
1540
1776
  const separatorPrecision = this.getPrecision(separatorExpression);
1541
1777
  let value = separatorValue;
1542
- if (separatorExpression.indexOf('2') > 0 ||
1543
- (this.leadZero && Number(separatorPrecision) > 0 && Number.isFinite(separatorPrecision))) {
1778
+ if ((separatorExpression.indexOf('2') > 0 && !this._isFocused()) ||
1779
+ (this.leadZero &&
1780
+ !this._isFocused() &&
1781
+ Number(separatorPrecision) > 0 &&
1782
+ Number.isFinite(separatorPrecision))) {
1544
1783
  if (this.decimalMarker === MaskExpression.COMMA && this.leadZero) {
1545
1784
  value = value.replace(',', '.');
1546
1785
  }
1547
- return this.leadZero
1548
- ? Number(value).toFixed(Number(separatorPrecision))
1549
- : Number(value).toFixed(2);
1786
+ const precision = this.leadZero ? Number(separatorPrecision) : 2;
1787
+ // #1567: Number(value).toFixed() corrupts values with more significant digits
1788
+ // than an IEEE-754 double can hold (e.g. '999999999999999.99' becomes
1789
+ // '1000000000000000.00'). Round such values textually instead; safe-range
1790
+ // values keep the original toFixed() semantics.
1791
+ if (this._exceedsDoublePrecision(value)) {
1792
+ return this._stringToFixed(value, precision);
1793
+ }
1794
+ return Number(value).toFixed(precision);
1550
1795
  }
1551
1796
  return this.numberToString(value);
1552
1797
  }
1798
+ /**
1799
+ * True when the plain decimal string carries more significant digits than an IEEE-754
1800
+ * double can represent exactly (15 is the guaranteed round-trip digit count), meaning a
1801
+ * Number() round-trip would corrupt it (#1567).
1802
+ */
1803
+ _exceedsDoublePrecision(value) {
1804
+ if (!/^-?\d+(\.\d+)?$/.test(value)) {
1805
+ return false;
1806
+ }
1807
+ const significantDigits = value.replace(/\D/g, '').replace(/^0+/, '');
1808
+ return significantDigits.length > 15;
1809
+ }
1810
+ /**
1811
+ * Exact string-based equivalent of Number.prototype.toFixed (round half away from zero)
1812
+ * for plain decimal strings beyond double precision (#1567).
1813
+ */
1814
+ _stringToFixed(value, precision) {
1815
+ const isNegative = value.startsWith(MaskExpression.MINUS);
1816
+ const absValue = isNegative ? value.slice(1) : value;
1817
+ const [integerPart = '0', fractionPart = ''] = absValue.split(MaskExpression.DOT);
1818
+ const paddedFraction = fractionPart.padEnd(precision + 1, '0');
1819
+ const keptFraction = paddedFraction.slice(0, precision);
1820
+ const shouldRoundUp = (paddedFraction.charCodeAt(precision) || 0) >= 53; // '5'
1821
+ let scaled = BigInt(integerPart + keptFraction);
1822
+ if (shouldRoundUp) {
1823
+ scaled += 1n;
1824
+ }
1825
+ const digits = scaled.toString().padStart(precision + 1, '0');
1826
+ const sign = isNegative && scaled > 0n ? MaskExpression.MINUS : '';
1827
+ return precision > 0
1828
+ ? `${sign}${digits.slice(0, -precision)}.${digits.slice(-precision)}`
1829
+ : `${sign}${digits}`;
1830
+ }
1553
1831
  _repeatPatternSymbols(maskExp) {
1554
1832
  return ((maskExp.match(/{[0-9]+}/) &&
1555
1833
  maskExp
@@ -1576,13 +1854,25 @@ class NgxMaskService extends NgxMaskApplierService {
1576
1854
  }, '')) ||
1577
1855
  maskExp);
1578
1856
  }
1857
+ /**
1858
+ * Decimal marker of the value being normalized in writeValue/pipe flows.
1859
+ *
1860
+ * #1573: this used to return the RUNTIME default locale's decimal marker
1861
+ * ((1.1).toLocaleString().substring(1, 2)), which made mask behavior depend on the
1862
+ * OS/browser regional format: under a comma-decimal locale (e.g. Edge + Austrian
1863
+ * regional settings) a preformatted value like '10,000' (thousandSeparator ',')
1864
+ * had its ',' replaced by the configured '.' decimalMarker, corrupting the value
1865
+ * 1000x. JS number stringification (String(n)) always uses '.', and string values
1866
+ * are expected to use the configured markers — the runtime locale is never the
1867
+ * right source, so this is always '.'.
1868
+ */
1579
1869
  currentLocaleDecimalMarker() {
1580
- return (1.1).toLocaleString().substring(1, 2);
1870
+ return MaskExpression.DOT;
1581
1871
  }
1582
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.1", ngImport: i0, type: NgxMaskService, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
1583
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.1", ngImport: i0, type: NgxMaskService });
1872
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: NgxMaskService, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
1873
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: NgxMaskService });
1584
1874
  }
1585
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.1", ngImport: i0, type: NgxMaskService, decorators: [{
1875
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: NgxMaskService, decorators: [{
1586
1876
  type: Injectable
1587
1877
  }] });
1588
1878
 
@@ -1619,82 +1909,160 @@ function provideEnvironmentNgxMask(configValue) {
1619
1909
 
1620
1910
  class NgxMaskDirective {
1621
1911
  // ===== Mask Configuration Inputs =====
1622
- mask = input('', ...(ngDevMode ? [{ debugName: "mask" }] : []));
1623
- specialCharacters = input([], ...(ngDevMode ? [{ debugName: "specialCharacters" }] : []));
1624
- patterns = input({}, ...(ngDevMode ? [{ debugName: "patterns" }] : []));
1625
- prefix = input('', ...(ngDevMode ? [{ debugName: "prefix" }] : []));
1626
- suffix = input('', ...(ngDevMode ? [{ debugName: "suffix" }] : []));
1627
- thousandSeparator = input(' ', ...(ngDevMode ? [{ debugName: "thousandSeparator" }] : []));
1628
- decimalMarker = input('.', ...(ngDevMode ? [{ debugName: "decimalMarker" }] : []));
1629
- dropSpecialCharacters = input(null, ...(ngDevMode ? [{ debugName: "dropSpecialCharacters" }] : []));
1630
- hiddenInput = input(null, ...(ngDevMode ? [{ debugName: "hiddenInput" }] : []));
1631
- showMaskTyped = input(null, ...(ngDevMode ? [{ debugName: "showMaskTyped" }] : []));
1632
- placeHolderCharacter = input(null, ...(ngDevMode ? [{ debugName: "placeHolderCharacter" }] : []));
1633
- shownMaskExpression = input(null, ...(ngDevMode ? [{ debugName: "shownMaskExpression" }] : []));
1634
- clearIfNotMatch = input(null, ...(ngDevMode ? [{ debugName: "clearIfNotMatch" }] : []));
1635
- validation = input(null, ...(ngDevMode ? [{ debugName: "validation" }] : []));
1636
- separatorLimit = input('', ...(ngDevMode ? [{ debugName: "separatorLimit" }] : []));
1637
- allowNegativeNumbers = input(null, ...(ngDevMode ? [{ debugName: "allowNegativeNumbers" }] : []));
1638
- leadZeroDateTime = input(null, ...(ngDevMode ? [{ debugName: "leadZeroDateTime" }] : []));
1639
- leadZero = input(null, ...(ngDevMode ? [{ debugName: "leadZero" }] : []));
1640
- triggerOnMaskChange = input(null, ...(ngDevMode ? [{ debugName: "triggerOnMaskChange" }] : []));
1641
- apm = input(null, ...(ngDevMode ? [{ debugName: "apm" }] : []));
1642
- inputTransformFn = input(null, ...(ngDevMode ? [{ debugName: "inputTransformFn" }] : []));
1643
- outputTransformFn = input(null, ...(ngDevMode ? [{ debugName: "outputTransformFn" }] : []));
1644
- keepCharacterPositions = input(null, ...(ngDevMode ? [{ debugName: "keepCharacterPositions" }] : []));
1645
- instantPrefix = input(null, ...(ngDevMode ? [{ debugName: "instantPrefix" }] : []));
1646
- value = model('', ...(ngDevMode ? [{ debugName: "value" }] : []));
1647
- errors = input([], ...(ngDevMode ? [{ debugName: "errors" }] : []));
1648
- disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : []));
1649
- touched = model(false, ...(ngDevMode ? [{ debugName: "touched" }] : []));
1650
- dirty = input(false, ...(ngDevMode ? [{ debugName: "dirty" }] : []));
1651
- invalid = input(false, ...(ngDevMode ? [{ debugName: "invalid" }] : []));
1652
- pending = input(false, ...(ngDevMode ? [{ debugName: "pending" }] : []));
1653
- readonly = input(false, ...(ngDevMode ? [{ debugName: "readonly" }] : []));
1654
- required = input(false, ...(ngDevMode ? [{ debugName: "required" }] : []));
1655
- name = input('', ...(ngDevMode ? [{ debugName: "name" }] : []));
1912
+ mask = input('', ...(ngDevMode ? [{ debugName: "mask" }] : /* istanbul ignore next */ []));
1913
+ specialCharacters = input([], ...(ngDevMode ? [{ debugName: "specialCharacters" }] : /* istanbul ignore next */ []));
1914
+ patterns = input({}, ...(ngDevMode ? [{ debugName: "patterns" }] : /* istanbul ignore next */ []));
1915
+ prefix = input('', ...(ngDevMode ? [{ debugName: "prefix" }] : /* istanbul ignore next */ []));
1916
+ suffix = input('', ...(ngDevMode ? [{ debugName: "suffix" }] : /* istanbul ignore next */ []));
1917
+ thousandSeparator = input(' ', ...(ngDevMode ? [{ debugName: "thousandSeparator" }] : /* istanbul ignore next */ []));
1918
+ decimalMarker = input('.', ...(ngDevMode ? [{ debugName: "decimalMarker" }] : /* istanbul ignore next */ []));
1919
+ dropSpecialCharacters = input(null, ...(ngDevMode ? [{ debugName: "dropSpecialCharacters" }] : /* istanbul ignore next */ []));
1920
+ hiddenInput = input(null, ...(ngDevMode ? [{ debugName: "hiddenInput" }] : /* istanbul ignore next */ []));
1921
+ showMaskTyped = input(null, ...(ngDevMode ? [{ debugName: "showMaskTyped" }] : /* istanbul ignore next */ []));
1922
+ placeHolderCharacter = input(null, ...(ngDevMode ? [{ debugName: "placeHolderCharacter" }] : /* istanbul ignore next */ []));
1923
+ shownMaskExpression = input(null, ...(ngDevMode ? [{ debugName: "shownMaskExpression" }] : /* istanbul ignore next */ []));
1924
+ clearIfNotMatch = input(null, ...(ngDevMode ? [{ debugName: "clearIfNotMatch" }] : /* istanbul ignore next */ []));
1925
+ validation = input(null, ...(ngDevMode ? [{ debugName: "validation" }] : /* istanbul ignore next */ []));
1926
+ separatorLimit = input('', ...(ngDevMode ? [{ debugName: "separatorLimit" }] : /* istanbul ignore next */ []));
1927
+ allowNegativeNumbers = input(null, ...(ngDevMode ? [{ debugName: "allowNegativeNumbers" }] : /* istanbul ignore next */ []));
1928
+ leadZeroDateTime = input(null, ...(ngDevMode ? [{ debugName: "leadZeroDateTime" }] : /* istanbul ignore next */ []));
1929
+ leadZero = input(null, ...(ngDevMode ? [{ debugName: "leadZero" }] : /* istanbul ignore next */ []));
1930
+ triggerOnMaskChange = input(null, ...(ngDevMode ? [{ debugName: "triggerOnMaskChange" }] : /* istanbul ignore next */ []));
1931
+ apm = input(null, ...(ngDevMode ? [{ debugName: "apm" }] : /* istanbul ignore next */ []));
1932
+ inputTransformFn = input(null, ...(ngDevMode ? [{ debugName: "inputTransformFn" }] : /* istanbul ignore next */ []));
1933
+ outputTransformFn = input(null, ...(ngDevMode ? [{ debugName: "outputTransformFn" }] : /* istanbul ignore next */ []));
1934
+ keepCharacterPositions = input(null, ...(ngDevMode ? [{ debugName: "keepCharacterPositions" }] : /* istanbul ignore next */ []));
1935
+ instantPrefix = input(null, ...(ngDevMode ? [{ debugName: "instantPrefix" }] : /* istanbul ignore next */ []));
1936
+ value = model('', ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
1937
+ disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
1938
+ touched = model(false, ...(ngDevMode ? [{ debugName: "touched" }] : /* istanbul ignore next */ []));
1656
1939
  maskFilled = output();
1657
- _maskValue = signal('', ...(ngDevMode ? [{ debugName: "_maskValue" }] : []));
1658
- _inputValue = signal('', ...(ngDevMode ? [{ debugName: "_inputValue" }] : []));
1659
- _position = signal(null, ...(ngDevMode ? [{ debugName: "_position" }] : []));
1660
- _code = signal('', ...(ngDevMode ? [{ debugName: "_code" }] : []));
1661
- _maskExpressionArray = signal([], ...(ngDevMode ? [{ debugName: "_maskExpressionArray" }] : []));
1662
- _justPasted = signal(false, ...(ngDevMode ? [{ debugName: "_justPasted" }] : []));
1663
- _isFocused = signal(false, ...(ngDevMode ? [{ debugName: "_isFocused" }] : []));
1940
+ _maskValue = signal('', ...(ngDevMode ? [{ debugName: "_maskValue" }] : /* istanbul ignore next */ []));
1941
+ _inputValue = signal('', ...(ngDevMode ? [{ debugName: "_inputValue" }] : /* istanbul ignore next */ []));
1942
+ _position = signal(null, ...(ngDevMode ? [{ debugName: "_position" }] : /* istanbul ignore next */ []));
1943
+ _code = signal('', ...(ngDevMode ? [{ debugName: "_code" }] : /* istanbul ignore next */ []));
1944
+ _maskExpressionArray = signal([], ...(ngDevMode ? [{ debugName: "_maskExpressionArray" }] : /* istanbul ignore next */ []));
1945
+ _justPasted = signal(false, ...(ngDevMode ? [{ debugName: "_justPasted" }] : /* istanbul ignore next */ []));
1946
+ _isFocused = signal(false, ...(ngDevMode ? [{ debugName: "_isFocused" }] : /* istanbul ignore next */ []));
1664
1947
  /** For IME composition event */
1665
- _isComposing = signal(false, ...(ngDevMode ? [{ debugName: "_isComposing" }] : []));
1666
- /** Track if we're using Signal Forms mode */
1667
- _isSignalFormsMode = signal(false, ...(ngDevMode ? [{ debugName: "_isSignalFormsMode" }] : []));
1948
+ _isComposing = signal(false, ...(ngDevMode ? [{ debugName: "_isComposing" }] : /* istanbul ignore next */ []));
1949
+ /**
1950
+ * True once Angular has driven this directive through the `ControlValueAccessor` contract
1951
+ * (`registerOnChange`). NOTE: Signal Forms' `FormField` ALSO takes this path — it prefers a
1952
+ * host-provided `NG_VALUE_ACCESSOR` over the custom-control `value` model binding (see
1953
+ * `FormField.ɵngControlCreate`), so it calls `registerOnChange`/`writeValue` too and this
1954
+ * flag is `true` in both modes. That is fine: with the flag set, the `value`-model effect
1955
+ * below is a no-op and all rendering goes through `writeValue()`. The `value` model is only
1956
+ * driven directly (flag stays `false`) when the directive is used standalone with a
1957
+ * `[(value)]` binding and no forms integration.
1958
+ */
1959
+ _isCvaMode = signal(false, ...(ngDevMode ? [{ debugName: "_isCvaMode" }] : /* istanbul ignore next */ []));
1960
+ /** Guards against the value effect echoing back a value we just propagated ourselves. */
1961
+ _skipNextValueEffect = signal(false, ...(ngDevMode ? [{ debugName: "_skipNextValueEffect" }] : /* istanbul ignore next */ []));
1962
+ /**
1963
+ * The exact stringified value last pushed through `onChange` (view → model). Signal Forms'
1964
+ * `FormField` echoes every model update back through `writeValue()` — including updates that
1965
+ * originated from the view. Re-masking that unmasked echo is at best a redundant re-render
1966
+ * and at worst destructive for masks whose unmasked form is ambiguous (e.g. IP:
1967
+ * '192168178' cannot reconstruct the typed dots of '192.168.1.78'). `writeValue()` consumes
1968
+ * this marker to skip exactly that echo. `null` = no pending propagation.
1969
+ */
1970
+ _lastPropagatedValue = null;
1971
+ /**
1972
+ * True once the first `ngOnChanges` pass has applied the mask configuration to the service.
1973
+ * Signal Forms' `FormField` syncs its field value through the template `ɵɵcontrol` update
1974
+ * instruction, which runs BEFORE the sibling directives' first `ngOnChanges` on the same
1975
+ * element — so the very first `writeValue()` would otherwise see an unconfigured service
1976
+ * (empty `maskExpression`, default `leadZero`/`thousandSeparator`/...) and render the raw
1977
+ * value. Until this flag is set, `writeValue()` stashes the incoming value and `ngOnChanges`
1978
+ * replays it once the configuration is in place.
1979
+ */
1980
+ _configApplied = false;
1981
+ _pendingInitialValue;
1982
+ _hasPendingInitialValue = false;
1983
+ /**
1984
+ * True once the `disabled` input has ever delivered `true`. The disabled effect's very
1985
+ * first run fires with the input's default `false` even when nothing binds `[disabled]`.
1986
+ * Because effects run after Angular Forms' `setUpControl` (which calls
1987
+ * `setDisabledState(true)` for initially-disabled controls) and both DOM writes are
1988
+ * queueMicrotask-deferred in FIFO order, forwarding that default `false` would land last
1989
+ * and re-enable an initially-disabled control (#1607, #1614). An initial `false` write is
1990
+ * never needed — inputs are enabled by default — so `false` is only forwarded after the
1991
+ * input has explicitly driven the state to `true` at least once.
1992
+ */
1993
+ _disabledEverSet = false;
1994
+ /** Ensures the multi-character placeHolderCharacter warning (#1347) is emitted only once. */
1995
+ _warnedAboutMultiCharPlaceholder = false;
1668
1996
  _maskService = inject(NgxMaskService, { self: true });
1669
1997
  document = inject(DOCUMENT);
1670
1998
  _config = inject(NGX_MASK_CONFIG);
1999
+ /**
2000
+ * Under zoneless change detection, writing to the underlying FormControl programmatically
2001
+ * (e.g. `setValue`/`patchValue` from outside a signal/effect context, or from a callback
2002
+ * zone.js used to auto-flush like `requestAnimationFrame`/`queueMicrotask`) does not itself
2003
+ * trigger a CD run. Bindings that read `form.pristine`/`form.dirty`/`form.value` on the host
2004
+ * template need an explicit `markForCheck()` once the directive finishes reacting to a
2005
+ * programmatic value write, otherwise the view stays stale until something else schedules CD.
2006
+ */
2007
+ _changeDetectorRef = inject(ChangeDetectorRef);
2008
+ _elementRef = inject(ElementRef);
2009
+ _renderer = inject(Renderer2);
2010
+ // Injector is used to lazily resolve NgControl. NgControl cannot be injected directly:
2011
+ // this directive is registered as the control's NG_VALUE_ACCESSOR, so a direct
2012
+ // inject(NgControl) would form a circular dependency. Resolving it lazily on first use
2013
+ // (after the control graph is wired) sidesteps the cycle.
2014
+ _injector = inject(Injector);
2015
+ _ngControl;
2016
+ _resolveNgControl() {
2017
+ if (typeof this._ngControl === 'undefined') {
2018
+ this._ngControl = this._injector.get(NgControl, null);
2019
+ }
2020
+ return this._ngControl;
2021
+ }
1671
2022
  // eslint-disable-next-line @typescript-eslint/no-empty-function
1672
2023
  onChange = (_) => { };
1673
2024
  // eslint-disable-next-line @typescript-eslint/no-empty-function
1674
2025
  onTouch = () => { };
1675
2026
  constructor() {
2027
+ // Default onChange used in Signal Forms mode, where Angular never calls registerOnChange.
2028
+ // It pushes the unmasked value into the `value` model so the `valueChange` output fires.
2029
+ // registerOnChange() overrides this to additionally invoke Angular's CVA callback.
2030
+ this._maskService.onChange = this.onChange = (value) => {
2031
+ this._propagateToValueModel(value);
2032
+ };
2033
+ // Signal Forms drives the `value` model input directly (custom-control mode).
2034
+ // In classic CVA mode `writeValue` handles rendering instead, so this effect is a no-op
2035
+ // there. We skip the pass immediately following our own onChange propagation to avoid
2036
+ // clobbering the raw `_inputValue` with the masked value echoed back from the model.
1676
2037
  effect(() => {
1677
2038
  const signalValue = this.value();
1678
- if (this._isSignalFormsMode() && signalValue !== untracked(() => this._inputValue())) {
1679
- untracked(() => {
2039
+ untracked(() => {
2040
+ if (this._isCvaMode()) {
2041
+ return;
2042
+ }
2043
+ if (this._skipNextValueEffect()) {
2044
+ this._skipNextValueEffect.set(false);
2045
+ return;
2046
+ }
2047
+ if (String(signalValue) !== String(this._inputValue())) {
1680
2048
  this.writeValue(signalValue);
1681
- });
1682
- }
2049
+ }
2050
+ });
1683
2051
  });
1684
2052
  effect(() => {
1685
2053
  const isDisabled = this.disabled();
1686
2054
  untracked(() => {
2055
+ // Never force-enable on the default `false`: it would clobber Angular Forms'
2056
+ // own setDisabledState(true) for initially-disabled controls (see
2057
+ // _disabledEverSet). `false` is forwarded only after an explicit `true`.
2058
+ if (!isDisabled && !this._disabledEverSet) {
2059
+ return;
2060
+ }
2061
+ this._disabledEverSet = true;
1687
2062
  this.setDisabledState(isDisabled);
1688
2063
  });
1689
2064
  });
1690
2065
  }
1691
- ngOnInit() {
1692
- // Detect if we're being used with Signal Forms
1693
- // Signal Forms will set the value model from outside
1694
- if (this.value() !== '') {
1695
- this._isSignalFormsMode.set(true);
1696
- }
1697
- }
1698
2066
  ngOnChanges(changes) {
1699
2067
  const { mask, specialCharacters, patterns, prefix, suffix, thousandSeparator, decimalMarker, dropSpecialCharacters, hiddenInput, showMaskTyped, placeHolderCharacter, shownMaskExpression, clearIfNotMatch, validation, separatorLimit, allowNegativeNumbers, leadZeroDateTime, leadZero, triggerOnMaskChange, apm, inputTransformFn, outputTransformFn, keepCharacterPositions, instantPrefix, } = changes;
1700
2068
  if (mask) {
@@ -1714,12 +2082,12 @@ class NgxMaskDirective {
1714
2082
  }
1715
2083
  }
1716
2084
  if (specialCharacters) {
1717
- if (!specialCharacters.currentValue || !Array.isArray(specialCharacters.currentValue)) {
1718
- return;
1719
- }
1720
- else {
1721
- this._maskService.specialCharacters = specialCharacters.currentValue || [];
2085
+ if (Array.isArray(specialCharacters.currentValue)) {
2086
+ this._maskService.specialCharacters = specialCharacters.currentValue;
1722
2087
  }
2088
+ // A non-array value (e.g. null from an unset dynamic config) keeps the current
2089
+ // service value (config defaults) and must not abort the whole ngOnChanges pass,
2090
+ // otherwise sibling inputs changed in the same cycle would be silently ignored (#1512).
1723
2091
  }
1724
2092
  if (allowNegativeNumbers) {
1725
2093
  this._maskService.allowNegativeNumbers = allowNegativeNumbers.currentValue;
@@ -1791,6 +2159,13 @@ class NgxMaskDirective {
1791
2159
  }
1792
2160
  if (placeHolderCharacter) {
1793
2161
  this._maskService.placeHolderCharacter = placeHolderCharacter.currentValue;
2162
+ if (typeof placeHolderCharacter.currentValue === 'string' &&
2163
+ placeHolderCharacter.currentValue.length > 1 &&
2164
+ !this._warnedAboutMultiCharPlaceholder) {
2165
+ this._warnedAboutMultiCharPlaceholder = true;
2166
+ // eslint-disable-next-line no-console
2167
+ console.warn('Ngx-mask: placeHolderCharacter should be a single character; behavior with multi-character values is undefined (e.g. keepCharacterPositions will not work). Current value:', placeHolderCharacter.currentValue);
2168
+ }
1794
2169
  }
1795
2170
  if (shownMaskExpression) {
1796
2171
  this._maskService.shownMaskExpression = shownMaskExpression.currentValue;
@@ -1823,6 +2198,17 @@ class NgxMaskDirective {
1823
2198
  this._maskService.keepCharacterPositions = keepCharacterPositions.currentValue;
1824
2199
  }
1825
2200
  this._applyMask();
2201
+ if (!this._configApplied) {
2202
+ this._configApplied = true;
2203
+ if (this._hasPendingInitialValue) {
2204
+ // Replay the writeValue() call that arrived before this first ngOnChanges pass
2205
+ // (see _configApplied) now that the mask configuration is applied.
2206
+ this._hasPendingInitialValue = false;
2207
+ const pendingValue = this._pendingInitialValue;
2208
+ this._pendingInitialValue = null;
2209
+ void this.writeValue(pendingValue);
2210
+ }
2211
+ }
1826
2212
  }
1827
2213
  validate({ value }) {
1828
2214
  const processedValue = typeof value === 'number' ? String(value) : value;
@@ -1881,9 +2267,31 @@ class NgxMaskDirective {
1881
2267
  else if (maskValue.indexOf(key) !== -1) {
1882
2268
  counterOfOpt++;
1883
2269
  }
1884
- if (maskValue.indexOf(key) !== -1 &&
1885
- processedValue.length >= maskValue.indexOf(key)) {
1886
- return null;
2270
+ const firstOptionalIndex = maskValue.indexOf(key);
2271
+ if (firstOptionalIndex !== -1 && processedValue.length >= firstOptionalIndex) {
2272
+ // #1515: the early "everything before the first optional token is
2273
+ // filled" return is only sound for trailing-optional layouts. When a
2274
+ // mandatory token follows the first optional one (e.g. `999SSS`,
2275
+ // indexOf === 0) it used to mark EVERY value as valid.
2276
+ const hasMandatoryAfterOptional = maskValue
2277
+ .slice(firstOptionalIndex)
2278
+ .split(MaskExpression.EMPTY_STRING)
2279
+ .some((symbol) => !!this._maskService.patterns[symbol] &&
2280
+ !this._maskService.patterns[symbol]?.optional);
2281
+ if (!hasMandatoryAfterOptional) {
2282
+ return null;
2283
+ }
2284
+ if (!this.prefix() && !this.suffix() && this._isPlainTokenMask(maskValue)) {
2285
+ // Position-aware check: match the value against the mask allowing
2286
+ // optional tokens to be skipped; every mandatory token must be
2287
+ // consumed (catches `12a` for `999SSS`, which a pure length
2288
+ // check cannot).
2289
+ return this._matchesMaskWithOptionalSkip(processedValue, maskValue)
2290
+ ? null
2291
+ : this._createValidationError(processedValue);
2292
+ }
2293
+ // Non-plain masks (prefix/suffix, exotic symbols) fall through to the
2294
+ // legacy length check below (`maskValue.length - counterOfOpt`).
1887
2295
  }
1888
2296
  if (counterOfOpt === maskValue.length) {
1889
2297
  return null;
@@ -1908,6 +2316,13 @@ class NgxMaskDirective {
1908
2316
  : maskValue.length - counterOfOpt;
1909
2317
  if (array.length === 1) {
1910
2318
  if (processedValue.length < length) {
2319
+ // #1583: for `||` multi-masks, a value shorter than the selected
2320
+ // alternative can still be complete: it must stop exactly at a
2321
+ // special-character boundary of the selected alternative and satisfy
2322
+ // the length requirement of another (shorter) alternative.
2323
+ if (this._isCompleteAlternativeBoundary(processedValue)) {
2324
+ return null;
2325
+ }
1911
2326
  return this._createValidationError(processedValue);
1912
2327
  }
1913
2328
  }
@@ -1950,6 +2365,7 @@ class NgxMaskDirective {
1950
2365
  }
1951
2366
  onFocus() {
1952
2367
  this._isFocused.set(true);
2368
+ this._maskService._isFocused.set(true);
1953
2369
  }
1954
2370
  onModelChange(value) {
1955
2371
  // on form reset we need to update the actualValue
@@ -1962,8 +2378,30 @@ class NgxMaskDirective {
1962
2378
  }
1963
2379
  onInput(e) {
1964
2380
  this._maskService.isInitialized = true;
1965
- // If IME is composing text, we wait for the composed text.
1966
- if (this._isComposing()) {
2381
+ // Android IMEs report every keydown as key 'Unidentified' / keyCode 229, so a
2382
+ // backspace is undetectable from keydown alone and all _code()-based deletion
2383
+ // branches misfire (cursor jumps, "stuck" backspace — #1497). The input event's
2384
+ // inputType is the reliable source: derive the deletion code from it.
2385
+ const inputType = e.inputType;
2386
+ if (inputType === 'deleteContentBackward') {
2387
+ this._code.set(MaskExpression.BACKSPACE);
2388
+ }
2389
+ else if (inputType === 'deleteContentForward') {
2390
+ this._code.set(MaskExpression.DELETE);
2391
+ }
2392
+ else if (inputType &&
2393
+ (this._code() === MaskExpression.BACKSPACE || this._code() === MaskExpression.DELETE)) {
2394
+ // A non-delete edit that produced no meaningful keydown (IME insertion,
2395
+ // context-menu paste): clear the stale deletion code so the edit is not
2396
+ // processed as a backspace.
2397
+ this._code.set(inputType);
2398
+ }
2399
+ // If IME is composing text, we wait for the composed text — but only when the mask
2400
+ // can actually accept letters. Purely numeric masks gain nothing from composition,
2401
+ // and some Android IMEs (Samsung Keyboard) deliver every keystroke as
2402
+ // insertCompositionText without firing compositionend until blur, which would leave
2403
+ // the FormControl stale for the whole edit (#1293).
2404
+ if (this._isComposing() && this._maskAcceptsLetterInput()) {
1967
2405
  return;
1968
2406
  }
1969
2407
  const el = e.target;
@@ -1972,51 +2410,90 @@ class NgxMaskDirective {
1972
2410
  : el.value;
1973
2411
  if (el.type !== 'number') {
1974
2412
  if (typeof transformedValue === 'string' || typeof transformedValue === 'number') {
1975
- el.value = transformedValue.toString();
2413
+ const transformedString = transformedValue.toString();
2414
+ // Only rewrite el.value when the transform actually changed it: any
2415
+ // programmatic `.value =` assignment clears the browser's "last changed
2416
+ // by a user edit" flag, which silently disables native constraint
2417
+ // validation (minlength -> validity.tooShort) even when the mask is a
2418
+ // no-op (#1379). With an empty mask the directive must stay a pure
2419
+ // passthrough for the native input.
2420
+ if (el.value !== transformedString) {
2421
+ el.value = transformedString;
2422
+ }
1976
2423
  this._inputValue.set(el.value);
1977
2424
  this._setMask();
1978
2425
  if (!this._maskValue()) {
1979
2426
  this.onChange(el.value);
1980
2427
  return;
1981
2428
  }
2429
+ // On paste of a raw value that does not yet carry the prefix, the caret
2430
+ // position reported by the element is prefix.length short of where it must
2431
+ // land once applyMask prepends the prefix (#1571). Captured here because the
2432
+ // applyValueChanges callback below resets the _justPasted flag.
2433
+ const pastedValueWithoutPrefix = this._justPasted() &&
2434
+ !!this._maskService.prefix &&
2435
+ !el.value.startsWith(this._maskService.prefix);
1982
2436
  let position = el.selectionStart === 1
1983
2437
  ? el.selectionStart + this._maskService.prefix.length
1984
2438
  : el.selectionStart;
1985
- if (this.showMaskTyped() &&
1986
- this.keepCharacterPositions() &&
1987
- this._maskService.placeHolderCharacter.length === 1) {
2439
+ if (this.keepCharacterPositions() &&
2440
+ this._maskService.placeHolderCharacter.length === 1 &&
2441
+ !this._justPasted()) {
1988
2442
  const suffix = this.suffix();
1989
2443
  const prefix = this.prefix();
1990
2444
  const inputSymbol = el.value.slice(position - 1, position);
1991
2445
  const prefixLength = prefix.length;
1992
- const checkSymbols = this._maskService._checkSymbolMask(inputSymbol, this._maskService.maskExpression[position - 1 - prefixLength] ??
1993
- MaskExpression.EMPTY_STRING);
1994
- const checkSpecialCharacter = this._maskService._checkSymbolMask(inputSymbol, this._maskService.maskExpression[position + 1 - prefixLength] ??
1995
- MaskExpression.EMPTY_STRING);
1996
- const selectRangeBackspace = this._maskService.selStart === this._maskService.selEnd;
1997
- const selStart = Number(this._maskService.selStart) - prefixLength;
1998
- const selEnd = Number(this._maskService.selEnd) - prefixLength;
2446
+ const showMaskTyped = this.showMaskTyped();
2447
+ const maskExpression = this._maskService.maskExpression;
2448
+ // Placeholder skeleton of the mask (special chars kept, fillable slots
2449
+ // replaced by the placeholder character). With showMaskTyped the service
2450
+ // renders it as maskIsShown; without showMaskTyped it is derived locally so
2451
+ // keepCharacterPositions works on its own (#1545, #1543).
2452
+ const maskSkeleton = this._maskService.maskIsShown.length
2453
+ ? this._maskService.maskIsShown
2454
+ : maskExpression.replace(/\w/g, this._maskService.placeHolderCharacter);
2455
+ const hasSelection = this._maskService.selStart !== this._maskService.selEnd;
2456
+ const selStartAbs = Number(this._maskService.selStart);
2457
+ const selEndAbs = Number(this._maskService.selEnd);
2458
+ const selStart = selStartAbs - prefixLength;
2459
+ const selEnd = selEndAbs - prefixLength;
1999
2460
  const backspaceOrDelete = this._code() === MaskExpression.BACKSPACE ||
2000
2461
  this._code() === MaskExpression.DELETE;
2462
+ // Whether this block fully resolved the resulting display value into
2463
+ // this._maskService.actualValue. When true, applyMask short-circuits and
2464
+ // renders actualValue as-is; when false the edit flows through regular
2465
+ // masking (e.g. appending at the end, or clearing the whole value).
2466
+ let kcpHandled = true;
2001
2467
  if (backspaceOrDelete) {
2002
- if (!selectRangeBackspace) {
2003
- if (this._maskService.selStart === prefixLength) {
2004
- this._maskService.actualValue = `${prefix}${this._maskService.maskIsShown.slice(0, selEnd)}${this._inputValue().split(prefix).join('')}`;
2468
+ if (hasSelection) {
2469
+ const preEditLength = el.value.length + (selEndAbs - selStartAbs);
2470
+ if (!showMaskTyped &&
2471
+ selStartAbs <= prefixLength &&
2472
+ selEndAbs >= preEditLength) {
2473
+ // The whole value was selected and deleted: clear through the
2474
+ // regular path instead of showing a placeholder skeleton.
2475
+ kcpHandled = false;
2476
+ }
2477
+ else if (this._maskService.selStart === prefixLength) {
2478
+ this._maskService.actualValue = `${prefix}${maskSkeleton.slice(0, selEnd)}${this._inputValue().split(prefix).join('')}`;
2005
2479
  }
2006
2480
  else if (this._maskService.selStart ===
2007
- this._maskService.maskIsShown.length + prefixLength) {
2008
- this._maskService.actualValue = `${this._inputValue()}${this._maskService.maskIsShown.slice(selStart, selEnd)}`;
2481
+ maskSkeleton.length + prefixLength) {
2482
+ this._maskService.actualValue = `${this._inputValue()}${maskSkeleton.slice(selStart, selEnd)}`;
2009
2483
  }
2010
2484
  else {
2011
2485
  this._maskService.actualValue = `${prefix}${this._inputValue()
2012
2486
  .split(prefix)
2013
2487
  .join('')
2014
- .slice(0, selStart)}${this._maskService.maskIsShown.slice(selStart, selEnd)}${this._maskService.actualValue.slice(selEnd + prefixLength, this._maskService.maskIsShown.length + prefixLength)}${suffix}`;
2488
+ .slice(0, selStart)}${maskSkeleton.slice(selStart, selEnd)}${this._maskService.actualValue.slice(selEnd + prefixLength, maskSkeleton.length + prefixLength)}${suffix}`;
2015
2489
  }
2016
2490
  }
2017
- else if (!this._maskService.specialCharacters.includes(this._maskService.maskExpression.slice(position - prefixLength, position + 1 - prefixLength)) &&
2018
- selectRangeBackspace) {
2019
- if (selStart === 1 && prefix) {
2491
+ else if (!this._maskService.specialCharacters.includes(maskExpression.slice(position - prefixLength, position + 1 - prefixLength))) {
2492
+ if (!showMaskTyped && position >= el.value.length) {
2493
+ // Deleting the last character: no gap needs to be kept.
2494
+ kcpHandled = false;
2495
+ }
2496
+ else if (selStart === 1 && prefix) {
2020
2497
  this._maskService.actualValue = `${prefix}${this._maskService.placeHolderCharacter}${el.value
2021
2498
  .split(prefix)
2022
2499
  .join('')
@@ -2032,35 +2509,90 @@ class NgxMaskDirective {
2032
2509
  }
2033
2510
  position = this._code() === MaskExpression.DELETE ? position + 1 : position;
2034
2511
  }
2035
- if (!backspaceOrDelete) {
2036
- if (!checkSymbols && !checkSpecialCharacter && selectRangeBackspace) {
2037
- position = Number(el.selectionStart) - 1;
2038
- }
2039
- else if (this._maskService.specialCharacters.includes(el.value.slice(position, position + 1)) &&
2040
- checkSpecialCharacter &&
2041
- !this._maskService.specialCharacters.includes(el.value.slice(position + 1, position + 2))) {
2042
- this._maskService.actualValue = `${el.value.slice(0, position - 1)}${el.value.slice(position, position + 1)}${inputSymbol}${el.value.slice(position + 2)}`;
2043
- position = position + 1;
2512
+ else if (hasSelection) {
2513
+ // A selected range was replaced by the typed symbol. Keep the layout:
2514
+ // blank the selection to the mask skeleton and put the typed symbol
2515
+ // into the first fillable slot of the selection (#1527, #1489).
2516
+ const preEditLength = el.value.length - 1 + (selEndAbs - selStartAbs);
2517
+ if (!showMaskTyped &&
2518
+ selStartAbs <= prefixLength &&
2519
+ selEndAbs >= preEditLength) {
2520
+ // The whole value was replaced: mask it through the regular path.
2521
+ kcpHandled = false;
2044
2522
  }
2045
- else if (checkSymbols) {
2046
- if (el.value.length === 1 && position === 1) {
2047
- this._maskService.actualValue = `${prefix}${inputSymbol}${this._maskService.maskIsShown.slice(1, this._maskService.maskIsShown.length)}${suffix}`;
2523
+ else {
2524
+ let maskIdx = Math.max(selStart, 0);
2525
+ while (maskIdx < maskExpression.length &&
2526
+ this._maskService.specialCharacters.includes(maskExpression[maskIdx] ?? MaskExpression.EMPTY_STRING)) {
2527
+ maskIdx += 1;
2528
+ }
2529
+ const blanked = `${el.value.slice(0, selStartAbs)}${maskSkeleton.slice(Math.max(selStart, 0), selEnd)}${el.value.slice(selStartAbs + 1)}`;
2530
+ const targetAbs = maskIdx + prefixLength;
2531
+ if (targetAbs < blanked.length - suffix.length &&
2532
+ this._maskService._checkSymbolMask(inputSymbol, maskExpression[maskIdx] ?? MaskExpression.EMPTY_STRING)) {
2533
+ this._maskService.actualValue = `${blanked.slice(0, targetAbs)}${inputSymbol}${blanked.slice(targetAbs + 1)}`;
2534
+ position = targetAbs + 1;
2048
2535
  }
2049
2536
  else {
2050
- this._maskService.actualValue = `${el.value.slice(0, position - 1)}${inputSymbol}${el.value
2051
- .slice(position + 1)
2052
- .split(suffix)
2053
- .join('')}${suffix}`;
2537
+ this._maskService.actualValue = blanked;
2538
+ position = selStartAbs;
2054
2539
  }
2055
2540
  }
2056
- else if (prefix &&
2057
- el.value.length === 1 &&
2058
- position - prefixLength === 1 &&
2059
- this._maskService._checkSymbolMask(el.value, this._maskService.maskExpression[position - 1 - prefixLength] ??
2060
- MaskExpression.EMPTY_STRING)) {
2061
- this._maskService.actualValue = `${prefix}${el.value}${this._maskService.maskIsShown.slice(1, this._maskService.maskIsShown.length)}${suffix}`;
2541
+ }
2542
+ else {
2543
+ // Single-caret insert (overwrite mode): the typed symbol lands in the
2544
+ // first fillable mask slot at or after the caret, skipping any number
2545
+ // of special characters (#1544, #1489).
2546
+ const oldDisplay = `${el.value.slice(0, position - 1)}${el.value.slice(position)}`;
2547
+ const oldDisplayNoSuffix = suffix
2548
+ ? oldDisplay.split(suffix).join('')
2549
+ : oldDisplay;
2550
+ let maskIdx = position - 1 - prefixLength;
2551
+ if (maskIdx < 0) {
2552
+ // Typed inside the prefix: reject the symbol.
2553
+ position = position - 1;
2554
+ }
2555
+ else {
2556
+ while (maskIdx < maskExpression.length &&
2557
+ this._maskService.specialCharacters.includes(maskExpression[maskIdx] ?? MaskExpression.EMPTY_STRING)) {
2558
+ maskIdx += 1;
2559
+ }
2560
+ const targetAbs = maskIdx + prefixLength;
2561
+ if (targetAbs >= oldDisplayNoSuffix.length) {
2562
+ if (oldDisplayNoSuffix.length <= prefixLength || !showMaskTyped) {
2563
+ // First symbol, or appending at the end without
2564
+ // showMaskTyped: regular masking handles it (including
2565
+ // leadZeroDateTime insertions).
2566
+ kcpHandled = false;
2567
+ }
2568
+ else {
2569
+ // All slots of the rendered mask are already consumed.
2570
+ position = position - 1;
2571
+ }
2572
+ }
2573
+ else if (this._maskService._checkSymbolMask(inputSymbol, maskExpression[maskIdx] ?? MaskExpression.EMPTY_STRING)) {
2574
+ this._maskService.actualValue = `${oldDisplayNoSuffix.slice(0, targetAbs)}${inputSymbol}${oldDisplayNoSuffix.slice(targetAbs + 1)}${suffix}`;
2575
+ position = targetAbs + 1;
2576
+ }
2577
+ else {
2578
+ // Rejected symbol: keep the current display.
2579
+ position = position - 1;
2580
+ }
2062
2581
  }
2063
2582
  }
2583
+ this._maskService.keepCharacterPositionsHandled = kcpHandled;
2584
+ }
2585
+ // A literal '*' typed into the VALUE (custom pattern allowing asterisks) makes
2586
+ // the service's applyMask take its hiddenInput shadow-value branch, whose
2587
+ // equal/shorter-length cases restore the stale pre-edit actualValue and discard
2588
+ // a selection replacement (#1504). Without hiddenInput there is no shadow state
2589
+ // to preserve, so drop it and let the edited value flow through regular masking.
2590
+ if (!this._maskService.hiddenInput &&
2591
+ !this._maskService.showMaskTyped &&
2592
+ !this.keepCharacterPositions() &&
2593
+ this._maskService.selStart !== this._maskService.selEnd &&
2594
+ el.value.includes(MaskExpression.SYMBOL_STAR)) {
2595
+ this._maskService.actualValue = MaskExpression.EMPTY_STRING;
2064
2596
  }
2065
2597
  let caretShift = 0;
2066
2598
  let backspaceShift = false;
@@ -2138,11 +2670,30 @@ class NgxMaskDirective {
2138
2670
  (this._code() === MaskExpression.BACKSPACE && !backspaceShift
2139
2671
  ? 0
2140
2672
  : caretShift);
2673
+ // For separator masks the applier's caret shift is computed on the raw
2674
+ // (prefix-less) value, so account for the prefix the mask just added (#1571).
2675
+ if (pastedValueWithoutPrefix &&
2676
+ this._maskValue().startsWith(MaskExpression.SEPARATOR)) {
2677
+ positionToApply += this._maskService.prefix.length;
2678
+ }
2141
2679
  if (positionToApply > this._getActualInputLength()) {
2142
- positionToApply =
2143
- el.value === this._maskService.decimalMarker && el.value.length === 1
2144
- ? this._getActualInputLength() + 1
2145
- : this._getActualInputLength();
2680
+ // A bare decimal marker ('.' or, with a prefix, '$.') is not a number:
2681
+ // formControlResult has just emitted null for it, which the ngModelChange
2682
+ // reset handler treats as a form reset and clears actualValue. The plain
2683
+ // length clamp would then land the caret BEFORE the marker — keep it right
2684
+ // after the marker instead (#1572).
2685
+ const decimalMarker = this._maskService.decimalMarker;
2686
+ const prefix = this._maskService.prefix;
2687
+ const valueWithoutPrefix = el.value.startsWith(prefix)
2688
+ ? el.value.slice(prefix.length)
2689
+ : el.value;
2690
+ const isBareDecimalMarker = valueWithoutPrefix.length === 1 &&
2691
+ (Array.isArray(decimalMarker)
2692
+ ? decimalMarker.includes(valueWithoutPrefix)
2693
+ : valueWithoutPrefix === decimalMarker);
2694
+ positionToApply = isBareDecimalMarker
2695
+ ? this._getActualInputLength() + 1
2696
+ : this._getActualInputLength();
2146
2697
  }
2147
2698
  if (positionToApply < 0) {
2148
2699
  positionToApply = 0;
@@ -2163,6 +2714,29 @@ class NgxMaskDirective {
2163
2714
  this._maskService.applyValueChanges(el.value.length, this._justPasted(), this._code() === MaskExpression.BACKSPACE || this._code() === MaskExpression.DELETE);
2164
2715
  }
2165
2716
  }
2717
+ /**
2718
+ * Whether any pattern slot of the current mask expression can accept a letter.
2719
+ * IME composition is only meaningful for such masks; for purely numeric masks
2720
+ * (digit patterns, separator, date/time, IP, CPF_CNPJ) waiting for compositionend
2721
+ * only delays the model sync — and Samsung Keyboard may never fire it until blur
2722
+ * (#1293). Unknown/letterless probe failures fall back to `false` (process live).
2723
+ */
2724
+ _maskAcceptsLetterInput() {
2725
+ const patterns = this._maskService.patterns;
2726
+ const maskExpression = this._maskService.maskExpression;
2727
+ for (const symbol of maskExpression) {
2728
+ const pattern = patterns[symbol]?.pattern;
2729
+ if (!pattern) {
2730
+ continue;
2731
+ }
2732
+ // Re-create without sticky/global flags: test() on those is stateful.
2733
+ const probe = new RegExp(pattern.source, pattern.flags.replace(/[gy]/g, ''));
2734
+ if (probe.test('a') || probe.test('A')) {
2735
+ return true;
2736
+ }
2737
+ }
2738
+ return false;
2739
+ }
2166
2740
  // IME starts
2167
2741
  onCompositionStart() {
2168
2742
  this._isComposing.set(true);
@@ -2170,6 +2744,12 @@ class NgxMaskDirective {
2170
2744
  // IME completes
2171
2745
  onCompositionEnd(e) {
2172
2746
  this._isComposing.set(false);
2747
+ if (!this._maskAcceptsLetterInput()) {
2748
+ // For letterless masks every edit was already processed live in onInput
2749
+ // (#1293); reprocessing the same value through the paste path would
2750
+ // double-apply the mask.
2751
+ return;
2752
+ }
2173
2753
  this._justPasted.set(true);
2174
2754
  this.onInput(e);
2175
2755
  }
@@ -2188,18 +2768,20 @@ class NgxMaskDirective {
2188
2768
  const decimalPart = el.value.split(decimalMarker)[1];
2189
2769
  el.value = el.value.includes(decimalMarker)
2190
2770
  ? el.value +
2191
- MaskExpression.NUMBER_ZERO.repeat(precision - decimalPart.length) +
2771
+ MaskExpression.NUMBER_ZERO.repeat(precision - (decimalPart?.length || 0)) +
2192
2772
  suffix
2193
2773
  : el.value +
2194
2774
  decimalMarker +
2195
2775
  MaskExpression.NUMBER_ZERO.repeat(precision) +
2196
2776
  suffix;
2197
2777
  this._maskService.actualValue = el.value;
2778
+ this.onChange(this._maskService.actualValue);
2198
2779
  }
2199
2780
  }
2200
2781
  this._maskService.clearIfNotMatchFn();
2201
2782
  }
2202
2783
  this._isFocused.set(false);
2784
+ this._maskService._isFocused.set(false);
2203
2785
  this.onTouch();
2204
2786
  }
2205
2787
  onClick(e) {
@@ -2263,7 +2845,15 @@ class NgxMaskDirective {
2263
2845
  // User finalize their choice from IME composition, so trigger onInput() for the composed text.
2264
2846
  if (e.key === 'Enter') {
2265
2847
  this.onCompositionEnd(event);
2848
+ return;
2266
2849
  }
2850
+ // Android IMEs can keep a single composition open across many keystrokes
2851
+ // (#1293): still capture the pre-edit value and selection that onInput
2852
+ // relies on when it processes edits during composition.
2853
+ const composingEl = e.target;
2854
+ this._inputValue.set(composingEl.value);
2855
+ this._maskService.selStart = composingEl.selectionStart;
2856
+ this._maskService.selEnd = composingEl.selectionEnd;
2267
2857
  return;
2268
2858
  }
2269
2859
  this._code.set(e.code ? e.code : e.key);
@@ -2283,10 +2873,10 @@ class NgxMaskDirective {
2283
2873
  }
2284
2874
  if (e.key === MaskExpression.BACKSPACE && el.selectionStart !== 0) {
2285
2875
  const prefixLength = this.prefix().length;
2286
- // If specialChars is false, (shouldn't ever happen) then set to the defaults
2287
- const specialCharacters = this.specialCharacters().length
2288
- ? this.specialCharacters()
2289
- : this._config.specialCharacters;
2876
+ // Use the service value: it holds the config defaults when the input is not
2877
+ // bound and the bound value otherwise, so an explicitly bound empty array is
2878
+ // respected instead of silently falling back to the defaults (#1512).
2879
+ const specialCharacters = this._maskService.specialCharacters;
2290
2880
  if (prefixLength > 1 && el.selectionStart <= prefixLength) {
2291
2881
  el.setSelectionRange(prefixLength, el.selectionEnd);
2292
2882
  }
@@ -2315,8 +2905,17 @@ class NgxMaskDirective {
2315
2905
  cursorStart === 0 &&
2316
2906
  el.selectionEnd === el.value.length &&
2317
2907
  el.value.length !== 0) {
2318
- this._position.set(this._maskService.prefix ? this._maskService.prefix.length : 0);
2319
- this._maskService.applyMask(this._maskService.prefix, this._maskService.maskExpression, this._position());
2908
+ // Handle the clear fully here instead of emitting the model change and
2909
+ // relying on the browser's default deletion + input event to update the
2910
+ // view: Firefox can drop the default action after the emission below
2911
+ // triggers change detection (DOM churn around the input), leaving the
2912
+ // model empty but the view untouched until a second Backspace (#1350).
2913
+ e.preventDefault();
2914
+ const displayValue = this._maskService.applyMask(MaskExpression.EMPTY_STRING, this._maskService.maskExpression, 0, false, true);
2915
+ el.value = displayValue;
2916
+ this._inputValue.set(displayValue);
2917
+ const caret = Math.min(this._maskService.prefix.length, displayValue.length);
2918
+ el.setSelectionRange(caret, caret);
2320
2919
  }
2321
2920
  }
2322
2921
  if (!!this.suffix() &&
@@ -2334,8 +2933,53 @@ class NgxMaskDirective {
2334
2933
  this._maskService.selEnd = el.selectionEnd;
2335
2934
  }
2336
2935
  }
2936
+ /**
2937
+ * Restores the control's pristine/untouched state after a writeValue-driven emission.
2938
+ *
2939
+ * writeValue is a one-way model->view sync. When the mask normalizes the written value
2940
+ * (e.g. leadZero '10.2' -> '10.20'), the directive must still emit the corrected value so
2941
+ * the model adopts it — but that emission runs through Angular's view-change pipeline, which
2942
+ * calls markAsDirty()/markAsTouched(). A programmatic setValue/patchValue must leave the
2943
+ * control pristine, so we undo that side effect here when the control was pristine before
2944
+ * the write. `onlySelf: true` keeps parent group state untouched.
2945
+ */
2946
+ _restoreControlStateAfterWrite(wasPristine, wasUntouched) {
2947
+ const ngControl = this._resolveNgControl();
2948
+ const control = ngControl?.control;
2949
+ if (!control) {
2950
+ return;
2951
+ }
2952
+ if (wasPristine && ngControl.dirty && typeof control.markAsPristine === 'function') {
2953
+ control.markAsPristine({ onlySelf: true });
2954
+ }
2955
+ if (wasUntouched && ngControl.touched && typeof control.markAsUntouched === 'function') {
2956
+ control.markAsUntouched({ onlySelf: true });
2957
+ }
2958
+ }
2337
2959
  /** It writes the value in the input */
2338
2960
  async writeValue(controlValue) {
2961
+ if (!this._configApplied && this.mask()) {
2962
+ // Called before the first ngOnChanges pass configured the mask service (happens with
2963
+ // Signal Forms' [formField], whose control-sync instruction runs before sibling
2964
+ // directives' ngOnChanges). Defer and replay once the configuration is applied.
2965
+ this._pendingInitialValue = controlValue;
2966
+ this._hasPendingInitialValue = true;
2967
+ return;
2968
+ }
2969
+ // Skip the model echo of a value we just propagated ourselves (see the
2970
+ // _lastPropagatedValue doc). Empty writes are never skipped: form reset('') must
2971
+ // always clear the display and the currentValue/previousValue service state below.
2972
+ const lastPropagated = this._lastPropagatedValue;
2973
+ this._lastPropagatedValue = null;
2974
+ if (lastPropagated !== null &&
2975
+ lastPropagated !== '' &&
2976
+ (typeof controlValue === 'string' || typeof controlValue === 'number') &&
2977
+ String(controlValue) === lastPropagated) {
2978
+ return;
2979
+ }
2980
+ const ngControl = this._resolveNgControl();
2981
+ const wasPristine = ngControl ? Boolean(ngControl.pristine) : true;
2982
+ const wasUntouched = ngControl ? Boolean(ngControl.untouched) : true;
2339
2983
  let value = controlValue;
2340
2984
  const inputTransformFn = this._maskService.inputTransformFn;
2341
2985
  if (typeof value === 'object' && value !== null && 'value' in value) {
@@ -2380,8 +3024,30 @@ class NgxMaskDirective {
2380
3024
  .replace(MaskExpression.DOT, MaskExpression.COMMA);
2381
3025
  }
2382
3026
  if (this.mask()?.startsWith(MaskExpression.SEPARATOR) && this.leadZero()) {
3027
+ const isFirstWrite = !this._maskService.isInitialized;
2383
3028
  requestAnimationFrame(() => {
3029
+ // On initial load, temporarily set isInitialized to false
3030
+ // so formControlResult returns early and doesn't mark form as dirty.
3031
+ // On later writeValue-driven writes, leave isInitialized as-is: this pass
3032
+ // may legitimately need to push a leadZero-normalized value (e.g.
3033
+ // '10.2' -> '10.20') back to the FormControl, which Angular's forms
3034
+ // pipeline can only do via the same onChange callback used for real user
3035
+ // edits — see the `should change formValue` unit tests, which assert the
3036
+ // normalized value lands in `form.value` after a programmatic setValue.
3037
+ if (isFirstWrite) {
3038
+ this._maskService.isInitialized = false;
3039
+ }
2384
3040
  this._maskService.applyMask(inputValue?.toString() ?? '', this._maskService.maskExpression);
3041
+ if (isFirstWrite) {
3042
+ this._maskService.isInitialized = true;
3043
+ }
3044
+ // The leadZero normalization above emits the corrected value through the
3045
+ // view-change pipeline, which dirties/touches the control. Undo that so a
3046
+ // programmatic write stays pristine.
3047
+ this._restoreControlStateAfterWrite(wasPristine, wasUntouched);
3048
+ // Zoneless CD does not auto-flush after requestAnimationFrame; request
3049
+ // a check so form.pristine/form.value bindings reflect the new state.
3050
+ this._changeDetectorRef.markForCheck();
2385
3051
  });
2386
3052
  }
2387
3053
  this._maskService.isNumberValue = true;
@@ -2396,48 +3062,87 @@ class NgxMaskDirective {
2396
3062
  (this._maskService.prefix || this._maskService.showMaskTyped))) {
2397
3063
  // Let the service we know we are writing value so that triggering onChange function won't happen during applyMask
2398
3064
  this._maskService.writingValue = true;
2399
- this._maskService.formElementProperty = [
2400
- 'value',
2401
- this._maskService.applyMask(inputValue, this._maskService.maskExpression),
2402
- ];
3065
+ const displayValue = this._maskedOrVerbatim(inputValue);
3066
+ this._maskService.formElementProperty = ['value', displayValue];
3067
+ this._writeElementValueSync(displayValue);
2403
3068
  // Let the service know we've finished writing value
2404
3069
  this._maskService.writingValue = false;
2405
3070
  this._maskService.isInitialized = true;
2406
3071
  }
2407
3072
  else {
2408
3073
  this._maskService.formElementProperty = ['value', inputValue];
3074
+ this._writeElementValueSync(inputValue);
2409
3075
  this._maskService.isInitialized = true;
2410
3076
  }
2411
- this._inputValue.set(inputValue);
3077
+ // A writeValue-driven emission may have dirtied/touched the control via the
3078
+ // view-change pipeline; undo that so programmatic writes stay pristine.
3079
+ this._restoreControlStateAfterWrite(wasPristine, wasUntouched);
3080
+ // Programmatic writes (setValue/patchValue called outside a signal/effect context)
3081
+ // don't schedule CD under zoneless change detection. Request a check so bindings
3082
+ // reading form.pristine/form.dirty/form.value on the host template stay in sync.
3083
+ this._changeDetectorRef.markForCheck();
2412
3084
  }
2413
3085
  else {
2414
3086
  // eslint-disable-next-line no-console
2415
3087
  console.warn('Ngx-mask writeValue work with string | number, your current value:', typeof value);
2416
3088
  }
2417
3089
  }
3090
+ /**
3091
+ * Mirrors a writeValue-driven render into the DOM synchronously, in the same
3092
+ * change-detection pass (#1305). The service's `formElementProperty` setter defers all
3093
+ * writes via queueMicrotask (to keep FIFO ordering with config-driven re-renders and
3094
+ * dodge ExpressionChanged issues), but consumers that read `nativeElement.value` DURING
3095
+ * the CD pass — Angular Material's floating label (`MatInput.empty`), CDK autofill —
3096
+ * never see a value that only lands in a later microtask. The deferred write still runs
3097
+ * afterwards and re-applies the same final value, so ordering guarantees are preserved.
3098
+ *
3099
+ * Skipped while a mask reconfiguration is pending (`mask()` input changed but
3100
+ * `ngOnChanges` has not applied it yet — e.g. `mask.set(...)` + `setValue(...)` before
3101
+ * the next CD pass): the value just computed used the STALE mask config, and rendering
3102
+ * it synchronously would expose an intermediate state that the deferred pipeline is
3103
+ * about to supersede. Multi-masks (`||`) resolve `_maskValue` to one alternative and
3104
+ * therefore also fall back to the deferred-only path.
3105
+ */
3106
+ _writeElementValueSync(value) {
3107
+ if ((this.mask() ?? MaskExpression.EMPTY_STRING) !== this._maskValue()) {
3108
+ return;
3109
+ }
3110
+ this._renderer.setProperty(this._elementRef.nativeElement, 'value', value);
3111
+ }
2418
3112
  registerOnChange(fn) {
2419
- // Wrap the original onChange to also update Signal Forms value
3113
+ // Angular only calls this in classic ControlValueAccessor mode (reactive/template forms).
3114
+ // Its invocation is therefore our reliable signal that we are NOT in Signal Forms mode.
3115
+ this._isCvaMode.set(true);
2420
3116
  const originalFn = fn;
2421
3117
  this._maskService.onChange = this.onChange = (value) => {
2422
3118
  originalFn(value);
2423
- // Update Signal Forms value model if in use
2424
- if (this._isSignalFormsMode()) {
2425
- const stringValue = value === null || typeof value === 'undefined' ? '' : String(value);
2426
- if (this.value() !== stringValue) {
2427
- this.value.set(stringValue);
2428
- }
2429
- }
3119
+ this._propagateToValueModel(value);
2430
3120
  };
2431
3121
  }
2432
3122
  registerOnTouched(fn) {
2433
3123
  this.onTouch = () => {
2434
3124
  fn();
2435
- // Update Signal Forms touched state
2436
- if (this._isSignalFormsMode() && !this.touched()) {
3125
+ if (!this.touched()) {
2437
3126
  this.touched.set(true);
2438
3127
  }
2439
3128
  };
2440
3129
  }
3130
+ /**
3131
+ * Pushes the current unmasked value into the `value` model input. In Signal Forms mode this
3132
+ * fires the `valueChange` output that Angular listens to; in CVA mode it is a harmless write
3133
+ * to a model nobody reads. We flag `_skipNextValueEffect` so the resulting model change does
3134
+ * not bounce back through the value effect and overwrite the raw `_inputValue`.
3135
+ */
3136
+ _propagateToValueModel(value) {
3137
+ const stringValue = value === null || typeof value === 'undefined' ? '' : String(value);
3138
+ this._lastPropagatedValue = stringValue;
3139
+ untracked(() => {
3140
+ if (String(this.value()) !== stringValue) {
3141
+ this._skipNextValueEffect.set(true);
3142
+ this.value.set(stringValue);
3143
+ }
3144
+ });
3145
+ }
2441
3146
  /**
2442
3147
  * Focus the input element.
2443
3148
  * Required by FormValueControl interface for Signal Forms.
@@ -2469,9 +3174,37 @@ class NgxMaskDirective {
2469
3174
  this._maskService.maskExpression = this._maskService._repeatPatternSymbols(this._maskValue() || '');
2470
3175
  this._maskService.formElementProperty = [
2471
3176
  'value',
2472
- this._maskService.applyMask(this._inputValue(), this._maskService.maskExpression),
3177
+ this._maskedOrVerbatim(this._inputValue()),
2473
3178
  ];
2474
3179
  }
3180
+ /**
3181
+ * Renders `inputValue` through the mask, falling back to the raw value verbatim when the
3182
+ * mask cannot process ANY of it (#1615, e.g. a sentinel like 'ONGOING' written into a
3183
+ * digits-only control). Values that PARTIALLY match keep regular masking.
3184
+ *
3185
+ * Shared by writeValue() and _applyMask() (called from every ngOnChanges pass, including
3186
+ * ones triggered by an UNRELATED input like `disabled`) so the verbatim verdict for a
3187
+ * value written once via writeValue() is not lost on a later re-render that replays the
3188
+ * same raw `inputValue` outside of writeValue — which would otherwise re-run regular
3189
+ * masking, produce an empty result, and emit it through onChange, clobbering the model.
3190
+ *
3191
+ * Excludes an actual mask RECONFIGURATION (`maskChanged`): when the mask itself just
3192
+ * changed, a value that no longer matches must clear through the regular path (see
3193
+ * trigger-on-mask-change.spec.ts) — verbatim passthrough only covers re-renders of the
3194
+ * SAME mask.
3195
+ */
3196
+ _maskedOrVerbatim(inputValue) {
3197
+ // Snapshot before applyMask(): it resets maskChanged internally as part of emitting
3198
+ // (or skipping) the change, so it must be read before the call, not after.
3199
+ const wasMaskChanged = this._maskService.maskChanged;
3200
+ const maskedResult = this._maskService.applyMask(inputValue, this._maskService.maskExpression);
3201
+ return !maskedResult &&
3202
+ inputValue &&
3203
+ !wasMaskChanged &&
3204
+ this._maskService.removeMask(inputValue)
3205
+ ? inputValue
3206
+ : maskedResult;
3207
+ }
2475
3208
  _validateTime(value) {
2476
3209
  const rowMaskLen = this._maskValue()
2477
3210
  .split(MaskExpression.EMPTY_STRING)
@@ -2489,6 +3222,98 @@ class NgxMaskDirective {
2489
3222
  return (this._maskService.actualValue.length ||
2490
3223
  this._maskService.actualValue.length + this._maskService.prefix.length);
2491
3224
  }
3225
+ /**
3226
+ * For `||` multi-masks only (#1583): a value shorter than the currently selected
3227
+ * alternative is still valid when it is a pattern-valid prefix of that alternative
3228
+ * ending exactly at a special-character boundary (e.g. `0` for `0,N`) and it meets
3229
+ * the length requirement of at least one alternative (e.g. `1`). Values stopping
3230
+ * mid-pattern-block (e.g. `112A` for `000SS`) remain invalid.
3231
+ */
3232
+ _isCompleteAlternativeBoundary(processedValue) {
3233
+ const alternatives = this._maskExpressionArray();
3234
+ if (!alternatives.length) {
3235
+ return false;
3236
+ }
3237
+ const maskValue = this._maskValue();
3238
+ const cleanValue = this._maskService.removeMask(processedValue);
3239
+ const cleanMask = this._maskService.removeMask(maskValue);
3240
+ const isPatternPrefix = cleanValue
3241
+ .split(MaskExpression.EMPTY_STRING)
3242
+ .every((character, index) => this._maskService._checkSymbolMask(character, cleanMask.charAt(index)));
3243
+ if (!isPatternPrefix) {
3244
+ return false;
3245
+ }
3246
+ let patternCount = 0;
3247
+ let boundaryCharacter = MaskExpression.EMPTY_STRING;
3248
+ for (const maskCharacter of maskValue) {
3249
+ if (patternCount === cleanValue.length) {
3250
+ boundaryCharacter = maskCharacter;
3251
+ break;
3252
+ }
3253
+ if (!this._maskService.specialCharacters.includes(maskCharacter)) {
3254
+ patternCount++;
3255
+ }
3256
+ }
3257
+ if (!boundaryCharacter ||
3258
+ !this._maskService.specialCharacters.includes(boundaryCharacter)) {
3259
+ return false;
3260
+ }
3261
+ return alternatives.some((alternative) => {
3262
+ const requiredLength = this._maskService.dropSpecialCharacters
3263
+ ? alternative.length - this._maskService.checkDropSpecialCharAmount(alternative)
3264
+ : this.prefix()
3265
+ ? alternative.length + this.prefix().length
3266
+ : alternative.length;
3267
+ return processedValue.length >= requiredLength;
3268
+ });
3269
+ }
3270
+ /**
3271
+ * True when every character of the mask expression is either a pattern token or a
3272
+ * special character — i.e. the mask has no quantifiers (`*`, `?`), curly-bracket
3273
+ * repetitions or other constructs the position-aware matcher does not model.
3274
+ */
3275
+ _isPlainTokenMask(mask) {
3276
+ return mask
3277
+ .split(MaskExpression.EMPTY_STRING)
3278
+ .every((symbol) => !!this._maskService.patterns[symbol] ||
3279
+ this._maskService.specialCharacters.includes(symbol));
3280
+ }
3281
+ /**
3282
+ * Backtracking match of a value against a mask mixing optional and mandatory pattern
3283
+ * tokens (#1515, e.g. `999SSS`). Optional tokens may be left unfilled; special
3284
+ * characters may be absent from the value (dropSpecialCharacters). The value is valid
3285
+ * when it is fully consumed and every remaining mask token is optional or special.
3286
+ */
3287
+ _matchesMaskWithOptionalSkip(value, mask) {
3288
+ const patterns = this._maskService.patterns;
3289
+ const match = (maskIndex, valueIndex) => {
3290
+ if (valueIndex === value.length) {
3291
+ return mask
3292
+ .slice(maskIndex)
3293
+ .split(MaskExpression.EMPTY_STRING)
3294
+ .every((symbol) => !patterns[symbol] || !!patterns[symbol]?.optional);
3295
+ }
3296
+ if (maskIndex === mask.length) {
3297
+ return false;
3298
+ }
3299
+ const maskSymbol = mask[maskIndex];
3300
+ const valueSymbol = value[valueIndex];
3301
+ const pattern = patterns[maskSymbol];
3302
+ if (pattern) {
3303
+ if (pattern.pattern.test(valueSymbol) && match(maskIndex + 1, valueIndex + 1)) {
3304
+ return true;
3305
+ }
3306
+ return !!pattern.optional && match(maskIndex + 1, valueIndex);
3307
+ }
3308
+ // Special character: consume it when present in the value, otherwise treat it
3309
+ // as dropped (dropSpecialCharacters).
3310
+ if (valueSymbol === maskSymbol && match(maskIndex + 1, valueIndex + 1)) {
3311
+ return true;
3312
+ }
3313
+ return match(maskIndex + 1, valueIndex);
3314
+ };
3315
+ return match(0, 0);
3316
+ }
2492
3317
  _createValidationError(actualValue) {
2493
3318
  return {
2494
3319
  mask: {
@@ -2555,8 +3380,8 @@ class NgxMaskDirective {
2555
3380
  return uniqueCharacters.size === 1;
2556
3381
  });
2557
3382
  }
2558
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.1", ngImport: i0, type: NgxMaskDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
2559
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.1.1", 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 }, 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 }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, errors: { classPropertyName: "errors", publicName: "errors", 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 }, dirty: { classPropertyName: "dirty", publicName: "dirty", isSignal: true, isRequired: false, transformFunction: null }, invalid: { classPropertyName: "invalid", publicName: "invalid", isSignal: true, isRequired: false, transformFunction: null }, pending: { classPropertyName: "pending", publicName: "pending", isSignal: true, isRequired: false, transformFunction: null }, readonly: { classPropertyName: "readonly", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, name: { classPropertyName: "name", publicName: "name", 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: [
3383
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: NgxMaskDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
3384
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.17", 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 }, 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 }, 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: [
2560
3385
  {
2561
3386
  provide: NG_VALUE_ACCESSOR,
2562
3387
  useExisting: NgxMaskDirective,
@@ -2570,7 +3395,7 @@ class NgxMaskDirective {
2570
3395
  NgxMaskService,
2571
3396
  ], exportAs: ["mask", "ngxMask"], usesOnChanges: true, ngImport: i0 });
2572
3397
  }
2573
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.1", ngImport: i0, type: NgxMaskDirective, decorators: [{
3398
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: NgxMaskDirective, decorators: [{
2574
3399
  type: Directive,
2575
3400
  args: [{
2576
3401
  selector: 'input[mask], textarea[mask]',
@@ -2590,7 +3415,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.1", ngImpor
2590
3415
  ],
2591
3416
  exportAs: 'mask,ngxMask',
2592
3417
  }]
2593
- }], ctorParameters: () => [], propDecorators: { mask: [{ type: i0.Input, args: [{ isSignal: true, alias: "mask", required: false }] }], specialCharacters: [{ type: i0.Input, args: [{ isSignal: true, alias: "specialCharacters", required: false }] }], patterns: [{ type: i0.Input, args: [{ isSignal: true, alias: "patterns", required: false }] }], prefix: [{ type: i0.Input, args: [{ isSignal: true, alias: "prefix", required: false }] }], suffix: [{ type: i0.Input, args: [{ isSignal: true, alias: "suffix", required: false }] }], thousandSeparator: [{ type: i0.Input, args: [{ isSignal: true, alias: "thousandSeparator", required: false }] }], decimalMarker: [{ type: i0.Input, args: [{ isSignal: true, alias: "decimalMarker", required: false }] }], dropSpecialCharacters: [{ type: i0.Input, args: [{ isSignal: true, alias: "dropSpecialCharacters", required: false }] }], hiddenInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "hiddenInput", required: false }] }], showMaskTyped: [{ type: i0.Input, args: [{ isSignal: true, alias: "showMaskTyped", required: false }] }], placeHolderCharacter: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeHolderCharacter", required: false }] }], shownMaskExpression: [{ type: i0.Input, args: [{ isSignal: true, alias: "shownMaskExpression", required: false }] }], clearIfNotMatch: [{ type: i0.Input, args: [{ isSignal: true, alias: "clearIfNotMatch", required: false }] }], validation: [{ type: i0.Input, args: [{ isSignal: true, alias: "validation", required: false }] }], separatorLimit: [{ type: i0.Input, args: [{ isSignal: true, alias: "separatorLimit", required: false }] }], allowNegativeNumbers: [{ type: i0.Input, args: [{ isSignal: true, alias: "allowNegativeNumbers", required: false }] }], leadZeroDateTime: [{ type: i0.Input, args: [{ isSignal: true, alias: "leadZeroDateTime", required: false }] }], leadZero: [{ type: i0.Input, args: [{ isSignal: true, alias: "leadZero", required: false }] }], triggerOnMaskChange: [{ type: i0.Input, args: [{ isSignal: true, alias: "triggerOnMaskChange", required: false }] }], apm: [{ type: i0.Input, args: [{ isSignal: true, alias: "apm", required: false }] }], inputTransformFn: [{ type: i0.Input, args: [{ isSignal: true, alias: "inputTransformFn", required: false }] }], outputTransformFn: [{ type: i0.Input, args: [{ isSignal: true, alias: "outputTransformFn", required: false }] }], keepCharacterPositions: [{ type: i0.Input, args: [{ isSignal: true, alias: "keepCharacterPositions", required: false }] }], instantPrefix: [{ type: i0.Input, args: [{ isSignal: true, alias: "instantPrefix", required: false }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], errors: [{ type: i0.Input, args: [{ isSignal: true, alias: "errors", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], touched: [{ type: i0.Input, args: [{ isSignal: true, alias: "touched", required: false }] }, { type: i0.Output, args: ["touchedChange"] }], dirty: [{ type: i0.Input, args: [{ isSignal: true, alias: "dirty", required: false }] }], invalid: [{ type: i0.Input, args: [{ isSignal: true, alias: "invalid", required: false }] }], pending: [{ type: i0.Input, args: [{ isSignal: true, alias: "pending", required: false }] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], name: [{ type: i0.Input, args: [{ isSignal: true, alias: "name", required: false }] }], maskFilled: [{ type: i0.Output, args: ["maskFilled"] }], onPaste: [{
3418
+ }], ctorParameters: () => [], propDecorators: { mask: [{ type: i0.Input, args: [{ isSignal: true, alias: "mask", required: false }] }], specialCharacters: [{ type: i0.Input, args: [{ isSignal: true, alias: "specialCharacters", required: false }] }], patterns: [{ type: i0.Input, args: [{ isSignal: true, alias: "patterns", required: false }] }], prefix: [{ type: i0.Input, args: [{ isSignal: true, alias: "prefix", required: false }] }], suffix: [{ type: i0.Input, args: [{ isSignal: true, alias: "suffix", required: false }] }], thousandSeparator: [{ type: i0.Input, args: [{ isSignal: true, alias: "thousandSeparator", required: false }] }], decimalMarker: [{ type: i0.Input, args: [{ isSignal: true, alias: "decimalMarker", required: false }] }], dropSpecialCharacters: [{ type: i0.Input, args: [{ isSignal: true, alias: "dropSpecialCharacters", required: false }] }], hiddenInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "hiddenInput", required: false }] }], showMaskTyped: [{ type: i0.Input, args: [{ isSignal: true, alias: "showMaskTyped", required: false }] }], placeHolderCharacter: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeHolderCharacter", required: false }] }], shownMaskExpression: [{ type: i0.Input, args: [{ isSignal: true, alias: "shownMaskExpression", required: false }] }], clearIfNotMatch: [{ type: i0.Input, args: [{ isSignal: true, alias: "clearIfNotMatch", required: false }] }], validation: [{ type: i0.Input, args: [{ isSignal: true, alias: "validation", required: false }] }], separatorLimit: [{ type: i0.Input, args: [{ isSignal: true, alias: "separatorLimit", required: false }] }], allowNegativeNumbers: [{ type: i0.Input, args: [{ isSignal: true, alias: "allowNegativeNumbers", required: false }] }], leadZeroDateTime: [{ type: i0.Input, args: [{ isSignal: true, alias: "leadZeroDateTime", required: false }] }], leadZero: [{ type: i0.Input, args: [{ isSignal: true, alias: "leadZero", required: false }] }], triggerOnMaskChange: [{ type: i0.Input, args: [{ isSignal: true, alias: "triggerOnMaskChange", required: false }] }], apm: [{ type: i0.Input, args: [{ isSignal: true, alias: "apm", required: false }] }], inputTransformFn: [{ type: i0.Input, args: [{ isSignal: true, alias: "inputTransformFn", required: false }] }], outputTransformFn: [{ type: i0.Input, args: [{ isSignal: true, alias: "outputTransformFn", required: false }] }], keepCharacterPositions: [{ type: i0.Input, args: [{ isSignal: true, alias: "keepCharacterPositions", required: false }] }], instantPrefix: [{ type: i0.Input, args: [{ isSignal: true, alias: "instantPrefix", required: false }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], touched: [{ type: i0.Input, args: [{ isSignal: true, alias: "touched", required: false }] }, { type: i0.Output, args: ["touchedChange"] }], maskFilled: [{ type: i0.Output, args: ["maskFilled"] }], onPaste: [{
2594
3419
  type: HostListener,
2595
3420
  args: ['paste']
2596
3421
  }], onFocus: [{
@@ -2703,10 +3528,10 @@ class NgxMaskPipe {
2703
3528
  });
2704
3529
  }
2705
3530
  }
2706
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.1", ngImport: i0, type: NgxMaskPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
2707
- static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.1", ngImport: i0, type: NgxMaskPipe, isStandalone: true, name: "mask" });
3531
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: NgxMaskPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
3532
+ static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.2.17", ngImport: i0, type: NgxMaskPipe, isStandalone: true, name: "mask" });
2708
3533
  }
2709
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.1", ngImport: i0, type: NgxMaskPipe, decorators: [{
3534
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: NgxMaskPipe, decorators: [{
2710
3535
  type: Pipe,
2711
3536
  args: [{
2712
3537
  name: 'mask',