import type { CSSProperties } from 'vue' ; import type { TriggerPosition } from '../_utils/constant' ; import { isArray } from '../_utils/is' ; const getViewPortSize = ( ) => { return { width : document . documentElement . clientWidth || window . innerWidth , height : document . documentElement . clientHeight || window . innerHeight } ; } ; export interface ElementRect { top : number ; bottom : number ; left : number ; right : number ; width : number ; height : number ; } interface ScrollRect { top : number ; bottom : number ; left : number ; right : number ; scrollTop : number ; scrollBottom : number ; scrollLeft : number ; scrollRight : number ; width : number ; height : number ; } export const getElementScrollRect = ( element , containerRect ) => { const rect = element . getBoundingClientRect ( ) ; return { top : rect . top , bottom : rect . bottom , left : rect . left , right : rect . right , scrollTop : rect . top - containerRect . top , scrollBottom : rect . bottom - containerRect . top , scrollLeft : rect . left - containerRect . left , scrollRight : rect . right - containerRect . left , width : element . offsetWidth ?? element . clientWidth , height : element . offsetHeight ?? element . clientHeight } ; } ; type BasePosition = 'top' | 'left' | 'bottom' | 'right' ; interface PositionOffset { left : number ; top : number ; } const getBoundaryPosition = ( position ) : BasePosition => { switch ( position ) { case 'top' : case 'tl' : case 'tr' : return 'top' ; case 'bottom' : case 'bl' : case 'br' : return 'bottom' ; case 'left' : case 'lt' : case 'lb' : return 'left' ; case 'right' : case 'rt' : case 'rb' : return 'right' ; default : return 'top' ; } } ; const changePosition = ( position , direction ) : TriggerPosition => { switch ( direction ) { case 'top' : switch ( position ) { case 'bottom' : return 'top' ; case 'bl' : return 'tl' ; case 'br' : return 'tr' ; default : return position ; } case 'bottom' : switch ( position ) { case 'top' : return 'bottom' ; case 'tl' : return 'bl' ; case 'tr' : return 'br' ; default : return position ; } case 'left' : switch ( position ) { case 'right' : return 'left' ; case 'rt' : return 'lt' ; case 'rb' : return 'lb' ; default : return position ; } case 'right' : switch ( position ) { case 'left' : return 'right' ; case 'lt' : return 'rt' ; case 'lb' : return 'rb' ; default : return position ; } default : return position ; } } ; const getFitPosition = ( position , popupPosition , { containerRect , triggerRect , popupRect , offset , translate , } ) => { const direction = getBoundaryPosition ( position ) ; const viewPortSize = getViewPortSize ( ) ; const viewPortBoundary = { top : containerRect . top + popupPosition . top , bottom : viewPortSize . height - ( containerRect . top + popupPosition . top + popupRect . height ) , left : containerRect . left + popupPosition . left , right : viewPortSize . width - ( containerRect . left + popupPosition . left + popupRect . width ) } ; let finalPosition = position ; if ( direction === 'top' && viewPortBoundary . top < 0 ) { if ( triggerRect . top > popupRect . height ) { popupPosition . top = - containerRect . top ; } else { const fitPosition = getPopupOffset ( 'bottom' , triggerRect , popupRect , { offset , translate } ) ; if ( viewPortSize . height - ( containerRect . top + fitPosition . top + popupRect . height ) > 0 ) { finalPosition = changePosition ( position , 'bottom' ) ; popupPosition . top = fitPosition . top ; } } } if ( direction === 'bottom' && viewPortBoundary . bottom < 0 ) { if ( viewPortSize . height - triggerRect . bottom > popupRect . height ) { popupPosition . top = - containerRect . top + ( viewPortSize . height - popupRect . height ) ; } else { const fitPosition = getPopupOffset ( 'top' , triggerRect , popupRect , { offset , translate } ) ; if ( containerRect . top + fitPosition . top > 0 ) { finalPosition = changePosition ( position , 'top' ) ; popupPosition . top = fitPosition . top ; } } } if ( direction === 'left' && viewPortBoundary . left < 0 ) { if ( triggerRect . left > popupRect . width ) { popupPosition . left = - containerRect . left ; } else { const fitPosition = getPopupOffset ( 'right' , triggerRect , popupRect , { offset , translate } ) ; if ( viewPortSize . width - ( containerRect . left + fitPosition . left + popupRect . width ) > 0 ) { finalPosition = changePosition ( position , 'right' ) ; popupPosition . left = fitPosition . left ; } } } if ( direction === 'right' && viewPortBoundary . right < 0 ) { if ( viewPortSize . width - triggerRect . right > popupRect . width ) { popupPosition . left = - containerRect . left + ( viewPortSize . width - popupRect . width ) ; } else { const fitPosition = getPopupOffset ( 'left' , triggerRect , popupRect , { offset , translate } ) ; if ( containerRect . left + fitPosition . left > 0 ) { finalPosition = changePosition ( position , 'left' ) ; popupPosition . left = fitPosition . left ; } } } if ( direction === 'top' || direction === 'bottom' ) { if ( viewPortBoundary . left < 0 ) { popupPosition . left = - containerRect . left ; } else if ( viewPortBoundary . right < 0 ) { popupPosition . left = - containerRect . left + ( viewPortSize . width - popupRect . width ) ; } } if ( direction === 'left' || direction === 'right' ) { if ( viewPortBoundary . top < 0 ) { popupPosition . top = - containerRect . top ; } else if ( viewPortBoundary . bottom < 0 ) { popupPosition . top = - containerRect . top + ( viewPortSize . height - popupRect . height ) ; } } return { popupPosition , position : finalPosition } ; } ; export type PopupTranslate = [ number , number ] | { [ key in TriggerPosition ] ? : [ number , number ] ; } ; const getPopupOffset = ( position , triggerRect , popupRect , { offset = 0 , translate = [ 0 , 0 ] , } = { } ) : PositionOffset => { const _translate = ( isArray ( translate ) ? translate : translate [ position ] ) ?? [ 0 , 0 ] ; switch ( position ) { case 'top' : return { left : triggerRect . scrollLeft + Math . round ( triggerRect . width / 2 ) - Math . round ( popupRect . width / 2 ) + _translate [ 0 ] , top : triggerRect . scrollTop - popupRect . height - offset + _translate [ 1 ] } ; case 'tl' : return { left : triggerRect . scrollLeft + _translate [ 0 ] , top : triggerRect . scrollTop - popupRect . height - offset + _translate [ 1 ] } ; case 'tr' : return { left : triggerRect . scrollRight - popupRect . width + _translate [ 0 ] , top : triggerRect . scrollTop - popupRect . height - offset + _translate [ 1 ] } ; case 'bottom' : return { left : triggerRect . scrollLeft + Math . round ( triggerRect . width / 2 ) - Math . round ( popupRect . width / 2 ) + _translate [ 0 ] , top : triggerRect . scrollBottom + offset + _translate [ 1 ] } ; case 'bl' : return { left : triggerRect . scrollLeft + _translate [ 0 ] , top : triggerRect . scrollBottom + offset + _translate [ 1 ] } ; case 'br' : return { left : triggerRect . scrollRight - popupRect . width + _translate [ 0 ] , top : triggerRect . scrollBottom + offset + _translate [ 1 ] } ; case 'left' : return { left : triggerRect . scrollLeft - popupRect . width - offset + _translate [ 0 ] , top : triggerRect . scrollTop + Math . round ( triggerRect . height / 2 ) - Math . round ( popupRect . height / 2 ) + _translate [ 1 ] } ; case 'lt' : return { left : triggerRect . scrollLeft - popupRect . width - offset + _translate [ 0 ] , top : triggerRect . scrollTop + _translate [ 1 ] } ; case 'lb' : return { left : triggerRect . scrollLeft - popupRect . width - offset + _translate [ 0 ] , top : triggerRect . scrollBottom - popupRect . height + _translate [ 1 ] } ; case 'right' : return { left : triggerRect . scrollRight + offset + _translate [ 0 ] , top : triggerRect . scrollTop + Math . round ( triggerRect . height / 2 ) - Math . round ( popupRect . height / 2 ) + _translate [ 1 ] } ; case 'rt' : return { left : triggerRect . scrollRight + offset + _translate [ 0 ] , top : triggerRect . scrollTop + _translate [ 1 ] } ; case 'rb' : return { left : triggerRect . scrollRight + offset + _translate [ 0 ] , top : triggerRect . scrollBottom - popupRect . height + _translate [ 1 ] } ; default : return { left : 0 , top : 0 } ; } } ; export const getPopupStyle = ( position , containerRect , triggerRect , popupRect , { offset = 0 , translate = [ 0 , 0 ] , customStyle = { } , autoFitPosition = false , autoFitTransformOrigin = false , } = { } ) : { style : CSSProperties ; position : TriggerPosition ; } => { let finalPosition = position ; let popupPosition = getPopupOffset ( position , triggerRect , popupRect , { offset , translate } ) ; if ( autoFitPosition ) { const result = getFitPosition ( position , popupPosition , { containerRect , popupRect , triggerRect , offset , translate } ) ; popupPosition = result . popupPosition ; finalPosition = result . position ; } const style = { left : `${ popupPosition . left }px` , top : `${ popupPosition . top }px` , ... customStyle } ; if ( autoFitTransformOrigin ) { let originX = '0' ; if ( [ 'top' , 'bottom' ] . includes ( finalPosition ) ) { originX = '50%' ; } else if ( [ 'left' , 'lt' , 'lb' , 'tr' , 'br' ] . includes ( finalPosition ) ) { originX = '100%' ; } let originY = '0' ; if ( [ 'left' , 'right' ] . includes ( finalPosition ) ) { originY = '50%' ; } else if ( [ 'top' , 'tl' , 'tr' , 'lt' , 'rt' ] . includes ( finalPosition ) ) { originY = '100%' ; } style . transformOrigin = `${ originX } ${ originY }` ; } return { style , position : finalPosition } ; } ; const withRotate = ( transform ) : string => { return `${ transform } rotate(45deg)` ; } ; export const getArrowStyle = ( position , triggerRect , popupRect , { customStyle = { } , } ) : CSSProperties => { switch ( position ) { case 'top' : return { left : '50%' , bottom : '0' , transform : withRotate ( 'translate(-50%,50%)' ) , ... customStyle } ; case 'tl' : { const halfWidth = triggerRect . width / 2 ; return { left : `${ Math . round ( halfWidth < popupRect . width ? halfWidth : popupRect . width / 2 ) }px` , bottom : '0' , transform : withRotate ( 'translate(-50%,50%)' ) , ... customStyle } ; } case 'tr' : { const halfWidth = triggerRect . width / 2 ; return { right : `${ Math . round ( halfWidth < popupRect . width ? halfWidth : popupRect . width / 2 ) }px` , bottom : '0' , transform : withRotate ( 'translate(50%,50%)' ) , ... customStyle } ; } case 'bottom' : return { left : '50%' , top : '0' , transform : withRotate ( 'translate(-50%,-50%)' ) , ... customStyle } ; case 'bl' : { const halfWidth = triggerRect . width / 2 ; return { left : `${ Math . round ( halfWidth < popupRect . width ? halfWidth : popupRect . width / 2 ) }px` , top : '0' , transform : withRotate ( 'translate(-50%,-50%)' ) , ... customStyle } ; } case 'br' : { const halfWidth = triggerRect . width / 2 ; return { right : `${ Math . round ( halfWidth < popupRect . width ? halfWidth : popupRect . width / 2 ) }px` , top : '0' , transform : withRotate ( 'translate(50%,-50%)' ) , ... customStyle } ; } case 'left' : return { right : '0' , top : '50%' , transform : withRotate ( 'translate(50%,-50%)' ) , ... customStyle } ; case 'lt' : { const halfHeight = triggerRect . height / 2 ; return { right : '0' , top : `${ Math . round ( halfHeight < popupRect . height ? halfHeight : popupRect . height / 2 ) }px` , transform : withRotate ( 'translate(50%,-50%)' ) , ... customStyle } ; } case 'lb' : { const halfHeight = triggerRect . height / 2 ; return { right : '0' , bottom : `${ Math . round ( halfHeight < popupRect . height ? halfHeight : popupRect . height / 2 ) }px` , transform : withRotate ( 'translate(50%,50%)' ) , ... customStyle } ; } case 'right' : return { left : '0' , top : '50%' , transform : withRotate ( 'translate(-50%,-50%)' ) , ... customStyle } ; case 'rt' : { const halfHeight = triggerRect . height / 2 ; return { left : '0' , top : `${ Math . round ( halfHeight < popupRect . height ? halfHeight : popupRect . height / 2 ) }px` , transform : withRotate ( 'translate(-50%,-50%)' ) , ... customStyle } ; } case 'rb' : { const halfHeight = triggerRect . height / 2 ; return { left : '0' , bottom : `${ Math . round ( halfHeight < popupRect . height ? halfHeight : popupRect . height / 2 ) }px` , transform : withRotate ( 'translate(-50%,50%)' ) , ... customStyle } ; } default : return { } ; } } ; export const isScrollElement = ( element ) => { return ( element . scrollHeight > element . offsetHeight || element . scrollWidth > element . offsetWidth ) ; } ; export const getScrollElements = ( container ) => { const scrollElements = [ ] ; let element = container ; while ( element && element !== document . documentElement ) { if ( isScrollElement ( element ) ) { scrollElements . push ( element ) ; } element = element . parentElement ; } return scrollElements ; } ;