import React , { FC , useRef } from "react" ; import { ScrollConfig } from "./ScrollSyncNode" ; export interface ScrollSyncProps { children : React . ReactNode ; /**syncing enable control */ disabled ? : boolean ; /** In case we want scroll to be proportionally applied regardless of the width and/or height*/ proportional ? : boolean ; } interface RecordMap < T > { [ key ] : T ; } /**
 * node should be scrollable
 */ type Node = ( EventTarget & HTMLElement ) | null ; /**
 * node should be scrollable
 */ interface SyncableElement { node : Node ; scroll : ScrollConfig ; } interface ScrollingSyncerContextValues { /**
     * register node to be synced with other scrolled nodes
     */ registerNode : ( node : SyncableElement , groups : string [ ] ) => void ; /**
     * unregister node to stop syncing with other scrolled nodes
     */ unregisterNode : ( node : SyncableElement , group : string [ ] ) => void ; /**
     * scroll handler for each node.onScroll
     */ onScroll : ( e : React . UIEvent < HTMLElement > , groups : string [ ] ) => void ; } /**
 * ScrollingSyncerContext is the context to be handling scrolled nodes
 */ export const ScrollingSyncerContext = React . createContext ( { registerNode : ( _node , _group ) => { } , unregisterNode : ( _node , _group ) => { } , onScroll : ( _e , _groups ) => { } , } ) ; param props export const ScrollSync = props => { /**
     * a map of group: and it's nodes
     * {
     *  groupA: [node1, node2, node3],
     *  groupB: [node4, node5],
     *  groupC: [node1, node4],
     * }
     */ const nodesRef = useRef < RecordMap < SyncableElement [ ] > > ( { } ) ; const elements = nodesRef . current ; /**
     * A simple trick to avoid calling `requestAnimationFrame` before the frame is painted, to enhance performance!
     */ const shouldPaintFrameRef = useRef < boolean > ( true ) ; let shouldPaintFrame = shouldPaintFrameRef . current ; param group const findGroup = ( group ) : boolean => { return ! ! elements [ group ] ; } ; param node param group const doesNodeExists = ( node , group ) : boolean => { const groupExists = findGroup ( group ) ; if ( ! groupExists ) return false ; const foundNode = elements [ group ] . find ( element => element . node === node ) ; if ( ! foundNode ) return false ; return ! ! foundNode ; } ; param node param groups const registerNode = ( element , groups ) => { groups . forEach ( group => { const groupExists = findGroup ( group ) ; if ( ! groupExists ) { elements [ group ] = [ ] ; } elements [ group ] . push ( { ... element } ) ; } ) ; } ; param element param groups const unregisterNode = ( element , groups ) => { groups . forEach ( group => { const doesNodeExist = doesNodeExists ( element . node , group ) ; if ( doesNodeExist ) { elements [ group ] . splice ( elements [ group ] . findIndex ( e => element . node === e . node ) , 1 ) ; } } ) ; } ; param scrolledNode param node const syncScrollPosition = ( scrolledNode , node ) => { if ( ! scrolledNode || ! node ) return ; const { scrollTop , scrollHeight , offsetHeight , scrollLeft , scrollWidth , offsetLeft , offsetWidth } = scrolledNode ; if ( ! props . proportional ) { node . scrollLeft = scrollLeft ; node . scrollTop = scrollTop ; return ; } const percentagePerHeight = scrollTop / ( scrollHeight - offsetHeight ) ; const percentagePerWidth = scrollLeft / ( scrollWidth - offsetWidth ) ; node . scrollTop = Math . round ( percentagePerHeight * ( node . scrollHeight - node . offsetHeight ) ) ; node . scrollLeft = Math . round ( percentagePerWidth * ( node . scrollWidth - node . offsetWidth ) ) ; } ; param scrolledNode param groups const syncScrollPositions = ( scrolledNode , groups ) => { groups . forEach ( group => { elements [ group ] . forEach ( element => { if ( scrolledNode !== element . node ) { const isEnabled = element . scroll === "two-way" ; const isSynced = element . scroll === "synced-only" ; ( isEnabled || isSynced ) && syncScrollPosition ( scrolledNode , element . node ) ; } } ) ; } ) ; } ; param node param groups const handleNodeScroll = ( node , groups ) => { if ( shouldPaintFrame ) { window . requestAnimationFrame ( ( ) => { syncScrollPositions ( node , groups ) ; shouldPaintFrame = true ; } ) ; shouldPaintFrame = false ; } } ; return ( < ScrollingSyncerContext . Provider value = { { registerNode , unregisterNode , onScroll : ( e , groups ) => ! props . disabled && handleNodeScroll ( e . currentTarget , groups ) , } } > { React . Children . only ( props . children ) } < /ScrollingSyncerContext.Provider>); } ; ScrollSync . defaultProps = { disabled : false , proportional : true , } ; export default ScrollSync ;