ngx-mask 22.0.1 → 22.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.
@@ -105,12 +105,14 @@ const initialConfig = {
105
105
  },
106
106
  d: {
107
107
  pattern: new RegExp('\\d'),
108
+ symbol: '*',
108
109
  },
109
110
  m: {
110
111
  pattern: new RegExp('\\d'),
111
112
  },
112
113
  M: {
113
114
  pattern: new RegExp('\\d'),
115
+ symbol: '*',
114
116
  },
115
117
  H: {
116
118
  pattern: new RegExp('\\d'),
@@ -550,7 +552,7 @@ const percentHandler = function (state, params) {
550
552
  }
551
553
  processedValue = `${base}${processedValue.substring(processedValue.indexOf(decimalMarker), processedValue.length)}`;
552
554
  }
553
- let value = '';
555
+ let value;
554
556
  // eslint-disable-next-line @typescript-eslint/no-unused-expressions
555
557
  this.allowNegativeNumbers && processedValue.slice(cursor, cursor + 1) === MaskExpression.MINUS
556
558
  ? (value = `${MaskExpression.MINUS}${processedValue.slice(cursor + 1, cursor + processedValue.length)}`)
@@ -580,7 +582,8 @@ const percentHandler = function (state, params) {
580
582
  */
581
583
  const separatorHandler = function (state, params) {
582
584
  const { backspaced, justPasted, startsWithPrefix, prefixAlreadyRemovedByCaller, cb, inputValue, } = params;
583
- let { processedValue, processedPosition, result, backspaceShift, shift, stepBack } = state;
585
+ let { processedValue, processedPosition, backspaceShift, shift, stepBack } = state;
586
+ let result;
584
587
  if (processedValue.match('[wа-яА-Я]') ||
585
588
  processedValue.match('[ЁёА-я]') ||
586
589
  processedValue.match('[a-z]|[A-Z]') ||
@@ -942,7 +945,7 @@ class NgxMaskApplierService {
942
945
  }
943
946
  let cursor = 0;
944
947
  let result = '';
945
- let multi = false;
948
+ const multi = false;
946
949
  let backspaceShift = false;
947
950
  let shift = 1;
948
951
  let stepBack = false;
@@ -1011,7 +1014,6 @@ class NgxMaskApplierService {
1011
1014
  processedPosition = resolved.processedPosition;
1012
1015
  cursor = resolved.cursor;
1013
1016
  result = resolved.result;
1014
- multi = resolved.multi;
1015
1017
  backspaceShift = resolved.backspaceShift;
1016
1018
  shift = resolved.shift;
1017
1019
  stepBack = resolved.stepBack;
@@ -1091,8 +1093,8 @@ class NgxMaskApplierService {
1091
1093
  false);
1092
1094
  }
1093
1095
  _formatWithSeparators = (str, thousandSeparatorChar, decimalChars, precision) => {
1094
- let x = [];
1095
- let decimalChar = '';
1096
+ let x;
1097
+ let decimalChar;
1096
1098
  if (Array.isArray(decimalChars)) {
1097
1099
  const regExp = new RegExp(decimalChars.map((v) => ('[\\^$.|?*+()'.indexOf(v) >= 0 ? `\\${v}` : v)).join('|'));
1098
1100
  x = str.split(regExp);
@@ -1302,10 +1304,10 @@ class NgxMaskApplierService {
1302
1304
  nonZeroIndex,
1303
1305
  };
1304
1306
  }
1305
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: NgxMaskApplierService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1306
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: NgxMaskApplierService });
1307
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: NgxMaskApplierService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1308
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: NgxMaskApplierService });
1307
1309
  }
1308
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: NgxMaskApplierService, decorators: [{
1310
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: NgxMaskApplierService, decorators: [{
1309
1311
  type: Injectable
1310
1312
  }] });
1311
1313
 
@@ -1488,10 +1490,6 @@ class NgxMaskService extends NgxMaskApplierService {
1488
1490
  actualResult = [];
1489
1491
  }
1490
1492
  }
1491
- // Remove mask if showMaskTyped is true and hiddenInput is false
1492
- if (this.showMaskTyped && !this.hiddenInput) {
1493
- newInputValue = this.removeMask(inputValue);
1494
- }
1495
1493
  // Handle actual value length
