UNPKG

@uiowa/digit-only

Version:

This package includes two Angular directives. The digitOnly directive only allows numbers in the input box when typing, pasting or drag/dropping. The mask directive checks the input pattern attribute.

373 lines (366 loc) 16.1 kB
import * as i0 from '@angular/core'; import { Directive, Input, HostListener, NgModule } from '@angular/core'; class DigitOnlyDirective { constructor(el) { this.el = el; this.hasDecimalPoint = false; this.hasNegativeSign = false; this.navigationKeys = [ 'Backspace', 'Delete', 'Tab', 'Escape', 'Enter', 'Home', 'End', 'ArrowLeft', 'ArrowRight', 'Clear', 'Copy', 'Paste', ]; this.decimal = false; this.decimalSeparator = '.'; this.allowNegatives = false; this.allowPaste = true; this.negativeSign = '-'; this.min = -Infinity; this.max = Infinity; this.regex = null; this.inputElement = el.nativeElement; } ngOnChanges(changes) { if (changes['pattern']) { this.regex = this.pattern ? RegExp(this.pattern) : null; } if (changes['min']) { const maybeMin = Number(this.min); this.min = isNaN(maybeMin) ? -Infinity : maybeMin; } if (changes['max']) { const maybeMax = Number(this.max); this.max = isNaN(maybeMax) ? Infinity : maybeMax; } } onBeforeInput(e) { if (isNaN(Number(e.data))) { if (e.data === this.decimalSeparator || (e.data === this.negativeSign && this.allowNegatives)) { return; // go on } e.preventDefault(); e.stopPropagation(); } } onKeyDown(e) { if (this.navigationKeys.indexOf(e.key) > -1 || // Allow: navigation keys: backspace, delete, arrows etc. ((e.key === 'a' || e.code === 'KeyA') && e.ctrlKey === true) || // Allow: Ctrl+A ((e.key === 'c' || e.code === 'KeyC') && e.ctrlKey === true) || // Allow: Ctrl+C ((e.key === 'v' || e.code === 'KeyV') && e.ctrlKey === true) || // Allow: Ctrl+V ((e.key === 'x' || e.code === 'KeyX') && e.ctrlKey === true) || // Allow: Ctrl+X ((e.key === 'a' || e.code === 'KeyA') && e.metaKey === true) || // Allow: Cmd+A (Mac) ((e.key === 'c' || e.code === 'KeyC') && e.metaKey === true) || // Allow: Cmd+C (Mac) ((e.key === 'v' || e.code === 'KeyV') && e.metaKey === true) || // Allow: Cmd+V (Mac) ((e.key === 'x' || e.code === 'KeyX') && e.metaKey === true) // Allow: Cmd+X (Mac) ) { // let it happen, don't do anything return; } let newValue = ''; if (this.decimal && e.key === this.decimalSeparator) { newValue = this.forecastValue(e.key); if (newValue.split(this.decimalSeparator).length > 2) { // has two or more decimal points e.preventDefault(); return; } else { this.hasDecimalPoint = newValue.indexOf(this.decimalSeparator) > -1; return; // Allow: only one decimal point } } if (e.key === this.negativeSign && this.allowNegatives) { newValue = this.forecastValue(e.key); if (newValue.charAt(0) !== this.negativeSign || newValue.split(this.negativeSign).length > 2) { e.preventDefault(); return; } else { this.hasNegativeSign = newValue.split(this.negativeSign).length > -1; return; } } // Ensure that it is a number and stop the keypress if (e.key === ' ' || isNaN(Number(e.key))) { e.preventDefault(); return; } newValue = newValue || this.forecastValue(e.key); // check the input pattern RegExp if (this.regex) { if (!this.regex.test(newValue)) { e.preventDefault(); return; } } const newNumber = Number(newValue); if (newNumber > this.max || newNumber < this.min) { e.preventDefault(); } } onPaste(event) { if (this.allowPaste === true) { let pastedInput = ''; if (window['clipboardData']) { // Browser is IE pastedInput = window['clipboardData'].getData('text'); } else if (event.clipboardData && event.clipboardData.getData) { // Other browsers pastedInput = event.clipboardData.getData('text/plain'); } this.pasteData(pastedInput); event.preventDefault(); } else { // this prevents the paste event.preventDefault(); event.stopPropagation(); } } onDrop(event) { const textData = event.dataTransfer?.getData('text') ?? ''; this.inputElement.focus(); this.pasteData(textData); event.preventDefault(); } pasteData(pastedContent) { const sanitizedContent = this.sanitizeInput(pastedContent); if (sanitizedContent.includes(this.negativeSign) && this.hasNegativeSign && !this.getSelection().includes(this.negativeSign)) { return; } const pasted = document.execCommand('insertText', false, sanitizedContent); if (!pasted) { if (this.inputElement.setRangeText) { const { selectionStart: start, selectionEnd: end } = this.inputElement; this.inputElement.setRangeText(sanitizedContent, start ?? 0, end ?? 0, 'end'); // Angular's Reactive Form relies on "input" event, but on Firefox, the setRangeText method doesn't trigger it // so we have to trigger it ourself. if (typeof window['InstallTrigger'] !== 'undefined') { this.inputElement.dispatchEvent(new Event('input', { cancelable: true })); } } else { // Browser does not support setRangeText, e.g. IE this.insertAtCursor(this.inputElement, sanitizedContent); } } if (this.decimal) { this.hasDecimalPoint = this.inputElement.value.indexOf(this.decimalSeparator) > -1; } this.hasNegativeSign = this.inputElement.value.indexOf(this.negativeSign) > -1; } // The following 2 methods were added from the below article for browsers that do not support setRangeText // https://stackoverflow.com/questions/11076975/how-to-insert-text-into-the-textarea-at-the-current-cursor-position insertAtCursor(myField, myValue) { const startPos = myField.selectionStart ?? 0; const endPos = myField.selectionEnd ?? 0; myField.value = myField.value.substring(0, startPos) + myValue + myField.value.substring(endPos, myField.value.length); const pos = startPos + myValue.length; myField.focus(); myField.setSelectionRange(pos, pos); this.triggerEvent(myField, 'input'); } triggerEvent(el, type) { if ('createEvent' in document) { // modern browsers, IE9+ const e = document.createEvent('HTMLEvents'); e.initEvent(type, false, true); el.dispatchEvent(e); } } // end stack overflow code sanitizeInput(input) { let result = ''; let regex; if (this.decimal && this.isValidDecimal(input)) { regex = new RegExp(`${this.getNegativeSignRegExp()}[^0-9${this.decimalSeparator}]`, 'g'); } else { regex = new RegExp(`${this.getNegativeSignRegExp()}[^0-9]`, 'g'); } result = input.replace(regex, ''); const maxLength = this.inputElement.maxLength; if (maxLength > 0) { // the input element has maxLength limit const allowedLength = maxLength - this.inputElement.value.length + (result.includes(`${this.negativeSign}`) ? 1 : 0); result = allowedLength > 0 ? result.substring(0, allowedLength) : ''; } return result; } getNegativeSignRegExp() { return this.allowNegatives && (!this.hasNegativeSign || this.getSelection().includes(this.negativeSign)) ? `(?!^${this.negativeSign})` : ''; } isValidDecimal(string) { if (!this.hasDecimalPoint) { return string.split(this.decimalSeparator).length <= 2; } else { // the input element already has a decimal separator const selectedText = this.getSelection(); if (selectedText && selectedText.indexOf(this.decimalSeparator) > -1) { return string.split(this.decimalSeparator).length <= 2; } else { return string.indexOf(this.decimalSeparator) < 0; } } } getSelection() { return this.inputElement.value.substring(this.inputElement.selectionStart ?? 0, this.inputElement.selectionEnd ?? 0); } forecastValue(key) { const selectionStart = this.inputElement.selectionStart ?? 0; const selectionEnd = this.inputElement.selectionEnd ?? 0; const oldValue = this.inputElement.value; return (oldValue.substring(0, selectionStart) + key + oldValue.substring(selectionEnd)); } } DigitOnlyDirective.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "13.2.4", ngImport: i0, type: DigitOnlyDirective, deps: [{ token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Directive }); DigitOnlyDirective.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "12.0.0", version: "13.2.4", type: DigitOnlyDirective, selector: "[digitOnly]", inputs: { decimal: "decimal", decimalSeparator: "decimalSeparator", allowNegatives: "allowNegatives", allowPaste: "allowPaste", negativeSign: "negativeSign", min: "min", max: "max", pattern: "pattern" }, host: { listeners: { "beforeinput": "onBeforeInput($event)", "keydown": "onKeyDown($event)", "paste": "onPaste($event)", "drop": "onDrop($event)" } }, usesOnChanges: true, ngImport: i0 }); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.2.4", ngImport: i0, type: DigitOnlyDirective, decorators: [{ type: Directive, args: [{ selector: '[digitOnly]', }] }], ctorParameters: function () { return [{ type: i0.ElementRef }]; }, propDecorators: { decimal: [{ type: Input }], decimalSeparator: [{ type: Input }], allowNegatives: [{ type: Input }], allowPaste: [{ type: Input }], negativeSign: [{ type: Input }], min: [{ type: Input }], max: [{ type: Input }], pattern: [{ type: Input }], onBeforeInput: [{ type: HostListener, args: ['beforeinput', ['$event']] }], onKeyDown: [{ type: HostListener, args: ['keydown', ['$event']] }], onPaste: [{ type: HostListener, args: ['paste', ['$event']] }], onDrop: [{ type: HostListener, args: ['drop', ['$event']] }] } }); class MaskDirective { constructor(el) { this.el = el; this.navigationKeys = [ 'Backspace', 'Delete', 'Tab', 'Escape', 'Enter', 'Home', 'End', 'ArrowLeft', 'ArrowRight', 'Clear', 'Copy', 'Paste', ]; this.regex = new RegExp(''); this.inputElement = el.nativeElement; } ngOnInit() { this.regex = new RegExp(this.inputElement.pattern); } onKeyDown(e) { if (this.navigationKeys.indexOf(e.key) > -1 || // Allow: navigation keys: backspace, delete, arrows etc. ((e.key === 'a' || e.code === 'KeyA') && e.ctrlKey === true) || // Allow: Ctrl+A ((e.key === 'c' || e.code === 'KeyC') && e.ctrlKey === true) || // Allow: Ctrl+C ((e.key === 'v' || e.code === 'KeyV') && e.ctrlKey === true) || // Allow: Ctrl+V ((e.key === 'x' || e.code === 'KeyX') && e.ctrlKey === true) || // Allow: Ctrl+X ((e.key === 'a' || e.code === 'KeyA') && e.metaKey === true) || // Allow: Cmd+A (Mac) ((e.key === 'c' || e.code === 'KeyC') && e.metaKey === true) || // Allow: Cmd+C (Mac) ((e.key === 'v' || e.code === 'KeyV') && e.metaKey === true) || // Allow: Cmd+V (Mac) ((e.key === 'x' || e.code === 'KeyX') && e.metaKey === true) // Allow: Cmd+X (Mac) ) { // let it happen, don't do anything return; } const newValue = this.forecastValue(e.key); if (!this.regex.test(newValue)) { e.preventDefault(); } } forecastValue(key) { const selectionStart = this.inputElement.selectionStart ?? 0; const selectionEnd = this.inputElement.selectionEnd ?? 0; const oldValue = this.inputElement.value; const selection = oldValue.substring(selectionStart, selectionEnd); return selection ? oldValue.replace(selection, key) : oldValue.substring(0, selectionStart) + key + oldValue.substring(selectionStart); } } MaskDirective.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "13.2.4", ngImport: i0, type: MaskDirective, deps: [{ token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Directive }); MaskDirective.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "12.0.0", version: "13.2.4", type: MaskDirective, selector: "[mask]", host: { listeners: { "keydown": "onKeyDown($event)" } }, ngImport: i0 }); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.2.4", ngImport: i0, type: MaskDirective, decorators: [{ type: Directive, args: [{ selector: '[mask]', }] }], ctorParameters: function () { return [{ type: i0.ElementRef }]; }, propDecorators: { onKeyDown: [{ type: HostListener, args: ['keydown', ['$event']] }] } }); class DigitOnlyModule { } DigitOnlyModule.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "13.2.4", ngImport: i0, type: DigitOnlyModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); DigitOnlyModule.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "12.0.0", version: "13.2.4", ngImport: i0, type: DigitOnlyModule, declarations: [DigitOnlyDirective, MaskDirective], exports: [DigitOnlyDirective, MaskDirective] }); DigitOnlyModule.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "13.2.4", ngImport: i0, type: DigitOnlyModule, imports: [[]] }); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.2.4", ngImport: i0, type: DigitOnlyModule, decorators: [{ type: NgModule, args: [{ imports: [], declarations: [DigitOnlyDirective, MaskDirective], exports: [DigitOnlyDirective, MaskDirective], }] }] }); /* * Public API Surface of digit-only */ /** * Generated bundle index. Do not edit. */ export { DigitOnlyDirective, DigitOnlyModule, MaskDirective }; //# sourceMappingURL=uiowa-digit-only.mjs.map