Files
Learn_System/frontend/js/admin/_shared.js
Maxim Dolgolyov 5f481f5d11 fix(admin): рендер KaTeX в секции «Вопросы» — добавлены разделители $…$ и $$…$$
renderMath в _shared.js распознавал только \(…\) и \[…\], из-за чего
873 вопроса с долларовыми разделителями не рендерили формулы в админке.
$$ ставится раньше $, чтобы auto-render не принял его за два пустых $.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 21:25:17 +03:00

134 lines
6.3 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use strict';
/* Admin shared helpers — referenced by admin.js orchestrator + every section module.
* Exposed on window.AdminCtx (filled by admin.js after LS.initPage()) and
* on window directly for utility functions used by HTML onclicks.
*/
(function () {
'use strict';
/* ─── Constants ─── */
const MODES = { exam:'Экзамен', practice:'Тренировка', repeat:'Обычный', ct:'ЦТ/ЦЭ', topic:'По теме', random:'Случайный' };
const DIFFS = { 1:'Лёгкий', 2:'Средний', 3:'Сложный' };
const DIFF_LABELS = DIFFS;
const TYPE_LABELS = { single:'Один', multi:'Несколько', true_false:'Верно/Нет', short_answer:'Краткий', matching:'Сопоставление' };
/* ─── Generic formatters ─── */
function pctClass(p) { return p === null ? '' : p >= 75 ? 'pct-hi' : p >= 50 ? 'pct-mid' : 'pct-lo'; }
function fmtDate(d) { return new Date(d).toLocaleDateString('ru', { day:'numeric', month:'short', year:'numeric' }); }
function fmtTime(sec) {
if (!sec || sec < 0) return '—';
const m = Math.floor(sec / 60), s = sec % 60;
return m ? `${m} мин ${s} сек` : `${s} сек`;
}
function fmtDuration(sec) {
if (!sec || sec < 0) return '—';
const h = Math.floor(sec / 3600), m = Math.floor((sec % 3600) / 60), s = sec % 60;
if (h) return `${h}ч ${m}м`;
if (m) return `${m} мин ${s} сек`;
return `${s} сек`;
}
/* ─── KaTeX rendering ─── */
const KATEX_OPTS = {
// Порядок важен: многосимвольные/display-разделители ($$, \[) идут раньше
// одиночного $, иначе auto-render распознает $$ как два пустых $.
delimiters: [
{ left: '$$', right: '$$', display: true },
{ left: '\\[', right: '\\]', display: true },
{ left: '\\(', right: '\\)', display: false },
{ left: '$', right: '$', display: false },
],
throwOnError: false,
};
function renderMath(el) {
if (!el) return;
const run = () => { if (window.renderMathInElement) renderMathInElement(el, KATEX_OPTS); };
if (window._katexReady) run(); else window._katexCb = run;
}
/* ─── Question type badges (used by tests + subjects sections) ─── */
function qTypeBadge(type) {
const MAP = { single:'Один', multi:'Несколько', true_false:'Верно/Нет', short_answer:'Ответ', matching:'Сопост.' };
const CLR = { single:'rgba(155,93,229,0.12)', multi:'rgba(6,214,224,0.12)', true_false:'rgba(255,179,71,0.14)', short_answer:'rgba(6,214,100,0.12)', matching:'rgba(241,91,181,0.10)' };
const TXT = { single:'var(--violet)', multi:'#05aab3', true_false:'var(--amber)', short_answer:'var(--green)', matching:'var(--pink)' };
return `<span class="tst-q-badge" style="background:${CLR[type]||'rgba(15,23,42,0.06)'};color:${TXT[type]||'var(--text-3)'}">${MAP[type]||type}</span>`;
}
function qOptsPreview(q) {
if (q.type === 'short_answer') return q.correct_text ? `<span class="tst-q-opts">Ответ: ${esc(q.correct_text)}</span>` : '';
if (!q.options?.length) return '';
const correct = q.options.filter(o => o.is_correct).map(o => esc(o.text)).join(', ');
return `<span class="tst-q-opts"><i data-lucide="check" style="width:12px;height:12px;vertical-align:-2px"></i> ${correct}</span>`;
}
/* ─── Pagination controls (users + future tables) ─── */
function ensurePgnStyles() {
if (document.getElementById('pgn-bar-style')) return;
const s = document.createElement('style');
s.id = 'pgn-bar-style';
s.textContent = `
.pgn-bar { display:flex; align-items:center; justify-content:space-between; gap:10px; padding:14px 4px 4px; font-size:0.85rem; color:var(--text-3); }
.pgn-info { font-weight:600; }
.pgn-ctrls { display:flex; align-items:center; gap:4px; }
.pgn-btn { min-width:32px; height:32px; padding:0 10px; border:1px solid var(--border); background:var(--surface); border-radius:8px; cursor:pointer; font-weight:600; font-family:inherit; font-size:0.85rem; color:var(--text-2); transition:background .12s, color .12s, border-color .12s; }
.pgn-btn:hover:not(:disabled) { background:rgba(155,93,229,.08); color:var(--violet); border-color:rgba(155,93,229,.3); }
.pgn-btn.active { background:var(--violet); color:#fff; border-color:var(--violet); }
.pgn-btn:disabled { opacity:.4; cursor:not-allowed; }
.pgn-ellip { padding:0 6px; color:var(--text-3); }
`;
document.head.appendChild(s);
}
function renderPgnControls(elId, page, total, perPage, gotoFn) {
const bar = document.getElementById(elId);
if (!bar) return;
const pages = Math.max(1, Math.ceil(total / perPage));
if (pages <= 1) { bar.style.display = 'none'; return; }
ensurePgnStyles();
const from = (page - 1) * perPage + 1;
const to = Math.min(page * perPage, total);
const nums = new Set([1, pages, page, page - 1, page + 1, page - 2, page + 2]);
const sorted = [...nums].filter(n => n >= 1 && n <= pages).sort((a, b) => a - b);
const numHtml = sorted.map((n, i) => {
const prev = sorted[i - 1];
const gap = prev && n - prev > 1 ? '<span class="pgn-ellip">…</span>' : '';
return `${gap}<button class="pgn-btn${n === page ? ' active' : ''}" onclick="${gotoFn}(${n})">${n}</button>`;
}).join('');
bar.innerHTML = `
<div class="pgn-info">${from}${to} из ${total}</div>
<div class="pgn-ctrls">
<button class="pgn-btn" onclick="${gotoFn}(${page - 1})" ${page <= 1 ? 'disabled' : ''}>←</button>
${numHtml}
<button class="pgn-btn" onclick="${gotoFn}(${page + 1})" ${page >= pages ? 'disabled' : ''}>→</button>
</div>`;
bar.style.display = '';
}
/* ─── Export ─── */
window.AdminCtx = window.AdminCtx || {
// filled by admin.js after LS.initPage():
user: null,
isTeacher: false,
isAdmin: false,
// constants:
MODES,
DIFFS,
DIFF_LABELS,
TYPE_LABELS,
// formatters:
pctClass,
fmtDate,
fmtTime,
fmtDuration,
// rendering:
renderMath,
qTypeBadge,
qOptsPreview,
// pagination:
renderPgnControls,
ensurePgnStyles,
};
window.AdminSections = window.AdminSections || {};
})();