Proposer: robust gitea fallback + safe gate + debug
This commit is contained in:
@@ -373,11 +373,37 @@ const GITEA_REPO = import.meta.env.PUBLIC_GITEA_REPO ?? "";
|
|||||||
updateResumeButton();
|
updateResumeButton();
|
||||||
|
|
||||||
// ==========================
|
// ==========================
|
||||||
// Gitea propose (optional)
|
// Gitea propose (optional, antifragile)
|
||||||
// ==========================
|
// ==========================
|
||||||
const giteaReady = Boolean(GITEA_BASE && GITEA_OWNER && GITEA_REPO);
|
function debugOn() {
|
||||||
const FULL_TEXT_SOFT_LIMIT = 1600;
|
try {
|
||||||
const URL_HARD_LIMIT = 6500;
|
const u = new URL(window.location.href);
|
||||||
|
return u.searchParams.get("debug") === "1" || localStorage.getItem("archicratie:debug") === "1";
|
||||||
|
} catch { return false; }
|
||||||
|
}
|
||||||
|
const DEBUG = debugOn();
|
||||||
|
|
||||||
|
// Fallbacks (si build args non injectés, on évite "giteaReady=false -> plus de Proposer")
|
||||||
|
const FALLBACK = (() => {
|
||||||
|
const host = (window.location.hostname || "").toLowerCase();
|
||||||
|
// prod/staging : on déduit gitea du domaine
|
||||||
|
if (host.endsWith("archicratie.trans-hands.synology.me")) {
|
||||||
|
return {
|
||||||
|
base: "https://gitea.archicratie.trans-hands.synology.me",
|
||||||
|
owner: "Archicratia",
|
||||||
|
repo: "archicratie-edition",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return { base: "", owner: "", repo: "" };
|
||||||
|
})();
|
||||||
|
|
||||||
|
const GITEA = {
|
||||||
|
base: String(GITEA_BASE || FALLBACK.base || "").trim(),
|
||||||
|
owner: String(GITEA_OWNER || FALLBACK.owner || "").trim(),
|
||||||
|
repo: String(GITEA_REPO || FALLBACK.repo || "").trim(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const giteaReady = Boolean(GITEA.base && GITEA.owner && GITEA.repo);
|
||||||
|
|
||||||
// ✅ Gate par groupe (Traefik→Authelia→LLDAP via /_auth/whoami)
|
// ✅ Gate par groupe (Traefik→Authelia→LLDAP via /_auth/whoami)
|
||||||
const WHOAMI_PATH = "/_auth/whoami";
|
const WHOAMI_PATH = "/_auth/whoami";
|
||||||
@@ -391,16 +417,26 @@ const GITEA_REPO = import.meta.env.PUBLIC_GITEA_REPO ?? "";
|
|||||||
return (m?.[1] ?? "").trim();
|
return (m?.[1] ?? "").trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function fetchWithTimeout(url, ms = 4500) {
|
||||||
|
const ac = new AbortController();
|
||||||
|
const t = setTimeout(() => ac.abort(), ms);
|
||||||
|
try {
|
||||||
|
return await fetch(url, {
|
||||||
|
credentials: "include",
|
||||||
|
cache: "no-store",
|
||||||
|
redirect: "follow",
|
||||||
|
signal: ac.signal,
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
clearTimeout(t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function getAuthInfo() {
|
async function getAuthInfo() {
|
||||||
if (_authInfoPromise) return _authInfoPromise;
|
if (_authInfoPromise) return _authInfoPromise;
|
||||||
|
|
||||||
_authInfoPromise = (async () => {
|
_authInfoPromise = (async () => {
|
||||||
const res = await fetch(`${WHOAMI_PATH}?_=${Date.now()}`, {
|
const res = await fetchWithTimeout(`${WHOAMI_PATH}?_=${Date.now()}`, 4500);
|
||||||
credentials: "include",
|
|
||||||
cache: "no-store",
|
|
||||||
redirect: "follow",
|
|
||||||
});
|
|
||||||
|
|
||||||
const text = await res.text().catch(() => "");
|
const text = await res.text().catch(() => "");
|
||||||
|
|
||||||
const groups = parseWhoamiLine(text, "Remote-Groups")
|
const groups = parseWhoamiLine(text, "Remote-Groups")
|
||||||
@@ -408,7 +444,7 @@ const GITEA_REPO = import.meta.env.PUBLIC_GITEA_REPO ?? "";
|
|||||||
.map((s) => s.trim())
|
.map((s) => s.trim())
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
|
|
||||||
return {
|
const info = {
|
||||||
ok: Boolean(res && res.ok),
|
ok: Boolean(res && res.ok),
|
||||||
user: parseWhoamiLine(text, "Remote-User"),
|
user: parseWhoamiLine(text, "Remote-User"),
|
||||||
name: parseWhoamiLine(text, "Remote-Name"),
|
name: parseWhoamiLine(text, "Remote-Name"),
|
||||||
@@ -416,22 +452,44 @@ const GITEA_REPO = import.meta.env.PUBLIC_GITEA_REPO ?? "";
|
|||||||
groups,
|
groups,
|
||||||
raw: text,
|
raw: text,
|
||||||
};
|
};
|
||||||
})().catch(() => ({
|
|
||||||
ok: false,
|
// Debug safe
|
||||||
user: "",
|
try {
|
||||||
name: "",
|
window.__archicratie = window.__archicratie || {};
|
||||||
email: "",
|
window.__archicratie.auth = { ok: info.ok, user: info.user, groups: info.groups };
|
||||||
groups: [],
|
} catch {}
|
||||||
raw: "",
|
|
||||||
}));
|
return info;
|
||||||
|
})().catch((err) => {
|
||||||
|
if (DEBUG) console.warn("[proposer] whoami failed", err);
|
||||||
|
try {
|
||||||
|
window.__archicratie = window.__archicratie || {};
|
||||||
|
window.__archicratie.auth = { ok: false, user: "", groups: [] };
|
||||||
|
} catch {}
|
||||||
|
return { ok: false, user: "", name: "", email: "", groups: [], raw: "" };
|
||||||
|
});
|
||||||
|
|
||||||
return _authInfoPromise;
|
return _authInfoPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Promise unique : est-on editor ?
|
// true/false/null (null = indéterminé => on laisse caché)
|
||||||
const isEditorP = giteaReady
|
const isEditorP = giteaReady
|
||||||
? getAuthInfo().then((info) => info.groups.includes(PROPOSE_REQUIRED_GROUP)).catch(() => false)
|
? getAuthInfo().then((info) => {
|
||||||
: Promise.resolve(false);
|
if (!info || !info.ok) return null;
|
||||||
|
return info.groups.includes(PROPOSE_REQUIRED_GROUP);
|
||||||
|
}).catch(() => null)
|
||||||
|
: Promise.resolve(null);
|
||||||
|
|
||||||
|
if (DEBUG) {
|
||||||
|
try {
|
||||||
|
window.__archicratie = window.__archicratie || {};
|
||||||
|
window.__archicratie.gitea = { ...GITEA, ready: giteaReady };
|
||||||
|
} catch {}
|
||||||
|
console.log("[proposer] giteaReady=", giteaReady, GITEA);
|
||||||
|
}
|
||||||
|
|
||||||
|
const FULL_TEXT_SOFT_LIMIT = 1600;
|
||||||
|
const URL_HARD_LIMIT = 6500;
|
||||||
|
|
||||||
const quoteBlock = (s) =>
|
const quoteBlock = (s) =>
|
||||||
String(s || "")
|
String(s || "")
|
||||||
@@ -440,8 +498,8 @@ const GITEA_REPO = import.meta.env.PUBLIC_GITEA_REPO ?? "";
|
|||||||
.join("\n");
|
.join("\n");
|
||||||
|
|
||||||
function buildIssueURL(anchorId, fullText, excerpt) {
|
function buildIssueURL(anchorId, fullText, excerpt) {
|
||||||
const base = String(GITEA_BASE).replace(/\/+$/, "");
|
const base = String(GITEA.base).replace(/\/+$/, "");
|
||||||
const issue = new URL(`${base}/${GITEA_OWNER}/${GITEA_REPO}/issues/new`);
|
const issue = new URL(`${base}/${GITEA.owner}/${GITEA.repo}/issues/new`);
|
||||||
|
|
||||||
const local = new URL(window.location.href);
|
const local = new URL(window.location.href);
|
||||||
local.search = "";
|
local.search = "";
|
||||||
@@ -501,6 +559,23 @@ const GITEA_REPO = import.meta.env.PUBLIC_GITEA_REPO ?? "";
|
|||||||
// ==========================
|
// ==========================
|
||||||
const paras = Array.from(document.querySelectorAll('.reading p[id^="p-"]'));
|
const paras = Array.from(document.querySelectorAll('.reading p[id^="p-"]'));
|
||||||
|
|
||||||
|
function hidePropose(el) {
|
||||||
|
try {
|
||||||
|
el.hidden = true;
|
||||||
|
el.style.display = "none";
|
||||||
|
el.setAttribute("aria-hidden", "true");
|
||||||
|
el.setAttribute("tabindex", "-1");
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
function showPropose(el) {
|
||||||
|
try {
|
||||||
|
el.hidden = false;
|
||||||
|
el.style.display = "";
|
||||||
|
el.removeAttribute("aria-hidden");
|
||||||
|
el.removeAttribute("tabindex");
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
for (const p of paras) {
|
for (const p of paras) {
|
||||||
if (p.querySelector(".para-tools")) continue;
|
if (p.querySelector(".para-tools")) continue;
|
||||||
|
|
||||||
@@ -547,24 +622,28 @@ const GITEA_REPO = import.meta.env.PUBLIC_GITEA_REPO ?? "";
|
|||||||
propose.textContent = "Proposer";
|
propose.textContent = "Proposer";
|
||||||
propose.setAttribute("aria-label", "Proposer une correction sur Gitea");
|
propose.setAttribute("aria-label", "Proposer une correction sur Gitea");
|
||||||
|
|
||||||
// ✅ fail-closed DUR : inline-style (ne peut pas être overridé par ton CSS)
|
|
||||||
propose.style.display = "none";
|
|
||||||
propose.dataset.requiresGroup = PROPOSE_REQUIRED_GROUP;
|
propose.dataset.requiresGroup = PROPOSE_REQUIRED_GROUP;
|
||||||
|
hidePropose(propose);
|
||||||
|
|
||||||
const raw = (p.textContent || "").trim().replace(/\s+/g, " ");
|
const raw = (p.textContent || "").trim().replace(/\s+/g, " ");
|
||||||
const excerpt = raw.length > 420 ? (raw.slice(0, 420) + "…") : raw;
|
const excerpt = raw.length > 420 ? (raw.slice(0, 420) + "…") : raw;
|
||||||
|
|
||||||
const issueUrl = buildIssueURL(p.id, raw, excerpt);
|
const issueUrl = buildIssueURL(p.id, raw, excerpt);
|
||||||
|
|
||||||
// Lien fallback (si JS casse totalement)
|
// Lien fallback (si la modale casse)
|
||||||
propose.href = issueUrl;
|
propose.href = issueUrl;
|
||||||
|
|
||||||
// ✅ Marqueurs pour ProposeModal (interception 2 étapes)
|
// Marqueurs pour ProposeModal (interception 2 étapes)
|
||||||
propose.dataset.propose = "1";
|
propose.dataset.propose = "1";
|
||||||
propose.dataset.url = issueUrl;
|
propose.dataset.url = issueUrl;
|
||||||
propose.dataset.full = raw;
|
propose.dataset.full = raw;
|
||||||
|
|
||||||
row.appendChild(propose);
|
row.appendChild(propose);
|
||||||
|
} else if (DEBUG) {
|
||||||
|
// debug : on marque pourquoi il n'y a pas de proposer
|
||||||
|
// (aucun impact prod)
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.warn("[proposer] disabled: giteaReady=false", GITEA_BASE, GITEA_OWNER, GITEA_REPO);
|
||||||
}
|
}
|
||||||
|
|
||||||
tools.appendChild(row);
|
tools.appendChild(row);
|
||||||
@@ -599,17 +678,20 @@ const GITEA_REPO = import.meta.env.PUBLIC_GITEA_REPO ?? "";
|
|||||||
p.appendChild(tools);
|
p.appendChild(tools);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ Après insertion : on autorise Proposer seulement si groupe editors
|
// ✅ Gate : on montre uniquement si ok===true. Si ok===false on supprime. Si null => on laisse caché (non destructif).
|
||||||
if (giteaReady) {
|
if (giteaReady) {
|
||||||
isEditorP.then((ok) => {
|
isEditorP.then((ok) => {
|
||||||
|
if (DEBUG) console.log("[proposer] isEditor=", ok);
|
||||||
const els = document.querySelectorAll(".para-propose");
|
const els = document.querySelectorAll(".para-propose");
|
||||||
|
|
||||||
for (const el of els) {
|
for (const el of els) {
|
||||||
if (ok) el.style.display = "";
|
if (ok === true) showPropose(el);
|
||||||
else el.remove();
|
else if (ok === false) el.remove();
|
||||||
|
else hidePropose(el); // indéterminé => on ne casse rien
|
||||||
}
|
}
|
||||||
}).catch(() => {
|
}).catch((err) => {
|
||||||
// fail-closed
|
if (DEBUG) console.warn("[proposer] gate failed", err);
|
||||||
document.querySelectorAll(".para-propose").forEach((el) => el.remove());
|
document.querySelectorAll(".para-propose").forEach((el) => hidePropose(el));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -654,7 +736,6 @@ const GITEA_REPO = import.meta.env.PUBLIC_GITEA_REPO ?? "";
|
|||||||
// ==========================
|
// ==========================
|
||||||
const h1 = reading ? reading.querySelector("h1") : null;
|
const h1 = reading ? reading.querySelector("h1") : null;
|
||||||
|
|
||||||
// H2 : soit via details-anchor (pour citabilité stable), soit via h2[id]
|
|
||||||
const h2Anchors = Array.from(document.querySelectorAll("span.details-anchor[id]"))
|
const h2Anchors = Array.from(document.querySelectorAll("span.details-anchor[id]"))
|
||||||
.map((s) => {
|
.map((s) => {
|
||||||
const d = s.closest("details");
|
const d = s.closest("details");
|
||||||
@@ -673,7 +754,6 @@ const GITEA_REPO = import.meta.env.PUBLIC_GITEA_REPO ?? "";
|
|||||||
h2
|
h2
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// On préfère le mode "anchors" s'il existe
|
|
||||||
const H2 = (h2Anchors.length ? h2Anchors : h2Plain)
|
const H2 = (h2Anchors.length ? h2Anchors : h2Plain)
|
||||||
.slice()
|
.slice()
|
||||||
.sort((a,b) => absTop(a.marker) - absTop(b.marker));
|
.sort((a,b) => absTop(a.marker) - absTop(b.marker));
|
||||||
@@ -686,7 +766,6 @@ const GITEA_REPO = import.meta.env.PUBLIC_GITEA_REPO ?? "";
|
|||||||
let lastY = window.scrollY || 0;
|
let lastY = window.scrollY || 0;
|
||||||
|
|
||||||
function computeLineY(followH) {
|
function computeLineY(followH) {
|
||||||
// “ligne” = bas du bandeau (header + page-gap + bandeau)
|
|
||||||
return headerH() + PAGE_GAP + (followH || 0) + HYST;
|
return headerH() + PAGE_GAP + (followH || 0) + HYST;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -712,24 +791,21 @@ const GITEA_REPO = import.meta.env.PUBLIC_GITEA_REPO ?? "";
|
|||||||
return Boolean(start.compareDocumentPosition(el) & Node.DOCUMENT_POSITION_FOLLOWING);
|
return Boolean(start.compareDocumentPosition(el) & Node.DOCUMENT_POSITION_FOLLOWING);
|
||||||
}
|
}
|
||||||
function isBefore(end, el) {
|
function isBefore(end, el) {
|
||||||
if (!end || !el) return true; // pas de borne haute => OK
|
if (!end || !el) return true;
|
||||||
return Boolean(end.compareDocumentPosition(el) & Node.DOCUMENT_POSITION_PRECEDING);
|
return Boolean(end.compareDocumentPosition(el) & Node.DOCUMENT_POSITION_PRECEDING);
|
||||||
}
|
}
|
||||||
|
|
||||||
function pickH3(lineY, activeH2) {
|
function pickH3(lineY, activeH2) {
|
||||||
if (!activeH2) return null;
|
if (!activeH2) return null;
|
||||||
|
|
||||||
// H3 visibles (pas dans un details fermé)
|
|
||||||
const visible = H3.filter((t) => {
|
const visible = H3.filter((t) => {
|
||||||
const d = t.el.closest?.("details");
|
const d = t.el.closest?.("details");
|
||||||
return !(d && !d.open);
|
return !(d && !d.open);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Scope : même <details> si applicable (plus robuste)
|
|
||||||
const d2 = activeH2.h2?.closest?.("details") || activeH2.anchor?.closest?.("details") || null;
|
const d2 = activeH2.h2?.closest?.("details") || activeH2.anchor?.closest?.("details") || null;
|
||||||
let scoped = d2 ? visible.filter((t) => t.el.closest?.("details") === d2) : visible;
|
let scoped = d2 ? visible.filter((t) => t.el.closest?.("details") === d2) : visible;
|
||||||
|
|
||||||
// Scope DOM : entre H2 courant et H2 suivant (gère aussi les pages "plates")
|
|
||||||
const i2 = H2.findIndex((t) => t.id === activeH2.id);
|
const i2 = H2.findIndex((t) => t.id === activeH2.id);
|
||||||
const nextH2 = (i2 >= 0 && i2 + 1 < H2.length) ? H2[i2 + 1] : null;
|
const nextH2 = (i2 >= 0 && i2 + 1 < H2.length) ? H2[i2 + 1] : null;
|
||||||
|
|
||||||
@@ -738,7 +814,6 @@ const GITEA_REPO = import.meta.env.PUBLIC_GITEA_REPO ?? "";
|
|||||||
|
|
||||||
scoped = scoped.filter((t) => isAfter(startNode, t.el) && isBefore(endNode, t.el));
|
scoped = scoped.filter((t) => isAfter(startNode, t.el) && isBefore(endNode, t.el));
|
||||||
|
|
||||||
// Choisir le dernier H3 du scope qui a passé la ligne
|
|
||||||
let cand = null;
|
let cand = null;
|
||||||
for (const t of scoped) {
|
for (const t of scoped) {
|
||||||
const top = t.el.getBoundingClientRect().top;
|
const top = t.el.getBoundingClientRect().top;
|
||||||
@@ -751,7 +826,6 @@ const GITEA_REPO = import.meta.env.PUBLIC_GITEA_REPO ?? "";
|
|||||||
function updateFollow() {
|
function updateFollow() {
|
||||||
if (!followEl) return;
|
if (!followEl) return;
|
||||||
|
|
||||||
// PASS 1 : H1 visible quand le H1 du reading passe sous la barre
|
|
||||||
const baseY = headerH() + PAGE_GAP;
|
const baseY = headerH() + PAGE_GAP;
|
||||||
const showH1 = !!(h1 && (h1.getBoundingClientRect().bottom <= baseY + HYST));
|
const showH1 = !!(h1 && (h1.getBoundingClientRect().bottom <= baseY + HYST));
|
||||||
|
|
||||||
@@ -762,7 +836,6 @@ const GITEA_REPO = import.meta.env.PUBLIC_GITEA_REPO ?? "";
|
|||||||
rfH1.hidden = true;
|
rfH1.hidden = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// PASS 2 : estimation H2/H3 sans bandeau (pour mesurer hauteur réelle)
|
|
||||||
const lineY0 = computeLineY(0);
|
const lineY0 = computeLineY(0);
|
||||||
curH2 = pickH2(lineY0);
|
curH2 = pickH2(lineY0);
|
||||||
curH3 = pickH3(lineY0, curH2);
|
curH3 = pickH3(lineY0, curH2);
|
||||||
@@ -780,19 +853,16 @@ const GITEA_REPO = import.meta.env.PUBLIC_GITEA_REPO ?? "";
|
|||||||
const inner = followEl.querySelector(".reading-follow__inner");
|
const inner = followEl.querySelector(".reading-follow__inner");
|
||||||
const followH = (any0 && inner) ? inner.getBoundingClientRect().height : 0;
|
const followH = (any0 && inner) ? inner.getBoundingClientRect().height : 0;
|
||||||
|
|
||||||
// PASS 3 : recalc H2/H3 avec la vraie ligne (bas du bandeau)
|
|
||||||
const lineY = computeLineY(followH);
|
const lineY = computeLineY(followH);
|
||||||
curH2 = pickH2(lineY);
|
curH2 = pickH2(lineY);
|
||||||
curH3 = pickH3(lineY, curH2);
|
curH3 = pickH3(lineY, curH2);
|
||||||
|
|
||||||
// H2 n’apparaît que s’il est “actif” (titre passé sous la ligne)
|
|
||||||
if (rfH2 && curH2) {
|
if (rfH2 && curH2) {
|
||||||
const on = curH2.marker.getBoundingClientRect().top <= lineY;
|
const on = curH2.marker.getBoundingClientRect().top <= lineY;
|
||||||
rfH2.hidden = !on;
|
rfH2.hidden = !on;
|
||||||
if (on) rfH2.textContent = curH2.title;
|
if (on) rfH2.textContent = curH2.title;
|
||||||
} else if (rfH2) rfH2.hidden = true;
|
} else if (rfH2) rfH2.hidden = true;
|
||||||
|
|
||||||
// H3 : actif seulement si un H3 du H2 actif a passé la ligne
|
|
||||||
if (rfH3 && curH3) {
|
if (rfH3 && curH3) {
|
||||||
const on = curH3.el.getBoundingClientRect().top <= lineY;
|
const on = curH3.el.getBoundingClientRect().top <= lineY;
|
||||||
rfH3.hidden = !on;
|
rfH3.hidden = !on;
|
||||||
@@ -809,17 +879,13 @@ const GITEA_REPO = import.meta.env.PUBLIC_GITEA_REPO ?? "";
|
|||||||
setRootVar("--followbar-h", px(followH2));
|
setRootVar("--followbar-h", px(followH2));
|
||||||
setRootVar("--sticky-offset-px", String(Math.round(headerH() + PAGE_GAP + followH2)));
|
setRootVar("--sticky-offset-px", String(Math.round(headerH() + PAGE_GAP + followH2)));
|
||||||
|
|
||||||
// Boutons actions
|
|
||||||
if (btnTopChapter) btnTopChapter.hidden = !any;
|
if (btnTopChapter) btnTopChapter.hidden = !any;
|
||||||
|
|
||||||
if (btnTopSection) {
|
if (btnTopSection) {
|
||||||
const ok = Boolean(curH2 && rfH2 && !rfH2.hidden);
|
const ok = Boolean(curH2 && rfH2 && !rfH2.hidden);
|
||||||
btnTopSection.hidden = !ok;
|
btnTopSection.hidden = !ok;
|
||||||
}
|
}
|
||||||
|
|
||||||
lastY = window.scrollY || 0;
|
lastY = window.scrollY || 0;
|
||||||
|
|
||||||
// recalage horizontal
|
|
||||||
syncReadingRect();
|
syncReadingRect();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -836,7 +902,6 @@ const GITEA_REPO = import.meta.env.PUBLIC_GITEA_REPO ?? "";
|
|||||||
window.addEventListener("scroll", onScroll, { passive: true });
|
window.addEventListener("scroll", onScroll, { passive: true });
|
||||||
window.addEventListener("resize", onScroll);
|
window.addEventListener("resize", onScroll);
|
||||||
|
|
||||||
// clics bandeau
|
|
||||||
if (rfH1) rfH1.addEventListener("click", () => {
|
if (rfH1) rfH1.addEventListener("click", () => {
|
||||||
if (!h1) return;
|
if (!h1) return;
|
||||||
scrollToElWithOffset(h1, 12, "smooth");
|
scrollToElWithOffset(h1, 12, "smooth");
|
||||||
@@ -856,7 +921,6 @@ const GITEA_REPO = import.meta.env.PUBLIC_GITEA_REPO ?? "";
|
|||||||
history.replaceState(null, "", `${window.location.pathname}#${curH3.id}`);
|
history.replaceState(null, "", `${window.location.pathname}#${curH3.id}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
// clics actions
|
|
||||||
if (btnTopChapter) btnTopChapter.addEventListener("click", () => {
|
if (btnTopChapter) btnTopChapter.addEventListener("click", () => {
|
||||||
if (!h1) return;
|
if (!h1) return;
|
||||||
scrollToElWithOffset(h1, 12, "smooth");
|
scrollToElWithOffset(h1, 12, "smooth");
|
||||||
@@ -871,7 +935,6 @@ const GITEA_REPO = import.meta.env.PUBLIC_GITEA_REPO ?? "";
|
|||||||
|
|
||||||
updateFollow();
|
updateFollow();
|
||||||
|
|
||||||
// arrivée avec hash => ouvrir + offset
|
|
||||||
const initialHash = (location.hash || "").slice(1);
|
const initialHash = (location.hash || "").slice(1);
|
||||||
if (initialHash) {
|
if (initialHash) {
|
||||||
const el = document.getElementById(initialHash);
|
const el = document.getElementById(initialHash);
|
||||||
|
|||||||
Reference in New Issue
Block a user