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 = ` + + + +
Path:
+
+
+ ` + } + 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 @@ + +