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 @@
+
+
+
+
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`
-
+
_notes2.current.goToNode(ROOT_NODE)}>

+
+

+
${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',