diff --git a/static/css/notes2.css b/static/css/notes2.css index 74b6392..0a23116 100644 --- a/static/css/notes2.css +++ b/static/css/notes2.css @@ -17,15 +17,10 @@ html { display: none; } } -/* -#tree-native { - grid-area: tree; -} -*/ #tree { grid-area: tree; - padding: 16px 32px; - background-color: #333; + display: grid; + padding: 16px 0px 16px 16px; color: #ddd; z-index: 100; border-left: 2px solid #333; @@ -37,9 +32,11 @@ html { display: grid; position: relative; justify-items: center; + margin-top: 8px; margin-bottom: 8px; margin-left: 24px; margin-right: 24px; + cursor: pointer; } #tree #logo img { width: 128px; @@ -85,12 +82,18 @@ html { #tree .node .children.collapsed { display: none; } +#tree-nodes { + padding: 16px 32px; + background-color: #333; + border-radius: 8px; + box-shadow: 5px 5px 10px -5px rgba(0, 0, 0, 0.75); +} #crumbs { grid-area: crumbs; display: grid; align-items: start; justify-items: center; - margin: 0px 16px; + margin: 16px 16px; } #crumbs .crumbs { display: flex; @@ -114,6 +117,10 @@ html { user-select: none; -webkit-tap-highlight-color: transparent; } +#crumbs .crumbs .crumb a { + text-decoration: none; + color: inherit; +} #crumbs .crumbs .crumb:after { content: "•"; margin-left: 8px; diff --git a/static/js/app.mjs b/static/js/app.mjs new file mode 100644 index 0000000..c6529fc --- /dev/null +++ b/static/js/app.mjs @@ -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 = `` + 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(` +