ngx-mask 21.0.1 → 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;
@@ -659,15 +728,22 @@ class NgxMaskApplierService {
659
728
  i--;
660
729
  }
661
730
  else if (this.maskExpression[cursor + 1] === MaskExpression.SYMBOL_STAR &&
662
- this._findSpecialChar(this.maskExpression[cursor + 2] ?? MaskExpression.EMPTY_STRING) &&
663
- 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]) &&
664
739
  multi) {
665
740
  cursor += 3;
666
741
  result += inputSymbol;
667
742
  }
668
743
  else if (this.maskExpression[cursor + 1] === MaskExpression.SYMBOL_QUESTION &&
669
- this._findSpecialChar(this.maskExpression[cursor + 2] ?? MaskExpression.EMPTY_STRING) &&
670
- 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]) &&
671
747
  multi) {
672
748
  cursor += 3;
673
749
  result += inputSymbol;
@@ -713,9 +789,20 @@ class NgxMaskApplierService {
713
789
  const isSpecialCharacterMaskFirstSymbol = processedValue.length === 1 &&
714
790
  this.specialCharacters.includes(maskExpression[0]) &&
715
791
  processedValue !== maskExpression[0];
716
- if (!this._checkSymbolMask(processedValue, maskExpression[1]) &&
717
- isSpecialCharacterMaskFirstSymbol) {
718
- 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
+ }
719
806
  }
720
807
  if (result.includes(MaskExpression.MINUS) && this.prefix && this.allowNegativeNumbers) {
721
808
  if (backspaced && result === MaskExpression.MINUS) {
@@ -794,11 +881,21 @@ class NgxMaskApplierService {
794
881
  checkAndRemoveSuffix = (inputValue) => {
795
882
  for (let i = this.suffix?.length - 1; i >= 0; i--) {
796
883
  const substr = this.suffix.substring(i, this.suffix?.length);
797
- if (inputValue.includes(substr) &&
884
+ if (inputValue.endsWith(substr) &&
798
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)) &&
799
896
  (i - 1 < 0 ||
800
- !inputValue.includes(this.suffix.substring(i - 1, this.suffix?.length)))) {
801
- 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);
802
899
  }
803
900
  }
804
901
  return inputValue;
@@ -912,10 +1009,10 @@ class NgxMaskApplierService {
912
1009
  nonZeroIndex,
913
1010
  };
914
1011
  }
915
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: NgxMaskApplierService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
916
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.2", 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 });
917
1014
  }
918
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: NgxMaskApplierService, decorators: [{
1015
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: NgxMaskApplierService, decorators: [{
919
1016
  type: Injectable
920
1017
  }] });
921
1018
 
@@ -928,12 +1025,20 @@ class NgxMaskService extends NgxMaskApplierService {
928
1025
  maskExpressionArray = [];
929
1026
  previousValue = '';
930
1027
  currentValue = '';
1028
+ // `writingValue` is declared on NgxMaskApplierService: applyMask needs it to
1029
+ // distinguish the writeValue flow from keystroke flows (#1611).
1030
+ isInitialized = false;
931
1031
  /**
932
- * Whether we are currently in writeValue function, in this case when applying the mask we don't want to trigger onChange function,
933
- * 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).
934
1039
  */
935
- writingValue = false;
936
- isInitialized = false;
1040
+ keepCharacterPositionsHandled = null;
1041
+ _isFocused = signal(false, ...(ngDevMode ? [{ debugName: "_isFocused" }] : /* istanbul ignore next */ []));
937
1042
  _emitValue = false;
938
1043
  _start;
939
1044
  _end;
@@ -956,10 +1061,35 @@ class NgxMaskService extends NgxMaskApplierService {
956
1061
  applyMask(inputValue, maskExpression, position = 0, justPasted = false, backspaced = false,
957
1062
  // eslint-disable-next-line @typescript-eslint/no-empty-function
958
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;
959
1069
  // If no mask expression, return the input value or the actual value
960
1070
  if (!maskExpression) {
961
1071
  return inputValue !== this.actualValue ? this.actualValue : inputValue;
962
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
+ }
963
1093
  // Show mask in input if required
964
1094
  this.maskIsShown = this.showMaskTyped
965
1095
  ? this.showMaskInInput()
@@ -968,7 +1098,9 @@ class NgxMaskService extends NgxMaskApplierService {
968
1098
  if (this.maskExpression === MaskExpression.IP && this.showMaskTyped) {
969
1099
  this.maskIsShown = this.showMaskInInput(inputValue || MaskExpression.HASH);
970
1100
  }
971
- 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) {
972
1104
  this.maskIsShown = this.showMaskInInput(inputValue || MaskExpression.HASH);
973
1105
  }
974
1106
  // Handle empty input value with mask typed
@@ -985,7 +1117,11 @@ class NgxMaskService extends NgxMaskApplierService {
985
1117
  if ((this.hiddenInput ||
986
1118
  (inputValue && inputValue.indexOf(MaskExpression.SYMBOL_STAR) >= 0)) &&
987
1119
  !this.writingValue) {
988
- 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
989
1125
  ? inputValue.split(MaskExpression.EMPTY_STRING)
990
1126
  : this.actualValue.split(MaskExpression.EMPTY_STRING);
991
1127
  // Handle backspace
@@ -1085,8 +1221,10 @@ class NgxMaskService extends NgxMaskApplierService {
1085
1221
  newInputValue =
1086
1222
  Boolean(newInputValue) && newInputValue.length ? newInputValue : inputValue;
1087
1223
  }
1088
- // Handle showMaskTyped and keepCharacterPositions
1089
- 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) &&
1090
1228
  this.keepCharacterPositions &&
1091
1229
  this.actualValue &&
1092
1230
  !justPasted &&
@@ -1101,6 +1239,27 @@ class NgxMaskService extends NgxMaskApplierService {
1101
1239
  }
1102
1240
  // Apply the mask using the parent class method
1103
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
+ }
1104
1263
  this.actualValue = this.getActualValue(result);
1105
1264
  // handle some separator implications:
1106
1265
  // a.) adjust decimalMarker default (. -> ,) if thousandSeparator is a dot
@@ -1144,14 +1303,19 @@ class NgxMaskService extends NgxMaskApplierService {
1144
1303
  const resLen = result.length;
1145
1304
  const prefNmask = `${this.prefix}${this.maskIsShown}${this.suffix}`;
1146
1305
  // Handle specific mask expressions
1147
- 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)) {
1148
1316
  const countSkipedSymbol = this._numberSkipedSymbols(result);
1149
1317
  return `${result}${prefNmask.slice(resLen + countSkipedSymbol)}`;
1150
1318
  }
1151
- else if (this.maskExpression === MaskExpression.IP ||
1152
- this.maskExpression === MaskExpression.CPF_CNPJ) {
1153
- return `${result}${prefNmask}`;
1154
- }
1155
1319
  return `${result}${prefNmask.slice(resLen)}`;
1156
1320
  }
1157
1321
  // get the number of characters that were shifted
@@ -1241,12 +1405,34 @@ class NgxMaskService extends NgxMaskApplierService {
1241
1405
  String(value).length > 14)) {
1242
1406
  return String(value);
1243
1407
  }
1244
- return Number(value)
1245
- .toLocaleString('fullwide', {
1246
- useGrouping: false,
1247
- maximumFractionDigits: 20,
1248
- })
1249
- .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)}`;
1250
1436
  }
1251
1437
  showMaskInInput(inputVal) {
1252
1438
  if (this.showMaskTyped && !!this.shownMaskExpression) {
@@ -1262,7 +1448,8 @@ class NgxMaskService extends NgxMaskApplierService {
1262
1448
  if (this.maskExpression === MaskExpression.IP) {
1263
1449
  return this._checkForIp(inputVal);
1264
1450
  }
1265
- if (this.maskExpression === MaskExpression.CPF_CNPJ) {
1451
+ if (this.maskExpression === MaskExpression.CPF_CNPJ ||
1452
+ this.maskExpression === MaskExpression.CPF_CNPJ_ALPHA) {
1266
1453
  return this._checkForCpfCnpj(inputVal);
1267
1454
  }
1268
1455
  }
@@ -1348,28 +1535,41 @@ class NgxMaskService extends NgxMaskApplierService {
1348
1535
  if (inputVal === MaskExpression.HASH) {
1349
1536
  return cpf;
1350
1537
  }
1351
- const arr = [];
1352
- // eslint-disable-next-line @typescript-eslint/prefer-for-of
1353
- for (let i = 0; i < inputVal.length; i++) {
1354
- const value = inputVal[i] ?? MaskExpression.EMPTY_STRING;
1355
- if (!value) {
1356
- 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);
1357
1548
  }
1358
- if (value.match('\\d')) {
1359
- arr.push(value);
1549
+ if (arr.length > 2 && arr.length <= 5) {
1550
+ return cnpj.slice(arr.length + 1, cnpj.length);
1360
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);
1361
1559
  }
1362
- if (arr.length <= 3) {
1363
- return cpf.slice(arr.length, cpf.length);
1364
- }
1365
- if (arr.length > 3 && arr.length <= 6) {
1366
- return cpf.slice(arr.length + 1, cpf.length);
1367
- }
1368
- if (arr.length > 6 && arr.length <= 9) {
1369
- return cpf.slice(arr.length + 2, cpf.length);
1370
- }
1371
- if (arr.length > 9 && arr.length < 11) {
1372
- 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
+ }
1373
1573
  }
1374
1574
  if (arr.length === 11) {
1375
1575
  return '';
@@ -1385,6 +1585,22 @@ class NgxMaskService extends NgxMaskApplierService {
1385
1585
  }
1386
1586
  return '';
1387
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
+ }
1388
1604
  /**
1389
1605
  * Recursively determine the current active element by navigating the Shadow DOM until the Active Element is found.
1390
1606
  */
@@ -1413,6 +1629,20 @@ class NgxMaskService extends NgxMaskApplierService {
1413
1629
  if (!this.isInitialized && this._emitValue) {
1414
1630
  return;
1415
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
+ }
1416
1646
  if (Array.isArray(this.dropSpecialCharacters)) {
1417
1647
  this.onChange(outputTransformFn(this._toNumber(this._checkSymbols(this._removeMask(this._removeSuffix(this._removePrefix(inputValue)), this.dropSpecialCharacters)))));
1418
1648
  }
@@ -1545,17 +1775,59 @@ class NgxMaskService extends NgxMaskApplierService {
1545
1775
  _checkPrecision(separatorExpression, separatorValue) {
1546
1776
  const separatorPrecision = this.getPrecision(separatorExpression);
1547
1777
  let value = separatorValue;
1548
- if (separatorExpression.indexOf('2') > 0 ||
1549
- (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))) {
1550
1783
  if (this.decimalMarker === MaskExpression.COMMA && this.leadZero) {
1551
1784
  value = value.replace(',', '.');
1552
1785
  }
1553
- return this.leadZero
1554
- ? Number(value).toFixed(Number(separatorPrecision))
1555
- : 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);
1556
1795
  }
1557
1796
  return this.numberToString(value);
1558
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
+ }
1559
1831
  _repeatPatternSymbols(maskExp) {
1560
1832
  return ((maskExp.match(/{[0-9]+}/) &&
1561
1833
  maskExp
@@ -1582,13 +1854,25 @@ class NgxMaskService extends NgxMaskApplierService {
1582
1854
  }, '')) ||
1583
1855
  maskExp);
1584
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
+ */
1585
1869
  currentLocaleDecimalMarker() {
1586
- return (1.1).toLocaleString().substring(1, 2);
1870
+ return MaskExpression.DOT;
1587
1871
  }
1588
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: NgxMaskService, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
1589
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.2", 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 });
1590
1874
  }
1591
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: NgxMaskService, decorators: [{
1875
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: NgxMaskService, decorators: [{
1592
1876
  type: Injectable
1593
1877
  }] });
1594
1878
 
@@ -1625,82 +1909,160 @@ function provideEnvironmentNgxMask(configValue) {
1625
1909
 
1626
1910
  class NgxMaskDirective {
1627
1911
  // ===== Mask Configuration Inputs =====
1628
- mask = input('', ...(ngDevMode ? [{ debugName: "mask" }] : []));
1629
- specialCharacters = input([], ...(ngDevMode ? [{ debugName: "specialCharacters" }] : []));
1630
- patterns = input({}, ...(ngDevMode ? [{ debugName: "patterns" }] : []));
1631
- prefix = input('', ...(ngDevMode ? [{ debugName: "prefix" }] : []));
1632
- suffix = input('', ...(ngDevMode ? [{ debugName: "suffix" }] : []));
1633
- thousandSeparator = input(' ', ...(ngDevMode ? [{ debugName: "thousandSeparator" }] : []));
1634
- decimalMarker = input('.', ...(ngDevMode ? [{ debugName: "decimalMarker" }] : []));
1635
- dropSpecialCharacters = input(null, ...(ngDevMode ? [{ debugName: "dropSpecialCharacters" }] : []));
1636
- hiddenInput = input(null, ...(ngDevMode ? [{ debugName: "hiddenInput" }] : []));
1637
- showMaskTyped = input(null, ...(ngDevMode ? [{ debugName: "showMaskTyped" }] : []));
1638
- placeHolderCharacter = input(null, ...(ngDevMode ? [{ debugName: "placeHolderCharacter" }] : []));
1639
- shownMaskExpression = input(null, ...(ngDevMode ? [{ debugName: "shownMaskExpression" }] : []));
1640
- clearIfNotMatch = input(null, ...(ngDevMode ? [{ debugName: "clearIfNotMatch" }] : []));
1641
- validation = input(null, ...(ngDevMode ? [{ debugName: "validation" }] : []));
1642
- separatorLimit = input('', ...(ngDevMode ? [{ debugName: "separatorLimit" }] : []));
1643
- allowNegativeNumbers = input(null, ...(ngDevMode ? [{ debugName: "allowNegativeNumbers" }] : []));
1644
- leadZeroDateTime = input(null, ...(ngDevMode ? [{ debugName: "leadZeroDateTime" }] : []));
1645
- leadZero = input(null, ...(ngDevMode ? [{ debugName: "leadZero" }] : []));
1646
- triggerOnMaskChange = input(null, ...(ngDevMode ? [{ debugName: "triggerOnMaskChange" }] : []));
1647
- apm = input(null, ...(ngDevMode ? [{ debugName: "apm" }] : []));
1648
- inputTransformFn = input(null, ...(ngDevMode ? [{ debugName: "inputTransformFn" }] : []));
1649
- outputTransformFn = input(null, ...(ngDevMode ? [{ debugName: "outputTransformFn" }] : []));
1650
- keepCharacterPositions = input(null, ...(ngDevMode ? [{ debugName: "keepCharacterPositions" }] : []));
1651
- instantPrefix = input(null, ...(ngDevMode ? [{ debugName: "instantPrefix" }] : []));
1652
- value = model('', ...(ngDevMode ? [{ debugName: "value" }] : []));
1653
- errors = input([], ...(ngDevMode ? [{ debugName: "errors" }] : []));
1654
- disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : []));
1655
- touched = model(false, ...(ngDevMode ? [{ debugName: "touched" }] : []));
1656
- dirty = input(false, ...(ngDevMode ? [{ debugName: "dirty" }] : []));
1657
- invalid = input(false, ...(ngDevMode ? [{ debugName: "invalid" }] : []));
1658
- pending = input(false, ...(ngDevMode ? [{ debugName: "pending" }] : []));
1659
- readonly = input(false, ...(ngDevMode ? [{ debugName: "readonly" }] : []));
1660
- required = input(false, ...(ngDevMode ? [{ debugName: "required" }] : []));
1661
- 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 */ []));
1662
1939
  maskFilled = output();
1663
- _maskValue = signal('', ...(ngDevMode ? [{ debugName: "_maskValue" }] : []));
1664
- _inputValue = signal('', ...(ngDevMode ? [{ debugName: "_inputValue" }] : []));
1665
- _position = signal(null, ...(ngDevMode ? [{ debugName: "_position" }] : []));
1666
- _code = signal('', ...(ngDevMode ? [{ debugName: "_code" }] : []));
1667
- _maskExpressionArray = signal([], ...(ngDevMode ? [{ debugName: "_maskExpressionArray" }] : []));
1668
- _justPasted = signal(false, ...(ngDevMode ? [{ debugName: "_justPasted" }] : []));
1669
- _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 */ []));
1670
1947
  /** For IME composition event */
1671
- _isComposing = signal(false, ...(ngDevMode ? [{ debugName: "_isComposing" }] : []));
1672
- /** Track if we're using Signal Forms mode */
1673
- _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;
1674
1996
  _maskService = inject(NgxMaskService, { self: true });
1675
1997
  document = inject(DOCUMENT);
1676
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
+ }
1677
2022
  // eslint-disable-next-line @typescript-eslint/no-empty-function
1678
2023
  onChange = (_) => { };
1679
2024
  // eslint-disable-next-line @typescript-eslint/no-empty-function
1680
2025
  onTouch = () => { };
1681
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.
1682
2037
  effect(() => {
1683
2038
  const signalValue = this.value();
1684
- if (this._isSignalFormsMode() && signalValue !== untracked(() => this._inputValue())) {
1685
- 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())) {
1686
2048
  this.writeValue(signalValue);
1687
- });
1688
- }
2049
+ }
2050
+ });
1689
2051
  });
1690
2052
  effect(() => {
1691
2053
  const isDisabled = this.disabled();
1692
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;
1693
2062
  this.setDisabledState(isDisabled);
1694
2063
  });
1695
2064
  });
1696
2065
  }
1697
- ngOnInit() {
1698
- // Detect if we're being used with Signal Forms
1699
- // Signal Forms will set the value model from outside
1700
- if (this.value() !== '') {
1701
- this._isSignalFormsMode.set(true);
1702
- }
1703
- }
1704
2066
  ngOnChanges(changes) {
1705
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;
1706
2068
  if (mask) {
@@ -1720,12 +2082,12 @@ class NgxMaskDirective {
1720
2082
  }
1721
2083
  }
1722
2084
  if (specialCharacters) {
1723
- if (!specialCharacters.currentValue || !Array.isArray(specialCharacters.currentValue)) {
1724
- return;
1725
- }
1726
- else {
1727
- this._maskService.specialCharacters = specialCharacters.currentValue || [];
2085
+ if (Array.isArray(specialCharacters.currentValue)) {
2086
+ this._maskService.specialCharacters = specialCharacters.currentValue;
1728
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).
1729
2091
  }
1730
2092
  if (allowNegativeNumbers) {
1731
2093
  this._maskService.allowNegativeNumbers = allowNegativeNumbers.currentValue;
@@ -1797,6 +2159,13 @@ class NgxMaskDirective {
1797
2159
  }
1798
2160
  if (placeHolderCharacter) {
1799
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
+ }
1800
2169
  }
1801
2170
  if (shownMaskExpression) {
1802
2171
  this._maskService.shownMaskExpression = shownMaskExpression.currentValue;
@@ -1829,6 +2198,17 @@ class NgxMaskDirective {
1829
2198
  this._maskService.keepCharacterPositions = keepCharacterPositions.currentValue;
1830
2199
  }
1831
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
+ }
1832
2212
  }
1833
2213
  validate({ value }) {
1834
2214
  const processedValue = typeof value === 'number' ? String(value) : value;
@@ -1887,9 +2267,31 @@ class NgxMaskDirective {
1887
2267
  else if (maskValue.indexOf(key) !== -1) {
1888
2268
  counterOfOpt++;
1889
2269
  }
1890
- if (maskValue.indexOf(key) !== -1 &&
1891
- processedValue.length >= maskValue.indexOf(key)) {
1892
- 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`).
1893
2295
  }
