Tree expansion and keyboard navigation works
This commit is contained in:
parent
23307d7967
commit
dd27be67b9
7 changed files with 433 additions and 13 deletions
|
|
@ -17,6 +17,11 @@ html {
|
|||
display: none;
|
||||
}
|
||||
}
|
||||
/*
|
||||
#tree-native {
|
||||
grid-area: tree;
|
||||
}
|
||||
*/
|
||||
#tree {
|
||||
grid-area: tree;
|
||||
padding: 16px 32px;
|
||||
|
|
|
|||
|
|
@ -408,7 +408,8 @@ export class Node {
|
|||
}
|
||||
|
||||
// Notify the tree that all children are fetched and ready to process.
|
||||
_notes2.current.tree.fetchChildrenOn(this.UUID)
|
||||
//_notes2.current.tree.fetchChildrenOn(this.UUID)
|
||||
_mbus.dispatch(`NODE_CHILDREN_FETCHED_${this.UUID}`)
|
||||
|
||||
return this.Children
|
||||
}//}}}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { signal } from 'preact/signals'
|
|||
import htm from 'htm'
|
||||
import { Node, NodeUI } from 'node'
|
||||
import { ROOT_NODE } from 'node_store'
|
||||
import { TreeNative, TreeNodeNative } from 'tree'
|
||||
const html = htm.bind(h)
|
||||
|
||||
export class Notes2 extends Component {
|
||||
|
|
@ -14,6 +15,7 @@ export class Notes2 extends Component {
|
|||
startNode: null,
|
||||
}
|
||||
this.op = signal('')
|
||||
this.treeNative = new TreeNative()
|
||||
|
||||
window._sync = new Sync()
|
||||
window._sync.run()
|
||||
|
|
@ -76,6 +78,7 @@ export class Notes2 extends Component {
|
|||
this.nodeUI.current.setNode(node)
|
||||
this.nodeUI.current.setCrumbs(ancestors)
|
||||
this.tree.setSelected(node, dontExpand)
|
||||
this.treeNative.setSelected(node, dontExpand)
|
||||
}//}}}
|
||||
logout() {//{{{
|
||||
localStorage.removeItem('session.UUID')
|
||||
|
|
@ -107,6 +110,7 @@ class Tree extends Component {
|
|||
this.treeNodeComponents[node.UUID] = createRef()
|
||||
return html`<${TreeNode} key=${`treenode_${node.UUID}`} tree=${this} node=${node} ref=${this.treeNodeComponents[node.UUID]} selected=${node.UUID === app.state.startNode?.UUID} />`
|
||||
})
|
||||
|
||||
return html`
|
||||
<div id="tree" ref=${this.treeDiv} tabindex="0">
|
||||
<div id="logo" onclick=${() => _notes2.current.goToNode(ROOT_NODE)}><img src="/images/${_VERSION}/logo.svg" /></div>
|
||||
|
|
@ -118,7 +122,7 @@ class Tree extends Component {
|
|||
</div>`
|
||||
}//}}}
|
||||
componentDidMount() {//{{{
|
||||
this.treeDiv.current.addEventListener('keydown', event => this.keyHandler(event))
|
||||
//this.treeDiv.current.addEventListener('keydown', event => this.keyHandler(event))
|
||||
|
||||
// This will show and select the treenode that is selected in the node UI.
|
||||
const node = _notes2.current?.nodeUI.current?.node.value
|
||||
|
|
|
|||
397
static/js/tree.mjs
Normal file
397
static/js/tree.mjs
Normal file
|
|
@ -0,0 +1,397 @@
|
|||
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
|
||||
|
||||
window._mbus.subscribe('TREE_TRUNK_FETCHED', ()=>{
|
||||
document.getElementById('tree').append(this.render())
|
||||
})
|
||||
|
||||
this.populateFirstLevel()
|
||||
}// }}}
|
||||
render() {// {{{
|
||||
if (this.rendered)
|
||||
alert('Tree should only be rendered once.')
|
||||
|
||||
const tmpl = document.createElement('template')
|
||||
tmpl.innerHTML = '<div id="tree-nodes" tabindex=0><div>'
|
||||
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 = `
|
||||
<div class="expand-toggle"></div>
|
||||
<div class="name" ></div>
|
||||
<div class="children"></div>
|
||||
`
|
||||
|
||||
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 <img> 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
|
||||
|
|
@ -46,6 +46,12 @@ html {
|
|||
}
|
||||
}
|
||||
|
||||
/*
|
||||
#tree-native {
|
||||
grid-area: tree;
|
||||
}
|
||||
*/
|
||||
|
||||
#tree {
|
||||
grid-area: tree;
|
||||
padding: 16px 32px;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue