diff --git a/lib/ts-types/jstree.d.ts b/lib/ts-types/jstree.d.ts index 2c9ed96e..6ee17c99 100644 --- a/lib/ts-types/jstree.d.ts +++ b/lib/ts-types/jstree.d.ts @@ -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 // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped @@ -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; + 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) diff --git a/package-lock.json b/package-lock.json index b3c2bf52..927ab698 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,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", @@ -755,11 +755,11 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" }, "node_modules/jstree": { - "version": "3.3.14", - "resolved": "https://registry.npmjs.org/jstree/-/jstree-3.3.14.tgz", - "integrity": "sha512-W8t+nFOKENXNIulvu+DW4gPcnpOXY0FswiTiOn1Fnhs6poRe6eA/Kf6fS1/GJJ8C8KEy0q3ttF6tbGRDmHIM/g==", + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/jstree/-/jstree-3.3.16.tgz", + "integrity": "sha512-yeeIJffi2WAqyMeHufXj/Ozy7GqgKdDkxfN8L8lwbG0h1cw/TgDafWmyhroH4AKgDSk9yW1W6jiJZu4zXAqzXw==", "dependencies": { - "jquery": "^3.6.0" + "jquery": "^3.5.0" } }, "node_modules/kaitai-struct": { diff --git a/package.json b/package.json index 677d671a..74974386 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/v1/app.ts b/src/v1/app.ts index a24122e7..d55cf531 100644 --- a/src/v1/app.ts +++ b/src/v1/app.ts @@ -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 = 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 = 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); diff --git a/src/v1/parsedToTree.ts b/src/v1/parsedToTree.ts index bb82887d..b61f3d79 100644 --- a/src/v1/parsedToTree.ts +++ b/src/v1/parsedToTree.ts @@ -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) { + const openNodes: string[] = state.core.open; + const nodesToLoad = new Set(); + 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[]) => (this.jstree).element.on(...args); - this.jstree.off = (...args: 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[]) => (this.jstree).get_container().on(...args); + this.jstree.off = (...args: 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(); } - 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 = te.node; - this.parsedTreeOpenedNodes[this.getNodeId(node)] = true; - this.saveOpenedNodes(); - }).on("close_node.jstree", (e, te) => { - var node = 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 { @@ -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 { - return new Promise((resolve, reject) => { - this.saveOpenedNodesDisabled = true; - var origAnim = (this.jstree).settings.core.animation; - (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); - (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 { + const pathParts = typeof path === "string" ? path.split("/") : path; - activatePath(path: string|string[]): Promise { - 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(); + }); }); } }