ngx-mask 22.2.2 → 22.2.4

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.
@@ -204,6 +204,22 @@ const cpfCnpjHandler = function (state, params) {
204
204
  return state;
205
205
  };
206
206
 
207
+ /** Punctuation the built-in email mask accepts besides `[a-zA-Z0-9]` (#1668). */
208
+ const EMAIL_AT = '@';
209
+ const EMAIL_LOCAL_PUNCTUATION = '._+-';
210
+ const EMAIL_DOMAIN_PUNCTUATION = '.-';
211
+ /**
212
+ * Whether `inputSymbol` is allowed email punctuation after `result` (the text accepted so far).
213
+ * Punctuation cannot be the first character of the address, and '..' is blocked.
214
+ */
215
+ function isEmailPunctuation(inputSymbol, result) {
216
+ const allowed = result.includes(EMAIL_AT) ? EMAIL_DOMAIN_PUNCTUATION : EMAIL_LOCAL_PUNCTUATION;
217
+ const last = result.slice(-1);
218
+ return (allowed.includes(inputSymbol) &&
219
+ last !== MaskExpression.EMPTY_STRING &&
220
+ last !== EMAIL_AT &&
221
+ !(inputSymbol === MaskExpression.DOT && last === MaskExpression.DOT));
222
+ }
207
223
  /**
208
224
  * Generic character-by-character pattern loop (moved verbatim from applyMask,
209
225
  * original lines 581-968). This is the default/fallback handler — always runs when
@@ -506,6 +522,12 @@ const genericPatternHandler = function (state, params) {
506
522
  cursor += 3;
507
523
  result += inputSymbol;
508
524
  }
525
+ else if (maskExpression === MaskExpression.EMAIL_MASK &&
526
+ isEmailPunctuation(inputSymbol, result)) {
527
+ // #1668: 'A' only matches [a-zA-Z0-9], so the punctuation real addresses carry
528
+ // would be dropped. Keep it inside the current 'A*' run (cursor is unchanged).
529
+ result += inputSymbol;
530
+ }
509
531
  else if (this.showMaskTyped &&
510
532
  this.specialCharacters.indexOf(inputSymbol) < 0 &&
511
533
  inputSymbol !== this.placeHolderCharacter &&
@@ -1377,9 +1399,7 @@ class NgxMaskService extends NgxMaskApplierService {
1377
1399
  if (this._emitValue && this.triggerOnMaskChange) {
1378
1400
  this.formControlResult(rawValue);
1379
1401
  }
1380
- if (!this.triggerOnMaskChange) {
1381
- this.maskChanged = false;
1382
- }
1402
+ this._clearMaskChangedUnlessTriggered();
1383
1403
  return rawValue;
1384
1404
  }
1385
1405
  return inputValue !== this.actualValue ? this.actualValue : inputValue;
@@ -1427,6 +1447,10 @@ class NgxMaskService extends NgxMaskApplierService {
1427
1447
  if (!this.writingValue) {
1428
1448
  this.formControlResult(this.prefix);
1429
1449
  }
1450
+ // The field is now empty: record it, or retyping the previous value in one
1451
+ // input event (autofill, undo) would match the stale currentValue and never emit.
1452
+ this.previousValue = this.currentValue;
1453
+ this.currentValue = MaskExpression.EMPTY_STRING;
1430
1454
  return `${this.prefix}${this.maskIsShown}${this.suffix}`;
1431
1455
  }
1432
1456
  const getSymbol = !!inputValue && typeof this.selStart === 'number'
@@ -1607,9 +1631,7 @@ class NgxMaskService extends NgxMaskApplierService {
1607
1631
  this.formControlResult(result);
1608
1632
  }
1609
1633
  // Reset maskChanged flag even if we didn't emit
1610
- if (this.maskChanged && !this.triggerOnMaskChange) {
1611
- this.maskChanged = false;
1612
- }
1634
+ this._clearMaskChangedUnlessTriggered();
1613
1635
  // Handle hidden input and showMaskTyped
1614
1636
  if (!this.showMaskTyped || (this.showMaskTyped && this.hiddenInput)) {
1615
1637
  if (this.hiddenInput) {
@@ -1820,6 +1842,16 @@ class NgxMaskService extends NgxMaskApplierService {
1820
1842
  .filter((item) => this._findDropSpecialChar(item));
1821
1843
  return chars.length;
1822
1844
  }
1845
+ /**
1846
+ * Consumes a pending `maskChanged` once a render has handled the reconfiguration, unless
1847
+ * `triggerOnMaskChange` still needs it to emit the re-masked value (formControlResult clears
1848
+ * it then).
1849
+ */
1850
+ _clearMaskChangedUnlessTriggered() {
1851
+ if (!this.triggerOnMaskChange) {
1852
+ this.maskChanged = false;
1853
+ }
1854
+ }
1823
1855
  removeMask(inputValue) {
1824
1856
  return this._removeMask(this._removeSuffix(this._removePrefix(inputValue)), this.specialCharacters.concat('_').concat(this.placeHolderCharacter));
1825
1857
  }
