265 lines
8.0 KiB
Plaintext
265 lines
8.0 KiB
Plaintext
<dialog id="propose-modal" aria-label="Qualifier la proposition" data-step="1">
|
||
<form method="dialog" class="box">
|
||
<header class="hd">
|
||
<h2>Qualifier la proposition</h2>
|
||
<button value="cancel" class="x" aria-label="Fermer">✕</button>
|
||
</header>
|
||
|
||
<!-- STEP 1 -->
|
||
<section class="step step-1" aria-label="Choisir le type">
|
||
<p class="sub">
|
||
D’abord : 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">
|
||
Optionnel : choisis une intention (pour tri & traitement éditorial). Sinon, continue sans préciser.
|
||
</p>
|
||
|
||
<div class="grid">
|
||
<button type="button" data-category="cat/style">Style / lisibilité</button>
|
||
<button type="button" data-category="cat/lexique">Lexique / terminologie</button>
|
||
<button type="button" data-category="cat/argument">Argument / structure</button>
|
||
<button type="button" data-category="cat/redondance">Redondance</button>
|
||
<button type="button" data-category="cat/source">Source / vérification</button>
|
||
<button type="button" data-category="">Continuer sans préciser</button>
|
||
</div>
|
||
|
||
<footer class="ft row">
|
||
<button type="button" class="back" data-back="1">← Retour</button>
|
||
<small>Étape 2/2 — <kbd>Esc</kbd> pour fermer.</small>
|
||
</footer>
|
||
</section>
|
||
</form>
|
||
</dialog>
|
||
|
||
<style>
|
||
dialog { border: none; padding: 0; border-radius: 18px; width: min(760px, calc(100vw - 2rem)); }
|
||
dialog::backdrop { background: rgba(0,0,0,.55); }
|
||
|
||
.box { padding: 1rem 1rem .85rem; }
|
||
.hd { display:flex; align-items:center; justify-content:space-between; gap:.75rem; }
|
||
.hd h2 { margin:0; font-size:1.1rem; }
|
||
.x { border:0; background:transparent; font-size:1.1rem; cursor:pointer; }
|
||
|
||
.sub { margin:.55rem 0 1rem; opacity:.92; line-height: 1.35; }
|
||
|
||
.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; }
|
||
|
||
.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; }
|
||
|
||
.step { display:none; }
|
||
dialog[data-step="1"] .step-1 { display:block; }
|
||
dialog[data-step="2"] .step-2 { display:block; }
|
||
</style>
|
||
|
||
<script is:inline>
|
||
(() => {
|
||
const dlg = document.getElementById("propose-modal");
|
||
if (!dlg) return;
|
||
|
||
/** @type {URL|null} */
|
||
let pending = null;
|
||
/** @type {"correction"|"fact"|""} */
|
||
let kind = "";
|
||
|
||
// Doit rester cohérent avec EditionLayout
|
||
const URL_HARD_LIMIT = 6500;
|
||
|
||
const esc = (s) => String(s).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||
|
||
const upsertLine = (text, key, value) => {
|
||
const re = new RegExp(`^\\s*${esc(key)}\\s*:\\s*.*$`, "mi");
|
||
|
||
// value vide => supprimer la ligne si elle existe
|
||
if (!value) {
|
||
if (!re.test(text)) return text;
|
||
return (
|
||
text
|
||
.replace(re, "")
|
||
.replace(/\n{3,}/g, "\n\n")
|
||
.trimEnd() + "\n"
|
||
);
|
||
}
|
||
|
||
// remplace si existe
|
||
if (re.test(text)) return text.replace(re, `${key}: ${value}`);
|
||
|
||
// sinon append
|
||
const sep = text && !text.endsWith("\n") ? "\n" : "";
|
||
return text + sep + `${key}: ${value}\n`;
|
||
};
|
||
|
||
const setTitlePrefix = (u, prefix) => {
|
||
const t = u.searchParams.get("title") || "";
|
||
const cleaned = t.replace(/^\[[^\]]+\]\s*/,"");
|
||
u.searchParams.set("title", `[${prefix}] ${cleaned}`.trim());
|
||
};
|
||
|
||
const tryUpgradeBodyWithFull = (u, full) => {
|
||
if (!full) return;
|
||
|
||
let body = u.searchParams.get("body") || "";
|
||
if (!body) return;
|
||
|
||
// Si déjà en "copie exacte", rien à faire
|
||
if (body.includes("Texte actuel (copie exacte du paragraphe)")) return;
|
||
|
||
// On ne tente que si le body est en mode extrait
|
||
if (!body.includes("Texte actuel (extrait):")) return;
|
||
|
||
const quoted = full.split(/\r?\n/).map(l => `> ${l}`.trimEnd()).join("\n");
|
||
|
||
// Remplace le bloc "Texte actuel (extrait)" jusqu'à "Proposition (remplacer par):"
|
||
const re = /Texte actuel \(extrait\):[\s\S]*?\n\nProposition \(remplacer par\):/m;
|
||
const next = body.replace(
|
||
re,
|
||
`Texte actuel (copie exacte du paragraphe):\n${quoted}\n\nProposition (remplacer par):`
|
||
);
|
||
|
||
if (next === body) return;
|
||
|
||
u.searchParams.set("body", next);
|
||
|
||
// garde-fou URL
|
||
if (u.toString().length > URL_HARD_LIMIT) {
|
||
// revert
|
||
u.searchParams.set("body", body);
|
||
}
|
||
};
|
||
|
||
// ✅ Ouvre EN NOUVEL ONGLET : stratégie unique (anchor click) => jamais 2 onglets
|
||
const openInNewTab = (url) => {
|
||
try {
|
||
const a = document.createElement("a");
|
||
a.href = url;
|
||
a.target = "_blank";
|
||
a.rel = "noopener noreferrer";
|
||
a.style.display = "none";
|
||
document.body.appendChild(a);
|
||
a.click();
|
||
a.remove();
|
||
return true;
|
||
} catch {}
|
||
|
||
// dernier recours
|
||
window.prompt("Popup bloquée. Copiez ce lien pour ouvrir le ticket :", url);
|
||
return false;
|
||
};
|
||
|
||
const openWith = (url) => {
|
||
pending = url;
|
||
kind = "";
|
||
dlg.dataset.step = "1";
|
||
if (typeof dlg.showModal === "function") dlg.showModal();
|
||
else openInNewTab(url.toString());
|
||
};
|
||
|
||
// Intercepte UNIQUEMENT les liens marqués data-propose
|
||
document.addEventListener("click", async (e) => {
|
||
const a = e.target?.closest?.("a[data-propose]");
|
||
if (!a) return;
|
||
|
||
e.preventDefault();
|
||
e.stopImmediatePropagation();
|
||
|
||
const rawUrl = a.dataset.url || a.getAttribute("href") || "";
|
||
if (!rawUrl || rawUrl === "#") return;
|
||
|
||
const full = (a.dataset.full || "").trim();
|
||
|
||
try {
|
||
const u = new URL(rawUrl);
|
||
|
||
if (full) {
|
||
try { await navigator.clipboard.writeText(full); } catch {}
|
||
tryUpgradeBodyWithFull(u, full);
|
||
}
|
||
|
||
openWith(u);
|
||
} catch {
|
||
openInNewTab(rawUrl);
|
||
}
|
||
}, { capture: true });
|
||
|
||
// 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) => {
|
||
const btn = e.target?.closest?.("button[data-category]");
|
||
if (!btn || !pending) return;
|
||
|
||
const cat = btn.getAttribute("data-category") || "";
|
||
let body = pending.searchParams.get("body") || "";
|
||
body = upsertLine(body, "Category", cat);
|
||
pending.searchParams.set("body", body);
|
||
|
||
const u = pending.toString();
|
||
dlg.close();
|
||
|
||
// ✅ ouvre en nouvel onglet, sans jamais remplacer l’onglet courant
|
||
openInNewTab(u);
|
||
});
|
||
})();
|
||
</script>
|
||
|