1496
1494
  if (this.actualValue.length) {
1497
1495
  if (actualResult.length < inputValue.length) {
@@ -1668,11 +1666,24 @@ class NgxMaskService extends NgxMaskApplierService {
1668
1666
  return inputValue
1669
1667
  .split(MaskExpression.EMPTY_STRING)
1670
1668
  .map((curr, index) => {
1671
- if (this.patterns &&
1672
- this.patterns[maskExpression[index] ?? MaskExpression.EMPTY_STRING] &&
1673
- this.patterns[maskExpression[index] ?? MaskExpression.EMPTY_STRING]?.symbol) {
1674
- return this.patterns[maskExpression[index] ?? MaskExpression.EMPTY_STRING]
1675
- ?.symbol;
1669
+ const maskChar = maskExpression[index] ?? MaskExpression.EMPTY_STRING;
1670
+ const pattern = this.patterns?.[maskChar];
1671
+ if (pattern?.symbol) {
1672
+ return pattern.symbol;
1673
+ }
1674
+ // #1574: a '0' immediately following a concealed DAY/MONTH token
1675
+ // is that field's second digit (d0/M0) — conceal it too so the
1676
+ // whole 2-digit field is hidden, not just its first character.
1677
+ // Display-only: does not touch maskExpression/processedValue, so
1678
+ // date validation/leadZero clamping in generic-pattern.handler.ts
1679
+ // is unaffected.
1680
+ const prevMaskChar = maskExpression[index - 1];
1681
+ const prevPattern = prevMaskChar ? this.patterns?.[prevMaskChar] : null;
1682
+ if (maskChar === MaskExpression.NUMBER_ZERO &&
1683
+ (prevMaskChar === MaskExpression.DAY ||
1684
+ prevMaskChar === MaskExpression.MONTH) &&
1685
+ prevPattern?.symbol) {
1686
+ return prevPattern.symbol;
1676
1687
  }
1677
1688
  return curr;
1678
1689
  })
@@ -2190,10 +2201,10 @@ class NgxMaskService extends NgxMaskApplierService {
2190
2201
  currentLocaleDecimalMarker() {
2191
2202
  return MaskExpression.DOT;
2192
2203
  }
2193
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: NgxMaskService, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
2194
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: NgxMaskService });
2204
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: NgxMaskService, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
2205
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: NgxMaskService });
2195
2206
  }
2196
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: NgxMaskService, decorators: [{
2207
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: NgxMaskService, decorators: [{
2197
2208
  type: Injectable
2198
2209
  }] });
2199
2210
 
@@ -2574,7 +2585,7 @@ class NgxMaskDirective {
2574
2585
  this._hasPendingInitialValue = false;
2575
2586
  const pendingValue = this._pendingInitialValue;
2576
2587
  this._pendingInitialValue = null;
2577
- void this.writeValue(pendingValue);
2588
+ this.writeValue(pendingValue);
2578
2589
  }
2579
2590
  }
2580
2591
  }
@@ -2977,10 +2988,17 @@ class NgxMaskDirective {
2977
2988
  inputSymbol +
2978
2989
  this._inputValue().slice(position + 1);
2979
2990
  }
