import React , { Component , ComponentType , createRef , PropsWithChildren , PureComponent , ReactElement , ReactNode , Ref , RefCallback , RefObject , } from 'react' ; import mergeRefs from 'react-merge-refs' ; import { Align , FixedSizeList , ListChildComponentProps , ListProps , VariableSizeList , } from 'react-window' ; import { DefaultTreeProps , DefaultTreeState , noop , RequestIdleCallbackDeadline , } from './utils' ; export type NodeData = Readonly < { /**
     * Unique ID of the current node.
     */ id : string ; /**
     * Default node openness state. If the Tree component performs building a new
     * Tree and the preservePreviousState prop is not set, this value will be used
     * to set the openness state of the node.
     */ isOpenByDefault : boolean ; } > ; export type TreeWalkerValue < TData extends NodeData , TMeta = { } > = Readonly < { data : TData ; } & TMeta > ; export type OpennessStateUpdateRules < TData extends NodeData , TNodePublicState extends NodePublicState < TData > > = Readonly < { open : boolean ; subtreeCallback ? : ( node : TNodePublicState , ownerNode : TNodePublicState ) => void ; } > ; export type NodePublicState < TData extends NodeData > = Readonly < { data : TData ; setOpen : ( state : boolean ) => Promise < void > ; } > & { isOpen : boolean ; } ; export type NodeRecord < TNodePublicState extends NodePublicState < any > > = Readonly < { public : TNodePublicState ; } > & { child : NodeRecord < TNodePublicState > | null ; isShown : boolean ; parent : NodeRecord < TNodePublicState > | null ; sibling : NodeRecord < TNodePublicState > | null ; visited : boolean ; } ; export type NodeComponentProps < TData extends NodeData , TNodePublicState extends NodePublicState < TData > > = Readonly < Omit < ListChildComponentProps , 'data' | 'index' > & TNodePublicState & { /**
     * The data provided by user via `itemData` Tree component property.
     */ treeData ? : any ; } > ; export type TreeWalker < TData extends NodeData , TMeta = { } > = ( ) => Generator < TreeWalkerValue < TData , TMeta > | undefined , undefined , TreeWalkerValue < TData , TMeta > > ; export type OpennessState < TData extends NodeData , TNodePublicState extends NodePublicState < TData > > = Readonly < Record < string , OpennessStateUpdateRules < TData , TNodePublicState > | boolean > > ; export type TreeComputerProps < TData extends NodeData > = Readonly < { async ? : boolean ; buildingTaskTimeout ? : number ; placeholder ? : ReactNode ; treeWalker : TreeWalker < TData > ; } > ; export type TreeComputerState < TData extends NodeData , TNodePublicState extends NodePublicState < TData > > = Readonly < { order ? : string [ ] ; records : ReadonlyMap < string | symbol , NodeRecord < TNodePublicState > > ; setState : Component < any , TreeComputerState < TData , TNodePublicState > > [ 'setState' ] ; updateRequest : object ; } > ; export type TreeProps < TData extends NodeData , TNodePublicState extends NodePublicState < TData > , TListComponent extends FixedSizeList | VariableSizeList > = Readonly < Omit < ListProps , 'children' | 'itemCount' | 'itemKey' > > & TreeComputerProps < TData > & Readonly < { children : ComponentType < NodeComponentProps < TData , TNodePublicState > > ; listRef ? : Ref < TListComponent > ; rowComponent ? : ComponentType < ListChildComponentProps > ; } > ; export type TreeState < TData extends NodeData , TNodePublicState extends NodePublicState < TData > , TListComponent extends FixedSizeList | VariableSizeList > = TreeComputerState < TData , TNodePublicState > & Readonly < { attachRefs : RefCallback < TListComponent > ; computeTree : TreeComputer < any , any , any , any > ; list : RefObject < TListComponent > ; recomputeTree : ( options : OpennessState < TData , TNodePublicState > ) => Promise < void > ; treeWalker : TreeWalker < TData > ; } > ; export type TypedListChildComponentData < TData extends NodeData , TNodePublicState extends NodePublicState < TData > > = Readonly < { /**
     * The Node component provided by the user.
     */ component : ComponentType < NodeComponentProps < TData , TNodePublicState > > ; param index getRecordData : ( index : number ) => TNodePublicState ; see NodeComponentProps treeData : any ; } > ; export type TypedListChildComponentProps < TData extends NodeData , TNodePublicState extends NodePublicState < TData > > = Readonly < Omit < ListChildComponentProps , 'data' > & { data : TypedListChildComponentData < TData , TNodePublicState > ; } > ; export const Row = < TData extends NodeData , TNodePublicState extends NodePublicState < TData > > ( { index , data : { component : Node , getRecordData , treeData } , style , isScrolling , } ) : ReactElement | null => { const data = getRecordData ( index ) ; return ( < Node isScrolling = { isScrolling } style = { style } treeData = { treeData } { ... data } />); } ; export type TreeCreatorOptions < TData extends NodeData , TNodePublicState extends NodePublicState < TData > , TState extends TreeComputerState < TData , TNodePublicState > > = Readonly < { createRecord : ( data : TData , state : TState , parent ? : NodeRecord < TNodePublicState > | null , previousRecord ? : NodeRecord < TNodePublicState > ) => NodeRecord < TNodePublicState > ; } > ; export type TreeComputerOptions < TData extends NodeData , TNodePublicState extends NodePublicState < TData > > = Readonly < { opennessState ? : OpennessState < TData , TNodePublicState > ; refresh ? : boolean ; } > ; export type TreeComputer < TData extends NodeData , TNodePublicState extends NodePublicState < TData > , TProps extends TreeComputerProps < TData > , TState extends TreeComputerState < TData , TNodePublicState > > = ( props : TProps , state : TState , options : TreeComputerOptions < TData , TNodePublicState > ) => ( Pick < TState , 'order' | 'records' > & Partial < Pick < TState , 'updateRequest' > > ) | null ; const generateNewTree = < TData extends NodeData , TNodePublicState extends NodePublicState < TData > , TProps extends TreeComputerProps < TData > , TState extends TreeComputerState < TData , TNodePublicState > > ( { createRecord } , { buildingTaskTimeout , placeholder , async = false , treeWalker } , state ) : ReturnType < TreeComputer < TData , TNodePublicState , TProps , TState > > => { const shouldPreservePreviousState = async && state . records !== undefined ; const { records : previousRecords } = state ; const order = [ ] ; const records = new Map < string | symbol , NodeRecord < TNodePublicState > > ( ) ; const requestIdleCallbackOptions = buildingTaskTimeout ? { timeout : buildingTaskTimeout } : undefined ; const meta = new WeakMap < NodeRecord < TNodePublicState > , TreeWalkerValue < TData > > ( ) ; const iter = treeWalker ( ) ; const { value : root } = iter . next ( ) ; const rootRecord = createRecord ( root ! . data , state , undefined , shouldPreservePreviousState ? previousRecords . get ( root ! . data . id ) : undefined ) ; records . set ( rootRecord . public . data . id , rootRecord ) ; meta . set ( rootRecord , root ! ) ; let currentRecord = rootRecord ; let isTraversingRoot = true ; let tempRecord = rootRecord ; const useIdleCallback = typeof requestIdleCallback !== 'undefined' && placeholder !== undefined && ! ( placeholder === null && ! state . order ) ; const hasTime = useIdleCallback ? ( deadline ) => deadline . timeRemaining ( ) > 0 : ( ) => true ; const task = ( deadline ? ) => { while ( currentRecord !== null ) { if ( ! hasTime ( deadline ! ) ) { requestIdleCallback ( task , requestIdleCallbackOptions ) ; return ; } if ( ! currentRecord . visited ) { const { value : child } = iter . next ( meta . get ( currentRecord ) ! ) ; if ( child === undefined ) { if ( isTraversingRoot ) { isTraversingRoot = false ; } else { if ( currentRecord . isShown ) { order . push ( currentRecord . public . data . id ) ; } currentRecord . visited = currentRecord . child !== null ; currentRecord = currentRecord . child !== null ? currentRecord . child : currentRecord . sibling !== null ? currentRecord . sibling : currentRecord . parent ; } tempRecord = currentRecord ; continue ; } const childRecord = createRecord ( child . data , state , isTraversingRoot ? undefined : currentRecord , shouldPreservePreviousState ? previousRecords . get ( child . data . id ) : undefined ) ; records . set ( childRecord . public . data . id , childRecord ) ; meta . set ( childRecord , child ) ; if ( ! isTraversingRoot && tempRecord === currentRecord ) { tempRecord . child = childRecord ; } else { tempRecord ! . sibling = childRecord ; } tempRecord = childRecord ; } else { currentRecord . visited = false ; currentRecord = currentRecord . sibling !== null ? currentRecord . sibling : currentRecord . parent ; tempRecord = currentRecord ; } } if ( useIdleCallback ) { state . setState ( { order , records , updateRequest : { } , } ) ; } } ; if ( useIdleCallback ) { requestIdleCallback ( task , requestIdleCallbackOptions ) ; } else { task ( ) ; } return placeholder !== undefined && async && state . order ? state : { order , records } ; } ; const MAX_FUNCTION_ARGUMENTS = 32768 ; const SPLICE_DEFAULT_ARGUMENTS_NUMBER = 2 ; const updateExistingTree = < TData extends NodeData , TNodePublicState extends NodePublicState < TData > , TProps extends TreeComputerProps < TData > , TState extends TreeComputerState < TData , TNodePublicState > > ( { order , records } , { opennessState } ) : ReturnType < TreeComputer < TData , TNodePublicState , TProps , TState > > => { if ( typeof opennessState !== 'object' ) { return null ; } for ( const id in opennessState ) { if ( ! records . has ( id ) ) { continue ; } const opts = opennessState [ id ] ; const ownerRecord = records . get ( id ) ! ; const { open , subtreeCallback = noop , } = typeof opts === 'boolean' ? { open : opts } : opts ; let update = noop ; let apply = noop ; if ( ownerRecord . isShown ) { if ( open ) { const index = order ! . indexOf ( id ) ; let recordNextToSubtree = ownerRecord ; while ( recordNextToSubtree !== null ) { if ( recordNextToSubtree . sibling !== null ) { recordNextToSubtree = recordNextToSubtree . sibling ; break ; } recordNextToSubtree = recordNextToSubtree . parent ; } const countToRemove = recordNextToSubtree === null ? order ! . length - 1 - index : order ! . indexOf ( recordNextToSubtree . public . data . id ) - 1 - index ; const orderParts = [ [ index + 1 , countToRemove ] , ] ; update = ( record ) => { record . isShown = record . parent ? record . parent . public . isOpen && record . parent . isShown : true ; if ( record . isShown ) { const currentOrderPart = orderParts [ orderParts . length - 1 ] ; currentOrderPart . push ( record . public . data . id ) ; if ( currentOrderPart . length === MAX_FUNCTION_ARGUMENTS + SPLICE_DEFAULT_ARGUMENTS_NUMBER ) { orderParts . push ( [ index + 1 + MAX_FUNCTION_ARGUMENTS * orderParts . length , 0 , ] ) ; } } } ; apply = ( ) => { for ( let i = 0 ; i < orderParts . length ; i ++ ) { order ! . splice ( ... orderParts [ i ] ) ; } } ; } else if ( ownerRecord . public . isOpen ) { const index = order ! . indexOf ( id ) ; let count = 0 ; update = ( record ) => { if ( record . isShown ) { count += 1 ; } record . isShown = record . parent ? record . parent . public . isOpen && record . parent . isShown : true ; } ; apply = ( ) => { order ! . splice ( index + 1 , count ) ; } ; } } let currentRecord = ownerRecord ; while ( currentRecord !== null ) { if ( ! currentRecord . visited ) { currentRecord . public . isOpen = currentRecord === ownerRecord ? open : currentRecord . public . isOpen ; subtreeCallback ( currentRecord . public , ownerRecord . public ) ; if ( currentRecord !== ownerRecord ) { update ( currentRecord ) ; } currentRecord . visited = currentRecord . child !== null ; currentRecord = currentRecord . child !== null ? currentRecord . child : currentRecord === ownerRecord ? null : currentRecord . sibling !== null ? currentRecord . sibling : currentRecord . parent ; } else { currentRecord . visited = false ; currentRecord = currentRecord === ownerRecord ? null : currentRecord . sibling !== null ? currentRecord . sibling : currentRecord . parent ; } } apply ( ) ; } return { order , records , updateRequest : { } , } ; } ; export const createTreeComputer = < TData extends NodeData , TNodePublicState extends NodePublicState < TData > , TProps extends TreeComputerProps < TData > , TState extends TreeComputerState < TData , TNodePublicState > > ( creatorOptions ) : TreeComputer < TData , TNodePublicState , TProps , TState > => ( props , state , options ) => options . refresh ? generateNewTree ( creatorOptions , props , state ) : updateExistingTree ( state , options ) ; class Tree < TData extends NodeData , TNodePublicState extends NodePublicState < TData > , TProps extends TreeProps < TData , TNodePublicState , TListComponent > , TState extends TreeState < TData , TNodePublicState , TListComponent > , TListComponent extends FixedSizeList | VariableSizeList > extends PureComponent < TProps , TState > { public static defaultProps : Partial < DefaultTreeProps > = { rowComponent : Row , } ; public static getDerivedStateFromProps ( props , state ) { const { listRef = null , treeWalker } = props ; const { computeTree , list , order , treeWalker : oldTreeWalker } = state ; return { attachRefs : mergeRefs ( [ list , listRef ] ) , ... ( treeWalker !== oldTreeWalker || ! order ? computeTree ( props , state , { refresh : true } ) : null ) , treeWalker , } ; } public constructor ( props , context ) { super ( props , context ) ; this . getRecordData = this . getRecordData . bind ( this ) ; this . state = { list : createRef ( ) , recomputeTree : this . recomputeTree . bind ( this ) , setState : this . setState . bind ( this ) , } as TState ; } protected getItemData ( ) { const { children : component , itemData : treeData } = this . props ; return { component , getRecordData : this . getRecordData , treeData , } ; } protected getRecordData ( index ) { const { order , records } = this . state ; return records . get ( order ! [ index ] ) ! . public ; } public recomputeTree ( state ) { return new Promise ( ( resolve ) => { this . setState < never > ( ( prevState ) => prevState . computeTree ( this . props , prevState , { opennessState : state , } ) , resolve ) ; } ) ; } public scrollTo ( scrollOffset ) { this . state . list . current ?. scrollTo ( scrollOffset ) ; } public scrollToItem ( id , align ? ) { this . state . list . current ?. scrollToItem ( this . state . order ! . indexOf ( id ) , align ) ; } } export default Tree ;