Skip to content

Commit

Permalink
Update inspect trees to support doing back in history and empty objects
Browse files Browse the repository at this point in the history
  • Loading branch information
JacobFischer committed Apr 19, 2019
1 parent 75e1ddc commit 6b3cd98
Show file tree
Hide file tree
Showing 7 changed files with 290 additions and 83 deletions.
9 changes: 9 additions & 0 deletions src/core/ui/tabular/tabular.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,15 @@ export class Tabular extends BaseElement {
}
}

/**
* Gets the currently active tab.
*
* @returns The currently active tab.
*/
public getActiveTab(): Tab {
return this.activeTab;
}

/**
* Fades a tab out, invoked when switching tabs.
*
Expand Down
1 change: 1 addition & 0 deletions src/core/ui/tree-view/tree-view-node.hbs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<li id="{{id}}" class="tree-view-node">
<header>
<span class="node-key">{{key}}</span>
<span class="node-key-value-spacer">=</span>
<span class="node-value">{{value}}</span>
</header>
<ul class="node-children"></ul>
Expand Down
34 changes: 32 additions & 2 deletions src/core/ui/tree-view/tree-view.scss
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
& ul {
list-style-type: none;
margin-left: 1em;
margin-bottom: 0.25em;
}

& li.tree-view-node > header {
Expand All @@ -22,16 +23,45 @@
color: $white;
}

.node-key:after {
&:before {
@include icon(square);
color: $dark-gray;
}

& .node-key-value-spacer {
color: $dark-gray;
content: " =";
}

&.inspect-expandable {
cursor: zoom-in;

&:before {
@include icon(plus-square);
}

&.expanded {
cursor: zoom-out;

&:before {
@include icon(minus-square);
}

& + ul:empty {
&:before, &:after {
color: $gray;
@extend .font-monospace;
}

&:before {
@include icon(square-o);
}

&:after {
content: "Empty";
font-style: italic;
margin-left: 0.5em;
}
}
}
}
}
Expand Down
222 changes: 176 additions & 46 deletions src/core/ui/tree-view/tree-view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,23 @@ interface ITreeableObject {
[key: string]: Treeable | undefined;
}

/** A node used t display a key/value pair */
export interface ITreeViewNode {
/** main li element */
/** The base empty node */
interface IBaseNode {
/** Parent node. */
parent?: ITreeViewNode;

/** the main element */
$element: JQuery;

/** Our node's key */
key: string | number;

/** A flag used to determine if this should be popped */
flag: boolean;
}

/** A node used t display a key/value pair */
export interface ITreeViewNode extends IBaseNode {
/** The header for the element */
$header: JQuery;

Expand All @@ -31,25 +43,42 @@ export interface ITreeViewNode {
/** The value element container */
$value: JQuery;

/** A flag to indicate if this node is/was expanded */
expanded: boolean;

/** The container for children */
$children: JQuery;

/** Child nodes of this node */
children: { [key: string]: undefined | ITreeViewNode };
}

/** a multi-level tree of expandable lists */
export class TreeView extends BaseElement {
/** The object we are currently displaying. */
private displaying: Treeable = {};
/** The name of this treeview */
public readonly name: string;

/** The root node for this tree. */
private readonly rootNode: ITreeViewNode;

// /** cache of items to re-use */
// private itemCache = new InsureMap<string, ITreeViewNode>();
/** Flag we flip to determine which nodes were not used */
private currentUnusedFlag = true;

/**
* Creates a new TreeView component.
*
* @param args - The base input args.
*/
constructor(args: IBaseElementArgs) {
constructor(args: IBaseElementArgs & {
/** The name of the root */
name: string;
}) {
super(args, treeViewHbs);

this.name = args.name;
this.rootNode = this.createNewNode(args.name || "root");
this.rootNode.$children.first().appendTo(this.element);
this.rootNode.expanded = true;
}

/**
Expand All @@ -58,10 +87,10 @@ export class TreeView extends BaseElement {
* @param tree - The object to display.
*/
public display(tree: ITreeableObject): void {
this.displaying = tree;
this.currentUnusedFlag = !this.currentUnusedFlag; // invert so all nodes have the opposite value

this.element.empty();
this.deepDisplay("root", this.displaying, this.element);
this.deepDisplay(this.rootNode.key, tree, this.rootNode);
this.pruneUnusedNodes(this.rootNode);
}

/**
Expand All @@ -84,43 +113,144 @@ export class TreeView extends BaseElement {
}

/**
* Displays some tree objects first keys in a parent.
* Creates a new node and inserts it as necessary.
*
* @param key - The key of this new node.
* @param parent - the parent node. If none then this is assume to be the root node.
* @returns The newly created node, inserted as necessary.
*/
protected createNewNode(key: string | number, parent?: ITreeViewNode): ITreeViewNode {
const $parent = parent
? parent.$children
: this.element;
const $element = partial(treeViewNodeHbs, { key }, $parent);

const node = {
$element,
$header: $element.find("header"),
$key: $element.find(".node-key"),
$value: $element.find(".node-value"),
$children: $element.find(".node-children"),
children: {},
expanded: false,
key,
flag: this.currentUnusedFlag,
parent,
};

if (parent) {
parent.children[key] = node;
}

return node;
}

/**
* Gets the keys for a given object.
*
* @param tree - The tree to get keys for
* @returns An array of the keys, in order to display
*/
protected getKeysFor<T extends ITreeableObject>(tree: T): Array<keyof T> {
return Object.keys(tree).sort((a, b) => {
const aNum = Number(a);
const bNum = Number(b);

if (!isNaN(aNum) && !isNaN(bNum)) {
return aNum - bNum;
}

if (a === b) {
return 0;
}
else {
return a < b ? -1 : 1;
}
});
}

/**
* Deeply displays a given node
* @param key - The key of the treeable from it's parent Treeable
* @param treeable - The Treeable to display, not an object with they given key set
* @param parentNode - The parent node of the Treeable, if a child node with the given key is not found, this will
* have a new child added to it.
*/
private deepDisplay(key: string | number, treeable: Treeable, parentNode: ITreeViewNode): void {
const node = parentNode.children[key] || this.createNewNode(key, parentNode);
node.flag = this.currentUnusedFlag;

if (node.children !== undefined) {
this.formatNodeValue(node, treeable);

if (isObject(treeable)) {
node.$header
.addClass("inspect-expandable")
.off("click")
.on("click", () => this.onNodeClicked(node, treeable));

if (node.expanded) {
const keys = this.getKeysFor(treeable);
for (const childKey of keys) {
const childValue = treeable[childKey];

this.deepDisplay(childKey, childValue, node);
}
}
}
}

// TODO:
// If this is NOT the current flag, then it was previously displayed
// so, deep display it, updating its flag
// also if this HAD children, then it was expanded,
// so make the above expandable already expanded
}

/**
* Invoked when a node's header is clicked to expand/retract
* @param node - The node clicked
* @param tree - The value of that given node
*/
private onNodeClicked(node: ITreeViewNode, tree: Treeable): void {
node.expanded = !node.expanded;
node.$header.toggleClass("expanded", node.expanded);

if (node.expanded && isObject(tree)) {
const keys = this.getKeysFor(tree);
for (const key of keys) {
this.deepDisplay(key, tree[key], node);
}
}
else {
node.$children.empty();
node.children = {};
}
}

/**
* Prunes this node, removing it from the dom, if it is no longer displayed.
*
* @param levelKey - The key level of the display
* @param tree - Treeable object to display
* @param $parent - The parent element for these keys.
* @param node - The node to recursively pruned.
* @returns True if removed, false otherwise
*/
private deepDisplay(levelKey: string, tree: ITreeableObject, $parent: JQuery): void {
for (const key of Object.keys(tree).sort()) {
const fullKey = `${levelKey}.${key}`;
const value = tree[key];
const $element = partial(treeViewNodeHbs, { key }, $parent);
const node = {
$element,
$header: $element.find("header"),
$key: $element.find(".node-key"),
$value: $element.find(".node-value"),
$children: $element.find(".node-children"),
};

this.formatNodeValue(node, value);
node.$element.appendTo($parent);
node.$element.attr("data-inspect-key", fullKey);

if (isObject(value)) {
node.$header.addClass("inspect-expandable");
const onClick = () => {
this.deepDisplay(fullKey, value, node.$children);
node.$header.addClass("expanded");

node.$header.one("click", () => {
node.$children.empty();
node.$header.one("click", onClick);
node.$header.removeClass("expanded");
});
};

node.$header.one("click", onClick);
private pruneUnusedNodes(node: ITreeViewNode): void {
if (node.flag !== this.currentUnusedFlag) {
node.$element.detach();
if (node.parent) {
delete node.parent.children[node.key];
}
}

if (node.children) {
const keys = Object.keys(node.children);
for (const key of keys) {
const child = node.children[key];
if (!child) {
throw new Error(`tree view ${key} should never have undefined children!`);
}

this.pruneUnusedNodes(child);
}
}
}
Expand Down
Loading

0 comments on commit 6b3cd98

Please sign in to comment.