license import { Platform , normalizePassiveListenerOptions , _getShadowRoot , _getEventTarget , } from '@angular/cdk/platform' ; import { Directive , ElementRef , EventEmitter , Inject , Injectable , InjectionToken , NgZone , OnDestroy , Optional , Output , AfterViewInit , } from '@angular/core' ; import { Observable , of as observableOf , Subject , Subscription } from 'rxjs' ; import { takeUntil } from 'rxjs/operators' ; import { coerceElement } from '@angular/cdk/coercion' ; import { DOCUMENT } from '@angular/common' ; import { InputModalityDetector , TOUCH_BUFFER_MS } from '../input-modality/input-modality-detector' ; export type FocusOrigin = 'touch' | 'mouse' | 'keyboard' | 'program' | null ; /**
 * Corresponds to the options that can be passed to the native `focus` event.
 * via https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus
 */ export interface FocusOptions { /** Whether the browser should scroll to the element when it is focused. */ preventScroll ? : boolean ; } /** Detection mode used for attributing the origin of a focus event. */ export const enum FocusMonitorDetectionMode { /**
     * Any mousedown, keydown, or touchstart event that happened in the previous
     * tick or the current tick will be used to assign a focus event's origin (to
     * either mouse, keyboard, or touch). This is the default option.
     */ IMMEDIATE , /**
     * A focus event's origin is always attributed to the last corresponding
     * mousedown, keydown, or touchstart event, no matter how long ago it occurred.
     */ EVENTUAL } /** Injectable service-level options for FocusMonitor. */ export interface FocusMonitorOptions { detectionMode ? : FocusMonitorDetectionMode ; } /** InjectionToken for FocusMonitorOptions. */ export const FOCUS_MONITOR_DEFAULT_OPTIONS = new InjectionToken < FocusMonitorOptions > ( 'cdk-focus-monitor-default-options' ) ; type MonitoredElementInfo = { checkChildren : boolean ; readonly subject : Subject < FocusOrigin > ; rootNode : HTMLElement | ShadowRoot | Document ; } ; /**
 * Event listener options that enable capturing and also
 * mark the listener as passive if the browser supports it.
 */ const captureEventListenerOptions = normalizePassiveListenerOptions ( { passive : true , capture : true , } ) ; /** Monitors mouse and keyboard events to determine the cause of focus events. */ @ Injectable ( { providedIn : 'root' } ) export class FocusMonitor implements OnDestroy { /** The focus origin that the next focus event is a result of. */ private _origin : FocusOrigin = null ; /** The FocusOrigin of the last focus event tracked by the FocusMonitor. */ private _lastFocusOrigin : FocusOrigin ; /** Whether the window has just been focused. */ private _windowFocused = false ; /** The timeout id of the window focus timeout. */ private _windowFocusTimeoutId : number ; /** The timeout id of the origin clearing timeout. */ private _originTimeoutId : number ; /**
     * Whether the origin was determined via a touch interaction. Necessary as properly attributing
     * focus events to touch interactions requires special logic.
     */ private _originFromTouchInteraction = false ; /** Map of elements being monitored to their info. */ private _elementInfo = new Map < HTMLElement , MonitoredElementInfo > ( ) ; /** The number of elements currently being monitored. */ private _monitoredElementCount = 0 ; /**
     * Keeps track of the root nodes to which we've currently bound a focus/blur handler,
     * as well as the number of monitored elements that they contain. We have to treat focus/blur
     * handlers differently from the rest of the events, because the browser won't emit events
     * to the document when focus moves inside of a shadow root.
     */ private _rootNodeFocusListenerCount = new Map < HTMLElement | Document | ShadowRoot , number > ( ) ; /**
     * The specified detection mode, used for attributing the origin of a focus
     * event.
     */ private readonly _detectionMode : FocusMonitorDetectionMode ; /**
     * Event listener for `focus` events on the window.
     * Needs to be an arrow function in order to preserve the context when it gets bound.
     */ private _windowFocusListener = ( ) => { this . _windowFocused = true ; this . _windowFocusTimeoutId = window . setTimeout ( ( ) => ( this . _windowFocused = false ) ) ; } ; /** Used to reference correct document/window */ protected _document ? : Document ; /** Subject for stopping our InputModalityDetector subscription. */ private readonly _stopInputModalityDetector = new Subject < void > ( ) ; constructor ( private _ngZone , private _platform , private readonly _inputModalityDetector , breaking-change @ Optional ( ) @ Inject ( DOCUMENT ) document , @ Optional ( ) @ Inject ( FOCUS_MONITOR_DEFAULT_OPTIONS ) options ) { this . _document = document ; this . _detectionMode = options ?. detectionMode || FocusMonitorDetectionMode . IMMEDIATE ; } /**
     * Event listener for `focus` and 'blur' events on the document.
     * Needs to be an arrow function in order to preserve the context when it gets bound.
     */ private _rootNodeFocusAndBlurListener = ( event ) => { const target = _getEventTarget < HTMLElement > ( event ) ; const handler = event . type === 'focus' ? this . _onFocus : this . _onBlur ; for ( let element = target ; element ; element = element . parentElement ) { handler . call ( this , event as FocusEvent , element ) ; } } ; param element param checkChildren returns monitor ( element , checkChildren ? ) ; param element param checkChildren returns monitor ( element , checkChildren ? ) ; monitor ( element , checkChildren = false ) { const nativeElement = coerceElement ( element ) ; if ( ! this . _platform . isBrowser || nativeElement . nodeType !== 1 ) { return observableOf ( null ) ; } const rootNode = _getShadowRoot ( nativeElement ) || this . _getDocument ( ) ; const cachedInfo = this . _elementInfo . get ( nativeElement ) ; if ( cachedInfo ) { if ( checkChildren ) { cachedInfo . checkChildren = true ; } return cachedInfo . subject ; } const info = { checkChildren : checkChildren , subject : new Subject < FocusOrigin > ( ) , rootNode , } ; this . _elementInfo . set ( nativeElement , info ) ; this . _registerGlobalListeners ( info ) ; return info . subject ; } param element stopMonitoring ( element ) ; param element stopMonitoring ( element ) ; stopMonitoring ( element ) { const nativeElement = coerceElement ( element ) ; const elementInfo = this . _elementInfo . get ( nativeElement ) ; if ( elementInfo ) { elementInfo . subject . complete ( ) ; this . _setClasses ( nativeElement ) ; this . _elementInfo . delete ( nativeElement ) ; this . _removeGlobalListeners ( elementInfo ) ; } } param element param origin param options focusVia ( element , origin , options ? ) ; param element param origin param options focusVia ( element , origin , options ? ) ; focusVia ( element , origin , options ? ) { const nativeElement = coerceElement ( element ) ; const focusedElement = this . _getDocument ( ) . activeElement ; if ( nativeElement === focusedElement ) { this . _getClosestElementsInfo ( nativeElement ) . forEach ( ( [ currentElement , info ] ) => this . _originChanged ( currentElement , origin , info ) ) ; } else { this . _setOrigin ( origin ) ; if ( typeof nativeElement . focus === 'function' ) { nativeElement . focus ( options ) ; } } } ngOnDestroy ( ) { this . _elementInfo . forEach ( ( _info , element ) => this . stopMonitoring ( element ) ) ; } /** Access injected document if available or fallback to global document reference */ private _getDocument ( ) { return this . _document || document ; } /** Use defaultView of injected document if available or fallback to global window reference */ private _getWindow ( ) { const doc = this . _getDocument ( ) ; return doc . defaultView || window ; } private _getFocusOrigin ( focusEventTarget ) { if ( this . _origin ) { if ( this . _originFromTouchInteraction ) { return this . _shouldBeAttributedToTouch ( focusEventTarget ) ? 'touch' : 'program' ; } else { return this . _origin ; } } return this . _windowFocused && this . _lastFocusOrigin ? this . _lastFocusOrigin : 'program' ; } param focusEventTarget private _shouldBeAttributedToTouch ( focusEventTarget ) { return ( this . _detectionMode === FocusMonitorDetectionMode . EVENTUAL || ! ! focusEventTarget ?. contains ( this . _inputModalityDetector . _mostRecentTarget ) ) ; } param element param origin private _setClasses ( element , origin ? ) { element . classList . toggle ( 'cdk-focused' , ! ! origin ) ; element . classList . toggle ( 'cdk-touch-focused' , origin === 'touch' ) ; element . classList . toggle ( 'cdk-keyboard-focused' , origin === 'keyboard' ) ; element . classList . toggle ( 'cdk-mouse-focused' , origin === 'mouse' ) ; element . classList . toggle ( 'cdk-program-focused' , origin === 'program' ) ; } param origin param isFromInteraction private _setOrigin ( origin , isFromInteraction = false ) { this . _ngZone . runOutsideAngular ( ( ) => { this . _origin = origin ; this . _originFromTouchInteraction = origin === 'touch' && isFromInteraction ; if ( this . _detectionMode === FocusMonitorDetectionMode . IMMEDIATE ) { clearTimeout ( this . _originTimeoutId ) ; const ms = this . _originFromTouchInteraction ? TOUCH_BUFFER_MS : 1 ; this . _originTimeoutId = setTimeout ( ( ) => ( this . _origin = null ) , ms ) ; } } ) ; } param event param element private _onFocus ( event , element ) { const elementInfo = this . _elementInfo . get ( element ) ; const focusEventTarget = _getEventTarget < HTMLElement > ( event ) ; if ( ! elementInfo || ( ! elementInfo . checkChildren && element !== focusEventTarget ) ) { return ; } this . _originChanged ( element , this . _getFocusOrigin ( focusEventTarget ) , elementInfo ) ; } param event param element _onBlur ( event , element ) { const elementInfo = this . _elementInfo . get ( element ) ; if ( ! elementInfo || ( elementInfo . checkChildren && event . relatedTarget instanceof Node && element . contains ( event . relatedTarget ) ) ) { return ; } this . _setClasses ( element ) ; this . _emitOrigin ( elementInfo . subject , null ) ; } private _emitOrigin ( subject , origin ) { this . _ngZone . run ( ( ) => subject . next ( origin ) ) ; } private _registerGlobalListeners ( elementInfo ) { if ( ! this . _platform . isBrowser ) { return ; } const rootNode = elementInfo . rootNode ; const rootNodeFocusListeners = this . _rootNodeFocusListenerCount . get ( rootNode ) || 0 ; if ( ! rootNodeFocusListeners ) { this . _ngZone . runOutsideAngular ( ( ) => { rootNode . addEventListener ( 'focus' , this . _rootNodeFocusAndBlurListener , captureEventListenerOptions ) ; rootNode . addEventListener ( 'blur' , this . _rootNodeFocusAndBlurListener , captureEventListenerOptions ) ; } ) ; } this . _rootNodeFocusListenerCount . set ( rootNode , rootNodeFocusListeners + 1 ) ; if ( ++ this . _monitoredElementCount === 1 ) { this . _ngZone . runOutsideAngular ( ( ) => { const window = this . _getWindow ( ) ; window . addEventListener ( 'focus' , this . _windowFocusListener ) ; } ) ; this . _inputModalityDetector . modalityDetected . pipe ( takeUntil ( this . _stopInputModalityDetector ) ) . subscribe ( modality => { this . _setOrigin ( modality , true ) ; } ) ; } } private _removeGlobalListeners ( elementInfo ) { const rootNode = elementInfo . rootNode ; if ( this . _rootNodeFocusListenerCount . has ( rootNode ) ) { const rootNodeFocusListeners = this . _rootNodeFocusListenerCount . get ( rootNode ) ! ; if ( rootNodeFocusListeners > 1 ) { this . _rootNodeFocusListenerCount . set ( rootNode , rootNodeFocusListeners - 1 ) ; } else { rootNode . removeEventListener ( 'focus' , this . _rootNodeFocusAndBlurListener , captureEventListenerOptions ) ; rootNode . removeEventListener ( 'blur' , this . _rootNodeFocusAndBlurListener , captureEventListenerOptions ) ; this . _rootNodeFocusListenerCount . delete ( rootNode ) ; } } if ( ! -- this . _monitoredElementCount ) { const window = this . _getWindow ( ) ; window . removeEventListener ( 'focus' , this . _windowFocusListener ) ; this . _stopInputModalityDetector . next ( ) ; clearTimeout ( this . _windowFocusTimeoutId ) ; clearTimeout ( this . _originTimeoutId ) ; } } /** Updates all the state on an element once its focus origin has changed. */ private _originChanged ( element , origin , elementInfo ) { this . _setClasses ( element , origin ) ; this . _emitOrigin ( elementInfo . subject , origin ) ; this . _lastFocusOrigin = origin ; } param element private _getClosestElementsInfo ( element ) { const results = [ ] ; this . _elementInfo . forEach ( ( info , currentElement ) => { if ( currentElement === element || ( info . checkChildren && currentElement . contains ( element ) ) ) { results . push ( [ currentElement , info ] ) ; } } ) ; return results ; } } /**
 * Directive that determines how a particular element was focused (via keyboard, mouse, touch, or
 * programmatically) and adds corresponding classes to the element.
 *
 * There are two variants of this directive:
 * 1) cdkMonitorElementFocus: does not consider an element to be focused if one of its children is
 *    focused.
 * 2) cdkMonitorSubtreeFocus: considers an element focused if it or any of its children are focused.
 */ @ Directive ( { selector : '[cdkMonitorElementFocus], [cdkMonitorSubtreeFocus]' , } ) export class CdkMonitorFocus implements AfterViewInit , OnDestroy { private _monitorSubscription : Subscription ; @ Output ( ) readonly cdkFocusChange = new EventEmitter < FocusOrigin > ( ) ; constructor ( private _elementRef , private _focusMonitor ) { } ngAfterViewInit ( ) { const element = this . _elementRef . nativeElement ; this . _monitorSubscription = this . _focusMonitor . monitor ( element , element . nodeType === 1 && element . hasAttribute ( 'cdkMonitorSubtreeFocus' ) ) . subscribe ( origin => this . cdkFocusChange . emit ( origin ) ) ; } ngOnDestroy ( ) { this . _focusMonitor . stopMonitoring ( this . _elementRef ) ; if ( this . _monitorSubscription ) { this . _monitorSubscription . unsubscribe ( ) ; } } }