2980
- if (this._maskService.maskExpression === MaskExpression.DAYS_MONTHS_YEARS &&
2981
- this.leadZeroDateTime()) {
2982
- if ((position < 3 && Number(el.value) > 31 && Number(el.value) < 40) ||
2983
- (position === 5 && Number(el.value.slice(3, 5)) > 12)) {
2991
+ // #1488: generalizes the single-mask (d0/M0/0000) caret-shift-past-separator
2992
+ // fix to any date/time field. Locates the date/time field containing
2993
+ // `position` by scanning maskExpression for a DAY/MONTH/HOURS/HOUR/MINUTE/
2994
+ // SECOND token followed by its digit run, then checks overflow against the
2995
+ // same per-field thresholds generic-pattern.handler.ts already establishes
2996
+ // (day 31, month 12, hour 23/12 w/ apm, minute/second 59). Read-only against
2997
+ // generic-pattern.handler.ts — mirrors its thresholds, does not import from it.
2998
+ if (this.leadZeroDateTime()) {
2999
+ const field = this._findDateTimeFieldAt(this._maskService.maskExpression, position);
3000
+ if (field &&
3001
+ this._isFieldOverflowing(field, position, el.value, this._inputValue(), !!this.apm())) {
2984
3002
  position = position + 2;
2985
3003
  }
2986
3004
  }
@@ -3082,6 +3100,86 @@ class NgxMaskDirective {
3082
3100
  this._maskService.applyValueChanges(el.value.length, this._justPasted(), this._code() === MaskExpression.BACKSPACE || this._code() === MaskExpression.DELETE);
3083
3101
  }
3084
3102
  }
3103
+ /**
3104
+ * #1488: locates the date/time field (DAY/MONTH/HOURS/HOUR/MINUTE/SECOND) that
3105
+ * `position` falls within, scanning `maskExpression` left-to-right. A field spans
3106
+ * its token plus its digit-pair companion — `0` for DAY/MONTH/MINUTE/SECOND (e.g.
3107
+ * `d0`, `M0`, `m0`, `s0`), or `HOUR` for the combined `Hh` hour field. Matches any
3108
+ * position from the field's first char up to and including its end (separator index),
3109
+ * so both a still-mid-typing caret and the just-completed field are covered — the
3110
+ * precise overflow decision (single first digit vs. completed 2-digit value) happens
3111
+ * in `_isFieldOverflowing`. Returns null for non-date/time masks, a no-op for the caller.
3112
+ */
3113
+ _findDateTimeFieldAt(maskExpression, position) {
3114
+ const dateTimeTokens = [
3115
+ MaskExpression.DAY,
3116
+ MaskExpression.MONTH,
3117
+ MaskExpression.HOURS,
3118
+ MaskExpression.HOUR,
3119
+ MaskExpression.MINUTE,
3120
+ MaskExpression.SECOND,
3121
+ ];
3122
+ let index = 0;
3123
+ while (index < maskExpression.length) {
3124
+ const token = maskExpression[index] ?? MaskExpression.EMPTY_STRING;
3125
+ if (dateTimeTokens.includes(token)) {
3126
+ const next = maskExpression[index + 1];
3127
+ const isCombinedHour = token === MaskExpression.HOURS && next === MaskExpression.HOUR;
3128
+ const end = next === MaskExpression.NUMBER_ZERO || isCombinedHour ? index + 2 : index + 1;
3129
+ if (position >= index && position <= end) {
3130
+ return {
3131
+ token: token,
3132
+ start: index,
3133
+ end,
3134
+ };
3135
+ }
3136
+ index = end;
3137
+ continue;
3138
+ }
3139
+ index++;
3140
+ }
3141
+ return null;
3142
+ }
3143
+ /**
3144
+ * Per-token overflow thresholds, mirroring the ones `generic-pattern.handler.ts`
3145
+ * already establishes (day 31, month 12, hour 23/12 w/ apm, minute/second 59) — kept
3146
+ * as a small local table rather than importing from the handler (see design's
3147
+ * Rejected Alternatives: the directive and the handler stay on separate sides of the
3148
+ * display/parse boundary).
3149
+ *
3150
+ * Two overflow shapes are checked, mirroring how the handler itself decides mid-type:
3151
+ * - `firstDigitValue`: when `position` is exactly one char past the field's start (the
3152
+ * just-typed char is the field's first digit), an out-of-range first digit alone
3153
+ * (e.g. month digit `3`, day digit `4`) already triggers the handler's own leadZero
3154
+ * pad within this same keystroke — `el.value` hasn't caught up yet at this point in
3155
+ * the pipeline, so the raw typed digit (`inputValue`) must be read instead.
3156
+ * - `fieldValue`: once both digits of the field are present in `el.value`, an
3157
+ * out-of-range 2-digit value (e.g. day `33`, month `13`) triggers the shift directly.
3158
+ */
3159
+ _isFieldOverflowing(field, position, elValue, inputValue, apm) {
3160
+ const fieldValue = Number(elValue.slice(field.start, field.end));
3161
+ const isFirstDigitOfField = position === field.start + 1;
3162
+ const firstDigitValue = isFirstDigitOfField
3163
+ ? Number(inputValue.slice(position - 1, position))
3164
+ : NaN;
3165
+ switch (field.token) {
3166
+ case MaskExpression.DAY:
3167
+ return (fieldValue > 31 && fieldValue < 40) || firstDigitValue > 3;
3168
+ case MaskExpression.MONTH:
3169
+ return fieldValue > 12 || firstDigitValue > 1;
3170
+ case MaskExpression.HOURS:
3171
+ return apm
3172
+ ? fieldValue > 9 || firstDigitValue > 9
3173
+ : fieldValue > 23 || firstDigitValue > 2;
3174
+ case MaskExpression.HOUR:
3175
+ return apm ? fieldValue > 12 : fieldValue > 23;
3176
+ case MaskExpression.MINUTE:
3177
+ case MaskExpression.SECOND:
3178
+ return fieldValue > 59 || firstDigitValue > 5;
3179
+ default:
3180
+ return false;
3181
+ }
3182
+ }
3085
3183
  /**
3086
3184
  * Whether any pattern slot of the current mask expression can accept a letter.
3087
3185
  * IME composition is only meaningful for such masks; for purely numeric masks
@@ -3144,7 +3242,11 @@ class NgxMaskDirective {
3144
3242
  MaskExpression.NUMBER_ZERO.repeat(precision) +
3145
3243
  suffix;
3146
3244
  this._maskService.actualValue = el.value;
3147
- this.onChange(this._maskService.actualValue);
3245
+ // #1634: propagate through the regular formControlResult pipeline (not a
3246
+ // raw onChange) so outputTransformFn runs on this blur-time reformat, same
3247
+ // as every typing-time emission. A raw onChange here sent the masked display
3248
+ // string straight to the model, bypassing outputTransformFn entirely.
3249
+ this._maskService.formControlResult(this._maskService.actualValue);
3148
3250
  }
3149
3251
  }
3150
3252
  this._maskService.clearIfNotMatchFn();
@@ -3356,7 +3458,7 @@ class NgxMaskDirective {
3356
3458
  }
3357
3459
  }
3358
3460
  /** It writes the value in the input */
3359
- async writeValue(controlValue) {
3461
+ writeValue(controlValue) {
3360
3462
  if (!this._configApplied && this.mask()) {
3361
3463
  // Called before the first ngOnChanges pass configured the mask service (happens with
3362
3464
  // Signal Forms' [formField], whose control-sync instruction runs before sibling
@@ -3570,8 +3672,23 @@ class NgxMaskDirective {
3570
3672
  el.selectionStart = Math.min(Math.max(prefixLength, el.selectionStart), inputValueLength - suffixLength);
3571
3673
  el.selectionEnd = Math.min(Math.max(prefixLength, el.selectionEnd), inputValueLength - suffixLength);
3572
3674
  }
3573
- /** It disables the input element */
3675
+ /**
3676
+ * It disables the input element.
3677
+ *
3678
+ * Mirrors `_writeElementValueSync` (#1305): the service's `formElementProperty` setter
3679
+ * defers ALL DOM writes via `queueMicrotask` to dodge ExpressionChangedAfterItHasBeenChecked
3680
+ * and keep FIFO ordering with config-driven re-renders. For `disabled` specifically, that
3681
+ * deferral leaves `nativeElement.disabled` stale for at least one microtask after Angular
3682
+ * Forms' `setUpControl` calls this method synchronously during init — long enough for a
3683
+ * consumer reading the DOM property in the same synchronous phase (or another deferred write
3684
+ * racing in FIFO order) to observe the wrong value, e.g. an initially-disabled FormControl
3685
+ * whose native input briefly (or, depending on interleaving, persistently) reports
3686
+ * `disabled === false` (#1633). Writing synchronously here closes that gap; the deferred
3687
+ * write below still runs afterwards and re-applies the same final value, preserving the
3688
+ * existing ordering guarantees other call sites rely on.
3689
+ */
3574
3690
  setDisabledState(isDisabled) {
3691
+ this._renderer.setProperty(this._elementRef.nativeElement, 'disabled', isDisabled);
3575
3692
  this._maskService.formElementProperty = ['disabled', isDisabled];
3576
3693
  }
3577
3694
  _applyMask() {
@@ -3784,8 +3901,8 @@ class NgxMaskDirective {
3784
3901
  return uniqueCharacters.size === 1;
3785
3902
  });
3786
3903
  }
3787
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: NgxMaskDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
3788
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.5", type: NgxMaskDirective, isStandalone: true, selector: "input[mask], textarea[mask]", inputs: { mask: { classPropertyName: "mask", publicName: "mask", isSignal: true, isRequired: false, transformFunction: null }, specialCharacters: { classPropertyName: "specialCharacters", publicName: "specialCharacters", isSignal: true, isRequired: false, transformFunction: null }, patterns: { classPropertyName: "patterns", publicName: "patterns", isSignal: true, isRequired: false, transformFunction: null }, prefix: { classPropertyName: "prefix", publicName: "prefix", isSignal: true, isRequired: false, transformFunction: null }, suffix: { classPropertyName: "suffix", publicName: "suffix", isSignal: true, isRequired: false, transformFunction: null }, thousandSeparator: { classPropertyName: "thousandSeparator", publicName: "thousandSeparator", isSignal: true, isRequired: false, transformFunction: null }, decimalMarker: { classPropertyName: "decimalMarker", publicName: "decimalMarker", isSignal: true, isRequired: false, transformFunction: null }, dropSpecialCharacters: { classPropertyName: "dropSpecialCharacters", publicName: "dropSpecialCharacters", isSignal: true, isRequired: false, transformFunction: null }, hiddenInput: { classPropertyName: "hiddenInput", publicName: "hiddenInput", isSignal: true, isRequired: false, transformFunction: null }, showMaskTyped: { classPropertyName: "showMaskTyped", publicName: "showMaskTyped", isSignal: true, isRequired: false, transformFunction: null }, placeHolderCharacter: { classPropertyName: "placeHolderCharacter", publicName: "placeHolderCharacter", isSignal: true, isRequired: false, transformFunction: null }, shownMaskExpression: { classPropertyName: "shownMaskExpression", publicName: "shownMaskExpression", isSignal: true, isRequired: false, transformFunction: null }, clearIfNotMatch: { classPropertyName: "clearIfNotMatch", publicName: "clearIfNotMatch", isSignal: true, isRequired: false, transformFunction: null }, validation: { classPropertyName: "validation", publicName: "validation", isSignal: true, isRequired: false, transformFunction: null }, separatorLimit: { classPropertyName: "separatorLimit", publicName: "separatorLimit", isSignal: true, isRequired: false, transformFunction: null }, typeFromDecimals: { classPropertyName: "typeFromDecimals", publicName: "typeFromDecimals", isSignal: true, isRequired: false, transformFunction: null }, allowNegativeNumbers: { classPropertyName: "allowNegativeNumbers", publicName: "allowNegativeNumbers", isSignal: true, isRequired: false, transformFunction: null }, leadZeroDateTime: { classPropertyName: "leadZeroDateTime", publicName: "leadZeroDateTime", isSignal: true, isRequired: false, transformFunction: null }, leadZero: { classPropertyName: "leadZero", publicName: "leadZero", isSignal: true, isRequired: false, transformFunction: null }, triggerOnMaskChange: { classPropertyName: "triggerOnMaskChange", publicName: "triggerOnMaskChange", isSignal: true, isRequired: false, transformFunction: null }, apm: { classPropertyName: "apm", publicName: "apm", isSignal: true, isRequired: false, transformFunction: null }, inputTransformFn: { classPropertyName: "inputTransformFn", publicName: "inputTransformFn", isSignal: true, isRequired: false, transformFunction: null }, outputTransformFn: { classPropertyName: "outputTransformFn", publicName: "outputTransformFn", isSignal: true, isRequired: false, transformFunction: null }, keepCharacterPositions: { classPropertyName: "keepCharacterPositions", publicName: "keepCharacterPositions", isSignal: true, isRequired: false, transformFunction: null }, instantPrefix: { classPropertyName: "instantPrefix", publicName: "instantPrefix", isSignal: true, isRequired: false, transformFunction: null }, defaultValueOnBlur: { classPropertyName: "defaultValueOnBlur", publicName: "defaultValueOnBlur", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, touched: { classPropertyName: "touched", publicName: "touched", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", touched: "touchedChange", maskFilled: "maskFilled" }, host: { listeners: { "paste": "onPaste()", "focus": "onFocus()", "ngModelChange": "onModelChange($event)", "input": "onInput($event)", "compositionstart": "onCompositionStart()", "compositionend": "onCompositionEnd($event)", "blur": "onBlur($event)", "click": "onClick($event)", "keydown": "onKeyDown($event)" } }, providers: [
3904
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: NgxMaskDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
3905
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.7", type: NgxMaskDirective, isStandalone: true, selector: "input[mask], textarea[mask]", inputs: { mask: { classPropertyName: "mask", publicName: "mask", isSignal: true, isRequired: false, transformFunction: null }, specialCharacters: { classPropertyName: "specialCharacters", publicName: "specialCharacters", isSignal: true, isRequired: false, transformFunction: null }, patterns: { classPropertyName: "patterns", publicName: "patterns", isSignal: true, isRequired: false, transformFunction: null }, prefix: { classPropertyName: "prefix", publicName: "prefix", isSignal: true, isRequired: false, transformFunction: null }, suffix: { classPropertyName: "suffix", publicName: "suffix", isSignal: true, isRequired: false, transformFunction: null }, thousandSeparator: { classPropertyName: "thousandSeparator", publicName: "thousandSeparator", isSignal: true, isRequired: false, transformFunction: null }, decimalMarker: { classPropertyName: "decimalMarker", publicName: "decimalMarker", isSignal: true, isRequired: false, transformFunction: null }, dropSpecialCharacters: { classPropertyName: "dropSpecialCharacters", publicName: "dropSpecialCharacters", isSignal: true, isRequired: false, transformFunction: null }, hiddenInput: { classPropertyName: "hiddenInput", publicName: "hiddenInput", isSignal: true, isRequired: false, transformFunction: null }, showMaskTyped: { classPropertyName: "showMaskTyped", publicName: "showMaskTyped", isSignal: true, isRequired: false, transformFunction: null }, placeHolderCharacter: { classPropertyName: "placeHolderCharacter", publicName: "placeHolderCharacter", isSignal: true, isRequired: false, transformFunction: null }, shownMaskExpression: { classPropertyName: "shownMaskExpression", publicName: "shownMaskExpression", isSignal: true, isRequired: false, transformFunction: null }, clearIfNotMatch: { classPropertyName: "clearIfNotMatch", publicName: "clearIfNotMatch", isSignal: true, isRequired: false, transformFunction: null }, validation: { classPropertyName: "validation", publicName: "validation", isSignal: true, isRequired: false, transformFunction: null }, separatorLimit: { classPropertyName: "separatorLimit", publicName: "separatorLimit", isSignal: true, isRequired: false, transformFunction: null }, typeFromDecimals: { classPropertyName: "typeFromDecimals", publicName: "typeFromDecimals", isSignal: true, isRequired: false, transformFunction: null }, allowNegativeNumbers: { classPropertyName: "allowNegativeNumbers", publicName: "allowNegativeNumbers", isSignal: true, isRequired: false, transformFunction: null }, leadZeroDateTime: { classPropertyName: "leadZeroDateTime", publicName: "leadZeroDateTime", isSignal: true, isRequired: false, transformFunction: null }, leadZero: { classPropertyName: "leadZero", publicName: "leadZero", isSignal: true, isRequired: false, transformFunction: null }, triggerOnMaskChange: { classPropertyName: "triggerOnMaskChange", publicName: "triggerOnMaskChange", isSignal: true, isRequired: false, transformFunction: null }, apm: { classPropertyName: "apm", publicName: "apm", isSignal: true, isRequired: false, transformFunction: null }, inputTransformFn: { classPropertyName: "inputTransformFn", publicName: "inputTransformFn", isSignal: true, isRequired: false, transformFunction: null }, outputTransformFn: { classPropertyName: "outputTransformFn", publicName: "outputTransformFn", isSignal: true, isRequired: false, transformFunction: null }, keepCharacterPositions: { classPropertyName: "keepCharacterPositions", publicName: "keepCharacterPositions", isSignal: true, isRequired: false, transformFunction: null }, instantPrefix: { classPropertyName: "instantPrefix", publicName: "instantPrefix", isSignal: true, isRequired: false, transformFunction: null }, defaultValueOnBlur: { classPropertyName: "defaultValueOnBlur", publicName: "defaultValueOnBlur", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, touched: { classPropertyName: "touched", publicName: "touched", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", touched: "touchedChange", maskFilled: "maskFilled" }, host: { listeners: { "paste": "onPaste()", "focus": "onFocus()", "ngModelChange": "onModelChange($event)", "input": "onInput($event)", "compositionstart": "onCompositionStart()", "compositionend": "onCompositionEnd($event)", "blur": "onBlur($event)", "click": "onClick($event)", "keydown": "onKeyDown($event)" } }, providers: [
3789
3906
  {
3790
3907
  provide: NG_VALUE_ACCESSOR,
3791
3908
  useExisting: NgxMaskDirective,
@@ -3799,7 +3916,7 @@ class NgxMaskDirective {
3799
3916
  NgxMaskService,
3800
3917
  ], exportAs: ["mask", "ngxMask"], usesOnChanges: true, ngImport: i0 });
3801
3918
  }
3802
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: NgxMaskDirective, decorators: [{
3919
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: NgxMaskDirective, decorators: [{
3803
3920
  type: Directive,
3804
3921
  args: [{
3805
3922
  selector: 'input[mask], textarea[mask]',
@@ -3937,10 +4054,10 @@ class NgxMaskPipe {
3937
4054
  });
3938
4055
  }
3939
4056
  }
3940
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: NgxMaskPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
3941
- static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "22.0.5", ngImport: i0, type: NgxMaskPipe, isStandalone: true, name: "mask" });
4057
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: NgxMaskPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
4058
+ static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "22.0.7", ngImport: i0, type: NgxMaskPipe, isStandalone: true, name: "mask" });
3942
4059
  }
