/* math6_engine.js — общий движок-плумбинг интерактивного учебника «Математика 6». * * Каждая страница-глава объявляет конфиг `window.M6` и (опционально) функции-билдеры * buildXX(), затем подключает этот файл. Движок строит para-selector, навигацию, * прогресс/XP/достижения, сайдбар (шпаргалка + подсказка), поиск, глоссарий, тему. * * § без билдера автоматически получает заглушку (.m6-placeholder + кнопка прочтения), * поэтому каркас новой главы = только M6.paras. Кастомные интерактивы § пишутся * inline-билдерами на странице и пользуются глобальными хелперами этого движка: * makeCard, secNav, readBtn, feedback, renderMath, fmt, num, addXp, bumpProgress, * achievement, setupSorter, confetti, goTo, M6icon. * * Конфиг window.M6 = { * slug, lsPrefix, xpKey, paras:[{id,num,name,sub,final?,applied?}], * achLabels:{}, startAch:[id,text], finalAch:[id,text], * sidebars:{id:{title,rows:[[k,v]]}}, tips:[{sec,html}], * glossary:[{term,def,sec,aliases:[]}], searchRows:[[kind,title,desc,sec]], * builders:{id:fn}, footer * } */ (function () { 'use strict'; if (window.__M6_ENGINE) return; window.__M6_ENGINE = true; var M6 = window.M6 || (window.M6 = {}); var LSPRE = function () { return M6.lsPrefix || 'math6'; }; var XPKEY = function () { return M6.xpKey || 'math6_xp'; }; /* ============================================================ STATE */ var STATE = { current: null, progress: {}, achievements: new Map(), xp: 0, level: 1 }; window.M6STATE = STATE; function paras() { return M6.paras || []; } function total() { return paras().length || 1; } function calcLevel(xp) { return Math.floor(Math.sqrt((xp || 0) / 100)) + 1; } function _xpForLevel(lv) { return (lv - 1) * (lv - 1) * 100; } function loadProgress() { paras().forEach(function (p) { if (STATE.progress[p.id] == null) STATE.progress[p.id] = 0; }); try { var s = localStorage.getItem(LSPRE() + '_progress'); if (s) Object.assign(STATE.progress, JSON.parse(s)); var a = localStorage.getItem(LSPRE() + '_achievements'); if (a) { var p = JSON.parse(a); if (Array.isArray(p)) p.forEach(function (id) { STATE.achievements.set(id, achLabel(id)); }); else if (p && typeof p === 'object') Object.keys(p).forEach(function (id) { STATE.achievements.set(id, (p[id] && p[id] !== id) ? p[id] : achLabel(id)); }); } STATE.xp = +(localStorage.getItem(XPKEY()) || 0); STATE.level = calcLevel(STATE.xp); } catch (e) {} } function saveProgress() { try { localStorage.setItem(LSPRE() + '_progress', JSON.stringify(STATE.progress)); localStorage.setItem(LSPRE() + '_achievements', JSON.stringify(Object.fromEntries(STATE.achievements))); localStorage.setItem(XPKEY(), String(STATE.xp)); } catch (e) {} } function achLabel(id) { return (M6.achLabels && M6.achLabels[id]) || id; } function bumpProgress(key, delta) { STATE.progress[key] = Math.max(0, Math.min(100, (STATE.progress[key] || 0) + delta)); saveProgress(); refreshProgressUI(); if (STATE.progress[key] >= 50) markParaRead(key); if (STATE.progress[key] >= 100) { achievement(key + '_done'); var fin = paras().filter(function (p) { return p.final; }).map(function (p) { return p.id; }); if (fin.indexOf(key) >= 0 && M6.finalAch) achievement(M6.finalAch[0], M6.finalAch[1]); } } /* ====================================================== SERVER SYNC */ var _markedRead = new Set(); var _pendingProgressBody = null, _progressTimer = null; function _flushProgress() { var body = _pendingProgressBody; _pendingProgressBody = null; if (!body) return; var tok = (window.LS && LS.getToken) ? LS.getToken() : ''; if (!tok) return; fetch('/api/textbooks/' + M6.slug + '/progress', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + tok }, body: JSON.stringify(body), keepalive: true }).catch(function () {}); } function _queueProgress(patch) { _pendingProgressBody = Object.assign(_pendingProgressBody || {}, patch); if (_progressTimer) clearTimeout(_progressTimer); _progressTimer = setTimeout(_flushProgress, 600); } function markLastPara(id) { _queueProgress({ last_para: id }); } function markParaRead(id) { if (_markedRead.has(id)) return; _markedRead.add(id); _queueProgress({ mark_read: id }); } window.addEventListener('beforeunload', _flushProgress); function loadServerReadState() { var tok = (window.LS && LS.getToken) ? LS.getToken() : ''; if (!tok) return; fetch('/api/textbooks/' + M6.slug, { headers: { 'Authorization': 'Bearer ' + tok } }) .then(function (r) { return r.ok ? r.json() : null; }) .then(function (d) { if (!d || !d.progress) return; (d.progress.read || []).forEach(function (k) { _markedRead.add(k); if ((STATE.progress[k] || 0) < 50) STATE.progress[k] = 100; }); saveProgress(); refreshProgressUI(); }).catch(function () {}); } /* ============================================================ XP */ function addXp(n, src) { if (!n) return; var prev = STATE.level; STATE.xp = Math.max(0, (STATE.xp || 0) + n); STATE.level = calcLevel(STATE.xp); saveProgress(); refreshProgressUI(); if (window.LS && window.LS.xp) window.LS.xp.add(n, (M6.slug || 'math6') + '-' + (src || 'misc')); if (STATE.level > prev) { popup('Уровень ' + STATE.level + '!'); if (window.confetti) try { confetti(); } catch (e) {} } } function refreshProgressUI() { var sum = 0; paras().forEach(function (p) { sum += (STATE.progress[p.id] || 0); }); var tot = Math.round(sum / total()); var f = document.getElementById('hero-hp-fill'); if (f) f.style.width = tot + '%'; var t = document.getElementById('hero-hp-text'); if (t) t.textContent = tot + '% пройдено'; document.querySelectorAll('[data-prog-card]').forEach(function (el) { var k = el.dataset.progCard; var fl = el.querySelector('.psel-prog-fill'); if (fl) fl.style.width = (STATE.progress[k] || 0) + '%'; }); var xpBadge = document.getElementById('hero-xp-badge'); if (xpBadge) xpBadge.innerHTML = ' Ур. ' + STATE.level + ' · ' + (STATE.xp || 0) + ' XP'; if (STATE.current && document.getElementById('sidebar-content')) { try { buildSidebar(STATE.current); } catch (e) {} } } function popup(text, gold) { var pop = document.getElementById('ach-popup'); if (!pop) return; document.getElementById('ach-text').textContent = text; pop.classList.add('show'); setTimeout(function () { pop.classList.remove('show'); }, gold ? 3300 : 2600); } function achievement(id, text) { if (STATE.achievements.has(id)) return; if (!text && !(M6.achLabels && M6.achLabels[id])) return; /* неизвестные id игнорируем */ STATE.achievements.set(id, text || achLabel(id)); saveProgress(); popup(text || achLabel(id), true); addXp(20, 'ach-' + id); } /* ================================================ SECTIONS */ function buildSections() { var host = document.getElementById('sections'); if (!host) return; /* статичные секции уже в HTML */ if (host.dataset.built) return; host.dataset.built = '1'; var html = ''; paras().forEach(function (p) { var wm = p.wm || (p.final ? '★' : (M6.wm || '')); var numCls = p.final ? ' style="background:linear-gradient(135deg,var(--warn,#f59e0b),var(--pri))"' : ''; html += '
' + '
' + p.num + '

' + p.name + '

' + '
'; }); host.innerHTML = html; } /* ================================================ PARA SELECTOR/NAV */ function buildParaSelector() { var g = document.getElementById('psel-grid'); if (!g) return; g.innerHTML = ''; paras().forEach(function (p) { var card = document.createElement('div'); card.className = 'psel-card' + (p.final ? ' final' : '') + (p.applied ? ' applied' : ''); card.dataset.id = p.id; card.dataset.progCard = p.id; card.innerHTML = '
' + p.num + '
' + p.name + '
'; card.addEventListener('click', function () { goTo(p.id); }); g.appendChild(card); }); } var BUILT = new Set(); function ensureBuilt(id) { if (BUILT.has(id)) return; BUILT.add(id); var cfg = window.M6 || M6; /* читаем актуальный конфиг из window */ var fn = cfg.builders && cfg.builders[id]; if (fn) { try { fn(); } catch (e) { placeholder(id); } } else placeholder(id); } function placeholder(id) { var box = document.getElementById(id + '-body'); if (!box) return; box.innerHTML = '
Содержание этого параграфа готовится.
' + readBtn(id); } function goTo(id) { STATE.current = id; ensureBuilt(id); document.querySelectorAll('.sec').forEach(function (s) { s.classList.remove('active'); }); var el = document.getElementById('sec-' + id); if (el) el.classList.add('active'); document.querySelectorAll('.psel-card').forEach(function (c) { c.classList.toggle('active', c.dataset.id === id); }); buildSidebar(id); window.scrollTo({ top: 0, behavior: 'smooth' }); if ((STATE.progress[id] || 0) < 10) bumpProgress(id, 10); if (el && window.renderMathInElement) setTimeout(function () { renderMath(el); }, 0); setTimeout(function () { try { wrapGlossary(el); } catch (e) {} }, 60); markLastPara(id); if (window.innerWidth <= 980) { var side = document.getElementById('col-side'), bk = document.getElementById('col-side-backdrop'); if (side) side.classList.remove('open'); if (bk) bk.classList.remove('show'); } } /* ============================================================ SIDEBAR */ function buildSidebar(id) { var box = document.getElementById('sidebar-content'); if (!box) return; var SB = M6.sidebars || {}; var sb = SB[id] || SB[Object.keys(SB)[0]] || { title: 'Шпаргалка', rows: [] }; var html = ''; var xpForLv = _xpForLevel(STATE.level), xpNext = _xpForLevel(STATE.level + 1); var xpInLv = STATE.xp - xpForLv, xpRange = xpNext - xpForLv; var xpPct = xpRange > 0 ? Math.round(xpInLv / xpRange * 100) : 100; html += '
XP-прогрессУр. ' + STATE.level + '
' + STATE.xp + ' XP' + xpNext + ' XP
'; html += '

' + sb.title + '

'; (sb.rows || []).forEach(function (r) { html += '
' + r[0] + '' + (r[1] ? ' — ' + r[1] : '') + '
'; }); html += '
'; var tips = M6.tips || []; var tip = tips.filter(function (t) { return t.sec === id; })[0] || tips[0]; if (tip) html += '

Подсказка

' + tip.html + '
'; if (STATE.achievements.size > 0) { html += '

Достижения ' + STATE.achievements.size + '

'; Array.from(STATE.achievements.values()).slice(-5).forEach(function (text) { html += '
✓ ' + text + '
'; }); html += '
'; } box.innerHTML = html; if (window.renderMathInElement) try { renderMath(box); } catch (e) {} } /* ============================================================ THEME */ function initTheme() { var key = LSPRE() + '_theme'; var t = localStorage.getItem(key) || localStorage.getItem('theme') || 'light'; if (t === 'dark') document.documentElement.classList.add('dark'); var lab = document.getElementById('theme-lab'); if (lab) lab.textContent = t === 'dark' ? 'Светлая' : 'Тёмная'; var btn = document.getElementById('theme-btn'); if (!btn) return; btn.addEventListener('click', function () { document.documentElement.classList.toggle('dark'); var dark = document.documentElement.classList.contains('dark'); localStorage.setItem(key, dark ? 'dark' : 'light'); localStorage.setItem('theme', dark ? 'dark' : 'light'); if (lab) lab.textContent = dark ? 'Светлая' : 'Тёмная'; }); } /* ============================================================ HELPERS */ function renderMath(root) { if (root && window.renderMathInElement) { try { renderMathInElement(root, { delimiters: [{ left: '$$', right: '$$', display: true }, { left: '$', right: '$', display: false }, { left: '\\[', right: '\\]', display: true }, { left: '\\(', right: '\\)', display: false }], throwOnError: false }); } catch (e) {} } } function feedback(elm, ok, text) { if (!elm) return; elm.className = 'feedback ' + (ok ? 'ok' : 'fail'); elm.innerHTML = text || (ok ? '✓ Верно!' : '✗ Неверно'); elm.style.display = 'block'; try { renderMath(elm); } catch (e) {} } function num(n) { if (!isFinite(n)) return '?'; if (Number.isInteger(n)) return String(n); return (Math.round(n * 1e9) / 1e9).toString().replace('.', ','); } function fmt(n) { return num(n); } var ICONS = { repeat: '', theory: '', algo: '', rule: '', example: '', oral: '' }; function M6icon(k) { return ICONS[k] || ''; } function makeCard(kind, title, n, body) { var labels = { repeat: 'Повторение', theory: 'Теория', algo: 'Алгоритм', rule: 'Правило', example: 'Пример', oral: 'Устно' }; return '
' + (ICONS[kind] || '') + '
' + (labels[kind] || '') + (title && title !== labels[kind] ? ' · ' + title : '') + '
' + (n ? '
' + n + '
' : '') + '
' + body + '
'; } function shortName(id) { var p = paras().filter(function (x) { return x.id === id; })[0]; return p ? p.num : id; } function secNav(prev, next) { var h = '
'; h += prev ? '' : ''; h += next ? '' : ''; h += '
'; return h; } function readBtn(id, label) { return '
'; } document.addEventListener('click', function (e) { var b = e.target.closest && e.target.closest('[data-read]'); if (!b) return; var id = b.getAttribute('data-read'); addXp(10, id + '-read'); bumpProgress(id, 30); b.textContent = 'Прочитано! +10 XP'; b.disabled = true; b.style.opacity = .6; }); /* ============================================================ CONFETTI */ var _cc = null, _cp = [], _craf = null; function confetti() { try { if (/jsdom/i.test(navigator.userAgent || '')) return; /* headless-guard: canvas в jsdom не реализован */ if (!_cc) { _cc = document.createElement('canvas'); _cc.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:9999'; document.body.appendChild(_cc); } var c = _cc; c.width = window.innerWidth; c.height = window.innerHeight; var ctx = c.getContext('2d'); if (!ctx) return; var colors = ['#4f46e5', '#6366f1', '#0891b2', '#10b981', '#f59e0b', '#e11d48']; for (var i = 0; i < 80; i++) _cp.push({ x: window.innerWidth / 2 + (Math.random() - .5) * 200, y: window.innerHeight / 2, vx: (Math.random() - .5) * 14, vy: -10 - Math.random() * 10, g: .4, life: 100, color: colors[i % colors.length], r: 4 + Math.random() * 4, rot: 0, vRot: (Math.random() - .5) * .3 }); if (_craf) cancelAnimationFrame(_craf); function frame() { ctx.clearRect(0, 0, c.width, c.height); _cp = _cp.filter(function (p) { p.x += p.vx; p.y += p.vy; p.vy += p.g; p.life--; p.rot += p.vRot; ctx.save(); ctx.translate(p.x, p.y); ctx.rotate(p.rot); ctx.fillStyle = p.color; ctx.fillRect(-p.r, -p.r / 2, p.r * 2, p.r); ctx.restore(); return p.life > 0 && p.y < c.height + 50; }); if (_cp.length > 0) _craf = requestAnimationFrame(frame); else { ctx.clearRect(0, 0, c.width, c.height); _craf = null; } } frame(); } catch (e) {} } /* ============================================================ SORTER (DnD) */ function setupSorter(cfg) { var placed = {}; var pool = document.getElementById(cfg.poolId); var scope = document.querySelector(cfg.scopeSelector); if (!pool || !scope) return { placed: placed, render: function () {}, reset: function () {} }; pool.classList.add('dnd-pool'); if (cfg.columnLayout) pool.classList.add('col'); var armed = null; function buildChip(it, isPlaced) { var e = document.createElement('div'); e.className = 'dnd-chip' + (isPlaced ? ' placed' : ''); e.dataset.id = it.id; e.innerHTML = '' + it.html + '×'; attach(e, it.id); return e; } function attach(elm, itId) { elm.addEventListener('pointerdown', function (ev) { if (ev.button !== undefined && ev.button !== 0) return; ev.preventDefault(); 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; } var sx = ev.clientX, sy = ev.clientY; var r = elm.getBoundingClientRect(); var ox = ev.clientX - r.left, oy = ev.clientY - r.top; var ghost = null, dragging = false; try { elm.setPointerCapture(ev.pointerId); } catch (e) {} function onMove(e2) { var dx = e2.clientX - sx, dy = e2.clientY - sy; if (!dragging && Math.hypot(dx, dy) > 8) { dragging = true; ghost = elm.cloneNode(true); 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:' + r.width + 'px;left:' + (e2.clientX - ox) + 'px;top:' + (e2.clientY - oy) + 'px'; document.body.appendChild(ghost); elm.classList.add('dragging'); } if (dragging && ghost) { ghost.style.left = (e2.clientX - ox) + 'px'; ghost.style.top = (e2.clientY - oy) + 'px'; var under = document.elementsFromPoint(e2.clientX, e2.clientY); scope.querySelectorAll('.drop-box.over,.dnd-pool.over').forEach(function (n) { n.classList.remove('over'); }); var tgt = under.find(function (n) { return n.classList && (n.classList.contains('drop-box') || n.classList.contains('dnd-pool')); }); if (tgt) tgt.classList.add('over'); } } function onUp(e2) { elm.removeEventListener('pointermove', onMove); elm.removeEventListener('pointerup', onUp); elm.removeEventListener('pointercancel', onUp); elm.classList.remove('dragging'); if (ghost) { ghost.remove(); ghost = null; } scope.querySelectorAll('.drop-box.over,.dnd-pool.over').forEach(function (n) { n.classList.remove('over'); }); if (dragging) { var under = document.elementsFromPoint(e2.clientX, e2.clientY); var box = under.find(function (n) { return n.classList && n.classList.contains('drop-box'); }); var pl = under.find(function (n) { return n.classList && n.classList.contains('dnd-pool'); }); if (box) { var 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; } } else { if (placed[itId]) { delete placed[itId]; armed = null; render(); } else { armed = (armed === itId) ? null : itId; render(); } } dragging = false; } elm.addEventListener('pointermove', onMove); elm.addEventListener('pointerup', onUp); elm.addEventListener('pointercancel', onUp); }); } function attachBoxTaps() { scope.querySelectorAll('.drop-box').forEach(function (box) { box.addEventListener('click', function (ev) { if (!armed) return; if (ev.target.closest('.dnd-chip')) return; var di = box.querySelector('[data-cat]'); if (di) { placed[armed] = di.dataset.cat; armed = null; render(); } }); }); } function render() { pool.innerHTML = ''; cfg.items.forEach(function (it) { if (placed[it.id]) return; var c = buildChip(it, false); if (armed === it.id) c.classList.add('armed'); pool.appendChild(c); }); cfg.cats.forEach(function (cat) { var box = scope.querySelector('.drop-items[data-cat="' + cat + '"]'); if (!box) return; box.innerHTML = ''; cfg.items.forEach(function (it) { if (placed[it.id] !== cat) return; box.appendChild(buildChip(it, true)); }); }); if (window.renderMathInElement) try { renderMath(scope); } catch (e) {} } attachBoxTaps(); render(); return { placed: placed, render: render, reset: function () { for (var k in placed) delete placed[k]; armed = null; render(); } }; } /* ============================================================ GLOSSARY */ function wrapGlossary(root) { var GL = M6.glossary || []; if (!root || root.__glossDone || !GL.length) return; var allAliases = []; GL.forEach(function (g, i) { (g.aliases || [g.term]).forEach(function (a) { allAliases.push({ a: a, i: i }); }); }); allAliases.sort(function (x, y) { return y.a.length - x.a.length; }); var re = new RegExp('(? cursor) out.appendChild(document.createTextNode(text.slice(cursor, m.index))); var found = m[0].toLowerCase(); var hit = allAliases.find(function (x) { return x.a.toLowerCase() === found; }); var g = hit ? GL[hit.i] : null; var sp = document.createElement('span'); sp.className = 'gloss-term'; sp.dataset.gloss = g ? g.term : ''; sp.textContent = m[0]; out.appendChild(sp); cursor = m.index + m[0].length; } if (cursor < text.length) out.appendChild(document.createTextNode(text.slice(cursor))); node.parentNode.replaceChild(out, node); }); root.__glossDone = true; } function initGlossaryTip() { var tip = document.getElementById('gloss-tip'); if (!tip) return; var GL = M6.glossary || []; var lockOpen = null; function show(elm) { var g = GL.find(function (x) { return x.term === elm.dataset.gloss; }); if (!g) return; tip.innerHTML = '' + g.term[0].toUpperCase() + g.term.slice(1) + '
' + g.def + '
' + (g.sec ? '
См. ' + shortName(g.sec) + '
' : ''); if (window.renderMathInElement) renderMath(tip); var r = elm.getBoundingClientRect(); tip.classList.add('show'); var tw = tip.offsetWidth, th = tip.offsetHeight; var left = r.left, top = r.bottom + 8; if (left + tw > window.innerWidth - 12) left = window.innerWidth - tw - 12; if (top + th > window.innerHeight - 12) top = r.top - th - 8; tip.style.left = Math.max(8, left) + 'px'; tip.style.top = Math.max(8, top) + 'px'; } function hide() { tip.classList.remove('show'); } document.addEventListener('mouseover', function (e) { var elm = e.target.closest && e.target.closest('.gloss-term'); if (elm && !lockOpen) show(elm); }); document.addEventListener('mouseout', function (e) { var elm = e.target.closest && e.target.closest('.gloss-term'); if (elm && !lockOpen) hide(); }); document.addEventListener('click', function (e) { var elm = e.target.closest && e.target.closest('.gloss-term'); if (elm) { if (lockOpen === elm) { lockOpen = null; hide(); } else { lockOpen = elm; show(elm); } } else if (lockOpen && !e.target.closest('.gloss-tip')) { lockOpen = null; hide(); } }); } /* ============================================================ SEARCH */ function buildSearchIndex() { var arr = []; paras().forEach(function (p) { arr.push({ kind: p.final ? 'Финал' : 'Параграф', title: p.num + ' ' + p.name, desc: p.sub || '', sec: p.id }); }); (M6.glossary || []).forEach(function (g) { arr.push({ kind: 'Понятие', title: g.term, desc: (g.def || '').replace(/\$/g, ''), sec: g.sec }); }); (M6.searchRows || []).forEach(function (r) { arr.push({ kind: r[0], title: r[1], desc: r[2], sec: r[3] }); }); return arr; } function initSearch() { var modal = document.getElementById('search-modal'), inp = document.getElementById('search-input'), out = document.getElementById('search-results'), btn = document.getElementById('search-btn'); if (!modal || !inp || !out) return; var INDEX = buildSearchIndex(); var cur = 0, rows = []; function score(q, it) { var t = (it.title + ' ' + it.desc).toLowerCase(); if (t.includes(q)) return 100 + (it.title.toLowerCase().startsWith(q) ? 50 : 0); var s = 0; q.split(/\s+/).forEach(function (w) { if (w && t.includes(w)) s += 10; }); return s; } function rank(q) { q = q.trim().toLowerCase(); if (!q) return INDEX.slice(0, 12); return INDEX.map(function (it) { return { it: it, s: score(q, it) }; }).filter(function (x) { return x.s > 0; }).sort(function (a, b) { return b.s - a.s; }).slice(0, 20).map(function (x) { return x.it; }); } function render() { cur = 0; if (!rows.length) { out.innerHTML = '
Ничего не найдено
'; return; } out.innerHTML = rows.map(function (r, i) { return ''; }).join(''); out.querySelectorAll('.search-row').forEach(function (b) { b.addEventListener('click', function () { cur = +b.dataset.i; pick(); }); }); } function pick() { var r = rows[cur]; if (!r) return; close(); goTo(r.sec); } function move(d) { var items = out.querySelectorAll('.search-row'); if (!items.length) return; items[cur] && items[cur].classList.remove('active'); cur = (cur + d + items.length) % items.length; items[cur].classList.add('active'); items[cur].scrollIntoView({ block: 'nearest' }); } function open() { modal.classList.add('show'); inp.value = ''; rows = rank(''); render(); setTimeout(function () { inp.focus(); }, 50); } function close() { modal.classList.remove('show'); } btn && btn.addEventListener('click', open); modal.addEventListener('click', function (e) { if (e.target === modal) close(); }); inp.addEventListener('input', function () { rows = rank(inp.value); render(); }); inp.addEventListener('keydown', function (e) { if (e.key === 'ArrowDown') { e.preventDefault(); move(1); } else if (e.key === 'ArrowUp') { e.preventDefault(); move(-1); } else if (e.key === 'Enter') { e.preventDefault(); pick(); } else if (e.key === 'Escape') { e.preventDefault(); close(); } }); document.addEventListener('keydown', function (e) { if ((e.ctrlKey || e.metaKey) && (e.key === 'k' || e.key === 'K')) { e.preventDefault(); if (modal.classList.contains('show')) close(); else open(); } }); } function initSidebarToggle() { var side = document.getElementById('col-side'), back = document.getElementById('col-side-backdrop'), btn = document.getElementById('sidebar-btn'); if (!side || !btn) return; function open() { side.classList.add('open'); if (back) back.classList.add('show'); } function close() { side.classList.remove('open'); if (back) back.classList.remove('show'); } btn.addEventListener('click', function () { if (side.classList.contains('open')) close(); else open(); }); if (back) back.addEventListener('click', close); document.addEventListener('keydown', function (e) { if (e.key === 'Escape') close(); }); } /* ============================================================ INIT */ function init() { M6 = window.M6 || M6; /* перечитываем конфиг (порядок выполнения скриптов мог опередить объявление M6) */ loadProgress(); initTheme(); initSidebarToggle(); initGlossaryTip(); initSearch(); buildSections(); buildParaSelector(); refreshProgressUI(); loadServerReadState(); var first = (paras()[0] || {}).id; if (first) goTo(first); if (M6.startAch) setTimeout(function () { achievement(M6.startAch[0], M6.startAch[1]); }, 600); var foot = document.getElementById('m6-foot'); if (foot && M6.footer) foot.textContent = M6.footer; if (window.LS && window.LS.xp) { window.LS.xp.load().then(function (s) { if (s && s.xp > STATE.xp) { STATE.xp = s.xp; STATE.level = calcLevel(STATE.xp); saveProgress(); refreshProgressUI(); if (STATE.current) buildSidebar(STATE.current); } }).catch(function () {}); } } /* ============================================================ EXPORTS (для inline-билдеров) */ window.goTo = goTo; window.makeCard = makeCard; window.secNav = secNav; window.readBtn = readBtn; window.feedback = feedback; window.renderMath = renderMath; window.fmt = fmt; window.num = num; window.addXp = addXp; window.bumpProgress = bumpProgress; window.achievement = achievement; window.setupSorter = setupSorter; window.confetti = confetti; window.M6icon = M6icon; window.M6engine = { goTo: goTo, ensureBuilt: ensureBuilt, refreshProgressUI: refreshProgressUI, buildSidebar: buildSidebar }; /* Запуск init — СТРОГО ПОСЛЕ экспортов в window. Иначе при defer-старте (readyState='interactive') синхронная ветка else init() вызовет билдеры, которые обращаются к makeCard/secNav/feedback ДО их экспорта → ReferenceError → перехват в ensureBuilt → заглушка. */ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init); else init(); })();