import { ROOT_NODE } from 'node_store' import { N2Tree } from 'tree' import { NodeUINative, Node } from 'node' import { SyncProgress } from 'sync' export class App { constructor() {// {{{ this.currentNode = null this.treeNative = new N2Tree() 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)) document.getElementById('notes2').addEventListener('click', event => { if (event.target.id === 'notes2') document.getElementById('node-content')?.focus() }) const syncProgress = document.getElementById('sync-progress') new SyncProgress(syncProgress) 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': 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 createNode() {//{{{ let name = prompt("Name") if (!name) return const nn = Node.create(name, this.currentNode.UUID) nn.save() nodeStore.sendQueue.add(nn) nodeStore.add([nn]) }//}}} 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 }// }}} } function tmpl(html) {// {{{ const el = document.createElement('template') el.innerHTML = html return el.content.children }// }}} 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 = `${html}` 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)) }// }}} } class OpSearch extends Op { constructor() {// {{{ super('op-search') }// }}} render() {// {{{ super.render(`
Search
Results
`) 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(`
${a.data.Name}
`) div[0].addEventListener('click', () => _notes2.current.goToNode(a.UUID)) return div[0] }) const div = tmpl(`
${r.name}
`) div[0].addEventListener('click', () => _notes2.current.goToNode(r.uuid)) rs.push(...div) const ancDev = tmpl('
') ancDev[0].append(...ancestors) rs.push(ancDev[0]) } this.get('.results').replaceChildren(...rs) }// }}} } // vim: foldmethod=marker