From b3e5d7940338d08bfe43deba30a2ba490ce485fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20=C3=85hall?= Date: Mon, 8 Jun 2026 16:48:25 +0200 Subject: [PATCH 01/60] Refactored tree to sidebar --- static/css/notes2.css | 6 +++--- static/js/app.mjs | 12 ++++++------ static/js/{tree.mjs => sidebar.mjs} | 30 ++++++++++++++--------------- views/layouts/main.gotmpl | 2 +- 4 files changed, 25 insertions(+), 25 deletions(-) rename static/js/{tree.mjs => sidebar.mjs} (94%) diff --git a/static/css/notes2.css b/static/css/notes2.css index 743b4b3..b4d7e02 100644 --- a/static/css/notes2.css +++ b/static/css/notes2.css @@ -92,7 +92,7 @@ button { border-right: none; } - n2-tree { + n2-sidebar { display: none; } @@ -164,7 +164,7 @@ button { border-right: 1px solid var(--line-color); - n2-tree { + n2-sidebar { .el-treenodes { margin: 24px 32px 32px 32px; } @@ -508,7 +508,7 @@ dialog.op { } #tree { - n2-tree { + n2-sidebar { .el-treenodes { height: calc(100vh - 64px - 64px); margin: 0px; diff --git a/static/js/app.mjs b/static/js/app.mjs index b444140..2b5cbe1 100644 --- a/static/js/app.mjs +++ b/static/js/app.mjs @@ -1,12 +1,12 @@ import { ROOT_NODE } from 'node_store' import { CustomHTMLElement } from './lib/custom_html_element.mjs' -import { N2Tree } from 'tree' +import { N2Sidebar } from 'sidebar' import { Node } from 'node' export class App { constructor() {// {{{ this.currentNode = null - this.tree = new N2Tree() + this.sidebar = new N2Sidebar() // XXX - rename this.tree this.crumbs = new N2Crumbs() this.crumbsElement = document.getElementById('crumbs') this.nodeUI = document.getElementById('note') @@ -22,7 +22,7 @@ export class App { this.goToNode(startNode.UUID, false, false) }) - document.getElementById('tree').append(await this.tree.render()) + document.getElementById('tree').append(await this.sidebar.render()) document.getElementById('tree-nodes')?.focus() if (startNode.UUID == ROOT_NODE) @@ -85,7 +85,7 @@ export class App { if (document.activeElement.id === 'tree-nodes') { this.nodeUI.takeFocus() } else { - this.tree.focus() + this.sidebar.focus() } break @@ -184,7 +184,7 @@ export class App { node.reset() // any modifications are discarded. this.currentNode = node - this.tree.setSelected(node, dontExpand) + this.sidebar.setSelected(node, dontExpand) const ancestors = await nodeStore.getNodeAncestry(node) _mbus.dispatch('CRUMBS_SET', ancestors, () => this.crumbsElement.replaceChildren(this.crumbs.render())) @@ -193,7 +193,7 @@ export class App { _mbus.dispatch('TREE_EXPANSION', { expand: false, when: 'narrow' }) // Scrolls node into view. - this.tree.makeVisible(node) + this.sidebar.makeVisible(node) }//}}} } diff --git a/static/js/tree.mjs b/static/js/sidebar.mjs similarity index 94% rename from static/js/tree.mjs rename to static/js/sidebar.mjs index 6443455..c0ef99e 100644 --- a/static/js/tree.mjs +++ b/static/js/sidebar.mjs @@ -59,12 +59,12 @@ class TreeExpansionHandler {// {{{ } }// }}} -export class N2Tree extends CustomHTMLElement { +export class N2Sidebar extends CustomHTMLElement { static {// {{{ this.tmpl = document.createElement('template') this.tmpl.innerHTML = ` + + ` + }// }}} + 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 36/60] 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 46/60] 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 @@
- - -