Refonte de la slide 5 FT en exercice à relier + boutons Recommencer

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).
This commit is contained in:
2026-07-16 09:15:07 +02:00
parent 2a2ed26563
commit f720beaf3c

View File

@@ -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
<button class="verify-btn btn-amber" id="fds-verify" onclick="verifyFDS()" disabled>✓ Vérifier mes réponses</button>
<div id="fds-result" class="result-message"></div>
<div class="actions-row">
<button class="btn btn-outline" onclick="resetFDS()">↺ Recommencer</button>
<button class="btn btn-coral" onclick="goTo(4)">Passer à la partie 2 →</button>
</div>
</div>
@@ -568,15 +624,21 @@ select:focus { outline: none; border-color: var(--amber); box-shadow: 0 0 0 3px
<button class="btn btn-coral" onclick="goTo(5)" style="margin-top:1.5rem;">Répondre aux questions →</button>
</div>
<!-- ============ SLIDE 5 — FT QUIZ ============ -->
<!-- ============ SLIDE 5 — FT : RELIER ============ -->
<div class="slide" id="slide-5">
<div class="scroll-area">
<div class="section-tag" style="background:var(--coral-light);color:var(--coral);">Partie 2 / 3 — FT</div>
<div class="slide-title">Sections de la fiche technique</div>
<div class="quiz-list" id="ft-questions"></div>
<p class="match-hint">Reliez chaque question à la bonne section : cliquez sur un point à gauche, maintenez, puis glissez jusqu'au point de la section correspondante à droite.</p>
<div class="match-wrap" id="ft-match">
<svg class="match-svg" id="ft-svg"></svg>
<div class="match-col match-left" id="ft-left"></div>
<div class="match-col match-right" id="ft-right"></div>
</div>
<button class="verify-btn btn-coral" id="ft-verify" onclick="verifyFT()" disabled>✓ Vérifier mes réponses</button>
<div id="ft-result" class="result-message"></div>
<div class="actions-row">
<button class="btn btn-outline" onclick="resetFT()">↺ Recommencer</button>
<button class="btn btn-teal" onclick="goTo(6)">Passer à la partie 3 →</button>
</div>
</div>
@@ -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 = `<span>${q.text}</span><div class="match-dot" data-side="left" data-qid="${q.id}"></div>`;
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 = `<div class="match-dot" data-side="right" data-answer="${sec}"></div><span>${sec}</span>`;
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) => `<div class="dnd-tile" draggable="true" data-value="${v}" data-label="${l}">${l}</div>`;
@@ -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');