import { ROOT_NODE } from 'node_store' export class TreeNative { constructor() {// {{{ this.treeNodeComponents = {} this.treeTrunk = [] this.expandedNodes = {} // keyed on UUID this.selectedNode = null this.rendered = false this.populateFirstLevel() }// }}} render() {// {{{ if (this.rendered) alert('Tree should only be rendered once.') const tmpl = document.createElement('template') tmpl.innerHTML = `
` const treeEl = tmpl.content.getElementById('tree-nodes') treeEl.addEventListener('keydown', event=>this.keyHandler(event)) tmpl.content.querySelector('.icons .search').addEventListener('click', ()=>_mbus.dispatch('op-search')) tmpl.content.querySelector('.icons .sync').addEventListener('click', ()=>_sync.run()) tmpl.content.getElementById('logo').addEventListener('click', ()=>_app.goToNode(ROOT_NODE, false, false)) for (const node of this.treeTrunk) { const treenode = new TreeNodeNative(this, node) this.treeNodeComponents[node.UUID] = treenode treeEl.appendChild(treenode.render()) } this.rendered = true return tmpl.content }// }}} populateFirstLevel() {//{{{ nodeStore.get(ROOT_NODE) .then(node => node.fetchChildren()) .then(children => { this.treeNodeComponents = {} this.treeTrunk = [] for (const node of children) { // The root node isn't supposed to be shown in the tree. if (node.UUID === ROOT_NODE) continue if (node.ParentUUID === ROOT_NODE) this.treeTrunk.push(node) } _mbus.dispatch('TREE_TRUNK_FETCHED') }) .catch(e => { console.log(e); console.log(e.type, e.error); alert(e.error) }) }//}}} getNodeExpanded(UUID) {//{{{ if (this.expandedNodes[UUID] === undefined) this.expandedNodes[UUID] = false return this.expandedNodes[UUID] }//}}} setNodeExpanded(node, value) {//{{{ let expanded = this.expandedNodes[node.UUID] if (expanded === undefined) { this.expandedNodes[node.UUID] = false expanded = false } if (expanded === value) return this.expandedNodes[node.UUID] = value _mbus.dispatch(`NODE_EXPAND_${node.UUID}`, value) }//}}} setSelected(node, dontExpand) {//{{{ if (node === undefined) return // The previously selected node, if any, needs to be rerendered // to not retain its 'selected' class. const prevUUID = this.selectedNode?.UUID this.selectedNode = node if (prevUUID) this.treeNodeComponents[prevUUID]?.render(true) // And now the newly selected node is rerendered. this.treeNodeComponents[node.UUID]?.render(true) if (!dontExpand) this.setNodeExpanded(node, true) }//}}} isSelected(node) {//{{{ return this.selectedNode?.UUID === node.UUID }//}}} async keyHandler(event) {//{{{ let handled = true const n = this.selectedNode const Space = ' ' // This handler would otherwise react to stuff like Ctrl+L. if (event.ctrlKey || event.altKey) return switch (event.key) { // Space and enter is toggling expansion. // Holding shift down does it recursively. case Space: case 'Enter': const expanded = this.getNodeExpanded(n.UUID) if (event.shiftKey) { this.recursiveExpand(n, !expanded) } else { this.setNodeExpanded(n, !expanded) } break case 'g': case 'Home': this.navigateTop() break case 'G': case 'End': this.navigateBottom() break case 'j': case 'ArrowDown': await this.navigateDown(this.selectedNode) break case 'k': case 'ArrowUp': await this.navigateUp(this.selectedNode) break case 'h': case 'ArrowLeft': await this.navigateLeft(this.selectedNode) break case 'l': case 'ArrowRight': await this.navigateRight(this.selectedNode) break default: // nonsole.log(event.key) handled = false } if (handled) { event.preventDefault() event.stopPropagation() } }//}}} async navigateLeft(n) {//{{{ if (n === null || n === undefined) return const expanded = this.getNodeExpanded(n.UUID) if (expanded && n.hasChildren()) { this.setNodeExpanded(n, false) return } if (n.isFirstSibling() && n.getParent().UUID !== ROOT_NODE) { _mbus.dispatch("GO_TO_NODE", { nodeUUID: n.getParent()?.UUID, dontPush: true, dontExpand: true }) return } const siblingBefore = n.getSiblingBefore() const siblingExpanded = this.getNodeExpanded(siblingBefore?.UUID) if (siblingBefore !== null && siblingExpanded && siblingBefore.hasChildren()) { const siblingAbove = this.getLastExpandedNode(siblingBefore) _mbus.dispatch("GO_TO_NODE", { nodeUUID: siblingAbove?.UUID, dontPush: true, dontExpand: true }) return } _mbus.dispatch("GO_TO_NODE", { nodeUUID: n.getSiblingBefore()?.UUID, dontPush: true, dontExpand: true }) }//}}} async navigateRight(n) {//{{{ if (n === null || n === undefined) return const siblingAfter = n.getSiblingAfter() const expanded = this.getNodeExpanded(n.UUID) if (!expanded && n.hasChildren()) { this.setNodeExpanded(n, true) return } if (expanded && n.hasChildren()) { _mbus.dispatch("GO_TO_NODE", { nodeUUID: n.Children[0]?.UUID, dontPush: true, dontExpand: true }) return } if (n.isLastSibling()) { const nextNode = this.getParentWithNextSibling(n) _mbus.dispatch("GO_TO_NODE", { nodeUUID: nextNode?.UUID, dontPush: true, dontExpand: true }) return } _mbus.dispatch("GO_TO_NODE", { nodeUUID: n.getSiblingAfter()?.UUID, dontPush: true, dontExpand: true }) }//}}} async navigateUp(n) {//{{{ if (n === null || n === undefined) return let parent = null const siblingBefore = n.getSiblingBefore() let siblingExpanded = false if (siblingBefore !== null) siblingExpanded = this.getNodeExpanded(siblingBefore.UUID) if (n.isFirstSibling()) { parent = n.getParent() if (parent?.UUID === ROOT_NODE) return _mbus.dispatch("GO_TO_NODE", { nodeUUID: parent?.UUID, dontPush: true, dontExpand: true }) return } if (siblingBefore !== null && siblingExpanded && siblingBefore.hasChildren()) { _mbus.dispatch("GO_TO_NODE", { nodeUUID: siblingBefore.Children[siblingBefore.Children.length - 1]?.UUID, dontPush: true, dontExpand: true }) return } if (siblingBefore) { _mbus.dispatch("GO_TO_NODE", { nodeUUID: siblingBefore.UUID, dontPush: true, dontExpand: true }) return } }//}}} async navigateDown(n) {//{{{ if (n === null || n === undefined) return const nodeExpanded = this.getNodeExpanded(n.UUID) // Last node, not expanded, so it matters not whether it has children or not. // Traverse upward to nearest parent with next sibling. if (!nodeExpanded && n.isLastSibling()) { const wantedNode = this.getParentWithNextSibling(n) _mbus.dispatch("GO_TO_NODE", { nodeUUID: wantedNode?.UUID, dontPush: true, dontExpand: true }) return } if (nodeExpanded && n.isLastSibling() && !n.hasChildren()) { const wantedNode = this.getParentWithNextSibling(n) _mbus.dispatch("GO_TO_NODE", { nodeUUID: wantedNode?.UUID, dontPush: true, dontExpand: true }) return } // Node not expanded. Go to this node's next sibling. // GoToNode will abort if given null. if (!nodeExpanded || !n.hasChildren()) { _mbus.dispatch("GO_TO_NODE", { nodeUUID: n.getSiblingAfter()?.UUID, dontPush: true, dontExpand: true }) return } // Node is expanded. // Children will be visually beneath this node, if any. if (nodeExpanded && n.hasChildren()) { _mbus.dispatch("GO_TO_NODE", { nodeUUID: n.Children[0].UUID, dontPush: true, dontExpand: true }) return } }//}}} async navigateTop() {//{{{ const root = await nodeStore.get(ROOT_NODE) if (root.Children.length === 0) return _mbus.dispatch("GO_TO_NODE", { nodeUUID: root.Children[0]?.UUID, dontPush: true, dontExpand: true }) }//}}} async navigateBottom() {//{{{ const root = await nodeStore.get(ROOT_NODE) if (root.Children.length === 0) return const toplevel = root.Children[root.Children.length - 1] const toplevelExpanded = this.getNodeExpanded(toplevel?.UUID) if (toplevelExpanded) { const lastnode = this.getLastExpandedNode(toplevel) _mbus.dispatch("GO_TO_NODE", { nodeUUID: lastnode?.UUID, dontPush: true, dontExpand: true }) } else _mbus.dispatch("GO_TO_NODE", { nodeUUID: root.Children[root.Children.length - 1]?.UUID, dontPush: true, dontExpand: true }) }//}}} getParentWithNextSibling(node) {//{{{ let currNode = node while (currNode !== null && currNode.UUID !== ROOT_NODE && currNode.getSiblingAfter() === null) { currNode = currNode.getParent() } return currNode?.getSiblingAfter() }//}}} getLastExpandedNode(node) {//{{{ let currNode = node while (this.getNodeExpanded(currNode.UUID) && currNode.hasChildren()) { currNode = currNode.Children[currNode.Children.length - 1] } return currNode }//}}} async recursiveExpand(node, state) {//{{{ if (state) await this.setNodeExpanded(node, true) for (const child of node.Children) await this.recursiveExpand(child, state) if (!state) await this.setNodeExpanded(node, false) }//}}} async makeVisible(node) {// {{{ const treenode = this.treeNodeComponents[node.UUID] const ancestors = await nodeStore.getNodeAncestry(node) for (const ancestor of ancestors.reverse()) { this.setNodeExpanded(ancestor, true) } // The ROOT_NODE for example hasn't got a treenode. treenode?.element.scrollIntoView({ block: 'nearest' }) }// }}} } export class TreeNodeNative { constructor(tree, node, parent) {//{{{ this.tree = tree this.node = node this.parent = parent this.element = document.createElement('div') this.element.classList.add('node') this.icon_expand = document.createElement('img') this.children_populated = false this.rendered = false this.createElements() _mbus.subscribe(`NODE_CHILDREN_FETCHED_${node.UUID}`, ()=>{ this.render(true) }) _mbus.subscribe(`NODE_EXPAND_${node.UUID}`, state=>{ this.render(true) }) if (this.node.Level === 0 || this.tree.getNodeExpanded(this.node.UUID)) this.fetchChildren() }//}}} createElements() {// {{{ this.element.innerHTML = `
` this.element.children[0].addEventListener('click', ()=>this.tree.setNodeExpanded(this.node, !this.tree.getNodeExpanded(this.node.UUID))) this.element.children[0].appendChild(this.icon_expand) this.element.children[1].addEventListener('click', ()=>_mbus.dispatch('TREE_NODE_SELECTED', this.node)) }// }}} async fetchChildren() {//{{{ await this.node.fetchChildren() this.children_populated = true }//}}} render(force_update) {//{{{ if (this.rendered && force_update !== true) return this.element // Fetch the next level of children if the parent tree node is expanded and our children thus will be visible. const expanded = this.node.Children.length > 0 && this.tree.getNodeExpanded(this.node.UUID) const selected = this.tree.isSelected(this.node) ? 'selected' : '' if (!this.children_populated && this.tree.getNodeExpanded(this.parent?.node.UUID)) { this.node.fetchChildren().then(()=>this.children_populated = true) } // Update the name and selected status this.element.children[1].innerText = this.node.get('Name') this.element.children[1].className = `name ${selected}` // Update expansion state this.element.children[2].className = `children ${expanded ? 'expanded' : 'collapsed'}` // The expand icon is cached to not get a flickering when re-rendering. if (this.icon_expand === null) this.icon_expand = document.createElement('img') if (this.node.Children.length === 0) this.setImgSrc(this.icon_expand, `/images/${window._VERSION}/leaf.svg`) else if (this.tree.getNodeExpanded(this.node.UUID)) this.setImgSrc(this.icon_expand, `/images/${window._VERSION}/expanded.svg`) else this.setImgSrc(this.icon_expand, `/images/${window._VERSION}/collapsed.svg`) // Should children be rendered? this.element.children[2].innerHTML = '' let children = [] if (expanded) children = this.node.Children.map(node => { let treenode = this.tree.treeNodeComponents[node.UUID] if (treenode === undefined) { treenode = new TreeNodeNative(this.tree, node, this) this.tree.treeNodeComponents[node.UUID] = treenode } return treenode //return html`<${TreeNode} key=${`treenode_${node.UUID}`} tree=${tree} node=${node} parent=${this} ref=${tree.treeNodeComponents[node.UUID]} selected=${node.UUID === tree.props.app.startNode?.UUID} />` }) for(const c of children) this.element.children[2].appendChild(c.render()) this.rendered = true return this.element }//}}} setImgSrc(img, newSrc) {// {{{ if (img.getAttribute('src') === newSrc) return img.setAttribute('src', newSrc) }// }}} } // vim: foldmethod=marker