@rxap/tree
Version:
This package provides a tree component and data source for Angular applications. It includes features such as searching, filtering, and displaying hierarchical data. The package also offers directives for customizing the content of tree nodes.
1,227 lines (1,037 loc) • 42.4 kB
HTML
<html class="no-js" lang="">
<head>
<meta charset="utf-8">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<title>angular-tree</title>
<meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="../images/favicon.ico">
<link rel="stylesheet" href="../styles/style.css">
<link rel="stylesheet" href="../styles/dark.css">
</head>
<body>
<script>
// Blocking script to avoid flickering dark mode
// Dark mode toggle button
var useDark = window.matchMedia('(prefers-color-scheme: dark)');
var darkModeState = useDark.matches;
var $darkModeToggleSwitchers = document.querySelectorAll('.dark-mode-switch input');
var $darkModeToggles = document.querySelectorAll('.dark-mode-switch');
var darkModeStateLocal = localStorage.getItem('compodoc_darkmode-state');
function checkToggle(check) {
for (var i = 0; i < $darkModeToggleSwitchers.length; i++) {
$darkModeToggleSwitchers[i].checked = check;
}
}
function toggleDarkMode(state) {
if (window.localStorage) {
localStorage.setItem('compodoc_darkmode-state', state);
}
checkToggle(state);
const hasClass = document.body.classList.contains('dark');
if (state) {
for (var i = 0; i < $darkModeToggles.length; i++) {
$darkModeToggles[i].classList.add('dark');
}
if (!hasClass) {
document.body.classList.add('dark');
}
} else {
for (var i = 0; i < $darkModeToggles.length; i++) {
$darkModeToggles[i].classList.remove('dark');
}
if (hasClass) {
document.body.classList.remove('dark');
}
}
}
useDark.addEventListener('change', function (evt) {
toggleDarkMode(evt.matches);
});
if (darkModeStateLocal) {
darkModeState = darkModeStateLocal === 'true';
}
toggleDarkMode(darkModeState);
</script>
<div class="navbar navbar-default navbar-fixed-top d-md-none p-0">
<div class="d-flex">
<a href="../" class="navbar-brand">angular-tree</a>
<button type="button" class="btn btn-default btn-menu ion-ios-menu" id="btn-menu"></button>
</div>
</div>
<div class="xs-menu menu" id="mobile-menu">
<div id="book-search-input" role="search"><input type="text" placeholder="Type to search"></div> <compodoc-menu></compodoc-menu>
</div>
<div class="container-fluid main">
<div class="row main">
<div class="d-none d-md-block menu">
<compodoc-menu mode="normal"></compodoc-menu>
</div>
<!-- START CONTENT -->
<div class="content injectable">
<div class="content-data">
<ol class="breadcrumb">
<li class="breadcrumb-item">Injectables</li>
<li class="breadcrumb-item" >DefaultTreeApplyFilterMethod</li>
</ol>
<ul class="nav nav-tabs" role="tablist">
<li class="nav-item">
<a href="#info"
class="nav-link"
class="nav-link active"
role="tab" id="info-tab" data-bs-toggle="tab" data-link="info">Info</a>
</li>
<li class="nav-item">
<a href="#source"
class="nav-link"
role="tab" id="source-tab" data-bs-toggle="tab" data-link="source">Source</a>
</li>
</ul>
<div class="tab-content">
<div class="tab-pane fade active in" id="info">
<p class="comment">
<h3>File</h3>
</p>
<p class="comment">
<code>src/lib/tree.data-source.ts</code>
</p>
<section data-compodoc="block-index">
<h3 id="index">Index</h3>
<table class="table table-sm table-bordered index-table">
<tbody>
<tr>
<td class="col-md-4">
<h6><b>Properties</b></h6>
</td>
</tr>
<tr>
<td class="col-md-4">
<ul class="index-list">
<li>
<span class="modifier">Protected</span>
<a href="#lastFilter" >lastFilter</a>
</li>
</ul>
</td>
</tr>
<tr>
<td class="col-md-4">
<h6><b>Methods</b></h6>
</td>
</tr>
<tr>
<td class="col-md-4">
<ul class="index-list">
<li>
<a href="#call" >call</a>
</li>
<li>
<span class="modifier">Protected</span>
<a href="#isEqualToLastFilter" >isEqualToLastFilter</a>
</li>
</ul>
</td>
</tr>
</tbody>
</table>
</section>
<section data-compodoc="block-methods">
<h3 id="methods">
Methods
</h3>
<table class="table table-sm table-bordered">
<tbody>
<tr>
<td class="col-md-4">
<a name="call"></a>
<span class="name">
<span ><b>call</b></span>
<a href="#call"><span class="icon ion-ios-link"></span></a>
</span>
</td>
</tr>
<tr>
<td class="col-md-4">
<code>call(undefined: <a href="../interfaces/TreeApplyFilterParameter.html" target="_self">TreeApplyFilterParameter</a>)</code>
</td>
</tr>
<tr>
<td class="col-md-4">
<div class="io-line">Defined in <a href="" data-line="131"
class="link-to-prism">src/lib/tree.data-source.ts:131</a></div>
</td>
</tr>
<tr>
<td class="col-md-4">
<div class="io-description">
<b>Parameters :</b>
<table class="params">
<thead>
<tr>
<td>Name</td>
<td>Type</td>
<td>Optional</td>
</tr>
</thead>
<tbody>
<tr>
<td>
<code><a href="../interfaces/TreeApplyFilterParameter.html" target="_self" >TreeApplyFilterParameter</a></code>
</td>
<td>
No
</td>
</tr>
</tbody>
</table>
</div>
<div class="io-description">
<b>Returns : </b> <code>Array | Promise</code>
</div>
<div class="io-description">
</div>
</td>
</tr>
</tbody>
</table>
<table class="table table-sm table-bordered">
<tbody>
<tr>
<td class="col-md-4">
<a name="isEqualToLastFilter"></a>
<span class="name">
<span class="modifier">Protected</span>
<span ><b>isEqualToLastFilter</b></span>
<a href="#isEqualToLastFilter"><span class="icon ion-ios-link"></span></a>
</span>
</td>
</tr>
<tr>
<td class="col-md-4">
<span class="modifier-icon icon ion-ios-reset"></span>
<code>isEqualToLastFilter(filter: <a href="../interfaces/ISearchForm.html" target="_self">ISearchForm</a>)</code>
</td>
</tr>
<tr>
<td class="col-md-4">
<div class="io-line">Defined in <a href="" data-line="215"
class="link-to-prism">src/lib/tree.data-source.ts:215</a></div>
</td>
</tr>
<tr>
<td class="col-md-4">
<div class="io-description">
<b>Parameters :</b>
<table class="params">
<thead>
<tr>
<td>Name</td>
<td>Type</td>
<td>Optional</td>
</tr>
</thead>
<tbody>
<tr>
<td>filter</td>
<td>
<code><a href="../interfaces/ISearchForm.html" target="_self" >ISearchForm</a></code>
</td>
<td>
No
</td>
</tr>
</tbody>
</table>
</div>
<div class="io-description">
<b>Returns : </b> <code><a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/boolean" target="_blank" >boolean</a></code>
</div>
<div class="io-description">
</div>
</td>
</tr>
</tbody>
</table>
</section>
<section data-compodoc="block-properties">
<h3 id="inputs">
Properties
</h3>
<table class="table table-sm table-bordered">
<tbody>
<tr>
<td class="col-md-4">
<a name="lastFilter"></a>
<span class="name">
<span class="modifier">Protected</span>
<span ><b>lastFilter</b></span>
<a href="#lastFilter"><span class="icon ion-ios-link"></span></a>
</span>
</td>
</tr>
<tr>
<td class="col-md-4">
<i>Type : </i> <code><a href="../injectables/SearchForm.html" target="_self" >ISearchForm | null</a></code>
</td>
</tr>
<tr>
<td class="col-md-4">
<i>Default value : </i><code>null</code>
</td>
</tr>
<tr>
<td class="col-md-4">
<div class="io-line">Defined in <a href="" data-line="129" class="link-to-prism">src/lib/tree.data-source.ts:129</a></div>
</td>
</tr>
</tbody>
</table>
</section>
</div>
<div class="tab-pane fade tab-source-code" id="source">
<pre class="line-numbers compodoc-sourcecode"><code class="language-typescript">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();
}
}
}
</code></pre>
</div>
</div>
</div><div class="search-results">
<div class="has-results">
<h1 class="search-results-title"><span class='search-results-count'></span> results matching "<span class='search-query'></span>"</h1>
<ul class="search-results-list"></ul>
</div>
<div class="no-results">
<h1 class="search-results-title">No results matching "<span class='search-query'></span>"</h1>
</div>
</div>
</div>
<!-- END CONTENT -->
</div>
</div>
<label class="dark-mode-switch">
<input type="checkbox">
<span class="slider">
<svg class="slider-icon" viewBox="0 0 24 24" fill="none" height="20" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" width="20" xmlns="http://www.w3.org/2000/svg">
<path d="M21 12.79A9 9 0 1111.21 3 7 7 0 0021 12.79z"></path>
</svg>
</span>
</label>
<script>
var COMPODOC_CURRENT_PAGE_DEPTH = 1;
var COMPODOC_CURRENT_PAGE_CONTEXT = 'injectable';
var COMPODOC_CURRENT_PAGE_URL = 'DefaultTreeApplyFilterMethod.html';
var MAX_SEARCH_RESULTS = 15;
</script>
<script>
$darkModeToggleSwitchers = document.querySelectorAll('.dark-mode-switch input');
checkToggle(darkModeState);
if ($darkModeToggleSwitchers.length > 0) {
for (var i = 0; i < $darkModeToggleSwitchers.length; i++) {
$darkModeToggleSwitchers[i].addEventListener('change', function (event) {
darkModeState = !darkModeState;
toggleDarkMode(darkModeState);
});
}
}
</script>
<script src="../js/libs/custom-elements.min.js"></script>
<script src="../js/libs/lit-html.js"></script>
<script src="../js/menu-wc.js" defer></script>
<script nomodule src="../js/menu-wc_es5.js" defer></script>
<script src="../js/libs/bootstrap-native.js"></script>
<script src="../js/libs/es6-shim.min.js"></script>
<script src="../js/libs/EventDispatcher.js"></script>
<script src="../js/libs/promise.min.js"></script>
<script src="../js/libs/zepto.min.js"></script>
<script src="../js/compodoc.js"></script>
<script src="../js/tabs.js"></script>
<script src="../js/menu.js"></script>
<script src="../js/libs/clipboard.min.js"></script>
<script src="../js/libs/prism.js"></script>
<script src="../js/sourceCode.js"></script>
<script src="../js/search/search.js"></script>
<script src="../js/search/lunr.min.js"></script>
<script src="../js/search/search-lunr.js"></script>
<script src="../js/search/search_index.js"></script>
<script src="../js/lazy-load-graphs.js"></script>
</body>
</html>