diff options
| author | Danilo M. <danix@danix.xyz> | 2026-11-09 18:53:11 +0100 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-11-09 18:53:11 +0100 |
| commit | a9d709f6dd893b6fab6750b67f0c4f367458f40c (patch) | |
| tree | 56da28331b95eac727ac715c71293d1e3d254f7d /static/app.js | |
| parent | c00a4775106bc5927c821f5359ec265c318d73ec (diff) | |
| download | deck-it-a9d709f6dd893b6fab6750b67f0c4f367458f40c.tar.gz deck-it-a9d709f6dd893b6fab6750b67f0c4f367458f40c.zip | |
feat: vendored Gridstack for drag and free grid resize
Diffstat (limited to 'static/app.js')
| -rw-r--r-- | static/app.js | 135 |
1 files changed, 55 insertions, 80 deletions
diff --git a/static/app.js b/static/app.js index 526f57a..c6fb06e 100644 --- a/static/app.js +++ b/static/app.js @@ -1,71 +1,47 @@ -// The shell: discover modules, mount each panel, poll its state. -// A module is static/modules/<name>.js exporting default {title, interval, -// mount(el), update(state, el)}. A missing or broken panel is skipped. -// Panel order and width are kept in this browser's localStorage. +// The shell: discover modules, mount each panel as a Gridstack card, poll its +// state. A module is static/modules/<name>.js exporting default {title, +// interval, mount(el), update(state, el)}. Card position and size are kept in +// this browser's localStorage. -const grid = document.getElementById("grid"); +const gridEl = document.getElementById("grid"); const refreshers = []; const LAYOUT_KEY = "deckit.layout"; +const DEFAULT_W = 6; +const DEFAULT_H = 4; + +const grid = GridStack.init({ + column: 12, + cellHeight: 80, + margin: 8, + float: true, + handle: ".panel-drag", + resizable: { handles: "se" }, +}, gridEl); function loadLayout() { try { - return JSON.parse(localStorage.getItem(LAYOUT_KEY)) || {}; + const saved = JSON.parse(localStorage.getItem(LAYOUT_KEY)); + return Array.isArray(saved) ? saved : []; } catch { - return {}; + return []; } } -function saveLayout(layout) { +function saveLayout() { + if (!ready) return; // don't clobber the stored layout while the cards mount try { - localStorage.setItem(LAYOUT_KEY, JSON.stringify(layout)); + localStorage.setItem(LAYOUT_KEY, JSON.stringify(grid.save(false))); } catch { // private mode or storage full: the layout just won't persist } } -function panelOrder() { - return [...grid.querySelectorAll(".panel")].map((p) => p.dataset.module); -} +let ready = false; +grid.on("change", saveLayout); -function enableDrag(el) { - const handle = el.querySelector(".drag-handle"); - handle.addEventListener("pointerdown", (start) => { - start.preventDefault(); - el.classList.add("dragging"); - handle.setPointerCapture(start.pointerId); - const move = (ev) => { - const target = [...grid.querySelectorAll(".panel")].find((p) => { - if (p === el) return false; - const r = p.getBoundingClientRect(); - return ev.clientX >= r.left && ev.clientX <= r.right - && ev.clientY >= r.top && ev.clientY <= r.bottom; - }); - if (!target) return; - const r = target.getBoundingClientRect(); - const after = ev.clientY > r.top + r.height / 2; - grid.insertBefore(el, after ? target.nextSibling : target); - }; - const end = () => { - el.classList.remove("dragging"); - handle.releasePointerCapture(start.pointerId); - handle.removeEventListener("pointermove", move); - handle.removeEventListener("pointerup", end); - handle.removeEventListener("pointercancel", end); - saveLayout({ ...loadLayout(), order: panelOrder() }); - }; - handle.addEventListener("pointermove", move); - handle.addEventListener("pointerup", end); - handle.addEventListener("pointercancel", end); - }); -} - -function enableResize(el, name) { - el.querySelector(".resize-handle").addEventListener("click", () => { - el.classList.toggle("wide"); - const layout = loadLayout(); - layout.spans = { ...(layout.spans ?? {}), [name]: el.classList.contains("wide") ? 2 : 1 }; - saveLayout(layout); - }); +const savedById = {}; +for (const node of loadLayout()) { + if (node.id) savedById[node.id] = node; } function header(title) { @@ -74,34 +50,43 @@ function header(title) { const h = document.createElement("h2"); h.textContent = title; const drag = document.createElement("button"); - drag.className = "drag-handle"; + drag.className = "panel-drag"; drag.textContent = "≡"; drag.setAttribute("aria-label", "Move panel"); - const resize = document.createElement("button"); - resize.className = "resize-handle"; - resize.textContent = "⤢"; - resize.setAttribute("aria-label", "Toggle panel width"); - head.append(h, drag, resize); + head.append(h, drag); return head; } -async function mountModule(name, layout) { +function makeCard(name, title) { + const item = document.createElement("div"); + item.className = "grid-stack-item"; + item.setAttribute("gs-id", name); + const s = savedById[name]; + item.setAttribute("gs-w", String(s?.w ?? DEFAULT_W)); + item.setAttribute("gs-h", String(s?.h ?? DEFAULT_H)); + if (s) { + item.setAttribute("gs-x", String(s.x)); + item.setAttribute("gs-y", String(s.y)); + } + const content = document.createElement("div"); + content.className = "grid-stack-item-content panel"; + content.append(header(title)); + item.append(content); + gridEl.append(item); + grid.makeWidget(item); + return content; +} + +async function mountModule(name) { try { const panel = (await import(`./modules/${name}.js`)).default; - const el = document.createElement("section"); - el.className = "panel"; - el.dataset.module = name; - if ((layout.spans ?? {})[name] >= 2) el.classList.add("wide"); - el.append(header(panel.title ?? name)); - grid.append(el); - panel.mount?.(el); - enableDrag(el); - enableResize(el, name); + const content = makeCard(name, panel.title ?? name); + panel.mount?.(content); const poll = async () => { try { const r = await fetch(`/api/${name}`); - if (r.ok) panel.update?.(await r.json(), el); + if (r.ok) panel.update?.(await r.json(), content); } catch { // keep the last state; the next tick retries } @@ -117,18 +102,8 @@ async function mountModule(name, layout) { async function boot() { const r = await fetch("/api/modules"); if (!r.ok) return; - const layout = loadLayout(); - const order = layout.order ?? []; - const mods = await r.json(); - mods.sort((a, b) => { - const ia = order.indexOf(a.name); - const ib = order.indexOf(b.name); - if (ia === -1 && ib === -1) return 0; - if (ia === -1) return 1; - if (ib === -1) return -1; - return ia - ib; - }); - for (const { name } of mods) await mountModule(name, layout); + for (const { name } of await r.json()) await mountModule(name); + ready = true; } document.addEventListener("visibilitychange", () => { |
