import { ROOT_NODE } from 'node_store' import { CustomHTMLElement } from './lib/custom_html_element.mjs' import { N2Sidebar } from 'sidebar' import { Node } from 'node' export class App { constructor() {// {{{ this.currentNode = null this.sidebar = new N2Sidebar() this.crumbs = new N2Crumbs() this.crumbsElement = document.getElementById('crumbs') this.nodeUI = document.getElementById('note') this.sidebar.render().then(sidebar => { document.getElementById('tree').append(sidebar) document.getElementById('tree-nodes')?.focus() }) _mbus.subscribe('TREE_RENDERED', async () => { // Subscribing to the start node existing after the tree trunk is // fetched since the NODE_COMPONENT_EXIST message isn't sent for the // root node itself, and the root node should be selected in the tree // after it is rendered when the site is shown without UUID in the URL. const startNode = await this.getStartNode() if (startNode.UUID == ROOT_NODE) this.goToNode(startNode.UUID, false, false) else 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) }) _mbus.subscribe('SHOW_PAGE', ({ detail: { data: { page } } }) => { let classList = document.querySelector('#main-page').classList classList.forEach(e => classList.remove(e) ) classList.add(page) classList = document.querySelector('#notes2').classList classList.forEach(e => { if (e.startsWith('page-')) classList.remove(e) }) classList.add('page-' + page) }) 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() }) _mbus.dispatch('SHOW_PAGE', { page: 'node' }) 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.sidebar.focus() } break case 'F': _mbus.dispatch('op-search') break /* case 'C': this.showPage('node') break case 'E': this.showPage('keys') break */ case 'M': globalThis._mbus.dispatch('MARKDOWN_TOGGLE') break case 'N': this.createNode() break /* case 'P': this.showPage('node-properties') break */ case 'S': this.nodeUI.saveNode() 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() {//{{{ }//}}} async createNode() {//{{{ let name = prompt("Name") if (!name) return const nn = Node.create(name, this.currentNode.UUID) nn.save() }//}}} 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 }, '', `/#${nodeUUID}`) const node = nodeStore.node(nodeUUID) node.reset() // any modifications are discarded. this.currentNode = node this.sidebar.setSelected(node, dontExpand) const ancestors = await nodeStore.getNodeAncestry(node) // Scrolls node into view. // makeVisible normally expands all ancestor nodes to make the whole chain visible. // This is a bad idea when quickly navigating the tree, since the arrow navigation // has collapsed nodes which the event calling goToNode can come to undo, if the // event processing lags behind. await this.sidebar.makeVisible(node, ancestors, dontExpand) _mbus.dispatch('CRUMBS_SET', ancestors, () => this.crumbsElement.replaceChildren(this.crumbs.render())) _mbus.dispatch('NODE_UI_OPEN', node) _mbus.dispatch('TREE_EXPANSION', { expand: false, when: 'narrow' }) _mbus.dispatch('NODE_UNMODIFIED') _mbus.dispatch('SHOW_PAGE', { page: 'node' }) }//}}} pageIsVisible(page) {// {{{ let classList = document.querySelector('#main-page').classList return classList.contains(page) }// }}} } 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('', 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() // The house makes it a bit more graphical than just a bunch of text. if (uuid === ROOT_NODE) { const start = document.createElement('div') start.innerHTML = `` start.addEventListener('click', () => _mbus.dispatch("GO_TO_NODE", { nodeUUID: ROOT_NODE, dontPush: false, dontExpand: true })) this.classList.add('home') this.replaceChildren(start) return } this.classList.add('crumb') this.label = label this.uuid = uuid this.elLink.href = `/#${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 = `${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