244 lines
12 KiB
JavaScript
244 lines
12 KiB
JavaScript
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,
|
||
};
|
||
})();
|