feat(algebra-8 ch2): 3 сортировки переведены на drag-and-drop

Универсальный хелпер setupSorter(cfg) с pointer-events:
- desktop: тащим карточку → подсветка целевого ящика → отпускаем = поставлено
- touch / mobile: тап по карточке (становится "armed") → тап по ящику = поставлено
- × кнопка на placed-чипе → возврат в pool
- drop за пределы ящика на сам pool тоже возвращает чип
- threshold 8px — клик не превращается в drag случайно

Стили: .dnd-chip с cursor:grab/active grabbing, .armed shadow,
.dragging opacity, .drop-box.over подсветка с лёгким scale.

Применено к:
- § 7 INT 2 (полное / неполное / не квадратное) — 8 уравнений
- § 10 INT 5 (раскладывается / не раскладывается) — 8 трёхчленов
- § 11 INT 5 (движение / работа / числа / геометрия) — 8 задач,
  columnLayout:true для длинных текстов

Старые «лесенки кнопок Полн./Неполн./Не квадр.» удалены — теперь
один-клик-затем-один-клик или drag. § 12 INT 4 оставлен как
<select> (другой паттерн: одна метка для нескольких уравнений).
This commit is contained in:
Maxim Dolgolyov
2026-05-27 15:27:44 +03:00
parent 75792c93aa
commit 0cd187b693
+197 -166
View File
@@ -266,6 +266,23 @@ input,select,textarea{font-family:inherit}
.eq-show{font-family:'JetBrains Mono',monospace}
.pipe-tabs .btn.active{background:var(--sec-acc,var(--pri));color:#fff;border-color:var(--sec-acc,var(--pri))}
/* DRAG & DROP — sortable chips */
.dnd-pool{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:14px;padding:10px;border:1.5px dashed var(--border);border-radius:10px;min-height:54px;transition:border-color .18s,background .18s}
.dnd-pool.over{border-color:var(--sec-acc,var(--pri));background:var(--sec-acc-soft,var(--pri-soft));border-style:solid}
.dnd-pool.col{flex-direction:column;align-items:stretch}
.dnd-pool.col .dnd-chip{width:auto}
.dnd-chip{display:inline-flex;align-items:center;gap:6px;padding:6px 12px;background:var(--card);border:1.5px solid var(--border);border-radius:10px;cursor:grab;user-select:none;font-size:.92rem;line-height:1.4;transition:transform .12s,box-shadow .12s,border-color .12s;touch-action:none;max-width:100%}
.dnd-chip:hover{transform:translateY(-1px);border-color:var(--sec-acc,var(--pri));box-shadow:var(--sh)}
.dnd-chip:active{cursor:grabbing}
.dnd-chip.armed{border-color:var(--sec-acc,var(--pri));background:var(--sec-acc-soft,var(--pri-soft));box-shadow:0 0 0 3px rgba(244,63,94,.22);transform:translateY(-1px)}
.dnd-chip.dragging{opacity:.28}
.dnd-chip.placed{background:var(--sec-acc-soft,var(--pri-soft));border-color:var(--sec-acc,var(--pri))}
.dnd-chip .dnd-x{padding:0 5px;color:var(--muted);font-weight:700;font-size:1.05rem;border-radius:4px;cursor:pointer}
.dnd-chip .dnd-x:hover{color:var(--bad,var(--fail));background:var(--fail-bg)}
.drop-box.over{border-color:var(--sec-acc,var(--pri));background:var(--sec-acc-soft,var(--pri-soft));border-style:solid;transform:scale(1.015)}
.dnd-hint{font-size:.78rem;color:var(--muted);margin-bottom:8px;display:flex;align-items:center;gap:6px}
.dnd-hint svg{width:14px;height:14px;flex-shrink:0}
/* SIDEBAR DRAWER for narrow viewports */
.col-side-backdrop{position:fixed;inset:0;background:rgba(0,0,0,.42);z-index:9990;display:none;animation:fadeIn .18s ease}
.col-side-backdrop.show{display:block}
@@ -712,6 +729,137 @@ function confetti(){
frame();
}
/* ============================================================
DRAG & DROP SORTER — shared helper
============================================================
- desktop: drag chip → drop on .drop-box (or back on .dnd-pool)
- mobile/tap: tap chip → armed; tap a box → placed; tap × → remove
- re-rendering keeps it simple; setupSorter returns { placed, render } */
function setupSorter(cfg){
// cfg: { poolId, cats:[...], items:[{id,html,cat}], scopeSelector, columnLayout?:bool }
const placed = {};
const pool = document.getElementById(cfg.poolId);
const scope = document.querySelector(cfg.scopeSelector);
if(!pool || !scope) return { placed, render: ()=>{} };
pool.classList.add('dnd-pool');
if(cfg.columnLayout) pool.classList.add('col');
let armed = null;
function buildChip(it, isPlaced){
const el = document.createElement('div');
el.className = 'dnd-chip' + (isPlaced ? ' placed' : '');
el.dataset.id = it.id;
el.innerHTML = '<span class="dnd-txt">' + it.html + '</span><span class="dnd-x" title="Убрать">×</span>';
attachHandlers(el, it.id);
return el;
}
function attachHandlers(el, itId){
let ghost = null, dragging = false, startX = 0, startY = 0, captured = false;
el.addEventListener('pointerdown', ev => {
if(ev.button !== undefined && ev.button !== 0) return;
if(ev.target.classList && ev.target.classList.contains('dnd-x')){
ev.stopPropagation();
if(placed[itId]){ delete placed[itId]; render(); }
else if(armed === itId){ armed = null; render(); }
return;
}
startX = ev.clientX; startY = ev.clientY;
const rect = el.getBoundingClientRect();
const ox = ev.clientX - rect.left, oy = ev.clientY - rect.top;
try { el.setPointerCapture(ev.pointerId); captured = true; } catch(e){}
function onMove(e){
const dx = e.clientX - startX, dy = e.clientY - startY;
if(!dragging && Math.hypot(dx, dy) > 8){
dragging = true;
ghost = el.cloneNode(true);
ghost.classList.remove('armed');
ghost.style.cssText = 'position:fixed;z-index:9999;pointer-events:none;opacity:.9;transform:rotate(-2.5deg);box-shadow:0 14px 36px rgba(0,0,0,.32);width:' + rect.width + 'px;left:' + (e.clientX - ox) + 'px;top:' + (e.clientY - oy) + 'px';
document.body.appendChild(ghost);
el.classList.add('dragging');
}
if(dragging && ghost){
ghost.style.left = (e.clientX - ox) + 'px';
ghost.style.top = (e.clientY - oy) + 'px';
const under = document.elementsFromPoint(e.clientX, e.clientY);
scope.querySelectorAll('.drop-box.over, .dnd-pool.over').forEach(n => n.classList.remove('over'));
const tgt = under.find(n => n.classList && (n.classList.contains('drop-box') || n.classList.contains('dnd-pool')));
if(tgt) tgt.classList.add('over');
}
}
function onUp(e){
el.removeEventListener('pointermove', onMove);
el.removeEventListener('pointerup', onUp);
el.removeEventListener('pointercancel', onUp);
if(captured){ try { el.releasePointerCapture(ev.pointerId); } catch(_){} }
el.classList.remove('dragging');
if(ghost){ ghost.remove(); ghost = null; }
scope.querySelectorAll('.drop-box.over, .dnd-pool.over').forEach(n => n.classList.remove('over'));
if(dragging){
const under = document.elementsFromPoint(e.clientX, e.clientY);
const box = under.find(n => n.classList && n.classList.contains('drop-box'));
const pl = under.find(n => n.classList && n.classList.contains('dnd-pool'));
if(box){
const di = box.querySelector('[data-cat]');
if(di){ placed[itId] = di.dataset.cat; armed = null; render(); return; }
} else if(pl){ delete placed[itId]; armed = null; render(); return; }
// fell outside — revert
} else {
// tap fallback
if(placed[itId]){ delete placed[itId]; armed = null; render(); }
else { armed = (armed === itId) ? null : itId; render(); }
}
dragging = false;
}
el.addEventListener('pointermove', onMove);
el.addEventListener('pointerup', onUp);
el.addEventListener('pointercancel', onUp);
});
}
function attachBoxTaps(){
scope.querySelectorAll('.drop-box').forEach(box => {
box.addEventListener('click', ev => {
if(!armed) return;
if(ev.target.closest('.dnd-chip')) return;
const di = box.querySelector('[data-cat]');
if(di){ placed[armed] = di.dataset.cat; armed = null; render(); }
});
});
pool.addEventListener('click', ev => {
if(!armed) return;
if(ev.target.closest('.dnd-chip')) return;
// empty pool click also de-arms
armed = null; render();
});
}
function render(){
pool.innerHTML = '';
cfg.items.forEach(it => {
if(placed[it.id]) return;
const chip = buildChip(it, false);
if(armed === it.id) chip.classList.add('armed');
pool.appendChild(chip);
});
cfg.cats.forEach(cat => {
const box = scope.querySelector('.drop-items[data-cat="' + cat + '"]');
if(!box) return;
box.innerHTML = '';
cfg.items.forEach(it => {
if(placed[it.id] !== cat) return;
box.appendChild(buildChip(it, true));
});
});
if(window.renderMathInElement) try { renderMath(scope); } catch(_){}
}
attachBoxTaps();
render();
return { placed, render, reset(){ for(const k in placed) delete placed[k]; armed = null; render(); } };
}
const DND_HINT_HTML = '<div class="dnd-hint"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 11V6a3 3 0 0 1 6 0v5"/><path d="M9 11h6v8a4 4 0 0 1-8 0z"/></svg> Перетащите карточку или нажмите её, затем — на нужный ящик.</div>';
/* INIT */
function initSidebarToggle(){
const side = document.getElementById('col-side');
@@ -1068,8 +1216,9 @@ function buildP10(){
<button class="btn primary" id="p10f-start" style="margin-top:10px">Начать</button>`);
/* INT 5 — Drag: разложимо или нет */
html += widget('Разложимо или нет?','INTERACT 5','По знаку дискриминанта разнесите трёхчлены: раскладываются на множители или нет.',`
<div id="p10z-pool" style="display:flex;flex-wrap:wrap;gap:8px;margin-bottom:14px"></div>
html += widget('Разложимо или нет?','INTERACT 5','По знаку дискриминанта разнесите трёхчлены в нужные ящики.',`
${DND_HINT_HTML}
<div id="p10z-pool"></div>
<div class="drop-row" style="display:grid;grid-template-columns:1fr 1fr;gap:10px">
<div class="drop-box"><h5>Раскладывается ($D \\geq 0$)</h5><div class="drop-items" data-cat="yes"></div></div>
<div class="drop-box"><h5>Не раскладывается ($D < 0$)</h5><div class="drop-items" data-cat="no"></div></div>
@@ -1246,64 +1395,26 @@ function buildP10(){
/* INIT 5 — Drag */
(function(){
const items = [
{ id:1, txt:'$x^2 - 5x + 6$', cat:'yes' },
{ id:2, txt:'$x^2 + 1$', cat:'no' },
{ id:3, txt:'$2x^2 + 5x - 3$', cat:'yes' },
{ id:4, txt:'$x^2 - 2x + 5$', cat:'no' },
{ id:5, txt:'$x^2 - 9$', cat:'yes' },
{ id:6, txt:'$3x^2 + x + 1$', cat:'no' },
{ id:7, txt:'$x^2 + 6x + 9$', cat:'yes' },
{ id:8, txt:'$x^2 + 4$', cat:'no' },
{ id:1, html:'$x^2 - 5x + 6$', cat:'yes' },
{ id:2, html:'$x^2 + 1$', cat:'no' },
{ id:3, html:'$2x^2 + 5x - 3$', cat:'yes' },
{ id:4, html:'$x^2 - 2x + 5$', cat:'no' },
{ id:5, html:'$x^2 - 9$', cat:'yes' },
{ id:6, html:'$3x^2 + x + 1$', cat:'no' },
{ id:7, html:'$x^2 + 6x + 9$', cat:'yes' },
{ id:8, html:'$x^2 + 4$', cat:'no' },
];
const cats = ['yes','no'];
const labels = { yes:'Раскл.', no:'Не раскл.' };
let placed = {};
function makeChip(it, where){
const wrap = document.createElement('div');
wrap.style.cssText = 'display:inline-flex;align-items:center;gap:4px;background:var(--sec-acc-soft);border-radius:8px;padding:3px 6px;margin:2px';
const sp = document.createElement('span');
sp.innerHTML = it.txt; sp.style.cssText = 'padding:2px 4px';
wrap.appendChild(sp);
if(where === 'pool'){
cats.forEach(cat=>{
const b = document.createElement('button');
b.className = 'btn small'; b.textContent = labels[cat];
b.style.cssText = 'padding:3px 7px;font-size:.72rem';
b.addEventListener('click', ()=>{ placed[it.id] = cat; render(); });
wrap.appendChild(b);
});
} else {
const b = document.createElement('button');
b.className = 'btn small'; b.textContent = '×';
b.style.cssText = 'padding:2px 7px';
b.addEventListener('click', ()=>{ delete placed[it.id]; render(); });
wrap.appendChild(b);
}
return wrap;
}
function render(){
const pool = document.getElementById('p10z-pool');
pool.innerHTML = '';
items.forEach(it=>{ if(!placed[it.id]) pool.appendChild(makeChip(it, 'pool')); });
cats.forEach(cat=>{
const box = document.querySelector('#p10-body .drop-items[data-cat="' + cat + '"]');
if(!box) return;
box.innerHTML = '';
items.forEach(it=>{ if(placed[it.id] === cat) box.appendChild(makeChip(it, 'placed')); });
});
if(window.renderMathInElement) renderMath(pool.parentElement);
}
const sorter = setupSorter({ poolId:'p10z-pool', cats:['yes','no'], items, scopeSelector:'#p10-body' });
document.getElementById('p10z-check').addEventListener('click', ()=>{
const fb = document.getElementById('p10z-fb');
fb.style.display = 'block';
const placedCount = Object.keys(placed).length;
const placedCount = Object.keys(sorter.placed).length;
if(placedCount < items.length){ feedback(fb, false, '&#9888; Разложите все ' + items.length + ' трёхчленов.'); return; }
let ok = 0; items.forEach(it=>{ if(placed[it.id] === it.cat) ok++; });
let ok = 0; items.forEach(it=>{ if(sorter.placed[it.id] === it.cat) ok++; });
if(ok === items.length){ feedback(fb, true, '&#10003; Все ' + items.length + ' верно!'); achievement('p10_sort'); bumpProgress('p10', 14); confetti(); }
else feedback(fb, false, 'Верно ' + ok + ' из ' + items.length);
});
document.getElementById('p10z-reset').addEventListener('click', ()=>{ placed = {}; document.getElementById('p10z-fb').style.display='none'; render(); });
render();
document.getElementById('p10z-reset').addEventListener('click', ()=>{ sorter.reset(); document.getElementById('p10z-fb').style.display='none'; });
})();
}
function buildP11stub(){ buildP11(); }
@@ -1372,8 +1483,9 @@ function buildP11(){
<div id="p11d-sol" style="display:none;margin-top:10px;padding:12px;background:var(--card);border-radius:8px;border:1px solid var(--border)"></div>`);
/* INT 5 — Drag: типы задач */
html += widget('Классифицируем тип задачи','INTERACT 5','Прочитайте задачу — кликом отнесите её к одной из четырёх категорий.',`
<div id="p11c-pool" style="display:flex;flex-direction:column;gap:6px;margin-bottom:14px"></div>
html += widget('Классифицируем тип задачи','INTERACT 5','Прочитайте задачу и перетащите её в нужный ящик.',`
${DND_HINT_HTML}
<div id="p11c-pool"></div>
<div class="drop-row" style="display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:10px">
<div class="drop-box"><h5>Движение</h5><div class="drop-items" data-cat="dv"></div></div>
<div class="drop-box"><h5>Работа</h5><div class="drop-items" data-cat="wk"></div></div>
@@ -1513,57 +1625,24 @@ function buildP11(){
/* INIT 5 — Drag типы */
(function(){
const items = [
{ id:1, txt:'Лодка прошла 24 км по течению и обратно.', cat:'dv' },
{ id:2, txt:'Двое рабочих вместе закончили работу за 6 ч.', cat:'wk' },
{ id:3, txt:'Произведение последовательных чисел равно 90.', cat:'nm' },
{ id:4, txt:'Сторона квадрата увеличена на 3 см, площадь увеличилась на 33 см².', cat:'gm' },
{ id:5, txt:'Автомобиль и автобус выехали навстречу из A и B.', cat:'dv' },
{ id:6, txt:'Сумма квадратов цифр двузначного числа = 25.', cat:'nm' },
{ id:7, txt:'Бассейн наполняется одной трубой на 2 ч быстрее, чем другой.', cat:'wk' },
{ id:8, txt:'Площадь прямоугольника 48 см², а периметр 28 см.', cat:'gm' },
{ id:1, html:'Лодка прошла 24 км по течению и обратно.', cat:'dv' },
{ id:2, html:'Двое рабочих вместе закончили работу за 6 ч.', cat:'wk' },
{ id:3, html:'Произведение последовательных чисел равно 90.', cat:'nm' },
{ id:4, html:'Сторона квадрата увеличена на 3 см, площадь увеличилась на 33 см².', cat:'gm' },
{ id:5, html:'Автомобиль и автобус выехали навстречу из A и B.', cat:'dv' },
{ id:6, html:'Сумма квадратов цифр двузначного числа = 25.', cat:'nm' },
{ id:7, html:'Бассейн наполняется одной трубой на 2 ч быстрее, чем другой.', cat:'wk' },
{ id:8, html:'Площадь прямоугольника 48 см², а периметр 28 см.', cat:'gm' },
];
const cats = ['dv','wk','nm','gm'];
const labels = { dv:'Движ.', wk:'Раб.', nm:'Числа', gm:'Геом.' };
let placed = {};
function makeChip(it, where){
const wrap = document.createElement('div');
wrap.style.cssText = 'display:inline-flex;align-items:center;gap:4px;background:var(--sec-acc-soft);border-radius:8px;padding:4px 6px;font-size:.86rem;flex-wrap:wrap;width:100%';
const sp = document.createElement('span'); sp.textContent = it.txt; sp.style.cssText = 'padding:2px 4px;flex:1;min-width:140px';
wrap.appendChild(sp);
if(where === 'pool'){
cats.forEach(cat=>{
const b = document.createElement('button'); b.className = 'btn small'; b.textContent = labels[cat];
b.style.cssText = 'padding:3px 7px;font-size:.7rem';
b.addEventListener('click', ()=>{ placed[it.id] = cat; render(); });
wrap.appendChild(b);
});
} else {
const b = document.createElement('button'); b.className = 'btn small'; b.textContent = '×';
b.style.cssText = 'padding:2px 7px'; b.addEventListener('click', ()=>{ delete placed[it.id]; render(); });
wrap.appendChild(b);
}
return wrap;
}
function render(){
const pool = document.getElementById('p11c-pool');
pool.innerHTML = '';
items.forEach(it=>{ if(!placed[it.id]) pool.appendChild(makeChip(it, 'pool')); });
cats.forEach(cat=>{
const box = document.querySelector('#p11-body .drop-items[data-cat="' + cat + '"]');
if(!box) return;
box.innerHTML = '';
items.forEach(it=>{ if(placed[it.id] === cat) box.appendChild(makeChip(it, 'placed')); });
});
}
const sorter = setupSorter({ poolId:'p11c-pool', cats:['dv','wk','nm','gm'], items, scopeSelector:'#p11-body', columnLayout:true });
document.getElementById('p11c-check').addEventListener('click', ()=>{
const fb = document.getElementById('p11c-fb'); fb.style.display = 'block';
if(Object.keys(placed).length < items.length){ feedback(fb, false, '&#9888; Разложите все ' + items.length + ' задач.'); return; }
let ok = 0; items.forEach(it=>{ if(placed[it.id] === it.cat) ok++; });
if(Object.keys(sorter.placed).length < items.length){ feedback(fb, false, '&#9888; Разложите все ' + items.length + ' задач.'); return; }
let ok = 0; items.forEach(it=>{ if(sorter.placed[it.id] === it.cat) ok++; });
if(ok === items.length){ feedback(fb, true, '&#10003; Все верно!'); achievement('p11_class'); bumpProgress('p11', 14); confetti(); }
else feedback(fb, false, 'Верно ' + ok + ' из ' + items.length);
});
document.getElementById('p11c-reset').addEventListener('click', ()=>{ placed = {}; document.getElementById('p11c-fb').style.display='none'; render(); });
render();
document.getElementById('p11c-reset').addEventListener('click', ()=>{ sorter.reset(); document.getElementById('p11c-fb').style.display='none'; });
})();
}
function buildP12stub(){ buildP12(); }
@@ -2203,12 +2282,13 @@ function buildP7(){
<div id="p7c-roots" style="text-align:center;margin-bottom:6px"></div>`);
/* ===== INTERACTIVE 2: Drag-сортировка ===== */
html += widget('Сортировка уравнений по типу','INTERACT 2','Кликом отправьте каждое уравнение в нужный ящик. Серый — лишнее (не квадратное).',`
<div id="p7s-pool" style="display:flex;flex-wrap:wrap;gap:8px;margin-bottom:14px"></div>
html += widget('Сортировка уравнений по типу','INTERACT 2','Перетащите каждое уравнение в нужный ящик. На тач-экранах: тап по карточке, затем тап по ящику.',`
${DND_HINT_HTML}
<div id="p7s-pool"></div>
<div class="drop-row" style="display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:10px">
<div class="drop-box" data-type="full"><h5>Полное</h5><div class="drop-items" data-cat="full"></div></div>
<div class="drop-box" data-type="incomplete"><h5>Неполное</h5><div class="drop-items" data-cat="incomplete"></div></div>
<div class="drop-box" data-type="notquad"><h5>Не квадратное</h5><div class="drop-items" data-cat="notquad"></div></div>
<div class="drop-box"><h5>Полное</h5><div class="drop-items" data-cat="full"></div></div>
<div class="drop-box"><h5>Неполное</h5><div class="drop-items" data-cat="incomplete"></div></div>
<div class="drop-box"><h5>Не квадратное</h5><div class="drop-items" data-cat="notquad"></div></div>
</div>
<div class="actions"><button class="btn primary" id="p7s-check">Проверить</button><button class="btn" id="p7s-reset">Сначала</button></div>
<div class="feedback" id="p7s-fb" style="display:none"></div>`);
@@ -2352,76 +2432,27 @@ function buildP7(){
/* ===== INIT INTERACTIVE 2 ===== */
(function initSort(){
const items = [
{ id:1, txt:'$2x^2 + 3x - 5 = 0$', cat:'full' },
{ id:2, txt:'$x^2 - 16 = 0$', cat:'incomplete' },
{ id:3, txt:'$3x^2 + 6x = 0$', cat:'incomplete' },
{ id:4, txt:'$x + 5 = 0$', cat:'notquad' },
{ id:5, txt:'$5x^2 - 2x + 1 = 0$', cat:'full' },
{ id:6, txt:'$7x^2 = 0$', cat:'incomplete' },
{ id:7, txt:'$x^3 - x = 0$', cat:'notquad' },
{ id:8, txt:'$x^2 + x + 1 = 0$', cat:'full' },
{ id:1, html:'$2x^2 + 3x - 5 = 0$', cat:'full' },
{ id:2, html:'$x^2 - 16 = 0$', cat:'incomplete' },
{ id:3, html:'$3x^2 + 6x = 0$', cat:'incomplete' },
{ id:4, html:'$x + 5 = 0$', cat:'notquad' },
{ id:5, html:'$5x^2 - 2x + 1 = 0$', cat:'full' },
{ id:6, html:'$7x^2 = 0$', cat:'incomplete' },
{ id:7, html:'$x^3 - x = 0$', cat:'notquad' },
{ id:8, html:'$x^2 + x + 1 = 0$', cat:'full' },
];
const pool = document.getElementById('p7s-pool');
const cats = ['full','incomplete','notquad'];
const labels = { full:'Полное', incomplete:'Неполное', notquad:'Не квадр.' };
let placed = {};
function makeChip(it, where){
const wrap = document.createElement('div');
wrap.className = 'chip-wrap';
wrap.style.cssText = 'display:inline-flex;align-items:center;gap:4px;background:var(--sec-acc-soft);border-radius:8px;padding:3px 6px;margin:2px';
const sp = document.createElement('span');
sp.className = 'chip';
sp.style.cssText = 'background:transparent;padding:2px 4px';
sp.innerHTML = it.txt;
wrap.appendChild(sp);
if(where === 'pool'){
cats.forEach(cat=>{
const b = document.createElement('button');
b.className = 'btn small';
b.textContent = labels[cat];
b.style.cssText = 'padding:3px 7px;font-size:.7rem';
b.addEventListener('click', ()=>{ placed[it.id] = cat; renderAll(); });
wrap.appendChild(b);
});
} else {
const b = document.createElement('button');
b.className = 'btn small';
b.textContent = '×';
b.style.cssText = 'padding:2px 7px';
b.addEventListener('click', ()=>{ delete placed[it.id]; renderAll(); });
wrap.appendChild(b);
}
return wrap;
}
function render(){
pool.innerHTML = '';
items.forEach(it=>{
if(placed[it.id]) return;
pool.appendChild(makeChip(it, 'pool'));
});
cats.forEach(cat=>{
const box = document.querySelector('.drop-items[data-cat="' + cat + '"]');
box.innerHTML = '';
items.forEach(it=>{
if(placed[it.id] !== cat) return;
box.appendChild(makeChip(it, 'placed'));
});
});
if(window.renderMathInElement) renderMath(document.getElementById('p7s-pool').parentElement);
}
function renderAll(){ render(); }
const sorter = setupSorter({ poolId:'p7s-pool', cats:['full','incomplete','notquad'], items, scopeSelector:'#p7-body' });
document.getElementById('p7s-check').addEventListener('click', ()=>{
const total = items.length;
const placedCount = Object.keys(placed).length;
if(placedCount < total){ feedback(document.getElementById('p7s-fb'), false, '&#9888; Разложите ВСЕ уравнения по ящикам.'); document.getElementById('p7s-fb').style.display='block'; return; }
let ok = 0; items.forEach(it=>{ if(placed[it.id] === it.cat) ok++; });
const placedCount = Object.keys(sorter.placed).length;
const fb = document.getElementById('p7s-fb');
fb.style.display = 'block';
if(placedCount < total){ feedback(fb, false, '&#9888; Разложите ВСЕ уравнения по ящикам.'); return; }
let ok = 0; items.forEach(it=>{ if(sorter.placed[it.id] === it.cat) ok++; });
if(ok === total){ feedback(fb, true, '&#10003; Идеально! Все ' + total + ' верно.'); achievement('p7_sort'); bumpProgress('p7', 14); confetti(); }
else feedback(fb, false, 'Верно ' + ok + ' из ' + total + '. Попробуйте перепроверить.');
});
document.getElementById('p7s-reset').addEventListener('click', ()=>{ placed = {}; document.getElementById('p7s-fb').style.display='none'; renderAll(); });
renderAll();
document.getElementById('p7s-reset').addEventListener('click', ()=>{ sorter.reset(); document.getElementById('p7s-fb').style.display='none'; });
})();
/* ===== INIT INTERACTIVE 3 ===== */