import { ROOT_NODE } from 'node_store' import { CustomHTMLElement } from './lib/custom_html_element.mjs' import { Color, Solver } from './lib/css_colorize.mjs' // TreeExpandedHandler is responsible for collapsing or expanding // the node tree, wide view or narrow "mobile" view. class TreeExpansionHandler {// {{{ constructor() { this.isNarrow = false this.initializeMediaHandler() this.initializeBusEvents() } initializeBusEvents() { _mbus.subscribe('TREE_EXPANSION', ({ detail }) => { // When a node is selected on the screen and the screen // is narrow the tree is automatically hidden. // // Can't always hide the tree automatically when a node // is selected since the wide mode shows the tree as standard. if (detail.data?.when == 'narrow' && !this.isNarrow) return this.treeExpansion(detail.data?.expand) }) } initializeMediaHandler() { const query = window.matchMedia('(max-width: 800px)') query.addEventListener('change', event => this.screenNarrowHandler(event)) // Run once to set initial state, instead of needing to toggle state. this.screenNarrowHandler(query) } // When screen becomes narrow, the tree is automatically hidden. // Primary purpose is to read content, not browse, which is why // the tree is hidden as standard. screenNarrowHandler(event) { this.isNarrow = event.matches if (this.isNarrow) this.treeExpansion(false) else this.treeExpansion(true) } treeExpansion(expanded) { const notes2 = document.getElementById('notes2') if (expanded) { notes2.classList.remove('hide-tree') notes2.classList.add('show-tree') } else { notes2.classList.add('hide-tree') notes2.classList.remove('show-tree') } } }// }}} export class N2Sidebar extends CustomHTMLElement { static {// {{{ this.tmpl = document.createElement('template') this.tmpl.innerHTML = `
` }// }}} constructor() {// {{{ super() this.id = 'tree-nodes' this.tabIndex = 0 this.treeNodeComponents = {} this.treeTrunk = [] this.expandedNodes = {} // keyed on UUID this.selectedNode = null this.rendered = false new TreeExpansionHandler() this.addEventListener('keydown', event => this.keyHandler(event)) this.elSearch.addEventListener('click', () => _mbus.dispatch('op-search')) this.elSync.addEventListener('click', () => _sync.run()) this.elLogo.addEventListener('click', () => _app.goToNode(ROOT_NODE, false, false)) this.elHideTree.addEventListener('click', event => { event.stopPropagation() _mbus.dispatch('TREE_EXPANSION', { expand: false }) }) _mbus.subscribe('NODE_MODIFIED', ({ detail }) => { const node = detail.data.node const treenode = this.treeNodeComponents[node.get('UUID')] if (!treenode) return treenode.node = node treenode.render(true) }) /* XXX - set color */ let color = new Color(0x80, 0x00, 0x33) let solver = new Solver(color) let result = solver.solve() console.log(result.filter) }// }}} async render() {// {{{ if (this.rendered) alert('Tree should only be rendered once.') this.expandedNodes[ROOT_NODE] = true const startnode = await nodeStore.get(ROOT_NODE) const starttreenode = new N2TreeNode(this, startnode, null) this.treeNodeComponents[startnode.UUID] = starttreenode this.elTreenodes.appendChild(await starttreenode.render()) // Notify the application that the initial tree is rendered (with children) // and that initial node selection can take place. App will check URL to // select the correct one. _mbus.dispatch('TREE_RENDERED') this.rendered = true return this }// }}} reset() {// {{{ this.treeNodeComponents = {} this.treeTrunk = [] this.rendered = false this.elTreenodes.replaceChildren() this.populateFirstLevel() }// }}} 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': if (n.UUID === ROOT_NODE) return 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() && n.UUID !== ROOT_NODE) { this.setNodeExpanded(n, false) return } if (n.isFirstSibling()) { _mbus.dispatch("GO_TO_NODE", { nodeUUID: n.getParent()?.UUID, dontPush: false, 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: false, dontExpand: true }) return } _mbus.dispatch("GO_TO_NODE", { nodeUUID: n.getSiblingBefore()?.UUID, dontPush: false, 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: false, dontExpand: true }) return } if (n.isLastSibling()) { const nextNode = this.getParentWithNextSibling(n) _mbus.dispatch("GO_TO_NODE", { nodeUUID: nextNode?.UUID, dontPush: false, dontExpand: true }) return } _mbus.dispatch("GO_TO_NODE", { nodeUUID: n.getSiblingAfter()?.UUID, dontPush: false, 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() _mbus.dispatch("GO_TO_NODE", { nodeUUID: parent?.UUID, dontPush: false, dontExpand: true }) return } if (siblingBefore !== null && siblingExpanded && siblingBefore.hasChildren()) { _mbus.dispatch("GO_TO_NODE", { nodeUUID: siblingBefore.Children[siblingBefore.Children.length - 1]?.UUID, dontPush: false, dontExpand: true }) return } if (siblingBefore) { _mbus.dispatch("GO_TO_NODE", { nodeUUID: siblingBefore.UUID, dontPush: false, 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: false, dontExpand: true }) return } if (nodeExpanded && n.isLastSibling() && !n.hasChildren()) { const wantedNode = this.getParentWithNextSibling(n) _mbus.dispatch("GO_TO_NODE", { nodeUUID: wantedNode?.UUID, dontPush: false, 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: false, 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: false, 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: false, 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: false, dontExpand: true }) } else _mbus.dispatch("GO_TO_NODE", { nodeUUID: root.Children[root.Children.length - 1]?.UUID, dontPush: false, 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, providedAncestors, dontExpand) {// {{{ const treenode = this.treeNodeComponents[node.UUID] if (!dontExpand) { const ancestors = providedAncestors || await nodeStore.getNodeAncestry(node) for (const ancestor of ancestors.reverse()) { this.setNodeExpanded(ancestor, true) } } treenode?.scrollIntoView({ block: 'nearest' }) }// }}} } customElements.define('n2-sidebar', N2Sidebar) export class N2TreeNode extends CustomHTMLElement { static {// {{{ this.tmpl = document.createElement('template') this.tmpl.innerHTML = `
` }// }}} constructor(sidebar, node, parent) {//{{{ super() this.classList.add('node') this.sidebar = sidebar this.node = node this.parent = parent this.children_populated = false this.rendered = false this.elExpandToggle.addEventListener('click', () => this.sidebar.setNodeExpanded(this.node, !this.sidebar.getNodeExpanded(this.node.UUID))) this.elName.addEventListener('click', () => _mbus.dispatch('TREE_NODE_SELECTED', this.node)) _mbus.subscribe(`NODE_EXPAND_${node.UUID}`, _state => { this.render(true) }) }// }}} async fetchChildren(force_fetch) {//{{{ if (this.children_populated && !force_fetch) return await this.node.fetchChildren() this.children_populated = true }//}}} async render(force_update, force_refetch_children) {//{{{ if (this.rendered && force_update !== true) return this if (this.sidebar.getNodeExpanded(this.node.UUID)) await this.fetchChildren() // Update the name and selected status. this.elName.querySelector('span').innerText = this.node.get('Name') if (this.sidebar.isSelected(this.node)) this.elName.classList.add('selected') else this.elName.classList.remove('selected') // Update expansion state const expanded = this.node.hasChildren() && this.sidebar.getNodeExpanded(this.node.UUID) if (expanded) { this.elChildren.classList.add('expanded') this.elChildren.classList.remove('collapsed') } else { this.elChildren.classList.remove('expanded') this.elChildren.classList.add('collapsed') } // The expand icon is only changed to not get a flickering when re-rendering. if (this.node.UUID === ROOT_NODE) this.setImgSrc(this.elExpand, `/images/${window._VERSION}/icon_home.svg`) else if (!this.node.hasChildren()) this.setImgSrc(this.elExpand, `/images/${window._VERSION}/leaf.svg`) else if (this.sidebar.getNodeExpanded(this.node.UUID)) this.setImgSrc(this.elExpand, `/images/${window._VERSION}/expanded.svg`) else this.setImgSrc(this.elExpand, `/images/${window._VERSION}/collapsed.svg`) // Should children be rendered? let children = [] if (expanded) children = this.node.Children.map(node => { let treenode = this.sidebar.treeNodeComponents[node.UUID] if (treenode === undefined) { treenode = new N2TreeNode(this.sidebar, node, this) this.sidebar.treeNodeComponents[node.UUID] = treenode } return treenode }) const renderedChildren = [] for (const c of children) renderedChildren.push(await c.render()) this.elChildren.replaceChildren(...renderedChildren) this.rendered = true return this }//}}} setImgSrc(img, newSrc) {// {{{ if (img.getAttribute('src') === newSrc) return img.setAttribute('src', newSrc) }// }}} } customElements.define('n2-treenode', N2TreeNode) // vim: foldmethod=marker