ngx-mask 22.0.2 → 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'),
@@ -1664,11 +1666,24 @@ class NgxMaskService extends NgxMaskApplierService {
1664
1666
  return inputValue
1665
1667
  .split(MaskExpression.EMPTY_STRING)
1666
1668
  .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;
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;
1672
1687
  }
1673
1688
  return curr;
1674
1689
  })
@@ -2973,10 +2988,17 @@ class NgxMaskDirective {
2973
2988
  inputSymbol +
2974
2989
  this._inputValue().slice(position + 1);
2975
2990
  }
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)) {
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())) {
2980
3002
  position = position + 2;
2981
3003
  }
2982
3004
  }
@@ -3078,6 +3100,86 @@ class NgxMaskDirective {
3078
3100
  this._maskService.applyValueChanges(el.value.length, this._justPasted(), this._code() === MaskExpression.BACKSPACE || this._code() === MaskExpression.DELETE);
3079
3101
  }
3080
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
+ }
3081
3183
  /**
3082
3184
  * Whether any pattern slot of the current mask expression can accept a letter.
3083
3185
  * IME composition is only meaningful for such masks; for purely numeric masks
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ngx-mask",
3
- "version": "22.0.2",
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",
@@ -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