File

src/lib/tree.data-source.ts

Extends

BaseDataSource

Index

Properties
Methods
Accessors

Constructor

constructor(rootRemoteMethod: Method, childrenRemoteMethod: Method> | null, applyFilterMethod: Method<Array<Node<Data>> | TreeApplyFilterParameter> | null, metadata: TreeDataSourceMetadata | null)
Parameters :
Name Type Optional
rootRemoteMethod Method<Data | [] | RootParameters> No
childrenRemoteMethod Method<Data[] | Node<Data>> | null No
applyFilterMethod Method<Array<Node<Data>> | TreeApplyFilterParameter> | null No
metadata TreeDataSourceMetadata | null No

Methods

Protected _connect
_connect(collectionViewer: Required)
Parameters :
Name Type Optional
collectionViewer Required<BaseDataSourceViewer> No
Returns : Observable<Array<Node<Data>>>
Public collapseNode
collapseNode(node: Node, options?: EventOptions)
Parameters :
Name Type Optional
node Node<Data> No
options EventOptions Yes
Returns : Promise<void>
Public deselectNode
deselectNode(node: Node)
Parameters :
Name Type Optional
node Node<Data> No
Returns : Promise<void>
Public destroy
destroy()
Returns : void
Public Async expandNode
expandNode(node: Node, options?: EventOptions)
Parameters :
Name Type Optional
node Node<Data> No
options EventOptions Yes
Returns : Promise<void>
Public flatTree
flatTree(tree: Node, all)

Converts the tree structure into a list.

Parameters :
Name Type Optional Default value Description
tree Node<Data> No
all No false

true - include nodes children that are not expanded

Returns : Array<Node<Data>>
Public Async getChildren
getChildren(node: Node)
Parameters :
Name Type Optional
node Node<Data> No
Returns : Promise<Data[]>
Public getNodeById
getNodeById(id: string)
Parameters :
Name Type Optional
id string No
Returns : Node | null
Public Async getRoot
getRoot(options: literal type)
Parameters :
Name Type Optional Default value
options literal type No {}
Returns : Promise<Data | []>
Public Async getRootParameters
getRootParameters(options: literal type)
Parameters :
Name Type Optional Default value
options literal type No {}
Returns : Promise<RootParameters>
Public Async getTreeRoot
getTreeRoot(options: literal type)
Parameters :
Name Type Optional Default value
options literal type No {}
Returns : Promise<Array<Node<Data>>>
Public Async refresh
refresh()
Returns : Promise<any>
Public refreshMatchFilter
refreshMatchFilter()
Returns : void
Public reset
reset()
Returns : any
Public selectNode
selectNode(node: Node)
Parameters :
Name Type Optional
node Node<Data> No
Returns : Promise<void>
Public setGetIcon
setGetIcon(getIcon: NodeGetIconFunction)
Parameters :
Name Type Optional Default value
getIcon NodeGetIconFunction<Data> No this.getIcon
Returns : void
Public setGetStyle
setGetStyle(getStyle: NodeGetStyleFunction)
Parameters :
Name Type Optional Default value
getStyle NodeGetStyleFunction<any> No this.getStyle
Returns : void
Public setGetType
setGetType(getType: NodeGetTypeFunction)
Parameters :
Name Type Optional Default value
getType NodeGetTypeFunction<any> No this.getType
Returns : void
Public setHasDetails
setHasDetails(hasDetails: NodeHasDetailsFunction)
Parameters :
Name Type Optional Default value
hasDetails NodeHasDetailsFunction<Data> No this.hasDetails
Returns : void
Public setMatchFilter
setMatchFilter(matchFilter: (node: Node) => void)
Parameters :
Name Type Optional
matchFilter function No
Returns : void
Public setToDisplay
setToDisplay(toDisplay: NodeToDisplayFunction)
Parameters :
Name Type Optional Default value
toDisplay NodeToDisplayFunction<Data> No this.toDisplay
Returns : void
Public setTreeControl
setTreeControl(treeControl: FlatTreeControl>)
Parameters :
Name Type Optional
treeControl FlatTreeControl<Node<Data>> No
Returns : void
Public Async toNode
toNode(parent: Node | null, item: Data, depth: number, onExpand: ExpandNodeFunction, onCollapse: ExpandNodeFunction, onSelect: ExpandNodeFunction, onDeselect: ExpandNodeFunction)
Parameters :
Name Type Optional Default value
parent Node<Data> | null No
item Data No
depth number No 0
onExpand ExpandNodeFunction<Data> No this.expandNode.bind(this)
onCollapse ExpandNodeFunction<Data> No this.collapseNode.bind(this)
onSelect ExpandNodeFunction<Data> No this.selectNode.bind(this)
onDeselect ExpandNodeFunction<Data> No this.deselectNode.bind(this)
Returns : Promise<Node<Data>>
Public updateNodes
updateNodes()

