import { Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; import { Component, OnInit, OnDestroy, Input, ViewChild, ChangeDetectionStrategy, ChangeDetectorRef, EventEmitter, Output } from '@angular/core'; import { trigger, transition, style, animate } from '@angular/animations'; import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling'; import { FlatTreeControl } from '@angular/cdk/tree'; import { MatTreeFlattener, MatTree } from '@angular/material/tree'; import { PerfectScrollbarDirective } from 'ngx-perfect-scrollbar'; import { LoginResponse } from '@ucap/protocol-authentication'; import { DeptInfo } from '@ucap/protocol-query'; import { LogService } from '@ucap/ng-logger'; import { VirtualScrollTreeFlatDataSource } from '@ucap/ng-ui'; export interface OrganizationNode { deptInfo: DeptInfo; ancestorSeq?: number; children?: OrganizationNode[]; } export interface FlatNode { expandable: boolean; level: number; data: OrganizationNode; } @Component({ selector: 'ucap-organization-tree', templateUrl: './tree.component.html', styleUrls: ['./tree.component.scss'], animations: [ trigger('removeAdd', [ transition('remove <=> add', [ style({ transform: `rotate(45deg)`, opacity: 0 }), animate('.2s 0s ease-out') ]) ]) ], changeDetection: ChangeDetectionStrategy.OnPush }) export class TreeComponent implements OnInit, OnDestroy { @Input() loginRes: LoginResponse; @Input() set deptInfoList(data: { deptInfoList: DeptInfo[]; expanded?: number[]; displayRoot?: boolean; }) { const deptInfoList = data.deptInfoList; const displayRoot = undefined === data.displayRoot ? true : data.displayRoot; if (!deptInfoList || 0 === deptInfoList.length) { return; } const nodeMap = new Map(); let rootNodeList: OrganizationNode[] = []; const remainChildNodeList: OrganizationNode[] = []; deptInfoList.forEach((deptInfo) => { const node: OrganizationNode = { deptInfo, children: [] }; if (nodeMap.has(deptInfo.seq)) { this.logService.warn('duplicate seq', deptInfo.seq); return; } nodeMap.set(deptInfo.seq, node); if (0 === deptInfo.parentSeq) { rootNodeList.push(node); return; } if (nodeMap.has(deptInfo.parentSeq)) { const ancestor = nodeMap.get(deptInfo.parentSeq); node.ancestorSeq = ancestor.deptInfo.seq; ancestor.children.push(node); } else { remainChildNodeList.push(node); } }); if ( !displayRoot && 0 < rootNodeList.length && 0 === rootNodeList[0].deptInfo.parentSeq && !!rootNodeList[0].children && 0 < rootNodeList[0].children.length ) { rootNodeList = [...rootNodeList[0].children]; } remainChildNodeList.forEach((node) => { const ancestor = nodeMap.get(node.deptInfo.parentSeq); if (!!ancestor) { node.ancestorSeq = ancestor.deptInfo.seq; ancestor.children.push(node); } }); this.dataSource.data = rootNodeList; if (!!data.expanded) { this.expand(...data.expanded); } this.changeDetectorRef.detectChanges(); } @Output() currentDeptSeqChange: EventEmitter = new EventEmitter(); @Input() set currentDeptSeq(deptInfoSeq: number) { this._currentDeptSeq = deptInfoSeq; } get currentDeptSeq() { return this._currentDeptSeq; } // tslint:disable-next-line: variable-name _currentDeptSeq: number; @Output() clickNode = new EventEmitter(); @ViewChild('treeList', { static: false }) treeList: MatTree; @ViewChild('cvsvList', { static: false }) cvsvList: CdkVirtualScrollViewport; @ViewChild(PerfectScrollbarDirective, { static: false }) psDirectiveRef?: PerfectScrollbarDirective; treeControl: FlatTreeControl; treeFlattener: MatTreeFlattener; dataSource: VirtualScrollTreeFlatDataSource; // tslint:disable-next-line: variable-name private _ngOnDestroySubject: Subject; constructor( private changeDetectorRef: ChangeDetectorRef, private logService: LogService ) { 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, level, data: node }; }, (node) => node.level, (node) => node.expandable, (node) => node.children ); this.dataSource = new VirtualScrollTreeFlatDataSource< OrganizationNode, FlatNode >(this.treeControl, this.treeFlattener); } ngOnInit(): void { this._ngOnDestroySubject = new Subject(); this.dataSource.cdkVirtualScrollViewport = this.cvsvList; this.treeControl.expansionModel.changed .pipe(takeUntil(this._ngOnDestroySubject)) .subscribe(() => { this.cvsvList.checkViewportSize(); this.psDirectiveRef.update(); }); } ngOnDestroy(): void { if (!!this._ngOnDestroySubject) { this._ngOnDestroySubject.next(); this._ngOnDestroySubject.complete(); } } hasChild = (_: number, node: FlatNode) => node.expandable; trackBy = (_: number, node: FlatNode) => node.data.deptInfo.seq; onClickNode(event: Event, node: FlatNode) { event.stopPropagation(); this.clickNode.emit(node.data.deptInfo); } expand(...deptSeq: number[]) { if (!this.treeControl.dataNodes || !deptSeq || 0 === deptSeq.length) { return; } const flatNodes: FlatNode[] = []; deptSeq.forEach((s) => { const node = this.treeControl.dataNodes.find( (n) => n.data.deptInfo.seq === s ); if (!!node) { flatNodes.push(node); this.selectHierarchy(flatNodes, node); } }); if (0 < flatNodes.length) { this.treeControl.expansionModel.select(...flatNodes); } } expandAll() { this.treeControl.expandAll(); } collapse(deptSeq: number) { if (!this.treeControl.dataNodes) { return; } const node = this.treeControl.dataNodes.find( (n) => n.data.deptInfo.seq === deptSeq ); if (!!node) { this.treeControl.collapse(node); } } collapseAll() { this.treeControl.collapseAll(); } private selectHierarchy(flatNodes: FlatNode[], flatNode: FlatNode): void { // tslint:disable-next-line: variable-name let _flatNode = flatNode; while (true) { if (!_flatNode) { return; } const ancestorSeq = _flatNode.data.ancestorSeq; if (!ancestorSeq) { return; } _flatNode = this.treeControl.dataNodes.find( (n) => n.data.deptInfo.seq === ancestorSeq ); if (!_flatNode) { return; } const i = flatNodes.findIndex( (n) => n.data.deptInfo.seq === _flatNode.data.deptInfo.seq ); if (-1 === i) { flatNodes.push(_flatNode); } } } }