import { ROOT_NODE } from 'node_store' import { CustomHTMLElement } from './lib/custom_html_element.mjs' import { N2Tree } from 'tree' import { Node } from 'node' export class App { constructor() {// {{{ this.currentNode = null this.tree = new N2Tree() this.crumbs = new N2Crumbs() this.crumbsElement = document.getElementById('crumbs') this.nodeUI = document.getElementById('note') _mbus.subscribe('TREE_TRUNK_FETCHED', async () => { document.getElementById('tree').append(this.tree.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.addEventListener('popstate', event => this.popState(event)) document.getElementById('notes2').addEventListener('click', event => { if (event.target.id === 'notes2') document.getElementById('node-content')?.focus() }) window._sync = new Sync() // I think it is uncomfortable having the sync running as soon as the page load. // I haven't gotten the time to look at the page before stuff jumps around. // There a slight delay to initiate sync seems reasonable. setTimeout(() => window._sync.run(), 1000) }// }}} 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') this.nodeUI.takeFocus() else this.nodeUI.takeFocus() 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() } }//}}} popState(event) {// {{{ _mbus.dispatch("GO_TO_NODE", { nodeUUID: event.state.nodeUUID, dontPush: true, dontExpand: true }) }// }}} 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.tree.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.tree.makeVisible(node) }//}}} } class N2Crumbs extends CustomHTMLElement { static {// {{{ this.tmpl = document.createElement('template') this.tmpl.innerHTML = ` ` }// }}} constructor() {// {{{ super() this.classList.add('crumbs') this.crumbs = [] _mbus.subscribe('CRUMBS_SET', event => { this.crumbs = event.detail.data }) }// }}} render() {// {{{ const crumbs = this.crumbs.map(node => new N2Crumb( node.get('Name'), node.UUID, ) ) const start = new N2Crumb('Start', ROOT_NODE) crumbs.push(start) this.replaceChildren(...crumbs.reverse()) return this }// }}} } customElements.define('n2-crumbs', N2Crumbs) class N2Crumb extends CustomHTMLElement { static {// {{{ this.tmpl = document.createElement('template') this.tmpl.innerHTML = ` ` }// }}} constructor(label, uuid) {// {{{ super() this.classList.add('crumb') this.label = label this.uuid = uuid this.elLink.href = `/notes2#${this.uuid}` this.elLink.innerText = this.label this.elLink.addEventListener('click', () => _mbus.dispatch("GO_TO_NODE", { nodeUUID: this.uuid, dontPush: false, dontExpand: true })) }// }}} } customElements.define('n2-crumb', N2Crumb) 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 = `` 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(`