recall the getStyle, getIcon and toDisplay methods and update the node objects

Returns : void

Properties

Protected _data$
Default value : new BehaviorSubject<Array<Node<Data>>>([])
Public Readonly childrenRemoteMethod
Type : Method<Data[] | Node<Data>> | null
Default value : null
Decorators :
@Optional()
@Inject(RXAP_TREE_DATA_SOURCE_CHILDREN_REMOTE_METHOD)
Public expanded
Type : SelectionModel<string>
Public getIcon
Type : NodeGetIconFunction<Data>
Default value : () => {...}
Public getStyle
Type : NodeGetStyleFunction<Data>
Default value : () => {...}
Public getType
Type : NodeGetTypeFunction<Data>
Default value : () => {...}
Public hasDetails
Type : NodeHasDetailsFunction<Data>
Default value : () => {...}
Public loading$
Default value : new ToggleSubject(true)
Public matchFilter
Type : function
Default value : () => {...}
Public Readonly rootRemoteMethod
Type : Method<Data | [] | RootParameters>
Decorators :
@Inject(RXAP_TREE_DATA_SOURCE_ROOT_REMOTE_METHOD)
Public searchForm
Type : SearchForm | null
Default value : null
Public selected
Type : SelectionModel<Node<Data>>
Public toDisplay
Type : NodeToDisplayFunction<Data>
Default value : () => {...}
Public tree$
Default value : new BehaviorSubject<Array<Node<Data>>>([])
Public treeControl
Type : FlatTreeControl<Node<Data>>
Decorators :
@Required()

Accessors

nodeParameters
getnodeParameters()
setnodeParameters(nodeParameters: NodeParameters | null)
Parameters :
Name Type Optional
nodeParameters NodeParameters | null No
Returns : void
import {
  SelectionChange,
  SelectionModel,
} from '@angular/cdk/collections';
import { FlatTreeControl } from '@angular/cdk/tree';
import {
  Inject,
  Injectable,
  InjectionToken,
  isDevMode,
  OnInit,
  Optional,
} from '@angular/core';
import {
  BaseDataSource,
  BaseDataSourceMetadata,
  BaseDataSourceViewer,
  RXAP_DATA_SOURCE_METADATA,
  RxapDataSource,
} from '@rxap/data-source';
import {
  EventOptions,
  ExpandNodeFunction,
  Node,
  NodeGetIconFunction,
  NodeGetStyleFunction,
  NodeGetTypeFunction,
  NodeHasDetailsFunction,
  NodeToDisplayFunction,
} from '@rxap/data-structure-tree';
import { Method } from '@rxap/pattern';
import { ToggleSubject } from '@rxap/rxjs';
import {
  coerceArray,
  getIdentifierPropertyValue,
  joinPath,
  Required,
  WithChildren,
  WithIdentifier,
} from '@rxap/utilities';
import {
  BehaviorSubject,
  combineLatest,
  debounceTime,
  from,
  merge,
  Observable,
  Subject,
  Subscription,
} from 'rxjs';
import {
  map,
  startWith,
  switchMap,
  tap,
} from 'rxjs/operators';
import {
  ISearchForm,
  SearchForm,
} from './search.form';

export function isSelectionChange<T>(obj: any): obj is SelectionChange<T> {
  return !!obj && obj['added'] !== undefined && obj['removed'] !== undefined;
}

export interface TreeDataSourceMetadata extends BaseDataSourceMetadata {
  selectMultiple?: boolean;
  expandMultiple?: boolean;
  scopeTypes?: string[];
  /**
   * If true the tree will be refreshed with caching disbabled after the first load
   */
  autoRefreshWithoutCache?: boolean;
}

export const RXAP_TREE_DATA_SOURCE_ROOT_REMOTE_METHOD     = new InjectionToken(
  'rxap/tree/data-source/root-remote-method',
);
export const RXAP_TREE_DATA_SOURCE_CHILDREN_REMOTE_METHOD = new InjectionToken(
  'rxap/tree/data-source/children-remote-method',
);

