From f720beaf3c584c0c0fcad89238e65a203be9c79e Mon Sep 17 00:00:00 2001 From: "gauthier.chombart" Date: Thu, 16 Jul 2026 09:15:07 +0200 Subject: [PATCH] =?UTF-8?q?Refonte=20de=20la=20slide=205=20FT=20en=20exerc?= =?UTF-8?q?ice=20=C3=A0=20relier=20+=20boutons=20Recommencer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit La slide 5 (Fiche Technique) devient un exercice de mise en relation : questions à gauche, sections mélangées à droite, reliées au glisser (pointer events + tracé SVG). La vérification colore les liaisons (vert/rouge) et n'est active qu'une fois les 5 questions reliées. L'export PDF lit désormais les liaisons. Ajout d'un bouton Recommencer sur les slides 3 (FDS glisser-déposer) et 5 (FT), qui réinitialise l'exercice concerné (réponses effacées, message et vérification remis à zéro, éléments re-mélangés). --- exercices/Exo_FDS.html | 232 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 212 insertions(+), 20 deletions(-) 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
+
@@ -568,15 +624,21 @@ select:focus { outline: none; border-color: var(--amber); box-shadow: 0 0 0 3px - +
Sections de la fiche technique
-
+

Reliez chaque question à la bonne section : cliquez sur un point à gauche, maintenez, puis glissez jusqu'au point de la section correspondante à droite.

+
+ +
+
+
+
@@ -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');