From e707becbf8e56be2e41f914fea186951006ae658 Mon Sep 17 00:00:00 2001 From: Bruno Charest Date: Sat, 12 Sep 2026 21:26:56 -0400 Subject: [PATCH] =?UTF-8?q?feat(nav):=20partial=20HTMX=20navigation=20?= =?UTF-8?q?=E2=80=94=20swap=20main=20content=20without=20full=20reload?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Intercept internal links and load only .main-wrapper via htmx.ajax, keeping the sidebar/header shell in place. Adds history pushState/popstate handling, sidebar active-state resync and document title update. Moves page script blocks inside #main-content so they run on swap, and makes editor/board scripts idempotent. Routes navigateTo/breadcrumb/library/local-workspace through window.fdNavigate and fixes the duplicated navigateTo definition. --- app/templates/_header.html | 6 +- app/templates/_notification_bell.html | 3 + app/templates/_page_editor_realtime.html | 24 +++- app/templates/_page_editor_scripts.html | 4 + app/templates/base.html | 26 ++--- app/templates/board.html | 6 +- app/templates/library.html | 6 +- app/templates/local_workspace.html | 11 +- static/js/app.js | 138 +++++++++++++++++++++++ 9 files changed, 197 insertions(+), 27 deletions(-) diff --git a/app/templates/_header.html b/app/templates/_header.html index d5e2e52..a319a40 100644 --- a/app/templates/_header.html +++ b/app/templates/_header.html @@ -247,7 +247,11 @@ document.addEventListener('alpine:init', function () { this.activeItems = []; }, - go: function (url) { if (url) window.location.href = url; } + go: function (url) { + if (!url) return; + if (window.fdNavigate) window.fdNavigate(url); + else window.location.href = url; + } }; }); }); diff --git a/app/templates/_notification_bell.html b/app/templates/_notification_bell.html index 521aafc..76d274d 100644 --- a/app/templates/_notification_bell.html +++ b/app/templates/_notification_bell.html @@ -73,6 +73,9 @@ document.addEventListener('alpine:init', function () { var self = this; this._timer = setInterval(function () { self.refreshCount(); }, 30000); }, + destroy() { + if (this._timer) { clearInterval(this._timer); this._timer = null; } + }, toggle() { this.open = !this.open; if (this.open) this.load(); }, timeAgo(s) { if (!s) return ''; diff --git a/app/templates/_page_editor_realtime.html b/app/templates/_page_editor_realtime.html index 50e61b2..4db6882 100644 --- a/app/templates/_page_editor_realtime.html +++ b/app/templates/_page_editor_realtime.html @@ -37,6 +37,19 @@ window.__fdRT = (function () { let emitT = null, selT = null, titleT = null, drawT = null, retryT = null, pollT = null; let _pid = 0; + // Detach global listeners from a previous editor instance so that + // partial (HTMX) navigation doesn't accumulate stale handlers. + function unbindGlobal() { + const g = window.__fdRTGlobal; + if (!g) return; + try { + document.removeEventListener('selectionchange', g.onSel, true); + window.removeEventListener('scroll', g.onScroll, true); + window.removeEventListener('resize', g.onResize); + } catch (e) { /* noop */ } + window.__fdRTGlobal = null; + } + const clone = (o) => JSON.parse(JSON.stringify(o)); function colorOf(uid) { return COLORS[Math.abs(uid || 0) % COLORS.length]; } @@ -459,9 +472,14 @@ window.__fdRT = (function () { } const tEl = document.getElementById('_titleEl'); if (tEl) tEl.addEventListener('input', titleInput); - document.addEventListener('selectionchange', () => { clearTimeout(selT); selT = setTimeout(emitCursor, 150); }, true); - window.addEventListener('scroll', scheduleDraw, true); - window.addEventListener('resize', scheduleDraw); + unbindGlobal(); + const onSel = () => { clearTimeout(selT); selT = setTimeout(emitCursor, 150); }; + const onScroll = () => scheduleDraw(); + const onResize = () => scheduleDraw(); + document.addEventListener('selectionchange', onSel, true); + window.addEventListener('scroll', onScroll, true); + window.addEventListener('resize', onResize); + window.__fdRTGlobal = { onSel, onScroll, onResize }; } function start(ed) { diff --git a/app/templates/_page_editor_scripts.html b/app/templates/_page_editor_scripts.html index 25cb5ee..29b17fe 100644 --- a/app/templates/_page_editor_scripts.html +++ b/app/templates/_page_editor_scripts.html @@ -3,6 +3,8 @@ diff --git a/app/templates/base.html b/app/templates/base.html index 61270b3..dc2bc1b 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -211,7 +211,7 @@
FlowDeck v{{ app_version }}
- {{ fd_icon("log-out", 16) }} Log out + {{ fd_icon("log-out", 16) }} Log out @@ -265,7 +265,7 @@ {% else %} {% endif %} - @@ -598,6 +598,7 @@
{% block content %}{% endblock %} + {% block scripts %}{% endblock %}
@@ -1255,11 +1256,6 @@ toggleWorkspaceMenu() { this.workspaceMenuOpen = !this.workspaceMenuOpen; }, - navigateTo(url) { - this.mobileSidebarOpen = false; - this.sidebarCollapsed = true; - window.location.href = url; - }, openQuickFind() { if (window.FlowDeckPalette) window.FlowDeckPalette.open(); }, // ── Tree state ── @@ -1328,12 +1324,15 @@ }, navigateTo(url) { + this.mobileSidebarOpen = false; + this.sidebarCollapsed = true; // Preserve workspace context when navigating to library/trash if ((url === '/library' || url === '/trash') && this.workspaceKey && this.workspaceKey.includes('/')) { const parts = this.workspaceKey.split('/'); url = `${url}?owner=${parts[0]}&repo=${parts.slice(1).join('/')}`; } - window.location.href = url; + if (window.fdNavigate) window.fdNavigate(url); + else window.location.href = url; }, showContextMenu(event, pageId, pageName) { @@ -1669,7 +1668,8 @@ .catch(err => { this.toast('Failed: ' + err.message, 'error'); }); }, openWorkspacePage(pageId) { - window.location = `/pages/${pageId}`; + if (window.fdNavigate) window.fdNavigate('/pages/' + pageId); + else window.location = `/pages/${pageId}`; }, // ── Tree expand/collapse + folder navigation ── @@ -1689,7 +1689,8 @@ localStorage.setItem('fd_expanded_folders', JSON.stringify(this.expandedFolders)); localStorage.setItem('fd_sections', JSON.stringify(this.sectionsOpen)); } catch(e) {} - window.location = `/local-workspace?folder=${id}`; + if (window.fdNavigate) window.fdNavigate(`/local-workspace?folder=${id}`); + else window.location = `/local-workspace?folder=${id}`; }, createEmptyPage() { @@ -1841,8 +1842,7 @@ } }); - -{% block scripts %}{% endblock %} +