ngx-mask 22.2.1 → 22.2.3

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,5 +1,5 @@
1
1
  import * as i0 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';
2
+ import { InjectionToken, EventEmitter, inject, Injectable, signal, ElementRef, Renderer2, makeEnvironmentProviders, input, model, booleanAttribute, output, ChangeDetectorRef, Injector, effect, untracked, HostListener, Directive, EnvironmentInjector, Pipe } from '@angular/core';
3
3
  import { DOCUMENT } from '@angular/common';
4
4
  import { NgControl, NG_VALUE_ACCESSOR, NG_VALIDATORS } from '@angular/forms';
5
5
 
@@ -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;
@@ -1607,9 +1627,7 @@ class NgxMaskService extends NgxMaskApplierService {
1607
1627
  this.formControlResult(result);
1608
1628
  }
1609
1629
  // Reset maskChanged flag even if we didn't emit
1610
- if (this.maskChanged && !this.triggerOnMaskChange) {
1611
- this.maskChanged = false;
1612
- }
1630
+ this._clearMaskChangedUnlessTriggered();
1613
1631
  // Handle hidden input and showMaskTyped
1614
1632
  if (!this.showMaskTyped || (this.showMaskTyped && this.hiddenInput)) {
1615
1633
  if (this.hiddenInput) {
@@ -1820,6 +1838,16 @@ class NgxMaskService extends NgxMaskApplierService {
1820
1838
  .filter((item) => this._findDropSpecialChar(item));
1821
1839
  return chars.length;
1822
1840
  }
1841
+ /**
1842
+ * Consumes a pending `maskChanged` once a render has handled the reconfiguration, unless
1843
+ * `triggerOnMaskChange` still needs it to emit the re-masked value (formControlResult clears
1844
+ * it then).
1845
+ */
1846
+ _clearMaskChangedUnlessTriggered() {
1847
+ if (!this.triggerOnMaskChange) {
1848
+ this.maskChanged = false;
1849
+ }
1850
+ }
1823
1851
  removeMask(inputValue) {
1824
1852
  return this._removeMask(this._removeSuffix(this._removePrefix(inputValue)), this.specialCharacters.concat('_').concat(this.placeHolderCharacter));
1825
1853
  }
@@ -1973,11 +2001,15 @@ class NgxMaskService extends NgxMaskApplierService {
1973
2001
  .split(this.placeHolderCharacter)
1974
2002
  .join(MaskExpression.EMPTY_STRING);
1975
2003
  }
1976
- if (Array.isArray(this.dropSpecialCharacters)) {
2004
+ // #1668: an email address is meaningless without its '@' and '.', so the built-in
2005
+ // email mask always reports the full address, whatever dropSpecialCharacters says.
2006
+ const keepSpecialCharacters = this.maskExpression === MaskExpression.EMAIL_MASK;
2007
+ if (Array.isArray(this.dropSpecialCharacters) && !keepSpecialCharacters) {
1977
2008
  this.onChange(outputTransformFn(this._toNumber(this._checkSymbols(this._removeMask(this._removeSuffix(this._removePrefix(inputValue)), this.dropSpecialCharacters)))));
1978
2009
  }
1979
- else if (this.dropSpecialCharacters ||
1980
- (!this.dropSpecialCharacters && this.prefix === inputValue)) {
2010
+ else if (!keepSpecialCharacters &&
2011
+ (this.dropSpecialCharacters ||
2012
+ (!this.dropSpecialCharacters && this.prefix === inputValue))) {
1981
2013
  this.onChange(outputTransformFn(this._toNumber(this._checkSymbols(this._removeSuffix(this._removePrefix(inputValue))))));
1982
2014
  }
1983
2015
  else {
@@ -2351,7 +2383,8 @@ class NgxMaskDirective {
2351
2383
  * originated from the view. Re-masking that unmasked echo is at best a redundant re-render
2352
2384
  * and at worst destructive for masks whose unmasked form is ambiguous (e.g. IP:
2353
2385
  * '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.
2386
+ * this marker to skip exactly that echo; ngOnChanges reads it to recognise a `[value]`
2387
+ * binding echo of it (#1671). `null` = no pending propagation.
2355
2388
  */
2356
2389
  _lastPropagatedValue = null;
2357
2390
  /**
@@ -2423,15 +2456,11 @@ class NgxMaskDirective {
2423
2456
  effect(() => {
2424
2457
  const signalValue = this.value();
2425
2458
  untracked(() => {
2426
- if (this._isCvaMode()) {
2427
- return;
2459
+ if (this._valueEffectWillWrite(signalValue)) {
2460
+ this.writeValue(signalValue);
2428
2461
  }
2429
- if (this._skipNextValueEffect()) {
2462
+ else {
2430
2463
  this._skipNextValueEffect.set(false);
2431
- return;
2432
- }
2433
- if (String(signalValue) !== String(this._inputValue())) {
2434
- this.writeValue(signalValue);
2435
2464
  }
2436
2465
  });
2437
2466
  });
@@ -2591,7 +2620,20 @@ class NgxMaskDirective {
2591
2620
  if (keepCharacterPositions) {
2592
2621
  this._maskService.keepCharacterPositions = keepCharacterPositions.currentValue;
2593
2622
  }
2594
- this._applyMask();
2623
+ // #1671: a standalone `[(value)]` change reaches ngOnChanges BEFORE the value effect
2624
+ // writes it, so `_inputValue` still holds the PREVIOUS value here. Re-rendering it would
2625
+ // run applyMask on stale input, and whatever that emits (the showMaskTyped skeleton's
2626
+ // '', a hiddenInput / keepCharacterPositions reconstruction) flows back through onChange,
2627
+ // overwrites the freshly bound model and makes the effect skip the real write. When the
2628
+ // effect is about to render the new value via writeValue() anyway, only apply the mask
2629
+ // configuration here. The effect only runs when the `value` SIGNAL changes: a parent
2630
+ // re-binding the value we last propagated (an echo, possibly applied in a later task)
2631
+ // changes the binding but not the signal, which already holds it — that pass renders.
2632
+ const valueChange = changes['value'];
2633
+ const valueEffectWillRender = !!valueChange &&
2634
+ String(valueChange.currentValue) !== this._lastPropagatedValue &&
2635
+ this._valueEffectWillWrite(valueChange.currentValue);
2636
+ this._applyMask(!valueEffectWillRender);
2595
2637
  if (!this._configApplied) {
2596
2638
  this._configApplied = true;
2597
2639
  if (this._hasPendingInitialValue) {
@@ -3713,6 +3755,18 @@ class NgxMaskDirective {
3713
3755
  }
3714
3756
  });
3715
3757
  }
3758
+ /**
3759
+ * Whether the `value`-model effect will render `value` itself via writeValue(): only in
3760
+ * standalone `[(value)]` / Signal Forms mode (Angular's writeValue owns rendering in CVA mode), not for
3761
+ * the model echo of our own onChange propagation (`_skipNextValueEffect`), and only when it
3762
+ * differs from what is already rendered. Shared with ngOnChanges (#1671), which must leave
3763
+ * exactly that render to the effect.
3764
+ */
3765
+ _valueEffectWillWrite(value) {
3766
+ return (!this._isCvaMode() &&
3767
+ !this._skipNextValueEffect() &&
3768
+ String(value) !== String(this._inputValue()));
3769
+ }
3716
3770
  /**
3717
3771
  * Focus the input element.
3718
3772
  * Required by FormValueControl interface for Signal Forms.
@@ -3755,8 +3809,16 @@ class NgxMaskDirective {
3755
3809
  this._renderer.setProperty(this._elementRef.nativeElement, 'disabled', isDisabled);
3756
3810
  this._maskService.formElementProperty = ['disabled', isDisabled];
3757
3811
  }
3758
- _applyMask() {
3812
+ _applyMask(renderInputValue) {
3759
3813
  this._maskService.maskExpression = this._maskService._repeatPatternSymbols(this._maskValue() || '');
3814
+ if (!renderInputValue) {
3815
+ // Consume the one-shot flags ngOnChanges set for THIS render, as applyMask would:
3816
+ // writingValue (thousandSeparator change) and maskChanged (mask change). Nothing
3817
+ // else clears them when the following writeValue() takes its no-applyMask branch.
3818
+ this._maskService.writingValue = false;
3819
+ this._maskService._clearMaskChangedUnlessTriggered();
3820
+ return;
3821
+ }
3760
3822
  this._maskService.formElementProperty = [
3761
3823
  'value',
3762
3824
  this._maskedOrVerbatim(this._inputValue()),
@@ -4033,11 +4095,22 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
4033
4095
 
4034
4096
  class NgxMaskPipe {
4035
4097
  defaultOptions = inject(NGX_MASK_CONFIG);
4036
- _maskService = inject(NgxMaskService);
4098
+ _environmentInjector = inject(EnvironmentInjector);
4099
+ _maskService;
4037
4100
  _maskExpressionArray = [];
4038
4101
  mask = '';
4039
4102
  transform(value, mask, { patterns, maskAliases, ...config } = {}) {
4040
4103
  let processedValue = value;
4104
+ // NgxMaskService is stateful (built for the directive, which keeps value history across
4105
+ // keystrokes), so each transform gets a fresh instance: nothing from a previous call can
4106
+ // leak into this one (#1669).
4107
+ this._maskService = Injector.create({
4108
+ providers: [
4109
+ NgxMaskService,
4110
+ { provide: NGX_MASK_CONFIG, useValue: this.defaultOptions },
4111
+ ],
4112
+ parent: this._environmentInjector,
4113
+ }).get(NgxMaskService);
4041
4114
  // User-defined aliases expand BEFORE any other mask processing (incl. `||` handling).
4042
4115
  const resolvedMask = resolveMaskAlias(mask, {
4043
4116
  ...this.defaultOptions.maskAliases,
@@ -4048,7 +4121,7 @@ class NgxMaskPipe {
4048
4121
  ...this.defaultOptions,
4049
4122
  ...config,
4050
4123
  patterns: {
4051
- ...this._maskService.patterns,
4124
+ ...this.defaultOptions.patterns,
4052
4125
  ...patterns,
4053
4126
  },
4054
4127
  };
@@ -4096,7 +4169,6 @@ class NgxMaskPipe {
4096
4169
  if (this._maskService.decimalMarker === MaskExpression.COMMA) {
4097
4170
  processedValue = processedValue.replace(MaskExpression.DOT, MaskExpression.COMMA);
4098
4171
  }
4099
- this._maskService.isNumberValue = true;
4100
4172
  }
4101
4173
  if (processedValue === null || typeof processedValue === 'undefined') {
4102
4174
  return this._maskService.applyMask('', resolvedMask);