This commit is contained in:
Mateusz Gruszczyński committed 2026-10-13 21:29:57 +02:00
1 parent bb025dcf44
commit b0f211555a
74 files changed
+6089 -556

No files matched your search

@@ -0,0 +1,200 @@
window.rustProxyInlineLayout = (() => {
const grid = $("dashboard-overview-grid");
const unlock = $("overview-layout-unlock");
const actions = $("overview-inline-actions");
const saveButton = $("overview-inline-save");
const cancelButton = $("overview-inline-cancel");
const hint = $("overview-inline-hint");
const hiddenShelf = $("overview-inline-hidden");
const hiddenList = $("overview-inline-hidden-list");
const hiddenCount = $("overview-inline-hidden-count");
const modalButton = $("overview-layout-open");
let draft = null;
let original = null;
let dragging = null;
let opening = false;
let saving = false;
const groupFor = id => window.rustProxyMasonry?.groupFor?.(id) ?? 0;
const visibleIds = () => draft.order.filter(id => !draft.hidden.has(id))
.sort((a, b) => groupFor(a) - groupFor(b));
const nodes = () => [...grid.children].filter(node => node.dataset.dashboardWidget);
const nodeFor = id => nodes().find(node => node.dataset.dashboardWidget === id);
const label = id => dashboardWidgetLabel(id);
const dirty = () => draft && original && (draft.order.some((id, i) => id !== original.order[i])
|| [...draft.hidden].some(id => !original.hidden.has(id)) || [...original.hidden].some(id => !draft.hidden.has(id)));
function renderHidden() {
if (!hiddenList || !draft) return;
const entries = draft.order.filter(id => draft.hidden.has(id));
if (hiddenCount) hiddenCount.textContent = `(${entries.length})`;
hiddenList.replaceChildren(...(entries.length ? entries.map(id => {
const add = control("+", `${t("overviewLayoutInlineRestore")}: ${label(id)}`, "overview-inline-restore", () => {
draft.hidden.delete(id);
paint();
nodeFor(id)?.querySelector(".overview-inline-grip")?.focus();
});
const item = el("div", undefined, "overview-inline-hidden-item");
item.append(el("span", label(id)), dashboardWidgetTypeBadge(id), add);
return item;
}) : [el("span", t("overviewLayoutInlineHiddenEmpty"), "fine")]));
}
function hide(id) {
if (!draft || !visibleIds().includes(id)) return;
draft.hidden.add(id);
paint();
if (hiddenShelf) { hiddenShelf.open = true; hiddenShelf.querySelector("summary")?.focus(); }
}
function paint() {
applyDashboardLayout(draft.order, [...draft.hidden]);
renderHidden();
const visible = visibleIds();
for (const node of nodes()) {
const i = visible.indexOf(node.dataset.dashboardWidget);
const up = node.querySelector(".overview-inline-up");
const down = node.querySelector(".overview-inline-down");
if (up) up.disabled = i <= 0 || groupFor(visible[i - 1]) !== groupFor(node.dataset.dashboardWidget);
if (down) down.disabled = i < 0 || i === visible.length - 1 ||
groupFor(visible[i + 1]) !== groupFor(node.dataset.dashboardWidget);
}
}
function clearDrag() {
dragging = null;
for (const node of nodes()) node.classList.remove("is-overview-dragging", "is-overview-drop-before", "is-overview-drop-after");
}
function move(id, target, after = false) {
if (!draft || id === target || groupFor(id) !== groupFor(target)) return;
const visible = visibleIds();
if (!visible.includes(id) || !visible.includes(target)) return;
visible.splice(visible.indexOf(id), 1);
visible.splice(visible.indexOf(target) + (after ? 1 : 0), 0, id);
const iter = visible.values();
draft.order = draft.order.map(item => draft.hidden.has(item) ? item : iter.next().value);
paint();
}
function nudge(id, delta) {
const visible = visibleIds();
const index = visible.indexOf(id), target = visible[index + delta];
if (!target || groupFor(id) !== groupFor(target)) return;
move(id, target, delta > 0);
nodeFor(id)?.querySelector(delta < 0 ? ".overview-inline-up" : ".overview-inline-down")?.focus();
}
function control(text, title, className, run) {
const button = el("button", text, `quiet ${className}`);
button.type = "button";
button.setAttribute("aria-label", title);
button.title = title;
button.addEventListener("click", run);
return button;
}
function toolbar(node) {
const id = node.dataset.dashboardWidget;
const title = label(id);
const bar = el("div", undefined, "overview-inline-toolbar");
const grip = control("⠿", `${t("overviewLayoutInlineDrag")}: ${title}`, "overview-inline-grip", () => { });
grip.draggable = true;
grip.dataset.overviewDragHandle = id;
bar.append(grip, dashboardWidgetTypeBadge(id),
control("←", `${t("moveUp")}: ${title}`, "overview-inline-up", () => nudge(id, -1)),
control("→", `${t("moveDown")}: ${title}`, "overview-inline-down", () => nudge(id, 1)),
control("×", `${t("overviewLayoutInlineHide")}: ${title}`, "overview-inline-hide", () => hide(id)));
node.append(bar);
}
function showEditing() {
grid.classList.add("is-unlocked");
unlock.setAttribute("aria-pressed", "true");
unlock.dataset.i18n = "overviewLayoutLock";
unlock.textContent = t("overviewLayoutLock");
actions.hidden = false;
hint.hidden = false;
hint.textContent = t("overviewLayoutInlineHint");
if (hiddenShelf) { hiddenShelf.hidden = false; hiddenShelf.open = false; }
if (modalButton) modalButton.disabled = true;
for (const node of nodes()) toolbar(node);
paint();
}
function finish(preserveSaved = false) {
clearDrag();
for (const node of nodes()) node.querySelector(".overview-inline-toolbar")?.remove();
grid.classList.remove("is-unlocked");
unlock.setAttribute("aria-pressed", "false");
unlock.dataset.i18n = "overviewLayoutUnlock";
unlock.textContent = t("overviewLayoutUnlock");
actions.hidden = true;
hint.hidden = true;
if (hiddenShelf) { hiddenShelf.open = false; hiddenShelf.hidden = true; }
if (hiddenList) hiddenList.replaceChildren();
if (modalButton) modalButton.disabled = false;
if (!preserveSaved && original) applyDashboardLayout(original.order, [...original.hidden]);
draft = original = null;
}
async function begin() {
if (draft || opening || saving || $("overview-layout-dialog")?.open) return;
opening = true;
unlock.disabled = true;
try {
await window.rustProxyPreferences?.load?.();
const saved = window.rustProxyPreferences?.current?.() || ADMIN_APPEARANCE_DEFAULTS;
const order = normalizeDashboardWidgets(saved.dashboard_widgets);
const hidden = new Set(normalizeHiddenDashboardWidgets(saved.dashboard_hidden_widgets, saved.dashboard_widgets));
original = { order: [...order], hidden: new Set(hidden) };
draft = { order: [...order], hidden: new Set(hidden) };
showEditing();
} catch (error) { if (error?.status !== 401) toast(error.message, true); }
finally { opening = false; unlock.disabled = false; }
}
async function save() {
if (!draft || saving) return;
if (!dirty()) { finish(true); return; }
saving = true;
saveButton.disabled = cancelButton.disabled = unlock.disabled = true;
try {
await window.rustProxyPreferences.update({
dashboard_widgets: [...draft.order], dashboard_hidden_widgets: [...draft.hidden],
});
finish(true);
toast(t("overviewLayoutEditorSaved"));
} catch (error) { hint.textContent = error.message || t("overviewLayoutInlineError"); }
finally { saving = false; saveButton.disabled = cancelButton.disabled = unlock.disabled = false; }
}
function cancel() { if (draft && !saving) finish(false); }
function dropTarget(event) {
const widget = event.target?.closest?.("[data-dashboard-widget]");
return widget && grid.contains(widget) && !widget.hidden && visibleIds().includes(widget.dataset.dashboardWidget) ? widget : null;
}
function positionAfter(widget, event) {
const rect = widget.getBoundingClientRect();
return event.clientX >= rect.left + rect.width / 2;
}
grid?.addEventListener("dragstart", event => {
const handle = event.target?.closest?.("[data-overview-drag-handle]");
if (!draft || !handle || !grid.contains(handle)) { event.preventDefault(); return; }
dragging = handle.dataset.overviewDragHandle;
event.dataTransfer?.setData("text/plain", dragging);
if (event.dataTransfer) event.dataTransfer.effectAllowed = "move";
nodeFor(dragging)?.classList.add("is-overview-dragging");
});
grid?.addEventListener("dragover", event => {
if (!draft || !dragging) return;
const target = dropTarget(event);
if (!target || target.dataset.dashboardWidget === dragging ||
groupFor(target.dataset.dashboardWidget) !== groupFor(dragging)) return;
event.preventDefault();
for (const node of nodes()) node.classList.remove("is-overview-drop-before", "is-overview-drop-after");
target.classList.add(positionAfter(target, event) ? "is-overview-drop-after" : "is-overview-drop-before");
if (event.dataTransfer) event.dataTransfer.dropEffect = "move";
});
grid?.addEventListener("drop", event => {
if (!draft || !dragging) return;
event.preventDefault();
const target = dropTarget(event), from = dragging;
clearDrag();
if (target && target.dataset.dashboardWidget !== from)
move(from, target.dataset.dashboardWidget, positionAfter(target, event));
});
grid?.addEventListener("dragend", clearDrag);
unlock?.addEventListener("click", () => { if (draft) void save(); else void begin(); });
saveButton?.addEventListener("click", () => { void save(); });
cancelButton?.addEventListener("click", cancel);
return { isEditing: () => !!draft, cancel };
})();