license import { PositionStrategy } from './position-strategy' ; import { ElementRef } from '@angular/core' ; import { ViewportRuler , CdkScrollable , ViewportScrollPosition } from '@angular/cdk/scrolling' ; import { ConnectedOverlayPositionChange , ConnectionPositionPair , ScrollingVisibility , validateHorizontalPosition , validateVerticalPosition , } from './connected-position' ; import { Observable , Subscription , Subject } from 'rxjs' ; import { OverlayReference } from '../overlay-reference' ; import { isElementScrolledOutsideView , isElementClippedByScrolling } from './scroll-clip' ; import { coerceCssPixelValue , coerceArray } from '@angular/cdk/coercion' ; import { Platform } from '@angular/cdk/platform' ; import { OverlayContainer } from '../overlay-container' ; /** Class to be added to the overlay bounding box. */ const boundingBoxClass = 'cdk-overlay-connected-position-bounding-box' ; /** Regex used to split a string on its CSS units. */ const cssUnitPattern = /([A-Za-z%]+)$/ ; /** Possible values that can be set as the origin of a FlexibleConnectedPositionStrategy. */ export type FlexibleConnectedPositionStrategyOrigin = ElementRef | Element | ( Point & { width ? : number ; height ? : number ; } ) ; /** Equivalent of `ClientRect` without some of the properties we don't care about. */ type Dimensions = Omit < ClientRect , 'x' | 'y' | 'toJSON' > ; /**
 * A strategy for positioning overlays. Using this strategy, an overlay is given an
 * implicit position relative some origin element. The relative position is defined in terms of
 * a point on the origin element that is connected to a point on the overlay element. For example,
 * a basic dropdown is connecting the bottom-left corner of the origin to the top-left corner
 * of the overlay.
 */ export class FlexibleConnectedPositionStrategy implements PositionStrategy { /** The overlay to which this strategy is attached. */ private _overlayRef : OverlayReference ; /** Whether we're performing the very first positioning of the overlay. */ private _isInitialRender : boolean ; /** Last size used for the bounding box. Used to avoid resizing the overlay after open. */ private _lastBoundingBoxSize = { width : 0 , height : 0 } ; /** Whether the overlay was pushed in a previous positioning. */ private _isPushed = false ; /** Whether the overlay can be pushed on-screen on the initial open. */ private _canPush = true ; /** Whether the overlay can grow via flexible width/height after the initial open. */ private _growAfterOpen = false ; /** Whether the overlay's width and height can be constrained to fit within the viewport. */ private _hasFlexibleDimensions = true ; /** Whether the overlay position is locked. */ private _positionLocked = false ; /** Cached origin dimensions */ private _originRect : Dimensions ; /** Cached overlay dimensions */ private _overlayRect : Dimensions ; /** Cached viewport dimensions */ private _viewportRect : Dimensions ; /** Cached container dimensions */ private _containerRect : Dimensions ; /** Amount of space that must be maintained between the overlay and the edge of the viewport. */ private _viewportMargin = 0 ; /** The Scrollable containers used to check scrollable view properties on position change. */ private _scrollables : CdkScrollable [ ] = [ ] ; /** Ordered list of preferred positions, from most to least desirable. */ _preferredPositions : ConnectionPositionPair [ ] = [ ] ; /** The origin element against which the overlay will be positioned. */ private _origin : FlexibleConnectedPositionStrategyOrigin ; /** The overlay pane element. */ private _pane : HTMLElement ; /** Whether the strategy has been disposed of already. */ private _isDisposed : boolean ; /**
     * Parent element for the overlay panel used to constrain the overlay panel's size to fit
     * within the viewport.
     */ private _boundingBox : HTMLElement | null ; /** The last position to have been calculated as the best fit position. */ private _lastPosition : ConnectedPosition | null ; /** Subject that emits whenever the position changes. */ private readonly _positionChanges = new Subject < ConnectedOverlayPositionChange > ( ) ; /** Subscription to viewport size changes. */ private _resizeSubscription = Subscription . EMPTY ; /** Default offset for the overlay along the x axis. */ private _offsetX = 0 ; /** Default offset for the overlay along the y axis. */ private _offsetY = 0 ; /** Selector to be used when finding the elements on which to set the transform origin. */ private _transformOriginSelector : string ; /** Keeps track of the CSS classes that the position strategy has applied on the overlay panel. */ private _appliedPanelClasses : string [ ] = [ ] ; /** Amount by which the overlay was pushed in each axis during the last time it was positioned. */ private _previousPushAmount : { x : number ; y : number ; } | null ; /** Observable sequence of position changes. */ positionChanges : Observable < ConnectedOverlayPositionChange > = this . _positionChanges ; /** Ordered list of preferred positions, from most to least desirable. */ get positions ( ) : ConnectionPositionPair [ ] { return this . _preferredPositions ; } constructor ( connectedTo , private _viewportRuler , private _document , private _platform , private _overlayContainer ) { this . setOrigin ( connectedTo ) ; } /** Attaches this position strategy to an overlay. */ attach ( overlayRef ) { if ( this . _overlayRef && overlayRef !== this . _overlayRef && ( typeof ngDevMode === 'undefined' || ngDevMode ) ) { throw Error ( 'This position strategy is already attached to an overlay' ) ; } this . _validatePositions ( ) ; overlayRef . hostElement . classList . add ( boundingBoxClass ) ; this . _overlayRef = overlayRef ; this . _boundingBox = overlayRef . hostElement ; this . _pane = overlayRef . overlayElement ; this . _isDisposed = false ; this . _isInitialRender = true ; this . _lastPosition = null ; this . _resizeSubscription . unsubscribe ( ) ; this . _resizeSubscription = this . _viewportRuler . change ( ) . subscribe ( ( ) => { this . _isInitialRender = true ; this . apply ( ) ; } ) ; } docs-private apply ( ) { if ( this . _isDisposed || ! this . _platform . isBrowser ) { return ; } if ( ! this . _isInitialRender && this . _positionLocked && this . _lastPosition ) { this . reapplyLastPosition ( ) ; return ; } this . _clearPanelClasses ( ) ; this . _resetOverlayElementStyles ( ) ; this . _resetBoundingBoxStyles ( ) ; this . _viewportRect = this . _getNarrowedViewportRect ( ) ; this . _originRect = this . _getOriginRect ( ) ; this . _overlayRect = this . _pane . getBoundingClientRect ( ) ; this . _containerRect = this . _overlayContainer . getContainerElement ( ) . getBoundingClientRect ( ) ; const originRect = this . _originRect ; const overlayRect = this . _overlayRect ; const viewportRect = this . _viewportRect ; const containerRect = this . _containerRect ; const flexibleFits = [ ] ; let fallback ; for ( let pos of this . _preferredPositions ) { let originPoint = this . _getOriginPoint ( originRect , containerRect , pos ) ; let overlayPoint = this . _getOverlayPoint ( originPoint , overlayRect , pos ) ; let overlayFit = this . _getOverlayFit ( overlayPoint , overlayRect , viewportRect , pos ) ; if ( overlayFit . isCompletelyWithinViewport ) { this . _isPushed = false ; this . _applyPosition ( pos , originPoint ) ; return ; } if ( this . _canFitWithFlexibleDimensions ( overlayFit , overlayPoint , viewportRect ) ) { flexibleFits . push ( { position : pos , origin : originPoint , overlayRect , boundingBoxRect : this . _calculateBoundingBoxRect ( originPoint , pos ) , } ) ; continue ; } if ( ! fallback || fallback . overlayFit . visibleArea < overlayFit . visibleArea ) { fallback = { overlayFit , overlayPoint , originPoint , position : pos , overlayRect } ; } } if ( flexibleFits . length ) { let bestFit = null ; let bestScore = - 1 ; for ( const fit of flexibleFits ) { const score = fit . boundingBoxRect . width * fit . boundingBoxRect . height * ( fit . position . weight || 1 ) ; if ( score > bestScore ) { bestScore = score ; bestFit = fit ; } } this . _isPushed = false ; this . _applyPosition ( bestFit ! . position , bestFit ! . origin ) ; return ; } if ( this . _canPush ) { this . _isPushed = true ; this . _applyPosition ( fallback ! . position , fallback ! . originPoint ) ; return ; } this . _applyPosition ( fallback ! . position , fallback ! . originPoint ) ; } detach ( ) { this . _clearPanelClasses ( ) ; this . _lastPosition = null ; this . _previousPushAmount = null ; this . _resizeSubscription . unsubscribe ( ) ; } /** Cleanup after the element gets destroyed. */ dispose ( ) { if ( this . _isDisposed ) { return ; } if ( this . _boundingBox ) { extendStyles ( this . _boundingBox . style , { top : '' , left : '' , right : '' , bottom : '' , height : '' , width : '' , alignItems : '' , justifyContent : '' , } as CSSStyleDeclaration ) ; } if ( this . _pane ) { this . _resetOverlayElementStyles ( ) ; } if ( this . _overlayRef ) { this . _overlayRef . hostElement . classList . remove ( boundingBoxClass ) ; } this . detach ( ) ; this . _positionChanges . complete ( ) ; this . _overlayRef = this . _boundingBox = null ! ; this . _isDisposed = true ; } /**
     * This re-aligns the overlay element with the trigger in its last calculated position,
     * even if a position higher in the "preferred positions" list would now fit. This
     * allows one to re-align the panel without changing the orientation of the panel.
     */ reapplyLastPosition ( ) { if ( ! this . _isDisposed && ( ! this . _platform || this . _platform . isBrowser ) ) { this . _originRect = this . _getOriginRect ( ) ; this . _overlayRect = this . _pane . getBoundingClientRect ( ) ; this . _viewportRect = this . _getNarrowedViewportRect ( ) ; this . _containerRect = this . _overlayContainer . getContainerElement ( ) . getBoundingClientRect ( ) ; const lastPosition = this . _lastPosition || this . _preferredPositions [ 0 ] ; const originPoint = this . _getOriginPoint ( this . _originRect , this . _containerRect , lastPosition ) ; this . _applyPosition ( lastPosition , originPoint ) ; } } /**
     * Sets the list of Scrollable containers that host the origin element so that
     * on reposition we can evaluate if it or the overlay has been clipped or outside view. Every
     * Scrollable must be an ancestor element of the strategy's origin element.
     */ withScrollableContainers ( scrollables ) { this . _scrollables = scrollables ; return this ; } param positions withPositions ( positions ) { this . _preferredPositions = positions ; if ( positions . indexOf ( this . _lastPosition ! ) === - 1 ) { this . _lastPosition = null ; } this . _validatePositions ( ) ; return this ; } param margin withViewportMargin ( margin ) { this . _viewportMargin = margin ; return this ; } /** Sets whether the overlay's width and height can be constrained to fit within the viewport. */ withFlexibleDimensions ( flexibleDimensions = true ) { this . _hasFlexibleDimensions = flexibleDimensions ; return this ; } /** Sets whether the overlay can grow after the initial open via flexible width/height. */ withGrowAfterOpen ( growAfterOpen = true ) { this . _growAfterOpen = growAfterOpen ; return this ; } /** Sets whether the overlay can be pushed on-screen if none of the provided positions fit. */ withPush ( canPush = true ) { this . _canPush = canPush ; return this ; } param isLocked withLockedPosition ( isLocked = true ) { this . _positionLocked = isLocked ; return this ; } param origin setOrigin ( origin ) { this . _origin = origin ; return this ; } param offset withDefaultOffsetX ( offset ) { this . _offsetX = offset ; return this ; } param offset withDefaultOffsetY ( offset ) { this . _offsetY = offset ; return this ; } param selector withTransformOriginOn ( selector ) { this . _transformOriginSelector = selector ; return this ; } /**
     * Gets the (x, y) coordinate of a connection point on the origin based on a relative position.
     */ private _getOriginPoint ( originRect , containerRect , pos ) { let x ; if ( pos . originX == 'center' ) { x = originRect . left + originRect . width / 2 ; } else { const startX = this . _isRtl ( ) ? originRect . right : originRect . left ; const endX = this . _isRtl ( ) ? originRect . left : originRect . right ; x = pos . originX == 'start' ? startX : endX ; } if ( containerRect . left < 0 ) { x -= containerRect . left ; } let y ; if ( pos . originY == 'center' ) { y = originRect . top + originRect . height / 2 ; } else { y = pos . originY == 'top' ? originRect . top : originRect . bottom ; } if ( containerRect . top < 0 ) { y -= containerRect . top ; } return { x , y } ; } /**
     * Gets the (x, y) coordinate of the top-left corner of the overlay given a given position and
     * origin point to which the overlay should be connected.
     */ private _getOverlayPoint ( originPoint , overlayRect , pos ) { let overlayStartX ; if ( pos . overlayX == 'center' ) { overlayStartX = - overlayRect . width / 2 ; } else if ( pos . overlayX === 'start' ) { overlayStartX = this . _isRtl ( ) ? - overlayRect . width : 0 ; } else { overlayStartX = this . _isRtl ( ) ? 0 : - overlayRect . width ; } let overlayStartY ; if ( pos . overlayY == 'center' ) { overlayStartY = - overlayRect . height / 2 ; } else { overlayStartY = pos . overlayY == 'top' ? 0 : - overlayRect . height ; } return { x : originPoint . x + overlayStartX , y : originPoint . y + overlayStartY , } ; } /** Gets how well an overlay at the given point will fit within the viewport. */ private _getOverlayFit ( point , rawOverlayRect , viewport , position ) { const overlay = getRoundedBoundingClientRect ( rawOverlayRect ) ; let { x , y } = point ; let offsetX = this . _getOffset ( position , 'x' ) ; let offsetY = this . _getOffset ( position , 'y' ) ; if ( offsetX ) { x += offsetX ; } if ( offsetY ) { y += offsetY ; } let leftOverflow = 0 - x ; let rightOverflow = x + overlay . width - viewport . width ; let topOverflow = 0 - y ; let bottomOverflow = y + overlay . height - viewport . height ; let visibleWidth = this . _subtractOverflows ( overlay . width , leftOverflow , rightOverflow ) ; let visibleHeight = this . _subtractOverflows ( overlay . height , topOverflow , bottomOverflow ) ; let visibleArea = visibleWidth * visibleHeight ; return { visibleArea , isCompletelyWithinViewport : overlay . width * overlay . height === visibleArea , fitsInViewportVertically : visibleHeight === overlay . height , fitsInViewportHorizontally : visibleWidth == overlay . width , } ; } param fit param point param viewport private _canFitWithFlexibleDimensions ( fit , point , viewport ) { if ( this . _hasFlexibleDimensions ) { const availableHeight = viewport . bottom - point . y ; const availableWidth = viewport . right - point . x ; const minHeight = getPixelValue ( this . _overlayRef . getConfig ( ) . minHeight ) ; const minWidth = getPixelValue ( this . _overlayRef . getConfig ( ) . minWidth ) ; const verticalFit = fit . fitsInViewportVertically || ( minHeight != null && minHeight <= availableHeight ) ; const horizontalFit = fit . fitsInViewportHorizontally || ( minWidth != null && minWidth <= availableWidth ) ; return verticalFit && horizontalFit ; } return false ; } param start param rawOverlayRect param scrollPosition returns private _pushOverlayOnScreen ( start , rawOverlayRect , scrollPosition ) { if ( this . _previousPushAmount && this . _positionLocked ) { return { x : start . x + this . _previousPushAmount . x , y : start . y + this . _previousPushAmount . y , } ; } const overlay = getRoundedBoundingClientRect ( rawOverlayRect ) ; const viewport = this . _viewportRect ; const overflowRight = Math . max ( start . x + overlay . width - viewport . width , 0 ) ; const overflowBottom = Math . max ( start . y + overlay . height - viewport . height , 0 ) ; const overflowTop = Math . max ( viewport . top - scrollPosition . top - start . y , 0 ) ; const overflowLeft = Math . max ( viewport . left - scrollPosition . left - start . x , 0 ) ; let pushX = 0 ; let pushY = 0 ; if ( overlay . width <= viewport . width ) { pushX = overflowLeft || - overflowRight ; } else { pushX = start . x < this . _viewportMargin ? viewport . left - scrollPosition . left - start . x : 0 ; } if ( overlay . height <= viewport . height ) { pushY = overflowTop || - overflowBottom ; } else { pushY = start . y < this . _viewportMargin ? viewport . top - scrollPosition . top - start . y : 0 ; } this . _previousPushAmount = { x : pushX , y : pushY } ; return { x : start . x + pushX , y : start . y + pushY , } ; } param position param originPoint private _applyPosition ( position , originPoint ) { this . _setTransformOrigin ( position ) ; this . _setOverlayElementStyles ( originPoint , position ) ; this . _setBoundingBoxStyles ( originPoint , position ) ; if ( position . panelClass ) { this . _addPanelClasses ( position . panelClass ) ; } this . _lastPosition = position ; if ( this . _positionChanges . observers . length ) { const scrollableViewProperties = this . _getScrollVisibility ( ) ; const changeEvent = new ConnectedOverlayPositionChange ( position , scrollableViewProperties ) ; this . _positionChanges . next ( changeEvent ) ; } this . _isInitialRender = false ; } /** Sets the transform origin based on the configured selector and the passed-in position.  */ private _setTransformOrigin ( position ) { if ( ! this . _transformOriginSelector ) { return ; } const elements = this . _boundingBox ! . querySelectorAll ( this . _transformOriginSelector ) ; let xOrigin ; let yOrigin = position . overlayY ; if ( position . overlayX === 'center' ) { xOrigin = 'center' ; } else if ( this . _isRtl ( ) ) { xOrigin = position . overlayX === 'start' ? 'right' : 'left' ; } else { xOrigin = position . overlayX === 'start' ? 'left' : 'right' ; } for ( let i = 0 ; i < elements . length ; i ++ ) { elements [ i ] . style . transformOrigin = `${ xOrigin } ${ yOrigin }` ; } } /**
     * Gets the position and size of the overlay's sizing container.
     *
     * This method does no measuring and applies no styles so that we can cheaply compute the
     * bounds for all positions and choose the best fit based on these results.
     */ private _calculateBoundingBoxRect ( origin , position ) { const viewport = this . _viewportRect ; const isRtl = this . _isRtl ( ) ; let height , top , bottom ; if ( position . overlayY === 'top' ) { top = origin . y ; height = viewport . height - top + this . _viewportMargin ; } else if ( position . overlayY === 'bottom' ) { bottom = viewport . height - origin . y + this . _viewportMargin * 2 ; height = viewport . height - bottom + this . _viewportMargin ; } else { const smallestDistanceToViewportEdge = Math . min ( viewport . bottom - origin . y + viewport . top , origin . y ) ; const previousHeight = this . _lastBoundingBoxSize . height ; height = smallestDistanceToViewportEdge * 2 ; top = origin . y - smallestDistanceToViewportEdge ; if ( height > previousHeight && ! this . _isInitialRender && ! this . _growAfterOpen ) { top = origin . y - previousHeight / 2 ; } } const isBoundedByRightViewportEdge = ( position . overlayX === 'start' && ! isRtl ) || ( position . overlayX === 'end' && isRtl ) ; const isBoundedByLeftViewportEdge = ( position . overlayX === 'end' && ! isRtl ) || ( position . overlayX === 'start' && isRtl ) ; let width , left , right ; if ( isBoundedByLeftViewportEdge ) { right = viewport . width - origin . x + this . _viewportMargin ; width = origin . x - this . _viewportMargin ; } else if ( isBoundedByRightViewportEdge ) { left = origin . x ; width = viewport . right - origin . x ; } else { const smallestDistanceToViewportEdge = Math . min ( viewport . right - origin . x + viewport . left , origin . x ) ; const previousWidth = this . _lastBoundingBoxSize . width ; width = smallestDistanceToViewportEdge * 2 ; left = origin . x - smallestDistanceToViewportEdge ; if ( width > previousWidth && ! this . _isInitialRender && ! this . _growAfterOpen ) { left = origin . x - previousWidth / 2 ; } } return { top : top ! , left : left ! , bottom : bottom ! , right : right ! , width , height } ; } param origin param position private _setBoundingBoxStyles ( origin , position ) { const boundingBoxRect = this . _calculateBoundingBoxRect ( origin , position ) ; if ( ! this . _isInitialRender && ! this . _growAfterOpen ) { boundingBoxRect . height = Math . min ( boundingBoxRect . height , this . _lastBoundingBoxSize . height ) ; boundingBoxRect . width = Math . min ( boundingBoxRect . width , this . _lastBoundingBoxSize . width ) ; } const styles = { } as CSSStyleDeclaration ; if ( this . _hasExactPosition ( ) ) { styles . top = styles . left = '0' ; styles . bottom = styles . right = styles . maxHeight = styles . maxWidth = '' ; styles . width = styles . height = '100%' ; } else { const maxHeight = this . _overlayRef . getConfig ( ) . maxHeight ; const maxWidth = this . _overlayRef . getConfig ( ) . maxWidth ; styles . height = coerceCssPixelValue ( boundingBoxRect . height ) ; styles . top = coerceCssPixelValue ( boundingBoxRect . top ) ; styles . bottom = coerceCssPixelValue ( boundingBoxRect . bottom ) ; styles . width = coerceCssPixelValue ( boundingBoxRect . width ) ; styles . left = coerceCssPixelValue ( boundingBoxRect . left ) ; styles . right = coerceCssPixelValue ( boundingBoxRect . right ) ; if ( position . overlayX === 'center' ) { styles . alignItems = 'center' ; } else { styles . alignItems = position . overlayX === 'end' ? 'flex-end' : 'flex-start' ; } if ( position . overlayY === 'center' ) { styles . justifyContent = 'center' ; } else { styles . justifyContent = position . overlayY === 'bottom' ? 'flex-end' : 'flex-start' ; } if ( maxHeight ) { styles . maxHeight = coerceCssPixelValue ( maxHeight ) ; } if ( maxWidth ) { styles . maxWidth = coerceCssPixelValue ( maxWidth ) ; } } this . _lastBoundingBoxSize = boundingBoxRect ; extendStyles ( this . _boundingBox ! . style , styles ) ; } /** Resets the styles for the bounding box so that a new positioning can be computed. */ private _resetBoundingBoxStyles ( ) { extendStyles ( this . _boundingBox ! . style , { top : '0' , left : '0' , right : '0' , bottom : '0' , height : '' , width : '' , alignItems : '' , justifyContent : '' , } as CSSStyleDeclaration ) ; } /** Resets the styles for the overlay pane so that a new positioning can be computed. */ private _resetOverlayElementStyles ( ) { extendStyles ( this . _pane . style , { top : '' , left : '' , bottom : '' , right : '' , position : '' , transform : '' , } as CSSStyleDeclaration ) ; } /** Sets positioning styles to the overlay element. */ private _setOverlayElementStyles ( originPoint , position ) { const styles = { } as CSSStyleDeclaration ; const hasExactPosition = this . _hasExactPosition ( ) ; const hasFlexibleDimensions = this . _hasFlexibleDimensions ; const config = this . _overlayRef . getConfig ( ) ; if ( hasExactPosition ) { const scrollPosition = this . _viewportRuler . getViewportScrollPosition ( ) ; extendStyles ( styles , this . _getExactOverlayY ( position , originPoint , scrollPosition ) ) ; extendStyles ( styles , this . _getExactOverlayX ( position , originPoint , scrollPosition ) ) ; } else { styles . position = 'static' ; } let transformString = '' ; let offsetX = this . _getOffset ( position , 'x' ) ; let offsetY = this . _getOffset ( position , 'y' ) ; if ( offsetX ) { transformString += `translateX(${ offsetX }px) ` ; } if ( offsetY ) { transformString += `translateY(${ offsetY }px)` ; } styles . transform = transformString . trim ( ) ; if ( config . maxHeight ) { if ( hasExactPosition ) { styles . maxHeight = coerceCssPixelValue ( config . maxHeight ) ; } else if ( hasFlexibleDimensions ) { styles . maxHeight = '' ; } } if ( config . maxWidth ) { if ( hasExactPosition ) { styles . maxWidth = coerceCssPixelValue ( config . maxWidth ) ; } else if ( hasFlexibleDimensions ) { styles . maxWidth = '' ; } } extendStyles ( this . _pane . style , styles ) ; } /** Gets the exact top/bottom for the overlay when not using flexible sizing or when pushing. */ private _getExactOverlayY ( position , originPoint , scrollPosition ) { let styles = { top : '' , bottom : '' } as CSSStyleDeclaration ; let overlayPoint = this . _getOverlayPoint ( originPoint , this . _overlayRect , position ) ; if ( this . _isPushed ) { overlayPoint = this . _pushOverlayOnScreen ( overlayPoint , this . _overlayRect , scrollPosition ) ; } if ( position . overlayY === 'bottom' ) { const documentHeight = this . _document . documentElement ! . clientHeight ; styles . bottom = `${ documentHeight - ( overlayPoint . y + this . _overlayRect . height ) }px` ; } else { styles . top = coerceCssPixelValue ( overlayPoint . y ) ; } return styles ; } /** Gets the exact left/right for the overlay when not using flexible sizing or when pushing. */ private _getExactOverlayX ( position , originPoint , scrollPosition ) { let styles = { left : '' , right : '' } as CSSStyleDeclaration ; let overlayPoint = this . _getOverlayPoint ( originPoint , this . _overlayRect , position ) ; if ( this . _isPushed ) { overlayPoint = this . _pushOverlayOnScreen ( overlayPoint , this . _overlayRect , scrollPosition ) ; } let horizontalStyleProperty ; if ( this . _isRtl ( ) ) { horizontalStyleProperty = position . overlayX === 'end' ? 'left' : 'right' ; } else { horizontalStyleProperty = position . overlayX === 'end' ? 'right' : 'left' ; } if ( horizontalStyleProperty === 'right' ) { const documentWidth = this . _document . documentElement ! . clientWidth ; styles . right = `${ documentWidth - ( overlayPoint . x + this . _overlayRect . width ) }px` ; } else { styles . left = coerceCssPixelValue ( overlayPoint . x ) ; } return styles ; } /**
     * Gets the view properties of the trigger and overlay, including whether they are clipped
     * or completely outside the view of any of the strategy's scrollables.
     */ private _getScrollVisibility ( ) { const originBounds = this . _getOriginRect ( ) ; const overlayBounds = this . _pane . getBoundingClientRect ( ) ; const scrollContainerBounds = this . _scrollables . map ( scrollable => { return scrollable . getElementRef ( ) . nativeElement . getBoundingClientRect ( ) ; } ) ; return { isOriginClipped : isElementClippedByScrolling ( originBounds , scrollContainerBounds ) , isOriginOutsideView : isElementScrolledOutsideView ( originBounds , scrollContainerBounds ) , isOverlayClipped : isElementClippedByScrolling ( overlayBounds , scrollContainerBounds ) , isOverlayOutsideView : isElementScrolledOutsideView ( overlayBounds , scrollContainerBounds ) , } ; } /** Subtracts the amount that an element is overflowing on an axis from its length. */ private _subtractOverflows ( length , ... overflows ) { return overflows . reduce ( ( currentValue , currentOverflow ) => { return currentValue - Math . max ( currentOverflow , 0 ) ; } , length ) ; } /** Narrows the given viewport rect by the current _viewportMargin. */ private _getNarrowedViewportRect ( ) { const width = this . _document . documentElement ! . clientWidth ; const height = this . _document . documentElement ! . clientHeight ; const scrollPosition = this . _viewportRuler . getViewportScrollPosition ( ) ; return { top : scrollPosition . top + this . _viewportMargin , left : scrollPosition . left + this . _viewportMargin , right : scrollPosition . left + width - this . _viewportMargin , bottom : scrollPosition . top + height - this . _viewportMargin , width : width - 2 * this . _viewportMargin , height : height - 2 * this . _viewportMargin , } ; } /** Whether the we're dealing with an RTL context */ private _isRtl ( ) { return this . _overlayRef . getDirection ( ) === 'rtl' ; } /** Determines whether the overlay uses exact or flexible positioning. */ private _hasExactPosition ( ) { return ! this . _hasFlexibleDimensions || this . _isPushed ; } /** Retrieves the offset of a position along the x or y axis. */ private _getOffset ( position , axis ) { if ( axis === 'x' ) { return position . offsetX == null ? this . _offsetX : position . offsetX ; } return position . offsetY == null ? this . _offsetY : position . offsetY ; } /** Validates that the current position match the expected values. */ private _validatePositions ( ) { if ( typeof ngDevMode === 'undefined' || ngDevMode ) { if ( ! this . _preferredPositions . length ) { throw Error ( 'FlexibleConnectedPositionStrategy: At least one position is required.' ) ; } this . _preferredPositions . forEach ( pair => { validateHorizontalPosition ( 'originX' , pair . originX ) ; validateVerticalPosition ( 'originY' , pair . originY ) ; validateHorizontalPosition ( 'overlayX' , pair . overlayX ) ; validateVerticalPosition ( 'overlayY' , pair . overlayY ) ; } ) ; } } /** Adds a single CSS class or an array of classes on the overlay panel. */ private _addPanelClasses ( cssClasses ) { if ( this . _pane ) { coerceArray ( cssClasses ) . forEach ( cssClass => { if ( cssClass !== '' && this . _appliedPanelClasses . indexOf ( cssClass ) === - 1 ) { this . _appliedPanelClasses . push ( cssClass ) ; this . _pane . classList . add ( cssClass ) ; } } ) ; } } /** Clears the classes that the position strategy has applied from the overlay panel. */ private _clearPanelClasses ( ) { if ( this . _pane ) { this . _appliedPanelClasses . forEach ( cssClass => { this . _pane . classList . remove ( cssClass ) ; } ) ; this . _appliedPanelClasses = [ ] ; } } /** Returns the ClientRect of the current origin. */ private _getOriginRect ( ) { const origin = this . _origin ; if ( origin instanceof ElementRef ) { return origin . nativeElement . getBoundingClientRect ( ) ; } if ( origin instanceof Element ) { return origin . getBoundingClientRect ( ) ; } const width = origin . width || 0 ; const height = origin . height || 0 ; return { top : origin . y , bottom : origin . y + height , left : origin . x , right : origin . x + width , height , width , } ; } } /** A simple (x, y) coordinate. */ interface Point { x : number ; y : number ; } /** Record of measurements for how an overlay (at a given position) fits into the viewport. */ interface OverlayFit { /** Whether the overlay fits completely in the viewport. */ isCompletelyWithinViewport : boolean ; /** Whether the overlay fits in the viewport on the y-axis. */ fitsInViewportVertically : boolean ; /** Whether the overlay fits in the viewport on the x-axis. */ fitsInViewportHorizontally : boolean ; /** The total visible area (in px^2) of the overlay inside the viewport. */ visibleArea : number ; } /** Record of the measurements determining whether an overlay will fit in a specific position. */ interface FallbackPosition { position : ConnectedPosition ; originPoint : Point ; overlayPoint : Point ; overlayFit : OverlayFit ; overlayRect : Dimensions ; } /** Position and size of the overlay sizing wrapper for a specific position. */ interface BoundingBoxRect { top : number ; left : number ; bottom : number ; right : number ; height : number ; width : number ; } /** Record of measures determining how well a given position will fit with flexible dimensions. */ interface FlexibleFit { position : ConnectedPosition ; origin : Point ; overlayRect : Dimensions ; boundingBoxRect : BoundingBoxRect ; } /** A connected position as specified by the user. */ export interface ConnectedPosition { originX : 'start' | 'center' | 'end' ; originY : 'top' | 'center' | 'bottom' ; overlayX : 'start' | 'center' | 'end' ; overlayY : 'top' | 'center' | 'bottom' ; weight ? : number ; offsetX ? : number ; offsetY ? : number ; panelClass ? : string | string [ ] ; } /** Shallow-extends a stylesheet object with another stylesheet object. */ function extendStyles ( destination , source ) { for ( let key in source ) { if ( source . hasOwnProperty ( key ) ) { destination [ key ] = source [ key ] ; } } return destination ; } /**
 * Extracts the pixel value as a number from a value, if it's a number
 * or a CSS pixel string (e.g. `1337px`). Otherwise returns null.
 */ function getPixelValue ( input ) { if ( typeof input !== 'number' && input != null ) { const [ value , units ] = input . split ( cssUnitPattern ) ; return ! units || units === 'px' ? parseFloat ( value ) : null ; } return input || null ; } /**
 * Gets a version of an element's bounding `ClientRect` where all the values are rounded down to
 * the nearest pixel. This allows us to account for the cases where there may be sub-pixel
 * deviations in the `ClientRect` returned by the browser (e.g. when zoomed in with a percentage
 * size, see #21350).
 */ function getRoundedBoundingClientRect ( clientRect ) { return { top : Math . floor ( clientRect . top ) , right : Math . floor ( clientRect . right ) , bottom : Math . floor ( clientRect . bottom ) , left : Math . floor ( clientRect . left ) , width : Math . floor ( clientRect . width ) , height : Math . floor ( clientRect . height ) , } ; }