--- /dev/null
+
+.eg-tree-node-expandy .material-icons {
+ font-size: 16px;
+}
+
+.eg-tree-node {
+ padding: 2px;
+}
+
+.eg-tree-node.active {
+ background-color: rgba(0,0,0,.03);
+ border: 1px solid rgba(0,0,0,.125);
+ font-style: italic;
+}
+
+.eg-tree-node-nochild {
+ border-left: 2px dashed rgba(0,0,0,.125);
+}
+
--- /dev/null
+
+
+<div class="eg-tree" *ngFor="let node of displayNodes()">
+ <div class="eg-tree-node-wrapper d-flex"
+ [ngStyle]="{'padding-left': (node.depth * 20) + 'px'}">
+ <div class="eg-tree-node-expandy">
+ <div *ngIf="node.children.length" (click)="node.toggleExpand()"
+ i18n-title title="Toggle Expand Node">
+ <span *ngIf="!node.expanded" class="material-icons">expand_more</span>
+ <span *ngIf="node.expanded" class="material-icons">expand_less</span>
+ </div>
+ <div *ngIf="!node.children.length" class="eg-tree-node-nochild">
+
+ </div>
+ </div>
+ <div class="eg-tree-node" [ngClass]="{active : node.active}">
+ <a [routerLink]="" (click)="handleNodeClick(node)">{{node.label}}</a>
+ </div>
+ </div>
+</div>
--- /dev/null
+import {Component, OnInit, Input, Output, EventEmitter} from '@angular/core';
+import {Tree, TreeNode} from './tree';
+
+/*
+Tree Widget:
+
+<eg-tree
+ [tree]="myTree"
+ (nodeClicked)="nodeClicked($event)">
+</eg-tree>
+
+----
+
+constructor() {
+
+ const rootNode = new TreeNode({
+ id: 1,
+ label: 'Root',
+ children: [
+ new TreeNode({id: 2, label: 'Child'}),
+ new TreeNode({id: 3, label: 'Child2'})
+ ]
+ ]});
+
+ this.myTree = new Tree(rootNode);
+}
+
+nodeClicked(node: TreeNode) {
+ console.log('someone clicked on ' + node.label);
+}
+*/
+
+@Component({
+ selector: 'eg-tree',
+ templateUrl: 'tree.component.html',
+ styleUrls: ['tree.component.css']
+})
+export class TreeComponent implements OnInit {
+
+ @Input() tree: Tree;
+ @Output() nodeClicked: EventEmitter<TreeNode>;
+
+ constructor() {
+ this.nodeClicked = new EventEmitter<TreeNode>();
+ }
+
+ ngOnInit() {}
+
+ displayNodes(): TreeNode[] {
+ return this.tree.nodeList(true);
+ }
+
+ handleNodeClick(node: TreeNode) {
+ this.tree.activateNode(node);
+ this.nodeClicked.emit(node);
+ }
+}
+
+
+
--- /dev/null
+import {NgModule} from '@angular/core';
+import {EgCommonModule} from '@eg/common.module';
+import {TreeComponent} from './tree.component';
+
+@NgModule({
+ declarations: [
+ TreeComponent
+ ],
+ imports: [
+ EgCommonModule
+ ],
+ exports: [
+ TreeComponent
+ ],
+ providers: [
+ ]
+})
+
+export class TreeModule {}
+
--- /dev/null
+
+export class TreeNode {
+ id: any;
+ label: string;
+ expanded: boolean;
+ children: TreeNode[];
+ depth: number;
+ active: boolean;
+
+ constructor(values: {[key: string]: any}) {
+ this.children = [];
+ this.expanded = false;
+ this.depth = 0;
+ this.active = false;
+ if (values) {
+ if ('id' in values) { this.id = values.id; }
+ if ('label' in values) { this.label = values.label; }
+ if ('children' in values) { this.children = values.children; }
+ if ('expanded' in values) { this.expanded = values.expanded; }
+ }
+ }
+
+ toggleExpand() {
+ this.expanded = !this.expanded;
+ }
+}
+
+export class Tree {
+
+ rootNode: TreeNode;
+ idMap: {[id: string]: TreeNode};
+
+ constructor(rootNode?: TreeNode) {
+ this.rootNode = rootNode;
+ this.idMap = {};
+ }
+
+ // Returns a depth-first list of tree nodes
+ // Tweaks node attributes along the way to match the shape of the tree.
+ nodeList(filterHidden?: boolean): TreeNode[] {
+
+ const nodes = [];
+
+ const recurseTree =
+ (node: TreeNode, depth: number, hidden: boolean) => {
+ if (!node) { return; }
+
+ node.depth = depth++;
+ this.idMap[node.id + ''] = node;
+
+ if (hidden) {
+ // it could be confusing for a hidden node to be active.
+ node.active = false;
+ }
+
+ if (hidden && filterHidden) {
+ // Avoid adding hidden child nodes to the list.
+ } else {
+ nodes.push(node);
+ }
+
+ node.children.forEach(n => recurseTree(n, depth, !node.expanded));
+ }
+
+ recurseTree(this.rootNode, 0, false);
+ return nodes;
+ }
+
+ findNode(id: any): TreeNode {
+ if (this.idMap[id + '']) {
+ return this.idMap[id + ''];
+ } else {
+ // nodeList re-indexes all the nodes.
+ this.nodeList();
+ return this.idMap[id + ''];
+ }
+ }
+
+ findParentNode(node: TreeNode) {
+ const list = this.nodeList();
+ for (let idx = 0; idx < list.length; idx++) {
+ const pnode = list[idx];
+ if (pnode.children.filter(c => c.id === node.id).length) {
+ return pnode;
+ }
+ }
+ return null;
+ }
+
+ removeNode(node: TreeNode) {
+ if (!node) { return; }
+ const pnode = this.findParentNode(node);
+ if (pnode) {
+ pnode.children = pnode.children.filter(n => n.id !== node.id);
+ } else {
+ this.rootNode = null;
+ }
+ }
+
+ expandAll() {
+ this.nodeList().forEach(node => node.expanded = true);
+ }
+
+ collapseAll() {
+ this.nodeList().forEach(node => node.expanded = false);
+ }
+
+ activeNode(): TreeNode {
+ return this.nodeList().filter(node => node.active)[0];
+ }
+
+ activateNode(node: TreeNode) {
+ this.nodeList().forEach(n => n.active = false);
+ node.active = true;
+ }
+}
+