Files
RustProxy/src/admin/web/app/dashboard-inline-layout.js
T
2026-10-19 11:09:48 +02:00

244 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 visibleIds = () => dashboardVisualOrder(draft.order, draft.sizes)
.filter(id => !draft.hidden.has(id));
function enterFreeLayout() {
if (draft.sizes === null) {
draft.order = dashboardVisualOrder(draft.order, null);
draft.sizes = {};
}
}
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))
|| JSON.stringify(draft.sizes) !== JSON.stringify(original.sizes));
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], draft.sizes);
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;
if (down) down.disabled = i < 0 || i === visible.length - 1;
const widths = dashboardAllowedWidths(node.dataset.dashboardWidget);
const span = dashboardWidgetWidth(node.dataset.dashboardWidget, draft.sizes);
const at = widths.indexOf(span);
const value = node.querySelector(".overview-inline-width-value");
if (value) value.textContent = `${span}/12`;
const narrow = node.querySelector(".overview-inline-narrow");
const widen = node.querySelector(".overview-inline-widen");
if (narrow) narrow.disabled = at <= 0;
if (widen) widen.disabled = at === widths.length - 1;
}
}
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) return;
if (!visibleIds().includes(id) || !visibleIds().includes(target)) return;
enterFreeLayout();
const visible = visibleIds();
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 resize(id, delta) {
if (!draft) return;
const widths = dashboardAllowedWidths(id);
const next = widths[widths.indexOf(dashboardWidgetWidth(id, draft.sizes)) + delta];
if (next === undefined) return;
enterFreeLayout();
if (next === dashboardDefaultWidth(id)) delete draft.sizes[id];
else draft.sizes[id] = next;
paint();
nodeFor(id)?.querySelector(delta < 0 ? ".overview-inline-narrow" : ".overview-inline-widen")?.focus();
}
function nudge(id, delta) {
const visible = visibleIds();
const index = visible.indexOf(id), target = visible[index + delta];
if (!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;
const sizeControls = el("span", undefined, "overview-inline-width");
const widthValue = el("span", "", "overview-inline-width-value");
widthValue.setAttribute("aria-label", t("overviewLayoutWidth"));
sizeControls.append(
control("−", `${t("overviewLayoutNarrow")}: ${title}`, "overview-inline-narrow", () => resize(id, -1)),
widthValue,
control("+", `${t("overviewLayoutWiden")}: ${title}`, "overview-inline-widen", () => resize(id, 1)));
bar.append(grip, dashboardWidgetTypeBadge(id), sizeControls,
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");
unlock.hidden = true; // Save and lock is the only save action while unlocked.
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");
unlock.hidden = false;
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], original.sizes);
draft = original = null;
}
async function begin() {
if (draft || opening || saving || $("overview-presets-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));
const sizes = normalizeDashboardWidgetSizes(saved.dashboard_widget_sizes);
original = { order: [...order], hidden: new Set(hidden), sizes: sizes === null ? null : { ...sizes } };
draft = { order: [...order], hidden: new Set(hidden), sizes: sizes === null ? null : { ...sizes } };
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(false); return; }
saving = true;
saveButton.disabled = cancelButton.disabled = unlock.disabled = true;
try {
await window.rustProxyPreferences.update({
dashboard_widgets: [...draft.order], dashboard_hidden_widgets: [...draft.hidden],
dashboard_widget_sizes: draft.sizes === null ? null : { ...draft.sizes },
});
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) 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,
currentDraft: () => draft ? {
order: [...draft.order], hidden: [...draft.hidden],
sizes: draft.sizes === null ? null : { ...draft.sizes },
} : null,
cancel,
};
})();