1894
2296
  if (counterOfOpt === maskValue.length) {
1895
2297
  return null;
@@ -1914,6 +2316,13 @@ class NgxMaskDirective {
1914
2316
  : maskValue.length - counterOfOpt;
1915
2317
  if (array.length === 1) {
1916
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
+ }
1917
2326
  return this._createValidationError(processedValue);
1918
2327
  }
1919
2328
  }
@@ -1956,6 +2365,7 @@ class NgxMaskDirective {
1956
2365
  }
1957
2366
  onFocus() {
1958
2367
  this._isFocused.set(true);
2368
+ this._maskService._isFocused.set(true);
1959
2369
  }
1960
2370
  onModelChange(value) {
1961
2371
  // on form reset we need to update the actualValue
@@ -1968,8 +2378,30 @@ class NgxMaskDirective {
1968
2378
  }
1969
2379
  onInput(e) {
1970
2380
  this._maskService.isInitialized = true;
1971
- // If IME is composing text, we wait for the composed text.
1972
- 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()) {
1973
2405
  return;
1974
2406
  }
1975
2407
  const el = e.target;
@@ -1978,51 +2410,90 @@ class NgxMaskDirective {
1978
2410
  : el.value;
1979
2411
  if (el.type !== 'number') {
1980
2412
  if (typeof transformedValue === 'string' || typeof transformedValue === 'number') {
1981
- 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
+ }
1982
2423
  this._inputValue.set(el.value);
1983
2424
  this._setMask();
1984
2425
  if (!this._maskValue()) {
1985
2426
  this.onChange(el.value);
1986
2427
  return;
1987
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);
1988
2436
  let position = el.selectionStart === 1
1989
2437
  ? el.selectionStart + this._maskService.prefix.length
1990
2438
  : el.selectionStart;
1991
- if (this.showMaskTyped() &&
1992
- this.keepCharacterPositions() &&
1993
- this._maskService.placeHolderCharacter.length === 1) {
2439
+ if (this.keepCharacterPositions() &&
2440
+ this._maskService.placeHolderCharacter.length === 1 &&
2441
+ !this._justPasted()) {
1994
2442
  const suffix = this.suffix();
1995
2443
  const prefix = this.prefix();
1996
2444
  const inputSymbol = el.value.slice(position - 1, position);
1997
2445
  const prefixLength = prefix.length;
1998
- const checkSymbols = this._maskService._checkSymbolMask(inputSymbol, this._maskService.maskExpression[position - 1 - prefixLength] ??
1999
- MaskExpression.EMPTY_STRING);
2000
- const checkSpecialCharacter = this._maskService._checkSymbolMask(inputSymbol, this._maskService.maskExpression[position + 1 - prefixLength] ??
2001
- MaskExpression.EMPTY_STRING);
2002
- const selectRangeBackspace = this._maskService.selStart === this._maskService.selEnd;
2003
- const selStart = Number(this._maskService.selStart) - prefixLength;
2004
- 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;
2005
2460
  const backspaceOrDelete = this._code() === MaskExpression.BACKSPACE ||
2006
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;
2007
2467
  if (backspaceOrDelete) {
2008
- if (!selectRangeBackspace) {
2009
- if (this._maskService.selStart === prefixLength) {
2010
- 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('')}`;
2011
2479
  }
2012
2480
  else if (this._maskService.selStart ===
2013
- this._maskService.maskIsShown.length + prefixLength) {
2014
- 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)}`;
2015
2483
  }
2016
2484
  else {
2017
2485
  this._maskService.actualValue = `${prefix}${this._inputValue()
2018
2486
  .split(prefix)
2019
2487
  .join('')
2020
- .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}`;
2021
2489
  }
2022
2490
  }
2023
- else if (!this._maskService.specialCharacters.includes(this._maskService.maskExpression.slice(position - prefixLength, position + 1 - prefixLength)) &&
2024
- selectRangeBackspace) {
2025
- 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) {
2026
2497
  this._maskService.actualValue = `${prefix}${this._maskService.placeHolderCharacter}${el.value
2027
2498
  .split(prefix)
2028
2499
  .join('')
@@ -2038,35 +2509,90 @@ class NgxMaskDirective {
2038
2509
  }
2039
2510
  position = this._code() === MaskExpression.DELETE ? position + 1 : position;
2040
2511
  }
2041
- if (!backspaceOrDelete) {
2042
- if (!checkSymbols && !checkSpecialCharacter && selectRangeBackspace) {
2043
- position = Number(el.selectionStart) - 1;
2044
- }
2045
- else if (this._maskService.specialCharacters.includes(el.value.slice(position, position + 1)) &&
2046
- checkSpecialCharacter &&
2047
- !this._maskService.specialCharacters.includes(el.value.slice(position + 1, position + 2))) {
2048
- this._maskService.actualValue = `${el.value.slice(0, position - 1)}${el.value.slice(position, position + 1)}${inputSymbol}${el.value.slice(position + 2)}`;
2049
- 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;
2050
2522
  }
2051
- else if (checkSymbols) {
2052
- if (el.value.length === 1 && position === 1) {
2053
- 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;
2054
2535
  }
2055
2536
  else {
2056
- this._maskService.actualValue = `${el.value.slice(0, position - 1)}${inputSymbol}${el.value
2057
- .slice(position + 1)
2058
- .split(suffix)
2059
- .join('')}${suffix}`;
2537
+ this._maskService.actualValue = blanked;
2538
+ position = selStartAbs;
2060
2539
  }
2061
2540
  }
2062
- else if (prefix &&
2063
- el.value.length === 1 &&
2064
- position - prefixLength === 1 &&
2065
- this._maskService._checkSymbolMask(el.value, this._maskService.maskExpression[position - 1 - prefixLength] ??
2066
- MaskExpression.EMPTY_STRING)) {
2067
- 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
+ }
2068
2581
  }
2069
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;
2070
2596
  }
2071
2597
  let caretShift = 0;
2072
2598
  let backspaceShift = false;
@@ -2144,11 +2670,30 @@ class NgxMaskDirective {
2144
2670
  (this._code() === MaskExpression.BACKSPACE && !backspaceShift
2145
2671
  ? 0
2146
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
+ }
2147
2679
  if (positionToApply > this._getActualInputLength()) {
2148
- positionToApply =
2149
- el.value === this._maskService.decimalMarker && el.value.length === 1
2150
- ? this._getActualInputLength() + 1
2151
- : 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();
2152
2697
  }
2153
2698
  if (positionToApply < 0) {
2154
2699
  positionToApply = 0;
@@ -2169,6 +2714,29 @@ class NgxMaskDirective {
2169
2714
  this._maskService.applyValueChanges(el.value.length, this._justPasted(), this._code() === MaskExpression.BACKSPACE || this._code() === MaskExpression.DELETE);
2170
2715
  }
2171
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
+ }
2172
2740
  // IME starts
2173
2741
  onCompositionStart() {
2174
2742
  this._isComposing.set(true);
@@ -2176,6 +2744,12 @@ class NgxMaskDirective {
2176
2744
  // IME completes
2177
2745
  onCompositionEnd(e) {
2178
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
+ }
2179
2753
  this._justPasted.set(true);
2180
2754
  this.onInput(e);
2181
2755
  }
@@ -2194,18 +2768,20 @@ class NgxMaskDirective {
2194
2768
  const decimalPart = el.value.split(decimalMarker)[1];
2195
2769
  el.value = el.value.includes(decimalMarker)
2196
2770
  ? el.value +
2197
- MaskExpression.NUMBER_ZERO.repeat(precision - decimalPart.length) +
2771
+ MaskExpression.NUMBER_ZERO.repeat(precision - (decimalPart?.length || 0)) +
2198
2772
  suffix
2199
2773
  : el.value +
2200
2774
  decimalMarker +
2201
2775
  MaskExpression.NUMBER_ZERO.repeat(precision) +
2202
2776
  suffix;
2203
2777
  this._maskService.actualValue = el.value;
2778
+ this.onChange(this._maskService.actualValue);
2204
2779
  }
2205
2780
  }
2206
2781
  this._maskService.clearIfNotMatchFn();
2207
2782
  }
2208
2783
  this._isFocused.set(false);
2784
+ this._maskService._isFocused.set(false);
2209
2785
  this.onTouch();
2210
2786
  }
2211
2787
  onClick(e) {
@@ -2269,7 +2845,15 @@ class NgxMaskDirective {
2269
2845
  // User finalize their choice from IME composition, so trigger onInput() for the composed text.
2270
2846
  if (e.key === 'Enter') {
2271
2847
  this.onCompositionEnd(event);
2848
+ return;
2272
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;
2273
2857
  return;
2274
2858
  }
2275
2859
  this._code.set(e.code ? e.code : e.key);
@@ -2289,10 +2873,10 @@ class NgxMaskDirective {
2289
2873
  }
2290
2874
  if (e.key === MaskExpression.BACKSPACE && el.selectionStart !== 0) {
2291
2875
  const prefixLength = this.prefix().length;
2292
- // If specialChars is false, (shouldn't ever happen) then set to the defaults
2293
- const specialCharacters = this.specialCharacters().length
2294
- ? this.specialCharacters()
2295
- : 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;
2296
2880
  if (prefixLength > 1 && el.selectionStart <= prefixLength) {
2297
2881
  el.setSelectionRange(prefixLength, el.selectionEnd);
2298
2882
  }
@@ -2321,8 +2905,17 @@ class NgxMaskDirective {
2321
2905
  cursorStart === 0 &&
2322
2906
  el.selectionEnd === el.value.length &&
2323
2907
  el.value.length !== 0) {
2324
- this._position.set(this._maskService.prefix ? this._maskService.prefix.length : 0);
2325
- 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);
2326
2919
  }
2327
2920
  }
2328
2921
  if (!!this.suffix() &&
@@ -2340,8 +2933,53 @@ class NgxMaskDirective {
2340
2933
  this._maskService.selEnd = el.selectionEnd;
2341
2934
  }
2342
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
+ }
2343
2959
  /** It writes the value in the input */
2344
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;
2345
2983
  let value = controlValue;
2346
2984
  const inputTransformFn = this._maskService.inputTransformFn;
2347
2985
  if (typeof value === 'object' && value !== null && 'value' in value) {
@@ -2389,7 +3027,13 @@ class NgxMaskDirective {
2389
3027
  const isFirstWrite = !this._maskService.isInitialized;
2390
3028
  requestAnimationFrame(() => {
2391
3029
  // On initial load, temporarily set isInitialized to false
2392
- // so formControlResult returns early and doesn't mark form as dirty
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.
2393
3037
  if (isFirstWrite) {
2394
3038
  this._maskService.isInitialized = false;
2395
3039
  }
@@ -2397,6 +3041,13 @@ class NgxMaskDirective {
2397
3041
  if (isFirstWrite) {
2398
3042
  this._maskService.isInitialized = true;
2399
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();
2400
3051
  });
2401
3052
  }
2402
3053
  this._maskService.isNumberValue = true;
@@ -2411,48 +3062,87 @@ class NgxMaskDirective {
2411
3062
  (this._maskService.prefix || this._maskService.showMaskTyped))) {
2412
3063
  // Let the service we know we are writing value so that triggering onChange function won't happen during applyMask
2413
3064
  this._maskService.writingValue = true;
2414
- this._maskService.formElementProperty = [
2415
- 'value',
2416
- this._maskService.applyMask(inputValue, this._maskService.maskExpression),
2417
- ];
3065
+ const displayValue = this._maskedOrVerbatim(inputValue);
3066
+ this._maskService.formElementProperty = ['value', displayValue];
3067
+ this._writeElementValueSync(displayValue);
2418
3068
  // Let the service know we've finished writing value
2419
3069
  this._maskService.writingValue = false;
2420
3070
  this._maskService.isInitialized = true;
2421
3071
  }
2422
3072
  else {
2423
3073
  this._maskService.formElementProperty = ['value', inputValue];
3074
+ this._writeElementValueSync(inputValue);
2424
3075
  this._maskService.isInitialized = true;
2425
3076
  }
2426
- 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();
2427
3084
  }
2428
3085
  else {
2429
3086
  // eslint-disable-next-line no-console
2430
3087
  console.warn('Ngx-mask writeValue work with string | number, your current value:', typeof value);
2431
3088
  }
2432
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
+ }
2433
3112
  registerOnChange(fn) {
2434
- // 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);
2435
3116
  const originalFn = fn;
2436
3117
  this._maskService.onChange = this.onChange = (value) => {
2437
3118
  originalFn(value);
2438
- // Update Signal Forms value model if in use
2439
- if (this._isSignalFormsMode()) {
2440
- const stringValue = value === null || typeof value === 'undefined' ? '' : String(value);
2441
- if (this.value() !== stringValue) {
2442
- this.value.set(stringValue);
2443
- }
2444
- }
3119
+ this._propagateToValueModel(value);
2445
3120
  };
2446
3121
  }
2447
3122
  registerOnTouched(fn) {
2448
3123
  this.onTouch = () => {
2449
3124
  fn();
2450
- // Update Signal Forms touched state
2451
- if (this._isSignalFormsMode() && !this.touched()) {
3125
+ if (!this.touched()) {
2452
3126
  this.touched.set(true);
2453
3127
  }
2454
3128
  };
2455
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
+ }
2456
3146
  /**
2457
3147
  * Focus the input element.
2458
3148
  * Required by FormValueControl interface for Signal Forms.
@@ -2484,9 +3174,37 @@ class NgxMaskDirective {
2484
3174
  this._maskService.maskExpression = this._maskService._repeatPatternSymbols(this._maskValue() || '');
2485
3175
  this._maskService.formElementProperty = [
2486
3176
  'value',
2487
- this._maskService.applyMask(this._inputValue(), this._maskService.maskExpression),
3177
+ this._maskedOrVerbatim(this._inputValue()),
2488
3178
  ];
2489
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
+ }
2490
3208
  _validateTime(value) {
2491
3209
  const rowMaskLen = this._maskValue()
2492
3210
  .split(MaskExpression.EMPTY_STRING)
@@ -2504,6 +3222,98 @@ class NgxMaskDirective {
2504
3222
  return (this._maskService.actualValue.length ||
2505
3223
  this._maskService.actualValue.length + this._maskService.prefix.length);
2506
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
+ }
2507
3317
  _createValidationError(actualValue) {
2508
3318
  return {
2509
3319
  mask: {
@@ -2570,8 +3380,8 @@ class NgxMaskDirective {
2570
3380
  return uniqueCharacters.size === 1;
2571
3381
  });
2572
3382
  }
2573
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: NgxMaskDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
2574
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.1.2", 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: [
2575
3385
  {
2576
3386
  provide: NG_VALUE_ACCESSOR,
2577
3387
  useExisting: NgxMaskDirective,
@@ -2585,7 +3395,7 @@ class NgxMaskDirective {
2585
3395
  NgxMaskService,
2586
3396
  ], exportAs: ["mask", "ngxMask"], usesOnChanges: true, ngImport: i0 });
2587
3397
  }
2588
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: NgxMaskDirective, decorators: [{
3398
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: NgxMaskDirective, decorators: [{
2589
3399
  type: Directive,
2590
3400
  args: [{
2591
3401
  selector: 'input[mask], textarea[mask]',
@@ -2605,7 +3415,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImpor
2605
3415
  ],
2606
3416
  exportAs: 'mask,ngxMask',
2607
3417
  }]
2608
- }], 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: [{
2609
3419
  type: HostListener,
2610
3420
  args: ['paste']
2611
3421
  }], onFocus: [{
@@ -2718,10 +3528,10 @@ class NgxMaskPipe {
2718
3528
  });
2719
3529
  }
2720
3530
  }
2721
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: NgxMaskPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
2722
- static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.2", 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" });
2723
3533
  }
2724
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: NgxMaskPipe, decorators: [{
3534
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: NgxMaskPipe, decorators: [{
2725
3535
  type: Pipe,
2726
3536
  args: [{
2727
3537
  name: 'mask',