Tree render and navigation with note rendering
This commit is contained in:
parent
dd27be67b9
commit
1ce8e29e37
7 changed files with 515 additions and 62 deletions
298
static/js/app.mjs
Normal file
298
static/js/app.mjs
Normal file
|
|
@ -0,0 +1,298 @@
|
|||
import { ROOT_NODE } from 'node_store'
|
||||
import { TreeNative } from 'tree'
|
||||
import { NodeUINative } from 'node'
|
||||
|
||||
export class App {
|
||||
constructor() {// {{{
|
||||
this.currentNode = null
|
||||
this.treeNative = new TreeNative()
|
||||
this.crumbs = new Crumbs()
|
||||
this.crumbsElement = document.getElementById('crumbs')
|
||||
this.nodeUI = new NodeUINative(document.getElementById('note'))
|
||||
|
||||
_mbus.subscribe('TREE_TRUNK_FETCHED', async () => {
|
||||
document.getElementById('tree').append(this.treeNative.render())
|
||||
document.getElementById('tree-nodes')?.focus()
|
||||
|
||||
const startNode = await this.getStartNode()
|
||||
this.goToNode(startNode.UUID, false, false)
|
||||
})
|
||||
|
||||
_mbus.subscribe('TREE_NODE_SELECTED', event => {
|
||||
const node = event.detail.data
|
||||
this.goToNode(node.UUID, false, false)
|
||||
})
|
||||
|
||||
_mbus.subscribe('GO_TO_NODE', event => {
|
||||
const node = event.detail.data
|
||||
this.goToNode(node.nodeUUID, node.dontPush, node.dontExpand)
|
||||
})
|
||||
|
||||
window.addEventListener('keydown', event => this.keyHandler(event))
|
||||
|
||||
window._sync = new Sync()
|
||||
window._sync.run()
|
||||
}// }}}
|
||||
|
||||
keyHandler(event) {//{{{
|
||||
let handled = true
|
||||
|
||||
// All keybindings is Alt+Shift, since the popular browsers at the time (2023) allows to override thees.
|
||||
// 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.
|
||||
if (!(event.shiftKey && event.altKey) && !(event.key.toUpperCase() === 'S' && event.ctrlKey))
|
||||
return
|
||||
|
||||
switch (event.key.toUpperCase()) {
|
||||
case 'T':
|
||||
console.log(document.activeElement.id)
|
||||
if (document.activeElement.id === 'tree-nodes')
|
||||
document.getElementById('node-content').focus()
|
||||
else
|
||||
document.getElementById('tree-nodes').focus()
|
||||
break
|
||||
|
||||
case 'F':
|
||||
_mbus.dispatch('op-search')
|
||||
break
|
||||
/*
|
||||
case 'C':
|
||||
this.showPage('node')
|
||||
break
|
||||
|
||||
case 'E':
|
||||
this.showPage('keys')
|
||||
break
|
||||
|
||||
case 'M':
|
||||
this.toggleMarkdown()
|
||||
break
|
||||
|
||||
case 'N':
|
||||
this.createNode()
|
||||
break
|
||||
|
||||
case 'P':
|
||||
this.showPage('node-properties')
|
||||
break
|
||||
|
||||
*/
|
||||
case 'S':
|
||||
this.saveNode()
|
||||
/*
|
||||
else if (this.page.value === 'node-properties')
|
||||
this.nodeProperties.current.save()
|
||||
*/
|
||||
break
|
||||
/*
|
||||
|
||||
case 'U':
|
||||
this.showPage('upload')
|
||||
break
|
||||
|
||||
case 'F':
|
||||
this.showPage('search')
|
||||
break
|
||||
*/
|
||||
|
||||
default:
|
||||
handled = false
|
||||
}
|
||||
|
||||
if (handled) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
}
|
||||
}//}}}
|
||||
async getStartNode() {//{{{
|
||||
let nodeUUID = ROOT_NODE
|
||||
|
||||
// Is a UUID provided on the URI as an anchor?
|
||||
const parts = document.URL.split('#')
|
||||
if (parts[1]?.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i))
|
||||
nodeUUID = parts[1]
|
||||
|
||||
return await nodeStore.get(nodeUUID)
|
||||
}//}}}
|
||||
async saveNode() {//{{{
|
||||
if (!this.currentNode.isModified())
|
||||
return
|
||||
|
||||
/* The node history is a local store for node history.
|
||||
* This could be provisioned from the server or cleared if
|
||||
* deemed unnecessary.
|
||||
*
|
||||
* The send queue is what will be sent back to the server
|
||||
* to have a recorded history of the notes.
|
||||
*
|
||||
* A setting to be implemented in the future could be to
|
||||
* not save the history locally at all. */
|
||||
const node = this.currentNode
|
||||
|
||||
// The node is still in its old state and will present
|
||||
// the unmodified content to the node store.
|
||||
const history = nodeStore.nodesHistory.add(node)
|
||||
|
||||
// Prepares the node object for saving.
|
||||
// Sets Updated value to current date and time.
|
||||
await node.save()
|
||||
|
||||
// Updated node is added to the send queue to be stored on server.
|
||||
const sendQueue = nodeStore.sendQueue.add(node)
|
||||
|
||||
// Updated node is saved to the primary node store.
|
||||
const nodeStoreAdding = nodeStore.add([node])
|
||||
|
||||
await Promise.all([history, sendQueue, nodeStoreAdding])
|
||||
}//}}}
|
||||
async goToNode(nodeUUID, dontPush, dontExpand) {//{{{
|
||||
if (nodeUUID === null || nodeUUID === undefined)
|
||||
return
|
||||
|
||||
// Don't switch notes until saved.
|
||||
if (this.nodeUI.isModified()) {
|
||||
if (!confirm("Changes not saved. Do you want to discard changes?"))
|
||||
return
|
||||
}
|
||||
|
||||
if (!dontPush)
|
||||
history.pushState({ nodeUUID }, '', `/notes2#${nodeUUID}`)
|
||||
|
||||
const node = nodeStore.node(nodeUUID)
|
||||
|
||||
node.reset() // any modifications are discarded.
|
||||
|
||||
this.currentNode = node
|
||||
this.treeNative.setSelected(node, dontExpand)
|
||||
|
||||
const ancestors = await nodeStore.getNodeAncestry(node)
|
||||
_mbus.dispatch('CRUMBS_SET', ancestors, () => this.crumbsElement.replaceChildren(this.crumbs.render()))
|
||||
_mbus.dispatch('NODE_UI_OPEN', node)
|
||||
_mbus.dispatch('NODE_UNMODIFIED')
|
||||
|
||||
// Scrolls node into view.
|
||||
this.treeNative.makeVisible(node)
|
||||
}//}}}
|
||||
}
|
||||
|
||||
class Crumbs {
|
||||
constructor() {// {{{
|
||||
this.crumbs = []
|
||||
this.crumbsDiv = document.createElement('div')
|
||||
this.crumbsDiv.classList.add('crumbs')
|
||||
|
||||
_mbus.subscribe('CRUMBS_SET', event => {
|
||||
this.crumbs = event.detail.data
|
||||
})
|
||||
}// }}}
|
||||
render() {// {{{
|
||||
const crumbs = this.crumbs.map(node =>
|
||||
(new Crumb(
|
||||
node.get('Name'),
|
||||
node.UUID,
|
||||
)).render()
|
||||
)
|
||||
|
||||
const start = (new Crumb('Start', ROOT_NODE)).render()
|
||||
crumbs.push(start)
|
||||
|
||||
this.crumbsDiv.replaceChildren(...crumbs.reverse())
|
||||
return this.crumbsDiv
|
||||
}// }}}
|
||||
}
|
||||
|
||||
class Crumb {
|
||||
constructor(label, uuid) {// {{{
|
||||
this.label = label
|
||||
this.uuid = uuid
|
||||
}// }}}
|
||||
render() {// {{{
|
||||
const crumb = document.createElement('div')
|
||||
crumb.classList.add('crumb')
|
||||
|
||||
const link = document.createElement('a')
|
||||
link.href = `/notes2#${this.uuid}`
|
||||
link.innerText = this.label
|
||||
|
||||
crumb.appendChild(link)
|
||||
return crumb
|
||||
|
||||
}// }}}
|
||||
}
|
||||
|
||||
class Op {
|
||||
constructor(id) {
|
||||
this.id = id
|
||||
_mbus.subscribe(this.id, p => this.render(p))
|
||||
}
|
||||
render(html) {
|
||||
const op = document.getElementById('op')
|
||||
const t = document.createElement('template')
|
||||
t.innerHTML = `<dialog id="${this.id}" class="op">${html}</dialog>`
|
||||
op.replaceChildren(t.content)
|
||||
document.getElementById(this.id).showModal()
|
||||
}
|
||||
get(selector) {
|
||||
return document.querySelector(`#${this.id} ${selector}`)
|
||||
}
|
||||
bind(selector, event, fn) {
|
||||
this.get(selector).addEventListener(event, evt => fn(evt))
|
||||
}
|
||||
}
|
||||
|
||||
function tmpl(html) {
|
||||
const el = document.createElement('template')
|
||||
el.innerHTML = html
|
||||
return el.content.children
|
||||
}
|
||||
|
||||
class OpSearch extends Op {
|
||||
constructor() {
|
||||
super('op-search')
|
||||
}
|
||||
|
||||
render() {
|
||||
super.render(`
|
||||
<div class="header">Search</div>
|
||||
<div>
|
||||
<input type="text" />
|
||||
</div>
|
||||
<div class="header">Results</div>
|
||||
<div class="results"></div>
|
||||
`)
|
||||
|
||||
this.bind('input[type="text"]', 'keydown', evt => this.search(evt))
|
||||
}
|
||||
|
||||
search(event) {
|
||||
if (event.key !== 'Enter')
|
||||
return
|
||||
|
||||
const searchFor = document.querySelector('#op-search input').value
|
||||
nodeStore.search(searchFor, ROOT_NODE)
|
||||
.then(res => this.displayResults(res))
|
||||
}
|
||||
|
||||
displayResults(results) {
|
||||
const rs = []
|
||||
for (const r of results) {
|
||||
const ancestors = r.ancestry.reverse().map(a => {
|
||||
const div = tmpl(`<div class="ancestor">${a.data.Name}</div>`)
|
||||
div[0].addEventListener('click', ()=>_notes2.current.goToNode(a.UUID))
|
||||
return div[0]
|
||||
})
|
||||
|
||||
|
||||
const div = tmpl(`<div>${r.name}</div>`)
|
||||
div[0].addEventListener('click', ()=>_notes2.current.goToNode(r.uuid))
|
||||
rs.push(...div)
|
||||
|
||||
const ancDev = tmpl('<div class="ancestors"></div>')
|
||||
ancDev[0].append(...ancestors)
|
||||
rs.push(ancDev[0])
|
||||
}
|
||||
this.get('.results').replaceChildren(...rs)
|
||||
}
|
||||
}
|
||||
|
||||
// vim: foldmethod=marker
|
||||
Loading…
Add table
Add a link
Reference in a new issue