More caching, first tree navigation with up/down.

This commit is contained in:
Magnus Åhall 2025-02-06 22:31:11 +01:00
parent 0bd5d08edf
commit 82f09dcb1d
8 changed files with 317 additions and 151 deletions

View file

@ -209,6 +209,9 @@ export class NodeUI extends Component {
return
switch (evt.key.toUpperCase()) {
case 'T':
_notes2.current.tree.treeDiv.current?.focus()
break
/*
case 'C':
this.showPage('node')
@ -342,6 +345,10 @@ export class Node {
this._content = this.data.Content
this._modified = false
this._sibling_before = null
this._sibling_after = null
this._parent = null
/*
this.RenderMarkdown = signal(nodeData.RenderMarkdown)
this.Markdown = false
@ -371,10 +378,45 @@ export class Node {
async fetchChildren() {//{{{
if (this._children_fetched)
return this.Children
this.Children = await nodeStore.getTreeNodes(this.UUID, this.Level + 1)
this._children_fetched = true
// Children are sorted to allow for storing siblings befare and after.
// These are used with keyboard navigation in the tree.
this.Children.sort(Node.sort)
const numChildren = this.Children.length
for (let i = 0; i < numChildren; i++) {
if (i > 0)
this.Children[i]._sibling_before = this.Children[i - 1]
if (i < numChildren - 1)
this.Children[i]._sibling_after = this.Children[i + 1]
this.Children[i]._parent = this
}
return this.Children
}//}}}
hasChildren() {//{{{
return this.Children.length > 0
}//}}}
getSiblingBefore() {// {{{
return this._sibling_before
}// }}}
getSiblingAfter() {// {{{
return this._sibling_after
}// }}}
getParent() {//{{{
return this._parent
}//}}}
isLastSibling() {//{{{
return this._sibling_after === null
}//}}}
isFirstSibling() {//{{{
return this._sibling_before === null
}//}}}
content() {//{{{
/* TODO - implement crypto
if (this.CryptoKeyID != 0 && !this._decrypted)

View file

@ -222,10 +222,16 @@ export class NodeStore {
}//}}}
async getTreeNodes(parent, newLevel) {//{{{
return new Promise((resolve, reject) => {
// Parent of toplevel nodes is '' in indexedDB,
// but can also be set to the ROOT_NODE uuid.
let storeParent = parent
if (parent === ROOT_NODE)
storeParent = ''
const trx = this.db.transaction('nodes', 'readonly')
const nodeStore = trx.objectStore('nodes')
const index = nodeStore.index('byParent')
const req = index.getAll(parent)
const req = index.getAll(storeParent)
req.onsuccess = (event) => {
const nodes = []
for (const i in event.target.result) {

View file

@ -47,7 +47,10 @@ export class Notes2 extends Component {
this.setState({ startNode: node })
})
}//}}}
async goToNode(nodeUUID, dontPush) {//{{{
async goToNode(nodeUUID, dontPush, dontExpand) {//{{{
if (nodeUUID === null || nodeUUID === undefined)
return
// Don't switch notes until saved.
if (this.nodeUI.current.nodeModified.value) {
if (!confirm("Changes not saved. Do you want to discard changes?"))
@ -63,7 +66,7 @@ export class Notes2 extends Component {
const ancestors = await nodeStore.getNodeAncestry(node)
this.nodeUI.current.setNode(node)
this.nodeUI.current.setCrumbs(ancestors)
this.tree.setSelected(node)
this.tree.setSelected(node, dontExpand)
}//}}}
logout() {//{{{
localStorage.removeItem('session.UUID')
@ -79,6 +82,7 @@ class Tree extends Component {
this.treeTrunk = []
this.selectedNode = null
this.expandedNodes = {} // keyed on UUID
this.treeDiv = createRef()
this.props.app.tree = this
@ -90,12 +94,17 @@ class Tree extends Component {
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">
<div id="tree" ref=${this.treeDiv} tabindex="0">
<div id="logo" onclick=${() => _notes2.current.goToNode(ROOT_NODE)}><img src="/images/${_VERSION}/logo.svg" /></div>
<div class="icons">
<img src="/images/${_VERSION}/icon_refresh.svg" />
</div>
${renderedTreeTrunk}
</div>`
}//}}}
componentDidMount() {//{{{
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
if (node === null)
@ -105,14 +114,14 @@ class Tree extends Component {
}//}}}
populateFirstLevel(callback = null) {//{{{
nodeStore.getTreeNodes('', 0)
.then(async res => {
res.sort(Node.sort)
nodeStore.get(ROOT_NODE)
.then(node => node.fetchChildren())
.then(children => {
this.treeNodeComponents = {}
this.treeTrunk = []
for (const node of res) {
for (const node of children) {
// The root node isn't supposed to be shown in the tree.
if (node.UUID === '00000000-0000-0000-0000-000000000000')
if (node.UUID === ROOT_NODE)
continue
if (node.ParentUUID === '')
this.treeTrunk.push(node)
@ -124,7 +133,7 @@ class Tree extends Component {
})
.catch(e => { console.log(e); console.log(e.type, e.error); alert(e.error) })
}//}}}
setSelected(node) {//{{{
setSelected(node, dontExpand) {//{{{
// The previously selected node, if any, needs to be rerendered
// to not retain its 'selected' class.
const prevUUID = this.selectedNode?.UUID
@ -135,8 +144,8 @@ class Tree extends Component {
// And now the newly selected node is rerendered.
this.treeNodeComponents[node.UUID]?.current.forceUpdate()
// Expanding selected nodes... I don't know...
this.setNodeExpanded(node.UUID, true)
if (!dontExpand)
this.setNodeExpanded(node.UUID, true)
}//}}}
isSelected(node) {//{{{
return this.selectedNode?.UUID === node.UUID
@ -154,7 +163,7 @@ class Tree extends Component {
return
// Start the chain of by expanding the top node.
this.setNodeExpanded(ancestry[ancestry.length-1].UUID, true)
this.setNodeExpanded(ancestry[ancestry.length - 1].UUID, true)
}//}}}
getNodeExpanded(UUID) {//{{{
if (this.expandedNodes[UUID] === undefined)
@ -166,6 +175,95 @@ class Tree extends Component {
this.getNodeExpanded(UUID)
this.expandedNodes[UUID].value = value
}//}}}
getParentNodeWithNextSibling(node) {//{{{
let currNode = node
while (currNode !== null && currNode.UUID !== ROOT_NODE && currNode.getSiblingAfter() === null) {
currNode = currNode.getParent()
}
return currNode?.getSiblingAfter()
}//}}}
async keyHandler(event) {//{{{
let handled = true
let nodeExpanded = false
let siblingBefore = null
let siblingExpanded = false
let parent = null
const n = this.selectedNode
switch (event.key) {
case 'j':
case 'ArrowDown':
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.getParentNodeWithNextSibling(n)
if (wantedNode?.UUID === ROOT_NODE)
break
await _notes2.current.goToNode(wantedNode?.UUID, true, true)
break
}
if (nodeExpanded && n.isLastSibling() && !n.hasChildren()) {
const wantedNode = this.getParentNodeWithNextSibling(n)
await _notes2.current.goToNode(wantedNode?.UUID, true, true)
break
}
// 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)
break
}
// 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)
break
}
break
case 'k':
case 'ArrowUp':
siblingBefore = n.getSiblingBefore()
if (siblingBefore !== null)
siblingExpanded = this.getNodeExpanded(siblingBefore.UUID)
if (n.isFirstSibling()) {
parent = n.getParent()
if (parent?.UUID === ROOT_NODE)
break
await _notes2.current.goToNode(parent?.UUID, true, true)
break
}
if (siblingBefore !== null && siblingExpanded && siblingBefore.hasChildren()) {
await _notes2.current.goToNode(siblingBefore.Children[siblingBefore.Children.length - 1]?.UUID, true, true)
break
}
if (siblingBefore) {
await _notes2.current.goToNode(siblingBefore.UUID, true, true)
break
}
break
case 'h':
case 'ArrowLeft':
break
default:
handled = false
}
if (handled) {
event.preventDefault()
event.stopPropagation()
}
}//}}}
}
class TreeNode extends Component {