/**
 * -------------------------------------------------------------------------------------------
 * Copyright (c) Microsoft Corporation.  All Rights Reserved.  Licensed under the MIT License.
 * See License in the project root for license information.
 * -------------------------------------------------------------------------------------------
 */ import { customElement , html , property , PropertyValues , TemplateResult } from 'lit-element' ; import { classMap } from 'lit-html/directives/class-map' ; import { getSegmentAwareWindow , isWindowSegmentAware , IWindowSegment } from '../../../utils/WindowSegmentHelpers' ; import { styles } from './mgt-flyout-css' ; import { MgtBaseComponent } from '@microsoft/mgt-element/' ; export class extends LitElement @ customElement ( 'mgt-flyout' ) export class MgtFlyout extends MgtBaseComponent { /**
     * Array of styles to apply to the element. The styles should be defined
     * using the `css` tag function.
     */ static get styles ( ) { return styles ; } type { boolean } memberof @ property ( { attribute : 'light-dismiss' , type : Boolean } ) public isLightDismiss : boolean ; type { boolean } memberof @ property ( { attribute : null , type : Boolean } ) public avoidHidingAnchor : boolean ; type { string } memberof @ property ( { attribute : 'isOpen' , type : Boolean } ) public get isOpen ( ) : boolean { return this . _isOpen ; } public set isOpen ( value ) { const oldValue = this . _isOpen ; if ( oldValue === value ) { return ; } this . _isOpen = value ; window . requestAnimationFrame ( ( ) => { this . setupWindowEvents ( this . isOpen ) ; const flyout = this . _flyout ; if ( ! this . isOpen && flyout ) { flyout . style . width = null ; flyout . style . setProperty ( '--mgt-flyout-set-width' , null ) ; flyout . style . setProperty ( '--mgt-flyout-set-height' , null ) ; flyout . style . maxHeight = null ; flyout . style . top = null ; flyout . style . left = null ; flyout . style . bottom = null ; } } ) ; this . requestUpdate ( 'isOpen' , oldValue ) ; this . dispatchEvent ( new Event ( value ? 'opened' : 'closed' ) ) ; } private _edgePadding : number = 24 ; private _renderedOnce = false ; private get _flyout ( ) : HTMLElement { return this . renderRoot . querySelector ( '.flyout' ) ; } private get _anchor ( ) : HTMLElement { return this . renderRoot . querySelector ( '.anchor' ) ; } private get _topScout ( ) : HTMLElement { return this . renderRoot . querySelector ( '.scout-top' ) ; } private get _bottomScout ( ) : HTMLElement { return this . renderRoot . querySelector ( '.scout-bottom' ) ; } private _isOpen : boolean ; constructor ( ) { super ( ) ; this . avoidHidingAnchor = true ; this . handleWindowEvent = this . handleWindowEvent . bind ( this ) ; this . handleResize = this . handleResize . bind ( this ) ; this . handleKeyUp = this . handleKeyUp . bind ( this ) ; this . addEventListener ( 'expanded' , ( ) => { window . requestAnimationFrame ( ( ) => { this . updateFlyout ( ) ; } ) ; } ) ; } /**
     * Show the flyout.
     */ public open ( ) { this . isOpen = true ; } /**
     * Close the flyout.
     */ public close ( ) { this . isOpen = false ; } memberof public disconnectedCallback ( ) { this . setupWindowEvents ( false ) ; super . disconnectedCallback ( ) ; } param changedProperties protected updated ( changedProps ) { super . updated ( changedProps ) ; window . requestAnimationFrame ( ( ) => { this . updateFlyout ( ) ; } ) ; } /**
     * Invoked on each update to perform rendering tasks. This method must return
     * a lit-html TemplateResult. Setting properties inside this method will *not*
     * trigger the element to update.
     */ protected render ( ) { const flyoutClasses = { dir : this . direction , root : true , visible : this . isOpen } ; const anchorTemplate = this . renderAnchor ( ) ; let flyoutTemplate = null ; if ( this . isOpen || this . _renderedOnce ) { this . _renderedOnce = true ; flyoutTemplate = html `
        <div class="flyout" @wheel=${ this . handleFlyoutWheel }>
          ${ this . renderFlyout ( ) }
        </div>
      ` ; } return html `
      <div class=${ classMap ( flyoutClasses ) }>
        <div class="anchor">
          ${ anchorTemplate }
        </div>
        <div class="scout-top"></div>
        <div class="scout-bottom"></div>
        ${ flyoutTemplate }
      </div>
    ` ; } protected returns memberof protected renderAnchor ( ) { return html `
      <slot></slot>
    ` ; } /**
     * Renders the flyout.
     */ protected renderFlyout ( ) { return html `
      <slot name="flyout"></slot>
    ` ; } private updateFlyout ( ) { if ( ! this . isOpen ) { return ; } const anchor = this . _anchor ; const flyout = this . _flyout ; if ( flyout && anchor ) { const windowWidth = window . innerWidth && document . documentElement . clientWidth ? Math . min ( window . innerWidth , document . documentElement . clientWidth ) : window . innerWidth || document . documentElement . clientWidth ; const windowHeight = window . innerHeight && document . documentElement . clientHeight ? Math . min ( window . innerHeight , document . documentElement . clientHeight ) : window . innerHeight || document . documentElement . clientHeight ; let left = 0 ; let bottom ; let top = 0 ; let height ; let width ; const flyoutRect = flyout . getBoundingClientRect ( ) ; const anchorRect = anchor . getBoundingClientRect ( ) ; const topScoutRect = this . _topScout . getBoundingClientRect ( ) ; const bottomScoutRect = this . _bottomScout . getBoundingClientRect ( ) ; const windowRect = { height : windowHeight , left : 0 , top : 0 , width : windowWidth } ; if ( isWindowSegmentAware ( ) ) { const segmentAwareWindow = getSegmentAwareWindow ( ) ; const screenSegments = segmentAwareWindow . getWindowSegments ( ) ; let anchorSegment ; const anchorCenterX = anchorRect . left + anchorRect . width / 2 ; const anchorCenterY = anchorRect . top + anchorRect . height / 2 ; for ( const segment of screenSegments ) { if ( anchorCenterX >= segment . left && anchorCenterY >= segment . top ) { anchorSegment = segment ; break ; } } if ( anchorSegment ) { windowRect . left = anchorSegment . left ; windowRect . top = anchorSegment . top ; windowRect . width = anchorSegment . width ; windowRect . height = anchorSegment . height ; } } if ( flyoutRect . width + 2 * this . _edgePadding > windowRect . width ) { if ( flyoutRect . width > windowRect . width ) { width = windowRect . width ; left = 0 ; } else { left = ( windowRect . width - flyoutRect . width ) / 2 ; } } else if ( anchorRect . left + flyoutRect . width + this . _edgePadding > windowRect . width ) { left = anchorRect . left - ( anchorRect . left + flyoutRect . width + this . _edgePadding - windowRect . width ) ; } else if ( anchorRect . left < this . _edgePadding ) { left = this . _edgePadding ; } else { left = anchorRect . left ; } const anchorRectBottomToWindowBottom = windowRect . height - ( anchorRect . top + anchorRect . height ) ; const anchorRectTopToWindowTop = anchorRect . top ; if ( this . avoidHidingAnchor ) { if ( anchorRectBottomToWindowBottom <= flyoutRect . height ) { if ( anchorRectTopToWindowTop < flyoutRect . height ) { if ( anchorRectTopToWindowTop > anchorRectBottomToWindowBottom ) { bottom = windowRect . height - anchorRect . top ; height = anchorRectTopToWindowTop ; } else { top = anchorRect . bottom ; height = anchorRectBottomToWindowBottom ; } } else { bottom = windowRect . height - anchorRect . top ; height = anchorRectTopToWindowTop ; } } else { top = anchorRect . bottom ; height = anchorRectBottomToWindowBottom ; } } else { if ( flyoutRect . height + 2 * this . _edgePadding > windowRect . height ) { if ( flyoutRect . height >= windowRect . height ) { height = windowRect . height ; top = 0 ; } else { top = ( windowRect . height - flyoutRect . height ) / 2 ; } } else { if ( anchorRect . top + anchorRect . height + flyoutRect . height + this . _edgePadding > windowRect . height ) { top = windowRect . height - flyoutRect . height - this . _edgePadding ; } else { top = Math . max ( anchorRect . top + anchorRect . height , this . _edgePadding ) ; } } } if ( topScoutRect . top !== 0 || topScoutRect . left !== 0 ) { left -= topScoutRect . left ; if ( typeof bottom !== 'undefined' ) { bottom += bottomScoutRect . top - windowHeight ; } else { top -= topScoutRect . top ; } } if ( this . direction === 'rtl' ) { if ( left > 100 && this . offsetLeft > 100 ) { flyout . style . left = `${ windowRect . width - left + flyoutRect . left - flyoutRect . width - 30 }px` ; } } else { flyout . style . left = `${ left + windowRect . left }px` ; } if ( typeof bottom !== 'undefined' ) { flyout . style . top = 'unset' ; flyout . style . bottom = `${ bottom }px` ; } else { flyout . style . bottom = 'unset' ; flyout . style . top = `${ top + windowRect . top }px` ; } if ( width ) { flyout . style . width = `${ width }px` ; flyout . style . setProperty ( '--mgt-flyout-set-width' , `${ width }px` ) ; window . requestAnimationFrame ( ( ) => this . updateFlyout ( ) ) ; } if ( height ) { flyout . style . maxHeight = `${ height }px` ; flyout . style . setProperty ( '--mgt-flyout-set-height' , `${ height }px` ) ; } else { flyout . style . maxHeight = null ; flyout . style . setProperty ( '--mgt-flyout-set-height' , `unset` ) ; } } } private setupWindowEvents ( isOpen ) { if ( isOpen && this . isLightDismiss ) { window . addEventListener ( 'wheel' , this . handleWindowEvent ) ; window . addEventListener ( 'pointerdown' , this . handleWindowEvent ) ; window . addEventListener ( 'resize' , this . handleResize ) ; window . addEventListener ( 'keyup' , this . handleKeyUp ) ; } else { window . removeEventListener ( 'wheel' , this . handleWindowEvent ) ; window . removeEventListener ( 'pointerdown' , this . handleWindowEvent ) ; window . removeEventListener ( 'resize' , this . handleResize ) ; window . removeEventListener ( 'keyup' , this . handleKeyUp ) ; } } private handleWindowEvent ( e ) { const flyout = this . _flyout ; if ( flyout ) { if ( ! e . composedPath ) { let currentElem = e . target as HTMLElement ; while ( currentElem ) { currentElem = currentElem . parentElement ; if ( currentElem === flyout || ( e . type === 'pointerdown' && currentElem === this ) ) { return ; } } } else { const path = e . composedPath ( ) ; if ( path . includes ( flyout ) || ( e . type === 'pointerdown' && path . includes ( this ) ) ) { return ; } } } this . close ( ) ; } private handleResize ( e ) { this . close ( ) ; } private handleKeyUp ( e ) { if ( e . key === 'Escape' ) { this . close ( ) ; } } private handleFlyoutWheel ( e ) { e . preventDefault ( ) ; } }