@@ -1973,11 +2005,15 @@ class NgxMaskService extends NgxMaskApplierService {
1973
2005
  .split(this.placeHolderCharacter)
1974
2006
  .join(MaskExpression.EMPTY_STRING);
1975
2007
  }
1976
- if (Array.isArray(this.dropSpecialCharacters)) {
2008
+ // #1668: an email address is meaningless without its '@' and '.', so the built-in
2009
+ // email mask always reports the full address, whatever dropSpecialCharacters says.
2010
+ const keepSpecialCharacters = this.maskExpression === MaskExpression.EMAIL_MASK;
2011
+ if (Array.isArray(this.dropSpecialCharacters) && !keepSpecialCharacters) {
1977
2012
  this.onChange(outputTransformFn(this._toNumber(this._checkSymbols(this._removeMask(this._removeSuffix(this._removePrefix(inputValue)), this.dropSpecialCharacters)))));
1978
2013
  }
1979
- else if (this.dropSpecialCharacters ||
1980
- (!this.dropSpecialCharacters && this.prefix === inputValue)) {
2014
+ else if (!keepSpecialCharacters &&
2015
+ (this.dropSpecialCharacters ||
2016
+ (!this.dropSpecialCharacters && this.prefix === inputValue))) {
1981
2017
  this.onChange(outputTransformFn(this._toNumber(this._checkSymbols(this._removeSuffix(this._removePrefix(inputValue))))));
1982
2018
  }
1983
2019
  else {
@@ -2351,7 +2387,8 @@ class NgxMaskDirective {
2351
2387
  * originated from the view. Re-masking that unmasked echo is at best a redundant re-render
2352
2388
  * and at worst destructive for masks whose unmasked form is ambiguous (e.g. IP:
2353
2389
  * '192168178' cannot reconstruct the typed dots of '192.168.1.78'). `writeValue()` consumes
2354
- * this marker to skip exactly that echo. `null` = no pending propagation.
2390
+ * this marker to skip exactly that echo; ngOnChanges reads it to recognise a `[value]`
2391
+ * binding echo of it (#1671). `null` = no pending propagation.
2355
2392
  */
2356
2393
  _lastPropagatedValue = null;
2357
2394
  /**
@@ -2423,15 +2460,11 @@ class NgxMaskDirective {
2423
2460
  effect(() => {
2424
2461
  const signalValue = this.value();
2425
2462
  untracked(() => {
2426
- if (this._isCvaMode()) {
2427
- return;
2463
+ if (this._valueEffectWillWrite(signalValue)) {
2464
+ this.writeValue(signalValue);
2428
2465
  }
2429
- if (this._skipNextValueEffect()) {
2466
+ else {
2430
2467
  this._skipNextValueEffect.set(false);
2431
- return;
2432
- }
2433
- if (String(signalValue) !== String(this._inputValue())) {
2434
- this.writeValue(signalValue);
2435
2468
  }
2436
2469
  });
2437
2470
  });
@@ -2591,7 +2624,20 @@ class NgxMaskDirective {
2591
2624
  if (keepCharacterPositions) {
2592
2625
  this._maskService.keepCharacterPositions = keepCharacterPositions.currentValue;
2593
2626
  }
2594
- this._applyMask();
2627
+ // #1671: a standalone `[(value)]` change reaches ngOnChanges BEFORE the value effect
2628
+ // writes it, so `_inputValue` still holds the PREVIOUS value here. Re-rendering it would
2629
+ // run applyMask on stale input, and whatever that emits (the showMaskTyped skeleton's
2630
+ // '', a hiddenInput / keepCharacterPositions reconstruction) flows back through onChange,
2631
+ // overwrites the freshly bound model and makes the effect skip the real write. When the
2632
+ // effect is about to render the new value via writeValue() anyway, only apply the mask
2633
+ // configuration here. The effect only runs when the `value` SIGNAL changes: a parent
2634
+ // re-binding the value we last propagated (an echo, possibly applied in a later task)
2635
+ // changes the binding but not the signal, which already holds it — that pass renders.
2636
+ const valueChange = changes['value'];
2637
+ const valueEffectWillRender = !!valueChange &&
2638
+ String(valueChange.currentValue) !== this._lastPropagatedValue &&
2639
+ this._valueEffectWillWrite(valueChange.currentValue);
2640
+ this._applyMask(!valueEffectWillRender);
2595
2641
  if (!this._configApplied) {
2596
2642
  this._configApplied = true;
2597
2643
  if (this._hasPendingInitialValue) {
@@ -3713,6 +3759,18 @@ class NgxMaskDirective {
3713
3759
  }
3714
3760
  });
3715
3761
  }
3762
+ /**
3763
+ * Whether the `value`-model effect will render `value` itself via writeValue(): only in
3764
+ * standalone `[(value)]` / Signal Forms mode (Angular's writeValue owns rendering in CVA mode), not for
3765
+ * the model echo of our own onChange propagation (`_skipNextValueEffect`), and only when it
3766
+ * differs from what is already rendered. Shared with ngOnChanges (#1671), which must leave
3767
+ * exactly that render to the effect.
3768
+ */
3769
+ _valueEffectWillWrite(value) {
3770
+ return (!this._isCvaMode() &&
3771
+ !this._skipNextValueEffect() &&
3772
+ String(value) !== String(this._inputValue()));
3773
+ }
3716
3774
  /**
3717
3775
  * Focus the input element.
3718
3776
  * Required by FormValueControl interface for Signal Forms.
@@ -3755,8 +3813,16 @@ class NgxMaskDirective {
3755
3813
  this._renderer.setProperty(this._elementRef.nativeElement, 'disabled', isDisabled);
3756
3814
  this._maskService.formElementProperty = ['disabled', isDisabled];
3757
3815
  }
3758
- _applyMask() {
3816
+ _applyMask(renderInputValue) {
3759
3817
  this._maskService.maskExpression = this._maskService._repeatPatternSymbols(this._maskValue() || '');
3818
+ if (!renderInputValue) {
3819
+ // Consume the one-shot flags ngOnChanges set for THIS render, as applyMask would:
3820
+ // writingValue (thousandSeparator change) and maskChanged (mask change). Nothing
3821
+ // else clears them when the following writeValue() takes its no-applyMask branch.
3822
+ this._maskService.writingValue = false;
3823
+ this._maskService._clearMaskChangedUnlessTriggered();
3824
+ return;
3825
+ }
3760
3826
  this._maskService.formElementProperty = [
3761
3827
  'value',
3762
3828
  this._maskedOrVerbatim(this._inputValue()),