license import { FocusTrap } from '@angular/cdk/a11y' ; import { OverlayRef , PositionStrategy } from '@angular/cdk/overlay' ; import { TemplatePortal } from '@angular/cdk/portal' ; import { AfterViewInit , Directive , ElementRef , EmbeddedViewRef , NgZone , OnDestroy , TemplateRef , ViewContainerRef , } from '@angular/core' ; import { fromEvent , fromEventPattern , merge , Subject } from 'rxjs' ; import { filter , map , mapTo , share , startWith , takeUntil , throttleTime , withLatestFrom , } from 'rxjs/operators' ; import { CELL_SELECTOR , EDIT_PANE_CLASS , EDIT_PANE_SELECTOR , ROW_SELECTOR } from './constants' ; import { EditEventDispatcher , HoverContentState } from './edit-event-dispatcher' ; import { EditServices } from './edit-services' ; import { FocusDispatcher } from './focus-dispatcher' ; import { FocusEscapeNotifier , FocusEscapeNotifierDirection , FocusEscapeNotifierFactory , } from './focus-escape-notifier' ; import { closest } from './polyfill' ; import { EditRef } from './edit-ref' ; /**
 * Describes the number of columns before and after the originating cell that the
 * edit popup should span. In left to right locales, before means left and after means
 * right. In right to left locales before means right and after means left.
 */ export interface CdkPopoverEditColspan { before ? : number ; after ? : number ; } /** Used for rate-limiting mousemove events. */ const MOUSE_MOVE_THROTTLE_TIME_MS = 10 ; /**
 * A directive that must be attached to enable editability on a table.
 * It is responsible for setting up delegated event handlers and providing the
 * EditEventDispatcher service for use by the other edit directives.
 */ @ Directive ( { selector : 'table[editable], cdk-table[editable], mat-table[editable]' , providers : [ EditEventDispatcher , EditServices ] , } ) export class CdkEditable implements AfterViewInit , OnDestroy { protected readonly destroyed = new Subject < void > ( ) ; constructor ( protected readonly elementRef , protected readonly editEventDispatcher , protected readonly focusDispatcher , protected readonly ngZone ) { } ngAfterViewInit ( ) { this . _listenForTableEvents ( ) ; } ngOnDestroy ( ) { this . destroyed . next ( ) ; this . destroyed . complete ( ) ; } private _listenForTableEvents ( ) { const element = this . elementRef . nativeElement ; const toClosest = ( selector ) => map ( ( event ) => closest ( event . target , selector ) ) ; this . ngZone . runOutsideAngular ( ( ) => { fromEvent < MouseEvent > ( element , 'mouseover' ) . pipe ( toClosest ( ROW_SELECTOR ) , takeUntil ( this . destroyed ) ) . subscribe ( this . editEventDispatcher . hovering ) ; fromEvent < MouseEvent > ( element , 'mouseleave' ) . pipe ( mapTo ( null ) , takeUntil ( this . destroyed ) ) . subscribe ( this . editEventDispatcher . hovering ) ; fromEvent < MouseEvent > ( element , 'mousemove' ) . pipe ( throttleTime ( MOUSE_MOVE_THROTTLE_TIME_MS ) , toClosest ( ROW_SELECTOR ) , takeUntil ( this . destroyed ) ) . subscribe ( this . editEventDispatcher . mouseMove ) ; fromEventPattern < FocusEvent > ( handler => element . addEventListener ( 'focus' , handler , true ) , handler => element . removeEventListener ( 'focus' , handler , true ) ) . pipe ( toClosest ( ROW_SELECTOR ) , share ( ) , takeUntil ( this . destroyed ) ) . subscribe ( this . editEventDispatcher . focused ) ; merge ( fromEventPattern < FocusEvent > ( handler => element . addEventListener ( 'blur' , handler , true ) , handler => element . removeEventListener ( 'blur' , handler , true ) ) , fromEvent < KeyboardEvent > ( element , 'keydown' ) . pipe ( filter ( event => event . key === 'Escape' ) ) ) . pipe ( mapTo ( null ) , share ( ) , takeUntil ( this . destroyed ) ) . subscribe ( this . editEventDispatcher . focused ) ; this . ngZone . onStable . pipe ( withLatestFrom ( this . editEventDispatcher . editingOrFocused ) , filter ( ( [ _ , activeRow ] ) => activeRow == null ) , map ( ( ) => element . querySelectorAll ( ROW_SELECTOR ) ) , share ( ) , takeUntil ( this . destroyed ) ) . subscribe ( this . editEventDispatcher . allRows ) ; fromEvent < KeyboardEvent > ( element , 'keydown' ) . pipe ( filter ( event => event . key === 'Enter' ) , toClosest ( CELL_SELECTOR ) , takeUntil ( this . destroyed ) ) . subscribe ( this . editEventDispatcher . editing ) ; fromEvent < KeyboardEvent > ( element , 'keydown' ) . pipe ( takeUntil ( this . destroyed ) ) . subscribe ( this . focusDispatcher . keyObserver ) ; } ) ; } } const POPOVER_EDIT_HOST_BINDINGS = { '[attr.tabindex]' : 'disabled ? null : 0' , 'class' : 'cdk-popover-edit-cell' , '[attr.aria-haspopup]' : '!disabled' , } ; const POPOVER_EDIT_INPUTS = [ 'template: cdkPopoverEdit' , 'context: cdkPopoverEditContext' , 'colspan: cdkPopoverEditColspan' , 'disabled: cdkPopoverEditDisabled' , ] ; /**
 * Attaches an ng-template to a cell and shows it when instructed to by the
 * EditEventDispatcher service.
 * Makes the cell focusable.
 */ @ Directive ( { selector : '[cdkPopoverEdit]:not([cdkPopoverEditTabOut])' , host : POPOVER_EDIT_HOST_BINDINGS , inputs : POPOVER_EDIT_INPUTS , } ) export class CdkPopoverEdit < C > implements AfterViewInit , OnDestroy { /** The edit lens template shown over the cell on edit. */ template : TemplateRef < any > | null = null ; /**
     * Implicit context to pass along to the template. Can be omitted if the template
     * is defined within the cell.
     */ context ? : C ; /**
     * Specifies that the popup should cover additional table cells before and/or after
     * this one.
     */ get colspan ( ) : CdkPopoverEditColspan { return this . _colspan ; } set colspan ( value ) { this . _colspan = value ; if ( this . overlayRef ) { this . overlayRef . updatePositionStrategy ( this . _getPositionStrategy ( ) ) ; if ( this . overlayRef . hasAttached ( ) ) { this . _updateOverlaySize ( ) ; } } } private _colspan : CdkPopoverEditColspan = { } ; /** Whether popover edit is disabled for this cell. */ get disabled ( ) : boolean { return this . _disabled ; } set disabled ( value ) { this . _disabled = value ; if ( value ) { this . services . editEventDispatcher . doneEditingCell ( this . elementRef . nativeElement ! ) ; this . services . editEventDispatcher . disabledCells . set ( this . elementRef . nativeElement ! , true ) ; } else { this . services . editEventDispatcher . disabledCells . delete ( this . elementRef . nativeElement ! ) ; } } private _disabled = false ; protected focusTrap ? : FocusTrap ; protected overlayRef ? : OverlayRef ; protected readonly destroyed = new Subject < void > ( ) ; constructor ( protected readonly services , protected readonly elementRef , protected readonly viewContainerRef ) { } ngAfterViewInit ( ) { this . _startListeningToEditEvents ( ) ; } ngOnDestroy ( ) { this . destroyed . next ( ) ; this . destroyed . complete ( ) ; if ( this . focusTrap ) { this . focusTrap . destroy ( ) ; this . focusTrap = undefined ; } if ( this . overlayRef ) { this . overlayRef . dispose ( ) ; } } protected initFocusTrap ( ) { this . focusTrap = this . services . focusTrapFactory . create ( this . overlayRef ! . overlayElement ) ; } protected closeEditOverlay ( ) { this . services . editEventDispatcher . doneEditingCell ( this . elementRef . nativeElement ! ) ; } protected panelClass ( ) { return EDIT_PANE_CLASS ; } private _startListeningToEditEvents ( ) { this . services . editEventDispatcher . editingCell ( this . elementRef . nativeElement ! ) . pipe ( takeUntil ( this . destroyed ) ) . subscribe ( open => { if ( open && this . template ) { if ( ! this . overlayRef ) { this . _createEditOverlay ( ) ; } this . _showEditOverlay ( ) ; } else if ( this . overlayRef ) { this . _maybeReturnFocusToCell ( ) ; this . overlayRef . detach ( ) ; } } ) ; } private _createEditOverlay ( ) { this . overlayRef = this . services . overlay . create ( { disposeOnNavigation : true , panelClass : this . panelClass ( ) , positionStrategy : this . _getPositionStrategy ( ) , scrollStrategy : this . services . overlay . scrollStrategies . reposition ( ) , direction : this . services . directionality , } ) ; this . initFocusTrap ( ) ; this . overlayRef . overlayElement . setAttribute ( 'aria-role' , 'dialog' ) ; this . overlayRef . detachments ( ) . subscribe ( ( ) => this . closeEditOverlay ( ) ) ; } private _showEditOverlay ( ) { this . overlayRef ! . attach ( new TemplatePortal ( this . template ! , this . viewContainerRef , { $implicit : this . context } ) ) ; this . services . ngZone . runOutsideAngular ( ( ) => { setTimeout ( ( ) => { this . focusTrap ! . focusInitialElement ( ) ; } ) ; } ) ; merge ( this . services . scrollDispatcher . scrolled ( ) , this . services . viewportRuler . change ( ) ) . pipe ( startWith ( null ) , takeUntil ( merge ( this . overlayRef ! . detachments ( ) , this . destroyed ) ) ) . subscribe ( ( ) => { this . _updateOverlaySize ( ) ; } ) ; } private _getOverlayCells ( ) { const cell = closest ( this . elementRef . nativeElement ! , CELL_SELECTOR ) as HTMLElement ; if ( ! this . _colspan . before && ! this . _colspan . after ) { return [ cell ] ; } const row = closest ( this . elementRef . nativeElement ! , ROW_SELECTOR ) ! ; const rowCells = Array . from ( row . querySelectorAll ( CELL_SELECTOR ) ) as HTMLElement [ ] ; const ownIndex = rowCells . indexOf ( cell ) ; return rowCells . slice ( ownIndex - ( this . _colspan . before || 0 ) , ownIndex + ( this . _colspan . after || 0 ) + 1 ) ; } private _getPositionStrategy ( ) { return this . services . positionFactory . positionStrategyForCells ( this . _getOverlayCells ( ) ) ; } private _updateOverlaySize ( ) { this . overlayRef ! . updateSize ( this . services . positionFactory . sizeConfigForCells ( this . _getOverlayCells ( ) ) ) ; } private _maybeReturnFocusToCell ( ) { if ( closest ( document . activeElement , EDIT_PANE_SELECTOR ) === this . overlayRef ! . overlayElement ) { this . elementRef . nativeElement ! . focus ( ) ; } } } /**
 * Attaches an ng-template to a cell and shows it when instructed to by the
 * EditEventDispatcher service.
 * Makes the cell focusable.
 */ @ Directive ( { selector : '[cdkPopoverEdit][cdkPopoverEditTabOut]' , host : POPOVER_EDIT_HOST_BINDINGS , inputs : POPOVER_EDIT_INPUTS , } ) export class CdkPopoverEditTabOut < C > extends CdkPopoverEdit < C > { protected override focusTrap ? : FocusEscapeNotifier ; constructor ( elementRef , viewContainerRef , services , protected readonly focusEscapeNotifierFactory ) { super ( services , elementRef , viewContainerRef ) ; } protected override initFocusTrap ( ) { this . focusTrap = this . focusEscapeNotifierFactory . create ( this . overlayRef ! . overlayElement ) ; this . focusTrap . escapes ( ) . pipe ( takeUntil ( this . destroyed ) ) . subscribe ( direction => { if ( this . services . editEventDispatcher . editRef ) { this . services . editEventDispatcher . editRef . blur ( ) ; } this . services . focusDispatcher . moveFocusHorizontally ( closest ( this . elementRef . nativeElement ! , CELL_SELECTOR ) as HTMLElement , direction === FocusEscapeNotifierDirection . START ? - 1 : 1 ) ; this . closeEditOverlay ( ) ; } ) ; } } /**
 * A structural directive that shows its contents when the table row containing
 * it is hovered or when an element in the row has focus.
 */ @ Directive ( { selector : '[cdkRowHoverContent]' , } ) export class CdkRowHoverContent implements AfterViewInit , OnDestroy { protected readonly destroyed = new Subject < void > ( ) ; protected viewRef : EmbeddedViewRef < any > | null = null ; private _row ? : Element ; constructor ( protected readonly services , protected readonly elementRef , protected readonly templateRef , protected readonly viewContainerRef ) { } ngAfterViewInit ( ) { this . _row = closest ( this . elementRef . nativeElement ! , ROW_SELECTOR ) ! ; this . services . editEventDispatcher . registerRowWithHoverContent ( this . _row ) ; this . _listenForHoverAndFocusEvents ( ) ; } ngOnDestroy ( ) { this . destroyed . next ( ) ; this . destroyed . complete ( ) ; if ( this . viewRef ) { this . viewRef . destroy ( ) ; } if ( this . _row ) { this . services . editEventDispatcher . deregisterRowWithHoverContent ( this . _row ) ; } } /**
     * Called immediately after the hover content is created and added to the dom.
     * In the CDK version, this is a noop but subclasses such as MatRowHoverContent use this
     * to prepare/style the inserted element.
     */ protected initElement ( _ ) { } /**
     * Called when the hover content needs to be focusable to preserve a reasonable tab ordering
     * but should not yet be shown.
     */ protected makeElementHiddenButFocusable ( element ) { element . style . opacity = '0' ; } /**
     * Called when the hover content needs to be focusable to preserve a reasonable tab ordering
     * but should not yet be shown.
     */ protected makeElementVisible ( element ) { element . style . opacity = '' ; } private _listenForHoverAndFocusEvents ( ) { this . services . editEventDispatcher . hoverOrFocusOnRow ( this . _row ! ) . pipe ( takeUntil ( this . destroyed ) ) . subscribe ( eventState => { if ( eventState === HoverContentState . ON || eventState === HoverContentState . FOCUSABLE ) { if ( ! this . viewRef ) { this . viewRef = this . viewContainerRef . createEmbeddedView ( this . templateRef , { } ) ; this . initElement ( this . viewRef . rootNodes [ 0 ] as HTMLElement ) ; this . viewRef . markForCheck ( ) ; } else if ( this . viewContainerRef . indexOf ( this . viewRef ) === - 1 ) { this . viewContainerRef . insert ( this . viewRef ! ) ; this . viewRef . markForCheck ( ) ; } if ( eventState === HoverContentState . ON ) { this . makeElementVisible ( this . viewRef . rootNodes [ 0 ] as HTMLElement ) ; } else { this . makeElementHiddenButFocusable ( this . viewRef . rootNodes [ 0 ] as HTMLElement ) ; } } else if ( this . viewRef ) { this . viewContainerRef . detach ( this . viewContainerRef . indexOf ( this . viewRef ) ) ; } } ) ; } } /**
 * Opens the closest edit popover to this element, whether it's associated with this exact
 * element or an ancestor element.
 */ @ Directive ( { selector : '[cdkEditOpen]' , host : { '(click)' : 'openEdit($event)' , } , } ) export class CdkEditOpen { constructor ( protected readonly elementRef , protected readonly editEventDispatcher ) { const nativeElement = elementRef . nativeElement ; if ( nativeElement . nodeName === 'BUTTON' && ! nativeElement . getAttribute ( 'type' ) ) { nativeElement . setAttribute ( 'type' , 'button' ) ; } } openEdit ( evt ) { this . editEventDispatcher . editing . next ( closest ( this . elementRef . nativeElement ! , CELL_SELECTOR ) ) ; evt . stopPropagation ( ) ; } }