diff options
Diffstat (limited to 'static/app.js')
| -rw-r--r-- | static/app.js | 76 |
1 files changed, 75 insertions, 1 deletions
diff --git a/static/app.js b/static/app.js index 4c562ff..31083a6 100644 --- a/static/app.js +++ b/static/app.js @@ -20,10 +20,13 @@ // the Dashboard. Card order and size are kept in this browser's localStorage. const tabBar = document.getElementById("tabs"); +document.getElementById("reload").onclick = () => location.reload(); const tabBody = document.getElementById("tab-body"); const refreshers = []; const LAYOUT_KEY = "deckit.layout"; const TAB_KEY = "deckit.tab"; +// Folded cards: id -> the height to restore when unfolded. +const FOLD_KEY = "deckit.folded"; const DEFAULT_W = 6; const DEFAULT_H = 4; const tabs = {}; @@ -113,6 +116,65 @@ for (const node of loadLayout()) { if (node.id) savedById[node.id] = node; } +function loadFolded() { + try { + const saved = JSON.parse(localStorage.getItem(FOLD_KEY)); + return saved && typeof saved === "object" ? saved : {}; + } catch { + return {}; + } +} +const folded = loadFolded(); + +// Folding shrinks the card to one grid row and hides its body; the row it +// keeps is only the title bar (the content stops at the header). Its height +// before folding is kept so unfolding restores it, and it cannot be resized +// while folded. +function setFolded(item, fold) { + const id = item.getAttribute("gs-id"); + const content = item.querySelector(".panel"); + const button = item.querySelector(".panel-fold"); + if (fold) { + const node = item.gridstackNode; + if (!(id in folded)) folded[id] = node?.h ?? DEFAULT_H; + grid.update(item, { h: 1, noResize: true }); + closeGaps(); + } else if (id in folded) { + grid.update(item, { h: folded[id], noResize: false }); + delete folded[id]; + } + content.classList.toggle("folded", fold); + button.textContent = fold ? "▸" : "▾"; + button.setAttribute("aria-label", fold ? "Unfold panel" : "Fold panel"); + button.setAttribute("aria-expanded", String(!fold)); + try { + localStorage.setItem(FOLD_KEY, JSON.stringify(folded)); + } catch {} +} + +// The grid floats, so nothing moves into rows a folded card gave back. Pull +// each card below a folded one, in its columns, up as far as the space is +// free (never above the folded card's title bar). Unfolding needs no mirror: +// the grid pushes down whatever the regrown card hits. +function closeGaps() { + const nodes = grid.engine.nodes; + const clash = (n, y) => nodes.some((o) => o !== n && o.x < n.x + n.w && o.x + o.w > n.x + && o.y < y + n.h && o.y + o.h > y); + const folds = nodes.filter((n) => n.el.querySelector(".panel.folded")); + const below = nodes + .filter((n) => folds.some((f) => n.y > f.y && n.x < f.x + f.w && n.x + n.w > f.x)) + .sort((a, b) => a.y - b.y); + for (const n of below) { + const floor = Math.max(...folds.filter((f) => n.y > f.y && n.x < f.x + f.w && n.x + n.w > f.x) + .map((f) => f.y + f.h)); + let y = n.y; + while (y - 1 >= floor && !clash(n, y - 1)) y--; + if (y !== n.y) { + grid.update(n.el, { y }); + } + } +} + function header(title) { const head = document.createElement("div"); head.className = "panel-head"; @@ -120,11 +182,20 @@ function header(title) { dot.className = "panel-dot"; const h = document.createElement("h2"); h.textContent = title; + const fold = document.createElement("button"); + fold.className = "panel-fold"; + fold.textContent = "▾"; + fold.setAttribute("aria-label", "Fold panel"); + fold.setAttribute("aria-expanded", "true"); + fold.onclick = () => { + const item = fold.closest(".grid-stack-item"); + setFolded(item, !item.querySelector(".panel").classList.contains("folded")); + }; const drag = document.createElement("button"); drag.className = "panel-drag"; drag.textContent = "⠿"; drag.setAttribute("aria-label", "Move panel"); - head.append(dot, h, drag); + head.append(dot, h, fold, drag); return head; } @@ -147,6 +218,7 @@ function makeCard(name, title) { item.append(content); gridEl.append(item); grid.makeWidget(item); + if (name in folded) setFolded(item, true); return body; } @@ -196,7 +268,9 @@ async function boot() { // a broken panel must not take down the rest of the dashboard } } + closeGaps(); ready = true; + saveLayout(); sortTabs(); const saved = localStorage.getItem(TAB_KEY); showTab(tabs[saved] ? saved : "Dashboard"); |
