Skip to content

Commit

Permalink
Merge pull request #162 from kaitai-io/use-jstree-state-plugin
Browse files Browse the repository at this point in the history
Use jsTree's state plugin to persist object tree state
  • Loading branch information
generalmimon authored Feb 15, 2024
2 parents 5de330d + dcfdca6 commit bd9c288
Show file tree
Hide file tree
Showing 5 changed files with 119 additions and 130 deletions.
5 changes: 3 additions & 2 deletions lib/ts-types/jstree.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Type definitions for jsTree v3.3.2
// Type definitions for jsTree v3.3.2
// Project: http://www.jstree.com/
// Definitions by: Adam Pluciński <https://github.com/adaskothebeast>
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
Expand Down Expand Up @@ -814,7 +814,8 @@ interface JSTree extends JQuery {
* @param {Boolean} as_dom
* @return {Object|jQuery}
*/
get_node: (obj: any, as_dom?: boolean) => any;
get_node(obj: any, as_dom?: false): Record<string, any>;
get_node(obj: any, as_dom: true): JQuery;

/**
* get the path to a node, either consisting of node texts, or of node IDs, optionally glued together (otherwise an array)
Expand Down
10 changes: 5 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"golden-layout": "^1.5.9",
"jquery": "^3.5.0",
"js-yaml": "^4.1.0",
"jstree": "^3.3.4",
"jstree": "^3.3.16",
"kaitai-struct": "next",
"kaitai-struct-compiler": "next",
"localforage": "^1.5.0",
Expand Down
34 changes: 17 additions & 17 deletions src/v1/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,23 +123,23 @@ class AppController {
//console.log("reparse exportedRoot", exportedRoot);

this.ui.parsedDataTreeHandler = new ParsedTreeHandler(this.ui.parsedDataTreeCont.getElement(), exportedRoot, this.compilerService.ksyTypes);
await this.ui.parsedDataTreeHandler.initNodeReopenHandling();
this.ui.hexViewer.onSelectionChanged();

this.ui.parsedDataTreeHandler.jstree.on("select_node.jstree", (e, selectNodeArgs) => {
var node = <IParsedTreeNode>selectNodeArgs.node;
//console.log("node", node);
var exp = this.ui.parsedDataTreeHandler.getNodeData(node).exported;

if (exp && exp.path)
$("#parsedPath").text(exp.path.join("/"));

if (!this.blockRecursive && exp && exp.start < exp.end) {
this.selectedInTree = true;
//console.log("setSelection", exp.ioOffset, exp.start);
this.ui.hexViewer.setSelection(exp.ioOffset + exp.start, exp.ioOffset + exp.end - 1);
this.selectedInTree = false;
}

this.ui.parsedDataTreeHandler.jstree.on("state_ready.jstree", () => {
this.ui.parsedDataTreeHandler.jstree.on("select_node.jstree", (e, selectNodeArgs) => {
var node = <IParsedTreeNode>selectNodeArgs.node;
//console.log("node", node);
var exp = this.ui.parsedDataTreeHandler.getNodeData(node).exported;

if (exp && exp.path)
$("#parsedPath").text(exp.path.join("/"));

if (!this.blockRecursive && exp && exp.start < exp.end) {
this.selectedInTree = true;
//console.log("setSelection", exp.ioOffset, exp.start);
this.ui.hexViewer.setSelection(exp.ioOffset + exp.start, exp.ioOffset + exp.end - 1);
this.selectedInTree = false;
}
});
});

this.errors.handle(null);
Expand Down
198 changes: 93 additions & 105 deletions src/v1/parsedToTree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,45 +34,80 @@ export class ParsedTreeHandler {
this.jstree = jsTreeElement.jstree({
core: {
data: (node: IParsedTreeNode, cb: any) =>
this.getNode(node).then(x => cb(x), e => app.errors.handle(e)), themes: { icons: false }, multiple: false, force_text: false
}
this.getNode(node).then(x => cb(x), e => app.errors.handle(e)),
themes: { icons: false },
multiple: false,
force_text: false,
allow_reselect: true,
loaded_state: true,
},
plugins: [ "state" ],
state: {
preserve_loaded: true,
filter: function (state: Record<string, any>) {
const openNodes: string[] = state.core.open;
const nodesToLoad = new Set<string>();
for (const path of openNodes) {
const pathParts = path.split("-");
if (pathParts[0] !== "inputField") {
continue;
}
let subPath = pathParts.shift();
for (const part of pathParts) {
subPath += "-" + part;
if (this.is_loaded(subPath)) {
continue;
}
nodesToLoad.add(subPath);
}
}
// If we want to preserve the open state of nodes with closed parents,
// we must at least load their parents so that such nodes appear
// in the internal list of nodes that jsTree knows and the jsTree
// 'state' plugin can mark them as open.
state.core.loaded = Array.from(nodesToLoad);
return state;
},
},
}).jstree(true);
this.jstree.on = (...args: any[]) => (<any>this.jstree).element.on(...args);
this.jstree.off = (...args: any[]) => (<any>this.jstree).element.off(...args);
this.jstree.on("keyup.jstree", e => this.jstree.activate_node(e.target.id, null));
this.jstree.on = (...args: any[]) => (<any>this.jstree).get_container().on(...args);
this.jstree.off = (...args: any[]) => (<any>this.jstree).get_container().off(...args);
this.jstree.on("state_ready.jstree", () => {
// These settings have been set to `true` only temporarily so that our
// approach of populating the `state.core.loaded` property in the
// `state.filter` function takes effect.
this.jstree.settings.state.preserve_loaded = false;
this.jstree.settings.core.loaded_state = false;

this.updateActiveJstreeNode();
});
this.jstree.on("focus.jstree", ".jstree-anchor", e => {
const focusedNode = e.currentTarget;
if (!this.jstree.is_selected(focusedNode)) {
this.jstree.deselect_all(true);
this.jstree.select_node(focusedNode);
}
});
this.intervalHandler = new IntervalHandler<IParsedTreeInterval>();
}

private parsedTreeOpenedNodes: { [id: string]: boolean } = {};
private saveOpenedNodesDisabled = false;

private saveOpenedNodes() {
if (this.saveOpenedNodesDisabled) return;
localStorage.setItem("parsedTreeOpenedNodes", Object.keys(this.parsedTreeOpenedNodes).join(","));
}

public initNodeReopenHandling() {
var parsedTreeOpenedNodesStr = localStorage.getItem("parsedTreeOpenedNodes");
if (parsedTreeOpenedNodesStr)
parsedTreeOpenedNodesStr.split(",").forEach(x => this.parsedTreeOpenedNodes[x] = true);

return new Promise((resolve, reject) => {
this.jstree.on("ready.jstree", _ => {
this.openNodes(Object.keys(this.parsedTreeOpenedNodes)).then(() => {
this.jstree.on("open_node.jstree", (e, te) => {
var node = <IParsedTreeNode>te.node;
this.parsedTreeOpenedNodes[this.getNodeId(node)] = true;
this.saveOpenedNodes();
}).on("close_node.jstree", (e, te) => {
var node = <IParsedTreeNode>te.node;
delete this.parsedTreeOpenedNodes[this.getNodeId(node)];
this.saveOpenedNodes();
});

resolve();
}, err => reject(err));
});
});
updateActiveJstreeNode(): void {
const selectedNode = this.jstree.get_selected()[0];
if (!selectedNode) {
return;
}
// This ensures that next time the jsTree is focused (even when clicking
// somewhere in the empty space of the jsTree pane without clicking or
// hovering over any particular node first), the selected node (if any)
// will be focused. If we don't do this, jsTree will instead focus the
// most recently node that the user directly interacted with or (upon
// page load) the very first node of the entire tree, which is not
// ideal.
//
// As of jsTree 3.3.16, jsTree uses the `aria-activedescendant`
// attribute as the only means of persisting the active node, so we
// don't have much choice how to implement this.
this.jstree.get_container().attr('aria-activedescendant', selectedNode);
}

primitiveToText(exported: IExportedValue, detailed: boolean = true): string {
Expand Down Expand Up @@ -335,83 +370,36 @@ export class ParsedTreeHandler {
var path = nodeData.exported ? nodeData.exported.path : nodeData.instance.path;
if (nodeData.arrayStart || nodeData.arrayEnd)
path = path.concat([`${nodeData.arrayStart || 0}`, `${nodeData.arrayEnd || 0}`]);
return "inputField_" + path.join("_");
return ["inputField", ...path].join("-");
}

openNodes(nodesToOpen: string[]): Promise<boolean> {
return new Promise((resolve, reject) => {
this.saveOpenedNodesDisabled = true;
var origAnim = (<any>this.jstree).settings.core.animation;
(<any>this.jstree).settings.core.animation = 0;
//console.log("saveOpenedNodesDisabled = true");

var openCallCounter = 1;
var openRound = (e: any) => {
openCallCounter--;
//console.log("openRound", openCallCounter, nodesToOpen);

var newNodesToOpen: string[] = [];
var existingNodes: string[] = [];
nodesToOpen.forEach(nodeId => {
var node = this.jstree.get_node(nodeId);
if (node) {
if (!node.state.opened)
existingNodes.push(node);
} else
newNodesToOpen.push(nodeId);
});
nodesToOpen = newNodesToOpen;

//console.log("existingNodes", existingNodes, "openCallCounter", openCallCounter);

if (existingNodes.length > 0)
existingNodes.forEach(node => {
openCallCounter++;
//console.log(`open_node called on ${node.id}`)
this.jstree.open_node(node);
});
else if (openCallCounter === 0) {
//console.log("saveOpenedNodesDisabled = false");
this.saveOpenedNodesDisabled = false;
if (e)
this.jstree.off(e);
(<any>this.jstree).settings.core.animation = origAnim;
this.saveOpenedNodes();

resolve(nodesToOpen.length === 0);
}
};

this.jstree.on("open_node.jstree", e => openRound(e));
openRound(null);
});
}
activatePath(path: string|string[]): Promise<void> {
const pathParts = typeof path === "string" ? path.split("/") : path;

activatePath(path: string|string[]): Promise<boolean> {
var pathParts = typeof path === "string" ? path.split("/") : path;

var expandNodes = [];
var pathStr = "inputField";
for (var i = 0; i < pathParts.length; i++) {
pathStr += "_" + pathParts[i];
expandNodes.push(pathStr);
const nodesToLoad: string[] = [];
let pathStr = "inputField";
for (let i = 0; i < pathParts.length; i++) {
pathStr += "-" + pathParts[i];
nodesToLoad.push(pathStr);
}
var activateId = expandNodes.pop();

return this.openNodes(expandNodes).then(foundAll => {
//console.log("activatePath", foundAll, activateId);
this.jstree.activate_node(activateId, null);
const nodeToSelect = nodesToLoad.pop();

if (foundAll) {
var element = $(`#${activateId}`).get(0);
if (element)
return new Promise((resolve, reject) => {
this.jstree.load_node(nodesToLoad, () => {
// First select the node - the `select_node` method also recursively opens
// all parents of the selected node by default.
this.jstree.deselect_all(true);
this.jstree.select_node(nodeToSelect);

const element = this.jstree.get_node(nodeToSelect, true)[0];
if (element) {
element.scrollIntoView();
else {
console.log("element not found", activateId);
} else {
console.warn("element not found", nodeToSelect);
}
}

return foundAll;
this.updateActiveJstreeNode();
resolve();
});
});
}
}

0 comments on commit bd9c288

Please sign in to comment.