import { Align , Justify , ElementPosition } from './types' ; interface ElementViewportPositions { referenceElViewportPos : ElementPosition ; contentElViewportPos : ElementPosition ; spacing : number ; } interface ElementDocumentPositions { referenceElDocumentPos : ElementPosition ; contentElDocumentPos : ElementPosition ; spacing : number ; } interface ElementPositions extends Partial < ElementViewportPositions > , Partial < ElementDocumentPositions > { } interface WindowSize { windowWidth : number ; windowHeight : number ; } interface CalculatePosition extends Required < ElementPositions > , Partial < WindowSize > { useRelativePositioning : boolean ; align : Align ; justify : Justify ; } export function calculatePosition ( { useRelativePositioning , spacing , align , justify , referenceElViewportPos = defaultElementPosition , referenceElDocumentPos = defaultElementPosition , contentElViewportPos = defaultElementPosition , contentElDocumentPos = defaultElementPosition , windowHeight = window . innerHeight , windowWidth = window . innerWidth , } ) { const windowSafeCommonArgs = { windowWidth , windowHeight , referenceElViewportPos , contentElViewportPos , spacing , } ; const windowSafeAlign = getWindowSafeAlign ( align , windowSafeCommonArgs ) ; const windowSafeJustify = getWindowSafeJustify ( justify , windowSafeAlign , windowSafeCommonArgs ) ; const transformOrigin = getTransformOrigin ( { align : windowSafeAlign , justify : windowSafeJustify , } ) ; const transform = getTransform ( windowSafeAlign , spacing ) ; if ( useRelativePositioning ) { return { align : windowSafeAlign , justify : windowSafeJustify , positionCSS : { ... calcRelativePosition ( { align : windowSafeAlign , justify : windowSafeJustify , referenceElDocumentPos , contentElDocumentPos , spacing , } ) , transformOrigin , transform , } , } ; } return { align : windowSafeAlign , justify : windowSafeJustify , positionCSS : { ... calcAbsolutePosition ( { align : windowSafeAlign , justify : windowSafeJustify , referenceElDocumentPos , contentElDocumentPos , spacing , windowHeight , windowWidth , } ) , transformOrigin , transform , } , } ; } const defaultElementPosition = { top : 0 , bottom : 0 , left : 0 , right : 0 , height : 0 , width : 0 , } ; export function getElementDocumentPosition ( element , scrollContainer ? ) { if ( ! element ) { return defaultElementPosition ; } const { top , bottom , left , right } = element . getBoundingClientRect ( ) ; const { offsetHeight : height , offsetWidth : width } = element ; if ( scrollContainer ) { const { scrollTop , scrollLeft } = scrollContainer ; const { top : offsetTop , bottom : offsetBottom , left : offsetLeft , right : offsetRight , } = scrollContainer . getBoundingClientRect ( ) ; return { top : top + scrollTop - offsetTop , bottom : bottom + scrollTop - offsetBottom , left : left + scrollLeft - offsetLeft , right : right + scrollLeft - offsetRight , height , width , } ; } const { scrollX , scrollY } = window ; return { top : top + scrollY , bottom : bottom + scrollY , left : left + scrollX , right : right + scrollX , height , width , } ; } export function getElementViewportPosition ( element , scrollContainer ? ) { if ( ! element ) { return defaultElementPosition ; } const { top , bottom , left , right } = element . getBoundingClientRect ( ) ; const { offsetHeight : height , offsetWidth : width } = element ; if ( scrollContainer ) { const { top : offsetTop , bottom : offsetBottom , left : offsetLeft , right : offsetRight , } = scrollContainer . getBoundingClientRect ( ) ; return { top : top - offsetTop , bottom : bottom - offsetBottom , left : left - offsetLeft , right : right - offsetRight , height , width , } ; } return { top , bottom , left , right , height , width , } ; } interface TransformOriginArgs { align : Align ; justify : Justify ; } type XOrigin = 'left' | 'right' | 'center' ; type YOrigin = 'top' | 'bottom' | 'center' ; const yJustifyOrigins = { [ Justify . Start ] : 'top' , [ Justify . Middle ] : 'center' , [ Justify . End ] : 'bottom' , [ Justify . Fit ] : 'center' , } ; const xJustifyOrigins = { [ Justify . Start ] : 'left' , [ Justify . Middle ] : 'center' , [ Justify . End ] : 'right' , [ Justify . Fit ] : 'center' , } ; const transformOriginMappings = { [ Align . Left ] : { x : 'right' } , [ Align . Right ] : { x : 'left' } , [ Align . Top ] : { y : 'bottom' } , [ Align . Bottom ] : { y : 'top' } , [ Align . CenterHorizontal ] : { x : 'center' } , [ Align . CenterVertical ] : { y : 'center' } , } ; function getTransformOrigin ( { align , justify } ) { const alignMapping = transformOriginMappings [ align ] ; const x = alignMapping . x ?? xJustifyOrigins [ justify ] ; const y = alignMapping . y ?? yJustifyOrigins [ justify ] ; return `${ x } ${ y }` ; } function getTransform ( align , transformAmount ) { const scaleAmount = 0.8 ; switch ( align ) { case Align . Top : return `translate3d(0, ${ transformAmount }px, 0) scale(${ scaleAmount })` ; case Align . Bottom : return `translate3d(0, -${ transformAmount }px, 0) scale(${ scaleAmount })` ; case Align . Left : return `translate3d(${ transformAmount }px, 0, 0) scale(${ scaleAmount })` ; case Align . Right : return `translate3d(-${ transformAmount }px, 0, 0) scale(${ scaleAmount })` ; case Align . CenterHorizontal : case Align . CenterVertical : return `scale(${ scaleAmount })` ; } } interface AbsolutePositionObject { top ? : string | 0 ; bottom ? : string | 0 ; left ? : string | 0 ; right ? : string | 0 ; } interface CalcPositionArgs extends ElementDocumentPositions { align : Align ; justify : Justify ; spacing : number ; } type JustifyPositions = { readonly [ J in Justify ] : AbsolutePositionObject | ( ( positions : ElementDocumentPositions ) => AbsolutePositionObject ) ; } ; /**
 * Position mappings for when the main axis alignment is horizontal
 * (left/right/horizontal-center)
 */ const verticalJustifyRelativePositions = { [ Justify . Start ] : { top : 0 } , [ Justify . End ] : { bottom : 0 } , [ Justify . Middle ] : ( { contentElDocumentPos , referenceElDocumentPos } ) => ( { top : `${ referenceElDocumentPos . height / 2 - contentElDocumentPos . height / 2 }px` , } ) , [ Justify . Fit ] : { top : 0 , bottom : 0 } , } ; /**
 * Position mappings for when the main axis alignment is vertical
 * (top/bottom/vertical-center)
 */ const horizontalJustifyRelativePositions = { [ Justify . Start ] : { left : 0 } , [ Justify . End ] : { right : 0 } , [ Justify . Middle ] : ( { contentElDocumentPos , referenceElDocumentPos } ) => ( { left : `${ referenceElDocumentPos . width / 2 - contentElDocumentPos . width / 2 }px` , } ) , [ Justify . Fit ] : { left : 0 , right : 0 } , } ; const relativePositionMappings = { [ Align . Top ] : { constant : ( { spacing } ) => ( { bottom : `calc(100% + ${ spacing }px)` } ) , justifyPositions : horizontalJustifyRelativePositions , } , [ Align . Bottom ] : { constant : ( { spacing } ) => ( { top : `calc(100% + ${ spacing }px)` } ) , justifyPositions : horizontalJustifyRelativePositions , } , [ Align . CenterVertical ] : { constant : ( { referenceElDocumentPos } ) => ( { top : `calc(${ referenceElDocumentPos . height / 2 }px - 50%)` , } ) , justifyPositions : horizontalJustifyRelativePositions , } , [ Align . Left ] : { constant : ( { spacing } ) => ( { right : `calc(100% + ${ spacing }px)` } ) , justifyPositions : verticalJustifyRelativePositions , } , [ Align . Right ] : { constant : ( { spacing } ) => ( { left : `calc(100% + ${ spacing }px)` } ) , justifyPositions : verticalJustifyRelativePositions , } , [ Align . CenterHorizontal ] : { constant : ( { referenceElDocumentPos } ) => ( { left : `calc(${ referenceElDocumentPos . width / 2 }px - 50%)` , } ) , justifyPositions : verticalJustifyRelativePositions , } , } ; function calcRelativePosition ( { align , justify , referenceElDocumentPos , contentElDocumentPos , spacing , } ) { const alignMapping = relativePositionMappings [ align ] ; const justifyMapping = alignMapping . justifyPositions [ justify ] ; const mappingArgs = { contentElDocumentPos , referenceElDocumentPos , spacing } ; return { ... alignMapping . constant ?. ( mappingArgs ) , ... ( typeof justifyMapping === 'function' ? justifyMapping ( mappingArgs ) : justifyMapping ) , } ; } type CalcAbsolutePositionArgs = CalcPositionArgs & WindowSize ; function calcAbsolutePosition ( { align , justify , referenceElDocumentPos , contentElDocumentPos , spacing , windowWidth , windowHeight , } ) { const left = `${ calcLeft ( { align , justify , referenceElPos : referenceElDocumentPos , contentElPos : contentElDocumentPos , spacing , } ) }px` ; const top = `${ calcTop ( { align , justify , referenceElPos : referenceElDocumentPos , contentElPos : contentElDocumentPos , spacing , } ) }px` ; if ( justify !== Justify . Fit ) { return { left , top } ; } if ( ( [ Align . Left , Align . Right , Align . CenterHorizontal , ] as Array < Align > ) . includes ( align ) ) { return { left , top , bottom : `${ windowHeight - referenceElDocumentPos . bottom }px` , } ; } return { left , top , right : `${ windowWidth - referenceElDocumentPos . right }px` , } ; } interface CalcPosition { align ? : Align ; justify ? : Justify ; spacing : number ; contentElPos : ElementPosition ; referenceElPos : ElementPosition ; } function calcTop ( { align , justify , contentElPos , referenceElPos , spacing , } ) { switch ( align ) { case Align . Left : case Align . Right : case Align . CenterHorizontal : switch ( justify ) { case Justify . Start : case Justify . Fit : return referenceElPos . top ; case Justify . End : return ( referenceElPos . top + referenceElPos . height - contentElPos . height ) ; case Justify . Middle : default : return ( referenceElPos . top - ( contentElPos . height - referenceElPos . height ) / 2 ) ; } case Align . CenterVertical : return ( referenceElPos . top - ( contentElPos . height - referenceElPos . height ) / 2 ) ; case Align . Top : return referenceElPos . top - contentElPos . height - spacing ; case Align . Bottom : default : return referenceElPos . top + referenceElPos . height + spacing ; } } function calcLeft ( { align , justify , contentElPos , referenceElPos , spacing , } ) { switch ( align ) { case Align . Top : case Align . Bottom : case Align . CenterVertical : switch ( justify ) { case Justify . End : return ( referenceElPos . left + referenceElPos . width - contentElPos . width ) ; case Justify . Middle : return ( referenceElPos . left - ( contentElPos . width - referenceElPos . width ) / 2 ) ; case Justify . Start : case Justify . Fit : default : return referenceElPos . left ; } case Align . Left : return referenceElPos . left - contentElPos . width - spacing ; case Align . Right : return referenceElPos . left + referenceElPos . width + spacing ; case Align . CenterHorizontal : default : return ( referenceElPos . left - ( contentElPos . width - referenceElPos . width ) / 2 ) ; } } function safelyWithinHorizontalWindow ( { left , windowWidth , contentWidth , } ) { const tooWide = left + contentWidth > windowWidth ; return left >= 0 && ! tooWide ; } function safelyWithinVerticalWindow ( { top , windowHeight , contentHeight , } ) { const tooTall = top + contentHeight > windowHeight ; return top >= 0 && ! tooTall ; } interface WindowSafeCommonArgs extends ElementViewportPositions , WindowSize { } const alignFallbacks = { [ Align . Top ] : [ Align . Bottom ] , [ Align . Bottom ] : [ Align . Top ] , [ Align . Left ] : [ Align . Right ] , [ Align . Right ] : [ Align . Left ] , [ Align . CenterHorizontal ] : [ Align . Left , Align . Right ] , [ Align . CenterVertical ] : [ Align . Top , Align . Bottom ] , } ; function getWindowSafeAlign ( align , windowSafeCommon ) { const { spacing , windowWidth , windowHeight , contentElViewportPos , referenceElViewportPos , } = windowSafeCommon ; const alignOptions = [ align , ... alignFallbacks [ align ] ] ; return ( alignOptions . find ( fallback => { if ( ( [ Align . Top , Align . Bottom , Align . CenterVertical , ] as Array < Align > ) . includes ( fallback ) ) { const top = calcTop ( { align : fallback , contentElPos : contentElViewportPos , referenceElPos : referenceElViewportPos , spacing , } ) ; return safelyWithinVerticalWindow ( { top , windowHeight , contentHeight : contentElViewportPos . height , } ) ; } if ( ( [ Align . Left , Align . Right , Align . CenterHorizontal , ] as Array < Align > ) . includes ( fallback ) ) { const left = calcLeft ( { align : fallback , contentElPos : contentElViewportPos , referenceElPos : referenceElViewportPos , spacing , } ) ; return safelyWithinHorizontalWindow ( { left , windowWidth , contentWidth : contentElViewportPos . width , } ) ; } return false ; } ) || align ) ; } const justifyFallbacks = { [ Justify . Start ] : [ Justify . End , Justify . Middle ] , [ Justify . Middle ] : [ Justify . End , Justify . Start ] , [ Justify . End ] : [ Justify . Start , Justify . Middle ] , [ Justify . Fit ] : [ Justify . Middle , Justify . Start , Justify . End ] , } ; function getWindowSafeJustify ( justify , align , windowSafeCommon ) { const { spacing , windowWidth , windowHeight , contentElViewportPos , referenceElViewportPos , } = windowSafeCommon ; const justifyOptions = [ justify , ... justifyFallbacks [ justify ] ] ; switch ( align ) { case Align . Top : case Align . Bottom : case Align . CenterVertical : return ( justifyOptions . find ( fallback => safelyWithinHorizontalWindow ( { contentWidth : fallback === Justify . Fit ? referenceElViewportPos . width : contentElViewportPos . width , windowWidth , left : calcLeft ( { contentElPos : contentElViewportPos , referenceElPos : referenceElViewportPos , spacing , align : align , justify : fallback , } ) , } ) ) ?? justifyFallbacks [ justify ] [ 0 ] ) ; case Align . Left : case Align . Right : case Align . CenterHorizontal : return ( justifyOptions . find ( fallback => safelyWithinVerticalWindow ( { contentHeight : fallback === Justify . Fit ? referenceElViewportPos . height : contentElViewportPos . height , windowHeight , top : calcTop ( { contentElPos : contentElViewportPos , referenceElPos : referenceElViewportPos , spacing , align , justify : fallback , } ) , } ) ) ?? justifyFallbacks [ justify ] [ 0 ] ) ; } }