mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-09-25 09:53:08 +02:00
feat(admin): cleanup page — remove 'Fichiers temporaires' level, promote sections to h2 TOC entries
This commit is contained in:
@@ -5,12 +5,8 @@
|
||||
* All functions are attached to `window` for onclick handlers in PHP templates.
|
||||
*/
|
||||
(() => {
|
||||
function cleanupToggleAll(src, group) {
|
||||
const selector =
|
||||
group === "filepond"
|
||||
? 'input[name="filepond_dirs[]"]'
|
||||
: 'input[name="trash_files[]"]';
|
||||
document.querySelectorAll(selector).forEach((cb) => {
|
||||
function cleanupToggleAll(src) {
|
||||
document.querySelectorAll('input[name="trash_files[]"]').forEach((cb) => {
|
||||
cb.checked = src.checked;
|
||||
});
|
||||
cleanupUpdateBulk();
|
||||
@@ -23,7 +19,7 @@
|
||||
if (!bar || !countEl) return;
|
||||
|
||||
const allChecked = document.querySelectorAll(
|
||||
'input[name="filepond_dirs[]"]:checked, input[name="trash_files[]"]:checked',
|
||||
'input[name="trash_files[]"]:checked',
|
||||
);
|
||||
const n = allChecked.length;
|
||||
|
||||
@@ -48,13 +44,11 @@
|
||||
// Populate hidden inputs from checked checkboxes
|
||||
container.innerHTML = "";
|
||||
document
|
||||
.querySelectorAll(
|
||||
'input[name="filepond_dirs[]"]:checked, input[name="trash_files[]"]:checked',
|
||||
)
|
||||
.querySelectorAll('input[name="trash_files[]"]:checked')
|
||||
.forEach((cb) => {
|
||||
const inp = document.createElement("input");
|
||||
inp.type = "hidden";
|
||||
inp.name = cb.name; // "filepond_dirs[]" or "trash_files[]"
|
||||
inp.name = cb.name; // "trash_files[]"
|
||||
inp.value = cb.value;
|
||||
container.appendChild(inp);
|
||||
});
|
||||
@@ -65,13 +59,17 @@
|
||||
|
||||
function cleanupBulkRestore() {
|
||||
const form = document.getElementById("cleanup-bulk-restore-form");
|
||||
const container = document.getElementById("cleanup-bulk-restore-checkboxes");
|
||||
const container = document.getElementById(
|
||||
"cleanup-bulk-restore-checkboxes",
|
||||
);
|
||||
if (!form || !container) return;
|
||||
|
||||
container.innerHTML = "";
|
||||
let count = 0;
|
||||
document
|
||||
.querySelectorAll('input[name="trash_files[]"]:checked[data-restorable="1"]')
|
||||
.querySelectorAll(
|
||||
'input[name="trash_files[]"]:checked[data-restorable="1"]',
|
||||
)
|
||||
.forEach((cb) => {
|
||||
const inp = document.createElement("input");
|
||||
inp.type = "hidden";
|
||||
@@ -83,19 +81,16 @@
|
||||
|
||||
if (count === 0) return;
|
||||
|
||||
if (!confirm(`Restaurer ${count} fichier(s) vers leur(s) TFE associé(s) ?`)) return;
|
||||
if (!confirm(`Restaurer ${count} fichier(s) vers leur(s) TFE associé(s) ?`))
|
||||
return;
|
||||
|
||||
htmx.trigger(form, "submit");
|
||||
}
|
||||
|
||||
function reattachListeners() {
|
||||
document
|
||||
.querySelectorAll(
|
||||
'input[name="filepond_dirs[]"], input[name="trash_files[]"]',
|
||||
)
|
||||
.forEach((cb) => {
|
||||
cb.addEventListener("change", cleanupUpdateBulk);
|
||||
});
|
||||
document.querySelectorAll('input[name="trash_files[]"]').forEach((cb) => {
|
||||
cb.addEventListener("change", cleanupUpdateBulk);
|
||||
});
|
||||
cleanupUpdateBulk();
|
||||
}
|
||||
|
||||
|
||||
@@ -7,63 +7,92 @@
|
||||
* section[aria-labelledby] headings inside #main-content.
|
||||
* Hides the TOC if fewer than 2 sections exist.
|
||||
*
|
||||
* Guarded: only runs once.
|
||||
* Rebuilds whenever HTMX swaps content so sections that are loaded
|
||||
* asynchronously (e.g. cleanup page fragments) appear in the TOC.
|
||||
*/
|
||||
(() => {
|
||||
if (window.__adminTocBuilt) return;
|
||||
window.__adminTocBuilt = true;
|
||||
let observer = null;
|
||||
let items = [];
|
||||
|
||||
// Generate a spread of thresholds for smooth IntersectionObserver firing.
|
||||
function buildThresholds() {
|
||||
const t = [];
|
||||
for (let i = 0; i <= 20; i++) t.push(i / 20);
|
||||
return t;
|
||||
}
|
||||
|
||||
function tearDown() {
|
||||
if (observer) {
|
||||
observer.disconnect();
|
||||
observer = null;
|
||||
}
|
||||
items = [];
|
||||
}
|
||||
|
||||
function build() {
|
||||
var main = document.getElementById("main-content");
|
||||
var toc = document.getElementById("admin-toc");
|
||||
var list = document.getElementById("admin-toc-list");
|
||||
const main = document.getElementById("main-content");
|
||||
const toc = document.getElementById("admin-toc");
|
||||
const list = document.getElementById("admin-toc-list");
|
||||
if (!main || !toc || !list) return;
|
||||
|
||||
// Guard against double population
|
||||
if (list.children.length > 0) return;
|
||||
tearDown();
|
||||
|
||||
var sections = main.querySelectorAll("section[aria-labelledby]");
|
||||
// Repopulate from scratch.
|
||||
list.textContent = "";
|
||||
|
||||
const sections = main.querySelectorAll("section[aria-labelledby]");
|
||||
if (sections.length < 2) {
|
||||
toc.hidden = true;
|
||||
return;
|
||||
}
|
||||
toc.hidden = false;
|
||||
|
||||
var items = [];
|
||||
sections.forEach((sec) => {
|
||||
var headingId = sec.getAttribute("aria-labelledby");
|
||||
var heading = document.getElementById(headingId);
|
||||
const headingId = sec.getAttribute("aria-labelledby");
|
||||
const heading = document.getElementById(headingId);
|
||||
if (!heading) return;
|
||||
if (!sec.id) sec.id = headingId;
|
||||
|
||||
var a = document.createElement("a");
|
||||
a.href = `#${sec.id}`;
|
||||
a.textContent = heading.textContent.trim();
|
||||
// Anchor the link to the heading id. Do NOT assign the heading id to
|
||||
// the <section> itself — that would create a duplicate id on rebuild
|
||||
// (getElementById would then return the section, whose textContent
|
||||
// drags in the whole table).
|
||||
const anchorId = headingId;
|
||||
|
||||
var li = document.createElement("li");
|
||||
// Label excludes .n-meta counters (e.g. “Corbeille 5 fichiers · 2.1 GB”
|
||||
// should read just “Corbeille” in the TOC).
|
||||
let label;
|
||||
if (heading.querySelector?.(".n-meta")) {
|
||||
const clone = heading.cloneNode(true);
|
||||
clone.querySelectorAll(".n-meta").forEach((n) => {
|
||||
n.remove();
|
||||
});
|
||||
label = clone.textContent.trim();
|
||||
} else {
|
||||
label = heading.textContent.trim();
|
||||
}
|
||||
|
||||
const a = document.createElement("a");
|
||||
a.href = `#${anchorId}`;
|
||||
a.textContent = label;
|
||||
|
||||
const li = document.createElement("li");
|
||||
li.appendChild(a);
|
||||
list.appendChild(li);
|
||||
items.push({ section: sec, link: a });
|
||||
});
|
||||
|
||||
// Generate a spread of thresholds for smooth IntersectionObserver firing.
|
||||
function buildThresholds() {
|
||||
var t = [];
|
||||
for (var i = 0; i <= 20; i++) t.push(i / 20);
|
||||
return t;
|
||||
}
|
||||
|
||||
var observer = new IntersectionObserver(
|
||||
observer = new IntersectionObserver(
|
||||
() => {
|
||||
// Active section = whose top is closest to the midpoint of the
|
||||
// viewport from above. At page bottom, the last section wins
|
||||
// even if its heading can't reach 50%.
|
||||
var line = window.innerHeight * 0.5;
|
||||
var best = null;
|
||||
var bestDist = Infinity;
|
||||
const line = window.innerHeight * 0.5;
|
||||
let best = null;
|
||||
let bestDist = Infinity;
|
||||
|
||||
// Pass 1: sections whose top is above/at the line
|
||||
items.forEach((item) => {
|
||||
var top = item.section.getBoundingClientRect().top;
|
||||
const top = item.section.getBoundingClientRect().top;
|
||||
if (top <= line && line - top < bestDist) {
|
||||
bestDist = line - top;
|
||||
best = item;
|
||||
@@ -73,8 +102,8 @@
|
||||
// Pass 2: no section above the line — pick closest from below
|
||||
if (!best) {
|
||||
items.forEach((item) => {
|
||||
var top = item.section.getBoundingClientRect().top;
|
||||
var dist = top - line;
|
||||
const top = item.section.getBoundingClientRect().top;
|
||||
const dist = top - line;
|
||||
if (dist >= 0 && dist < bestDist) {
|
||||
bestDist = dist;
|
||||
best = item;
|
||||
@@ -85,10 +114,10 @@
|
||||
// Pass 3: at page bottom, if the last section is visible but
|
||||
// its heading never crossed 50%, promote it anyway.
|
||||
if (best) {
|
||||
var lastItem = items[items.length - 1];
|
||||
const lastItem = items[items.length - 1];
|
||||
if (lastItem !== best) {
|
||||
var lastTop = lastItem.section.getBoundingClientRect().top;
|
||||
var atBottom =
|
||||
const lastTop = lastItem.section.getBoundingClientRect().top;
|
||||
const atBottom =
|
||||
window.innerHeight + window.scrollY + 5 >=
|
||||
document.documentElement.scrollHeight;
|
||||
if (atBottom && lastTop > line && lastTop < window.innerHeight) {
|
||||
@@ -110,9 +139,28 @@
|
||||
});
|
||||
}
|
||||
|
||||
function run() {
|
||||
if (document.getElementById("admin-toc")) build();
|
||||
}
|
||||
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", build);
|
||||
document.addEventListener("DOMContentLoaded", run);
|
||||
} else {
|
||||
build();
|
||||
run();
|
||||
}
|
||||
|
||||
// HTMX loads many admin sections asynchronously (cleanup fragments, tables).
|
||||
// Rebuild the TOC whenever the content around it changes.
|
||||
if (window.htmx) {
|
||||
document.body.addEventListener("htmx:afterSwap", (e) => {
|
||||
if (e.target?.closest?.("#main-content")) {
|
||||
run();
|
||||
}
|
||||
});
|
||||
document.body.addEventListener("htmx:afterSettle", (e) => {
|
||||
if (e.target?.closest?.("#main-content")) {
|
||||
run();
|
||||
}
|
||||
});
|
||||
}
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user