Skip to content
Snippets Groups Projects
Commit f9911ca2 authored by Xiao Gui's avatar Xiao Gui Committed by xgui3783
Browse files

feat: flat tree for complex trees (eg allen mouse, 1k+ nodes)

parent 3209f8eb
No related branches found
No related tags found
No related merge requests found
......@@ -16,6 +16,12 @@ import { SearchResultPaginationPipe } from '../util/pipes/pagination.pipe';
import { ToastComponent } from './toast/toast.component';
import { TreeSearchPipe } from '../util/pipes/treeSearch.pipe';
import { TreeBaseDirective } from './tree/treeBase.directive';
import { FlatTreeComponent } from './flatTree/flatTree.component';
import { FlattenTreePipe } from './flatTree/flattener.pipe';
import { RenderPipe } from './flatTree/render.pipe';
import { HighlightPipe } from './flatTree/highlight.pipe';
import { HasVisibleChildrenPipe } from './flatTree/hasVisibleChildren.pipe';
import { FitlerRowsByVisibilityPipe } from './flatTree/filterRowsByVisibility.pipe';
@NgModule({
......@@ -33,6 +39,7 @@ import { TreeBaseDirective } from './tree/treeBase.directive';
PanelComponent,
PaginationComponent,
ToastComponent,
FlatTreeComponent,
/* directive */
HoverableBlockDirective,
......@@ -42,6 +49,11 @@ import { TreeBaseDirective } from './tree/treeBase.directive';
SafeHtmlPipe,
SearchResultPaginationPipe,
TreeSearchPipe,
FlattenTreePipe,
RenderPipe,
HighlightPipe,
HasVisibleChildrenPipe,
FitlerRowsByVisibilityPipe
],
exports : [
BrowserAnimationsModule,
......@@ -53,12 +65,13 @@ import { TreeBaseDirective } from './tree/treeBase.directive';
PanelComponent,
PaginationComponent,
ToastComponent,
FlatTreeComponent,
SearchResultPaginationPipe,
TreeSearchPipe,
HoverableBlockDirective,
TreeBaseDirective,
TreeBaseDirective
]
})
......
import { Pipe, PipeTransform } from "@angular/core";
@Pipe({
name : 'filterRowsByVisbilityPipe'
})
export class FitlerRowsByVisibilityPipe implements PipeTransform{
public transform(rows:any[], getChildren : (item:any)=>any[], filterFn : (item:any)=>boolean){
return rows.filter(row => this.recursive(row, getChildren, filterFn) )
}
private recursive(single : any, getChildren : (item:any) => any[], filterFn:(item:any) => boolean):boolean{
return filterFn(single) || getChildren(single).some(c => this.recursive(c, getChildren, filterFn))
}
}
\ No newline at end of file
import { EventEmitter, Component, Input, Output, ChangeDetectionStrategy, ChangeDetectorRef, Optional } from "@angular/core";
import { FlattenedTreeInterface } from "./flattener.pipe";
@Component({
selector : 'flat-tree-component',
templateUrl : './flatTree.template.html',
styleUrls : [
'./flatTree.style.css'
],
changeDetection:ChangeDetectionStrategy.OnPush
})
export class FlatTreeComponent{
@Input() inputItem : any = {
name : 'Untitled',
children : []
}
@Input() childrenExpanded : boolean = true
@Output() treeNodeClick : EventEmitter<any> = new EventEmitter()
@Output() treeNodeEnter : EventEmitter<any> = new EventEmitter()
@Output() treeNodeLeave : EventEmitter<any> = new EventEmitter()
@Input() renderNode : (item:any)=>string = (item)=>item.name
@Input() findChildren : (item:any)=>any[] = (item)=>item.children ? item.children : []
@Input() searchFilter : (item:any)=>boolean | null = ()=>true
constructor(){
// this.searchedVisibleSet = this.searchFilter()
}
getClass(level:number){
// return `render-node-level-${level}`
return [...Array(level+1)].map((v,idx) => `render-node-level-${idx}`).join(' ')
}
collapsedLevels: Set<string> = new Set()
searchedVisibleSet: Set<string> = new Set()
toggleCollapse(flattenedItem:FlattenedTreeInterface){
this.collapsedLevels.has(flattenedItem.lvlId)
? this.collapsedLevels.delete(flattenedItem.lvlId)
: this.collapsedLevels.add(flattenedItem.lvlId)
}
isCollapsed(flattenedItem:FlattenedTreeInterface):boolean{
return this.collapsedLevels.has(flattenedItem.lvlId)
}
collapseRow(flattenedItem:FlattenedTreeInterface):boolean{
return flattenedItem.lvlId.split('_')
.filter((v,idx,arr) => idx < arr.length -1 )
.reduce((acc,curr) => acc
.concat(acc.length === 0
? curr
: acc[acc.length -1].concat(`_${curr}`)), [])
.find(id => this.collapsedLevels.has(id))
? true
: false
}
filterRows(items:any[], ){
}
}
\ No newline at end of file
:host
{
display: block;
}
.render-node-text
{
cursor:default;
}
.padding-block-container
{
position:absolute;
left:0;
top:0;
height:1.5em;
width: 0px;
white-space:nowrap;
}
span[renderText]
{
white-space: nowrap;
overflow: hidden;
}
.padding-block
{
display: inline-block;
width: 1em;
height:1.5em;
position:relative;
}
.padding-block[hidemargin="false"]:before
{
pointer-events: none;
content: ' ';
position:absolute;
width:100%;
height:100%;
left:0.5em;
top:-0.75em;
border-left:rgba(128,128,128,0.6) 1px dashed;
}
.render-node-text:hover
{
color:rgba(219, 181, 86,1);
}
.render-node-level-1
{
position:relative;
white-space: nowrap;
}
.render-node-level-1:before
{
pointer-events: none;
content: ' ';
height:1.5em;
width: 1.5em;
bottom: 0.75em;
left: -0.5em;
position: absolute;
border-left: rgba(128,128,128,0.6) 1px dashed;
border-bottom: rgba(128,128,128,0.6) 1px dashed;
}
.render-node-level-0
{
margin-left: 0em;
}
.render-node-level-1
{
margin-left: 1em;
}
.render-node-level-2
{
margin-left: 2em;
}
.render-node-level-3
{
margin-left: 3em;
}
.render-node-level-4
{
margin-left: 4em;
}
.render-node-level-5
{
margin-left: 5em;
}
.render-node-level-6
{
margin-left: 6em;
}
.render-node-level-7
{
margin-left: 7em;
}
.render-node-level-8
{
margin-left: 8em;
}
.render-node-level-9
{
margin-left: 9em;
}
.render-node-level-10
{
margin-left: 10em;
}
.render-node-level-0 > .padding-block-container
{
left: -0em;
}
.render-node-level-1 > .padding-block-container
{
left: -1em;
}
.render-node-level-2 > .padding-block-container
{
left: -2em;
}
.render-node-level-3 > .padding-block-container
{
left: -3em;
}
.render-node-level-4 > .padding-block-container
{
left: -4em;
}
.render-node-level-5 > .padding-block-container
{
left: -5em;
}
.render-node-level-6 > .padding-block-container
{
left: -6em;
}
.render-node-level-7 > .padding-block-container
{
left: -7em;
}
.render-node-level-8 > .padding-block-container
{
left: -8em;
}
.render-node-level-9 > .padding-block-container
{
left: -9em;
}
.render-node-level-10 > .padding-block-container
{
left: -10em;
}
\ No newline at end of file
<div
*ngFor = "let flattenedItem of (inputItem | flattenTreePipe : findChildren | filterRowsByVisbilityPipe : findChildren : searchFilter)"
[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) = "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
(mouseenter) = "treeNodeEnter.emit(flattenedItem)"
(mouseleave) = "treeNodeLeave.emit(flattenedItem)"
(click) = "treeNodeClick.emit(flattenedItem)"
class = "render-node-text"
[innerHtml] = "flattenedItem | renderPipe : renderNode ">
</span>
</div>
\ No newline at end of file
import { Pipe, PipeTransform } from "@angular/core";
@Pipe({
name : 'flattenTreePipe'
})
export class FlattenTreePipe implements PipeTransform{
public transform(root:any, findChildren: (root:any) => any[]):any&FlattenedTreeInterface[]{
return this.recursiveFlatten(root,findChildren,0, '0', [])
}
private recursiveFlatten(obj, findChildren, flattenedTreeLevel, lvlId, siblingFlags){
return [
this.attachLvlAndLvlIdAndSiblingFlag(
obj,
flattenedTreeLevel,
lvlId,
siblingFlags
)
].concat(
...findChildren(obj)
.map((c,idx,arr) => this.recursiveFlatten(c,findChildren,flattenedTreeLevel + 1, `${lvlId}_${idx}`, siblingFlags.concat( idx === (arr.length - 1)) ))
)
}
private attachLvlAndLvlIdAndSiblingFlag(obj:any, flattenedTreeLevel:number, lvlId:string, siblingFlags : boolean[]){
return Object.assign({}, obj,{
flattenedTreeLevel,
collapsed : typeof obj.collapsed === 'undefined' ? false : true,
lvlId,
siblingFlags
})
}
}
export interface FlattenedTreeInterface{
flattenedTreeLevel : number
lvlId : string
}
\ No newline at end of file
import { Pipe, PipeTransform } from "@angular/core";
@Pipe({
name : 'hasVisibleChildrenPipe'
})
export class HasVisibleChildrenPipe implements PipeTransform{
public transform(item:any, getChildren: (item:any)=>any[], filterFn: (item:any)=>boolean){
return filterFn(item) || getChildren(item).some(c => this.transform(c, getChildren, filterFn))
}
}
\ No newline at end of file
import { Pipe, PipeTransform, SecurityContext } from "@angular/core";
import { SafeHtml, DomSanitizer } from "@angular/platform-browser";
@Pipe({
name : 'highlightPipe'
})
export class HighlightPipe implements PipeTransform{
constructor(private sanitizer: DomSanitizer){
}
public transform(input:string, searchTerm:string){
return searchTerm && searchTerm !== ''
? this.sanitizer.bypassSecurityTrustHtml(input.replace(new RegExp( searchTerm, 'gi'), (s) => `<span class = "highlight">${s}</span>`))
: input
}
}
\ No newline at end of file
import { Pipe, PipeTransform } from "@angular/core";
@Pipe({
name : 'renderPipe'
})
export class RenderPipe implements PipeTransform{
public transform(node:any, renderFunction:(node:any)=>string):string{
return renderFunction(node)
}
}
\ No newline at end of file
......@@ -47,14 +47,14 @@
<span>{{ selectedRegions.length }} {{ selectedRegions.length > 1 ? 'regions' : 'region' }} selected</span>
<span (click) = "clearRegions($event)" *ngIf = "selectedRegions.length > 0" class = "btn btn-link">clear all</span>
</div>
<tree-component
*ngFor = "let child of (selectedParcellation.regions | treeSearch : filterTreeBySearch.bind(this) : getChildren )"
(treeNodeClick) = "handleClickRegion($event)"
[renderNode]="(displayTreeNode).bind(this)"
[searchFilter]="(filterTreeBySearch).bind(this)"
[inputItem]="child"
treebase>
</tree-component>
<flat-tree-component
*ngFor = "let child of selectedParcellation.regions "
(treeNodeClick) = "handleClickRegion({inputItem:$event})"
[inputItem] = "child"
[renderNode] = "displayTreeNode.bind(this)"
[searchFilter] = "filterTreeBySearch.bind(this)">
</flat-tree-component>
</div>
</div>
</ng-template>
\ No newline at end of file
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment