angular-forms-input-masks
Version:
Mask your @angular/forms formControl inputs with these directives!
355 lines (345 loc) • 14.6 kB
JavaScript
import { Directive, Optional, Self, ElementRef, Input, NgModule } from '@angular/core';
import { NgControl, Validators } from '@angular/forms';
import { Subject } from 'rxjs';
import { startWith, tap, filter, map, takeUntil } from 'rxjs/operators';
function maskedNumericValueFor(value = '', thousandSeparator = ' ', decimalSeparator = '.', prefix = '', digitsAfterSeparator = 2, maxDigits = 12, allowNegatives = true) {
var _a;
const isNegative = allowNegatives && ((_a = value.toString().match(/-/g)) === null || _a === void 0 ? void 0 : _a.length) === 1;
let baseValue = unmaskedNumericValueFor(value, true);
baseValue = (baseValue.length >= 1 && String(parseInt(baseValue, 10))) || '000';
const integerLength = baseValue.length - digitsAfterSeparator;
const cents = baseValue
.substr((integerLength > 0 && integerLength) || 0)
.padStart(digitsAfterSeparator, '0');
let integerValue = baseValue
.substring(0, baseValue.length - digitsAfterSeparator > maxDigits
? maxDigits
: baseValue.length - digitsAfterSeparator)
.padStart(1, '0');
if (thousandSeparator)
integerValue = integerValue.replace(/\B(?=(\d{3})+(?!\d))/g, thousandSeparator);
return `${prefix ? `${prefix} ` : ''}${isNegative ? '-' : ''}${integerValue}${decimalSeparator}${cents}`;
}
function unmaskedNumericValueFor(value, removeNegative = false) {
return removeNegative
? value.toString().replace(/[^0-9]+/g, '')
: value.toString().replace(/[^0-9-]+/g, '');
}
function unmaskedValueFor(value) {
return value.toString().replace(/[^\w]+/g, '');
;
}
function hasNonDecimalCharacters(value) {
return !/^-?\d+$/.test(value.toString());
}
function matchAndReplaceFor(text, pattern) {
let patternOffset = 0;
const testPositionFunc = (prevText, cur, i) => {
switch (pattern[i + patternOffset]) {
case 'D':
if (/[\d]/.test(cur))
return `${prevText}${cur}`;
break;
case 'C':
if (/[\A-Z, a-z]/.test(cur))
return `${prevText}${cur}`;
break;
case 'W':
if (/[\w]/.test(cur))
return `${prevText}${cur}`;
break;
default: {
if (/[^\w]/.test(pattern[i + patternOffset])) {
patternOffset++;
return testPositionFunc(`${prevText}${pattern[i + patternOffset - 1]}`, cur, i);
}
}
}
patternOffset--;
return `${prevText}`;
};
return text.split('').reduce(testPositionFunc, '');
}
function cursorPositionFor(el) {
return (el.nativeElement || el).selectionStart;
}
/**
* Set cursor position at a given `nativeEl` from a `ElementRef` or its wrapper.
*/
function setCursorPositionFor(el, nextPos) {
const nativeEl = el.nativeElement || el;
nativeEl.selectionStart = nativeEl.selectionEnd = nextPos;
}
/** Adjusts cursorPosition for input element. Skips non decimal/letter chars.
* @param addingAtLeft cursor will it keep its position.
* @param decimalsOnly if should takes into consideration both decimals and letters for determining cursor position */
function nextCursorPositionFor(el, previousValue, nextValue, addingAtLeft = false, decimalsOnly = false, removingAtLeft = false) {
const initialCursorPosition = cursorPositionFor(el);
const maskCheck = decimalsOnly ? /[^\d]/ : /[^\w]/;
const isAdding = removingAtLeft || !addingAtLeft
? nextValue.length > (previousValue === null || previousValue === void 0 ? void 0 : previousValue.length)
: nextValue.length >= (previousValue === null || previousValue === void 0 ? void 0 : previousValue.length);
let nextCursorPosition = initialCursorPosition;
if (addingAtLeft && previousValue) {
if (isAdding) {
nextCursorPosition += nextValue.length - previousValue.length - 1;
}
else if (previousValue.length > nextValue.length) {
nextCursorPosition += nextValue.length - previousValue.length + 1;
}
else {
nextCursorPosition += (unmaskedNumericValueFor(previousValue) > unmaskedNumericValueFor(nextValue) ? 1 : 0);
}
}
let testPosition = nextCursorPosition - 1;
while (maskCheck.test(nextValue[testPosition])) {
if (isAdding) {
testPosition++;
nextCursorPosition++;
}
else {
testPosition--;
nextCursorPosition--;
}
if (testPosition < 0) {
nextCursorPosition = initialCursorPosition + 1;
break;
}
}
return nextCursorPosition;
}
function maskFormatValidator(masks) {
return (control) => {
if (!control.value) {
return null;
}
if (Array.isArray(masks)) {
if (!masks.find((mask) => mask.length === control.value.length)) {
return { invalidLength: true };
}
}
else {
if (control.value.length !== masks.length) {
return { invalidLength: true };
}
}
return null;
};
}
class AngularFormsCurrencyMaskDirective {
constructor(selfNgControl, elRef) {
this.selfNgControl = selfNgControl;
this.elRef = elRef;
this.prefix = '$';
this.thousandsSeparator = ' ';
this.decimalSeparator = '.';
this.digitsAfterSeparator = 2;
this.maxIntegerDigits = 8;
this.allowNegatives = false;
/** For when the form already has a initial value or is expected to boot as `zero *masked*`. */
this.validateOnInit = true;
this.valueHasChanged = false;
this.directiveExists$ = new Subject();
}
ngAfterViewInit() {
var _a, _b;
this.control = (_b = (_a = this.selfNgControl) === null || _a === void 0 ? void 0 : _a.control) !== null && _b !== void 0 ? _b : this.ngControl;
if (!this.control) {
console.warn('AngularFormsCurrencyMaskDirective: A FormControl value is required for the directive to be initiated.');
return;
}
this.nativeEl = this.elRef.nativeElement.hasChildNodes()
? this.elRef.nativeElement.getElementsByTagName('input')[0]
: this.elRef.nativeElement;
if (!this.nativeEl) {
console.warn('AngularFormsCurrencyMaskDirective: A elRef of type input is required for the directive to be initiated.');
return;
}
const boot = this.validateOnInit
? startWith(this.control.value)
: tap(() => { });
this.control.valueChanges
.pipe(boot, filter((value) => {
const lastValueWasChanged = this.valueHasChanged;
this.valueHasChanged = false;
return !this.previousValue ||
unmaskedNumericValueFor(value) !== unmaskedNumericValueFor(this.previousValue) ||
hasNonDecimalCharacters(value) ||
!lastValueWasChanged;
}), map((value) => { var _a; return (_a = value === null || value === void 0 ? void 0 : value.toString()) !== null && _a !== void 0 ? _a : ''; }), takeUntil(this.directiveExists$))
.subscribe((value) => {
var _a;
this.adjustCursorIfSeparator(value);
this.setValue(this.maskedValueFor(value), value.length < ((_a = this.previousValue) === null || _a === void 0 ? void 0 : _a.length));
});
}
ngOnDestroy() {
this.directiveExists$.next();
this.directiveExists$.unsubscribe();
}
setValue(nextValue, removing = false) {
let nextCursorPosition = cursorPositionFor(this.nativeEl);
if (nextValue) {
nextCursorPosition = nextCursorPosition <= this.prefix.length + 1
? nextValue.length
: nextCursorPositionFor(this.nativeEl, this.previousValue, nextValue, true, true, removing);
}
const wasInitialValue = this.valueHasChanged;
this.valueHasChanged = !!this.previousValue;
this.previousValue = nextValue;
this.control.setValue(nextValue, { emitEvent: false });
this.control.setValue(unmaskedNumericValueFor(nextValue), {
emitEvent: true,
emitModelToViewChange: false,
});
if (wasInitialValue) {
nextCursorPosition = nextValue.length + 1;
}
setCursorPositionFor(this.nativeEl, nextCursorPosition);
}
maskedValueFor(value) {
return maskedNumericValueFor(value, this.thousandsSeparator, this.decimalSeparator, this.prefix, this.digitsAfterSeparator, this.maxIntegerDigits, this.allowNegatives);
}
adjustCursorIfSeparator(value) {
const decimalSeparatorPressed = value.indexOf(this.decimalSeparator) !== value.lastIndexOf(this.decimalSeparator);
if (decimalSeparatorPressed) {
const curPos = cursorPositionFor(this.elRef);
const nextPos = curPos - 1 <= value.indexOf(this.decimalSeparator)
? value.length
: value.indexOf(this.decimalSeparator) + 1;
setCursorPositionFor(this.elRef, nextPos);
}
}
}
AngularFormsCurrencyMaskDirective.decorators = [
{ type: Directive, args: [{
selector: '[angularFormsCurrency]',
},] }
];
AngularFormsCurrencyMaskDirective.ctorParameters = () => [
{ type: NgControl, decorators: [{ type: Optional }, { type: Self }] },
{ type: ElementRef }
];
AngularFormsCurrencyMaskDirective.propDecorators = {
ngControl: [{ type: Input }],
prefix: [{ type: Input }],
thousandsSeparator: [{ type: Input }],
decimalSeparator: [{ type: Input }],
digitsAfterSeparator: [{ type: Input }],
maxIntegerDigits: [{ type: Input }],
allowNegatives: [{ type: Input }],
validateOnInit: [{ type: Input }]
};
class AngularFormsMaskDirective {
constructor(selfNgControl, elRef) {
this.selfNgControl = selfNgControl;
this.elRef = elRef;
/** Add validation so that the input should match the length of the mask, else returns `invalidLength` validation error at the `ngControl`. */
this.validateMaskInput = false;
/** Set clear values to the formControl */
this.unmasked = false;
this.directiveExists$ = new Subject();
}
/**
* Mask formats, accepts a single or multiple, matching by order. e.g "DDD-WWW.CCC"
*
* D: numbers; C: letters; W: both; All other characters are treated as part of the mask just displayed. */
set angularFormsMask(value) {
this.mask = Array.isArray(value)
? [...value].sort((a, b) => a.length - a.length)
: value;
}
ngOnInit() {
var _a, _b;
if (!this.mask) {
console.warn('AngularFormsMaskDirective: A Mask value is required for the directive to be initiated.');
return;
}
this.control = (_b = (_a = this.selfNgControl) === null || _a === void 0 ? void 0 : _a.control) !== null && _b !== void 0 ? _b : this.ngControl;
if (!this.control) {
console.warn('AngularFormsMaskDirective: A FormControl value is required for the directive to be initiated.');
return;
}
this.nativeEl = this.elRef.nativeElement.hasChildNodes()
? this.elRef.nativeElement.getElementsByTagName('input')[0]
: this.elRef.nativeElement;
if (!this.nativeEl) {
console.warn('AngularFormsMaskDirective: A elRef of type input is required for the directive to be initiated.');
return;
}
if (this.validateMaskInput) {
this.control.setValidators([
Validators.required,
maskFormatValidator(this.mask),
]);
}
this.control.valueChanges
.pipe(startWith(this.control.value), takeUntil(this.directiveExists$))
.subscribe((value) => this.setValue(this.maskValueFor(value)));
}
ngOnDestroy() {
this.directiveExists$.next();
this.directiveExists$.unsubscribe();
}
setValue(nextValue) {
const nextCursorPosition = nextValue
? nextCursorPositionFor(this.nativeEl, this.previousValue, nextValue)
: cursorPositionFor(this.nativeEl);
this.previousValue = nextValue;
this.control.setValue(nextValue, { emitEvent: false });
if (this.unmasked && nextValue) {
this.control.setValue(unmaskedValueFor(nextValue), {
emitEvent: false,
emitModelToViewChange: false,
});
}
setCursorPositionFor(this.nativeEl, nextCursorPosition);
}
maskValueFor(value) {
if (!value)
return;
const unmaskedValue = unmaskedValueFor(value);
const nextMask = !Array.isArray(this.mask)
? this.mask
: this.mask.find((mask) => unmaskedValueFor(mask).length >= unmaskedValue.length) || this.mask[this.mask.length - 1];
return matchAndReplaceFor(unmaskedValue, nextMask);
}
}
AngularFormsMaskDirective.decorators = [
{ type: Directive, args: [{
selector: '[angularFormsMask]',
},] }
];
AngularFormsMaskDirective.ctorParameters = () => [
{ type: NgControl, decorators: [{ type: Optional }, { type: Self }] },
{ type: ElementRef }
];
AngularFormsMaskDirective.propDecorators = {
ngControl: [{ type: Input }],
angularFormsMask: [{ type: Input }],
validateMaskInput: [{ type: Input }],
unmasked: [{ type: Input }]
};
class AngularFormsInputMasksModule {
}
AngularFormsInputMasksModule.decorators = [
{ type: NgModule, args: [{
declarations: [
AngularFormsCurrencyMaskDirective,
AngularFormsMaskDirective,
],
imports: [],
exports: [
AngularFormsCurrencyMaskDirective,
AngularFormsMaskDirective,
],
},] }
];
/*
* Public API Surface of angular-forms-input-masks
*/
/**
* Generated bundle index. Do not edit.
*/
export { AngularFormsCurrencyMaskDirective, AngularFormsInputMasksModule, AngularFormsMaskDirective, cursorPositionFor, hasNonDecimalCharacters, maskFormatValidator, maskedNumericValueFor, matchAndReplaceFor, nextCursorPositionFor, setCursorPositionFor, unmaskedNumericValueFor, unmaskedValueFor };
//# sourceMappingURL=angular-forms-input-masks.js.map