LP#1775466 Tree widget
authorBill Erickson <berickxx@gmail.com>
Wed, 18 Jul 2018 21:42:39 +0000 (17:42 -0400)
committerBill Erickson <berickxx@gmail.com>
Wed, 25 Jul 2018 17:11:54 +0000 (13:11 -0400)
Signed-off-by: Bill Erickson <berickxx@gmail.com>
Open-ILS/src/eg2/src/app/share/tree/tree.component.css [new file with mode: 0644]
Open-ILS/src/eg2/src/app/share/tree/tree.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/share/tree/tree.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/share/tree/tree.module.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/share/tree/tree.ts [new file with mode: 0644]

diff --git a/Open-ILS/src/eg2/src/app/share/tree/tree.component.css b/Open-ILS/src/eg2/src/app/share/tree/tree.component.css
new file mode 100644 (file)
index 0000000..0d29dd7
--- /dev/null
@@ -0,0 +1,19 @@
+
+.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);
+}
+
diff --git a/Open-ILS/src/eg2/src/app/share/tree/tree.component.html b/Open-ILS/src/eg2/src/app/share/tree/tree.component.html
new file mode 100644 (file)
index 0000000..80125b3
--- /dev/null
@@ -0,0 +1,20 @@
+
+
+<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">
+       &nbsp; 
+      </div>
+    </div>
+    <div class="eg-tree-node" [ngClass]="{active : node.active}">
+      <a [routerLink]="" (click)="handleNodeClick(node)">{{node.label}}</a>
+    </div>
+  </div>
+</div>
diff --git a/Open-ILS/src/eg2/src/app/share/tree/tree.component.ts b/Open-ILS/src/eg2/src/app/share/tree/tree.component.ts
new file mode 100644 (file)
index 0000000..d933957
--- /dev/null
@@ -0,0 +1,60 @@
+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);
+    }
+}
+
+
+
diff --git a/Open-ILS/src/eg2/src/app/share/tree/tree.module.ts b/Open-ILS/src/eg2/src/app/share/tree/tree.module.ts
new file mode 100644 (file)
index 0000000..3894fcd
--- /dev/null
@@ -0,0 +1,20 @@
+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 {}
+
diff --git a/Open-ILS/src/eg2/src/app/share/tree/tree.ts b/Open-ILS/src/eg2/src/app/share/tree/tree.ts
new file mode 100644 (file)
index 0000000..f230780
--- /dev/null
@@ -0,0 +1,117 @@
+
+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;
+    }
+}
+