diff --git a/exercices/Exo_FDS.html b/exercices/Exo_FDS.html
index 4926658..4dc4c66 100644
--- a/exercices/Exo_FDS.html
+++ b/exercices/Exo_FDS.html
@@ -263,6 +263,61 @@
.dnd-layout { grid-template-columns: 1fr; }
.dnd-bank { grid-template-columns: repeat(auto-fill, minmax(60px, 1fr)); }
}
+/* SLIDE 5 — RELIER (MATCHING) */
+.match-hint { font-size: 12.5px; color: var(--text-muted); text-align: center; margin: -0.4rem 0 0.1rem; max-width: 640px; line-height: 1.5; }
+.match-wrap {
+ position: relative;
+ width: 100%;
+ max-width: 780px;
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ column-gap: clamp(56px, 12vw, 140px);
+ align-items: start;
+}
+.match-svg { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; z-index: 2; overflow: visible; }
+.match-col { display: flex; flex-direction: column; gap: 14px; }
+.match-right { justify-content: center; }
+.match-item {
+ position: relative;
+ background: white;
+ border: 1px solid var(--border);
+ border-radius: 12px;
+ padding: 12px 16px;
+ font-size: 13.5px;
+ color: var(--text);
+ line-height: 1.4;
+ min-height: 46px;
+ display: flex; align-items: center;
+ transition: border-color 0.2s ease, box-shadow 0.2s ease;
+}
+.match-left .match-item { padding-right: 24px; }
+.match-right .match-item {
+ padding-left: 24px;
+ justify-content: center; text-align: center;
+ font-family: 'Syne', sans-serif; font-weight: 800; font-size: 13px;
+ color: var(--coral); background: var(--coral-light); border-color: var(--coral-mid);
+}
+.match-item.connected { border-color: var(--coral); box-shadow: 0 4px 12px rgba(153,60,29,0.10); }
+.match-dot {
+ position: absolute; top: 50%; transform: translateY(-50%);
+ width: 16px; height: 16px; border-radius: 50%;
+ background: white; border: 2.5px solid var(--coral-mid);
+ cursor: pointer; z-index: 3; touch-action: none;
+ transition: transform 0.15s ease, background 0.15s ease;
+}
+.match-left .match-dot { right: -8px; }
+.match-right .match-dot { left: -8px; }
+.match-dot:hover { transform: translateY(-50%) scale(1.3); background: var(--coral-mid); }
+.match-dot.filled { background: var(--coral-mid); }
+.match-dot.active { background: var(--coral); border-color: var(--coral); transform: translateY(-50%) scale(1.3); }
+@media (max-width: 700px) {
+ .match-hint { font-size: 12px; }
+ .match-wrap { column-gap: 44px; }
+ .match-item { font-size: 12.5px; padding: 10px 12px; }
+ .match-left .match-item { padding-right: 20px; }
+ .match-right .match-item { padding-left: 20px; }
+}
+
.answer-row {
background: white;
border: 1px solid var(--border);
@@ -551,6 +606,7 @@ select:focus { outline: none; border-color: var(--amber); box-shadow: 0 0 0 3px
✓ Vérifier mes réponses
@@ -717,14 +779,8 @@ function generateQuizzes() {
// FDS — glisser-déposer : banques d'étiquettes + cases de dépôt
buildFdsDnd();
- // FT
- const ftOpts = ftOptions.map(o => ({ value: o, label: o }));
- ftOpts.push({ value: 'absent', label: 'Absent' });
- const ftC = document.getElementById('ft-questions');
- ftC.innerHTML = '';
- ftQuestions.forEach((q, i) => {
- ftC.innerHTML += buildRow(i + 1, 'ft' + q.id, q.answer, q.text, ftOpts, 'background:var(--coral-light);color:var(--coral);');
- });
+ // FT — exercice « relier » (questions à gauche, sections à droite)
+ buildFtMatch();
// Étiquette
const etiOpts = etiquetteOptions.map(o => ({ value: o, label: o }));
@@ -736,10 +792,141 @@ function generateQuizzes() {
});
// Bloque la vérification tant que toutes les questions ne sont pas répondues
- wireVerifyGate('ft-questions', 'ft-verify');
wireVerifyGate('etiquette-questions', 'eti-verify');
}
+/* ---- FT : exercice « relier » ---- */
+const ftConnections = {}; // { qId: valeur de la section reliée }
+let ftDrag = null; // { qid } pendant un glissement
+let ftVerified = false;
+const SVGNS = 'http://www.w3.org/2000/svg';
+const FT_COLORS = { line: '#D85A30', temp: '#993C1D', ok: '#2e7d32', ko: '#c62828' };
+
+function qById(id) { return ftQuestions.find(q => String(q.id) === String(id)); }
+
+function buildFtMatch() {
+ Object.keys(ftConnections).forEach(k => delete ftConnections[k]);
+ ftDrag = null; ftVerified = false;
+
+ const leftC = document.getElementById('ft-left');
+ const rightC = document.getElementById('ft-right');
+ leftC.innerHTML = '';
+ rightC.innerHTML = '';
+
+ ftQuestions.forEach(q => {
+ const item = document.createElement('div');
+ item.className = 'match-item';
+ item.dataset.qid = q.id;
+ item.innerHTML = `
${q.text}
`;
+ leftC.appendChild(item);
+ });
+
+ const sections = shuffleArray([...new Set(ftQuestions.map(q => q.answer))]);
+ sections.forEach(sec => {
+ const item = document.createElement('div');
+ item.className = 'match-item';
+ item.dataset.answer = sec;
+ item.innerHTML = `
${sec} `;
+ rightC.appendChild(item);
+ });
+
+ initFtMatch();
+ document.getElementById('ft-svg').innerHTML = '';
+ document.getElementById('ft-verify').disabled = true;
+}
+
+function resetFT() {
+ const r = document.getElementById('ft-result');
+ r.className = 'result-message'; r.innerHTML = '';
+ buildFtMatch();
+}
+
+function ftSvgRect() { return document.getElementById('ft-svg').getBoundingClientRect(); }
+function ftPointerPos(e) { const r = ftSvgRect(); return { x: e.clientX - r.left, y: e.clientY - r.top }; }
+function ftDotCenter(dot) {
+ const r = dot.getBoundingClientRect(); const s = ftSvgRect();
+ return { x: r.left + r.width / 2 - s.left, y: r.top + r.height / 2 - s.top };
+}
+
+function ftDrawLine(svg, a, b, color, dashed) {
+ const line = document.createElementNS(SVGNS, 'line');
+ line.setAttribute('x1', a.x); line.setAttribute('y1', a.y);
+ line.setAttribute('x2', b.x); line.setAttribute('y2', b.y);
+ line.setAttribute('stroke', color);
+ line.setAttribute('stroke-width', '3');
+ line.setAttribute('stroke-linecap', 'round');
+ if (dashed) line.setAttribute('stroke-dasharray', '6 5');
+ svg.appendChild(line);
+}
+
+function drawFtLines(tempEnd) {
+ const svg = document.getElementById('ft-svg');
+ if (!svg) return;
+ svg.innerHTML = '';
+ Object.entries(ftConnections).forEach(([qid, ans]) => {
+ const from = document.querySelector('.match-dot[data-side="left"][data-qid="' + qid + '"]');
+ const to = document.querySelector('.match-dot[data-side="right"][data-answer="' + CSS.escape(ans) + '"]');
+ if (!from || !to) return;
+ const color = ftVerified ? (ans === qById(qid).answer ? FT_COLORS.ok : FT_COLORS.ko) : FT_COLORS.line;
+ ftDrawLine(svg, ftDotCenter(from), ftDotCenter(to), color);
+ });
+ if (ftDrag && tempEnd) {
+ const from = document.querySelector('.match-dot[data-side="left"][data-qid="' + ftDrag.qid + '"]');
+ if (from) ftDrawLine(svg, ftDotCenter(from), tempEnd, FT_COLORS.temp, true);
+ }
+}
+
+function refreshFtState() {
+ ftQuestions.forEach(q => {
+ const dot = document.querySelector('.match-dot[data-side="left"][data-qid="' + q.id + '"]');
+ if (!dot) return;
+ const has = ftConnections[q.id] != null;
+ dot.classList.toggle('filled', has);
+ dot.closest('.match-item').classList.toggle('connected', has);
+ });
+ document.querySelectorAll('.match-dot[data-side="right"]').forEach(dot => {
+ const used = Object.values(ftConnections).includes(dot.dataset.answer);
+ dot.classList.toggle('filled', used);
+ dot.closest('.match-item').classList.toggle('connected', used);
+ });
+ const all = ftQuestions.every(q => ftConnections[q.id] != null);
+ document.getElementById('ft-verify').disabled = !all;
+}
+
+function initFtMatch() {
+ const wrap = document.getElementById('ft-match');
+ wrap.querySelectorAll('.match-dot[data-side="left"]').forEach(dot => {
+ dot.addEventListener('pointerdown', e => {
+ e.preventDefault();
+ const qid = dot.dataset.qid;
+ delete ftConnections[qid]; // repartir d'une question efface son lien
+ ftVerified = false;
+ document.getElementById('ft-result').className = 'result-message';
+ ftDrag = { qid };
+ dot.classList.add('active');
+ refreshFtState();
+ drawFtLines(ftPointerPos(e));
+ });
+ });
+}
+
+// Gestionnaires globaux (le glissement peut sortir de la zone)
+document.addEventListener('pointermove', e => {
+ if (!ftDrag) return;
+ drawFtLines(ftPointerPos(e));
+});
+document.addEventListener('pointerup', e => {
+ if (!ftDrag) return;
+ const target = document.elementFromPoint(e.clientX, e.clientY);
+ const answerDot = target && target.closest('.match-dot[data-side="right"]');
+ if (answerDot) ftConnections[ftDrag.qid] = answerDot.dataset.answer;
+ document.querySelectorAll('.match-dot.active').forEach(d => d.classList.remove('active'));
+ ftDrag = null;
+ refreshFtState();
+ drawFtLines();
+});
+window.addEventListener('resize', () => { if (currentSlide === 5) drawFtLines(); });
+
/* ---- FDS : glisser-déposer ---- */
function buildFdsDnd() {
const tile = (v, l) => `
${l}
`;
@@ -764,6 +951,12 @@ function buildFdsDnd() {
initFdsDnd();
}
+function resetFDS() {
+ const r = document.getElementById('fds-result');
+ r.className = 'result-message'; r.innerHTML = '';
+ buildFdsDnd();
+}
+
function initFdsDnd() {
const slide = document.getElementById('slide-3');
const tiles = slide.querySelectorAll('.dnd-tile');
@@ -900,14 +1093,12 @@ function showSlotResult(slot, isCorrect, isUnanswered, correctAnswerText) {
}
function verifyFT() {
+ ftVerified = true;
let correct = 0;
ftQuestions.forEach(q => {
- const select = document.getElementById('ft' + q.id);
- const userAnswer = select.value;
- const isCorrect = userAnswer === q.answer;
- if (isCorrect) correct++;
- showInlineResult(select, isCorrect, userAnswer === '', q.answer);
+ if (ftConnections[q.id] === q.answer) correct++;
});
+ drawFtLines();
showScore(document.getElementById('ft-result'), correct, ftQuestions.length);
}
@@ -959,10 +1150,11 @@ function exporterPDF() {
let scoreTotal = 0, nbTotal = 0;
- function partHtml(prefix, title, questions, selPrefix, answerLabelFn, expectedLabelFn, correctFn) {
+ function partHtml(prefix, title, questions, selPrefix, answerLabelFn, expectedLabelFn, correctFn, valueFn) {
const rows = questions.map((q, i) => {
- const sel = document.getElementById(selPrefix + q.id);
- const v = sel ? sel.value : '';
+ let v;
+ if (valueFn) { v = valueFn(q); }
+ else { const sel = document.getElementById(selPrefix + q.id); v = sel ? sel.value : ''; }
const ok = correctFn(q, v);
nbTotal++; if (ok) scoreTotal++;
const mark = v === '' ? '❓' : (ok ? '✓' : '✗');
@@ -975,7 +1167,7 @@ function exporterPDF() {
}
const p1 = partHtml('P1', 'Partie 1 — FDS (Fiche de Données de Sécurité)', fdsQuestions, 'fds', fdsAnswerLabel, fdsExpectedLabel, isFdsCorrect);
- const p2 = partHtml('P2', 'Partie 2 — FT (Fiche Technique)', ftQuestions, 'ft', textAnswerLabel, a => a, isTextCorrect);
+ const p2 = partHtml('P2', 'Partie 2 — FT (Fiche Technique)', ftQuestions, 'ft', textAnswerLabel, a => a, isTextCorrect, q => ftConnections[q.id] || '');
const p3 = partHtml('P3', 'Partie 3 — Étiquette', etiquetteQuestions, 'eti', textAnswerLabel, a => a, isTextCorrect);
const w = window.open('', '_blank');