vijay06
Version:
awesome ngx mask
677 lines (612 loc) • 20.6 kB
text/typescript
import {
ControlValueAccessor,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
ValidationErrors,
Validator,
FormControl,
} from '@angular/forms';
import {
Directive,
EventEmitter,
forwardRef,
HostListener,
Inject,
Input,
OnChanges,
Output,
SimpleChanges,
} from '@angular/core';
import { DOCUMENT } from '@angular/common';
import { CustomKeyboardEvent } from './custom-keyboard-event';
import { config, IConfig, timeMasks, withoutValidation } from './config';
import { MaskService } from './mask.service';
export class MaskDirective implements ControlValueAccessor, OnChanges, Validator {
// eslint-disable-next-line @angular-eslint/no-input-rename
public maskExpression: string = '';
public specialCharacters: IConfig['specialCharacters'] = [];
public patterns: IConfig['patterns'] = {};
public prefix: IConfig['prefix'] = '';
public suffix: IConfig['suffix'] = '';
public thousandSeparator: IConfig['thousandSeparator'] = ' ';
public decimalMarker: IConfig['decimalMarker'] = '.';
public dropSpecialCharacters: IConfig['dropSpecialCharacters'] | null = null;
public hiddenInput: IConfig['hiddenInput'] | null = null;
public showMaskTyped: IConfig['showMaskTyped'] | null = null;
public placeHolderCharacter: IConfig['placeHolderCharacter'] | null = null;
public shownMaskExpression: IConfig['shownMaskExpression'] | null = null;
public showTemplate: IConfig['showTemplate'] | null = null;
public clearIfNotMatch: IConfig['clearIfNotMatch'] | null = null;
public validation: IConfig['validation'] | null = null;
public separatorLimit: IConfig['separatorLimit'] | null = null;
public allowNegativeNumbers: IConfig['allowNegativeNumbers'] | null = null;
public leadZeroDateTime: IConfig['leadZeroDateTime'] | null = null;
public triggerOnMaskChange: IConfig['triggerOnMaskChange'] | null = null;
public maskFilled: IConfig['maskFilled'] = new EventEmitter<void>();
private _maskValue: string = '';
private _inputValue!: string;
private _position: number | null = null;
private _start!: number;
private _end!: number;
private _code!: string;
private _maskExpressionArray: string[] = [];
private _justPasted: boolean = false;
public constructor(
private document: any,
public _maskService: MaskService,
protected _config: IConfig,
) {}
public onChange = (_: any) => {};
public onTouch = () => {};
public ngOnChanges(changes: SimpleChanges): void {
const {
maskExpression,
specialCharacters,
patterns,
prefix,
suffix,
thousandSeparator,
decimalMarker,
dropSpecialCharacters,
hiddenInput,
showMaskTyped,
placeHolderCharacter,
shownMaskExpression,
showTemplate,
clearIfNotMatch,
validation,
separatorLimit,
allowNegativeNumbers,
leadZeroDateTime,
triggerOnMaskChange,
} = changes;
if (maskExpression) {
if (
maskExpression.currentValue !== maskExpression.previousValue &&
!maskExpression.firstChange
) {
this._maskService.maskChanged = true;
}
if (maskExpression.currentValue && maskExpression.currentValue.split('||').length > 1) {
this._maskExpressionArray = maskExpression.currentValue
.split('||')
.sort((a: string, b: string) => {
return a.length - b.length;
});
this._setMask();
} else {
this._maskExpressionArray = [];
this._maskValue = maskExpression.currentValue || '';
this._maskService.maskExpression = this._maskValue;
}
}
if (specialCharacters) {
if (!specialCharacters.currentValue || !Array.isArray(specialCharacters.currentValue)) {
return;
} else {
this._maskService.maskSpecialCharacters = specialCharacters.currentValue || [];
}
}
// Only overwrite the mask available patterns if a pattern has actually been passed in
if (patterns && patterns.currentValue) {
this._maskService.maskAvailablePatterns = patterns.currentValue;
}
if (prefix) {
this._maskService.prefix = prefix.currentValue;
}
if (suffix) {
this._maskService.suffix = suffix.currentValue;
}
if (thousandSeparator) {
this._maskService.thousandSeparator = thousandSeparator.currentValue;
}
if (decimalMarker) {
this._maskService.decimalMarker = decimalMarker.currentValue;
}
if (dropSpecialCharacters) {
this._maskService.dropSpecialCharacters = dropSpecialCharacters.currentValue;
}
if (hiddenInput) {
this._maskService.hiddenInput = hiddenInput.currentValue;
}
if (showMaskTyped) {
this._maskService.showMaskTyped = showMaskTyped.currentValue;
}
if (placeHolderCharacter) {
this._maskService.placeHolderCharacter = placeHolderCharacter.currentValue;
}
if (shownMaskExpression) {
this._maskService.shownMaskExpression = shownMaskExpression.currentValue;
}
if (showTemplate) {
this._maskService.showTemplate = showTemplate.currentValue;
}
if (clearIfNotMatch) {
this._maskService.clearIfNotMatch = clearIfNotMatch.currentValue;
}
if (validation) {
this._maskService.validation = validation.currentValue;
}
if (separatorLimit) {
this._maskService.separatorLimit = separatorLimit.currentValue;
}
if (allowNegativeNumbers) {
this._maskService.allowNegativeNumbers = allowNegativeNumbers.currentValue;
if (this._maskService.allowNegativeNumbers) {
this._maskService.maskSpecialCharacters = this._maskService.maskSpecialCharacters.filter(
(c: string) => c !== '-',
);
}
}
if (leadZeroDateTime) {
this._maskService.leadZeroDateTime = leadZeroDateTime.currentValue;
}
if (triggerOnMaskChange) {
this._maskService.triggerOnMaskChange = triggerOnMaskChange.currentValue;
}
this._applyMask();
}
// eslint-disable-next-line complexity
public validate({ value }: FormControl): ValidationErrors | null {
if (!this._maskService.validation || !this._maskValue) {
return null;
}
if (this._maskService.ipError) {
return this._createValidationError(value);
}
if (this._maskService.cpfCnpjError) {
return this._createValidationError(value);
}
if (this._maskValue.startsWith('separator')) {
return null;
}
if (withoutValidation.includes(this._maskValue)) {
return null;
}
if (this._maskService.clearIfNotMatch) {
return null;
}
if (timeMasks.includes(this._maskValue)) {
return this._validateTime(value);
}
if (value && value.toString().length >= 1) {
let counterOfOpt = 0;
for (const key in this._maskService.maskAvailablePatterns) {
if (this._maskService.maskAvailablePatterns[key]!.optional) {
if (this._maskValue.indexOf(key) !== this._maskValue.lastIndexOf(key)) {
const opt: string = this._maskValue
.split('')
.filter((i: string) => i === key)
.join('');
counterOfOpt += opt.length;
} else if (this._maskValue.indexOf(key) !== -1) {
counterOfOpt++;
}
if (
this._maskValue.indexOf(key) !== -1 &&
value.toString().length >= this._maskValue.indexOf(key)
) {
return null;
}
if (counterOfOpt === this._maskValue.length) {
return null;
}
}
}
if (
this._maskValue.indexOf('{') === 1 &&
value.toString().length ===
this._maskValue.length + Number(this._maskValue.split('{')[1]!.split('}')[0]) - 4
) {
return null;
}
if (this._maskValue.indexOf('*') === 1 || this._maskValue.indexOf('?') === 1) {
return null;
} else if (
(this._maskValue.indexOf('*') > 1 &&
value.toString().length < this._maskValue.indexOf('*')) ||
(this._maskValue.indexOf('?') > 1 &&
value.toString().length < this._maskValue.indexOf('?')) ||
this._maskValue.indexOf('{') === 1
) {
return this._createValidationError(value);
}
if (this._maskValue.indexOf('*') === -1 || this._maskValue.indexOf('?') === -1) {
const length: number = this._maskService.dropSpecialCharacters
? this._maskValue.length -
this._maskService.checkSpecialCharAmount(this._maskValue) -
counterOfOpt
: this._maskValue.length - counterOfOpt;
if (value.toString().length < length) {
return this._createValidationError(value);
}
}
}
if (value) {
this.maskFilled.emit();
return null;
}
return null;
}
public onPaste() {
this._justPasted = true;
}
public onModelChange(value: any): void {
// on form reset we need to update the actualValue
if ((value === '' || value === null || value === undefined) && this._maskService.actualValue) {
this._maskService.actualValue = this._maskService.getActualValue('');
}
}
public onInput(e: CustomKeyboardEvent): void {
const el: HTMLInputElement = e.target as HTMLInputElement;
this._inputValue = el.value;
this._setMask();
if (!this._maskValue) {
this.onChange(el.value);
return;
}
const position: number =
el.selectionStart === 1
? (el.selectionStart as number) + this._maskService.prefix.length
: (el.selectionStart as number);
let caretShift = 0;
let backspaceShift = false;
this._maskService.applyValueChanges(
position,
this._justPasted,
this._code === 'Backspace' || this._code === 'Delete',
(shift: number, _backspaceShift: boolean) => {
this._justPasted = false;
caretShift = shift;
backspaceShift = _backspaceShift;
},
);
// only set the selection if the element is active
if (this._getActiveElement() !== el) {
return;
}
this._position = this._position === 1 && this._inputValue.length === 1 ? null : this._position;
let positionToApply: number = this._position
? this._inputValue.length + position + caretShift
: position + (this._code === 'Backspace' && !backspaceShift ? 0 : caretShift);
if (positionToApply > this._getActualInputLength()) {
positionToApply = this._getActualInputLength();
}
if (positionToApply < 0) {
positionToApply = 0;
}
el.setSelectionRange(positionToApply, positionToApply);
this._position = null;
}
public onBlur(): void {
if (this._maskValue) {
this._maskService.clearIfNotMatchFn();
}
this.onTouch();
}
public onClick(e: MouseEvent | CustomKeyboardEvent): void {
if (!this._maskValue) {
return;
}
const el: HTMLInputElement = e.target as HTMLInputElement;
const posStart = 0;
const posEnd = 0;
if (
el !== null &&
el.selectionStart !== null &&
el.selectionStart === el.selectionEnd &&
el.selectionStart > this._maskService.prefix.length &&
// eslint-disable-next-line
(e as any).keyCode !== 38
) {
if (this._maskService.showMaskTyped) {
// We are showing the mask in the input
this._maskService.maskIsShown = this._maskService.showMaskInInput();
if (
el.setSelectionRange &&
this._maskService.prefix + this._maskService.maskIsShown === el.value
) {
// the input ONLY contains the mask, so position the cursor at the start
el.focus();
el.setSelectionRange(posStart, posEnd);
} else {
// the input contains some characters already
if (el.selectionStart > this._maskService.actualValue.length) {
// if the user clicked beyond our value's length, position the cursor at the end of our value
el.setSelectionRange(
this._maskService.actualValue.length,
this._maskService.actualValue.length,
);
}
}
}
}
const nextValue: string | null =
!el.value || el.value === this._maskService.prefix
? this._maskService.prefix + this._maskService.maskIsShown
: el.value;
/** Fix of cursor position jumping to end in most browsers no matter where cursor is inserted onFocus */
if (el.value !== nextValue) {
el.value = nextValue;
}
/** fix of cursor position with prefix when mouse click occur */
if (
((el.selectionStart as number) || (el.selectionEnd as number)) <=
this._maskService.prefix.length
) {
el.selectionStart = this._maskService.prefix.length;
return;
}
/** select only inserted text */
if ((el.selectionEnd as number) > this._getActualInputLength()) {
el.selectionEnd = this._getActualInputLength();
}
}
// eslint-disable-next-line complexity
public onKeyDown(e: CustomKeyboardEvent): void {
if (!this._maskValue) {
return;
}
this._code = e.code ? e.code : e.key;
const el: HTMLInputElement = e.target as HTMLInputElement;
this._inputValue = el.value;
this._setMask();
if (e.keyCode === 38) {
e.preventDefault();
}
if (e.keyCode === 37 || e.keyCode === 8 || e.keyCode === 46) {
if (e.keyCode === 8 && el.value.length === 0) {
el.selectionStart = el.selectionEnd;
}
if (e.keyCode === 8 && (el.selectionStart as number) !== 0) {
// If specialChars is false, (shouldn't ever happen) then set to the defaults
this.specialCharacters = this.specialCharacters?.length
? this.specialCharacters
: this._config.specialCharacters;
if (this.prefix.length > 1 && (el.selectionStart as number) <= this.prefix.length) {
el.setSelectionRange(this.prefix.length, el.selectionEnd);
} else {
if (
this._inputValue.length !== (el.selectionStart as number) &&
(el.selectionStart as number) !== 1
) {
while (
this.specialCharacters.includes(
this._inputValue[(el.selectionStart as number) - 1]!.toString(),
) &&
((this.prefix.length >= 1 && (el.selectionStart as number) > this.prefix.length) ||
this.prefix.length === 0)
) {
el.setSelectionRange((el.selectionStart as number) - 1, el.selectionEnd);
}
}
}
}
this.checkSelectionOnDeletion(el);
if (
this._maskService.prefix.length &&
(el.selectionStart as number) <= this._maskService.prefix.length &&
(el.selectionEnd as number) <= this._maskService.prefix.length
) {
e.preventDefault();
}
const cursorStart: number | null = el.selectionStart;
if (
e.keyCode === 8 &&
!el.readOnly &&
cursorStart === 0 &&
el.selectionEnd === el.value.length &&
el.value.length !== 0
) {
this._position = this._maskService.prefix ? this._maskService.prefix.length : 0;
this._maskService.applyMask(
this._maskService.prefix,
this._maskService.maskExpression,
this._position,
);
}
}
if (
!!this.suffix &&
this.suffix.length > 1 &&
this._inputValue.length - this.suffix.length < (el.selectionStart as number)
) {
el.setSelectionRange(this._inputValue.length - this.suffix.length, this._inputValue.length);
} else if (
(e.keyCode === 65 && e.ctrlKey) ||
(e.keyCode === 65 && e.metaKey) // Cmd + A (Mac)
) {
el.setSelectionRange(0, this._getActualInputLength());
e.preventDefault();
}
this._maskService.selStart = el.selectionStart;
this._maskService.selEnd = el.selectionEnd;
}
/** It writes the value in the input */
public async writeValue(
inputValue: string | number | { value: string | number; disable?: boolean },
): Promise<void> {
if (typeof inputValue === 'object' && inputValue !== null && 'value' in inputValue) {
if ('disable' in inputValue) {
this.setDisabledState(Boolean(inputValue.disable));
}
// eslint-disable-next-line no-param-reassign
inputValue = inputValue.value;
}
if (typeof inputValue === 'number' || this._maskValue.startsWith('separator')) {
// eslint-disable-next-line no-param-reassign
inputValue = this._maskService.numberToString(inputValue);
if (!Array.isArray(this.decimalMarker)) {
// eslint-disable-next-line no-param-reassign
inputValue =
this.decimalMarker !== '.' ? inputValue.replace('.', this.decimalMarker) : inputValue;
}
this._maskService.isNumberValue = true;
}
if (typeof inputValue !== 'string') {
// eslint-disable-next-line no-param-reassign
inputValue = '';
}
this._inputValue = inputValue;
this._setMask();
if (
(inputValue && this._maskService.maskExpression) ||
(this._maskService.maskExpression &&
(this._maskService.prefix || this._maskService.showMaskTyped))
) {
// Let the service we know we are writing value so that triggering onChange function wont happen during applyMask
this._maskService.writingValue = true;
this._maskService.formElementProperty = [
'value',
this._maskService.applyMask(inputValue, this._maskService.maskExpression),
];
// Let the service know we've finished writing value
this._maskService.writingValue = false;
} else {
this._maskService.formElementProperty = ['value', inputValue];
}
this._inputValue = inputValue;
}
public registerOnChange(fn: any): void {
this._maskService.onChange = this.onChange = fn;
}
public registerOnTouched(fn: any): void {
this.onTouch = fn;
}
private _getActiveElement(document: DocumentOrShadowRoot = this.document): Element | null {
const shadowRootEl = document?.activeElement?.shadowRoot;
if (!shadowRootEl?.activeElement) {
return document.activeElement;
} else {
return this._getActiveElement(shadowRootEl);
}
}
public checkSelectionOnDeletion(el: HTMLInputElement): void {
el.selectionStart = Math.min(
Math.max(this.prefix.length, el.selectionStart as number),
this._inputValue.length - this.suffix.length,
);
el.selectionEnd = Math.min(
Math.max(this.prefix.length, el.selectionEnd as number),
this._inputValue.length - this.suffix.length,
);
}
/** It disables the input element */
public setDisabledState(isDisabled: boolean): void {
this._maskService.formElementProperty = ['disabled', isDisabled];
}
private _repeatPatternSymbols(maskExp: string): string {
return (
(maskExp.match(/{[0-9]+}/) &&
maskExp.split('').reduce((accum: string, currval: string, index: number): string => {
this._start = currval === '{' ? index : this._start;
if (currval !== '}') {
return this._maskService._findSpecialChar(currval) ? accum + currval : accum;
}
this._end = index;
const repeatNumber: number = Number(maskExp.slice(this._start + 1, this._end));
const replaceWith: string = new Array(repeatNumber + 1).join(maskExp[this._start - 1]);
return accum + replaceWith;
}, '')) ||
maskExp
);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private _applyMask(): any {
this._maskService.maskExpression = this._repeatPatternSymbols(this._maskValue || '');
this._maskService.formElementProperty = [
'value',
this._maskService.applyMask(this._inputValue, this._maskService.maskExpression),
];
}
private _validateTime(value: string): ValidationErrors | null {
const rowMaskLen: number = this._maskValue.split('').filter((s: string) => s !== ':').length;
if (!value) {
return null; // Don't validate empty values to allow for optional form control
}
if (
(+value[value.length - 1]! === 0 && value.length < rowMaskLen) ||
value.length <= rowMaskLen - 2
) {
return this._createValidationError(value);
}
return null;
}
private _getActualInputLength() {
return (
this._maskService.actualValue.length ||
this._maskService.actualValue.length + this._maskService.prefix.length
);
}
private _createValidationError(actualValue: string): ValidationErrors {
return {
mask: {
requiredMask: this._maskValue,
actualValue,
},
};
}
private _setMask() {
if (this._maskExpressionArray.length > 0) {
this._maskExpressionArray.some((mask): boolean | void => {
const test =
this._maskService.removeMask(this._inputValue)?.length <=
this._maskService.removeMask(mask)?.length;
if (this._inputValue && test) {
this._maskValue = mask;
this.maskExpression = mask;
this._maskService.maskExpression = mask;
return test;
} else {
this._maskValue = this._maskExpressionArray[this._maskExpressionArray.length - 1]!;
this.maskExpression = this._maskExpressionArray[this._maskExpressionArray.length - 1]!;
this._maskService.maskExpression =
this._maskExpressionArray[this._maskExpressionArray.length - 1]!;
}
});
}
}
}