aboutsummaryrefslogtreecommitdiffstats
path: root/static/app.js
diff options
context:
space:
mode:
Diffstat (limited to 'static/app.js')
-rw-r--r--static/app.js135
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", () => {