From 6d34b62c2a81710ccbab2079d685f4b7a2a66cc0 Mon Sep 17 00:00:00 2001 From: Xiao Gui <xgui3783@gmail.com> Date: Tue, 2 Oct 2018 14:28:26 +0200 Subject: [PATCH] feat: cluster flat tree, improve initial load perf --- src/components/components.module.ts | 4 +- .../flatTree/appendSiblingFlag.pipe.ts | 26 ++++--- src/components/flatTree/flatTree.component.ts | 59 ++++++++++++++- src/components/flatTree/flatTree.style.css | 27 +++++++ .../flatTree/flatTree.template.html | 71 +++++++++++-------- 5 files changed, 141 insertions(+), 46 deletions(-) diff --git a/src/components/components.module.ts b/src/components/components.module.ts index edefaf871..8e3c9a588 100644 --- a/src/components/components.module.ts +++ b/src/components/components.module.ts @@ -22,6 +22,7 @@ import { RenderPipe } from './flatTree/render.pipe'; import { HighlightPipe } from './flatTree/highlight.pipe'; import { FitlerRowsByVisibilityPipe } from './flatTree/filterRowsByVisibility.pipe'; import { AppendSiblingFlagPipe } from './flatTree/appendSiblingFlag.pipe'; +import { ClusteringPipe } from './flatTree/clustering.pipe'; @NgModule({ @@ -53,7 +54,8 @@ import { AppendSiblingFlagPipe } from './flatTree/appendSiblingFlag.pipe'; RenderPipe, HighlightPipe, FitlerRowsByVisibilityPipe, - AppendSiblingFlagPipe + AppendSiblingFlagPipe, + ClusteringPipe ], exports : [ BrowserAnimationsModule, diff --git a/src/components/flatTree/appendSiblingFlag.pipe.ts b/src/components/flatTree/appendSiblingFlag.pipe.ts index bcec2058f..b927bbdbf 100644 --- a/src/components/flatTree/appendSiblingFlag.pipe.ts +++ b/src/components/flatTree/appendSiblingFlag.pipe.ts @@ -6,19 +6,17 @@ import { Pipe, PipeTransform } from "@angular/core"; export class AppendSiblingFlagPipe implements PipeTransform{ public transform(objs:any[]):any[]{ - const idSet = new Set(objs.map(obj => obj.lvlId)) - return objs.map(obj => - Object.assign({}, obj, { - siblingFlags : obj.lvlId - .split('_') - .reduce((acc,curr) => acc.concat(acc.length === 0 ? curr : acc[acc.length -1].concat(`_${curr}`)), []) - .map(prevIds => this.isLast(prevIds,idSet)) - .slice(1) - }) - ) - } - - private isLast(id:string, set:Set<string>):boolean{ - return !set.has(id.split('_').slice(0,-1).concat( (Number(id.split('_').reverse()[0]) + 1).toString() ).join('_')) + return objs + .reduceRight((acc,curr) => ({ + acc : acc.acc.concat(Object.assign({}, curr, { + siblingFlags : curr.lvlId.split('_').map((v, idx) => typeof acc.flags[idx] !== 'undefined' + ? acc.flags[idx] + : false) + .slice(1) + .map(v => !v) + })), + flags: curr.lvlId.split('_').map((_,idx) => acc.flags[idx] ).slice(0, -1).concat(true) + }), { acc:[], flags : Array(256).fill(false) }) + .acc.reverse() } } \ No newline at end of file diff --git a/src/components/flatTree/flatTree.component.ts b/src/components/flatTree/flatTree.component.ts index e31656057..5fde9a144 100644 --- a/src/components/flatTree/flatTree.component.ts +++ b/src/components/flatTree/flatTree.component.ts @@ -1,4 +1,4 @@ -import { EventEmitter, Component, Input, Output, ChangeDetectionStrategy, ChangeDetectorRef, Optional } from "@angular/core"; +import { EventEmitter, Component, Input, Output, ChangeDetectionStrategy, ElementRef, OnDestroy, ChangeDetectorRef, ViewChildren, QueryList, AfterViewChecked, AfterViewInit } from "@angular/core"; import { FlattenedTreeInterface } from "./flattener.pipe"; @Component({ @@ -10,7 +10,7 @@ import { FlattenedTreeInterface } from "./flattener.pipe"; changeDetection:ChangeDetectionStrategy.OnPush }) -export class FlatTreeComponent{ +export class FlatTreeComponent implements AfterViewChecked, AfterViewInit, OnDestroy{ @Input() inputItem : any = { name : 'Untitled', children : [] @@ -27,6 +27,55 @@ export class FlatTreeComponent{ @Input() findChildren : (item:any)=>any[] = (item)=>item.children ? item.children : [] @Input() searchFilter : (item:any)=>boolean | null = ()=>true + @Input() flatTreeViewPort : HTMLElement + + @ViewChildren('flatTreeStart',{read : ElementRef}) flatTreeStartCollection : QueryList<ElementRef> + @ViewChildren('flatTreeEnd',{read : ElementRef}) flatTreeEndCollection : QueryList<ElementRef> + + intersectionObserver : IntersectionObserver + + constructor( + private cdr:ChangeDetectorRef + ){ + + } + + ngAfterViewChecked(){ + if(this.intersectionObserver){ + this.intersectionObserver.disconnect() + this.flatTreeStartCollection.forEach(er => this.intersectionObserver.observe(er.nativeElement)) + this.flatTreeEndCollection.forEach(er => this.intersectionObserver.observe(er.nativeElement)) + } + } + + ngAfterViewInit(){ + + if(this.flatTreeViewPort){ + this.intersectionObserver = new IntersectionObserver(entries => { + const currPos = entries + .filter(entry => entry.isIntersecting) + .filter(entry => Number(entry.target.getAttribute('clusterindex')) !== NaN ) + .map(entry => Number(entry.target.getAttribute('clusterindex'))) + .reduce((acc, clusterindex, i, array) => acc + (clusterindex / array.length), 0) + + if( currPos - this._currentPos >= 1 ){ + this._currentPos = Math.round(currPos) + this.cdr.markForCheck() + } + },{ + root: this.flatTreeViewPort, + rootMargin : '0px', + threshold : 0.1 + }) + } + } + + ngOnDestroy(){ + if(this.intersectionObserver){ + this.intersectionObserver.disconnect() + } + } + getClass(level:number){ return [...Array(level+1)].map((v,idx) => `render-node-level-${idx}`).join(' ') } @@ -55,4 +104,10 @@ export class FlatTreeComponent{ ? true : false } + + private _currentPos : number = 0 + + showCluster(index:number){ + return index <= this._currentPos + 1 + } } \ No newline at end of file diff --git a/src/components/flatTree/flatTree.style.css b/src/components/flatTree/flatTree.style.css index 15a2cb786..cc5bdf60a 100644 --- a/src/components/flatTree/flatTree.style.css +++ b/src/components/flatTree/flatTree.style.css @@ -1,6 +1,33 @@ :host { display: block; + height:100%; +} + +[clusterContainer] +{ + position:relative; +} + +[flatTreeStart] +{ + position: absolute; + top: 0; + height:50%; + width:1em; + opacity:0; + background-color:rgba(200,250,200,0.2); +} + +[flatTreeEnd] +{ + position: absolute; + top: 0; + transform: translateY(100%); + width:1em; + opacity:0; + background-color:rgba(250,200,200,0.2); + height:50%; } .render-node-text diff --git a/src/components/flatTree/flatTree.template.html b/src/components/flatTree/flatTree.template.html index 33f7b3c95..f8b60d1ac 100644 --- a/src/components/flatTree/flatTree.template.html +++ b/src/components/flatTree/flatTree.template.html @@ -1,33 +1,46 @@ -<div - *ngFor = "let flattenedItem of (inputItem | flattenTreePipe : findChildren | filterRowsByVisbilityPipe : findChildren : searchFilter | appendSiblingFlagPipe)" - [ngClass] = "getClass(flattenedItem.flattenedTreeLevel)" - [attr.flattenedtreelevel] = "flattenedItem.flattenedTreeLevel" - [attr.collapsed] = "flattenedItem.collapsed ? flattenedItem.collapsed : false" - [attr.lvlId] = "flattenedItem.lvlId" - [hidden] = "collapseRow(flattenedItem) " - renderNode> +<div class="container"> + <div *ngFor = "let flattenedItems of (inputItem | flattenTreePipe : findChildren | filterRowsByVisbilityPipe : findChildren : searchFilter | appendSiblingFlagPipe | clusteringPipe : 50 ); let index = index" clusterContainer> - <span class = "padding-block-container"> - <span - [attr.hidemargin] = "block" - *ngFor = "let block of flattenedItem.siblingFlags" - class = "padding-block"> + <div [attr.clusterindex] = "index" flatTreeStart #flatTreeStart> + </div> - </span> - </span> - <span - *ngIf = "findChildren(flattenedItem).length > 0; else noChildren" - (click) = "$event.stopPropagation(); toggleCollapse(flattenedItem)" > - <i [ngClass] = "isCollapsed(flattenedItem) ? 'glyphicon-chevron-right' : 'glyphicon-chevron-down'" class="glyphicon"></i> - </span> - <ng-template #noChildren> - <i class="glyphicon glyphicon-none"> + <div *ngIf = "showCluster(index)"> + <div + *ngFor = "let flattenedItem of flattenedItems" + [ngClass] = "getClass(flattenedItem.flattenedTreeLevel)" + [attr.flattenedtreelevel] = "flattenedItem.flattenedTreeLevel" + [attr.collapsed] = "flattenedItem.collapsed ? flattenedItem.collapsed : false" + [attr.lvlId] = "flattenedItem.lvlId" + [hidden] = "collapseRow(flattenedItem) " + renderNode> + + <span class = "padding-block-container"> + <span + [attr.hidemargin] = "block" + *ngFor = "let block of flattenedItem.siblingFlags" + class = "padding-block"> + + </span> + </span> + <span + *ngIf = "findChildren(flattenedItem).length > 0; else noChildren" + (click) = "$event.stopPropagation(); toggleCollapse(flattenedItem)" > + <i [ngClass] = "isCollapsed(flattenedItem) ? 'glyphicon-chevron-right' : 'glyphicon-chevron-down'" class="glyphicon"></i> + </span> + <ng-template #noChildren> + <i class="glyphicon glyphicon-none"> + + </i> + </ng-template> + <span + (click) = "treeNodeClick.emit({event:$event,inputItem:flattenedItem})" + class = "render-node-text" + [innerHtml] = "flattenedItem | renderPipe : renderNode "> + </span> + </div> + </div> - </i> - </ng-template> - <span - (click) = "treeNodeClick.emit({event:$event,inputItem:flattenedItem})" - class = "render-node-text" - [innerHtml] = "flattenedItem | renderPipe : renderNode "> - </span> + <div [attr.clusterindex] = "index" flatTreeEnd #flatTreeEnd> + </div> + </div> </div> \ No newline at end of file -- GitLab