UNPKG

angular-forms-input-masks

Version:

Mask your @angular/forms formControl inputs with these directives!

355 lines (345 loc) 14.6 kB
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