From 82f09dcb1d9fcd1970b94e60b1837e4d5def3bf1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20=C3=85hall?= Date: Thu, 6 Feb 2025 22:31:11 +0100 Subject: [PATCH] More caching, first tree navigation with up/down. --- static/css/notes2.css | 7 +- static/images/design.svg | 223 +++++++++++++-------------------- static/images/icon_refresh.svg | 49 ++++++++ static/js/node.mjs | 42 +++++++ static/js/node_store.mjs | 8 +- static/js/notes2.mjs | 122 ++++++++++++++++-- static/less/notes2.less | 8 +- static/service_worker.js | 9 +- 8 files changed, 317 insertions(+), 151 deletions(-) create mode 100644 static/images/icon_refresh.svg diff --git a/static/css/notes2.css b/static/css/notes2.css index 4be2968..299b0af 100644 --- a/static/css/notes2.css +++ b/static/css/notes2.css @@ -27,7 +27,7 @@ html { display: grid; position: relative; justify-items: center; - margin-bottom: 32px; + margin-bottom: 8px; margin-left: 24px; margin-right: 24px; } @@ -35,6 +35,11 @@ html { width: 128px; left: -20px; } +#tree .icons { + display: flex; + justify-content: center; + margin-bottom: 32px; +} #tree .node { display: grid; grid-template-columns: 24px min-content; diff --git a/static/images/design.svg b/static/images/design.svg index 6af2931..7c10050 100644 --- a/static/images/design.svg +++ b/static/images/design.svg @@ -53,10 +53,10 @@ inkscape:pageopacity="0.0" inkscape:pageshadow="2" inkscape:zoom="1" - inkscape:cx="647.5" - inkscape:cy="483" + inkscape:cx="733" + inkscape:cy="494" inkscape:document-units="px" - inkscape:current-layer="layer1" + inkscape:current-layer="layer2" showgrid="false" units="px" inkscape:window-width="2190" @@ -98,66 +98,7 @@ inkscape:groupmode="layer" id="layer5" inkscape:label="Toplist #2" - style="display:inline">KBKunderNätverk Gustafsberg + r="0.69898546" /> + + + + + + diff --git a/static/images/icon_refresh.svg b/static/images/icon_refresh.svg new file mode 100644 index 0000000..d46322e --- /dev/null +++ b/static/images/icon_refresh.svg @@ -0,0 +1,49 @@ + + + + + + + + sync + + + diff --git a/static/js/node.mjs b/static/js/node.mjs index e0b3201..0dde132 100644 --- a/static/js/node.mjs +++ b/static/js/node.mjs @@ -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) diff --git a/static/js/node_store.mjs b/static/js/node_store.mjs index 448ca1b..68ffce4 100644 --- a/static/js/node_store.mjs +++ b/static/js/node_store.mjs @@ -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) { diff --git a/static/js/notes2.mjs b/static/js/notes2.mjs index 85378d5..987f2d7 100644 --- a/static/js/notes2.mjs +++ b/static/js/notes2.mjs @@ -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` -
+
+
+ +
${renderedTreeTrunk}
` }//}}} 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 { diff --git a/static/less/notes2.less b/static/less/notes2.less index c39d7af..93b75cd 100644 --- a/static/less/notes2.less +++ b/static/less/notes2.less @@ -50,7 +50,7 @@ html { display: grid; position: relative; justify-items: center; - margin-bottom: 32px; + margin-bottom: 8px; margin-left: 24px; margin-right: 24px; img { @@ -60,6 +60,12 @@ html { } } + .icons { + display: flex; + justify-content: center; + margin-bottom: 32px; + } + .node { display: grid; grid-template-columns: 24px min-content; diff --git a/static/service_worker.js b/static/service_worker.js index e10e3b0..6c77241 100644 --- a/static/service_worker.js +++ b/static/service_worker.js @@ -7,19 +7,26 @@ const CACHED_ASSETS = [ '/css/{{ .VERSION }}/notes2.css', '/js/{{ .VERSION }}/lib/preact/preact.mjs', - '/js/{{ .VERSION }}/lib/htm/htm.mjs', '/js/{{ .VERSION }}/lib/preact/devtools.mjs', '/js/{{ .VERSION }}/lib/signals/signals.mjs', '/js/{{ .VERSION }}/lib/signals/signals-core.mjs', '/js/{{ .VERSION }}/lib/preact/hooks.mjs', + '/js/{{ .VERSION }}/lib/preact/debug.mjs', + '/js/{{ .VERSION }}/lib/htm/htm.mjs', + '/js/{{ .VERSION }}/lib/fullcalendar.min.js', + '/js/{{ .VERSION }}/lib/node_modules/marked/marked.min.js', + '/js/{{ .VERSION }}/lib/sjcl.js', '/js/{{ .VERSION }}/api.mjs', + '/js/{{ .VERSION }}/node.mjs', '/js/{{ .VERSION }}/node_store.mjs', '/js/{{ .VERSION }}/notes2.mjs', + '/js/{{ .VERSION }}/sync.mjs', '/js/{{ .VERSION }}/key.mjs', '/js/{{ .VERSION }}/crypto.mjs', '/js/{{ .VERSION }}/checklist.mjs', + '/images/{{ .VERSION }}/logo.svg', '/images/{{ .VERSION }}/leaf.svg', '/images/{{ .VERSION }}/collapsed.svg', '/images/{{ .VERSION }}/expanded.svg',