'
+ const treeEl = tmpl.content.getElementById('tree-nodes')
+ treeEl.addEventListener('keydown', event=>this.keyHandler(event))
+
+ for (const node of this.treeTrunk) {
+ const treenode = new TreeNodeNative(this, node)
+ this.treeNodeComponents[node.UUID] = treenode
+ treeEl.appendChild(treenode.render())
+
+ //return html`<${TreeNode} key=${`treenode_${node.UUID}`} tree=${this} node=${node} ref=${this.treeNodeComponents[node.UUID]} selected=${node.UUID === app.state.startNode?.UUID} />`
+ }
+
+ 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) {//{{{
+ // Creating a default value if it doesn't exist already.
+ this.getNodeExpanded(node.UUID)
+ this.expandedNodes[node.UUID] = value
+ _mbus.dispatch(`NODE_EXPAND_${node.UUID}`, value)
+ }//}}}
+ setSelected(node, dontExpand) {//{{{
+ // 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)
+ return
+
+ const expanded = this.getNodeExpanded(n.UUID)
+ if (expanded && n.hasChildren()) {
+ this.setNodeExpanded(n, false)
+ return
+ }
+
+ if (n.isFirstSibling() && n.getParent().UUID !== ROOT_NODE) {
+ await _notes2.current.goToNode(n.getParent()?.UUID, true, true)
+ return
+ }
+
+ const siblingBefore = n.getSiblingBefore()
+ const siblingExpanded = this.getNodeExpanded(siblingBefore?.UUID)
+ if (siblingBefore !== null && siblingExpanded && siblingBefore.hasChildren()) {
+ const siblingAbove = this.getLastExpandedNode(siblingBefore)
+ await _notes2.current.goToNode(siblingAbove?.UUID, true, true)
+ return
+ }
+
+ await _notes2.current.goToNode(n.getSiblingBefore()?.UUID, true, true)
+ }//}}}
+ async navigateRight(n) {//{{{
+ if (n === null)
+ return
+
+ const siblingAfter = n.getSiblingAfter()
+ const expanded = this.getNodeExpanded(n.UUID)
+
+ if (!expanded && n.hasChildren()) {
+ this.setNodeExpanded(n, true)
+ return
+ }
+
+ if (expanded && n.hasChildren()) {
+ await _notes2.current.goToNode(n.Children[0]?.UUID, true, true)
+ return
+ }
+
+ if (n.isLastSibling()) {
+ const nextNode = this.getParentWithNextSibling(n)
+ await _notes2.current.goToNode(nextNode?.UUID, true, true)
+ return
+ }
+
+ await _notes2.current.goToNode(n.getSiblingAfter()?.UUID, true, true)
+ }//}}}
+ async navigateUp(n) {//{{{
+ if (n === null)
+ 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
+ await _notes2.current.goToNode(parent?.UUID, true, true)
+ return
+ }
+
+ if (siblingBefore !== null && siblingExpanded && siblingBefore.hasChildren()) {
+ await _notes2.current.goToNode(siblingBefore.Children[siblingBefore.Children.length - 1]?.UUID, true, true)
+ return
+ }
+
+ if (siblingBefore) {
+ await _notes2.current.goToNode(siblingBefore.UUID, true, true)
+ return
+ }
+ }//}}}
+ async navigateDown(n) {//{{{
+ if (n === null)
+ 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)
+ await _notes2.current.goToNode(wantedNode?.UUID, true, true)
+ return
+ }
+
+ if (nodeExpanded && n.isLastSibling() && !n.hasChildren()) {
+ const wantedNode = this.getParentWithNextSibling(n)
+ await _notes2.current.goToNode(wantedNode?.UUID, true, true)
+ return
+ }
+
+ // Node not expanded. Go to this node's next sibling.
+ // GoToNode will abort if given null.
+ if (!nodeExpanded || !n.hasChildren()) {
+ await _notes2.current.goToNode(n.getSiblingAfter()?.UUID, true, true)
+ return
+ }
+
+ // Node is expanded.
+ // Children will be visually beneath this node, if any.
+ if (nodeExpanded && n.hasChildren()) {
+ await _notes2.current.goToNode(n.Children[0].UUID, true, true)
+ return
+ }
+ }//}}}
+ async navigateTop() {//{{{
+ const root = await nodeStore.get(ROOT_NODE)
+ if (root.Children.length === 0)
+ return
+ await _notes2.current.goToNode(root.Children[0]?.UUID, true, 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)
+ await _notes2.current.goToNode(lastnode?.UUID, true, true)
+ } else
+ await _notes2.current.goToNode(root.Children[root.Children.length - 1]?.UUID, true, 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)
+ }//}}}
+}
+
+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.createElements()
+
+ _mbus.subscribe(`NODE_CHILDREN_FETCHED_${node.UUID}`, ()=>{
+ this.render(true)
+ })
+
+ _mbus.subscribe(`NODE_EXPAND_${node.UUID}`, state=>{
+ this.render(true)
+ })
+
+ this.rendered = false
+ }//}}}
+ 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', ()=>window._notes2.current.goToNode(this.node.UUID))
+ }// }}}
+ 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
diff --git a/static/less/notes2.less b/static/less/notes2.less
index a1ae783..a5778ed 100644
--- a/static/less/notes2.less
+++ b/static/less/notes2.less
@@ -46,6 +46,12 @@ html {
}
}
+/*
+#tree-native {
+ grid-area: tree;
+}
+*/
+
#tree {
grid-area: tree;
padding: 16px 32px;
diff --git a/views/layouts/main.gotmpl b/views/layouts/main.gotmpl
index 123fc68..470cfe5 100644
--- a/views/layouts/main.gotmpl
+++ b/views/layouts/main.gotmpl
@@ -4,16 +4,6 @@
-
-
+
+
diff --git a/views/pages/notes2.gotmpl b/views/pages/notes2.gotmpl
index f633692..4a828cd 100644
--- a/views/pages/notes2.gotmpl
+++ b/views/pages/notes2.gotmpl
@@ -1,4 +1,10 @@
{{ define "page" }}
+
+
+