From 5dac84efdcff92e04c7be3787f21b6174136c9a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20=C3=85hall?= Date: Fri, 12 Jun 2026 07:12:38 +0200 Subject: [PATCH 01/36] Cleaner CSS page management --- static/css/notes2.css | 50 ++++++++++++++++++++++++++----------------- static/js/app.mjs | 19 ++++++++-------- 2 files changed, 39 insertions(+), 30 deletions(-) diff --git a/static/css/notes2.css b/static/css/notes2.css index b57704d..017124a 100644 --- a/static/css/notes2.css +++ b/static/css/notes2.css @@ -218,27 +218,16 @@ button { } } + +/* =============== * + * PAGE MANAGEMENT * + * =============== */ [id^="page-"] { display: none; } -#main-page { - display: contents; - - &:focus-within { - background-color: #faf; - } - - &.node { - &.root-node-override { - #page-root { - display: contents; - } - - #page-node { - display: none; - } - } +#notes2 { + &.page-node { #page-root { display: none; @@ -249,7 +238,7 @@ button { } } - &.storage { + &.page-storage { #page-storage { display: contents; @@ -259,7 +248,7 @@ button { } } - &.history { + &.page-history { #page-history { display: grid; grid-area: n2-pagehistory; @@ -267,6 +256,27 @@ button { n2-pagehistory {} } } + + &.root-node-override { + [id^="page-"] { + display: none !important; + } + + #page-root { + display: contents !important; + } + + } + +} + +#main-page { + display: contents; + + &:focus-within { + background-color: #faf; + } + } #crumbs { @@ -358,7 +368,7 @@ n2-syncprogress { } #page-root { - & > div { + &>div { grid-area: content; align-self: start; margin-top: 64px; diff --git a/static/js/app.mjs b/static/js/app.mjs index 7601a90..a54be2e 100644 --- a/static/js/app.mjs +++ b/static/js/app.mjs @@ -4,6 +4,8 @@ import { N2Sidebar } from 'sidebar' import { Node } from 'node' export class App { + static PAGES = ['node', 'history', 'storage'] + constructor() {// {{{ this.currentNode = null this.sidebar = new N2Sidebar() @@ -16,12 +18,15 @@ export class App { document.getElementById('tree-nodes')?.focus() }) - const mainPage = document.getElementById('main-page') + // Start node shows a system-wide page instead of node editing + // since the start node is kind of magic and doesn't fit into + // the syncing system. const determineNodePage = uuid => { + const el = document.getElementById('notes2') if (uuid == ROOT_NODE) - mainPage.classList.add('root-node-override') + el.classList.add('root-node-override') else - mainPage.classList.remove('root-node-override') + el.classList.remove('root-node-override') } _mbus.subscribe('TREE_RENDERED', async () => { @@ -47,13 +52,7 @@ export class App { }) _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 + const classList = document.getElementById('notes2').classList classList.forEach(e => { if (e.startsWith('page-')) classList.remove(e) From 73d87d61c48cf5dad66ca3ce49d56eee1ea5af3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20=C3=85hall?= Date: Fri, 12 Jun 2026 07:13:25 +0200 Subject: [PATCH 02/36] Fixed syncing alert not showing the proper error --- static/js/sync.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/js/sync.mjs b/static/js/sync.mjs index b6328aa..fe72c3f 100644 --- a/static/js/sync.mjs +++ b/static/js/sync.mjs @@ -158,7 +158,7 @@ export class Sync { } catch (e) { console.trace(e) - alert(e) + alert(e.error) return } } From 2d036f847a9dd909c1123d531818c58acb27791b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20=C3=85hall?= Date: Fri, 12 Jun 2026 07:25:02 +0200 Subject: [PATCH 03/36] Better debuggability for node sync problems --- main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.go b/main.go index 9dabb27..a85ca72 100644 --- a/main.go +++ b/main.go @@ -379,7 +379,7 @@ func actionSyncToServer(w http.ResponseWriter, r *http.Request) { // {{{ _, err = db.Exec(`CALL add_nodes($1, $2::uuid, $3::jsonb)`, user.UserID, user.ClientUUID, request.NodeData) if err != nil { - Log.Error("sync", "error", err) + Log.Error("sync", "error", err, "user_id", user.UserID, "client_uuid", user.ClientUUID, "node_data", request.NodeData) httpError(w, err) return } From ffb7f4ac53f7ac933d16dc1a4f6017ecfd1a5c97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20=C3=85hall?= Date: Fri, 12 Jun 2026 08:27:06 +0200 Subject: [PATCH 04/36] Go to newly created node. --- static/js/app.mjs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/static/js/app.mjs b/static/js/app.mjs index a54be2e..4127b91 100644 --- a/static/js/app.mjs +++ b/static/js/app.mjs @@ -152,8 +152,9 @@ export class App { // Treenode is forcefully rerendered and children refetched to both show the new node // and to get it resorted. - const treenode = this.sidebar.getTreeNode(this.currentNode.UUID) - treenode.render(true, true) + const parentTreenode = this.sidebar.getTreeNode(this.currentNode.UUID) + await parentTreenode.render(true, true) + _mbus.dispatch('GO_TO_NODE', { nodeUUID: nn.UUID }) }//}}} async goToNode(nodeUUID, dontPush, dontExpand) {//{{{ if (nodeUUID === null || nodeUUID === undefined) From 9af733be641a0cd5f5945633347fd4ebbb9edb05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20=C3=85hall?= Date: Fri, 12 Jun 2026 08:47:24 +0200 Subject: [PATCH 05/36] Icons and keybindings for creating sub-documents and sibling documents --- static/css/notes2.css | 2 +- static/images/icon_new_document.svg | 49 +++++++++++++++++++++++++++++ static/js/app.mjs | 35 +++++++++++++-------- static/js/page_node.mjs | 10 +++++- 4 files changed, 81 insertions(+), 15 deletions(-) create mode 100644 static/images/icon_new_document.svg diff --git a/static/css/notes2.css b/static/css/notes2.css index 017124a..04c9f68 100644 --- a/static/css/notes2.css +++ b/static/css/notes2.css @@ -9,7 +9,7 @@ --line-color: #ccc; --tree-expander: 0px; - --functions-width: 150px; + --functions-width: 180px; } html { diff --git a/static/images/icon_new_document.svg b/static/images/icon_new_document.svg new file mode 100644 index 0000000..a105e05 --- /dev/null +++ b/static/images/icon_new_document.svg @@ -0,0 +1,49 @@ + + + + + + + + file-document-plus-outline + + + diff --git a/static/js/app.mjs b/static/js/app.mjs index 4127b91..876d11d 100644 --- a/static/js/app.mjs +++ b/static/js/app.mjs @@ -82,19 +82,18 @@ export class App { keyHandler(event) {//{{{ let handled = true - if (event.key == 'F2') { - this.nodeUI.renameNode() - return - } - - // All keybindings is Alt+Shift, since the popular browsers at the time (2023) allows to override thees. + // Most 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 + const SHIFT_ALT = event.shiftKey && !event.ctrlKey && event.altKey + const SHIFT_CTRL_ALT = event.shiftKey && event.ctrlKey && event.altKey switch (event.key.toUpperCase()) { + case 'F2': + this.nodeUI.renameNode() + break case 'T': + if (!SHIFT_ALT) break if (document.activeElement.id === 'tree-nodes') this.nodeUI.takeFocus() else @@ -102,18 +101,25 @@ export class App { break case 'F': + if (!SHIFT_ALT) break _mbus.dispatch('op-search') break case 'M': + if (!SHIFT_ALT) break globalThis._mbus.dispatch('MARKDOWN_TOGGLE') break case 'N': - this.createNode() + if (SHIFT_ALT) + this.createNode() + else if (SHIFT_CTRL_ALT) { + this.createNode(this.currentNode?.ParentUUID) + } break case 'S': + if (!SHIFT_ALT) break this.nodeUI.saveNode() break @@ -142,17 +148,20 @@ export class App { async saveNode() {//{{{ }//}}} - async createNode() {//{{{ - let name = prompt("Name") + async createNode(createUnderUUID) {//{{{ + const parentUUID = createUnderUUID ? createUnderUUID : this.currentNode.UUID + const p = createUnderUUID ? 'Name for sibling document' : 'Name for sub-document' + + let name = prompt(p) if (!name) return - const nn = Node.create(name, this.currentNode.UUID) + const nn = Node.create(name, parentUUID) await nn.save() // Treenode is forcefully rerendered and children refetched to both show the new node // and to get it resorted. - const parentTreenode = this.sidebar.getTreeNode(this.currentNode.UUID) + const parentTreenode = this.sidebar.getTreeNode(parentUUID) await parentTreenode.render(true, true) _mbus.dispatch('GO_TO_NODE', { nodeUUID: nn.UUID }) }//}}} diff --git a/static/js/page_node.mjs b/static/js/page_node.mjs index 28388f8..b86b172 100644 --- a/static/js/page_node.mjs +++ b/static/js/page_node.mjs @@ -9,10 +9,11 @@ export class N2PageNodeUI extends CustomHTMLElement { + + ` + }// }}} + constructor() {// {{{ + super(true) + + document.addEventListener('dragover', e => { + this.style.left = `${e.clientX + 8}px` + this.style.top = `${e.clientY}px` + }) + + this.dragTarget = null + }// }}} + start() {// {{{ + this.style.display = 'block' + }// }}} + end() {// {{{ + this.style.display = 'none' + }// }}} + icon(name) {// {{{ + if (name != '') + name = '_' + name + this.elIcon.setAttribute('src', `/images/${_VERSION}/icon_drag${name}.svg`) + }// }}} + setTarget(t) {// {{{ + this.dragTarget = t + }// }}} + getTarget() {// {{{ + return this.dragTarget + }// }}} +} + +customElements.define('n2-crumbs', N2Crumbs) +customElements.define('n2-crumb', N2Crumb) +customElements.define('n2-dragicon', N2DragIcon) + // vim: foldmethod=marker diff --git a/static/js/node_store.mjs b/static/js/node_store.mjs index f31a4b6..324f004 100644 --- a/static/js/node_store.mjs +++ b/static/js/node_store.mjs @@ -247,6 +247,7 @@ export class NodeStore { nodeStore = t.objectStore('nodes') t.oncomplete = (_event) => { + console.log('complete') resolve() } @@ -358,6 +359,7 @@ class SimpleNodeStore { // Node to be moved is first stored in the new queue. const req = store.put(node.data) req.onsuccess = () => { + console.log('here') resolve() } req.onerror = (event) => { diff --git a/static/js/page_node.mjs b/static/js/page_node.mjs index 834e22f..f65afb9 100644 --- a/static/js/page_node.mjs +++ b/static/js/page_node.mjs @@ -422,6 +422,11 @@ export class Node { getParent() {//{{{ return this._parent }//}}} + moveToParent(newParentUUID) {// {{{ + this.ParentUUID = newParentUUID + this.data.ParentUUID = newParentUUID + this._modified = true + }// }}} isLastSibling() {//{{{ return this._sibling_after === null }//}}} @@ -463,9 +468,10 @@ export class Node { // When stored into database and ancestry was changed, // the ancestry path could be interesting. + /* const ancestors = await nodeStore.getNodeAncestry(this) this.data.Ancestors = ancestors.map(a => a.get('Name')).reverse() - + */ /* The node history is a local store for node history. * This could be provisioned from the server or cleared if * deemed unnecessary. @@ -481,12 +487,17 @@ export class Node { const history = nodeStore.nodesHistory.add(this) // Updated node is added to the send queue to be stored on server. + const sendQueue = nodeStore.sendQueue.add(this) // Updated node is saved to the primary node store. const nodeStoreAdding = nodeStore.add([this]) - return Promise.all([history, sendQueue, nodeStoreAdding]) + console.log('waiting') + await Promise.all([history, sendQueue, nodeStoreAdding]) + console.log('waiting done') + + return }//}}} } diff --git a/static/js/sidebar.mjs b/static/js/sidebar.mjs index 7d73d6a..561de8d 100644 --- a/static/js/sidebar.mjs +++ b/static/js/sidebar.mjs @@ -449,15 +449,20 @@ export class N2Sidebar extends CustomHTMLElement { treenode?.scrollIntoView({ block: 'nearest' }) }// }}} } -customElements.define('n2-sidebar', N2Sidebar) export class N2TreeNode extends CustomHTMLElement { + static DRAG_ICON = new Image() + static DRAG_ICON_OK = new Image() + static {// {{{ + N2TreeNode.DRAG_ICON.src = `/images/${_VERSION}/leaf.svg` + N2TreeNode.DRAG_ICON_OK.src = `/images/${_VERSION}/expanded.svg` + this.tmpl = document.createElement('template') this.tmpl.innerHTML = `
- +
@@ -490,6 +545,7 @@ export class N2TreeNode extends CustomHTMLElement { constructor(sidebar, node, parent) {//{{{ super() + this.setAttribute('draggable', 'true') this.classList.add('node') this.sidebar = sidebar @@ -498,6 +554,7 @@ export class N2TreeNode extends CustomHTMLElement { this.children_populated = false this.rendered = false + this.dragNode = null this.elExpandToggle.addEventListener('click', () => this.sidebar.setNodeExpanded(this.node, !this.sidebar.getNodeExpanded(this.node.UUID))) this.elName.addEventListener('click', () => _mbus.dispatch('TREE_NODE_SELECTED', this.node)) @@ -505,6 +562,70 @@ export class N2TreeNode extends CustomHTMLElement { _mbus.subscribe(`NODE_EXPAND_${node.UUID}`, _state => { this.render(true) }) + + // Drag-and-dropping of nodes + this.addEventListener('dragstart', event => this.dragStart(event)) + this.addEventListener('dragend', event => this.dragEnd(event)) + this.addEventListener('dragover', event => this.dragOver(event)) + this.addEventListener('drop', event => this.dragDrop(event)) + this.elName.addEventListener('dragenter', event => this.dragEnter(event)) + this.elName.addEventListener('dragleave', event => this.dragLeave(event)) + }// }}} + dragStart(e) {// {{{ + if (this.node.isModified()) { + alert('Save note before moving it.') + e.stopPropagation() + e.preventDefault() + return + } + + this.classList.add('drag-source') + const blankPixel = new Image() + blankPixel.src = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7' + e.dataTransfer.setDragImage(blankPixel, 0, 0) + e.dataTransfer.allowedEffects = 'none' + e.stopPropagation() + _app.dragIcon.start() + }// }}} + dragEnd(e) {// {{{ + this.classList.remove('drag-source') + _app.dragIcon.end() + e.stopPropagation() + }// }}} + dragOver(e) {// {{{ + e.dataTransfer.dropEffect = 'move' + e.preventDefault() + }// }}} + async dragDrop(e) {// {{{ + e.stopPropagation() + const moveToNode = _app.dragIcon.getTarget() + await _app.moveNode(this.node, moveToNode.node.UUID) + return + + _app.sidebar.setNodeExpanded(moveToNode, true) + await this.render(true, true) + await moveToNode.render(true, true) + + this.dragLeave(e) + }// }}} + dragEnter(e) {// {{{ + const targetNode = e.target.closest('n2-treenode') + if (targetNode.classList.contains('drag-source')) + return + e.stopPropagation() + _app.dragIcon.icon('ok') + this.classList.add('drag-target') + + _app.dragIcon.setTarget(this) + }// }}} + dragLeave(e) {// {{{ + e.stopPropagation() + e.dataTransfer.dropEffect = 'none' + e.dataTransfer.setDragImage(N2TreeNode.DRAG_ICON, -16, 8) + _app.dragIcon.icon('') + this.classList.remove('drag-target') + + _app.dragIcon.setTarget(null) }// }}} async fetchChildren(force_fetch) {//{{{ if (this.children_populated && !force_fetch) @@ -575,6 +696,8 @@ export class N2TreeNode extends CustomHTMLElement { img.setAttribute('src', newSrc) }// }}} } + +customElements.define('n2-sidebar', N2Sidebar) customElements.define('n2-treenode', N2TreeNode) // vim: foldmethod=marker diff --git a/views/pages/notes2.gotmpl b/views/pages/notes2.gotmpl index abec2b0..381d672 100644 --- a/views/pages/notes2.gotmpl +++ b/views/pages/notes2.gotmpl @@ -1,4 +1,7 @@ {{ define "page" }} + + +
>
From 53d8d16086d8941b35b348ab4008a08513535171 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20=C3=85hall?= Date: Sun, 14 Jun 2026 14:36:52 +0200 Subject: [PATCH 12/36] Initial work on node menu --- static/css/notes2.css | 2 +- static/js/page_node.mjs | 17 ++++++++++++++++- views/pages/notes2.gotmpl | 2 ++ 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/static/css/notes2.css b/static/css/notes2.css index 079e3c1..dfe4156 100644 --- a/static/css/notes2.css +++ b/static/css/notes2.css @@ -9,7 +9,7 @@ --line-color: #ccc; --tree-expander: 0px; - --functions-width: 180px; + --functions-width: 216px; } html { diff --git a/static/js/page_node.mjs b/static/js/page_node.mjs index f65afb9..f6aa681 100644 --- a/static/js/page_node.mjs +++ b/static/js/page_node.mjs @@ -9,7 +9,7 @@ export class N2PageNodeUI extends CustomHTMLElement { +
+ + + + +
+ ` + }// }}} + constructor() {// {{{ + super() + }// }}} +} +customElements.define('n2-nodemenu', N2NodeMenu) + export class N2PageNodeUI extends CustomHTMLElement { static {// {{{ this.tmpl = document.createElement('template') this.tmpl.innerHTML = `
@@ -28,11 +92,13 @@ export class N2PageNodeUI extends CustomHTMLElement {
- - - + +
+ ` }// }}} @@ -41,7 +107,6 @@ export class N2PageNodeUI extends CustomHTMLElement { this.node = null this.style.display = 'contents' - this.classList.add('show-markdown') // TODO Should probably be moved to settings. this.marked = new MarkedPosition() _mbus.subscribe('NODE_UI_OPEN', event => { @@ -70,11 +135,27 @@ export class N2PageNodeUI extends CustomHTMLElement { _mbus.subscribe('MARKDOWN_EDIT', ({ detail }) => this.editMarkdown(detail.data)) _mbus.subscribe('MARKDOWN_CHANGE_CHECKBOX', ({ detail }) => this.checkboxUpdated(detail.data)) + // Binding the node rename handler. this.elName.addEventListener('click', async () => this.renameNode()) + + // Bind handlers for content keyboard input and paste. this.elNodeContent.addEventListener('input', event => this.contentChanged(event)) this.elNodeContent.addEventListener('paste', async (event) => this.pasteHandler(event)) + + // Bind node icon handlers. + this.elIconSave.addEventListener('click', () => this.saveNode()) this.elIconMarkdown.addEventListener('click', () => this.showMarkdown(!this.showMarkdown())) - this.elIconTableFormat.addEventListener('click', event => { + this.elIconNewDocument.addEventListener('click', event => { + if (event.shiftKey) + _app.createNode(this.node.ParentUUID) + else + _app.createNode() + }) + + // Bind node menu items to handlers. + this.elNodeMenu.elFormatTables.addEventListener('click', event => { + this.elNodeMenu.hidePopover() + if (!event.shiftKey) this.elNodeContent.value = this.formatAllTables(this.elNodeContent.value) else { @@ -88,15 +169,12 @@ export class N2PageNodeUI extends CustomHTMLElement { this.node.setContent(this.elNodeContent.value) }) - this.elIconHistory.addEventListener('click', () => _mbus.dispatch('SHOW_PAGE', { page: 'history' })) - this.elIconSave.addEventListener('click', () => this.saveNode()) - this.elIconNewDocument.addEventListener('click', event => { - if (event.shiftKey) - _app.createNode(this.node.ParentUUID) - else - _app.createNode() + this.elNodeMenu.elHistory.addEventListener('click', () => { + _mbus.dispatch('SHOW_PAGE', { page: 'history' }) }) + // Default is to always show markdown. + this.classList.add('show-markdown') // TODO Should probably be moved to settings. this.showMarkdown(true) }// }}} renderName() {// {{{ @@ -309,7 +387,7 @@ export class N2PageNodeUI extends CustomHTMLElement { // Node is modified with the new value. User has to save manually, otherwise other changes could be saved // when a save wasn't expected. - const newValue =`[${checkbox.checked ? 'x' : ' '}] ` + const newValue = `[${checkbox.checked ? 'x' : ' '}] ` const modifiedContent = this.node.content().slice(0, pos.start) + newValue + this.node.content().slice(pos.end) this.node.setContent(modifiedContent) @@ -506,22 +584,10 @@ export class Node { console.log('waiting') await Promise.all([history, sendQueue, nodeStoreAdding]) console.log('waiting done') - + return }//}}} } -class N2Menu extends CustomHTMLElement { - static {// {{{ - this.tmpl = document.createElement('template') - this.tmpl.innerHTML = ` -
Popover content
- ` - }// }}} - constructor() {// {{{ - super() - }// }}} -} -customElements.define('n2-menu', N2Menu) // vim: foldmethod=marker diff --git a/views/pages/notes2.gotmpl b/views/pages/notes2.gotmpl index a726451..381d672 100644 --- a/views/pages/notes2.gotmpl +++ b/views/pages/notes2.gotmpl @@ -6,8 +6,6 @@
>
- -
Popover content
From dbd3872f0fd90c9f672fcc64d572a56f080c8316 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20=C3=85hall?= Date: Tue, 16 Jun 2026 08:35:58 +0200 Subject: [PATCH 22/36] Fixed flashing history page --- views/pages/notes2.gotmpl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/views/pages/notes2.gotmpl b/views/pages/notes2.gotmpl index 381d672..d54c71d 100644 --- a/views/pages/notes2.gotmpl +++ b/views/pages/notes2.gotmpl @@ -1,9 +1,12 @@ {{ define "page" }} + + + -
+
>
@@ -37,9 +40,6 @@
- - -