import { Directive, ElementRef, HostListener, Input, Optional, AfterViewInit, } from '@angular/core'; import { NgControl } from '@angular/forms'; @Directive({ selector: '[mwNumeric]', }) export class MwNumericDirective implements AfterViewInit { @Input() decimals = 0; constructor( private readonly el: ElementRef, @Optional() private ngControl: NgControl ) {} @HostListener('keydown', ['$event']) onKeyDown(event: KeyboardEvent): void { const key = event.key; const isComma = ['.', ','].indexOf(key) !== -1; const isMinus = ['-'].indexOf(key) !== -1; if (this.isSystemKey(event)) { return; } const isAllowComma = !isComma || this.isAllowComma(key); const isValidKey = isComma || isMinus || this.isValid(key); const isAllowDecimal = this.isAllowDecimal(); const isAllowMinus = !isMinus || this.isAllowMinus(); if (!isAllowComma || !isValidKey || !isAllowDecimal || !isAllowMinus) { event.preventDefault(); } } @HostListener('paste', ['$event']) onPaste(event: ClipboardEvent): void { const data = event.clipboardData.getData('text'); if (!this.isParsableToFloat(data)) { event.preventDefault(); } } @HostListener('input', ['$event']) onEvent(event: any): void { const inputFieldValue = this.el.nativeElement.value ? this.replaceComma(this.el.nativeElement.value) : null; this.el.nativeElement.value = inputFieldValue; this.ngControl?.control.setValue(inputFieldValue); } ngAfterViewInit(): void { this.el.nativeElement.autocomplete = "off"; } private isParsableToFloat(data: string): boolean { const parsedData = parseFloat(data); return !isNaN(parsedData) && parsedData.toString().length === data.trim().length; } private isAllowComma(value: string): boolean { if (this.decimals === 0) { return false; } const currentValueHasComma = this.isCurrentValueHasComma(); const commasCount = this.countCommas(value); return !currentValueHasComma && commasCount === 1; } private isSystemKey(event: KeyboardEvent): boolean { const isPaste = event.key === 'v' && event.ctrlKey; const isCopy = event.key === 'c' && event.ctrlKey; const isCut = event.key === 'x' && event.ctrlKey; const isSpecial = ['Backspace', 'ArrowLeft', 'ArrowRight', 'Home', 'End', 'Delete'].indexOf( event.key ) !== -1; const isSelectAll = event.key === 'a' && event.ctrlKey; return isPaste || isCopy || isSelectAll || isSpecial || isCut; } private isCurrentValueHasComma(): boolean { const value: string = this.el.nativeElement.value; return this.countCommas(value) > 0; } private isAllowDecimal(): boolean { const selectionStart: number = this.el.nativeElement.selectionStart; const value: string = this.el.nativeElement.value; const valueTillSelectionStart = value.substr(0, selectionStart); const doCheck = valueTillSelectionStart.indexOf('.') !== -1; return !doCheck || value?.split('.')[1]?.length <= this.decimals; } private isAllowMinus(): boolean { const selectionStart: number = this.el.nativeElement.selectionStart; const value: string = this.el.nativeElement.value; return selectionStart === 0 && value[1] !== '-'; } private countCommas(value: string): number { return value.match(/[\.\,]/g)?.length || 0; } private isValid(value: string): boolean { if (this.decimals <= 0) { return String(value).match(new RegExp(/^\d+$/))?.length > 0; } else { const regExpString = '^\\s*((\\d+([\\.,]\\d{0,' + this.decimals + '})?)|((\\d*([\\.,]\\d{1,' + this.decimals + '}))))\\s*$'; return String(value).match(new RegExp(regExpString))?.length > 0; } } private replaceComma(value: any): any { if ( String(value).length === 1 && (String(value).endsWith(',') || String(value).endsWith('.')) ) { value = '0.'; return value; } return String(value).replace(',', '.'); } }