3943
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: NgxMaskPipe, decorators: [{
4060
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: NgxMaskPipe, decorators: [{
3944
4061
  type: Pipe,
3945
4062
  args: [{
3946
4063
  name: 'mask',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ngx-mask",
3
- "version": "22.0.1",
3
+ "version": "22.1.0",
4
4
  "description": "Input masking for modern Angular — Reactive, template-driven and Signal Forms, zoneless and SSR ready. Dates, numbers, separators, custom patterns.",
5
5
  "keywords": [
6
6
  "ng2-mask",
@@ -120,7 +120,7 @@ declare class NgxMaskApplierService {
120
120
  writingValue: boolean;
121
121
  ipError?: boolean;
122
122
  cpfCnpjError?: boolean;
123
- applyMask(inputValue: string | object | boolean | null | undefined, maskExpression: string, position?: number, justPasted?: boolean, backspaced?: boolean, cb?: (...args: any[]) => any): string;
123
+ applyMask(inputValue: string | object | boolean | null | undefined, maskExpression: string, position?: number, justPasted?: boolean, backspaced?: boolean, cb?: (actualShift: number, backspaceShift: boolean) => void): string;
124
124
  _findDropSpecialChar(inputSymbol: string): undefined | string;
125
125
  _findSpecialChar(inputSymbol: string): undefined | string;
126
126
  _checkSymbolMask(inputSymbol: string, maskSymbol: string): boolean;
@@ -177,7 +177,7 @@ declare class NgxMaskService extends NgxMaskApplierService {
177
177
  private _emitValue;
178
178
  private _start;
179
179
  private _end;
180
- onChange: (_: any) => void;
180
+ onChange: (_: unknown) => void;
181
181
  readonly _elementRef: ElementRef<any> | null;
182
182
  private readonly document;
183
183
  protected _config: NgxMaskConfig;
@@ -192,9 +192,9 @@ declare class NgxMaskService extends NgxMaskApplierService {
192
192
  * @param cb Callback function.
193
193
  * @returns The masked value.
194
194
  */
195
- applyMask(inputValue: string, maskExpression: string, position?: number, justPasted?: boolean, backspaced?: boolean, cb?: (...args: any[]) => any): string;
195
+ applyMask(inputValue: string, maskExpression: string, position?: number, justPasted?: boolean, backspaced?: boolean, cb?: (actualShift: number, backspaceShift: boolean) => void): string;
196
196
  private _numberSkipedSymbols;
197
- applyValueChanges(position: number, justPasted: boolean, backspaced: boolean, cb?: (...args: any[]) => any): void;
197
+ applyValueChanges(position: number, justPasted: boolean, backspaced: boolean, cb?: (actualShift: number, backspaceShift: boolean) => void): void;
198
198
  hideInput(inputValue: string, maskExpression: string): string;
199
199
  getActualValue(res: string): string;
200
200
  shiftTypedSymbols(inputValue: string): string;
@@ -231,7 +231,7 @@ declare class NgxMaskService extends NgxMaskApplierService {
231
231
  * model value too. Therefore, we don't trigger onChange in this scenario.
232
232
  * @param inputValue the current form input value
233
233
  */
234
- private formControlResult;
234
+ formControlResult(inputValue: string): void;
235
235
  private _toNumber;
236
236
  private _removeMask;
237
237
  private _removePrefix;
@@ -380,7 +380,7 @@ declare class NgxMaskDirective implements ControlValueAccessor, OnChanges, Valid
380
380
  private readonly _injector;
381
381
  private _ngControl;
382
382
  private _resolveNgControl;
383
- onChange: (_: any) => void;
383
+ onChange: (_: unknown) => void;
384
384
  onTouch: () => void;
385
385
  constructor();
386
386
  ngOnChanges(changes: SimpleChanges): void;
@@ -389,6 +389,34 @@ declare class NgxMaskDirective implements ControlValueAccessor, OnChanges, Valid
389
389
  onFocus(): void;
390
390
  onModelChange(value: unknown): void;
391
391
  onInput(e: Event): void;
392
+ /**
393
+ * #1488: locates the date/time field (DAY/MONTH/HOURS/HOUR/MINUTE/SECOND) that
394
+ * `position` falls within, scanning `maskExpression` left-to-right. A field spans
395
+ * its token plus its digit-pair companion — `0` for DAY/MONTH/MINUTE/SECOND (e.g.
396
+ * `d0`, `M0`, `m0`, `s0`), or `HOUR` for the combined `Hh` hour field. Matches any
397
+ * position from the field's first char up to and including its end (separator index),
398
+ * so both a still-mid-typing caret and the just-completed field are covered — the
399
+ * precise overflow decision (single first digit vs. completed 2-digit value) happens
400
+ * in `_isFieldOverflowing`. Returns null for non-date/time masks, a no-op for the caller.
401
+ */
402
+ private _findDateTimeFieldAt;
403
+ /**
404
+ * Per-token overflow thresholds, mirroring the ones `generic-pattern.handler.ts`
405
+ * already establishes (day 31, month 12, hour 23/12 w/ apm, minute/second 59) — kept
406
+ * as a small local table rather than importing from the handler (see design's
407
+ * Rejected Alternatives: the directive and the handler stay on separate sides of the
408
+ * display/parse boundary).
409
+ *
410
+ * Two overflow shapes are checked, mirroring how the handler itself decides mid-type:
411
+ * - `firstDigitValue`: when `position` is exactly one char past the field's start (the
412
+ * just-typed char is the field's first digit), an out-of-range first digit alone
413
+ * (e.g. month digit `3`, day digit `4`) already triggers the handler's own leadZero
414
+ * pad within this same keystroke — `el.value` hasn't caught up yet at this point in
415
+ * the pipeline, so the raw typed digit (`inputValue`) must be read instead.
416
+ * - `fieldValue`: once both digits of the field are present in `el.value`, an
417
+ * out-of-range 2-digit value (e.g. day `33`, month `13`) triggers the shift directly.
418
+ */
419
+ private _isFieldOverflowing;
392
420
  /**
393
421
  * Whether any pattern slot of the current mask expression can accept a letter.
394
422
  * IME composition is only meaningful for such masks; for purely numeric masks
@@ -424,7 +452,7 @@ declare class NgxMaskDirective implements ControlValueAccessor, OnChanges, Valid
424
452
  */
425
453
  private _restoreControlStateAfterWrite;
426
454
  /** It writes the value in the input */
427
- writeValue(controlValue: unknown): Promise<void>;
455
+ writeValue(controlValue: unknown): void;
428
456
  /**
429
457
  * Mirrors a writeValue-driven render into the DOM synchronously, in the same
430
458
  * change-detection pass (#1305). The service's `formElementProperty` setter defers all
@@ -460,7 +488,21 @@ declare class NgxMaskDirective implements ControlValueAccessor, OnChanges, Valid
460
488
  focus(): void;
461
489
  private _getActiveElement;
462
490
  checkSelectionOnDeletion(el: HTMLInputElement): void;
463
- /** It disables the input element */
491
+ /**
492
+ * It disables the input element.
493
+ *
494
+ * Mirrors `_writeElementValueSync` (#1305): the service's `formElementProperty` setter
495
+ * defers ALL DOM writes via `queueMicrotask` to dodge ExpressionChangedAfterItHasBeenChecked
496
+ * and keep FIFO ordering with config-driven re-renders. For `disabled` specifically, that
497
+ * deferral leaves `nativeElement.disabled` stale for at least one microtask after Angular
498
+ * Forms' `setUpControl` calls this method synchronously during init — long enough for a
499
+ * consumer reading the DOM property in the same synchronous phase (or another deferred write
500
+ * racing in FIFO order) to observe the wrong value, e.g. an initially-disabled FormControl
501
+ * whose native input briefly (or, depending on interleaving, persistently) reports
502
+ * `disabled === false` (#1633). Writing synchronously here closes that gap; the deferred
503
+ * write below still runs afterwards and re-applies the same final value, preserving the
504
+ * existing ordering guarantees other call sites rely on.
505
+ */
464
506
  setDisabledState(isDisabled: boolean): void;
465
507
  private _applyMask;
466
508
  /**