ngx-mask 22.0.2 → 22.2.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.
@@ -29,6 +29,8 @@ var MaskExpression;
29
29
  MaskExpression["SECOND"] = "s";
30
30
  MaskExpression["LETTER_S"] = "S";
31
31
  MaskExpression["DOT"] = ".";
32
+ MaskExpression["NUMPAD_DECIMAL"] = "Decimal";
33
+ MaskExpression["NUMPAD_DECIMAL_CODE"] = "NumpadDecimal";
32
34
  MaskExpression["COMMA"] = ",";
33
35
  MaskExpression["CURLY_BRACKETS_LEFT"] = "{";
34
36
  MaskExpression["CURLY_BRACKETS_RIGHT"] = "}";
@@ -105,12 +107,14 @@ const initialConfig = {
105
107
  },
106
108
  d: {
107
109
  pattern: new RegExp('\\d'),
110
+ symbol: '*',
108
111
  },
109
112
  m: {
110
113
  pattern: new RegExp('\\d'),
111
114
  },
112
115
  M: {
113
116
  pattern: new RegExp('\\d'),
117
+ symbol: '*',
114
118
  },
115
119
  H: {
116
120
  pattern: new RegExp('\\d'),
@@ -537,9 +541,9 @@ const percentHandler = function (state, params) {
537
541
  (processedValue.match(/[-!$%^&*()_+|~=`{}\[\]:";'<>?,\/.]/) && !backspaced)) {
538
542
  processedValue = this._stripToDecimal(processedValue);
539
543
  const precision = this.getPrecision(state.maskExpression);
540
- processedValue = this.checkInputPrecision(processedValue, precision, this.decimalMarker);
544
+ processedValue = this.checkInputPrecision(processedValue, precision);
541
545
  }
542
- const decimalMarker = typeof this.decimalMarker === 'string' ? this.decimalMarker : MaskExpression.DOT;
546
+ const decimalMarker = this._activeDecimalMarker(processedValue);
543
547
  if (processedValue.indexOf(decimalMarker) > 0 &&
544
548
  !this.percentage(processedValue.substring(0, processedValue.indexOf(decimalMarker)))) {
545
549
  let base = processedValue.substring(0, processedValue.indexOf(decimalMarker) - 1);
@@ -592,15 +596,7 @@ const separatorHandler = function (state, params) {
592
596
  const precision = this.getPrecision(state.maskExpression);
593
597
  let decimalMarker = this.decimalMarker;
594
598
  if (Array.isArray(this.decimalMarker)) {
595
- if (this.actualValue.includes(this.decimalMarker[0]) ||
596
- this.actualValue.includes(this.decimalMarker[1])) {
597
- decimalMarker = this.actualValue.includes(this.decimalMarker[0])
598
- ? this.decimalMarker[0]
599
- : this.decimalMarker[1];
600
- }
601
- else {
602
- decimalMarker = this.decimalMarker.find((dm) => dm !== this.thousandSeparator);
603
- }
599
+ decimalMarker = this._activeDecimalMarker(this.actualValue);
604
600
  }
605
601
  // Issues #733/#1414/#1315: opt-in "banking" typing mode — typed digits fill
606
602
  // the value from the decimal end, ATM/calculator style (5 -> 0.05 -> 0.57
@@ -802,7 +798,7 @@ const separatorHandler = function (state, params) {
802
798
  if (processedValue.match(invalidCharRegexp)) {
803
799
  processedValue = processedValue.substring(0, processedValue.length - 1);
804
800
  }
805
- processedValue = this.checkInputPrecision(processedValue, precision, this.decimalMarker);
801
+ processedValue = this.checkInputPrecision(processedValue, precision);
806
802
  const strForSep = processedValue.replace(new RegExp(thousandSeparatorCharEscaped, 'g'), '');
807
803
  result = this._formatWithSeparators(strForSep, this.thousandSeparator, this.decimalMarker, precision);
808
804
  const commaShift = result.indexOf(MaskExpression.COMMA) - processedValue.indexOf(MaskExpression.COMMA);
@@ -1191,18 +1187,11 @@ class NgxMaskApplierService {
1191
1187
  }
1192
1188
  return inputValue;
1193
1189
  };
1194
- checkInputPrecision = (inputValue, precision, decimalMarker) => {
1190
+ checkInputPrecision = (inputValue, precision) => {
1195
1191
  let processedInputValue = inputValue;
1196
- let processedDecimalMarker = decimalMarker;
1197
1192
  if (precision < Infinity) {
1198
- // With an array decimalMarker (default ['.', ',']), pick the first configured
1199
- // marker that isn't also the thousandSeparator — mirrors the same fallback
1200
- // resolution used in separator.handler.ts (no marker typed yet in this value).
1201
- if (Array.isArray(processedDecimalMarker)) {
1202
- const marker = processedDecimalMarker.find((dm) => dm !== this.thousandSeparator);
1203
- processedDecimalMarker = marker ? marker : processedDecimalMarker[0];
1204
- }
1205
- const precisionRegEx = new RegExp(this._charToRegExpExpression(processedDecimalMarker) + `\\d{${precision}}.*$`);
1193
+ const decimalMarker = this._activeDecimalMarker(inputValue);
1194
+ const precisionRegEx = new RegExp(this._charToRegExpExpression(decimalMarker) + `\\d{${precision}}.*$`);
1206
1195
  const precisionMatch = processedInputValue.match(precisionRegEx);
1207
1196
  const precisionMatchLength = (precisionMatch && precisionMatch[0]?.length) ?? 0;
1208
1197
  if (precisionMatchLength - 1 > precision) {
@@ -1210,7 +1199,7 @@ class NgxMaskApplierService {
1210
1199
  processedInputValue = processedInputValue.substring(0, processedInputValue.length - diff);
1211
1200
  }
1212
1201
  if (precision === 0 &&
1213
- this._compareOrIncludes(processedInputValue[processedInputValue.length - 1], processedDecimalMarker, this.thousandSeparator)) {
1202
+ this._compareOrIncludes(processedInputValue[processedInputValue.length - 1], decimalMarker, this.thousandSeparator)) {
1214
1203
  processedInputValue = processedInputValue.substring(0, processedInputValue.length - 1);
1215
1204
  }
1216
1205
  }
@@ -1258,13 +1247,24 @@ class NgxMaskApplierService {
1258
1247
  return value === MaskExpression.EMPTY_STRING || Number(value.substring(0, 3)) > 255;
1259
1248
  }));
1260
1249
  }
1250
+ /**
1251
+ * The single rule for "which decimal marker is active": the configured marker, or — for an
1252
+ * array — the first configured marker present in `value` that is not the thousandSeparator,
1253
+ * else the first configured marker that is not the thousandSeparator (#1653).
1254
+ */
1255
+ _activeDecimalMarker(value) {
1256
+ if (!Array.isArray(this.decimalMarker)) {
1257
+ return this.decimalMarker;
1258
+ }
1259
+ const candidates = this.decimalMarker.filter((dm) => dm !== this.thousandSeparator);
1260
+ return (candidates.find((dm) => value.includes(dm)) ?? candidates[0] ?? this.decimalMarker[0]);
1261
+ }
1261
1262
  _splitPercentZero(value) {
1262
1263
  if (value === MaskExpression.MINUS && this.allowNegativeNumbers) {
1263
1264
  return value;
1264
1265
  }
1265
- const decimalIndex = typeof this.decimalMarker === 'string'
1266
- ? value.indexOf(this.decimalMarker)
1267
- : value.indexOf(MaskExpression.DOT);
1266
+ const decimal = this._activeDecimalMarker(value);
1267
+ const decimalIndex = value.indexOf(decimal);
1268
1268
  const emptyOrMinus = this.allowNegativeNumbers && value.includes(MaskExpression.MINUS) ? '-' : '';
1269
1269
  if (decimalIndex === -1) {
1270
1270
  const parsedValue = parseInt(emptyOrMinus ? value.slice(1, value.length) : value, 10);
@@ -1276,7 +1276,6 @@ class NgxMaskApplierService {
1276
1276
  const integerPart = parseInt(value.replace('-', '').substring(0, decimalIndex), 10);
1277
1277
  const decimalPart = value.substring(decimalIndex + 1);
1278
1278
  const integerString = isNaN(integerPart) ? '' : integerPart.toString();
1279
- const decimal = typeof this.decimalMarker === 'string' ? this.decimalMarker : MaskExpression.DOT;
1280
1279
  return integerString === MaskExpression.EMPTY_STRING
1281
1280
  ? MaskExpression.EMPTY_STRING
1282
1281
  : `${emptyOrMinus}${integerString}${decimal}${decimalPart}`;
@@ -1302,10 +1301,10 @@ class NgxMaskApplierService {
1302
1301
  nonZeroIndex,
1303
1302
  };
1304
1303
  }
1305
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: NgxMaskApplierService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1306
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: NgxMaskApplierService });
1304
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: NgxMaskApplierService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1305
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: NgxMaskApplierService });
1307
1306
  }
1308
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: NgxMaskApplierService, decorators: [{
1307
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: NgxMaskApplierService, decorators: [{
1309
1308
  type: Injectable
1310
1309
  }] });
1311
1310
 
@@ -1664,11 +1663,24 @@ class NgxMaskService extends NgxMaskApplierService {
1664
1663
  return inputValue
1665
1664
  .split(MaskExpression.EMPTY_STRING)
1666
1665
  .map((curr, index) => {
1667
- if (this.patterns &&
1668
- this.patterns[maskExpression[index] ?? MaskExpression.EMPTY_STRING] &&
1669
- this.patterns[maskExpression[index] ?? MaskExpression.EMPTY_STRING]?.symbol) {
1670
- return this.patterns[maskExpression[index] ?? MaskExpression.EMPTY_STRING]
1671
- ?.symbol;
1666
+ const maskChar = maskExpression[index] ?? MaskExpression.EMPTY_STRING;
1667
+ const pattern = this.patterns?.[maskChar];
1668
+ if (pattern?.symbol) {
1669
+ return pattern.symbol;
1670
+ }
1671
+ // #1574: a '0' immediately following a concealed DAY/MONTH token
1672
+ // is that field's second digit (d0/M0) — conceal it too so the
1673
+ // whole 2-digit field is hidden, not just its first character.
1674
+ // Display-only: does not touch maskExpression/processedValue, so
1675
+ // date validation/leadZero clamping in generic-pattern.handler.ts
1676
+ // is unaffected.
1677
+ const prevMaskChar = maskExpression[index - 1];
1678
+ const prevPattern = prevMaskChar ? this.patterns?.[prevMaskChar] : null;
1679
+ if (maskChar === MaskExpression.NUMBER_ZERO &&
1680
+ (prevMaskChar === MaskExpression.DAY ||
1681
+ prevMaskChar === MaskExpression.MONTH) &&
1682
+ prevPattern?.symbol) {
1683
+ return prevPattern.symbol;
1672
1684
  }
1673
1685
  return curr;
1674
1686
  })
@@ -1992,7 +2004,10 @@ class NgxMaskService extends NgxMaskApplierService {
1992
2004
  }
1993
2005
  _removeMask(value, specialCharactersForRemove) {
1994
2006
  if (this.maskExpression.startsWith(MaskExpression.PERCENT) &&
1995
- value.includes(MaskExpression.DOT)) {
2007
+ (value.includes(MaskExpression.DOT) ||
2008
+ (Array.isArray(this.decimalMarker) &&
2009
+ value.includes(MaskExpression.COMMA) &&
2010
+ this._activeDecimalMarker(value) === MaskExpression.COMMA))) {
1996
2011
  return value;
1997
2012
  }
1998
2013
  return value
@@ -2048,7 +2063,7 @@ class NgxMaskService extends NgxMaskApplierService {
2048
2063
  return processedResult;
2049
2064
  }
2050
2065
  if (this.maskExpression.startsWith(MaskExpression.PERCENT) &&
2051
- this.decimalMarker === MaskExpression.COMMA) {
2066
+ this._activeDecimalMarker(processedResult) === MaskExpression.COMMA) {
2052
2067
  processedResult = processedResult.replace(MaskExpression.COMMA, MaskExpression.DOT);
2053
2068
  }
2054
2069
  const separatorPrecision = this._retrieveSeparatorPrecision(this.maskExpression);
@@ -2186,10 +2201,10 @@ class NgxMaskService extends NgxMaskApplierService {
2186
2201
  currentLocaleDecimalMarker() {
2187
2202
  return MaskExpression.DOT;
2188
2203
  }
2189
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: NgxMaskService, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
2190
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: NgxMaskService });
2204
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: NgxMaskService, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
2205
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: NgxMaskService });
2191
2206
  }
2192
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: NgxMaskService, decorators: [{
2207
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: NgxMaskService, decorators: [{
2193
2208
  type: Injectable
2194
2209
  }] });
2195
2210
 
@@ -2280,6 +2295,18 @@ class NgxMaskDirective {
2280
2295
  // config, so no ngOnChanges mirroring into the service is needed (#1435).
2281
2296
  defaultValueOnBlur = input(null, /* @ts-ignore */
2282
2297
  ...(ngDevMode ? [{ debugName: "defaultValueOnBlur" }] : /* istanbul ignore next */ []));
2298
+ /**
2299
+ * Signal Forms `FormValueControl` surface. At runtime `FormField` never drives this model
2300
+ * when the directive is bound (it prefers the `NG_VALUE_ACCESSOR` provided above, see
2301
+ * `_isCvaMode`), but the template type-checker treats any directive owning a `value` model
2302
+ * as the field's custom control and checks the bound field's value type against it (#1640).
2303
+ * The model therefore accepts exactly what `writeValue()` accepts — `string | number | null |
2304
+ * undefined` — so numeric and nullable fields (`signal({ amount: 0 })`,
2305
+ * `signal<number | null>(null)`, `signal<string | null>(null)`) compile under
2306
+ * `strictTemplates`. Values pushed back into the model are always the unmasked string (see
2307
+ * `_propagateToValueModel`), i.e. `value()` / `valueChange` are declared wider than what is
2308
+ * emitted.
2309
+ */
2283
2310
  value = model('', /* @ts-ignore */
2284
2311
  ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
2285
2312
  disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
@@ -2519,7 +2546,10 @@ class NgxMaskDirective {
2519
2546
  }
2520
2547
  }
2521
2548
  if (placeHolderCharacter) {
2522
- this._maskService.placeHolderCharacter = placeHolderCharacter.currentValue;
2549
+ // `null`/`undefined` means "no custom placeholder" (#1643): fall back to the default
2550
+ // instead of storing null, which broke every `.length` read on the service.
2551
+ this._maskService.placeHolderCharacter =
2552
+ placeHolderCharacter.currentValue ?? this._config.placeHolderCharacter;
2523
2553
  if (typeof placeHolderCharacter.currentValue === 'string' &&
2524
2554
  placeHolderCharacter.currentValue.length > 1 &&
2525
2555
  !this._warnedAboutMultiCharPlaceholder) {
@@ -2973,10 +3003,17 @@ class NgxMaskDirective {
2973
3003
  inputSymbol +
2974
3004
  this._inputValue().slice(position + 1);
2975
3005
  }
2976
- if (this._maskService.maskExpression === MaskExpression.DAYS_MONTHS_YEARS &&
2977
- this.leadZeroDateTime()) {
2978
- if ((position < 3 && Number(el.value) > 31 && Number(el.value) < 40) ||
2979
- (position === 5 && Number(el.value.slice(3, 5)) > 12)) {
3006
+ // #1488: generalizes the single-mask (d0/M0/0000) caret-shift-past-separator
3007
+ // fix to any date/time field. Locates the date/time field containing
3008
+ // `position` by scanning maskExpression for a DAY/MONTH/HOURS/HOUR/MINUTE/
3009
+ // SECOND token followed by its digit run, then checks overflow against the
3010
+ // same per-field thresholds generic-pattern.handler.ts already establishes
3011
+ // (day 31, month 12, hour 23/12 w/ apm, minute/second 59). Read-only against
3012
+ // generic-pattern.handler.ts — mirrors its thresholds, does not import from it.
3013
+ if (this.leadZeroDateTime()) {
3014
+ const field = this._findDateTimeFieldAt(this._maskService.maskExpression, position);
3015
+ if (field &&
3016
+ this._isFieldOverflowing(field, position, el.value, this._inputValue(), !!this.apm())) {
2980
3017
  position = position + 2;
2981
3018
  }
2982
3019
  }
@@ -3078,6 +3115,86 @@ class NgxMaskDirective {
3078
3115
  this._maskService.applyValueChanges(el.value.length, this._justPasted(), this._code() === MaskExpression.BACKSPACE || this._code() === MaskExpression.DELETE);
3079
3116
  }
3080
3117
  }
3118
+ /**
3119
+ * #1488: locates the date/time field (DAY/MONTH/HOURS/HOUR/MINUTE/SECOND) that
3120
+ * `position` falls within, scanning `maskExpression` left-to-right. A field spans
3121
+ * its token plus its digit-pair companion — `0` for DAY/MONTH/MINUTE/SECOND (e.g.
3122
+ * `d0`, `M0`, `m0`, `s0`), or `HOUR` for the combined `Hh` hour field. Matches any
3123
+ * position from the field's first char up to and including its end (separator index),
3124
+ * so both a still-mid-typing caret and the just-completed field are covered — the
3125
+ * precise overflow decision (single first digit vs. completed 2-digit value) happens
3126
+ * in `_isFieldOverflowing`. Returns null for non-date/time masks, a no-op for the caller.
3127
+ */
3128
+ _findDateTimeFieldAt(maskExpression, position) {
3129
+ const dateTimeTokens = [
3130
+ MaskExpression.DAY,
3131
+ MaskExpression.MONTH,
3132
+ MaskExpression.HOURS,
3133
+ MaskExpression.HOUR,
3134
+ MaskExpression.MINUTE,
3135
+ MaskExpression.SECOND,
3136
+ ];
3137
+ let index = 0;
3138
+ while (index < maskExpression.length) {
3139
+ const token = maskExpression[index] ?? MaskExpression.EMPTY_STRING;
3140
+ if (dateTimeTokens.includes(token)) {
3141
+ const next = maskExpression[index + 1];
3142
+ const isCombinedHour = token === MaskExpression.HOURS && next === MaskExpression.HOUR;
3143
+ const end = next === MaskExpression.NUMBER_ZERO || isCombinedHour ? index + 2 : index + 1;
3144
+ if (position >= index && position <= end) {
3145
+ return {
3146
+ token: token,
3147
+ start: index,
3148
+ end,
3149
+ };
3150
+ }
3151
+ index = end;
3152
+ continue;
3153
+ }
3154
+ index++;
3155
+ }
3156
+ return null;
3157
+ }
3158
+ /**
3159
+ * Per-token overflow thresholds, mirroring the ones `generic-pattern.handler.ts`
3160
+ * already establishes (day 31, month 12, hour 23/12 w/ apm, minute/second 59) — kept
3161
+ * as a small local table rather than importing from the handler (see design's
3162
+ * Rejected Alternatives: the directive and the handler stay on separate sides of the
3163
+ * display/parse boundary).
3164
+ *
3165
+ * Two overflow shapes are checked, mirroring how the handler itself decides mid-type:
3166
+ * - `firstDigitValue`: when `position` is exactly one char past the field's start (the
3167
+ * just-typed char is the field's first digit), an out-of-range first digit alone
3168
+ * (e.g. month digit `3`, day digit `4`) already triggers the handler's own leadZero
3169
+ * pad within this same keystroke — `el.value` hasn't caught up yet at this point in
3170
+ * the pipeline, so the raw typed digit (`inputValue`) must be read instead.
3171
+ * - `fieldValue`: once both digits of the field are present in `el.value`, an
3172
+ * out-of-range 2-digit value (e.g. day `33`, month `13`) triggers the shift directly.
3173
+ */
3174
+ _isFieldOverflowing(field, position, elValue, inputValue, apm) {
3175
+ const fieldValue = Number(elValue.slice(field.start, field.end));
3176
+ const isFirstDigitOfField = position === field.start + 1;
3177
+ const firstDigitValue = isFirstDigitOfField
3178
+ ? Number(inputValue.slice(position - 1, position))
3179
+ : NaN;
3180
+ switch (field.token) {
3181
+ case MaskExpression.DAY:
3182
+ return (fieldValue > 31 && fieldValue < 40) || firstDigitValue > 3;
3183
+ case MaskExpression.MONTH:
3184
+ return fieldValue > 12 || firstDigitValue > 1;
3185
+ case MaskExpression.HOURS:
3186
+ return apm
3187
+ ? fieldValue > 9 || firstDigitValue > 9
3188
+ : fieldValue > 23 || firstDigitValue > 2;
3189
+ case MaskExpression.HOUR:
3190
+ return apm ? fieldValue > 12 : fieldValue > 23;
3191
+ case MaskExpression.MINUTE:
3192
+ case MaskExpression.SECOND:
3193
+ return fieldValue > 59 || firstDigitValue > 5;
3194
+ default:
3195
+ return false;
3196
+ }
3197
+ }
3081
3198
  /**
3082
3199
  * Whether any pattern slot of the current mask expression can accept a letter.
3083
3200
  * IME composition is only meaningful for such masks; for purely numeric masks
@@ -3261,6 +3378,9 @@ class NgxMaskDirective {
3261
3378
  this._setMask();
3262
3379
  const isTextarea = el.tagName.toLowerCase() === 'textarea';
3263
3380
  if (el.type !== 'number') {
3381
+ if (this._insertNumpadDecimalMarker(e, el)) {
3382
+ return;
3383
+ }
3264
3384
  if (e.key === MaskExpression.ARROW_UP && !isTextarea) {
3265
3385
  e.preventDefault();
3266
3386
  }
@@ -3310,7 +3430,15 @@ class NgxMaskDirective {
3310
3430
  // triggers change detection (DOM churn around the input), leaving the
3311
3431
  // model empty but the view untouched until a second Backspace (#1350).
3312
3432
  e.preventDefault();
3313
- const displayValue = this._maskService.applyMask(MaskExpression.EMPTY_STRING, this._maskService.maskExpression, 0, false, true);
3433
+ // The default action (and its input event) is suppressed, so the
3434
+ // inputTransformFn would never see the cleared value; run it here (#1651).
3435
+ const transformedEmpty = this._maskService.inputTransformFn
3436
+ ? this._maskService.inputTransformFn(MaskExpression.EMPTY_STRING)
3437
+ : MaskExpression.EMPTY_STRING;
3438
+ const clearedValue = typeof transformedEmpty === 'string' || typeof transformedEmpty === 'number'
3439
+ ? String(transformedEmpty)
3440
+ : MaskExpression.EMPTY_STRING;
3441
+ const displayValue = this._maskService.applyMask(clearedValue, this._maskService.maskExpression, 0, false, true);
3314
3442
  el.value = displayValue;
3315
3443
  this._inputValue.set(displayValue);
3316
3444
  const caret = Math.min(this._maskService.prefix.length, displayValue.length);
@@ -3332,6 +3460,44 @@ class NgxMaskDirective {
3332
3460
  this._maskService.selEnd = el.selectionEnd;
3333
3461
  }
3334
3462
  }
3463
+ /**
3464
+ * #1641: on layouts whose decimal separator is a comma, the numeric keypad still emits
3465
+ * a period, which a separator mask configured with a comma decimal marker rejects. The
3466
+ * keypad decimal key (`key: 'Decimal'`, or a `.` from `code: 'NumpadDecimal'`) is
3467
+ * therefore turned into the configured marker. Only the keypad key is intercepted: a
3468
+ * period typed on the main keyboard keeps its normal behavior (e.g. as a thousand
3469
+ * separator). Returns true when the key press was handled.
3470
+ */
3471
+ _insertNumpadDecimalMarker(e, el) {
3472
+ const isNumpadDecimal = e.key === MaskExpression.NUMPAD_DECIMAL ||
3473
+ (e.code === MaskExpression.NUMPAD_DECIMAL_CODE && e.key === MaskExpression.DOT);
3474
+ if (!isNumpadDecimal ||
3475
+ e.ctrlKey ||
3476
+ e.metaKey ||
3477
+ e.altKey ||
3478
+ el.readOnly ||
3479
+ !this._maskValue().startsWith(MaskExpression.SEPARATOR)) {
3480
+ return false;
3481
+ }
3482
+ const configuredMarker = this._maskService.decimalMarker;
3483
+ const marker = Array.isArray(configuredMarker)
3484
+ ? configuredMarker.find((item) => item !== this._maskService.thousandSeparator)
3485
+ : configuredMarker;
3486
+ if (marker !== MaskExpression.COMMA) {
3487
+ return false;
3488
+ }
3489
+ const start = el.selectionStart ?? el.value.length;
3490
+ const end = el.selectionEnd ?? start;
3491
+ e.preventDefault();
3492
+ this._code.set(MaskExpression.NUMPAD_DECIMAL_CODE);
3493
+ this._inputValue.set(el.value);
3494
+ this._maskService.selStart = start;
3495
+ this._maskService.selEnd = end;
3496
+ el.value = `${el.value.slice(0, start)}${marker}${el.value.slice(end)}`;
3497
+ el.setSelectionRange(start + 1, start + 1);
3498
+ el.dispatchEvent(new InputEvent('input', { inputType: 'insertText', data: marker, bubbles: true }));
3499
+ return true;
3500
+ }
3335
3501
  /**
3336
3502
  * Restores the control's pristine/untouched state after a writeValue-driven emission.
3337
3503
  *
@@ -3683,7 +3849,9 @@ class NgxMaskDirective {
3683
3849
  : this.prefix()
3684
3850
  ? alternative.length + this.prefix().length
3685
3851
  : alternative.length;
3686
- return processedValue.length >= requiredLength;
3852
+ // Exact match, not `>=`: a longer value must not pass on the strength of a
3853
+ // shorter alternative it has already outgrown (#1645, 7 digits vs `00000 9`).
3854
+ return processedValue.length === requiredLength;
3687
3855
  });
3688
3856
  }
3689
3857
  /**
@@ -3799,8 +3967,8 @@ class NgxMaskDirective {
3799
3967
  return uniqueCharacters.size === 1;
3800
3968
  });
3801
3969
  }
3802
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: NgxMaskDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
3803
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.7", type: NgxMaskDirective, isStandalone: true, selector: "input[mask], textarea[mask]", inputs: { mask: { classPropertyName: "mask", publicName: "mask", isSignal: true, isRequired: false, transformFunction: null }, specialCharacters: { classPropertyName: "specialCharacters", publicName: "specialCharacters", isSignal: true, isRequired: false, transformFunction: null }, patterns: { classPropertyName: "patterns", publicName: "patterns", isSignal: true, isRequired: false, transformFunction: null }, prefix: { classPropertyName: "prefix", publicName: "prefix", isSignal: true, isRequired: false, transformFunction: null }, suffix: { classPropertyName: "suffix", publicName: "suffix", isSignal: true, isRequired: false, transformFunction: null }, thousandSeparator: { classPropertyName: "thousandSeparator", publicName: "thousandSeparator", isSignal: true, isRequired: false, transformFunction: null }, decimalMarker: { classPropertyName: "decimalMarker", publicName: "decimalMarker", isSignal: true, isRequired: false, transformFunction: null }, dropSpecialCharacters: { classPropertyName: "dropSpecialCharacters", publicName: "dropSpecialCharacters", isSignal: true, isRequired: false, transformFunction: null }, hiddenInput: { classPropertyName: "hiddenInput", publicName: "hiddenInput", isSignal: true, isRequired: false, transformFunction: null }, showMaskTyped: { classPropertyName: "showMaskTyped", publicName: "showMaskTyped", isSignal: true, isRequired: false, transformFunction: null }, placeHolderCharacter: { classPropertyName: "placeHolderCharacter", publicName: "placeHolderCharacter", isSignal: true, isRequired: false, transformFunction: null }, shownMaskExpression: { classPropertyName: "shownMaskExpression", publicName: "shownMaskExpression", isSignal: true, isRequired: false, transformFunction: null }, clearIfNotMatch: { classPropertyName: "clearIfNotMatch", publicName: "clearIfNotMatch", isSignal: true, isRequired: false, transformFunction: null }, validation: { classPropertyName: "validation", publicName: "validation", isSignal: true, isRequired: false, transformFunction: null }, separatorLimit: { classPropertyName: "separatorLimit", publicName: "separatorLimit", isSignal: true, isRequired: false, transformFunction: null }, typeFromDecimals: { classPropertyName: "typeFromDecimals", publicName: "typeFromDecimals", isSignal: true, isRequired: false, transformFunction: null }, allowNegativeNumbers: { classPropertyName: "allowNegativeNumbers", publicName: "allowNegativeNumbers", isSignal: true, isRequired: false, transformFunction: null }, leadZeroDateTime: { classPropertyName: "leadZeroDateTime", publicName: "leadZeroDateTime", isSignal: true, isRequired: false, transformFunction: null }, leadZero: { classPropertyName: "leadZero", publicName: "leadZero", isSignal: true, isRequired: false, transformFunction: null }, triggerOnMaskChange: { classPropertyName: "triggerOnMaskChange", publicName: "triggerOnMaskChange", isSignal: true, isRequired: false, transformFunction: null }, apm: { classPropertyName: "apm", publicName: "apm", isSignal: true, isRequired: false, transformFunction: null }, inputTransformFn: { classPropertyName: "inputTransformFn", publicName: "inputTransformFn", isSignal: true, isRequired: false, transformFunction: null }, outputTransformFn: { classPropertyName: "outputTransformFn", publicName: "outputTransformFn", isSignal: true, isRequired: false, transformFunction: null }, keepCharacterPositions: { classPropertyName: "keepCharacterPositions", publicName: "keepCharacterPositions", isSignal: true, isRequired: false, transformFunction: null }, instantPrefix: { classPropertyName: "instantPrefix", publicName: "instantPrefix", isSignal: true, isRequired: false, transformFunction: null }, defaultValueOnBlur: { classPropertyName: "defaultValueOnBlur", publicName: "defaultValueOnBlur", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, touched: { classPropertyName: "touched", publicName: "touched", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", touched: "touchedChange", maskFilled: "maskFilled" }, host: { listeners: { "paste": "onPaste()", "focus": "onFocus()", "ngModelChange": "onModelChange($event)", "input": "onInput($event)", "compositionstart": "onCompositionStart()", "compositionend": "onCompositionEnd($event)", "blur": "onBlur($event)", "click": "onClick($event)", "keydown": "onKeyDown($event)" } }, providers: [
3970
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: NgxMaskDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
3971
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.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: [
3804
3972
  {
3805
3973
  provide: NG_VALUE_ACCESSOR,
3806
3974
  useExisting: NgxMaskDirective,
@@ -3814,7 +3982,7 @@ class NgxMaskDirective {
3814
3982
  NgxMaskService,
3815
3983
  ], exportAs: ["mask", "ngxMask"], usesOnChanges: true, ngImport: i0 });
3816
3984
  }
3817
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: NgxMaskDirective, decorators: [{
3985
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: NgxMaskDirective, decorators: [{
3818
3986
  type: Directive,
3819
3987
  args: [{
3820
3988
  selector: 'input[mask], textarea[mask]',
@@ -3952,10 +4120,10 @@ class NgxMaskPipe {
3952
4120
  });
3953
4121
  }
3954
4122
  }
3955
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: NgxMaskPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
3956
- static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "22.0.7", ngImport: i0, type: NgxMaskPipe, isStandalone: true, name: "mask" });
4123
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: NgxMaskPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
4124
+ static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "22.1.7", ngImport: i0, type: NgxMaskPipe, isStandalone: true, name: "mask" });
3957
4125
  }
3958
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: NgxMaskPipe, decorators: [{
4126
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: NgxMaskPipe, decorators: [{
3959
4127
  type: Pipe,
3960
4128
  args: [{
3961
4129
  name: 'mask',