import { Directive , ElementRef , HostListener , Input , OnChanges , SimpleChanges , } from '@angular/core' ; @ Directive ( { selector : '[digitOnly]' , } ) export class DigitOnlyDirective implements OnChanges { private hasDecimalPoint = false ; private hasNegativeSign = false ; private navigationKeys = [ 'Backspace' , 'Delete' , 'Tab' , 'Escape' , 'Enter' , 'Home' , 'End' , 'ArrowLeft' , 'ArrowRight' , 'Clear' , 'Copy' , 'Paste' , ] ; @ Input ( ) decimal = false ; @ Input ( ) decimalSeparator = '.' ; @ Input ( ) allowNegatives = false ; @ Input ( ) allowPaste = true ; @ Input ( ) negativeSign = '-' ; @ Input ( ) min = - Infinity ; @ Input ( ) max = Infinity ; @ Input ( ) pattern ? : string | RegExp ; private regex : RegExp | null = null ; inputElement : HTMLInputElement ; constructor ( public el ) { 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 ; } } @ HostListener ( 'beforeinput' , [ '$event' ] ) onBeforeInput ( e ) { if ( isNaN ( Number ( e . data ) ) ) { if ( e . data === this . decimalSeparator || ( e . data === this . negativeSign && this . allowNegatives ) ) { return ; } e . preventDefault ( ) ; e . stopPropagation ( ) ; } } @ HostListener ( 'keydown' , [ '$event' ] ) onKeyDown ( e ) { if ( this . navigationKeys . indexOf ( e . key ) > - 1 || ( ( e . key === 'a' || e . code === 'KeyA' ) && e . ctrlKey === true ) || ( ( e . key === 'c' || e . code === 'KeyC' ) && e . ctrlKey === true ) || ( ( e . key === 'v' || e . code === 'KeyV' ) && e . ctrlKey === true ) || ( ( e . key === 'x' || e . code === 'KeyX' ) && e . ctrlKey === true ) || ( ( e . key === 'a' || e . code === 'KeyA' ) && e . metaKey === true ) || ( ( e . key === 'c' || e . code === 'KeyC' ) && e . metaKey === true ) || ( ( e . key === 'v' || e . code === 'KeyV' ) && e . metaKey === true ) || ( ( e . key === 'x' || e . code === 'KeyX' ) && e . metaKey === true ) ) { return ; } let newValue = '' ; if ( this . decimal && e . key === this . decimalSeparator ) { newValue = this . forecastValue ( e . key ) ; if ( newValue . split ( this . decimalSeparator ) . length > 2 ) { e . preventDefault ( ) ; return ; } else { this . hasDecimalPoint = newValue . indexOf ( this . decimalSeparator ) > - 1 ; return ; } } 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 ; } } if ( e . key === ' ' || isNaN ( Number ( e . key ) ) ) { e . preventDefault ( ) ; return ; } newValue = newValue || this . forecastValue ( e . key ) ; if ( this . regex ) { if ( ! this . regex . test ( newValue ) ) { e . preventDefault ( ) ; return ; } } const newNumber = Number ( newValue ) ; if ( newNumber > this . max || newNumber < this . min ) { e . preventDefault ( ) ; } } @ HostListener ( 'paste' , [ '$event' ] ) onPaste ( event ) { if ( this . allowPaste === true ) { let pastedInput = '' ; if ( ( window as { [ key ] : any ; } ) [ 'clipboardData' ] ) { pastedInput = ( window as { [ key ] : any ; } ) [ 'clipboardData' ] . getData ( 'text' ) ; } else if ( event . clipboardData && event . clipboardData . getData ) { pastedInput = event . clipboardData . getData ( 'text/plain' ) ; } this . pasteData ( pastedInput ) ; event . preventDefault ( ) ; } else { event . preventDefault ( ) ; event . stopPropagation ( ) ; } } @ HostListener ( 'drop' , [ '$event' ] ) onDrop ( event ) { const textData = event . dataTransfer ?. getData ( 'text' ) ?? '' ; this . inputElement . focus ( ) ; this . pasteData ( textData ) ; event . preventDefault ( ) ; } private 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' ) ; if ( typeof ( window as { [ key ] : any ; } ) [ 'InstallTrigger' ] !== 'undefined' ) { this . inputElement . dispatchEvent ( new Event ( 'input' , { cancelable : true } ) ) ; } } else { 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 ; } private 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' ) ; } private triggerEvent ( el , type ) { if ( 'createEvent' in document ) { const e = document . createEvent ( 'HTMLEvents' ) ; e . initEvent ( type , false , true ) ; el . dispatchEvent ( e ) ; } } private 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 ) { const allowedLength = maxLength - this . inputElement . value . length + ( result . includes ( `${ this . negativeSign }` ) ? 1 : 0 ) ; result = allowedLength > 0 ? result . substring ( 0 , allowedLength ) : '' ; } return result ; } private getNegativeSignRegExp ( ) { return this . allowNegatives && ( ! this . hasNegativeSign || this . getSelection ( ) . includes ( this . negativeSign ) ) ? `(?!^${ this . negativeSign })` : '' ; } private isValidDecimal ( string ) { if ( ! this . hasDecimalPoint ) { return string . split ( this . decimalSeparator ) . length <= 2 ; } else { 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 ; } } } private getSelection ( ) { return this . inputElement . value . substring ( this . inputElement . selectionStart ?? 0 , this . inputElement . selectionEnd ?? 0 ) ; } private 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 ) ) ; } }