diff --git a/main.go b/main.go
index 4724d7a..0768456 100644
--- a/main.go
+++ b/main.go
@@ -25,7 +25,7 @@ import (
"time"
)
-const VERSION = "v32"
+const VERSION = "v33"
const CONTEXT_USER = 1
const SYNC_PAGINATION = 200
diff --git a/static/js/app.mjs b/static/js/app.mjs
index 2fc713b..9b68c79 100644
--- a/static/js/app.mjs
+++ b/static/js/app.mjs
@@ -63,7 +63,7 @@ export class App {
classList.add('page-' + page)
})
- _mbus.subscribe('DEVICE_PREFERENCE_SET_UPDATED', ()=>{
+ _mbus.subscribe('DEVICE_PREFERENCE_SET_UPDATED', () => {
this.preferences = this.getPreferences()
})
@@ -93,9 +93,19 @@ export class App {
// Ctrl+S is the exception to using Alt+Shift, since it is overridable and in such widespread use for saving.
// Thus, the exception is acceptable to consequent use of alt+shift.
const CTRL = !event.shiftKey && event.ctrlKey && !event.altKey
+ const SHIFT_CTRL = event.shiftKey && event.ctrlKey && !event.altKey
const SHIFT_ALT = event.shiftKey && !event.ctrlKey && event.altKey
const SHIFT_CTRL_ALT = event.shiftKey && event.ctrlKey && event.altKey
+ // Space is better handled with event.code
+ if (event.code.toUpperCase() == 'SPACE' && SHIFT_CTRL) {
+ event.preventDefault()
+ event.stopPropagation()
+ document.querySelector('n2-nodenavigator').open()
+ return
+ }
+
+
switch (event.key.toUpperCase()) {
case 'F2':
this.nodeUI.renameNode()
@@ -133,6 +143,11 @@ export class App {
this.nodeUI.saveNode()
break
+ case ' ':
+ if (!CTRL) { handled = false; break }
+ console.log('ctrl+space')
+ break
+
default:
handled = false
}
@@ -403,8 +418,175 @@ class N2DragIcon extends CustomHTMLElement {
}// }}}
}
+class N2NodeNavigator extends CustomHTMLElement {
+ static {
+ this.tmpl = document.createElement('template')
+ this.tmpl.innerHTML = `
+
+
+ `
+ }
+ constructor() {// {{{
+ super(true)
+ this.rootNode = new Node({ UUID: ROOT_NODE, Name: 'Start', Special: true }, -1)
+ this.currNode = this.rootNode
+
+ this.fieldSearch.addEventListener('input', () => {
+ this.render()
+ })
+
+ this.fieldSearch.addEventListener('keydown', event => {
+ if (event.key == 'Enter' && this.currNode !== null && this.lastSearchTermResultedInNode) {
+ globalThis._app.goToNode(this.currNode.UUID, false, false)
+ this.close()
+ }
+ })
+ }// }}}
+ async open() {// {{{
+ if (this.elDlg.open)
+ return
+
+ this.currNode = null
+ this.fieldSearch.value = ''
+
+ this.render()
+ this.elDlg.showModal()
+ }// }}}
+ close() {// {{{
+ this.elDlg.close()
+ }// }}}
+ async render() {// {{{
+ let currNode = this.rootNode
+ this.lastSearchTermResultedInNode = false
+ const searchPath = this.getSearchPath()
+ const path = []
+
+ searchLoop:
+ for (const search of searchPath) {
+ // string.split returns an empty string if last character is a space.
+ // We don't want to make a difference if that's the case.
+ if (search === '')
+ continue
+
+ // fetched nodes hasn't fetched children automatically.
+ if (!currNode.hasFetchedChildren())
+ await currNode.fetchChildren()
+
+ // Normalized names of current node's children are matched against
+ // the current search term and compiled into a list.
+ // The algorithm stops when there are none or more than one.
+ const matches = []
+ for (const childNode of currNode.Children) {
+ const childName = this.normalizeName(childNode.get('Name'))
+ if (childName.match(search))
+ matches.push(childNode)
+ }
+
+ switch (matches.length) {
+ case 0:
+ this.fieldSearch.classList.add('no-match')
+ this.lastSearchTermResultedInNode = false
+ break searchLoop
+
+ case 1:
+ this.fieldSearch.classList.remove('no-match')
+ currNode = matches[0]
+ const separator = document.createElement('div')
+ separator.classList.add('separator')
+ separator.innerHTML = `>`
+
+ const pathEl = document.createElement('div')
+ pathEl.innerText = currNode.get('Name')
+ path.push(separator, pathEl)
+
+ this.lastSearchTermResultedInNode = true
+ continue
+
+ default:
+ // More than one match.
+ this.lastSearchTermResultedInNode = false
+ this.fieldSearch.classList.remove('no-match')
+ break searchLoop
+ }
+ }
+
+ // The currNode can be the last of the search and not having had it children fetched previously.
+ if (!currNode.hasFetchedChildren())
+ await currNode.fetchChildren()
+
+ this.elPath.replaceChildren(...path)
+
+ // Children are rendered for the current node to be found by the search path.
+ const search = this.lastSearchTermResultedInNode ? '' : searchPath.slice(-1)
+
+ const childNodes = []
+ for (const childNode of currNode.Children) {
+ const childName = this.normalizeName(childNode.get('Name'))
+ if (!childName.match(search))
+ continue
+
+ const childEl = document.createElement('div')
+ childEl.classList.add('child-node')
+ childEl.innerText = `${this.normalizeName(childNode.get('Name'))}`
+ childNodes.push(childEl)
+ }
+
+ this.currNode = currNode
+ globalThis.navNode = currNode.get('Name') + `, ${this.lastSearchTermResultedInNode}`
+
+ this.elChildren.replaceChildren(...childNodes)
+ }// }}}
+ normalizeName(name) {// {{{
+ return name.toLowerCase().replaceAll(/[^-_#()a-zA-Z0-9]/g, '_')
+ }// }}}
+ getSearchPath() {// {{{
+ return this.fieldSearch.value.toLowerCase().split(/\s+/)
+ }// }}}
+}
+
customElements.define('n2-crumbs', N2Crumbs)
customElements.define('n2-crumb', N2Crumb)
+customElements.define('n2-nodenavigator', N2NodeNavigator)
customElements.define('n2-dragicon', N2DragIcon)
// vim: foldmethod=marker
diff --git a/static/js/lib/custom_html_element.mjs b/static/js/lib/custom_html_element.mjs
index d1fb7ae..3cb3de3 100644
--- a/static/js/lib/custom_html_element.mjs
+++ b/static/js/lib/custom_html_element.mjs
@@ -20,6 +20,7 @@ export class CustomHTMLElement extends HTMLElement {
const fieldName = this.toElementName('field', field)
this[fieldName] = el
this._fields.set(this.toElementName('', field), el)
+ el.classList.add('field-' + el.dataset.field)
}
const name = el.dataset.el
diff --git a/views/pages/notes2.gotmpl b/views/pages/notes2.gotmpl
index c5df63e..65886c4 100644
--- a/views/pages/notes2.gotmpl
+++ b/views/pages/notes2.gotmpl
@@ -39,6 +39,8 @@
+
+