vijay06
Version:
awesome ngx mask
495 lines (446 loc) • 15.6 kB
text/typescript
import { ElementRef, Inject, Injectable, Renderer2 } from '@angular/core';
import { DOCUMENT } from '@angular/common';
import { config, IConfig } from './config';
import { MaskApplierService } from './mask-applier.service';
export class MaskService extends MaskApplierService {
public override maskExpression: string = '';
public isNumberValue: boolean = false;
public override placeHolderCharacter: string = '_';
public maskIsShown: string = '';
public selStart: number | null = null;
public selEnd: number | null = null;
/**
* Whether we are currently in writeValue function, in this case when applying the mask we don't want to trigger onChange function,
* since writeValue should be a one way only process of writing the DOM value based on the Angular model value.
*/
public writingValue: boolean = false;
public maskChanged: boolean = false;
public triggerOnMaskChange: boolean = false;
public onChange = (_: any) => {};
public constructor(
private document: any,
protected override _config: IConfig,
private _elementRef: ElementRef,
private _renderer: Renderer2,
) {
super(_config);
}
// eslint-disable-next-line complexity
public override applyMask(
inputValue: string,
maskExpression: string,
position: number = 0,
justPasted = false,
backspaced = false,
cb: Function = () => {},
): string {
if (!maskExpression) {
return inputValue !== this.actualValue ? this.actualValue : inputValue;
}
this.maskIsShown = this.showMaskTyped ? this.showMaskInInput() : '';
if (this.maskExpression === 'IP' && this.showMaskTyped) {
this.maskIsShown = this.showMaskInInput(inputValue || '#');
}
if (this.maskExpression === 'CPF_CNPJ' && this.showMaskTyped) {
this.maskIsShown = this.showMaskInInput(inputValue || '#');
}
if (!inputValue && this.showMaskTyped) {
this.formControlResult(this.prefix);
return this.prefix + this.maskIsShown;
}
const getSymbol: string | undefined =
!!inputValue && typeof this.selStart === 'number' ? inputValue[this.selStart] : '';
let newInputValue = '';
if (this.hiddenInput !== undefined && !this.writingValue) {
let actualResult: string[] = this.actualValue.split('');
// eslint-disable @typescript-eslint/no-unused-expressions
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
inputValue !== '' && actualResult.length
? typeof this.selStart === 'number' && typeof this.selEnd === 'number'
? inputValue.length > actualResult.length
? actualResult.splice(this.selStart, 0, getSymbol!)
: inputValue.length < actualResult.length
? actualResult.length - inputValue.length === 1
? actualResult.splice(this.selStart - 1, 1)
: actualResult.splice(this.selStart, this.selEnd - this.selStart)
: null
: null
: (actualResult = []);
if (this.showMaskTyped) {
// eslint-disable-next-line no-param-reassign
inputValue = this.removeMask(inputValue);
}
// eslint-enable @typescript-eslint/no-unused-expressions
newInputValue =
this.actualValue.length && actualResult.length <= inputValue.length
? this.shiftTypedSymbols(actualResult.join(''))
: inputValue;
}
if (this.showMaskTyped) {
// eslint-disable-next-line no-param-reassign
inputValue = this.removeMask(inputValue);
}
newInputValue = Boolean(newInputValue) && newInputValue.length ? newInputValue : inputValue;
const result: string = super.applyMask(
newInputValue,
maskExpression,
position,
justPasted,
backspaced,
cb,
);
this.actualValue = this.getActualValue(result);
// handle some separator implications:
// a.) adjust decimalMarker default (. -> ,) if thousandSeparator is a dot
if (this.thousandSeparator === '.' && this.decimalMarker === '.') {
this.decimalMarker = ',';
}
// b) remove decimal marker from list of special characters to mask
if (this.maskExpression.startsWith('separator') && this.dropSpecialCharacters === true) {
this.maskSpecialCharacters = this.maskSpecialCharacters.filter(
(item: string) =>
!this._compareOrIncludes(item, this.decimalMarker, this.thousandSeparator), //item !== this.decimalMarker, // !
);
}
this.formControlResult(result);
if (!this.showMaskTyped) {
if (this.hiddenInput) {
return result && result.length ? this.hideInput(result, this.maskExpression) : result;
}
return result;
}
const resLen: number = result.length;
const prefNmask: string = this.prefix + this.maskIsShown;
if (this.maskExpression.includes('H')) {
const countSkipedSymbol = this._numberSkipedSymbols(result);
return result + prefNmask.slice(resLen + countSkipedSymbol);
} else if (this.maskExpression === 'IP' || this.maskExpression === 'CPF_CNPJ') {
return result + prefNmask;
}
return result + prefNmask.slice(resLen);
}
// get the number of characters that were shifted
private _numberSkipedSymbols(value: string): number {
const regex = /(^|\D)(\d\D)/g;
let match = regex.exec(value);
let countSkipedSymbol = 0;
while (match != null) {
countSkipedSymbol += 1;
match = regex.exec(value);
}
return countSkipedSymbol;
}
public applyValueChanges(
position: number,
justPasted: boolean,
backspaced: boolean,
cb: Function = () => {},
): void {
const formElement = this._elementRef.nativeElement;
formElement.value = this.applyMask(
formElement.value,
this.maskExpression,
position,
justPasted,
backspaced,
cb,
);
if (formElement === this._getActiveElement()) {
return;
}
this.clearIfNotMatchFn();
}
public hideInput(inputValue: string, maskExpression: string): string {
return inputValue
.split('')
.map((curr: string, index: number) => {
if (
this.maskAvailablePatterns &&
this.maskAvailablePatterns[maskExpression[index]!] &&
this.maskAvailablePatterns[maskExpression[index]!]?.symbol
) {
return this.maskAvailablePatterns[maskExpression[index]!]?.symbol;
}
return curr;
})
.join('');
}
// this function is not necessary, it checks result against maskExpression
public getActualValue(res: string): string {
const compare: string[] = res
.split('')
.filter(
(symbol: string, i: number) =>
this._checkSymbolMask(symbol, this.maskExpression[i]!) ||
(this.maskSpecialCharacters.includes(this.maskExpression[i]!) &&
symbol === this.maskExpression[i]),
);
if (compare.join('') === res) {
return compare.join('');
}
return res;
}
public shiftTypedSymbols(inputValue: string): string {
let symbolToReplace = '';
const newInputValue: (string | undefined)[] =
(inputValue &&
inputValue.split('').map((currSymbol: string, index: number) => {
if (
this.maskSpecialCharacters.includes(inputValue[index + 1]!) &&
inputValue[index + 1] !== this.maskExpression[index + 1]
) {
symbolToReplace = currSymbol;
return inputValue[index + 1];
}
if (symbolToReplace.length) {
const replaceSymbol: string = symbolToReplace;
symbolToReplace = '';
return replaceSymbol;
}
return currSymbol;
})) ||
[];
return newInputValue.join('');
}
/**
* Convert number value to string
* 3.1415 -> '3.1415'
* 1e-7 -> '0.0000001'
*/
public numberToString(value: number | string): string {
if (!value && value !== 0) {
return String(value);
}
return Number(value).toLocaleString('fullwide', {
useGrouping: false,
maximumFractionDigits: 20,
});
}
public showMaskInInput(inputVal?: string): string {
if (this.showMaskTyped && !!this.shownMaskExpression) {
if (this.maskExpression.length !== this.shownMaskExpression.length) {
throw new Error('Mask expression must match mask placeholder length');
} else {
return this.shownMaskExpression;
}
} else if (this.showMaskTyped) {
if (inputVal) {
if (this.maskExpression === 'IP') {
return this._checkForIp(inputVal);
}
if (this.maskExpression === 'CPF_CNPJ') {
return this._checkForCpfCnpj(inputVal);
}
}
return this.maskExpression.replace(/\w/g, this.placeHolderCharacter);
}
return '';
}
public clearIfNotMatchFn(): void {
const formElement = this._elementRef.nativeElement;
if (
this.clearIfNotMatch &&
this.prefix.length + this.maskExpression.length + this.suffix.length !==
formElement.value.replace(/_/g, '').length
) {
this.formElementProperty = ['value', ''];
this.applyMask(formElement.value, this.maskExpression);
}
}
public set formElementProperty([name, value]: [string, string | boolean]) {
Promise.resolve().then(() =>
this._renderer.setProperty(this._elementRef.nativeElement, name, value),
);
}
public checkSpecialCharAmount(mask: string): number {
const chars: string[] = mask.split('').filter((item: string) => this._findSpecialChar(item));
return chars.length;
}
public removeMask(inputValue: string): string {
return this._removeMask(
this._removeSuffix(this._removePrefix(inputValue)),
this.maskSpecialCharacters.concat('_').concat(this.placeHolderCharacter),
);
}
private _checkForIp(inputVal: string): string {
if (inputVal === '#') {
return `${this.placeHolderCharacter}.${this.placeHolderCharacter}.${this.placeHolderCharacter}.${this.placeHolderCharacter}`;
}
const arr: string[] = [];
for (let i = 0; i < inputVal.length; i++) {
if (inputVal[i]?.match('\\d')) {
arr.push(inputVal[i]!);
}
}
if (arr.length <= 3) {
return `${this.placeHolderCharacter}.${this.placeHolderCharacter}.${this.placeHolderCharacter}`;
}
if (arr.length > 3 && arr.length <= 6) {
return `${this.placeHolderCharacter}.${this.placeHolderCharacter}`;
}
if (arr.length > 6 && arr.length <= 9) {
return this.placeHolderCharacter;
}
if (arr.length > 9 && arr.length <= 12) {
return '';
}
return '';
}
private _checkForCpfCnpj(inputVal: string): string {
const cpf =
`${this.placeHolderCharacter}${this.placeHolderCharacter}${this.placeHolderCharacter}` +
`.${this.placeHolderCharacter}${this.placeHolderCharacter}${this.placeHolderCharacter}` +
`.${this.placeHolderCharacter}${this.placeHolderCharacter}${this.placeHolderCharacter}` +
`-${this.placeHolderCharacter}${this.placeHolderCharacter}`;
const cnpj =
`${this.placeHolderCharacter}${this.placeHolderCharacter}` +
`.${this.placeHolderCharacter}${this.placeHolderCharacter}${this.placeHolderCharacter}` +
`.${this.placeHolderCharacter}${this.placeHolderCharacter}${this.placeHolderCharacter}` +
`/${this.placeHolderCharacter}${this.placeHolderCharacter}${this.placeHolderCharacter}${this.placeHolderCharacter}` +
`-${this.placeHolderCharacter}${this.placeHolderCharacter}`;
if (inputVal === '#') {
return cpf;
}
const arr: string[] = [];
for (let i = 0; i < inputVal.length; i++) {
if (inputVal[i]?.match('\\d')) {
arr.push(inputVal[i]!);
}
}
if (arr.length <= 3) {
return cpf.slice(arr.length, cpf.length);
}
if (arr.length > 3 && arr.length <= 6) {
return cpf.slice(arr.length + 1, cpf.length);
}
if (arr.length > 6 && arr.length <= 9) {
return cpf.slice(arr.length + 2, cpf.length);
}
if (arr.length > 9 && arr.length < 11) {
return cpf.slice(arr.length + 3, cpf.length);
}
if (arr.length === 11) {
return '';
}
if (arr.length === 12) {
if (inputVal.length === 17) {
return cnpj.slice(16, cnpj.length);
}
return cnpj.slice(15, cnpj.length);
}
if (arr.length > 12 && arr.length <= 14) {
return cnpj.slice(arr.length + 4, cnpj.length);
}
return '';
}
/**
* Recursively determine the current active element by navigating the Shadow DOM until the Active Element is found.
*/
private _getActiveElement(document: DocumentOrShadowRoot = this.document): Element | null {
const shadowRootEl = document?.activeElement?.shadowRoot;
if (!shadowRootEl?.activeElement) {
return document.activeElement;
} else {
return this._getActiveElement(shadowRootEl);
}
}
/**
* Propogates the input value back to the Angular model by triggering the onChange function. It won't do this if writingValue
* is true. If that is true it means we are currently in the writeValue function, which is supposed to only update the actual
* DOM element based on the Angular model value. It should be a one way process, i.e. writeValue should not be modifying the Angular
* model value too. Therefore, we don't trigger onChange in this scenario.
* @param inputValue the current form input value
*/
private formControlResult(inputValue: string): void {
if (this.writingValue || (!this.triggerOnMaskChange && this.maskChanged)) {
this.maskChanged = false;
return;
}
if (Array.isArray(this.dropSpecialCharacters)) {
this.onChange(
this._toNumber(
this._removeMask(
this._removeSuffix(this._removePrefix(inputValue)),
this.dropSpecialCharacters,
),
),
);
} else if (this.dropSpecialCharacters) {
this.onChange(this._toNumber(this._checkSymbols(inputValue)));
} else {
this.onChange(this._removeSuffix(inputValue));
}
}
private _toNumber(value: string | number | undefined | null) {
if (!this.isNumberValue || value === '') {
return value;
}
const num = Number(value);
return Number.isNaN(num) ? value : num;
}
private _removeMask(value: string, specialCharactersForRemove: string[]): string {
return value ? value.replace(this._regExpForRemove(specialCharactersForRemove), '') : value;
}
private _removePrefix(value: string): string {
if (!this.prefix) {
return value;
}
return value ? value.replace(this.prefix, '') : value;
}
private _removeSuffix(value: string): string {
if (!this.suffix) {
return value;
}
return value ? value.replace(this.suffix, '') : value;
}
private _retrieveSeparatorValue(result: string): string {
return this._removeMask(
this._removeSuffix(this._removePrefix(result)),
this.maskSpecialCharacters,
);
}
private _regExpForRemove(specialCharactersForRemove: string[]): RegExp {
return new RegExp(
specialCharactersForRemove.map((item: string) => `\\${item}`).join('|'),
'gi',
);
}
private _replaceDecimalMarkerToDot(value: string): string {
const markers = Array.isArray(this.decimalMarker) ? this.decimalMarker : [this.decimalMarker];
return value.replace(this._regExpForRemove(markers), '.');
}
private _checkSymbols(result: string): string | number | undefined | null {
if (result === '') {
return result;
}
const separatorPrecision: number | null = this._retrieveSeparatorPrecision(this.maskExpression);
const separatorValue: string = this._replaceDecimalMarkerToDot(
this._retrieveSeparatorValue(result),
);
if (!this.isNumberValue) {
return separatorValue;
}
if (separatorPrecision) {
if (result === this.decimalMarker) {
return null;
}
return this._checkPrecision(this.maskExpression, separatorValue);
} else {
return Number(separatorValue);
}
}
// TODO should think about helpers or separting decimal precision to own property
private _retrieveSeparatorPrecision(maskExpretion: string): number | null {
const matcher: RegExpMatchArray | null = maskExpretion.match(
new RegExp(`^separator\\.([^d]*)`),
);
return matcher ? Number(matcher[1]) : null;
}
private _checkPrecision(separatorExpression: string, separatorValue: string): number | string {
if (separatorExpression.indexOf('2') > 0) {
return Number(separatorValue).toFixed(2);
}
return Number(separatorValue);
}
}