packageDocumentation module import { Observable as RxjsObservable } from "rxjs/internal/Observable" ; import { defer } from "rxjs/internal/observable/defer" ; import { from } from "rxjs/internal/observable/from" ; import { of } from "rxjs/internal/observable/of" ; import { concatAll } from "rxjs/internal/operators/concatAll" ; import { concatMap } from "rxjs/internal/operators/concatMap" ; import { finalize } from "rxjs/internal/operators/finalize" ; import { map } from "rxjs/internal/operators/map" ; import { publish } from "rxjs/internal/operators/publish" ; import { refCount } from "rxjs/internal/operators/refCount" ; import { toArray } from "rxjs/internal/operators/toArray" ; import { BeEvent , IDisposable } from "@itwin/core-bentley" ; import { UiError } from "@itwin/appui-abstract" ; import { scheduleSubscription , SubscriptionScheduler } from "../../common/SubscriptionScheduler" ; import { UiComponents } from "../../UiComponents" ; import { ImmediatelyLoadedTreeNodeItem , isTreeDataProviderInterface , isTreeDataProviderMethod , isTreeDataProviderPromise , isTreeDataProviderRaw , TreeDataChangesListener , TreeDataProvider , TreeDataProviderRaw , TreeNodeItem , } from "../TreeDataProvider" ; import { Observable , toRxjsObservable } from "./Observable" ; import { isTreeModelNode , MutableTreeModel , TreeModelNode , TreeModelNodeInput , TreeModelRootNode , TreeNodeItemData } from "./TreeModel" ; import { TreeModelSource } from "./TreeModelSource" ; public export interface TreeNodeLoadResult { loadedNodes : TreeNodeItem [ ] ; } public export interface ITreeNodeLoader { param parentId param childIndex loadNode ( parentId , childIndex ) : Observable < TreeNodeLoadResult > ; } public export interface ITreeNodeLoaderWithProvider < TDataProvider extends TreeDataProvider > extends ITreeNodeLoader { /** Returns `TreeDataProvider` used to load nodes. */ readonly dataProvider : TDataProvider ; } public export abstract class AbstractTreeNodeLoader implements ITreeNodeLoader { private _treeModelSource : TreeModelSource ; private _loadScheduler = new SubscriptionScheduler < TreeNodeLoadResult > ( ) ; protected constructor ( modelSource ) { this . _treeModelSource = modelSource ; } public get modelSource ( ) { return this . _treeModelSource ; } /** Do not override this method. @see `load` */ public loadNode ( parent , childIndex ) { return toRxjsObservable ( this . load ( parent , childIndex ) ) . pipe ( map ( ( loadedHierarchy ) => { this . updateModel ( loadedHierarchy ) ; return { loadedNodes : collectTreeNodeItems ( loadedHierarchy . hierarchyItems ) } ; } ) , scheduleSubscription ( this . _loadScheduler ) ) ; } /**
     * A method that's called when `load` loads some nodes and we need to put them into model source. The
     * default implementation simply puts loaded child nodes under their parent at correct positions. Concrete
     * implementation may override this method to handle loaded nodes in a a custom way (put them at custom locations
     * in the hierarchy, etc.)
     */ protected updateModel ( loadedHierarchy ) { handleLoadedNodeHierarchy ( this . _treeModelSource , loadedHierarchy ) ; } /** An abstract method to load a node at the specific index for the specified parent. */ protected abstract load ( parent , childIndex ) ; } public export abstract class AbstractTreeNodeLoaderWithProvider < TDataProvider extends TreeDataProvider > extends AbstractTreeNodeLoader implements ITreeNodeLoaderWithProvider < TDataProvider > { private _dataProvider : TDataProvider ; protected constructor ( modelSource , dataProvider ) { super ( modelSource ) ; this . _dataProvider = dataProvider ; } public get dataProvider ( ) { return this . _dataProvider ; } } public export class TreeNodeLoader < TDataProvider extends TreeDataProvider > extends AbstractTreeNodeLoaderWithProvider < TDataProvider > implements IDisposable { private _treeDataSource : TreeDataSource ; private _activeRequests = new Map < string | undefined , RxjsObservable < LoadedNodeHierarchy > > ( ) ; constructor ( dataProvider , modelSource ) { super ( modelSource , dataProvider ) ; this . _treeDataSource = new TreeDataSource ( dataProvider ) ; } /** Disposes data source */ public dispose ( ) { this . _treeDataSource . dispose ( ) ; } note protected load ( parentNode ) { const parentItem = isTreeModelNode ( parentNode ) ? parentNode . item : undefined ; return this . loadForParent ( parentItem , parentNode . numChildren === undefined ) ; } private loadForParent ( parentItem , requestNumChildren ) { const parentId = parentItem && parentItem . id ; const activeRequest = this . _activeRequests . get ( parentId ) ; if ( activeRequest ) { return activeRequest ; } const newRequest = requestLoadedHierarchy ( parentItem , this . _treeDataSource , 0 , 0 , requestNumChildren , ( ) => { this . _activeRequests . delete ( parentId ) ; } ) ; this . _activeRequests . set ( parentId , newRequest ) ; return newRequest ; } } public export class PagedTreeNodeLoader < TDataProvider extends TreeDataProvider > extends AbstractTreeNodeLoaderWithProvider < TDataProvider > implements IDisposable { private _pageLoader : PageLoader ; private _pageSize : number ; constructor ( dataProvider , modelSource , pageSize ) { super ( modelSource , dataProvider ) ; this . _pageLoader = new PageLoader ( new TreeDataSource ( dataProvider ) , pageSize ) ; this . _pageSize = pageSize ; } /** Disposes data source */ public dispose ( ) { this . _pageLoader . dispose ( ) ; } /** Returns page size used by tree node loader. */ public get pageSize ( ) : number { return this . _pageSize ; } note protected load ( parentNode , childIndex ) { const parentItem = isTreeModelNode ( parentNode ) ? parentNode . item : undefined ; return this . _pageLoader . loadPageWithItem ( parentItem , childIndex , parentNode . numChildren === undefined ) ; } } public export interface LoadedNodeHierarchy { /** Node id of the parent node for loaded hierarchy. */ parentId : string | undefined ; /** Hierarchy items offset in parent node children array. */ offset : number ; /** Loaded hierarchy items. */ hierarchyItems : LoadedNodeHierarchyItem [ ] ; /** Number of children parent node has. */ numChildren ? : number ; } public export interface LoadedNodeHierarchyItem { /** Loaded tree node item. */ item : TreeNodeItemData ; /** Children of loaded tree node item. */ children ? : LoadedNodeHierarchyItem [ ] ; /** Number of children tree node item has. */ numChildren ? : number ; } class PageLoader implements IDisposable { private _dataSource : TreeDataSource ; private _pageSize : number ; private _activePageRequests = new Map < string | undefined , Map < number , RxjsObservable < LoadedNodeHierarchy > > > ( ) ; constructor ( dataSource , pageSize ) { this . _dataSource = dataSource ; this . _pageSize = pageSize ; } public dispose ( ) { this . _dataSource . dispose ( ) ; } public loadPageWithItem ( parentItem , itemIndex , requestNumChildren ) { const parentId = parentItem && parentItem . id ; const parentPageRequests = this . _activePageRequests . get ( parentId ) || new Map < number , RxjsObservable < LoadedNodeHierarchy > > ( ) ; const page = Math . trunc ( itemIndex / this . _pageSize ) ; const activeRequest = parentPageRequests . get ( page ) ; if ( activeRequest ) { return activeRequest ; } const startIndex = page * this . _pageSize ; const newRequest = requestLoadedHierarchy ( parentItem , this . _dataSource , startIndex , this . _pageSize , requestNumChildren , ( ) => { parentPageRequests . delete ( page ) ; if ( parentPageRequests . size === 0 ) { this . _activePageRequests . delete ( parentId ) ; } } ) ; parentPageRequests . set ( page , newRequest ) ; this . _activePageRequests . set ( parentId , parentPageRequests ) ; return newRequest ; } } function requestLoadedHierarchy ( parentItem , dataSource , start , take , requestNumChildren , finalizeCallback ) { const parentId = parentItem && parentItem . id ; return dataSource . requestItems ( parentItem , start , take , requestNumChildren ) . pipe ( concatMap ( ( { numChildren , loadedItems } ) => loadHierarchy ( loadedItems , dataSource , take ) . pipe ( map ( ( hierarchyItems ) => ( { parentId , offset : start , hierarchyItems , numChildren } ) ) ) ) , finalize ( finalizeCallback ) , publish ( ) , refCount ( ) ) ; } function loadHierarchy ( rootItems , dataSource , take ) { return from ( rootItems ) . pipe ( concatMap ( ( item ) => { if ( ! item . autoExpand || item . children ) { return of ( convertToLoadedNodeHierarchyItem ( item ) ) ; } return dataSource . requestItems ( item , 0 , take , true ) . pipe ( concatMap ( ( { numChildren , loadedItems } ) => loadHierarchy ( loadedItems , dataSource , take ) . pipe ( map ( ( children ) => ( { item , children , numChildren } ) ) ) ) ) ; } ) , toArray ( ) ) ; } function convertToLoadedNodeHierarchyItem ( item ) { return { item , children : item . children ? item . children . map ( ( child ) => convertToLoadedNodeHierarchyItem ( child ) ) : undefined , numChildren : item . children ? item . children . length : undefined } ; } function collectTreeNodeItems ( hierarchyItems , result = [ ] ) { for ( const hierarchyItem of hierarchyItems ) { result . push ( hierarchyItem . item ) ; if ( hierarchyItem . children ) collectTreeNodeItems ( hierarchyItem . children , result ) ; } return result ; } internal export function handleLoadedNodeHierarchy ( modelSource , loadedHierarchy ) { modelSource . modifyModel ( ( model ) => { if ( loadedHierarchy . parentId !== undefined ) { if ( model . getNode ( loadedHierarchy . parentId ) === undefined ) return ; } updateChildren ( model , loadedHierarchy . parentId , loadedHierarchy . hierarchyItems , loadedHierarchy . offset , loadedHierarchy . numChildren ) ; if ( loadedHierarchy . parentId !== undefined ) { const parentNode = model . getNode ( loadedHierarchy . parentId ) ; if ( parentNode && parentNode . isLoading && parentNode . numChildren !== undefined ) { parentNode . isLoading = false ; } } } ) ; } function updateChildren ( model , parentId , hierarchyItems , startIndex , numChildren ? ) { if ( numChildren !== undefined ) { model . setNumChildren ( parentId , numChildren ) ; } if ( model . getChildren ( parentId ) === undefined ) { return ; } let offset = startIndex ; for ( const hierarchyItem of hierarchyItems ) { const nodeInput = convertToTreeModelNodeInput ( hierarchyItem . item ) ; const existingNode = model . getNode ( hierarchyItem . item . id ) ; if ( ! existingNode || ! existingNode . isExpanded || model . getChildOffset ( parentId , existingNode . id ) !== offset || nodeInput . numChildren === 0 ) { model . setChildren ( parentId , [ nodeInput ] , offset ) ; } else { existingNode . label = nodeInput . label ; existingNode . description = nodeInput . description ?? "" ; existingNode . item = nodeInput . item ; } if ( hierarchyItem . children ) { updateChildren ( model , hierarchyItem . item . id , hierarchyItem . children , 0 , hierarchyItem . numChildren ) ; } offset ++ ; } } function convertToTreeModelNodeInput ( item ) { let numChildren ; if ( item . children ) { numChildren = item . children . length ; } else if ( ! item . hasChildren ) { numChildren = 0 ; } return { description : item . description , isExpanded : ! ! item . autoExpand , id : item . id , item , label : item . label , isLoading : false , numChildren , isSelected : false } ; } interface TreeDataSourceResult { loadedItems : TreeNodeItemData [ ] ; numChildren ? : number ; } internal export class TreeDataSource implements IDisposable { private _dataProvider : TreeDataProvider ; private _disposeTreeNodesChangedListener ? : ( ) => void ; public readonly onItemsChanged = new BeEvent < TreeDataChangesListener > ( ) ; constructor ( dataProvider ) { this . _dataProvider = dataProvider ; if ( isTreeDataProviderInterface ( this . _dataProvider ) && this . _dataProvider . onTreeNodeChanged ) { this . _disposeTreeNodesChangedListener = this . _dataProvider . onTreeNodeChanged . addListener ( ( changedItems ) => this . onItemsChanged . raiseEvent ( changedItems ) ) ; } } public dispose ( ) { this . _disposeTreeNodesChangedListener && this . _disposeTreeNodesChangedListener ( ) ; } public requestItems ( parent , firstItemIndex , numItems , requestNumChildren ) { return defer ( async ( ) => { if ( isTreeDataProviderInterface ( this . _dataProvider ) ) { const dataProvider = this . _dataProvider ; return from ( requestNumChildren ? dataProvider . getNodesCount ( parent ) : [ undefined ] ) . pipe ( concatMap ( async ( numChildren ) => { const pageOptions = numItems !== 0 ? { size : numItems , start : firstItemIndex } : undefined ; return { loadedItems : await dataProvider . getNodes ( parent , pageOptions ) , numChildren } ; } ) ) ; } return from ( this . getItems ( parent ) ) . pipe ( map ( ( loadedItems ) => ( { loadedItems : numItems !== 0 ? loadedItems . slice ( firstItemIndex , firstItemIndex + numItems ) : loadedItems , numChildren : loadedItems . length } ) ) ) ; } ) . pipe ( concatAll ( ) , publish ( ) , refCount ( ) ) ; } private async getItems ( parent ) { if ( isTreeDataProviderRaw ( this . _dataProvider ) ) { return this . getChildren ( this . _dataProvider , parent ) ; } if ( isTreeDataProviderMethod ( this . _dataProvider ) ) { return this . _dataProvider ( parent ) ; } if ( isTreeDataProviderPromise ( this . _dataProvider ) ) { this . _dataProvider = await this . _dataProvider ; return this . getChildren ( this . _dataProvider , parent ) ; } throw new UiError ( UiComponents . loggerCategory ( this ) , "Unsupported TreeDataProvider." ) ; } private getChildren ( rawProvider , parent ) { if ( parent === undefined ) return rawProvider ; return ( parent as ImmediatelyLoadedTreeNodeItem ) . children ?? [ ] ; } }