src/lib/tree.component.ts
OnInit
AfterContentInit
| changeDetection | ChangeDetectionStrategy.OnPush |
| selector | rxap-tree |
| imports |
NgStyle
NgIf
MatProgressBarModule
MatTreeModule
MatIconModule
IconDirective
MatCheckboxModule
MatButtonModule
ContenteditableDirective
MatProgressSpinnerModule
MatDividerModule
PortalModule
AsyncPipe
NgClass
NgForOf
|
| styleUrls | ./tree.component.scss |
| templateUrl | ./tree.component.html |
Properties |
|
Methods |
Inputs |
Outputs |
HostListeners |
Accessors |
constructor(viewContainerRef: ViewContainerRef, cdr: ChangeDetectorRef, contentEditableMethod: Method
|
|||||||||||||||||||||
|
Defined in src/lib/tree.component.ts:136
|
|||||||||||||||||||||
|
Parameters :
|
| contentEditableMethod | |
Type : Method<any | string | null> | null
|
|
|
Defined in src/lib/tree.component.ts:96
|
|
| dataSource | |
Type : TreeDataSource<Data>
|
|
| Required : true | |
|
Defined in src/lib/tree.component.ts:94
|
|
| dividerOffset | |
Type : string
|
|
Default value : '256px'
|
|
|
Defined in src/lib/tree.component.ts:118
|
|
| getIcon | |
Type : NodeGetIconFunction<any>
|
|
|
Defined in src/lib/tree.component.ts:100
|
|
| getStyle | |
Type : NodeGetStyleFunction<any>
|
|
|
Defined in src/lib/tree.component.ts:104
|
|
| getType | |
Type : NodeGetTypeFunction<any>
|
|
|
Defined in src/lib/tree.component.ts:102
|
|
| hasDetails | |
Type : NodeHasDetailsFunction<any>
|
|
|
Defined in src/lib/tree.component.ts:108
|
|
| hideLeafIcon | |
Type : boolean
|
|
Default value : false
|
|
|
Defined in src/lib/tree.component.ts:112
|
|
| id | |
Type : string
|
|
|
Defined in src/lib/tree.component.ts:114
|
|
| multiple | |
Type : boolean
|
|
Default value : false
|
|
|
Defined in src/lib/tree.component.ts:106
|
|
| toDisplay | |
Type : NodeToDisplayFunction<any>
|
|
|
Defined in src/lib/tree.component.ts:98
|
|
| details | |
Type : EventEmitter
|
|
|
Defined in src/lib/tree.component.ts:116
|
|
| mousemove |
Arguments : '$event'
|
mousemove($event: MouseEvent)
|
|
Defined in src/lib/tree.component.ts:235
|
| mouseup |
mouseup()
|
|
Defined in src/lib/tree.component.ts:230
|
| Public onContentEditableChange | |||||||||
onContentEditableChange(value: string | null, node: Node)
|
|||||||||
|
Defined in src/lib/tree.component.ts:220
|
|||||||||
|
Parameters :
Returns :
any
|
| onMousedown |
onMousedown()
|
|
Defined in src/lib/tree.component.ts:225
|
|
Returns :
void
|
| onMousemove | ||||||
onMousemove($event: MouseEvent)
|
||||||
Decorators :
@HostListener('mousemove', ['$event'])
|
||||||
|
Defined in src/lib/tree.component.ts:235
|
||||||
|
Parameters :
Returns :
void
|
| onMouseup |
onMouseup()
|
Decorators :
@HostListener('mouseup')
|
|
Defined in src/lib/tree.component.ts:230
|
|
Returns :
void
|
| Public openDetails | ||||||
openDetails(node: Node)
|
||||||
Decorators :
@DebounceCall(100)
|
||||||
|
Defined in src/lib/tree.component.ts:205
|
||||||
|
Parameters :
Returns :
void
|
| toggleTreeNavigation |
toggleTreeNavigation()
|
|
Defined in src/lib/tree.component.ts:249
|
|
Returns :
void
|
| Public Optional content |
Type : TreeContentDirective
|
Decorators :
@ContentChild(TreeContentDirective, {static: true})
|
|
Defined in src/lib/tree.component.ts:110
|
| Public getLevel |
Default value : () => {...}
|
|
Defined in src/lib/tree.component.ts:164
|
| Public hasChild |
Default value : () => {...}
|
|
Defined in src/lib/tree.component.ts:168
|
| Public isExpandable |
Default value : () => {...}
|
|
Defined in src/lib/tree.component.ts:166
|
| Public portal |
Type : TemplatePortal | null
|
Default value : null
|
|
Defined in src/lib/tree.component.ts:119
|
| Public Readonly searchForm |
Type : SearchForm | null
|
Decorators :
@Optional()
|
|
Defined in src/lib/tree.component.ts:150
|
| Public Readonly showTreeNavigation |
Default value : signal(true)
|
|
Defined in src/lib/tree.component.ts:123
|
| Public treeContainer |
Type : ElementRef
|
Decorators :
@ViewChild('treeContainer', {static: true})
|
|
Defined in src/lib/tree.component.ts:121
|
| Public treeControl |
Type : FlatTreeControl<Node<Data>>
|
|
Defined in src/lib/tree.component.ts:92
|
| nodeDisplayEditable |
getnodeDisplayEditable()
|
|
Defined in src/lib/tree.component.ts:156
|
| cacheId |
getcacheId()
|
|
Defined in src/lib/tree.component.ts:160
|
import {
PortalModule,
TemplatePortal,
} from '@angular/cdk/portal';
import { FlatTreeControl } from '@angular/cdk/tree';
import {
AsyncPipe,
NgClass,
NgForOf,
NgIf,
NgStyle,
} from '@angular/common';
import {
AfterContentInit,
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
ContentChild,
ElementRef,
EventEmitter,
HostListener,
Inject,
Input,
isDevMode,
OnInit,
Optional,
Output,
Renderer2,
signal,
ViewChild,
ViewContainerRef,
} from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatDividerModule } from '@angular/material/divider';
import { MatIconModule } from '@angular/material/icon';
import { MatProgressBarModule } from '@angular/material/progress-bar';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatTreeModule } from '@angular/material/tree';
import { ContenteditableDirective } from '@rxap/contenteditable';
import {
Node,
NodeGetIconFunction,
NodeGetStyleFunction,
NodeGetTypeFunction,
NodeHasDetailsFunction,
NodeToDisplayFunction,
} from '@rxap/data-structure-tree';
import { RXAP_FORM_DEFINITION } from '@rxap/forms';
import { IconDirective } from '@rxap/material-directives/icon';
import { Method } from '@rxap/pattern';
import {
DebounceCall,
WithChildren,
WithIdentifier,
} from '@rxap/utilities';
import {
map,
startWith,
tap,
} from 'rxjs/operators';
import { SearchForm } from './search.form';
import { RXAP_TREE_CONTENT_EDITABLE_METHOD } from './tokens';
import { TreeContentDirective } from './tree-content.directive';
import { TreeDataSource } from './tree.data-source';
@Component({
selector: 'rxap-tree',
templateUrl: './tree.component.html',
styleUrls: ['./tree.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
NgStyle,
NgIf,
MatProgressBarModule,
MatTreeModule,
MatIconModule,
IconDirective,
MatCheckboxModule,
MatButtonModule,
ContenteditableDirective,
MatProgressSpinnerModule,
MatDividerModule,
PortalModule,
AsyncPipe,
NgClass,
NgForOf,
]
})
export class TreeComponent<Data extends WithIdentifier & WithChildren = any>
implements OnInit, AfterContentInit {
public treeControl: FlatTreeControl<Node<Data>>;
@Input({required: true})
public dataSource!: TreeDataSource<Data>;
@Input()
public contentEditableMethod?: Method<any, string | null> | null;
@Input()
public toDisplay?: NodeToDisplayFunction<any>;
@Input()
public getIcon?: NodeGetIconFunction<any>;
@Input()
public getType?: NodeGetTypeFunction<any>;
@Input()
public getStyle?: NodeGetStyleFunction<any>;
@Input()
public multiple = false;
@Input()
public hasDetails?: NodeHasDetailsFunction<any>;
@ContentChild(TreeContentDirective, {static: true})
public content?: TreeContentDirective;
@Input()
public hideLeafIcon = false;
@Input()
public id?: string;
@Output()
public details = new EventEmitter();
@Input()
public dividerOffset = '256px';
public portal: TemplatePortal | null = null;
@ViewChild('treeContainer', {static: true})
public treeContainer!: ElementRef;
public readonly showTreeNavigation = signal(true);
/**
* Indicates that the divider is moved with mouse down
* @private
*/
private _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
*/
private _treeContainerWidth: number | null = null;
constructor(
@Inject(ViewContainerRef)
private readonly viewContainerRef: ViewContainerRef,
@Inject(ChangeDetectorRef)
private readonly cdr: ChangeDetectorRef,
@Optional()
@Inject(RXAP_TREE_CONTENT_EDITABLE_METHOD)
contentEditableMethod: Method<any, string | null> | null,
private readonly renderer: Renderer2,
private readonly elementRef: ElementRef<HTMLElement>,
@Optional()
@Inject(RXAP_FORM_DEFINITION)
public readonly searchForm: SearchForm | null,
) {
this.treeControl = new FlatTreeControl(this.getLevel, this.isExpandable);
this.contentEditableMethod = contentEditableMethod;
}
public get nodeDisplayEditable(): boolean {
return !!this.contentEditableMethod;
}
public get cacheId() {
return [ 'rxap', 'tree', this.id ].join('/');
}
public getLevel = (node: Node<Data>) => node.depth;
public isExpandable = (node: Node<Data>) => node.hasChildren;
public hasChild = (_: number, nodeData: Node<Data>) => nodeData.hasChildren;
public 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);
}
}
public ngAfterContentInit(): void {
this.dataSource.selected.changed
.pipe(
map(($event) => $event.source.selected),
startWith(this.dataSource.selected.selected),
tap((selected) => selected.forEach((node) => this.openDetails(node))),
)
.subscribe();
}
@DebounceCall(100)
public openDetails(node: Node<Data>): void {
if (this.content) {
this.portal = new TemplatePortal<any>(
this.content.template,
this.viewContainerRef,
{
$implicit: node.item,
node,
},
);
this.cdr.markForCheck();
}
this.details.emit(node.item);
}
public onContentEditableChange(value: string | null, node: Node<Data>) {
return this.contentEditableMethod?.call(value, node.item, node);
}
onMousedown() {
this._moveDivider = true;
}
@HostListener('mouseup')
onMouseup() {
this._moveDivider = false;
}
@HostListener('mousemove', [ '$event' ])
onMousemove($event: MouseEvent) {
if (this._moveDivider) {
if (!this._treeContainerWidth) {
this._treeContainerWidth = this.treeContainer.nativeElement.clientWidth as number;
}
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);
}
private setDividerOffset(offset: string) {
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);
}
}
<div class="grow flex flex-col gap-2 justify-start items-start">
<button (click)="toggleTreeNavigation()" class="!justify-start" mat-button type="button">
<span class="flex flex-row justify-start items-center gap-6">
<ng-template [ngIf]="showTreeNavigation()">
<mat-icon>arrow_back</mat-icon>
<span i18n>Hide tree navigation</span>
</ng-template>
<ng-template [ngIf]="!showTreeNavigation()">
<mat-icon>arrow_forward</mat-icon>
<span i18n>Show tree navigation</span>
</ng-template>
</span>
</button>
<div class="flex flex-row grow w-full">
<div #treeContainer [ngClass]="{ 'hidden': !showTreeNavigation() }"
[ngStyle]="{ maxWidth: dividerOffset, minWidth: dividerOffset, flexBasis: dividerOffset }"
class="w-fit grow-0 overflow-y-auto">
<mat-progress-bar *ngIf="!dataSource || (dataSource.loading$ | async)" mode="indeterminate"></mat-progress-bar>
<ng-content select="[searchHeader]"></ng-content>
<mat-tree *ngIf="dataSource; else loading" [dataSource]="dataSource" [treeControl]="treeControl">
<!-- Node without children -->
<mat-tree-node *matTreeNodeDef="let node" matTreeNodePadding>
<div class="flex flex-row justify-start items-center gap-2">
<mat-icon [ngClass]="{ 'hidden': hideLeafIcon }">subdirectory_arrow_right</mat-icon>
<ng-container *ngIf="node.icon?.length">
<mat-icon *ngFor="let icon of node.icon" [rxapIcon]="$any(icon)"></mat-icon>
</ng-container>
<ng-template [ngIf]="multiple">
<mat-checkbox
(change)="node.toggleSelect()"
[checked]="node.selected"
[disabled]="!node.hasDetails"
[ngStyle]="node.style"
class="grow-0 truncate">
{{ node.display }}
</mat-checkbox>
</ng-template>
<ng-template [ngIf]="!multiple">
<button
(click)="node.select()"
[color]="node.selected ? 'primary' : undefined"
[disabled]="!node.hasDetails"
[ngStyle]="node.style"
class="grow-0 truncate"
mat-button
type="button"
>
{{ node.display }}
</button>
</ng-template>
</div>
</mat-tree-node>
<!-- Node with children -->
<mat-tree-node *matTreeNodeDef="let node; when: hasChild" matTreeNodePadding>
<button [attr.aria-label]="'toggle ' + node.filename" mat-icon-button matTreeNodeToggle type="button">
<mat-icon class="mat-icon-rtl-mirror">
{{ treeControl.isExpanded(node) ? 'expand_more' : 'chevron_right' }}
</mat-icon>
</button>
<div class="flex flex-row justify-start items-center gap-2">
<ng-container *ngIf="node.icon?.length else withoutIcon">
<mat-icon *ngFor="let icon of node.icon" [rxapIcon]="$any(icon)"></mat-icon>
</ng-container>
<ng-template #withoutIcon>
<div></div>
</ng-template>
<ng-template [ngIfElse]="withoutDetails" [ngIf]="node.hasDetails">
<button
(click)="node.select()"
[color]="node.selected ? 'primary' : undefined"
[ngStyle]="node.style"
class="grow-0 truncate"
mat-button
type="button"
>
{{ node.display }}
</button>
</ng-template>
<ng-template #withoutDetails>
<span
(change)="onContentEditableChange($event, node)"
[disabled]="!nodeDisplayEditable"
[ngStyle]="node.style"
class="grow-0 truncate"
rxapContenteditable>
{{ node.display }}
</span>
</ng-template>
<mat-progress-spinner
*ngIf="node.isLoading$ | async"
[diameter]="16"
class="grow-0 pl-4"
mode="indeterminate"
></mat-progress-spinner>
</div>
</mat-tree-node>
</mat-tree>
</div>
<div (mousedown)="onMousedown()" *ngIf="showTreeNavigation()"
class="divider cursor-ew-resize px-3 grow-0">
<mat-divider [vertical]="true" class="h-full"></mat-divider>
</div>
<div class="grow">
<ng-container *ngIf="portal">
<ng-template [cdkPortalOutlet]="portal"></ng-template>
</ng-container>
<ng-content></ng-content>
</div>
</div>
</div>
<ng-template #loading>Load data source</ng-template>
./tree.component.scss