license import { Directive , Input , ViewContainerRef , Output , EventEmitter , Optional , OnDestroy , Inject , Injectable , InjectionToken , } from '@angular/core' ; import { Directionality } from '@angular/cdk/bidi' ; import { OverlayRef , Overlay , OverlayConfig , FlexibleConnectedPositionStrategy , ConnectedPosition , } from '@angular/cdk/overlay' ; import { TemplatePortal , Portal } from '@angular/cdk/portal' ; import { coerceBooleanProperty , BooleanInput } from '@angular/cdk/coercion' ; import { Subject , merge } from 'rxjs' ; import { takeUntil } from 'rxjs/operators' ; import { CdkMenuPanel } from './menu-panel' ; import { MenuStack } from './menu-stack' ; import { throwExistingMenuStackError } from './menu-errors' ; import { isClickInsideMenuOverlay } from './menu-item-trigger' ; /** Tracks the last open context menu trigger across the entire application. */ @ Injectable ( { providedIn : 'root' } ) export class ContextMenuTracker { /** The last open context menu trigger. */ private static _openContextMenuTrigger ? : CdkContextMenuTrigger ; param trigger update ( trigger ) { if ( ContextMenuTracker . _openContextMenuTrigger !== trigger ) { ContextMenuTracker . _openContextMenuTrigger ?. close ( ) ; ContextMenuTracker . _openContextMenuTrigger = trigger ; } } } /** Configuration options passed to the context menu. */ export type ContextMenuOptions = { /** The opened menus X coordinate offset from the triggering position. */ offsetX : number ; /** The opened menus Y coordinate offset from the triggering position. */ offsetY : number ; } ; /** Injection token for the ContextMenu options object. */ export const CDK_CONTEXT_MENU_DEFAULT_OPTIONS = new InjectionToken < ContextMenuOptions > ( 'cdk-context-menu-default-options' ) ; /** The coordinates of where the context menu should open. */ export type ContextMenuCoordinates = { x : number ; y : number ; } ; /**
 * A directive which when placed on some element opens a the Menu it is bound to when a user
 * right-clicks within that element. It is aware of nested Context Menus and the lowest level
 * non-disabled context menu will trigger.
 */ @ Directive ( { selector : '[cdkContextMenuTriggerFor]' , exportAs : 'cdkContextMenuTriggerFor' , host : { '(contextmenu)' : '_openOnContextMenu($event)' , } , providers : [ { provide : CDK_CONTEXT_MENU_DEFAULT_OPTIONS , useValue : { offsetX : 2 , offsetY : 2 } } , ] , } ) export class CdkContextMenuTrigger implements OnDestroy { /** Template reference variable to the menu to open on right click. */ @ Input ( 'cdkContextMenuTriggerFor' ) get menuPanel ( ) : CdkMenuPanel { return this . _menuPanel ; } set menuPanel ( panel ) { if ( ( typeof ngDevMode === 'undefined' || ngDevMode ) && panel . _menuStack ) { throwExistingMenuStackError ( ) ; } this . _menuPanel = panel ; if ( this . _menuPanel ) { this . _menuPanel . _menuStack = this . _menuStack ; } } /** Reference to the MenuPanel this trigger toggles. */ private _menuPanel : CdkMenuPanel ; /** Emits when the attached menu is requested to open. */ @ Output ( 'cdkContextMenuOpened' ) readonly opened : EventEmitter < void > = new EventEmitter ( ) ; /** Emits when the attached menu is requested to close. */ @ Output ( 'cdkContextMenuClosed' ) readonly closed : EventEmitter < void > = new EventEmitter ( ) ; /** Whether the context menu should be disabled. */ @ Input ( 'cdkContextMenuDisabled' ) get disabled ( ) : boolean { return this . _disabled ; } set disabled ( value ) { this . _disabled = coerceBooleanProperty ( value ) ; } private _disabled = false ; /** A reference to the overlay which manages the triggered menu. */ private _overlayRef : OverlayRef | null = null ; /** The content of the menu panel opened by this trigger. */ private _panelContent : TemplatePortal ; /** Emits when the element is destroyed. */ private readonly _destroyed = new Subject < void > ( ) ; /** The menu stack for this trigger and its associated menus. */ private readonly _menuStack = new MenuStack ( ) ; /** Emits when the outside pointer events listener on the overlay should be stopped. */ private readonly _stopOutsideClicksListener = merge ( this . closed , this . _destroyed ) ; constructor ( protected readonly _viewContainerRef , private readonly _overlay , private readonly _contextMenuTracker , @ Inject ( CDK_CONTEXT_MENU_DEFAULT_OPTIONS ) private readonly _options , @ Optional ( ) private readonly _directionality ? ) { this . _setMenuStackListener ( ) ; } param coordinates open ( coordinates ) { if ( this . disabled ) { return ; } else if ( this . isOpen ( ) ) { this . _menuStack . closeSubMenuOf ( this . _menuPanel . _menu ! ) ; ( this . _overlayRef ! . getConfig ( ) . positionStrategy as FlexibleConnectedPositionStrategy ) . setOrigin ( coordinates ) ; this . _overlayRef ! . updatePosition ( ) ; } else { this . opened . next ( ) ; if ( this . _overlayRef ) { ( this . _overlayRef . getConfig ( ) . positionStrategy as FlexibleConnectedPositionStrategy ) . setOrigin ( coordinates ) ; this . _overlayRef . updatePosition ( ) ; } else { this . _overlayRef = this . _overlay . create ( this . _getOverlayConfig ( coordinates ) ) ; } this . _overlayRef . attach ( this . _getMenuContent ( ) ) ; this . _subscribeToOutsideClicks ( ) ; } } /** Close the opened menu. */ close ( ) { this . _menuStack . closeAll ( ) ; } param event _openOnContextMenu ( event ) { if ( ! this . disabled ) { event . preventDefault ( ) ; event . stopPropagation ( ) ; this . _contextMenuTracker . update ( this ) ; this . open ( { x : event . clientX , y : event . clientY } ) ; if ( event . button === 2 ) { this . _menuPanel . _menu ?. focusFirstItem ( 'mouse' ) ; } else if ( event . button === 0 ) { this . _menuPanel . _menu ?. focusFirstItem ( 'keyboard' ) ; } else { this . _menuPanel . _menu ?. focusFirstItem ( 'program' ) ; } } } /** Whether the attached menu is open. */ isOpen ( ) { return ! ! this . _overlayRef ?. hasAttached ( ) ; } param coordinates private _getOverlayConfig ( coordinates ) { return new OverlayConfig ( { positionStrategy : this . _getOverlayPositionStrategy ( coordinates ) , scrollStrategy : this . _overlay . scrollStrategies . block ( ) , direction : this . _directionality , } ) ; } param coordinates private _getOverlayPositionStrategy ( coordinates ) { return this . _overlay . position ( ) . flexibleConnectedTo ( coordinates ) . withDefaultOffsetX ( this . _options . offsetX ) . withDefaultOffsetY ( this . _options . offsetY ) . withPositions ( this . _getOverlayPositions ( ) ) ; } /**
     * Determine and return where to position the opened menu relative to the mouse location.
     */ private _getOverlayPositions ( ) { return [ { originX : 'end' , originY : 'top' , overlayX : 'start' , overlayY : 'top' } , { originX : 'start' , originY : 'top' , overlayX : 'end' , overlayY : 'top' } , { originX : 'end' , originY : 'bottom' , overlayX : 'start' , overlayY : 'bottom' } , { originX : 'start' , originY : 'bottom' , overlayX : 'end' , overlayY : 'bottom' } , ] ; } /**
     * Get the portal to be attached to the overlay which contains the menu. Allows for the menu
     * content to change dynamically and be reflected in the application.
     */ private _getMenuContent ( ) { const hasMenuContentChanged = this . menuPanel . _templateRef !== this . _panelContent ?. templateRef ; if ( this . menuPanel && ( ! this . _panelContent || hasMenuContentChanged ) ) { this . _panelContent = new TemplatePortal ( this . menuPanel . _templateRef , this . _viewContainerRef ) ; } return this . _panelContent ; } /** Subscribe to the menu stack close events and close this menu when requested. */ private _setMenuStackListener ( ) { this . _menuStack . closed . pipe ( takeUntil ( this . _destroyed ) ) . subscribe ( item => { if ( item === this . _menuPanel . _menu && this . isOpen ( ) ) { this . closed . next ( ) ; this . _overlayRef ! . detach ( ) ; } } ) ; } /**
     * Subscribe to the overlays outside pointer events stream and handle closing out the stack if a
     * click occurs outside the menus.
     */ private _subscribeToOutsideClicks ( ) { if ( this . _overlayRef ) { this . _overlayRef . outsidePointerEvents ( ) . pipe ( takeUntil ( this . _stopOutsideClicksListener ) ) . subscribe ( event => { if ( ! isClickInsideMenuOverlay ( event . target as Element ) ) { this . _menuStack . closeAll ( ) ; } } ) ; } } ngOnDestroy ( ) { this . _destroyOverlay ( ) ; this . _resetPanelMenuStack ( ) ; this . _destroyed . next ( ) ; this . _destroyed . complete ( ) ; } /** Destroy and unset the overlay reference it if exists. */ private _destroyOverlay ( ) { if ( this . _overlayRef ) { this . _overlayRef . dispose ( ) ; this . _overlayRef = null ; } } /** Set the menu panels menu stack back to null. */ private _resetPanelMenuStack ( ) { if ( this . _menuPanel ) { this . _menuPanel . _menuStack = null ; } } }