license import { coerceBooleanProperty } from '@angular/cdk/coercion' ; import { Directive , ElementRef , Input , AfterViewInit , DoCheck , OnDestroy , NgZone , } from '@angular/core' ; import { Platform } from '@angular/cdk/platform' ; import { auditTime , takeUntil } from 'rxjs/operators' ; import { fromEvent , Subject } from 'rxjs' ; /** Directive to automatically resize a textarea to fit its content. */ @ Directive ( { selector : 'textarea[cdkTextareaAutosize]' , exportAs : 'cdkTextareaAutosize' , host : { 'class' : 'cdk-textarea-autosize' , 'rows' : '1' , '(input)' : '_noopInputHandler()' , } , } ) export class CdkTextareaAutosize implements AfterViewInit , DoCheck , OnDestroy { /** Keep track of the previous textarea value to avoid resizing when the value hasn't changed. */ private _previousValue ? : string ; private _initialHeight : string | null ; private readonly _destroyed = new Subject < void > ( ) ; private _minRows : number ; private _maxRows : number ; private _enabled : boolean = true ; /**
     * Value of minRows as of last resize. If the minRows has decreased, the
     * height of the textarea needs to be recomputed to reflect the new minimum. The maxHeight
     * does not have the same problem because it does not affect the textarea's scrollHeight.
     */ private _previousMinRows : number = - 1 ; private _textareaElement : HTMLTextAreaElement ; /** Minimum amount of rows in the textarea. */ @ Input ( 'cdkAutosizeMinRows' ) get minRows ( ) : number { return this . _minRows ; } set minRows ( value ) { this . _minRows = value ; this . _setMinHeight ( ) ; } /** Maximum amount of rows in the textarea. */ @ Input ( 'cdkAutosizeMaxRows' ) get maxRows ( ) : number { return this . _maxRows ; } set maxRows ( value ) { this . _maxRows = value ; this . _setMaxHeight ( ) ; } /** Whether autosizing is enabled or not */ @ Input ( 'cdkTextareaAutosize' ) get enabled ( ) : boolean { return this . _enabled ; } set enabled ( value ) { value = coerceBooleanProperty ( value ) ; if ( this . _enabled !== value ) { ( this . _enabled = value ) ? this . resizeToFitContent ( true ) : this . reset ( ) ; } } /** Cached height of a textarea with a single row. */ private _cachedLineHeight : number ; constructor ( private _elementRef , private _platform , private _ngZone ) { this . _textareaElement = this . _elementRef . nativeElement as HTMLTextAreaElement ; } /** Sets the minimum height of the textarea as determined by minRows. */ _setMinHeight ( ) { const minHeight = this . minRows && this . _cachedLineHeight ? `${ this . minRows * this . _cachedLineHeight }px` : null ; if ( minHeight ) { this . _textareaElement . style . minHeight = minHeight ; } } /** Sets the maximum height of the textarea as determined by maxRows. */ _setMaxHeight ( ) { const maxHeight = this . maxRows && this . _cachedLineHeight ? `${ this . maxRows * this . _cachedLineHeight }px` : null ; if ( maxHeight ) { this . _textareaElement . style . maxHeight = maxHeight ; } } ngAfterViewInit ( ) { if ( this . _platform . isBrowser ) { this . _initialHeight = this . _textareaElement . style . height ; this . resizeToFitContent ( ) ; this . _ngZone . runOutsideAngular ( ( ) => { fromEvent ( window , 'resize' ) . pipe ( auditTime ( 16 ) , takeUntil ( this . _destroyed ) ) . subscribe ( ( ) => this . resizeToFitContent ( true ) ) ; } ) ; } } ngOnDestroy ( ) { this . _destroyed . next ( ) ; this . _destroyed . complete ( ) ; } /**
     * Cache the height of a single-row textarea if it has not already been cached.
     *
     * We need to know how large a single "row" of a textarea is in order to apply minRows and
     * maxRows. For the initial version, we will assume that the height of a single line in the
     * textarea does not ever change.
     */ private _cacheTextareaLineHeight ( ) { if ( this . _cachedLineHeight ) { return ; } let textareaClone = this . _textareaElement . cloneNode ( false ) as HTMLTextAreaElement ; textareaClone . rows = 1 ; textareaClone . style . position = 'absolute' ; textareaClone . style . visibility = 'hidden' ; textareaClone . style . border = 'none' ; textareaClone . style . padding = '0' ; textareaClone . style . height = '' ; textareaClone . style . minHeight = '' ; textareaClone . style . maxHeight = '' ; textareaClone . style . overflow = 'hidden' ; this . _textareaElement . parentNode ! . appendChild ( textareaClone ) ; this . _cachedLineHeight = textareaClone . clientHeight ; this . _textareaElement . parentNode ! . removeChild ( textareaClone ) ; this . _setMinHeight ( ) ; this . _setMaxHeight ( ) ; } ngDoCheck ( ) { if ( this . _platform . isBrowser ) { this . resizeToFitContent ( ) ; } } param force resizeToFitContent ( force = false ) { if ( ! this . _enabled ) { return ; } this . _cacheTextareaLineHeight ( ) ; if ( ! this . _cachedLineHeight ) { return ; } const textarea = this . _elementRef . nativeElement as HTMLTextAreaElement ; const value = textarea . value ; if ( ! force && this . _minRows === this . _previousMinRows && value === this . _previousValue ) { return ; } const placeholderText = textarea . placeholder ; textarea . classList . add ( 'cdk-textarea-autosize-measuring' ) ; textarea . placeholder = '' ; const height = textarea . scrollHeight - 4 ; textarea . style . height = `${ height }px` ; textarea . classList . remove ( 'cdk-textarea-autosize-measuring' ) ; textarea . placeholder = placeholderText ; this . _ngZone . runOutsideAngular ( ( ) => { if ( typeof requestAnimationFrame !== 'undefined' ) { requestAnimationFrame ( ( ) => this . _scrollToCaretPosition ( textarea ) ) ; } else { setTimeout ( ( ) => this . _scrollToCaretPosition ( textarea ) ) ; } } ) ; this . _previousValue = value ; this . _previousMinRows = this . _minRows ; } /**
     * Resets the textarea to it's original size
     */ reset ( ) { if ( this . _initialHeight === undefined ) { return ; } this . _textareaElement . style . height = this . _initialHeight ; } _noopInputHandler ( ) { } /**
     * Scrolls a textarea to the caret position. On Firefox resizing the textarea will
     * prevent it from scrolling to the caret position. We need to re-set the selection
     * in order for it to scroll to the proper position.
     */ private _scrollToCaretPosition ( textarea ) { const { selectionStart , selectionEnd } = textarea ; if ( ! this . _destroyed . isStopped && document . activeElement === textarea ) { textarea . setSelectionRange ( selectionStart , selectionEnd ) ; } } }