Bumped to v33, implemented fast keyboard navigator
This commit is contained in:
parent
4c59ef4cda
commit
e33ae8a247
4 changed files with 187 additions and 2 deletions
2
main.go
2
main.go
|
|
@ -25,7 +25,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
const VERSION = "v32"
|
const VERSION = "v33"
|
||||||
const CONTEXT_USER = 1
|
const CONTEXT_USER = 1
|
||||||
const SYNC_PAGINATION = 200
|
const SYNC_PAGINATION = 200
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,7 @@ export class App {
|
||||||
classList.add('page-' + page)
|
classList.add('page-' + page)
|
||||||
})
|
})
|
||||||
|
|
||||||
_mbus.subscribe('DEVICE_PREFERENCE_SET_UPDATED', ()=>{
|
_mbus.subscribe('DEVICE_PREFERENCE_SET_UPDATED', () => {
|
||||||
this.preferences = this.getPreferences()
|
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.
|
// 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.
|
// Thus, the exception is acceptable to consequent use of alt+shift.
|
||||||
const CTRL = !event.shiftKey && event.ctrlKey && !event.altKey
|
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_ALT = event.shiftKey && !event.ctrlKey && event.altKey
|
||||||
const SHIFT_CTRL_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()) {
|
switch (event.key.toUpperCase()) {
|
||||||
case 'F2':
|
case 'F2':
|
||||||
this.nodeUI.renameNode()
|
this.nodeUI.renameNode()
|
||||||
|
|
@ -133,6 +143,11 @@ export class App {
|
||||||
this.nodeUI.saveNode()
|
this.nodeUI.saveNode()
|
||||||
break
|
break
|
||||||
|
|
||||||
|
case ' ':
|
||||||
|
if (!CTRL) { handled = false; break }
|
||||||
|
console.log('ctrl+space')
|
||||||
|
break
|
||||||
|
|
||||||
default:
|
default:
|
||||||
handled = false
|
handled = false
|
||||||
}
|
}
|
||||||
|
|
@ -403,8 +418,175 @@ class N2DragIcon extends CustomHTMLElement {
|
||||||
}// }}}
|
}// }}}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class N2NodeNavigator extends CustomHTMLElement {
|
||||||
|
static {
|
||||||
|
this.tmpl = document.createElement('template')
|
||||||
|
this.tmpl.innerHTML = `
|
||||||
|
<style>
|
||||||
|
:host {
|
||||||
|
dialog {
|
||||||
|
position: fixed;
|
||||||
|
top: 32px;
|
||||||
|
left: 32px;
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: auto;
|
||||||
|
margin-left: 0;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-search {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
&.no-match {
|
||||||
|
color: #a00;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.el-path, .el-children {
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-path {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row nowrap;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
.separator {
|
||||||
|
color: var(--color1);
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<dialog data-el="dlg">
|
||||||
|
<input data-field="search" value="">
|
||||||
|
<div data-el="path">Path:</div>
|
||||||
|
<div data-el="children"></div>
|
||||||
|
</dialog>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
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-crumbs', N2Crumbs)
|
||||||
customElements.define('n2-crumb', N2Crumb)
|
customElements.define('n2-crumb', N2Crumb)
|
||||||
|
customElements.define('n2-nodenavigator', N2NodeNavigator)
|
||||||
customElements.define('n2-dragicon', N2DragIcon)
|
customElements.define('n2-dragicon', N2DragIcon)
|
||||||
|
|
||||||
// vim: foldmethod=marker
|
// vim: foldmethod=marker
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ export class CustomHTMLElement extends HTMLElement {
|
||||||
const fieldName = this.toElementName('field', field)
|
const fieldName = this.toElementName('field', field)
|
||||||
this[fieldName] = el
|
this[fieldName] = el
|
||||||
this._fields.set(this.toElementName('', field), el)
|
this._fields.set(this.toElementName('', field), el)
|
||||||
|
el.classList.add('field-' + el.dataset.field)
|
||||||
}
|
}
|
||||||
|
|
||||||
const name = el.dataset.el
|
const name = el.dataset.el
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,8 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<n2-syncprogress></n2-syncprogress>
|
<n2-syncprogress></n2-syncprogress>
|
||||||
|
|
||||||
|
<n2-nodenavigator></n2-nodenavigator>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script type="module">
|
<script type="module">
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue