license import { Directionality } from '@angular/cdk/bidi' ; import { DOWN_ARROW , ENTER , ESCAPE , TAB , UP_ARROW } from '@angular/cdk/keycodes' ; import { FlexibleConnectedPositionStrategy , Overlay , OverlayConfig , OverlayRef , PositionStrategy , ScrollStrategy , } from '@angular/cdk/overlay' ; import { TemplatePortal } from '@angular/cdk/portal' ; import { DOCUMENT } from '@angular/common' ; import { filter , take , switchMap , delay , tap , map } from 'rxjs/operators' ; import { ChangeDetectorRef , Directive , ElementRef , forwardRef , Host , Inject , InjectionToken , Input , NgZone , OnDestroy , Optional , ViewContainerRef , } from '@angular/core' ; import { ViewportRuler } from '@angular/cdk/scrolling' ; import { ControlValueAccessor , NG_VALUE_ACCESSOR } from '@angular/forms' ; import { _countGroupLabelsBeforeOption , _getOptionScrollPosition , MatOption , MatOptionSelectionChange , } from '@angular/material/core' ; import { MatFormField } from '@angular/material/form-field' ; import { Subscription , defer , fromEvent , merge , of as observableOf , Subject , Observable } from 'rxjs' ; import { MatAutocomplete } from './autocomplete' ; import { coerceBooleanProperty } from '@angular/cdk/coercion' ; import { MatAutocompleteOrigin } from './autocomplete-origin' ; /**
 * The following style constants are necessary to save here in order
 * to properly calculate the scrollTop of the panel. Because we are not
 * actually focusing the active item, scroll must be handled manually.
 */ /** The height of each autocomplete option. */ export const AUTOCOMPLETE_OPTION_HEIGHT = 48 ; /** The total height of the autocomplete panel. */ export const AUTOCOMPLETE_PANEL_HEIGHT = 256 ; /** Injection token that determines the scroll handling while the autocomplete panel is open. */ export const MAT_AUTOCOMPLETE_SCROLL_STRATEGY = new InjectionToken < ( ) => ScrollStrategy > ( 'mat-autocomplete-scroll-strategy' ) ; docs-private export function MAT_AUTOCOMPLETE_SCROLL_STRATEGY_FACTORY ( overlay ) { return ( ) => overlay . scrollStrategies . reposition ( ) ; } docs-private export const MAT_AUTOCOMPLETE_SCROLL_STRATEGY_FACTORY_PROVIDER = { provide : MAT_AUTOCOMPLETE_SCROLL_STRATEGY , deps : [ Overlay ] , useFactory : MAT_AUTOCOMPLETE_SCROLL_STRATEGY_FACTORY , } ; docs-private export const MAT_AUTOCOMPLETE_VALUE_ACCESSOR = { provide : NG_VALUE_ACCESSOR , useExisting : forwardRef ( ( ) => MatAutocompleteTrigger ) , multi : true } ; docs-private export function getMatAutocompleteMissingPanelError ( ) { return Error ( 'Attempting to open an undefined instance of `mat-autocomplete`. ' + 'Make sure that the id passed to the `matAutocomplete` is correct and that ' + 'you\'re attempting to open it after the ngAfterContentInit hook.' ) ; } @ Directive ( { selector : `input[matAutocomplete], textarea[matAutocomplete]` , host : { '[attr.autocomplete]' : 'autocompleteAttribute' , '[attr.role]' : 'autocompleteDisabled ? null : "combobox"' , '[attr.aria-autocomplete]' : 'autocompleteDisabled ? null : "list"' , '[attr.aria-activedescendant]' : '(panelOpen && activeOption) ? activeOption.id : null' , '[attr.aria-expanded]' : 'autocompleteDisabled ? null : panelOpen.toString()' , '[attr.aria-owns]' : '(autocompleteDisabled || !panelOpen) ? null : autocomplete?.id' , '(focusin)' : '_handleFocus()' , '(blur)' : '_onTouched()' , '(input)' : '_handleInput($event)' , '(keydown)' : '_handleKeydown($event)' , } , exportAs : 'matAutocompleteTrigger' , providers : [ MAT_AUTOCOMPLETE_VALUE_ACCESSOR ] } ) export class MatAutocompleteTrigger implements ControlValueAccessor , OnDestroy { private _overlayRef : OverlayRef | null ; private _portal : TemplatePortal ; private _componentDestroyed = false ; private _autocompleteDisabled = false ; private _scrollStrategy : ( ) => ScrollStrategy ; /** Old value of the native input. Used to work around issues with the `input` event on IE. */ private _previousValue : string | number | null ; /** Strategy that is used to position the panel. */ private _positionStrategy : FlexibleConnectedPositionStrategy ; /** Whether or not the label state is being overridden. */ private _manuallyFloatingLabel = false ; /** The subscription for closing actions (some are bound to document). */ private _closingActionsSubscription : Subscription ; /** Subscription to viewport size changes. */ private _viewportSubscription = Subscription . EMPTY ; /**
     * Whether the autocomplete can open the next time it is focused. Used to prevent a focused,
     * closed autocomplete from being reopened if the user switches to another browser tab and then
     * comes back.
     */ private _canOpenOnNextFocus = true ; /** Stream of keyboard events that can close the panel. */ private readonly _closeKeyEventStream = new Subject < void > ( ) ; /**
     * Event handler for when the window is blurred. Needs to be an
     * arrow function in order to preserve the context.
     */ private _windowBlurHandler = ( ) => { this . _canOpenOnNextFocus = document . activeElement !== this . _element . nativeElement || this . panelOpen ; } ; /** `View -> model callback called when value changes` */ _onChange : ( value : any ) => void = ( ) => { } ; /** `View -> model callback called when autocomplete has been touched` */ _onTouched = ( ) => { } ; /** The autocomplete panel to be attached to this trigger. */ @ Input ( 'matAutocomplete' ) autocomplete : MatAutocomplete ; /**
     * Reference relative to which to position the autocomplete panel.
     * Defaults to the autocomplete trigger element.
     */ @ Input ( 'matAutocompleteConnectedTo' ) connectedTo : MatAutocompleteOrigin ; docs-private @ Input ( 'autocomplete' ) autocompleteAttribute : string = 'off' ; /**
     * Whether the autocomplete is disabled. When disabled, the element will
     * act as a regular input and the user won't be able to open the panel.
     */ @ Input ( 'matAutocompleteDisabled' ) get autocompleteDisabled ( ) : boolean { return this . _autocompleteDisabled ; } set autocompleteDisabled ( value ) { this . _autocompleteDisabled = coerceBooleanProperty ( value ) ; } constructor ( private _element , private _overlay , private _viewContainerRef , private _zone , private _changeDetectorRef , @ Inject ( MAT_AUTOCOMPLETE_SCROLL_STRATEGY ) scrollStrategy , @ Optional ( ) private _dir , @ Optional ( ) @ Host ( ) private _formField , @ Optional ( ) @ Inject ( DOCUMENT ) private _document , private _viewportRuler ? ) { if ( typeof window !== 'undefined' ) { _zone . runOutsideAngular ( ( ) => { window . addEventListener ( 'blur' , this . _windowBlurHandler ) ; } ) ; } this . _scrollStrategy = scrollStrategy ; } ngOnDestroy ( ) { if ( typeof window !== 'undefined' ) { window . removeEventListener ( 'blur' , this . _windowBlurHandler ) ; } this . _viewportSubscription . unsubscribe ( ) ; this . _componentDestroyed = true ; this . _destroyPanel ( ) ; this . _closeKeyEventStream . complete ( ) ; } /** Whether or not the autocomplete panel is open. */ get panelOpen ( ) : boolean { return this . _overlayAttached && this . autocomplete . showPanel ; } private _overlayAttached : boolean = false ; /** Opens the autocomplete suggestion panel. */ openPanel ( ) { this . _attachOverlay ( ) ; this . _floatLabel ( ) ; } /** Closes the autocomplete suggestion panel. */ closePanel ( ) { this . _resetLabel ( ) ; if ( ! this . _overlayAttached ) { return ; } if ( this . panelOpen ) { this . autocomplete . closed . emit ( ) ; } this . autocomplete . _isOpen = this . _overlayAttached = false ; if ( this . _overlayRef && this . _overlayRef . hasAttached ( ) ) { this . _overlayRef . detach ( ) ; this . _closingActionsSubscription . unsubscribe ( ) ; } if ( ! this . _componentDestroyed ) { this . _changeDetectorRef . detectChanges ( ) ; } } /**
     * Updates the position of the autocomplete suggestion panel to ensure that it fits all options
     * within the viewport.
     */ updatePosition ( ) { if ( this . _overlayAttached ) { this . _overlayRef ! . updatePosition ( ) ; } } /**
     * A stream of actions that should close the autocomplete panel, including
     * when an option is selected, on blur, and when TAB is pressed.
     */ get panelClosingActions ( ) : Observable < MatOptionSelectionChange | null > { return merge ( this . optionSelections , this . autocomplete . _keyManager . tabOut . pipe ( filter ( ( ) => this . _overlayAttached ) ) , this . _closeKeyEventStream , this . _getOutsideClickStream ( ) , this . _overlayRef ? this . _overlayRef . detachments ( ) . pipe ( filter ( ( ) => this . _overlayAttached ) ) : observableOf ( ) ) . pipe ( map ( event => event instanceof MatOptionSelectionChange ? event : null ) ) ; } /** Stream of autocomplete option selections. */ readonly optionSelections : Observable < MatOptionSelectionChange > = defer ( ( ) => { if ( this . autocomplete && this . autocomplete . options ) { return merge ( ... this . autocomplete . options . map ( option => option . onSelectionChange ) ) ; } return this . _zone . onStable . asObservable ( ) . pipe ( take ( 1 ) , switchMap ( ( ) => this . optionSelections ) ) ; } ) ; /** The currently active option, coerced to MatOption type. */ get activeOption ( ) : MatOption | null { if ( this . autocomplete && this . autocomplete . _keyManager ) { return this . autocomplete . _keyManager . activeItem ; } return null ; } /** Stream of clicks outside of the autocomplete panel. */ private _getOutsideClickStream ( ) { if ( ! this . _document ) { return observableOf ( null ) ; } return merge ( fromEvent < MouseEvent > ( this . _document , 'click' ) , fromEvent < TouchEvent > ( this . _document , 'touchend' ) ) . pipe ( filter ( event => { const clickTarget = event . target as HTMLElement ; const formField = this . _formField ? this . _formField . _elementRef . nativeElement : null ; return this . _overlayAttached && clickTarget !== this . _element . nativeElement && ( ! formField || ! formField . contains ( clickTarget ) ) && ( ! ! this . _overlayRef && ! this . _overlayRef . overlayElement . contains ( clickTarget ) ) ; } ) ) ; } writeValue ( value ) { Promise . resolve ( null ) . then ( ( ) => this . _setTriggerValue ( value ) ) ; } registerOnChange ( fn ) { this . _onChange = fn ; } registerOnTouched ( fn ) { this . _onTouched = fn ; } setDisabledState ( isDisabled ) { this . _element . nativeElement . disabled = isDisabled ; } _handleKeydown ( event ) { const keyCode = event . keyCode ; if ( keyCode === ESCAPE ) { event . preventDefault ( ) ; } if ( this . activeOption && keyCode === ENTER && this . panelOpen ) { this . activeOption . _selectViaInteraction ( ) ; this . _resetActiveItem ( ) ; event . preventDefault ( ) ; } else if ( this . autocomplete ) { const prevActiveItem = this . autocomplete . _keyManager . activeItem ; const isArrowKey = keyCode === UP_ARROW || keyCode === DOWN_ARROW ; if ( this . panelOpen || keyCode === TAB ) { this . autocomplete . _keyManager . onKeydown ( event ) ; } else if ( isArrowKey && this . _canOpen ( ) ) { this . openPanel ( ) ; } if ( isArrowKey || this . autocomplete . _keyManager . activeItem !== prevActiveItem ) { this . _scrollToOption ( ) ; } } } _handleInput ( event ) { let target = event . target as HTMLInputElement ; let value = target . value ; if ( target . type === 'number' ) { value = value == '' ? null : parseFloat ( value ) ; } if ( this . _previousValue !== value ) { this . _previousValue = value ; this . _onChange ( value ) ; if ( this . _canOpen ( ) && document . activeElement === event . target ) { this . openPanel ( ) ; } } } _handleFocus ( ) { if ( ! this . _canOpenOnNextFocus ) { this . _canOpenOnNextFocus = true ; } else if ( this . _canOpen ( ) ) { this . _previousValue = this . _element . nativeElement . value ; this . _attachOverlay ( ) ; this . _floatLabel ( true ) ; } } param shouldAnimate private _floatLabel ( shouldAnimate = false ) { if ( this . _formField && this . _formField . floatLabel === 'auto' ) { if ( shouldAnimate ) { this . _formField . _animateAndLockLabel ( ) ; } else { this . _formField . floatLabel = 'always' ; } this . _manuallyFloatingLabel = true ; } } /** If the label has been manually elevated, return it to its normal state. */ private _resetLabel ( ) { if ( this . _manuallyFloatingLabel ) { this . _formField . floatLabel = 'auto' ; this . _manuallyFloatingLabel = false ; } } /**
     * Given that we are not actually focusing active options, we must manually adjust scroll
     * to reveal options below the fold. First, we find the offset of the option from the top
     * of the panel. If that offset is below the fold, the new scrollTop will be the offset -
     * the panel height + the option height, so the active option will be just visible at the
     * bottom of the panel. If that offset is above the top of the visible panel, the new scrollTop
     * will become the offset. If that offset is visible within the panel already, the scrollTop is
     * not adjusted.
     */ private _scrollToOption ( ) { const index = this . autocomplete . _keyManager . activeItemIndex || 0 ; const labelCount = _countGroupLabelsBeforeOption ( index , this . autocomplete . options , this . autocomplete . optionGroups ) ; const newScrollPosition = _getOptionScrollPosition ( index + labelCount , AUTOCOMPLETE_OPTION_HEIGHT , this . autocomplete . _getScrollTop ( ) , AUTOCOMPLETE_PANEL_HEIGHT ) ; this . autocomplete . _setScrollTop ( newScrollPosition ) ; } /**
     * This method listens to a stream of panel closing actions and resets the
     * stream every time the option list changes.
     */ private _subscribeToClosingActions ( ) { const firstStable = this . _zone . onStable . asObservable ( ) . pipe ( take ( 1 ) ) ; const optionChanges = this . autocomplete . options . changes . pipe ( tap ( ( ) => this . _positionStrategy . reapplyLastPosition ( ) ) , delay ( 0 ) ) ; return merge ( firstStable , optionChanges ) . pipe ( switchMap ( ( ) => { this . _resetActiveItem ( ) ; this . autocomplete . _setVisibility ( ) ; if ( this . panelOpen ) { this . _overlayRef ! . updatePosition ( ) ; } return this . panelClosingActions ; } ) , take ( 1 ) ) . subscribe ( event => this . _setValueAndClose ( event ) ) ; } /** Destroys the autocomplete suggestion panel. */ private _destroyPanel ( ) { if ( this . _overlayRef ) { this . closePanel ( ) ; this . _overlayRef . dispose ( ) ; this . _overlayRef = null ; } } private _setTriggerValue ( value ) { const toDisplay = this . autocomplete && this . autocomplete . displayWith ? this . autocomplete . displayWith ( value ) : value ; const inputValue = toDisplay != null ? toDisplay : '' ; if ( this . _formField ) { this . _formField . _control . value = inputValue ; } else { this . _element . nativeElement . value = inputValue ; } this . _previousValue = inputValue ; } /**
     * This method closes the panel, and if a value is specified, also sets the associated
     * control to that value. It will also mark the control as dirty if this interaction
     * stemmed from the user.
     */ private _setValueAndClose ( event ) { if ( event && event . source ) { this . _clearPreviousSelectedOption ( event . source ) ; this . _setTriggerValue ( event . source . value ) ; this . _onChange ( event . source . value ) ; this . _element . nativeElement . focus ( ) ; this . autocomplete . _emitSelectEvent ( event . source ) ; } this . closePanel ( ) ; } /**
     * Clear any previous selected option and emit a selection change event for this option
     */ private _clearPreviousSelectedOption ( skip ) { this . autocomplete . options . forEach ( option => { if ( option != skip && option . selected ) { option . deselect ( ) ; } } ) ; } private _attachOverlay ( ) { if ( ! this . autocomplete ) { throw getMatAutocompleteMissingPanelError ( ) ; } if ( ! this . _overlayRef ) { this . _portal = new TemplatePortal ( this . autocomplete . template , this . _viewContainerRef ) ; this . _overlayRef = this . _overlay . create ( this . _getOverlayConfig ( ) ) ; this . _overlayRef . keydownEvents ( ) . subscribe ( event => { if ( event . keyCode === ESCAPE || ( event . keyCode === UP_ARROW && event . altKey ) ) { this . _resetActiveItem ( ) ; this . _closeKeyEventStream . next ( ) ; } } ) ; if ( this . _viewportRuler ) { this . _viewportSubscription = this . _viewportRuler . change ( ) . subscribe ( ( ) => { if ( this . panelOpen && this . _overlayRef ) { this . _overlayRef . updateSize ( { width : this . _getPanelWidth ( ) } ) ; } } ) ; } } else { this . _overlayRef . updateSize ( { width : this . _getPanelWidth ( ) } ) ; } if ( this . _overlayRef && ! this . _overlayRef . hasAttached ( ) ) { this . _overlayRef . attach ( this . _portal ) ; this . _closingActionsSubscription = this . _subscribeToClosingActions ( ) ; } const wasOpen = this . panelOpen ; this . autocomplete . _setVisibility ( ) ; this . autocomplete . _isOpen = this . _overlayAttached = true ; if ( this . panelOpen && wasOpen !== this . panelOpen ) { this . autocomplete . opened . emit ( ) ; } } private _getOverlayConfig ( ) { return new OverlayConfig ( { positionStrategy : this . _getOverlayPosition ( ) , scrollStrategy : this . _scrollStrategy ( ) , width : this . _getPanelWidth ( ) , direction : this . _dir } ) ; } private _getOverlayPosition ( ) { this . _positionStrategy = this . _overlay . position ( ) . flexibleConnectedTo ( this . _getConnectedElement ( ) ) . withFlexibleDimensions ( false ) . withPush ( false ) . withPositions ( [ { originX : 'start' , originY : 'bottom' , overlayX : 'start' , overlayY : 'top' } , { originX : 'start' , originY : 'top' , overlayX : 'start' , overlayY : 'bottom' , panelClass : 'mat-autocomplete-panel-above' } ] ) ; return this . _positionStrategy ; } private _getConnectedElement ( ) { if ( this . connectedTo ) { return this . connectedTo . elementRef ; } return this . _formField ? this . _formField . getConnectedOverlayOrigin ( ) : this . _element ; } private _getPanelWidth ( ) { return this . autocomplete . panelWidth || this . _getHostWidth ( ) ; } /** Returns the width of the input element, so the panel width can match it. */ private _getHostWidth ( ) { return this . _getConnectedElement ( ) . nativeElement . getBoundingClientRect ( ) . width ; } /**
     * Resets the active item to -1 so arrow events will activate the
     * correct options, or to 0 if the consumer opted into it.
     */ private _resetActiveItem ( ) { this . autocomplete . _keyManager . setActiveItem ( this . autocomplete . autoActiveFirstOption ? 0 : - 1 ) ; } /** Determines whether the panel can be opened. */ private _canOpen ( ) { const element = this . _element . nativeElement ; return ! element . readOnly && ! element . disabled && ! this . _autocompleteDisabled ; } }