export const RXAP_TREE_DATA_SOURCE_APPLY_FILTER_METHOD = new InjectionToken(
  'rxap/tree/data-source/apply-filter-method');

export function flatTree<Data extends WithIdentifier & WithChildren = any>(
  tree: Node<Data> | Array<Node<Data>>,
  all = false,
): Array<Node<Data>> {
  tree = coerceArray(tree);

  function flat(acc: any[], list: any[]) {
    return [ ...acc, ...list ];
  }

  const _flatTree = (node: Node<Data>): Array<Node<Data>> => {
    if (!Array.isArray(node.children)) {
      if (isDevMode()) {
        console.log(node);
      }
      throw new Error('Node has not defined children');
    }

    if (all || node.expanded) {
      return [
        node,
        ...node.children.map((child) => _flatTree(child)).reduce(flat, []),
      ];
    } else {
      return [ node ];
    }
  };

  return tree
  .map((child) => _flatTree(child))
  .reduce((acc, items) => [ ...acc, ...items ], []);
}

export interface TreeApplyFilterParameter<Form extends ISearchForm = ISearchForm, Data extends WithIdentifier & WithChildren = any> {
  tree: Array<Node<Data>>,
  filter: Form;
  scopeTypes?: string[]
}

@Injectable()
export class DefaultTreeApplyFilterMethod<Data extends WithIdentifier & WithChildren = any>
  implements Method<Array<Node<Data>>, TreeApplyFilterParameter> {

  protected lastFilter: ISearchForm | null = null;

  call(
    {tree, filter, scopeTypes}: TreeApplyFilterParameter,
  ): Array<Node<Data>> | Promise<Array<Node<Data>>> {

    const nodes = flatTree(tree, true);

    // if (this.isEqualToLastFilter(filter)) {
    //   return flatTree(tree, false).filter(node => node.isVisible);
    // }

    const hasScopeFilter = (
      filter.scope &&
      Object.keys(filter.scope) &&
      Object.values(filter.scope).some((list) => list.length > 0)
    );

    // if not scope and the search filter is an empty string, collapse all non-root nodes
    if (!filter.search && filter.search !== this.lastFilter?.search) {
      nodes
      .filter(node => node.parent)
      .forEach(node => node.collapse({quite: true}));
    }

    if (filter.search || hasScopeFilter) {

      nodes.forEach(node => node.hide());

      for (const [ type, list ] of Object.entries(filter.scope ?? {})) {
        for (const node of nodes) {
          if (node.type === type) {
            if (list.some(item => getIdentifierPropertyValue(item) === node.id)) {
              node.show({forEachChildren: true});
            }
          }
        }
      }

      if (filter.search) {
        for (const node of nodes) {
          if (hasScopeFilter && node.hidden) {
            // if the filter has a scope filter. Only check the search filter on nodes that are shown by the scope
            // filter
            continue;
          }
          const display = node.display?.toLowerCase();
          if (display) {
            if (display.includes(filter.search.toLowerCase())) {
              node.show({parents: true});
            } else {
              node.hide();
            }
          } else {
            // if node has no display, it is not shown
            node.hide();
          }
        }
        // hide each node that has no visible children
        // there exists the edge case that a node has visible children, but the node.hide() function is called
        // after the child.show({ parent:true })
        for (const node of nodes.filter(n => n.hidden && n.hasChildren && n.children.some(child => child.isVisible))) {
          if (node.hidden) {
            console.warn('Edge case detected. Node has visible children but is hidden.');
            node.show();
          }
        }
        // expand each node that has visible children
        for (const node of nodes.filter(n => n.hasChildren)) {
          if (node.children.some(child => child.isVisible)) {
            // set quite to true to prevent the tree from reloading -> this would result in a infinite loop
            node.expand({quite: true});
          }
        }
      }

    } else {
      nodes.forEach(node => node.show());
    }

    this.lastFilter = filter;

    return flatTree(tree, false).filter(node => node.isVisible);

  }

  protected isEqualToLastFilter(filter: ISearchForm): boolean {
    if (this.lastFilter) {
      if (this.lastFilter.search === filter.search) {
        if (this.lastFilter.scope && filter.scope) {
          if (Object.keys(this.lastFilter.scope).every(key => Object.keys(filter.scope).includes(key))) {
            if (Object.entries(this.lastFilter.scope)
            .every(([ key, scope ]) => scope.some(item => filter.scope[key].includes(item)))) {
              return true;
            }
          }
        }
        if (filter.scope === this.lastFilter.scope) {
          return true;
        }
      }
    }
    return false;
  }

}

