ui: propose modal 2-steps + clean local urls

This commit is contained in:
s-FunIA
2026-01-19 11:31:32 +01:00
parent b13aa10f69
commit aece8c5526
2 changed files with 153 additions and 39 deletions

View File

@@ -1,10 +1,28 @@
<dialog id="propose-modal" aria-label="Qualifier la proposition"> <dialog id="propose-modal" aria-label="Qualifier la proposition" data-step="1">
<form method="dialog" class="box"> <form method="dialog" class="box">
<header class="hd"> <header class="hd">
<h2>Qualifier la proposition</h2> <h2>Qualifier la proposition</h2>
<button value="cancel" class="x" aria-label="Fermer">✕</button> <button value="cancel" class="x" aria-label="Fermer">✕</button>
</header> </header>
<!-- STEP 1 -->
<section class="step step-1" aria-label="Choisir le type">
<p class="sub">
Dabord : quel type de ticket veux-tu ouvrir ?
</p>
<div class="grid">
<button type="button" data-kind="correction">✍️ Correction (rédaction / clarté)</button>
<button type="button" data-kind="fact">🔎 Vérification factuelle / sources</button>
</div>
<footer class="ft">
<small>Étape 1/2 — <kbd>Esc</kbd> pour fermer.</small>
</footer>
</section>
<!-- STEP 2 -->
<section class="step step-2" aria-label="Choisir la catégorie">
<p class="sub"> <p class="sub">
Optionnel : choisis une intention (pour tri & traitement éditorial). Sinon, continue sans préciser. Optionnel : choisis une intention (pour tri & traitement éditorial). Sinon, continue sans préciser.
</p> </p>
@@ -18,24 +36,53 @@
<button type="button" data-category="">Continuer sans préciser</button> <button type="button" data-category="">Continuer sans préciser</button>
</div> </div>
<footer class="ft"> <footer class="ft row">
<small>Astuce : touche <kbd>Esc</kbd> pour fermer.</small> <button type="button" class="back" data-back="1">← Retour</button>
<small>Étape 2/2 — <kbd>Esc</kbd> pour fermer.</small>
</footer> </footer>
</section>
</form> </form>
</dialog> </dialog>
<style> <style>
dialog { border: none; padding: 0; border-radius: 16px; width: min(720px, calc(100vw - 2rem)); } dialog { border: none; padding: 0; border-radius: 18px; width: min(760px, calc(100vw - 2rem)); }
dialog::backdrop { background: rgba(0,0,0,.55); } dialog::backdrop { background: rgba(0,0,0,.55); }
.box { padding: 1rem 1rem 0.75rem; }
.box { padding: 1rem 1rem .85rem; }
.hd { display:flex; align-items:center; justify-content:space-between; gap:.75rem; } .hd { display:flex; align-items:center; justify-content:space-between; gap:.75rem; }
.hd h2 { margin:0; font-size:1.1rem; } .hd h2 { margin:0; font-size:1.1rem; }
.x { border:0; background:transparent; font-size:1.1rem; cursor:pointer; } .x { border:0; background:transparent; font-size:1.1rem; cursor:pointer; }
.sub { margin:.5rem 0 1rem; opacity:.9; }
.grid { display:grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap:.5rem; } .sub { margin:.55rem 0 1rem; opacity:.92; line-height: 1.35; }
.grid button { padding: .75rem .8rem; border-radius: 12px; border: 1px solid rgba(0,0,0,.18); background: rgba(255,255,255,.96); color: #111; cursor: pointer; text-align: left; }
.grid { display:grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap:.55rem; }
.grid button {
padding: .8rem .9rem;
border-radius: 14px;
border: 1px solid rgba(0,0,0,.18);
background: rgba(255,255,255,.96);
color: #111;
cursor: pointer;
text-align: left;
line-height: 1.2;
}
.grid button:hover { background: #fff; } .grid button:hover { background: #fff; }
.ft { margin-top:.85rem; opacity:.88; }
.ft.row { display:flex; align-items:center; justify-content:space-between; gap:1rem; }
.back {
border: 1px solid rgba(0,0,0,.18);
background: rgba(255,255,255,.96);
color:#111;
border-radius: 12px;
padding: .55rem .7rem;
cursor: pointer;
}
kbd { padding:0 .35rem; border:1px solid rgba(0,0,0,.2); border-radius:6px; } kbd { padding:0 .35rem; border:1px solid rgba(0,0,0,.2); border-radius:6px; }
.step { display:none; }
dialog[data-step="1"] .step-1 { display:block; }
dialog[data-step="2"] .step-2 { display:block; }
</style> </style>
<script is:inline> <script is:inline>
@@ -45,48 +92,88 @@
/** @type {URL|null} */ /** @type {URL|null} */
let pending = null; let pending = null;
/** @type {"correction"|"fact"|""} */
let kind = "";
const upsertLine = (text, key, value) => { const upsertLine = (text, key, value) => {
const re = new RegExp(`^${key}:\\s*.*$`, "mi"); const re = new RegExp(`^\\s*${key}\\s*:\\s*.*$`, "mi");
// Si "continuer sans préciser" : on ne touche pas au body
if (!value) return text;
if (re.test(text)) return text.replace(re, `${key}: ${value}`); if (re.test(text)) return text.replace(re, `${key}: ${value}`);
const sep = text && !text.endsWith("\n") ? "\n" : ""; const sep = text && !text.endsWith("\n") ? "\n" : "";
return text + sep + `${key}: ${value}\n`; return text + sep + `${key}: ${value}\n`;
}; };
const openModalFor = (url) => { const setTitlePrefix = (u, prefix) => {
const t = u.searchParams.get("title") || "";
const cleaned = t.replace(/^\[[^\]]+\]\s*/,"");
u.searchParams.set("title", `[${prefix}] ${cleaned}`.trim());
};
const openWith = (url) => {
pending = url; pending = url;
kind = "";
dlg.dataset.step = "1";
if (typeof dlg.showModal === "function") dlg.showModal(); if (typeof dlg.showModal === "function") dlg.showModal();
else window.open(url.toString(), "_blank", "noopener,noreferrer"); else window.open(url.toString(), "_blank", "noopener,noreferrer");
}; };
// Interception des clics sur "Proposer" // Intercepte UNIQUEMENT les liens marqués data-propose
document.addEventListener("click", (e) => { document.addEventListener("click", (e) => {
const a = e.target?.closest?.("a[data-propose]"); const a = e.target?.closest?.("a[data-propose]");
if (!a) return; if (!a) return;
e.preventDefault(); e.preventDefault();
// URL réelle stockée par EditionLayout (propose.dataset.url) // L'URL réelle est dans data-url (préparée côté EditionLayout)
const rawUrl = a.dataset.url || a.getAttribute("href") || ""; const rawUrl = a.dataset.url || a.getAttribute("href") || "";
if (!rawUrl || rawUrl === "#") return; if (!rawUrl || rawUrl === "#") return;
try { try {
openModalFor(new URL(rawUrl, window.location.origin)); openWith(new URL(rawUrl));
} catch { } catch {
window.open(rawUrl, "_blank", "noopener,noreferrer"); window.open(rawUrl, "_blank", "noopener,noreferrer");
} }
}); });
// Choix dune catégorie -> injection "Category:" + ouverture Gitea // Step 1: type
dlg.addEventListener("click", (e) => {
const btn = e.target?.closest?.("button[data-kind]");
if (!btn || !pending) return;
kind = btn.getAttribute("data-kind") || "";
let body = pending.searchParams.get("body") || "";
if (kind === "fact") {
body = upsertLine(body, "Type", "type/fact-check");
body = upsertLine(body, "State", "state/a-sourcer");
setTitlePrefix(pending, "Fact-check");
} else {
body = upsertLine(body, "Type", "type/correction");
body = upsertLine(body, "State", "state/recevable");
setTitlePrefix(pending, "Correction");
}
pending.searchParams.set("body", body);
dlg.dataset.step = "2";
});
// Back
dlg.addEventListener("click", (e) => {
const back = e.target?.closest?.("button[data-back]");
if (!back) return;
dlg.dataset.step = "1";
});
// Step 2: category + open
dlg.addEventListener("click", (e) => { dlg.addEventListener("click", (e) => {
const btn = e.target?.closest?.("button[data-category]"); const btn = e.target?.closest?.("button[data-category]");
if (!btn || !pending) return; if (!btn || !pending) return;
const cat = btn.getAttribute("data-category") || ""; const cat = btn.getAttribute("data-category") || "";
const body = pending.searchParams.get("body") || ""; let body = pending.searchParams.get("body") || "";
pending.searchParams.set("body", upsertLine(body, "Category", cat)); if (cat) body = upsertLine(body, "Category", cat);
pending.searchParams.set("body", body);
dlg.close(); dlg.close();
window.open(pending.toString(), "_blank", "noopener,noreferrer"); window.open(pending.toString(), "_blank", "noopener,noreferrer");

View File

@@ -70,6 +70,11 @@ const GITEA_REPO = import.meta.env.PUBLIC_GITEA_REPO ?? "";
<!-- IMPORTANT: define:vars injecte les constantes dans le JS sans templating fragile --> <!-- IMPORTANT: define:vars injecte les constantes dans le JS sans templating fragile -->
<script is:inline define:vars={{ GITEA_BASE, GITEA_OWNER, GITEA_REPO }}> <script is:inline define:vars={{ GITEA_BASE, GITEA_OWNER, GITEA_REPO }}>
(() => { (() => {
// Nettoyage si un ancien bug a injecté ?body=... dans l'URL
if (window.location.search.includes("body=")) {
history.replaceState(null, "", window.location.pathname + window.location.hash);
}
const title = document.body.dataset.docTitle || document.title; const title = document.body.dataset.docTitle || document.title;
const version = document.body.dataset.docVersion || ""; const version = document.body.dataset.docVersion || "";
@@ -79,7 +84,11 @@ const GITEA_REPO = import.meta.env.PUBLIC_GITEA_REPO ?? "";
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); // URL locale "propre" : on ignore totalement query-string (?body=...)
const local = new URL(window.location.origin + window.location.pathname);
// évite dembarquer des paramètres parasites (ex: ?body=... issus de tests)
local.searchParams.delete("body");
local.searchParams.delete("title");
local.hash = anchorId; local.hash = anchorId;
const path = local.pathname; const path = local.pathname;
@@ -127,7 +136,7 @@ const GITEA_REPO = import.meta.env.PUBLIC_GITEA_REPO ?? "";
citeBtn.textContent = "Citer"; citeBtn.textContent = "Citer";
citeBtn.addEventListener("click", async () => { citeBtn.addEventListener("click", async () => {
const url = new URL(window.location.href); const url = new URL(window.location.origin + window.location.pathname);
url.hash = p.id; url.hash = p.id;
const cite = `${title}${version ? ` (v${version})` : ""} — ${url.toString()}`; const cite = `${title}${version ? ` (v${version})` : ""} — ${url.toString()}`;
@@ -150,11 +159,29 @@ const GITEA_REPO = import.meta.env.PUBLIC_GITEA_REPO ?? "";
propose.href = "#"; propose.href = "#";
propose.textContent = "Proposer"; propose.textContent = "Proposer";
propose.setAttribute("aria-label", "Proposer une correction sur Gitea"); propose.setAttribute("aria-label", "Proposer une correction sur Gitea");
propose.setAttribute("data-propose","1"); propose.setAttribute("data-propose","1");
tools.appendChild(propose); propose.addEventListener("click", (e) => {
e.preventDefault();
const raw = (p.textContent || "").trim().replace(/\s+/g, " ");
const excerpt = raw.length > 420 ? (raw.slice(0, 420) + "…") : raw;
const url = buildIssueURL(p.id, excerpt);
// on stocke l'URL pour le modal
propose.dataset.url = url.toString();
// fallback: si le modal n'existe pas -> ouverture directe
const dlg = document.getElementById("propose-modal");
if (!dlg || typeof dlg.showModal !== "function") {
window.open(propose.dataset.url, "_blank", "noopener,noreferrer");
return;
} }
// si modal existe, il va intercepter le click globalement
// donc on ne fait rien ici de plus.
});
tools.appendChild(propose);
}
p.appendChild(tools); p.appendChild(tools);
} }