import { Component, OnInit, Input, ChangeDetectorRef, ViewChild, Output, EventEmitter, AfterViewInit, OnDestroy } from '@angular/core'; import { MatTreeFlattener, MatTree } from '@angular/material'; import { NGXLogger } from 'ngx-logger'; import { DeptInfo } from '@ucap-webmessenger/protocol-query'; import { LoginResponse } from '@ucap-webmessenger/protocol-authentication'; import { FlatTreeControl } from '@angular/cdk/tree'; import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling'; import { VirtualScrollTreeFlatDataSource } from '@ucap-webmessenger/ui'; import { ucapAnimations } from '@ucap-webmessenger/ui'; import { trigger, transition, style, animate } from '@angular/animations'; import { PerfectScrollbarDirective } from 'ngx-perfect-scrollbar'; import { Subscription, Observable } from 'rxjs'; interface OrganizationNode { deptInfo: DeptInfo; name: string; children?: OrganizationNode[]; } /** Flat node with expandable and level information */ interface FlatNode { expandable: boolean; name: string; level: number; deptInfo: DeptInfo; } @Component({ selector: 'ucap-organization-tree', templateUrl: './tree.component.html', styleUrls: ['./tree.component.scss'], animations: [ ucapAnimations, trigger('romvoeAdd', [ transition('remove <=> add', [ style({ transform: `rotate(45deg)`, opacity: 0 }), animate('.2s 0s ease-out') ]) ]) ] }) export class TreeComponent implements OnInit, OnDestroy, AfterViewInit { @Input() loginRes: LoginResponse; @Input() set oraganizationList(deptInfoList: DeptInfo[]) { if (!deptInfoList || 0 === deptInfoList.length) { return; } const nodeMap = new Map(); const rootNodeList: OrganizationNode[] = []; const remainChildNodeList: OrganizationNode[] = []; let myNode: OrganizationNode; deptInfoList.forEach(deptInfo => { const node: OrganizationNode = { deptInfo, name: deptInfo.name, children: [] }; if (nodeMap.has(deptInfo.seq)) { this.logger.warn('duplicate seq', deptInfo.seq); return; } nodeMap.set(deptInfo.seq, node); if (deptInfo.seq === this.loginRes.departmentCode) { myNode = node; } if (0 === deptInfo.parentSeq) { rootNodeList.push(node); return; } if (nodeMap.has(deptInfo.parentSeq)) { nodeMap.get(deptInfo.parentSeq).children.push(node); } else { remainChildNodeList.push(node); } }); remainChildNodeList.forEach(node => { if (nodeMap.has(node.deptInfo.parentSeq)) { nodeMap.get(node.deptInfo.parentSeq).children.push(node); } }); this.dataSource.data = rootNodeList; } @Input() activate$: Observable; @Output() selected = new EventEmitter(); @ViewChild('cvsvOrganization', { static: false }) cvsvOrganization: CdkVirtualScrollViewport; @ViewChild('orgranizationTree', { static: false }) orgranizationTree: MatTree; @ViewChild(PerfectScrollbarDirective, { static: false }) psDirectiveRef?: PerfectScrollbarDirective; treeControl: FlatTreeControl; treeFlattener: MatTreeFlattener; dataSource: VirtualScrollTreeFlatDataSource; treeControlExpansionChangeSubscription: Subscription; activateSubscription: Subscription; constructor( private changeDetectorRef: ChangeDetectorRef, private logger: NGXLogger ) { this.treeControl = new FlatTreeControl( node => node.level, node => node.expandable ); this.treeFlattener = new MatTreeFlattener( (node: OrganizationNode, level: number) => { return { expandable: !!node.children && node.children.length > 0, name: node.name, level, deptInfo: node.deptInfo }; }, node => node.level, node => node.expandable, node => node.children ); this.dataSource = new VirtualScrollTreeFlatDataSource< OrganizationNode, FlatNode >(this.treeControl, this.treeFlattener); } ngOnInit() { this.treeControlExpansionChangeSubscription = this.treeControl.expansionModel.changed.subscribe( () => { this.psDirectiveRef.update(); } ); this.activateSubscription = this.activate$.subscribe(activate => { if (activate) { setTimeout(() => { if (!!this.cvsvOrganization) { this.cvsvOrganization.checkViewportSize(); } }, 100); } }); } ngOnDestroy(): void { if (!!this.treeControlExpansionChangeSubscription) { this.treeControlExpansionChangeSubscription.unsubscribe(); } if (!!this.activateSubscription) { this.activateSubscription.unsubscribe(); } } ngAfterViewInit(): void { this.dataSource.cdkVirtualScrollViewport = this.cvsvOrganization; } hasChild = (_: number, node: FlatNode) => node.expandable; isLastNode(node: FlatNode, depth: number): boolean { const i = this.findNodeIndex(node, depth); if (-1 === i) { return false; } if (i === this.dataSource.expandedDataSubject.value.length - 1) { return true; } const n = this.dataSource.expandedDataSubject.value[i]; for ( let idx = i + 1; idx < this.dataSource.expandedDataSubject.value.length; idx++ ) { const element = this.dataSource.expandedDataSubject.value[idx]; if (n.level === element.level) { return false; } if (n.level > element.level) { return true; } } return true; } appendDivArray(level: number): number[] { if (0 === level) { return []; } return Array.from({ length: level }, (_, i: number) => i); } onClickNode(node: OrganizationNode) { this.selected.emit(node.deptInfo); } private findNodeIndex(node: FlatNode, depth: number): number { if (!node) { return -1; } const i = this.dataSource.expandedDataSubject.value.findIndex(n => { return node.deptInfo.seq === n.deptInfo.seq; }); if (0 === depth) { return i; } return this.findNodeIndex(this.findParentNode(i), depth - 1); } private findParentNode(index: number): FlatNode { const node = this.dataSource.expandedDataSubject.value[index]; for (let idx = index - 1; idx >= 0; idx--) { const n = this.dataSource.expandedDataSubject.value[idx]; if (n.level === node.level - 1) { return n; } } return undefined; } }