UNPKG

@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.

829 lines (821 loc) 50.2 kB
import { __decorate, __metadata } from 'tslib'; import * as i0 from '@angular/core'; import { Injectable, INJECTOR, Optional, SkipSelf, InjectionToken, TemplateRef, Directive, Inject, isDevMode, EventEmitter, signal, ViewContainerRef, ChangeDetectorRef, Component, ChangeDetectionStrategy, Input, ContentChild, Output, ViewChild, HostListener } from '@angular/core'; import { UseFormControl, RxapFormControl, RxapForm, RxapFormBuilder, RXAP_FORM_DEFINITION, RXAP_FORM_INITIAL_STATE } from '@rxap/forms'; import * as i8 from '@angular/cdk/portal'; import { TemplatePortal, PortalModule } from '@angular/cdk/portal'; import { FlatTreeControl } from '@angular/cdk/tree'; import { NgStyle, NgIf, AsyncPipe, NgClass, NgForOf } from '@angular/common'; import * as i5 from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button'; import * as i4 from '@angular/material/checkbox'; import { MatCheckboxModule } from '@angular/material/checkbox'; import * as i7 from '@angular/material/divider'; import { MatDividerModule } from '@angular/material/divider'; import * as i3 from '@angular/material/icon'; import { MatIconModule } from '@angular/material/icon'; import * as i1 from '@angular/material/progress-bar'; import { MatProgressBarModule } from '@angular/material/progress-bar'; import * as i6 from '@angular/material/progress-spinner'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import * as i2 from '@angular/material/tree'; import { MatTreeModule } from '@angular/material/tree'; import { ContenteditableDirective } from '@rxap/contenteditable'; import { Node } from '@rxap/data-structure-tree'; import { IconDirective } from '@rxap/material-directives/icon'; import { coerceArray, getIdentifierPropertyValue, joinPath, Required, DebounceCall } from '@rxap/utilities'; import { switchMap, map, tap, startWith } from 'rxjs/operators'; import { SelectionModel } from '@angular/cdk/collections'; import { BaseDataSource, RXAP_DATA_SOURCE_METADATA, RxapDataSource } from '@rxap/data-source'; import { ToggleSubject } from '@rxap/rxjs'; import { BehaviorSubject, Subject, combineLatest, debounceTime, from, merge } from 'rxjs'; let SearchForm = class SearchForm { static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.1", ngImport: i0, type: SearchForm, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.1", ngImport: i0, type: SearchForm }); } }; __decorate([ UseFormControl(), __metadata("design:type", RxapFormControl) ], SearchForm.prototype, "search", void 0); __decorate([ UseFormControl(), __metadata("design:type", RxapFormControl) ], SearchForm.prototype, "scope", void 0); SearchForm = __decorate([ RxapForm({ id: 'search', autoSubmit: 500, }) ], SearchForm); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.1", ngImport: i0, type: SearchForm, decorators: [{ type: Injectable }], propDecorators: { search: [], scope: [] } }); function FormFactory(injector, state, existingFormDefinition) { if (existingFormDefinition) { return existingFormDefinition; } return new RxapFormBuilder(SearchForm, injector).build(state ?? {}); } const SearchFormProviders = [ SearchForm, { provide: RXAP_FORM_DEFINITION, useFactory: FormFactory, deps: [ INJECTOR, [new Optional(), RXAP_FORM_INITIAL_STATE], [new SkipSelf(), new Optional(), RXAP_FORM_DEFINITION], ], }, ]; const RXAP_TREE_CONTENT_EDITABLE_METHOD = new InjectionToken('rxap/tree-content-editable-method'); class TreeContentDirective { constructor(template) { this.template = template; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.1", ngImport: i0, type: TreeContentDirective, deps: [{ token: TemplateRef }], target: i0.ɵɵFactoryTarget.Directive }); } static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.1", type: TreeContentDirective, isStandalone: true, selector: "ng-template[rxapTreeContent]", ngImport: i0 }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.1", ngImport: i0, type: TreeContentDirective, decorators: [{ type: Directive, args: [{ selector: 'ng-template[rxapTreeContent]', standalone: true, }] }], ctorParameters: () => [{ type: i0.TemplateRef, decorators: [{ type: Inject, args: [TemplateRef] }] }] }); function isSelectionChange(obj) { return !!obj && obj['added'] !== undefined && obj['removed'] !== undefined; } const RXAP_TREE_DATA_SOURCE_ROOT_REMOTE_METHOD = new InjectionToken('rxap/tree/data-source/root-remote-method'); const RXAP_TREE_DATA_SOURCE_CHILDREN_REMOTE_METHOD = new InjectionToken('rxap/tree/data-source/children-remote-method'); const RXAP_TREE_DATA_SOURCE_APPLY_FILTER_METHOD = new InjectionToken('rxap/tree/data-source/apply-filter-method'); function flatTree(tree, all = false) { tree = coerceArray(tree); function flat(acc, list) { return [...acc, ...list]; } const _flatTree = (node) => { 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], []); } class DefaultTreeApplyFilterMethod { constructor() { this.lastFilter = null; } call({ tree, filter, scopeTypes }) { 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); } isEqualToLastFilter(filter) { 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; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.1", ngImport: i0, type: DefaultTreeApplyFilterMethod, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.1", ngImport: i0, type: DefaultTreeApplyFilterMethod }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.1", ngImport: i0, type: DefaultTreeApplyFilterMethod, decorators: [{ type: Injectable }] }); let TreeDataSource = class TreeDataSource extends BaseDataSource { constructor(rootRemoteMethod, childrenRemoteMethod = null, applyFilterMethod = null, metadata = null) { super(metadata); this.rootRemoteMethod = rootRemoteMethod; this.childrenRemoteMethod = childrenRemoteMethod; this.tree$ = new BehaviorSubject([]); // TODO : änlich problem wie bei der redundaten expand SelectionModel. this.loading$ = new ToggleSubject(true); this.searchForm = null; this._data$ = new BehaviorSubject([]); this._expandedLocalStorageSubscription = null; this._selectedLocalStorageSubscription = null; // im localStorage wird nur die id gespeichert. this._preSelected = []; this._refreshMatchFilter = new Subject(); this._nodeParameters = null; this.toDisplay = () => 'to display function not defined'; this.getIcon = () => null; this.getType = () => null; this.getStyle = () => ({}); this.hasDetails = () => true; this.matchFilter = () => true; this.applyFilterMethod = applyFilterMethod ?? new DefaultTreeApplyFilterMethod(); // TODO add new SelectModel class that saves the select model to the localStorage this.initSelected(); this.initExpanded(); } get nodeParameters() { return this._nodeParameters; } set nodeParameters(nodeParameters) { this._nodeParameters = nodeParameters; this.tree$.value.forEach(node => node.parameters = nodeParameters); } ngOnInit() { if (this.searchForm) { combineLatest([ this.tree$, this.searchForm.rxapFormGroup.value$.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)); } } async getTreeRoot(options = {}) { this.loading$.enable(); const root = await this.getRoot(options); let rootNodes; if (Array.isArray(root)) { rootNodes = await Promise.all(root.map((node) => this.toNode(null, node))); } else { rootNodes = [await this.toNode(null, root)]; } const tmpSelectedNodes = []; const restoreExpandAndSelectedState = (node) => { if (this.expanded.isSelected(node.id)) { node._expanded = true; } if (this.selected.selected.some(n => n.id === node.id)) { console.log('restore selected', node.display); node._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; } selectNode(node) { 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(); } setTreeControl(treeControl) { this.treeControl = treeControl; } setMatchFilter(matchFilter) { this.matchFilter = matchFilter; } setToDisplay(toDisplay = this.toDisplay) { this.toDisplay = toDisplay; } setGetIcon(getIcon = this.getIcon) { this.getIcon = getIcon; } setHasDetails(hasDetails = this.hasDetails) { this.hasDetails = hasDetails; } deselectNode(node) { this.selected.deselect(node); return Promise.resolve(); } async expandNode(node, options) { 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); } } async getChildren(node) { 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); } async getRoot(options = {}) { const rootParameters = await this.getRootParameters(options); return this.rootRemoteMethod.call(rootParameters); } async getRootParameters(options = {}) { return options; } // TODO : find better solution to allow the overwrite of the toNode method // without losing the custom preselect and preexpand function getNodeById(id) { function getNodeById(node, nodeId) { 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); } async toNode(parent, item, depth = 0, onExpand = this.expandNode.bind(this), onCollapse = this.collapseNode.bind(this), onSelect = this.selectNode.bind(this), onDeselect = this.deselectNode.bind(this)) { return Node.ToNode(parent, item, depth, onExpand, onCollapse, this.toDisplay, this.getIcon, this.getType, onSelect, onDeselect, this.hasDetails, this.getStyle, this.nodeParameters); } collapseNode(node, options) { 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 */ flatTree(tree, all = false) { return flatTree(tree, all); } destroy() { super.destroy(); // TODO : add subscription handler to BaseDataSource if (this._expandedLocalStorageSubscription) { this._expandedLocalStorageSubscription.unsubscribe(); } if (this._selectedLocalStorageSubscription) { this._selectedLocalStorageSubscription.unsubscribe(); } } refreshMatchFilter() { this._refreshMatchFilter.next(); } async refresh() { 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); } reset() { this.selected.clear(); this.expanded.clear(); return this.getTreeRoot(); } setGetStyle(getStyle = this.getStyle) { this.getStyle = getStyle; } setGetType(getType = this.getType) { this.getType = getType; } /** * recall the getStyle, getIcon and toDisplay methods * and update the node objects */ 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); } _connect(collectionViewer) { this.init(); this.treeControl.expansionModel.changed.pipe(tap(async (change) => { if (isSelectionChange(change)) { const promiseList = []; 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.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(); } } })); } initSelected() { 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) { console.error('parse expanded tree data source nodes failed'); } } } this.selected = new SelectionModel(!!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(); } } initExpanded() { 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) { console.error('parse expanded tree data source nodes failed'); } } } this.expanded = new SelectionModel(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(); } } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.1", ngImport: i0, type: TreeDataSource, deps: [{ token: RXAP_TREE_DATA_SOURCE_ROOT_REMOTE_METHOD }, { token: RXAP_TREE_DATA_SOURCE_CHILDREN_REMOTE_METHOD, optional: true }, { token: RXAP_TREE_DATA_SOURCE_APPLY_FILTER_METHOD, optional: true }, { token: RXAP_DATA_SOURCE_METADATA, optional: true }], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.1", ngImport: i0, type: TreeDataSource }); } }; __decorate([ Required, __metadata("design:type", FlatTreeControl) ], TreeDataSource.prototype, "treeControl", void 0); TreeDataSource = __decorate([ RxapDataSource('tree'), __metadata("design:paramtypes", [Object, Object, Object, Object]) ], TreeDataSource); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.1", ngImport: i0, type: TreeDataSource, decorators: [{ type: Injectable }], ctorParameters: () => [{ type: undefined, decorators: [{ type: Inject, args: [RXAP_TREE_DATA_SOURCE_ROOT_REMOTE_METHOD] }] }, { type: undefined, decorators: [{ type: Optional }, { type: Inject, args: [RXAP_TREE_DATA_SOURCE_CHILDREN_REMOTE_METHOD] }] }, { type: undefined, decorators: [{ type: Optional }, { type: Inject, args: [RXAP_TREE_DATA_SOURCE_APPLY_FILTER_METHOD] }] }, { type: undefined, decorators: [{ type: Optional }, { type: Inject, args: [RXAP_DATA_SOURCE_METADATA] }] }], propDecorators: { treeControl: [] } }); class TreeComponent { constructor(viewContainerRef, cdr, contentEditableMethod, renderer, elementRef, searchForm) { this.viewContainerRef = viewContainerRef; this.cdr = cdr; this.renderer = renderer; this.elementRef = elementRef; this.searchForm = searchForm; this.multiple = false; this.hideLeafIcon = false; this.details = new EventEmitter(); this.dividerOffset = '256px'; this.portal = null; this.showTreeNavigation = signal(true); /** * Indicates that the divider is moved with mouse down * @private */ this._moveDivider = false; /** * Holds the current tree container width. * If null the move divider feature was not yet used and the initial * container width is not calculated * @private */ this._treeContainerWidth = null; this.getLevel = (node) => node.depth; this.isExpandable = (node) => node.hasChildren; this.hasChild = (_, nodeData) => nodeData.hasChildren; this.treeControl = new FlatTreeControl(this.getLevel, this.isExpandable); this.contentEditableMethod = contentEditableMethod; } get nodeDisplayEditable() { return !!this.contentEditableMethod; } get cacheId() { return ['rxap', 'tree', this.id].join('/'); } ngOnInit() { this.dataSource.searchForm = this.searchForm; this.dataSource.setTreeControl(this.treeControl); this.dataSource.setToDisplay(this.toDisplay); this.dataSource.setGetIcon(this.getIcon); this.dataSource.setHasDetails(this.hasDetails); this.dataSource.setGetStyle(this.getStyle); this.dataSource.setGetType(this.getType); this.multiple = this.dataSource.metadata.selectMultiple ?? this.multiple; if (this.dataSource.selected.hasValue()) { this.dataSource.selected.selected.forEach((node) => this.openDetails(node)); } const cachedOffset = localStorage.getItem(this.cacheId); if (cachedOffset && cachedOffset.match(/^(\d+\.)?\d+px$/)) { this.setDividerOffset(cachedOffset); } else if (isDevMode()) { console.log('Divider offset cache is not available or invalid: ' + cachedOffset); } } ngAfterContentInit() { this.dataSource.selected.changed .pipe(map(($event) => $event.source.selected), startWith(this.dataSource.selected.selected), tap((selected) => selected.forEach((node) => this.openDetails(node)))) .subscribe(); } openDetails(node) { if (this.content) { this.portal = new TemplatePortal(this.content.template, this.viewContainerRef, { $implicit: node.item, node, }); this.cdr.markForCheck(); } this.details.emit(node.item); } onContentEditableChange(value, node) { return this.contentEditableMethod?.call(value, node.item, node); } onMousedown() { this._moveDivider = true; } onMouseup() { this._moveDivider = false; } onMousemove($event) { if (this._moveDivider) { if (!this._treeContainerWidth) { this._treeContainerWidth = this.treeContainer.nativeElement.clientWidth; } const rect = this.elementRef.nativeElement.getBoundingClientRect(); this._treeContainerWidth = Math.min(Math.max($event.clientX - (rect.left + 12), 128), rect.right - rect.left - 128); const offset = this._treeContainerWidth + 'px'; this.setDividerOffset(offset); } } toggleTreeNavigation() { this.showTreeNavigation.update((value) => !value); } setDividerOffset(offset) { this.dividerOffset = offset; this.renderer.setStyle(this.treeContainer.nativeElement, 'max-width', offset); this.renderer.setStyle(this.treeContainer.nativeElement, 'min-width', offset); this.renderer.setStyle(this.treeContainer.nativeElement, 'flex-basis', offset); localStorage.setItem(this.cacheId, offset); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.1", ngImport: i0, type: TreeComponent, deps: [{ token: ViewContainerRef }, { token: ChangeDetectorRef }, { token: RXAP_TREE_CONTENT_EDITABLE_METHOD, optional: true }, { token: i0.Renderer2 }, { token: i0.ElementRef }, { token: RXAP_FORM_DEFINITION, optional: true }], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.1", type: TreeComponent, isStandalone: true, selector: "rxap-tree", inputs: { dataSource: "dataSource", contentEditableMethod: "contentEditableMethod", toDisplay: "toDisplay", getIcon: "getIcon", getType: "getType", getStyle: "getStyle", multiple: "multiple", hasDetails: "hasDetails", hideLeafIcon: "hideLeafIcon", id: "id", dividerOffset: "dividerOffset" }, outputs: { details: "details" }, host: { listeners: { "mouseup": "onMouseup()", "mousemove": "onMousemove($event)" } }, queries: [{ propertyName: "content", first: true, predicate: TreeContentDirective, descendants: true, static: true }], viewQueries: [{ propertyName: "treeContainer", first: true, predicate: ["treeContainer"], descendants: true, static: true }], ngImport: i0, template: "<div class=\"grow flex flex-col gap-2 justify-start items-start\">\n <button (click)=\"toggleTreeNavigation()\" class=\"!justify-start\" mat-button type=\"button\">\n <span class=\"flex flex-row justify-start items-center gap-6\">\n <ng-template [ngIf]=\"showTreeNavigation()\">\n <mat-icon>arrow_back</mat-icon>\n <span i18n>Hide tree navigation</span>\n </ng-template>\n <ng-template [ngIf]=\"!showTreeNavigation()\">\n <mat-icon>arrow_forward</mat-icon>\n <span i18n>Show tree navigation</span>\n </ng-template>\n </span>\n </button>\n <div class=\"flex flex-row grow w-full\">\n <div #treeContainer [ngClass]=\"{ 'hidden': !showTreeNavigation() }\"\n [ngStyle]=\"{ maxWidth: dividerOffset, minWidth: dividerOffset, flexBasis: dividerOffset }\"\n class=\"w-fit grow-0 overflow-y-auto\">\n <mat-progress-bar *ngIf=\"!dataSource || (dataSource.loading$ | async)\" mode=\"indeterminate\"></mat-progress-bar>\n <ng-content select=\"[searchHeader]\"></ng-content>\n <mat-tree *ngIf=\"dataSource; else loading\" [dataSource]=\"dataSource\" [treeControl]=\"treeControl\">\n\n <!-- Node without children -->\n <mat-tree-node *matTreeNodeDef=\"let node\" matTreeNodePadding>\n <div class=\"flex flex-row justify-start items-center gap-2\">\n <mat-icon [ngClass]=\"{ 'hidden': hideLeafIcon }\">subdirectory_arrow_right</mat-icon>\n <ng-container *ngIf=\"node.icon?.length\">\n <mat-icon *ngFor=\"let icon of node.icon\" [rxapIcon]=\"$any(icon)\"></mat-icon>\n </ng-container>\n <ng-template [ngIf]=\"multiple\">\n <mat-checkbox\n (change)=\"node.toggleSelect()\"\n [checked]=\"node.selected\"\n [disabled]=\"!node.hasDetails\"\n [ngStyle]=\"node.style\"\n class=\"grow-0 truncate\">\n {{ node.display }}\n </mat-checkbox>\n </ng-template>\n <ng-template [ngIf]=\"!multiple\">\n <button\n (click)=\"node.select()\"\n [color]=\"node.selected ? 'primary' : undefined\"\n [disabled]=\"!node.hasDetails\"\n [ngStyle]=\"node.style\"\n class=\"grow-0 truncate\"\n mat-button\n type=\"button\"\n >\n {{ node.display }}\n </button>\n </ng-template>\n </div>\n </mat-tree-node>\n\n <!-- Node with children -->\n <mat-tree-node *matTreeNodeDef=\"let node; when: hasChild\" matTreeNodePadding>\n <button [attr.aria-label]=\"'toggle ' + node.filename\" mat-icon-button matTreeNodeToggle type=\"button\">\n <mat-icon class=\"mat-icon-rtl-mirror\">\n {{ treeControl.isExpanded(node) ? 'expand_more' : 'chevron_right' }}\n </mat-icon>\n </button>\n <div class=\"flex flex-row justify-start items-center gap-2\">\n <ng-container *ngIf=\"node.icon?.length else withoutIcon\">\n <mat-icon *ngFor=\"let icon of node.icon\" [rxapIcon]=\"$any(icon)\"></mat-icon>\n </ng-container>\n <ng-template #withoutIcon>\n <div></div>\n </ng-template>\n <ng-template [ngIfElse]=\"withoutDetails\" [ngIf]=\"node.hasDetails\">\n <button\n (click)=\"node.select()\"\n [color]=\"node.selected ? 'primary' : undefined\"\n [ngStyle]=\"node.style\"\n class=\"grow-0 truncate\"\n mat-button\n type=\"button\"\n >\n {{ node.display }}\n </button>\n </ng-template>\n <ng-template #withoutDetails>\n <span\n (change)=\"onContentEditableChange($event, node)\"\n [disabled]=\"!nodeDisplayEditable\"\n [ngStyle]=\"node.style\"\n class=\"grow-0 truncate\"\n rxapContenteditable>\n {{ node.display }}\n </span>\n </ng-template>\n <mat-progress-spinner\n *ngIf=\"node.isLoading$ | async\"\n [diameter]=\"16\"\n class=\"grow-0 pl-4\"\n mode=\"indeterminate\"\n ></mat-progress-spinner>\n </div>\n </mat-tree-node>\n\n </mat-tree>\n </div>\n\n <div (mousedown)=\"onMousedown()\" *ngIf=\"showTreeNavigation()\"\n class=\"divider cursor-ew-resize px-3 grow-0\">\n <mat-divider [vertical]=\"true\" class=\"h-full\"></mat-divider>\n </div>\n\n <div class=\"grow\">\n <ng-container *ngIf=\"portal\">\n <ng-template [cdkPortalOutlet]=\"portal\"></ng-template>\n </ng-container>\n <ng-content></ng-content>\n </div>\n </div>\n</div>\n\n<ng-template #loading>Load data source</ng-template>\n", styles: [""], dependencies: [{ kind: "directive", type: NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "directive", type: NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: MatProgressBarModule }, { kind: "component", type: i1.MatProgressBar, selector: "mat-progress-bar", inputs: ["color", "value", "bufferValue", "mode"], outputs: ["animationEnd"], exportAs: ["matProgressBar"] }, { kind: "ngmodule", type: MatTreeModule }, { kind: "directive", type: i2.MatTreeNodeDef, selector: "[matTreeNodeDef]", inputs: ["matTreeNodeDefWhen", "matTreeNode"] }, { kind: "directive", type: i2.MatTreeNodePadding, selector: "[matTreeNodePadding]", inputs: ["matTreeNodePadding", "matTreeNodePaddingIndent"] }, { kind: "directive", type: i2.MatTreeNodeToggle, selector: "[matTreeNodeToggle]", inputs: ["matTreeNodeToggleRecursive"] }, { kind: "component", type: i2.MatTree, selector: "mat-tree", exportAs: ["matTree"] }, { kind: "directive", type: i2.MatTreeNode, selector: "mat-tree-node", inputs: ["tabIndex", "disabled"], outputs: ["activation", "expandedChange"], exportAs: ["matTreeNode"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i3.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "directive", type: IconDirective, selector: "mat-icon[rxapIcon]", inputs: ["rxapIcon"] }, { kind: "ngmodule", type: MatCheckboxModule }, { kind: "component", type: i4.MatCheckbox, selector: "mat-checkbox", inputs: ["aria-label", "aria-labelledby", "aria-describedby", "aria-expanded", "aria-controls", "aria-owns", "id", "required", "labelPosition", "name", "value", "disableRipple", "tabIndex", "color", "disabledInteractive", "checked", "disabled", "indeterminate"], outputs: ["change", "indeterminateChange"], exportAs: ["matCheckbox"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i5.MatButton, selector: " button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ", exportAs: ["matButton"] }, { kind: "component", type: i5.MatIconButton, selector: "button[mat-icon-button]", exportAs: ["matButton"] }, { kind: "directive", type: ContenteditableDirective, selector: "[rxapContenteditable]", inputs: ["rxapContenteditable", "disabled"], outputs: ["change"] }, { kind: "ngmodule", type: MatProgressSpinnerModule }, { kind: "component", type: i6.MatProgressSpinner, selector: "mat-progress-spinner, mat-spinner", inputs: ["color", "mode", "value", "diameter", "strokeWidth"], exportAs: ["matProgressSpinner"] }, { kind: "ngmodule", type: MatDividerModule }, { kind: "component", type: i7.MatDivider, selector: "mat-divider", inputs: ["vertical", "inset"] }, { kind: "ngmodule", type: PortalModule }, { kind: "directive", type: i8.CdkPortalOutlet, selector: "[cdkPortalOutlet]", inputs: ["cdkPortalOutlet"], outputs: ["attached"], exportAs: ["cdkPortalOutlet"] }, { kind: "pipe", type: AsyncPipe, name: "async" }, { kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); } } __decorate([ DebounceCall(100), __metadata("design:type", Function), __metadata("design:paramtypes", [Node]), __metadata("design:returntype", void 0) ], TreeComponent.prototype, "openDetails", null); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.1", ngImport: i0, type: TreeComponent, decorators: [{ type: Component, args: [{ selector: 'rxap-tree', changeDetection: ChangeDetectionStrategy.OnPush, imports: [ NgStyle, NgIf, MatProgressBarModule, MatTreeModule, MatIconModule, IconDirective, MatCheckboxModule, MatButtonModule, ContenteditableDirective, MatProgressSpinnerModule, MatDividerModule, PortalModule, AsyncPipe, NgClass, NgForOf, ], template: "<div class=\"grow flex flex-col gap-2 justify-start items-start\">\n <button (click)=\"toggleTreeNavigation()\" class=\"!justify-start\" mat-button type=\"button\">\n <span class=\"flex flex-row justify-start items-center gap-6\">\n <ng-template [ngIf]=\"showTreeNavigation()\">\n <mat-icon>arrow_back</mat-icon>\n <span i18n>Hide tree navigation</span>\n </ng-template>\n <ng-template [ngIf]=\"!showTreeNavigation()\">\n <mat-icon>arrow_forward</mat-icon>\n <span i18n>Show tree navigation</span>\n </ng-template>\n </span>\n </button>\n <div class=\"flex flex-row grow w-full\">\n <div #treeContainer [ngClass]=\"{ 'hidden': !showTreeNavigation() }\"\n [ngStyle]=\"{ maxWidth: dividerOffset, minWidth: dividerOffset, flexBasis: dividerOffset }\"\n class=\"w-fit grow-0 overflow-y-auto\">\n <mat-progress-bar *ngIf=\"!dataSource || (dataSource.loading$ | async)\" mode=\"indeterminate\"></mat-progress-bar>\n <ng-content select=\"[searchHeader]\"></ng-content>\n <mat-tree *ngIf=\"dataSource; else loading\" [dataSource]=\"dataSource\" [treeControl]=\"treeControl\">\n\n <!-- Node without children -->\n <mat-tree-node *matTreeNodeDef=\"let node\" matTreeNodePadding>\n <div class=\"flex flex-row justify-start items-center gap-2\">\n <mat-icon [ngClass]=\"{ 'hidden': hideLeafIcon }\">subdirectory_arrow_right</mat-icon>\n <ng-container *ngIf=\"node.icon?.length\">\n <mat-icon *ngFor=\"let icon of node.icon\" [rxapIcon]=\"$any(icon)\"></mat-icon>\n </ng-container>\n <ng-template [ngIf]=\"multiple\">\n <mat-checkbox\n (change)=\"node.toggleSelect()\"\n [checked]=\"node.selected\"\n [disabled]=\"!node.hasDetails\"\n [ngStyle]=\"node.style\"\n class=\"grow-0 truncate\">\n {{ node.display }}\n </mat-checkbox>\n </ng-template>\n <ng-template [ngIf]=\"!multiple\">\n <button\n (click)=\"node.select()\"\n [color]=\"node.selected ? 'primary' : undefined\"\n [disabled]=\"!node.hasDetails\"\n [ngStyle]=\"node.style\"\n class=\"grow-0 truncate\"\n mat-button\n type=\"button\"\n >\n {{ node.display }}\n </button>\n </ng-template>\n </div>\n </mat-tree-node>\n\n <!-- Node with children -->\n <mat-tree-node *matTreeNodeDef=\"let node; when: hasChild\" matTreeNodePadding>\n <button [attr.aria-label]=\"'toggle ' + node.filename\" mat-icon-button matTreeNodeToggle type=\"button\">\n <mat-icon class=\"mat-icon-rtl-mirror\">\n {{ treeControl.isExpanded(node) ? 'expand_more' : 'chevron_right' }}\n </mat-icon>\n </button>\n <div class=\"flex flex-row justify-start items-center gap-2\">\n <ng-container *ngIf=\"node.icon?.length else withoutIcon\">\n <mat-icon *ngFor=\"let icon of node.icon\" [rxapIcon]=\"$any(icon)\"></mat-icon>\n </ng-container>\n <ng-template #withoutIcon>\n <div></div>\n </ng-template>\n <ng-template [ngIfElse]=\"withoutDetails\" [ngIf]=\"node.hasDetails\">\n <button\n (click)=\"node.select()\"\n [color]=\"node.selected ? 'primary' : undefined\"\n [ngStyle]=\"node.style\"\n class=\"grow-0 truncate\"\n mat-button\n type=\"button\"\n >\n {{ node.display }}\n </button>\n </ng-template>\n <ng-template #withoutDetails>\n <span\n (change)=\"onContentEditableChange($event, node)\"\n [disabled]=\"!nodeDisplayEditable\"\n [ngStyle]=\"node.style\"\n class=\"grow-0 truncate\"\n rxapContenteditable>\n {{ node.display }}\n </span>\n </ng-template>\n <mat-progress-spinner\n *ngIf=\"node.isLoading$ | async\"\n [diameter]=\"16\"\n class=\"grow-0 pl-4\"\n mode=\"indeterminate\"\n ></mat-progress-spinner>\n </div>\n </mat-tree-node>\n\n </mat-tree>\n </div>\n\n <div (mousedown)=\"onMousedown()\" *ngIf=\"showTreeNavigation()\"\n class=\"divider cursor-ew-resize px-3 grow-0\">\n <mat-divider [vertical]=\"true\" class=\"h-full\"></mat-divider>\n </div>\n\n <div class=\"grow\">\n <ng-container *ngIf=\"portal\">\n <ng-template [cdkPortalOutlet]=\"portal\"></ng-template>\n </ng-container>\n <ng-content></ng-content>\n </div>\n </div>\n</div>\n\n<ng-template #loading>Load data source</ng-template>\n" }] }], ctorParameters: () => [{ type: i0.ViewContainerRef, decorators: [{ type: Inject, args: [ViewContainerRef] }] }, { type: i0.ChangeDetectorRef, decorators: [{ type: Inject, args: [ChangeDetectorRef] }] }, { type: undefined, decorators: [{ type: Optional }, { type: Inject, args: [RXAP_TREE_CONTENT_EDITABLE_METHOD] }] }, { type: i0.Renderer2 }, { type: i0.ElementRef }, { type: SearchForm, decorators: [{ type: Optional }, { type: Inject, args: [RXAP_FORM_DEFINITION] }] }], propDecorators: { dataSource: [{ type: Input, args: [{ required: true }] }], contentEditableMethod: [{ type: Input }], toDisplay: [{ type: Input }], getIcon: [{ type: Input }], getType: [{ type: Input }], getStyle: [{ type: Input }], multiple: [{ type: Input }], hasDetails: [{ type: Input }], content: [{ type: ContentChild, args: [TreeContentDirective, { static: true }] }], hideLeafIcon: [{ type: Input }], id: [{ type: Input }], details: [{ type: Output }], dividerOffset: [{ type: Input }], treeContainer: [{ type: ViewChild, args: ['treeContainer', { static: true }] }], openDetails: [], onMouseup: [{ type: HostListener, args: ['mouseup'] }], onMousemove: [{ type: HostListener, args: ['mousemove', ['$event']] }] } }); // region // endregion /** * Generated bundle index. Do not edit. */ export { DefaultTreeApplyFilterMethod, FormFactory, RXAP_TREE_CONTENT_EDITABLE_METHOD, RXAP_TREE_DATA_SOURCE_APPLY_FILTER_METHOD, RXAP_TREE_DATA_SOURCE_CHILDREN_REMOTE_METHOD, RXAP_TREE_DATA_SOURCE_ROOT_REMOTE_METHO