@RxapDataSource('tree')
@Injectable()
export class TreeDataSource<
  Data extends WithIdentifier & WithChildren = any,
  RootParameters = any,
  NodeParameters = any,
> extends BaseDataSource<Array<Node<Data>>, TreeDataSourceMetadata> implements OnInit {
  public tree$                                                   = new BehaviorSubject<Array<Node<Data>>>([]);
  @Required public treeControl!: FlatTreeControl<Node<Data>>;
  public selected!: SelectionModel<Node<Data>>;
  public expanded!: SelectionModel<string>;
  // TODO : änlich problem wie bei der redundaten expand SelectionModel.
  public override loading$                                       = new ToggleSubject(true);
  public searchForm: SearchForm | null                           = null;
  protected override _data$                                      = new BehaviorSubject<Array<Node<Data>>>([]);
  private _expandedLocalStorageSubscription: Subscription | null = null;
  private _selectedLocalStorageSubscription: Subscription | null = null;
  // im localStorage wird nur die id gespeichert.
  private _preSelected: string[]                                 = [];
  private _refreshMatchFilter                                    = new Subject<void>();
  private readonly applyFilterMethod: Method<Array<Node<Data>>, TreeApplyFilterParameter>;

  constructor(
    @Inject(RXAP_TREE_DATA_SOURCE_ROOT_REMOTE_METHOD)
    public readonly rootRemoteMethod: Method<Data | Data[], RootParameters>,
    @Optional()
    @Inject(RXAP_TREE_DATA_SOURCE_CHILDREN_REMOTE_METHOD)
    public readonly childrenRemoteMethod: Method<Data[], Node<Data>> | null         = null,
    @Optional()
    @Inject(RXAP_TREE_DATA_SOURCE_APPLY_FILTER_METHOD)
      applyFilterMethod: Method<Array<Node<Data>>, TreeApplyFilterParameter> | null = null,
    @Optional()
    @Inject(RXAP_DATA_SOURCE_METADATA)
      metadata: TreeDataSourceMetadata | null                                       = null,
  ) {
    super(metadata);
    this.applyFilterMethod = applyFilterMethod ?? new DefaultTreeApplyFilterMethod();
    // TODO add new SelectModel class that saves the select model to the localStorage
    this.initSelected();
    this.initExpanded();
  }

  private _nodeParameters: NodeParameters | null = null;

  public get nodeParameters(): NodeParameters | null {
    return this._nodeParameters;
  }

  public set nodeParameters(nodeParameters: NodeParameters | null) {
    this._nodeParameters = nodeParameters;
    this.tree$.value.forEach(node => node.parameters = nodeParameters);
  }

  ngOnInit() {
    if (this.searchForm) {
      combineLatest([
        this.tree$,
        (this.searchForm.rxapFormGroup.value$ as Observable<any>).pipe(debounceTime(1000)),
      ]).pipe(
          switchMap(async ([ tree, filter ]) => await this.applyFilterMethod.call({
            tree,
            filter,
            scopeTypes: this.metadata?.scopeTypes,
          })),
          map(nodes => coerceArray(nodes)),
        )
        .subscribe(data => this._data$.next(data));
    } else {
      this.tree$.pipe(
        map(tree => flatTree(tree).filter(node => node.isVisible)),
        tap(nodes => nodes.forEach(node => node.show())),
      ).subscribe(data => this._data$.next(data));
    }
  }

  public toDisplay: NodeToDisplayFunction<Data> = () =>
    'to display function not defined';

  public getIcon: NodeGetIconFunction<Data> = () => null;

  public getType: NodeGetTypeFunction<Data> = () => null;

  public getStyle: NodeGetStyleFunction<Data> = () => (
    {}
  );

  public hasDetails: NodeHasDetailsFunction<Data> = () => true;

  public matchFilter: (node: Node<Data>) => boolean = () => true;

  public async getTreeRoot(options: { cache?: boolean } = {}): Promise<Array<Node<Data>>> {
    this.loading$.enable();
    const root: Data | Data[] = await this.getRoot(options);

    let rootNodes: Array<Node<Data>>;

    if (Array.isArray(root)) {
      rootNodes = await Promise.all(root.map((node) => this.toNode(null, node)));
    } else {
      rootNodes = [ await this.toNode(null, root) ];
    }

    const tmpSelectedNodes: Node<Data>[] = [];

    const restoreExpandAndSelectedState = (node: Node<Data>) => {
      if (this.expanded.isSelected(node.id)) {
        (node as any)._expanded = true;
      }
      if (this.selected.selected.some(n => n.id === node.id)) {
        console.log('restore selected', node.display);
        (node as any)._selected = true;
        tmpSelectedNodes.push(node);
      }
      node.children.forEach(restoreExpandAndSelectedState);
    };

    rootNodes.forEach(restoreExpandAndSelectedState);

    console.log('restore expand and selected state', tmpSelectedNodes);
    this.selected.setSelection(...tmpSelectedNodes);

    this.tree$.next(rootNodes);

    this.loading$.disable();

    return rootNodes;
  }

  public selectNode(node: Node<Data>): Promise<void> {
    if (!this.selected.isMultipleSelection()) {
      if (this.selected.hasValue()) {
        const currentSelectedNode = this.selected.selected[0];
        currentSelectedNode.deselect();
      }
    }
    this.selected.select(node);
    node.parent?.expand();
    return Promise.resolve();
  }

  public setTreeControl(treeControl: FlatTreeControl<Node<Data>>): void {
    this.treeControl = treeControl;
  }

  public setMatchFilter(matchFilter: (node: Node<Data>) => boolean): void {
    this.matchFilter = matchFilter;
  }

  public setToDisplay(
    toDisplay: NodeToDisplayFunction<Data> = this.toDisplay,
  ): void {
    this.toDisplay = toDisplay;
  }

  public setGetIcon(getIcon: NodeGetIconFunction<Data> = this.getIcon): void {
    this.getIcon = getIcon;
  }

  public setHasDetails(
    hasDetails: NodeHasDetailsFunction<Data> = this.hasDetails,
  ): void {
    this.hasDetails = hasDetails;
  }

  public deselectNode(node: Node<Data>): Promise<void> {
    this.selected.deselect(node);
    return Promise.resolve();
  }

  public async expandNode(node: Node<Data>, options?: EventOptions): Promise<void> {
    if (node.parent && !node.parent.expanded) {
      console.log('expand parent', node.parent.display);
      node.parent?.expand({quite: true});
    }
    if (!options?.onlySelf) {
      // required to sync the expanstion state with the tree control
      // if the collpase is trigged by node.expand this state is not
      // sync with the tree control
      this.treeControl.expansionModel.select(node);
    }

    if (node.item.hasChildren && !node.item.children?.length) {
      node.isLoading$.enable();

      const children = await this.getChildren(node);

      // add the loaded children to the item object
      node.item.children = children;

      node.addChildren(
        await Promise.all(children.map((child) =>
          this.toNode(node, child, node.depth + 1, node.onExpand, node.onCollapse),
        )),
      );

      node.isLoading$.disable();
    }

    console.log('expand node', node.display);
    this.expanded.select(node.id);

    // node.parent?.expand({quite: true});

    if (!options?.quite) {
      this.tree$.next(this.tree$.value);
    }

  }

  public async getChildren(node: Node<Data>): Promise<Data[]> {
    if (!this.childrenRemoteMethod) {
      throw new Error(
        `The node '${ node.id }' has unloaded children but the RXAP_TREE_DATA_SOURCE_CHILDREN_REMOTE_METHOD is not provided.`);
    }
    return this.childrenRemoteMethod.call(node);
  }

  public async getRoot(options: { cache?: boolean } = {}): Promise<Data | Data[]> {
    const rootParameters = await this.getRootParameters(options);
    return this.rootRemoteMethod.call(rootParameters);
  }

  public async getRootParameters(options: { cache?: boolean } = {}): Promise<RootParameters> {
    return options as any;
  }

  // TODO : find better solution to allow the overwrite of the toNode method
  // without losing the custom preselect and preexpand function

  public getNodeById(id: string): Node<Data> | null {
    function getNodeById(node: Node<Data>, nodeId: string) {
      if (node.id === nodeId) {
        return node;
      }
      if (node.hasNode(nodeId)) {
        return node.getNode(nodeId);
      } else {
        return null;
      }
    }

    return (
      this.tree$.value
        .map((node) => getNodeById(node, id))
        .filter(Boolean)[0] || null
    );
  }

  public async toNode(
    parent: Node<Data> | null,
    item: Data,
    depth                                = 0,
    onExpand: ExpandNodeFunction<Data>   = this.expandNode.bind(this),
    onCollapse: ExpandNodeFunction<Data> = this.collapseNode.bind(this),
    onSelect: ExpandNodeFunction<Data>   = this.selectNode.bind(this),
    onDeselect: ExpandNodeFunction<Data> = this.deselectNode.bind(this),
  ): Promise<Node<Data>> {
    return Node.ToNode(
      parent,
      item,
      depth,
      onExpand,
      onCollapse,
      this.toDisplay,
      this.getIcon,
      this.getType,
      onSelect,
      onDeselect,
      this.hasDetails,
      this.getStyle,
      this.nodeParameters,
    );
  }

  public collapseNode(node: Node<Data>, options?: EventOptions): Promise<void> {

    if (!options?.onlySelf) {
      // required to sync the expanstion state with the tree control
      // if the collpase is trigged by node.colapse this state is not
      // sync with the tree control
      this.treeControl.expansionModel.deselect(node);
    }

    this.expanded.deselect(node.id);

    if (!options?.quite) {
      this.tree$.next(this.tree$.value);
    }

    return Promise.resolve();
  }

  /**
   * Converts the tree structure into a list.
   *
   * @param tree
   * @param all true - include nodes children that are not expanded
   */
  public flatTree(tree: Node<Data>, all = false): Array<Node<Data>> {
    return flatTree(tree, all);
  }

  public override destroy(): void {
    super.destroy();
    // TODO : add subscription handler to BaseDataSource
    if (this._expandedLocalStorageSubscription) {
      this._expandedLocalStorageSubscription.unsubscribe();
    }
    if (this._selectedLocalStorageSubscription) {
      this._selectedLocalStorageSubscription.unsubscribe();
    }
  }

  public refreshMatchFilter(): void {
    this._refreshMatchFilter.next();
  }

  public override async refresh(): Promise<any> {
    console.log('selected', this.selected.selected.map((node) => node.id));
    console.log('expanded', this.expanded.selected.slice());
    await this.getTreeRoot({cache: false});

    // refresh all expanded nodes;

    // const loadExpandedNodes = async (children: ReadonlyArray<Node<Data>>) => {
    //   for (const child of children) {
    //     if (this.expanded.isSelected(child.id)) {
    //       // call the node method to ensure that the expanded property of
    //       // Node is set.
    //       await child.expand();
    //     }
    //
    //     if (child.hasChildren) {
    //       await loadExpandedNodes(child.children);
    //     }
    //   }
    // };
    //
    // await Promise.all(
    //   rootNodes
    //   .filter((node) => node.hasChildren)
    //   .map((node) => loadExpandedNodes(node.children)),
    // );

    // console.log('selected', this.selected.selected.map((node) => node.id));
    //
    // const preSelected = this.selected.selected.slice();
    // this.selected.clear();
    // preSelected.forEach(node => {
    //   node?.select();
    // });

    // const selected: Array<Node<Data>> = this.selected.selected
    // .map((node) => this.getNodeById(node.id))
    // .filter(Boolean) as any;
    // this.selected.clear();
    // this.selected.select(...selected);
  }

  public override reset(): any {
    this.selected.clear();
    this.expanded.clear();
    return this.getTreeRoot();
  }

  public setGetStyle(getStyle: NodeGetStyleFunction<any> = this.getStyle) {
    this.getStyle = getStyle;
  }

  public setGetType(getType: NodeGetTypeFunction<any> = this.getType) {
    this.getType = getType;
  }

  /**
   * recall the getStyle, getIcon and toDisplay methods
   * and update the node objects
   */
  public updateNodes() {
    this._data$.value.forEach(node => {
      node.style   = this.getStyle(node.item);
      node.icon    = coerceArray(this.getIcon(node.item));
      node.display = this.toDisplay(node.item);
    });
    this._data$.next(this._data$.value);
  }

  protected override _connect(
    collectionViewer: Required<BaseDataSourceViewer>,
  ): Observable<Array<Node<Data>>> {
    this.init();
    this.treeControl.expansionModel.changed.pipe(
      tap(async change => {
        if (isSelectionChange<Node<Data>>(change)) {
          const promiseList: Array<Promise<any>> = [];
          if (change.added) {
            promiseList.push(...change.added.map((node) => node.expand({onlySelf: true, quite: true})));
          }
          if (change.removed) {
            promiseList.push(...change.removed
              .slice()
              .reverse()
              .map((node) => node.collapse({onlySelf: true, quite: true})));
          }
          await Promise.all(promiseList);
          this.tree$.next(this.tree$.value);
        }
      }),
    ).subscribe();

    let loadRoot: Promise<Array<Node<Data>> | void> = Promise.resolve();

    if (this.tree$.value.length === 0) {
      loadRoot = this.getTreeRoot();
    }

    let autoRefreshExecuted = false;

    return from(loadRoot).pipe(
      // tap((rootNodes) => {
      //   if (rootNodes) {
      //     const promises: Promise<any>[] = [];
      //     if (this.metadata.selectMultiple) {
      //       // if (!this.selected.hasValue()) {
      //       //   promises.push(
      //       //     ...rootNodes
      //       //     .filter((node) => node.hasDetails)
      //       //     .map((node) => node.select()),
      //       //   );
      //       // }
      //       if (!this.expanded.hasValue()) {
      //         promises.push(
      //           ...rootNodes
      //           .filter((node) => node.hasChildren)
      //           .map((node) => node.expand()),
      //         );
      //       }
      //     } else if (rootNodes.length) {
      //       const rootNode = rootNodes[0];
      //       // if (!this.selected.hasValue()) {
      //       //   // TODO : rename hasDetails to isSelectable
      //       //   if (rootNode.hasDetails) {
      //       //     promises.push(rootNode.select());
      //       //   }
      //       // }
      //       if (!this.expanded.hasValue()) {
      //         promises.push(rootNode.expand());
      //       }
      //     }
      //     return Promise.all(promises);
      //   }
      //   return Promise.resolve();
      // }),
      switchMap(() =>
        merge(collectionViewer.viewChange, this._data$).pipe(
          map(() => this._data$.value),
        ),
      ),
      switchMap(nodeList => this._refreshMatchFilter.pipe(
        startWith(null),
        map(() => nodeList.filter(node => this.matchFilter(node))),
      )),
      tap(() => {
        if (this._preSelected.length) {
          console.log('restore selected', this._preSelected);
          this.selected.clear();
          const nodes       = this._preSelected.map((id) => this.getNodeById(id));
          this._preSelected = [];
          nodes.forEach(node => {
            node?.select();
          });
        }
      }),
      tap(() => {
        if (this.metadata.autoRefreshWithoutCache) {
          if (!autoRefreshExecuted) {
            autoRefreshExecuted = true;
            console.log('auto refresh');
            this.refresh();
          }
        }
      }),
    );
  }

  private initSelected(): void {
    const key = joinPath('rxap/tree', this.id, 'selected');
    if (this.metadata['cacheSelected']) {
      if (localStorage.getItem(key)) {
        try {
          this._preSelected = JSON.parse(localStorage.getItem(key)!);
        } catch (e: any) {
          console.error('parse expanded tree data source nodes failed');
        }
      }
    }

    this.selected = new SelectionModel<Node<Data>>(
      !!this.metadata.selectMultiple,
      [],
    );
    if (this.metadata['cacheSelected']) {
      this._selectedLocalStorageSubscription = this.selected.changed
        .pipe(
          tap(() =>
            localStorage.setItem(
              key,
              JSON.stringify(this.selected.selected.map((s) => s.id)),
            ),
          ),
        )
        .subscribe();
    }
  }

  private initExpanded(): void {
    const key    = joinPath('rxap/tree', this.id, 'expanded');
    let expanded = [];
    if (this.metadata['cacheExpanded']) {
      if (localStorage.getItem(key)) {
        try {
          expanded = JSON.parse(localStorage.getItem(key)!);
        } catch (e: any) {
          console.error('parse expanded tree data source nodes failed');
        }
      }
    }

    this.expanded = new SelectionModel<string>(
      this.metadata.expandMultiple !== false,
      expanded,
    );
    if (this.metadata['cacheExpanded']) {
      this._expandedLocalStorageSubscription = this.expanded.changed
        .pipe(
          tap(() =>
            localStorage.setItem(
              key,
              JSON.stringify(this.expanded.selected),
            ),
          ),
        )
        .subscribe();
    }
  }

}

results matching ""

    No results matching ""