packageDocumentation module import "./ControlledTree.scss" ; import classnames from "classnames" ; import * as React from "react" ; import { areEqual , ListChildComponentProps , ListOnItemsRenderedProps , VariableSizeList } from "react-window" ; import { concat } from "rxjs/internal/observable/concat" ; import { timer } from "rxjs/internal/observable/timer" ; import { assert } from "@itwin/core-bentley" ; import { Tree as CoreTree , TreeNodePlaceholder } from "@itwin/core-react" ; import { createContextWithMandatoryProvider } from "../../../common/UseContextWithMandatoryProvider" ; import { HighlightableTreeProps , HighlightingEngine } from "../../HighlightingEngine" ; import { TreeActions } from "../TreeActions" ; import { isTreeModelNode , isTreeModelNodePlaceholder , isTreeModelRootNode , TreeModelNode , TreeModelNodePlaceholder , VisibleTreeNodes , } from "../TreeModel" ; import { ITreeNodeLoader } from "../TreeNodeLoader" ; import { TreeNodeRenderer , TreeNodeRendererProps } from "./TreeNodeRenderer" ; const NODE_LOAD_DELAY = 500 ; beta export interface RenderedItemsRange { overscanStartIndex : number ; overscanStopIndex : number ; visibleStartIndex : number ; visibleStopIndex : number ; } public export interface TreeRendererProps { treeActions : TreeActions ; nodeLoader : ITreeNodeLoader ; /** Callback that is used to determine node height. */ nodeHeight : ( node : TreeModelNode | TreeModelNodePlaceholder , index : number ) => number ; /** Flat list of nodes to be rendered. */ visibleNodes : VisibleTreeNodes ; /** Callback to render custom node.  */ nodeRenderer ? : ( props : TreeNodeRendererProps ) => React . ReactNode ; /** Properties used to highlight nodes and scroll to active match while filtering. */ nodeHighlightingProps ? : HighlightableTreeProps ; beta onItemsRendered ? : ( renderedItems : RenderedItemsRange ) => void ; internal onNodeEditorClosed ? : ( ) => void ; /** Width of the tree area. */ width : number ; /** Height of the tree area. */ height : number ; } public export interface TreeRendererAttributes { param nodeId param alignment scrollToNode ( nodeId , alignment ? ) : void ; } type Alignment = "auto" | "smart" | "center" | "end" | "start" ; /** [[TreeRenderer]] context that is provided to each rendered node. */ interface TreeRendererContext { /** Callback to render custom node. */ nodeRenderer : ( props : TreeNodeRendererProps ) => React . ReactNode ; treeActions : TreeActions ; nodeLoader : ITreeNodeLoader ; /** Flat list of nodes to be rendered. */ visibleNodes : VisibleTreeNodes ; /** Engine used to created node highlighting properties. */ highlightingEngine ? : HighlightingEngine ; /** Callback used detect when label is rendered. It is used by TreeRenderer for scrolling to active match. */ onLabelRendered ? : ( node : TreeModelNode ) => void ; /** A callback that node calls after rendering to report its width */ onNodeWidthMeasured ? : ( width : number ) => void ; /** Callback used when an editor closes */ onNodeEditorClosed ? : ( ) => void ; } /** [[TreeRenderer]] context provider, consumer and custom hook. */ const [ TreeRendererContextProvider , _TreeRendererContextConsumer , useTreeRendererContext , ] = createContextWithMandatoryProvider < TreeRendererContext > ( "TreeRendererContext" ) ; public export class TreeRenderer extends React . Component < TreeRendererProps > implements TreeRendererAttributes { private _ref = React . createRef < TreeRendererAttributes > ( ) ; inheritdoc public scrollToNode ( nodeId , alignment ? ) { if ( this . _ref . current ) this . _ref . current . scrollToNode ( nodeId , alignment ) ; } public override render ( ) { return ( < TreeRendererInner ref = { this . _ref } { ... this . props } />); } } const TreeRendererInner = React . forwardRef < TreeRendererAttributes , TreeRendererProps > ( ( props , ref ) => { const variableSizeListRef = React . useRef < VariableSizeList > ( null ) ; useTreeRendererAttributes ( ref , variableSizeListRef , props . visibleNodes ) ; const previousVisibleNodes = usePrevious ( props . visibleNodes ) ; const previousNodeHeight = usePrevious ( props . nodeHeight ) ; if ( ( previousVisibleNodes !== undefined && previousVisibleNodes !== props . visibleNodes ) || ( previousNodeHeight !== undefined && previousNodeHeight !== props . nodeHeight ) ) { if ( variableSizeListRef . current ) { variableSizeListRef . current . resetAfterIndex ( 0 , false ) ; } } const coreTreeRef = React . useRef < CoreTree > ( null ) ; const minContainerWidth = React . useRef < number > ( 0 ) ; const onLabelRendered = useScrollToActiveMatch ( coreTreeRef , props . nodeHighlightingProps ) ; const highlightingEngine = React . useMemo ( ( ) => props . nodeHighlightingProps && new HighlightingEngine ( props . nodeHighlightingProps ) , [ props . nodeHighlightingProps ] ) ; const rendererContext = React . useMemo < TreeRendererContext > ( ( ) => ( { nodeRenderer : props . nodeRenderer ? props . nodeRenderer : ( nodeProps ) => ( < TreeNodeRenderer { ... nodeProps } / > ) , treeActions : props . treeActions , nodeLoader : props . nodeLoader , visibleNodes : props . visibleNodes , onLabelRendered , highlightingEngine , onNodeWidthMeasured : ( width ) => { if ( width > minContainerWidth . current ) minContainerWidth . current = width ; } , onNodeEditorClosed : ( ) => { setFocusToSelected ( coreTreeRef ) ; props . onNodeEditorClosed && props . onNodeEditorClosed ( ) ; } } ) , [ props , onLabelRendered , highlightingEngine ] ) ; const prevTreeWidth = React . useRef < number > ( 0 ) ; if ( props . width !== prevTreeWidth . current ) { minContainerWidth . current = 0 ; prevTreeWidth . current = props . width ; } const itemKey = React . useCallback ( ( index ) => getNodeKey ( props . visibleNodes . getAtIndex ( index ) ! ) , [ props . visibleNodes ] ) ; const { nodeHeight , visibleNodes } = props ; const itemSize = React . useCallback ( ( index ) => nodeHeight ( visibleNodes . getAtIndex ( index ) ! , index ) , [ nodeHeight , visibleNodes ] ) ; const { nodeHighlightingProps } = props ; React . useEffect ( ( ) => { const highlightedNodeId = getHighlightedNodeId ( nodeHighlightingProps ) ; if ( ! highlightedNodeId ) return ; let index = 0 ; for ( const node of visibleNodes ) { if ( isTreeModelNode ( node ) && node . id === highlightedNodeId ) break ; index ++ ; } assert ( variableSizeListRef . current !== null ) ; variableSizeListRef . current . scrollToItem ( index ) ; } , [ nodeHighlightingProps ] ) ; const innerElementType = React . useCallback ( React . forwardRef ( ( { style , ... rest } , innerRef ) => ( < div ref = { innerRef } style = { { ... style , minWidth : minContainerWidth . current } } { ... rest } / > ) ) , [ ] ) ; const handleKeyDown = React . useCallback ( ( e ) => { props . treeActions . onTreeKeyDown ( e ) ; } , [ props . treeActions ] ) ; const handleKeyUp = React . useCallback ( ( e ) => { props . treeActions . onTreeKeyUp ( e ) ; } , [ props . treeActions ] ) ; const onItemsRendered = props . onItemsRendered ; const handleRenderedItemsChange = React . useCallback ( ( onItemsRenderedProps ) => { onItemsRendered && onItemsRendered ( { ... onItemsRenderedProps } ) ; } , [ onItemsRendered ] ) ; return ( < TreeRendererContextProvider value = { rendererContext } > < CoreTree ref = { coreTreeRef } className = { classnames ( "components-controlledTree" , "components-smallEditor-host" ) } onKeyDown = { handleKeyDown } onKeyUp = { handleKeyUp } > < VariableSizeList ref = { variableSizeListRef } className = { "ReactWindow__VariableSizeList" } width = { props . width } height = { props . height } itemCount = { props . visibleNodes . getNumNodes ( ) } itemSize = { itemSize } estimatedItemSize = { 25 } overscanCount = { 10 } itemKey = { itemKey } innerElementType = { innerElementType } onItemsRendered = { handleRenderedItemsChange } > { Node } < /VariableSizeList> < /CoreTree> < /TreeRendererContextProvider>); } ) ; function getNodeKey ( node ) { if ( isTreeModelNode ( node ) ) { return node . id ; } return `${ node . parentId || "" }-${ node . childIndex }` ; } const Node = React . memo < React . FC < ListChildComponentProps > > ( ( props ) => { const { index , style } = props ; const { nodeRenderer , visibleNodes , treeActions , nodeLoader , onLabelRendered , highlightingEngine , onNodeWidthMeasured , onNodeEditorClosed , } = useTreeRendererContext ( Node ) ; const node = visibleNodes . getAtIndex ( index ) ! ; useNodeLoading ( node , visibleNodes , nodeLoader ) ; const className = classnames ( "node-wrapper" , { "is-selected" : isTreeModelNode ( node ) && node . isSelected } ) ; const ref = React . useRef < HTMLDivElement > ( null ) ; React . useEffect ( ( ) => { if ( onNodeWidthMeasured && ref . current ) onNodeWidthMeasured ( ref . current . offsetWidth ) ; } , [ onNodeWidthMeasured ] ) ; const isEditing = React . useRef ( false ) ; React . useEffect ( ( ) => { if ( ! isTreeModelNode ( node ) ) return ; if ( ! isEditing . current && node . editingInfo ) { isEditing . current = true ; } else if ( isEditing . current && node . editingInfo === undefined ) { isEditing . current = false ; if ( onNodeEditorClosed ) onNodeEditorClosed ( ) ; } } , [ node , onNodeEditorClosed ] ) ; return ( < div className = { className } style = { style } ref = { ref } > { React . useMemo ( ( ) => { if ( isTreeModelNode ( node ) ) { const nodeHighlightProps = highlightingEngine ? highlightingEngine . createRenderProps ( node ) : undefined ; return nodeRenderer ( { node , treeActions , onLabelRendered , nodeHighlightProps } ) ; } return < TreeNodePlaceholder level = { node . depth } />; } , [ node , treeActions , nodeRenderer , onLabelRendered , highlightingEngine ] ) } < /div>); } , areEqual ) ; function useNodeLoading ( node , visibleNodes , nodeLoader ) { React . useEffect ( ( ) => { if ( ! isTreeModelNodePlaceholder ( node ) ) { return ; } const treeModel = visibleNodes . getModel ( ) ; const parentNode = node . parentId ? treeModel . getNode ( node . parentId ) : treeModel . getRootNode ( ) ; if ( ! isTreeModelNode ( parentNode ) && ! isTreeModelRootNode ( parentNode ) ) { return ; } const subscription = concat ( timer ( NODE_LOAD_DELAY ) , nodeLoader . loadNode ( parentNode , node . childIndex ) ) . subscribe ( ) ; return ( ) => subscription . unsubscribe ( ) ; } , [ nodeLoader ] ) ; } function useTreeRendererAttributes ( ref , variableSizeListRef , visibleNodes ) { const visibleNodesRef = React . useRef ( visibleNodes ) ; visibleNodesRef . current = visibleNodes ; React . useImperativeHandle ( ref , ( ) => ( { scrollToNode : ( nodeId , alignment ) => { assert ( variableSizeListRef . current !== null ) ; variableSizeListRef . current . scrollToItem ( visibleNodesRef . current . getIndexOfNode ( nodeId ) , alignment ) ; } } ) , [ ] ) ; } function usePrevious < T > ( value ) { const ref = React . useRef < T > ( ) ; React . useEffect ( ( ) => { ref . current = value ; } , [ value ] ) ; return ref . current ; } function getHighlightedNodeId ( highlightableTreeProps ? ) { return ( highlightableTreeProps && highlightableTreeProps . activeMatch ) ? highlightableTreeProps . activeMatch . nodeId : undefined ; } function useScrollToActiveMatch ( treeRef , highlightableTreeProps ? ) { const scrollToActive = React . useRef ( false ) ; React . useEffect ( ( ) => { scrollToActive . current = true ; } , [ highlightableTreeProps ] ) ; const onLabelRendered = React . useCallback ( ( node ) => { const highlightedNodeId = getHighlightedNodeId ( highlightableTreeProps ) ; if ( ! treeRef . current || ! scrollToActive . current || ! highlightedNodeId || highlightedNodeId !== node . id ) return ; scrollToActive . current = false ; const scrollTo = [ ... treeRef . current . getElementsByClassName ( HighlightingEngine . ACTIVE_CLASS_NAME ) ] ; if ( scrollTo . length > 0 && scrollTo [ 0 ] . scrollIntoView ) scrollTo [ 0 ] . scrollIntoView ( { behavior : "auto" , block : "nearest" , inline : "end" } ) ; } , [ highlightableTreeProps , treeRef ] ) ; return onLabelRendered ; } function setFocusToSelected ( treeRef ) { if ( treeRef . current ) treeRef . current . setFocusByClassName ( ".core-tree-node.is-selected" ) ; }