Compare commits
2 Commits
2a2ed26563
...
0b4c2244fe
| Author | SHA1 | Date | |
|---|---|---|---|
| 0b4c2244fe | |||
| f720beaf3c |
@@ -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>
|
||||
@@ -672,7 +734,7 @@ const fdsQuestions = [
|
||||
|
||||
const ftQuestions = [
|
||||
{ id: 1, text: "Quel est le nom du produit et qui est le fabricant ?", answer: "Description" },
|
||||
{ id: 2, text: "Sur quelles matières ce produit est-il compatible ?", answer: "Description" },
|
||||
{ id: 2, text: "Sur quelles matières ce produit est-il compatible ?", answer: "Absent" },
|
||||
{ id: 3, text: "Quel est (sont) le(s) type(s) de ce produit ?", answer: "Propriétés biocides" },
|
||||
{ id: 4, text: "Quels sont les types de conditionnements disponibles ?", answer: "Conditionnements" },
|
||||
{ id: 5, text: "Où retrouver les différents modes d'utilisation de ce produit ?", answer: "Utilisations" }
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user