import { anticipate , backIn , backInOut , backOut , circIn , circInOut , circOut , easeIn , easeInOut , easeOut , linear , } from '@popmotion/easing' ; import sync from 'framesync' ; import throttle from 'lodash/throttle' ; import { tween } from 'popmotion' ; import { BoundingClientRect , CachedPositionData , ChildBoundingClientRect , Coords , ItemPosition , PopmotionEasing , WrapGridArguments , } from './types' ; const popmotionEasing = { anticipate , backIn , backInOut , backOut , circIn , circInOut , circOut , easeIn , easeInOut , easeOut , linear , } ; const DATASET_KEY = 'animateGridId' ; const toArray = ( arrLike ) : any [ ] => { if ( ! arrLike ) return [ ] ; return Array . prototype . slice . call ( arrLike ) ; } ; const getGridAwareBoundingClientRect = ( gridBoundingClientRect , el ) : BoundingClientRect => { const { top , left , width , height } = el . getBoundingClientRect ( ) ; const rect = { top , left , width , height } ; rect . top -= gridBoundingClientRect . top ; rect . left -= gridBoundingClientRect . left ; rect . top = Math . max ( rect . top , 0 ) ; rect . left = Math . max ( rect . left , 0 ) ; return rect ; } ; const applyCoordTransform = ( el , { translateX , translateY , scaleX , scaleY } , { immediate } = { } ) : void => { const isFinished = translateX === 0 && translateY === 0 && scaleX === 1 && scaleY === 1 ; const styleEl = ( ) => { el . style . transform = isFinished ? '' : `translateX(${ translateX }px) translateY(${ translateY }px) scaleX(${ scaleX }) scaleY(${ scaleY })` ; } ; if ( immediate ) { styleEl ( ) ; } else { sync . render ( styleEl ) ; } const firstChild = el . children [ 0 ] as HTMLElement ; if ( firstChild ) { const styleChild = ( ) => { firstChild . style . transform = isFinished ? '' : `scaleX(${ 1 / scaleX }) scaleY(${ 1 / scaleY })` ; } ; if ( immediate ) { styleChild ( ) ; } else { sync . render ( styleChild ) ; } } } ; export const wrapGrid = ( container , { duration = 250 , stagger = 0 , easing = 'easeInOut' , onStart = ( ) => { } , onEnd = ( ) => { } , } = { } ) => { if ( ! popmotionEasing [ easing ] ) { throw new Error ( `${ easing } is not a valid easing name` ) ; } let mutationsDisabled = false ; const disableMutationsWhileFunctionRuns = ( func ) => { mutationsDisabled = true ; func ( ) ; setTimeout ( ( ) => { mutationsDisabled = false ; } , 0 ) ; } ; const cachedPositionData = { } ; const recordPositions = ( elements ) => { const gridBoundingClientRect = container . getBoundingClientRect ( ) ; toArray ( elements ) . forEach ( el => { if ( typeof el . getBoundingClientRect !== 'function' ) { return ; } if ( ! el . dataset [ DATASET_KEY ] ) { const newId = `${ Math . random ( ) }` ; el . dataset [ DATASET_KEY ] = newId ; } const animateGridId = el . dataset [ DATASET_KEY ] as string ; if ( ! cachedPositionData [ animateGridId ] ) { cachedPositionData [ animateGridId ] = { } as ItemPosition ; } const rect = getGridAwareBoundingClientRect ( gridBoundingClientRect , el ) ; cachedPositionData [ animateGridId ] . rect = rect ; cachedPositionData [ animateGridId ] . gridBoundingClientRect = gridBoundingClientRect ; } ) ; } ; recordPositions ( container . children as HTMLCollectionOf < HTMLElement > ) ; const throttledResizeListener = throttle ( ( ) => { const bodyElement = document . querySelector ( 'body' ) ; const containerIsNoLongerInPage = bodyElement && ! bodyElement . contains ( container ) ; if ( ! container || containerIsNoLongerInPage ) { window . removeEventListener ( 'resize' , throttledResizeListener ) ; } recordPositions ( container . children as HTMLCollectionOf < HTMLElement > ) ; } , 250 ) ; window . addEventListener ( 'resize' , throttledResizeListener ) ; const throttledScrollListener = throttle ( ( ) => { recordPositions ( container . children as HTMLCollectionOf < HTMLElement > ) ; } , 20 ) ; container . addEventListener ( 'scroll' , throttledScrollListener ) ; const mutationCallback = ( mutationsList ) => { if ( mutationsList !== 'forceGridAnimation' ) { const relevantMutationHappened = mutationsList . filter ( ( m ) => m . attributeName === 'class' || m . addedNodes . length || m . removedNodes . length ) . length ; if ( ! relevantMutationHappened ) { return ; } if ( mutationsDisabled ) return ; } const gridBoundingClientRect = container . getBoundingClientRect ( ) ; const childrenElements = toArray ( container . children ) as HTMLElement [ ] ; childrenElements . filter ( el => { const itemPosition = cachedPositionData [ el . dataset [ DATASET_KEY ] as string ] ; if ( itemPosition && itemPosition . stopTween ) { itemPosition . stopTween ( ) ; delete itemPosition . stopTween ; return true ; } } ) . forEach ( el => { el . style . transform = '' ; const firstChild = el . children [ 0 ] as HTMLElement ; if ( firstChild ) { firstChild . style . transform = '' ; } } ) ; const animatedGridChildren = childrenElements . map ( el => ( { childCoords : { } as ChildBoundingClientRect , el , boundingClientRect : getGridAwareBoundingClientRect ( gridBoundingClientRect , el ) , } ) ) . filter ( ( { el , boundingClientRect } ) => { const itemPosition = cachedPositionData [ el . dataset [ DATASET_KEY ] as string ] ; if ( ! itemPosition ) { recordPositions ( [ el ] ) ; return false ; } else if ( boundingClientRect . top === itemPosition . rect . top && boundingClientRect . left === itemPosition . rect . left && boundingClientRect . width === itemPosition . rect . width && boundingClientRect . height === itemPosition . rect . height ) { return false ; } return true ; } ) ; animatedGridChildren . forEach ( ( { el } ) => { if ( toArray ( el . children ) . length > 1 ) { throw new Error ( 'Make sure every grid item has a single container element surrounding its children' ) ; } } ) ; if ( ! animatedGridChildren . length ) { return ; } const animatedElements = animatedGridChildren . map ( ( { el } ) => el ) ; disableMutationsWhileFunctionRuns ( ( ) => onStart ( animatedElements ) ) ; const completionPromises = [ ] ; animatedGridChildren . map ( data => { const firstChild = data . el . children [ 0 ] as HTMLElement ; if ( firstChild ) { data . childCoords = getGridAwareBoundingClientRect ( gridBoundingClientRect , firstChild ) ; } return data ; } ) . forEach ( ( { el , boundingClientRect : { top , left , width , height } , childCoords : { top : childTop , left : childLeft } , } , i ) => { const firstChild = el . children [ 0 ] as HTMLElement ; const itemPosition = cachedPositionData [ el . dataset [ DATASET_KEY ] as string ] ; const coords = { scaleX : itemPosition . rect . width / width , scaleY : itemPosition . rect . height / height , translateX : itemPosition . rect . left - left , translateY : itemPosition . rect . top - top , } ; el . style . transformOrigin = '0 0' ; if ( firstChild && childLeft === left && childTop === top ) { firstChild . style . transformOrigin = '0 0' ; } let cachedResolve = ( ) => { } ; const completionPromise = new Promise ( resolve => { cachedResolve = resolve ; } ) ; completionPromises . push ( completionPromise ) ; applyCoordTransform ( el , coords , { immediate : true } ) ; const startAnimation = ( ) => { const { stop } = tween ( { from : coords , to : { translateX : 0 , translateY : 0 , scaleX : 1 , scaleY : 1 } , duration , ease : popmotionEasing [ easing ] , } ) . start ( { update : ( transforms ) => { applyCoordTransform ( el , transforms ) ; sync . postRender ( ( ) => recordPositions ( [ el ] ) ) ; } , complete : cachedResolve , } ) ; itemPosition . stopTween = stop ; } ; if ( typeof stagger !== 'number' ) { startAnimation ( ) ; } else { const timeoutId = setTimeout ( ( ) => { sync . update ( startAnimation ) ; } , stagger * i ) ; itemPosition . stopTween = ( ) => clearTimeout ( timeoutId ) ; } } ) ; Promise . all ( completionPromises ) . then ( ( ) => { onEnd ( animatedElements ) ; } ) ; } ; const observer = new MutationObserver ( mutationCallback ) ; observer . observe ( container , { childList : true , attributes : true , subtree : true , attributeFilter : [ 'class' ] , } ) ; const unwrapGrid = ( ) => { window . removeEventListener ( 'resize' , throttledResizeListener ) ; container . removeEventListener ( 'scroll' , throttledScrollListener ) ; observer . disconnect ( ) ; } ; const forceGridAnimation = ( ) => mutationCallback ( 'forceGridAnimation' ) ; return { unwrapGrid , forceGridAnimation } ; } ;