fa67ad1294
Phase 2 review caught this: updateCharCounter was defined inside questions.js IIFE but never exposed via window.X; admin.html:1672 calls it via oninput, would throw ReferenceError on every keypress. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
537 lines
23 KiB
JavaScript
537 lines
23 KiB
JavaScript
'use strict';
|
||
/* admin → questions section (the biggest — список + Q-modal + CSV) */
|
||
(function () {
|
||
'use strict';
|
||
let inited = false;
|
||
|
||
let allQuestions = [];
|
||
let editingQId = null;
|
||
let openQId = null;
|
||
let _topicMap = {};
|
||
let _currentType = 'single';
|
||
// window._matchPairs is exposed on window because HTML oninput uses bare `_matchPairs[i].left=this.value`
|
||
window._matchPairs = window._matchPairs || [];
|
||
let _opts = [];
|
||
let _focusedInput = null;
|
||
let _prevTimer = null;
|
||
const OPT_LETTERS = 'АБВГДЕ';
|
||
|
||
function updateCharCounter(el, cntId, max) {
|
||
const n = el.value.length;
|
||
const cnt = document.getElementById(cntId);
|
||
if (!cnt) return;
|
||
cnt.textContent = `${n} / ${max}`;
|
||
cnt.className = 'char-counter' + (n > max * 0.9 ? ' warn' : '') + (n >= max ? ' over' : '');
|
||
}
|
||
|
||
async function onQSubjectChange() {
|
||
const slug = document.getElementById('q-subject').value;
|
||
const sel = document.getElementById('q-topic');
|
||
sel.innerHTML = '<option value="">Все темы</option>';
|
||
if (slug) {
|
||
try {
|
||
const topics = await LS.getTopics(slug);
|
||
topics.forEach(t => sel.appendChild(new Option(t.name, t.id)));
|
||
} catch {}
|
||
}
|
||
load();
|
||
}
|
||
|
||
async function load() {
|
||
const subject = document.getElementById('q-subject').value;
|
||
const topic_id = document.getElementById('q-topic').value;
|
||
const sort = document.getElementById('q-sort').value;
|
||
const wrap = document.getElementById('q-list-wrap');
|
||
wrap.innerHTML = LS.skeleton(5);
|
||
try {
|
||
allQuestions = await LS.getQuestions(subject || null, topic_id || null, sort);
|
||
renderQuestions();
|
||
} catch (e) {
|
||
wrap.innerHTML = `<div class="error">Ошибка загрузки: ${esc(e.message)}</div>`;
|
||
}
|
||
}
|
||
|
||
function renderQuestions() {
|
||
const { DIFFS } = AdminCtx;
|
||
const search = document.getElementById('q-search').value.toLowerCase();
|
||
const filtered = search
|
||
? allQuestions.filter(q => q.text.toLowerCase().includes(search) || (q.topic||'').toLowerCase().includes(search))
|
||
: allQuestions;
|
||
|
||
document.getElementById('q-count').textContent = `${filtered.length} вопросов`;
|
||
|
||
if (!filtered.length) {
|
||
document.getElementById('q-list-wrap').innerHTML = '<div class="empty">Вопросов не найдено</div>';
|
||
return;
|
||
}
|
||
|
||
const wrap = document.getElementById('q-list-wrap');
|
||
wrap.innerHTML =
|
||
`<div class="q-list">${filtered.map(q => {
|
||
const diffCls = `diff-${q.difficulty}`;
|
||
const optsHtml = (q.options || []).map(o =>
|
||
`<div class="q-opt-row ${o.is_correct ? 'correct' : ''}">
|
||
<span class="q-opt-icon">${o.is_correct ? '<i data-lucide="check" style="width:13px;height:13px"></i>' : '<svg class="ic" viewBox="0 0 24 24"><circle cx="12" cy="12" r="8"/></svg>'}</span>${esc(o.text)}
|
||
</div>`).join('');
|
||
const explHtml = q.explanation
|
||
? `<div class="q-expl"><strong>Пояснение:</strong> ${esc(q.explanation)}</div>` : '';
|
||
return `<div class="q-card" id="qcard-${q.id}">
|
||
<div class="q-card-head">
|
||
<span class="q-card-num">#${q.id}</span>
|
||
<div class="q-card-body" onclick="toggleQDetail(${q.id})">
|
||
<div class="q-card-text">${esc(q.text)}</div>
|
||
<div class="q-card-meta">
|
||
${q.subject_name ? `<span class="q-badge q-badge-subj">${esc(q.subject_name)}</span>` : ''}
|
||
${q.topic ? `<span class="q-badge q-badge-topic">${esc(q.topic)}</span>` : ''}
|
||
<span class="q-badge ${diffCls}">${DIFFS[q.difficulty]||q.difficulty}</span>
|
||
<span style="font-size:0.72rem;color:var(--text-3);background:rgba(15,23,42,0.05);padding:2px 7px;border-radius:999px">${{single:'Один',multi:'Несколько',true_false:'Верно/Неверно',short_answer:'Краткий',matching:'Сопост.'}[q.type]||q.type||'Один'}</span>
|
||
<span style="font-size:0.75rem;color:var(--text-3)">${q.options?.length||0} вар.</span>
|
||
</div>
|
||
</div>
|
||
<div class="q-card-actions">
|
||
<button class="btn-edit-q" onclick="editQ(${q.id})">Изменить</button>
|
||
<button class="btn-dup-q" onclick="dupQ(${q.id})" title="Дублировать">⧉</button>
|
||
<button class="btn-del-q" onclick="deleteQ(${q.id})" title="Удалить"><i data-lucide="x" style="width:14px;height:14px"></i></button>
|
||
</div>
|
||
</div>
|
||
<div class="q-card-detail" id="qdetail-${q.id}">
|
||
${optsHtml}${explHtml}
|
||
</div>
|
||
</div>`;
|
||
}).join('')}</div>`;
|
||
AdminCtx.renderMath(wrap);
|
||
if (window.lucide) lucide.createIcons();
|
||
}
|
||
|
||
function toggleQDetail(id) {
|
||
if (openQId === id) {
|
||
document.getElementById('qdetail-' + id)?.classList.remove('open');
|
||
openQId = null; return;
|
||
}
|
||
if (openQId) document.getElementById('qdetail-' + openQId)?.classList.remove('open');
|
||
document.getElementById('qdetail-' + id)?.classList.add('open');
|
||
openQId = id;
|
||
}
|
||
|
||
async function dupQ(id) {
|
||
try {
|
||
const { id: newId } = await LS.duplicateQuestion(id);
|
||
await load();
|
||
setTimeout(() => document.getElementById('qcard-' + newId)?.scrollIntoView({ behavior:'smooth', block:'center' }), 300);
|
||
} catch (e) { LS.toast('Ошибка: ' + e.message, 'error'); }
|
||
}
|
||
|
||
async function deleteQ(id) {
|
||
if (!await LS.confirm(`Удалить вопрос #${id}?`, { title: 'Удалить вопрос', confirmText: 'Удалить' })) return;
|
||
try {
|
||
await LS.deleteQuestion(id);
|
||
allQuestions = allQuestions.filter(q => q.id !== id);
|
||
renderQuestions();
|
||
} catch (e) { LS.toast('Ошибка: ' + e.message, 'error'); }
|
||
}
|
||
|
||
/* ─── Question type ─── */
|
||
function setQType(type) {
|
||
_currentType = type;
|
||
document.querySelectorAll('[data-type]').forEach(b => b.classList.toggle('active', b.dataset.type === type));
|
||
const isMatching = type === 'matching';
|
||
const isShort = type === 'short_answer';
|
||
const showOpts = !isShort && !isMatching;
|
||
const optsHeader = document.getElementById('qf-opts-header');
|
||
if (optsHeader) optsHeader.style.display = showOpts ? '' : 'none';
|
||
document.getElementById('qf-opts').style.display = showOpts ? '' : 'none';
|
||
document.getElementById('qf-short-wrap').style.display = isShort ? '' : 'none';
|
||
document.getElementById('qf-match-wrap').style.display = isMatching ? '' : 'none';
|
||
document.getElementById('btn-add-opt').style.display = showOpts && type !== 'true_false' ? '' : 'none';
|
||
|
||
if (type === 'true_false') {
|
||
initOpts([{ text:'Верно', is_correct:false }, { text:'Неверно', is_correct:false }]);
|
||
} else if (isShort) {
|
||
_opts = [];
|
||
} else if (isMatching) {
|
||
_opts = [];
|
||
if (window._matchPairs.length === 0) window._matchPairs = [{left:'',right:''},{left:'',right:''},{left:'',right:''}];
|
||
renderMatchRows();
|
||
} else {
|
||
if (_opts.length === 0 || _opts[0]?.text === 'Верно') initOpts([{},{},{},{}]);
|
||
else renderOptRows(_opts);
|
||
}
|
||
}
|
||
|
||
function renderMatchRows() {
|
||
const cont = document.getElementById('qf-match-rows');
|
||
cont.innerHTML = window._matchPairs.map((p, i) => `
|
||
<div style="display:grid;grid-template-columns:1fr 1fr auto;gap:8px;margin-bottom:8px" data-mi="${i}">
|
||
<input type="text" class="form-ctrl" placeholder="Элемент…" value="${esc(p.left)}"
|
||
oninput="window._matchPairs[${i}].left=this.value" style="margin:0" />
|
||
<input type="text" class="form-ctrl" placeholder="Пара к нему…" value="${esc(p.right)}"
|
||
oninput="window._matchPairs[${i}].right=this.value" style="margin:0" />
|
||
<button type="button" onclick="removeMatchPair(${i})" style="border:none;background:none;color:var(--text-3);cursor:pointer;padding:0 6px;display:flex;align-items:center" title="Удалить"><i data-lucide="x" style="width:15px;height:15px"></i></button>
|
||
</div>`).join('');
|
||
if (window.lucide) lucide.createIcons();
|
||
}
|
||
|
||
function addMatchPair() {
|
||
window._matchPairs.push({left:'',right:''});
|
||
renderMatchRows();
|
||
}
|
||
|
||
function removeMatchPair(i) {
|
||
window._matchPairs.splice(i, 1);
|
||
renderMatchRows();
|
||
}
|
||
|
||
/* ─── Formula bar ─── */
|
||
document.addEventListener('focusin', e => {
|
||
if (e.target.closest && e.target.closest('#q-modal') &&
|
||
(e.target.tagName === 'TEXTAREA' || (e.target.tagName === 'INPUT' && e.target.type === 'text'))) {
|
||
_focusedInput = e.target;
|
||
}
|
||
});
|
||
|
||
function ins(latex) {
|
||
const el = _focusedInput || document.getElementById('qf-text');
|
||
if (!el) return;
|
||
const s = el.selectionStart ?? el.value.length;
|
||
const e2= el.selectionEnd ?? el.value.length;
|
||
const before = el.value.slice(0, s), after = el.value.slice(e2);
|
||
const opens = (before.match(/\\\(/g)||[]).length;
|
||
const closes = (before.match(/\\\)/g)||[]).length;
|
||
const insert = opens > closes ? latex : `\\(${latex}\\)`;
|
||
el.value = before + insert + after;
|
||
el.setSelectionRange(s + insert.length, s + insert.length);
|
||
el.focus();
|
||
updateQPreview();
|
||
}
|
||
|
||
function wrapMath() {
|
||
const el = _focusedInput || document.getElementById('qf-text');
|
||
if (!el) return;
|
||
const s = el.selectionStart, e2 = el.selectionEnd;
|
||
const sel = el.value.slice(s, e2) || 'x';
|
||
el.value = el.value.slice(0, s) + `\\(${sel}\\)` + el.value.slice(e2);
|
||
el.focus();
|
||
updateQPreview();
|
||
}
|
||
|
||
/* ─── Live preview ─── */
|
||
function updateQPreview() {
|
||
clearTimeout(_prevTimer);
|
||
_prevTimer = setTimeout(() => {
|
||
const text = (document.getElementById('qf-text').value || '').trim();
|
||
const el = document.getElementById('q-preview-text');
|
||
const wrap = document.getElementById('q-preview-wrap');
|
||
wrap.classList.toggle('hidden', !text);
|
||
if (!text) return;
|
||
el.textContent = text;
|
||
AdminCtx.renderMath(el);
|
||
}, 150);
|
||
}
|
||
|
||
// Formula bar toggle (default collapsed)
|
||
window.toggleFormulaBar = function () {
|
||
const bar = document.getElementById('formula-bar');
|
||
const btn = document.getElementById('qf-fml-toggle');
|
||
const open = bar.classList.toggle('visible');
|
||
btn.classList.toggle('open', open);
|
||
};
|
||
|
||
// Wire textarea input to preview
|
||
setTimeout(() => {
|
||
const ta = document.getElementById('qf-text');
|
||
if (ta) ta.addEventListener('input', updateQPreview);
|
||
}, 0);
|
||
|
||
/* ─── Dynamic options ─── */
|
||
function renderOptRows(opts) {
|
||
const grid = document.getElementById('qf-opts');
|
||
const isMulti = _currentType === 'multi';
|
||
grid.innerHTML = opts.map((o, i) => `
|
||
<div class="opt-row${o.is_correct ? ' opt-correct' : ''}" data-i="${i}">
|
||
<span class="opt-letter">${OPT_LETTERS[i]}</span>
|
||
${isMulti
|
||
? `<input type="checkbox" class="opt-radio" value="${i}" ${o.is_correct ? 'checked' : ''}
|
||
onchange="onCheckChange(${i}, this.checked)" />`
|
||
: `<input type="radio" name="qf-correct" class="opt-radio" value="${i}" ${o.is_correct ? 'checked' : ''}
|
||
onchange="onRadioChange(${i})" />`}
|
||
<input type="text" class="opt-input" placeholder="Вариант ${OPT_LETTERS[i]}"
|
||
value="${esc(o.text||'')}" oninput="syncOptText(${i}, this.value)" />
|
||
${opts.length > 2
|
||
? `<button type="button" class="btn-rem-opt" onclick="removeOpt(${i})" title="Удалить">−</button>`
|
||
: '<span style="width:24px;flex-shrink:0"></span>'}
|
||
</div>`).join('');
|
||
document.getElementById('btn-add-opt').style.display = opts.length >= 6 ? 'none' : '';
|
||
}
|
||
|
||
function onCheckChange(idx, checked) {
|
||
_opts[idx].is_correct = checked;
|
||
document.querySelector(`#qf-opts .opt-row[data-i="${idx}"]`)?.classList.toggle('opt-correct', checked);
|
||
}
|
||
|
||
function initOpts(opts) {
|
||
_opts = opts.length ? opts.map(o => ({ text: o.text||'', is_correct: !!o.is_correct }))
|
||
: [{text:'',is_correct:false},{text:'',is_correct:false},{text:'',is_correct:false},{text:'',is_correct:false}];
|
||
renderOptRows(_opts);
|
||
}
|
||
|
||
function onRadioChange(idx) {
|
||
_opts.forEach((o, i) => o.is_correct = (i === idx));
|
||
renderOptRows(_opts);
|
||
}
|
||
|
||
function syncOptText(idx, val) { _opts[idx].text = val; }
|
||
|
||
function addOpt() {
|
||
if (_opts.length >= 6) return;
|
||
_opts.push({ text: '', is_correct: false });
|
||
renderOptRows(_opts);
|
||
const rows = document.querySelectorAll('#qf-opts .opt-row');
|
||
rows[rows.length - 1]?.querySelector('input[type=text]')?.focus();
|
||
}
|
||
|
||
function removeOpt(idx) {
|
||
if (_opts.length <= 2) return;
|
||
const wasCorrect = _opts[idx].is_correct;
|
||
_opts.splice(idx, 1);
|
||
if (wasCorrect && _opts.length > 0) _opts[0].is_correct = true;
|
||
renderOptRows(_opts);
|
||
}
|
||
|
||
/* ─── Modal ─── */
|
||
function openQModal(q = null) {
|
||
editingQId = q ? q.id : null;
|
||
document.getElementById('q-modal-title').textContent = q ? `Редактировать вопрос #${q.id}` : 'Добавить вопрос';
|
||
const textEl = document.getElementById('qf-text');
|
||
textEl.value = q?.text || '';
|
||
updateCharCounter(textEl, 'qf-text-cnt', 500);
|
||
document.getElementById('qf-explanation').value = q?.explanation || '';
|
||
document.getElementById('qf-difficulty').value = q?.difficulty ?? 2;
|
||
document.getElementById('qf-subject').value = q?.subject_slug || '';
|
||
document.getElementById('qf-topic-text').value = q?.topic || '';
|
||
document.getElementById('qf-correct-text').value = q?.correct_text || '';
|
||
document.getElementById('qf-error').textContent = '';
|
||
const imgVal = q?.image || '';
|
||
document.getElementById('qf-image').value = imgVal;
|
||
updateImagePreview(imgVal);
|
||
|
||
if (q?.type === 'matching') {
|
||
window._matchPairs = (q.options || []).map(o => ({ left: o.text, right: o.match_pair || '' }));
|
||
if (!window._matchPairs.length) window._matchPairs = [{left:'',right:''},{left:'',right:''},{left:'',right:''}];
|
||
} else {
|
||
window._matchPairs = [];
|
||
}
|
||
setQType(q?.type || 'single');
|
||
if (q?.type !== 'matching') initOpts(q?.options || []);
|
||
updateQPreview();
|
||
loadQModalTopics();
|
||
document.getElementById('q-modal').classList.add('open');
|
||
setTimeout(() => textEl.focus(), 80);
|
||
}
|
||
|
||
function editQ(id) {
|
||
const q = allQuestions.find(x => x.id === id);
|
||
if (q) openQModal(q);
|
||
}
|
||
|
||
function closeQModal() {
|
||
document.getElementById('q-modal').classList.remove('open');
|
||
editingQId = null;
|
||
}
|
||
|
||
async function loadQModalTopics() {
|
||
const slug = document.getElementById('qf-subject').value;
|
||
const dl = document.getElementById('qf-topic-list');
|
||
dl.innerHTML = '';
|
||
_topicMap = {};
|
||
if (!slug) return;
|
||
try {
|
||
const topics = await LS.getTopics(slug);
|
||
topics.forEach(t => {
|
||
dl.appendChild(new Option(t.name));
|
||
_topicMap[t.name.toLowerCase()] = t.id;
|
||
});
|
||
} catch {}
|
||
}
|
||
|
||
async function saveQuestion() {
|
||
const text = document.getElementById('qf-text').value.trim();
|
||
const explanation = document.getElementById('qf-explanation').value.trim();
|
||
const difficulty = Number(document.getElementById('qf-difficulty').value);
|
||
const subject_slug = document.getElementById('qf-subject').value;
|
||
const topicText = document.getElementById('qf-topic-text').value.trim();
|
||
const type = _currentType;
|
||
const errEl = document.getElementById('qf-error');
|
||
errEl.textContent = '';
|
||
|
||
if (!subject_slug) { errEl.textContent = 'Выберите предмет'; return; }
|
||
if (!text) { errEl.textContent = 'Введите текст вопроса'; return; }
|
||
|
||
let options = null;
|
||
let correct_text = null;
|
||
|
||
if (type === 'short_answer') {
|
||
correct_text = document.getElementById('qf-correct-text').value.trim();
|
||
if (!correct_text) { errEl.textContent = 'Введите правильный ответ'; return; }
|
||
} else if (type === 'matching') {
|
||
document.querySelectorAll('#qf-match-rows [data-mi]').forEach((row, i) => {
|
||
const [l, r] = row.querySelectorAll('input');
|
||
if (window._matchPairs[i]) { window._matchPairs[i].left = l.value.trim(); window._matchPairs[i].right = r.value.trim(); }
|
||
});
|
||
if (window._matchPairs.length < 2) { errEl.textContent = 'Нужно минимум 2 пары'; return; }
|
||
if (window._matchPairs.some(p => !p.left || !p.right)) { errEl.textContent = 'Заполните все пары'; return; }
|
||
options = window._matchPairs.map(p => ({ text: p.left, match_pair: p.right, is_correct: 0 }));
|
||
} else {
|
||
document.querySelectorAll('#qf-opts .opt-row').forEach((row, i) => {
|
||
if (_opts[i]) _opts[i].text = row.querySelector('input[type=text]').value.trim();
|
||
});
|
||
options = _opts.map(o => ({ text: o.text, is_correct: o.is_correct }));
|
||
if (options.length < 2) { errEl.textContent = 'Нужно минимум 2 варианта ответа'; return; }
|
||
if (options.some(o => !o.text)) { errEl.textContent = 'Заполните все варианты ответов'; return; }
|
||
if (!options.some(o => o.is_correct)) { errEl.textContent = 'Отметьте правильный ответ'; return; }
|
||
}
|
||
|
||
const knownId = _topicMap[topicText.toLowerCase()];
|
||
const topic_id = knownId || null;
|
||
const topic_name = !knownId && topicText ? topicText : null;
|
||
|
||
const image = document.getElementById('qf-image').value.trim() || null;
|
||
|
||
const btn = document.getElementById('qf-save');
|
||
btn.disabled = true; btn.textContent = 'Сохранение…';
|
||
try {
|
||
if (editingQId) {
|
||
await LS.updateQuestion(editingQId, { text, type, correct_text, difficulty, explanation: explanation||null, topic_id, topic_name, options, image });
|
||
} else {
|
||
await LS.createQuestion({ subject_slug, topic_id, topic_name, text, type, correct_text, difficulty, explanation: explanation||null, options, image });
|
||
}
|
||
closeQModal();
|
||
load();
|
||
} catch (e) {
|
||
errEl.textContent = 'Ошибка: ' + e.message;
|
||
} finally {
|
||
btn.disabled = false; btn.textContent = 'Сохранить';
|
||
}
|
||
}
|
||
|
||
/* ── Image upload & preview ── */
|
||
function updateImagePreview(url) {
|
||
const wrap = document.getElementById('qf-image-preview');
|
||
const img = document.getElementById('qf-image-img');
|
||
if (url) { img.src = url; wrap.classList.add('visible'); }
|
||
else { wrap.classList.remove('visible'); img.src = ''; }
|
||
}
|
||
|
||
function clearQuestionImage() {
|
||
document.getElementById('qf-image').value = '';
|
||
updateImagePreview('');
|
||
}
|
||
|
||
async function handleImageFileSelect(input) {
|
||
const file = input.files[0];
|
||
if (!file) return;
|
||
input.value = '';
|
||
const btn = document.getElementById('btn-img-upload');
|
||
const lbl = document.getElementById('btn-img-upload-lbl');
|
||
btn.disabled = true;
|
||
lbl.textContent = 'Загрузка…';
|
||
try {
|
||
const fd = new FormData();
|
||
fd.append('file', file);
|
||
fd.append('title', 'Question image: ' + file.name);
|
||
fd.append('is_public', '1');
|
||
const res = await fetch('/api/files', {
|
||
method: 'POST',
|
||
headers: { Authorization: 'Bearer ' + localStorage.getItem('ls_token') },
|
||
body: fd
|
||
});
|
||
if (!res.ok) throw new Error((await res.json()).error || res.statusText);
|
||
const { id } = await res.json();
|
||
const url = `/api/files/${id}/download`;
|
||
document.getElementById('qf-image').value = url;
|
||
updateImagePreview(url);
|
||
} catch (e) {
|
||
document.getElementById('qf-error').textContent = 'Ошибка загрузки: ' + e.message;
|
||
} finally {
|
||
btn.disabled = false;
|
||
lbl.textContent = 'Загрузить';
|
||
}
|
||
}
|
||
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
const imgInput = document.getElementById('qf-image');
|
||
if (imgInput) imgInput.addEventListener('input', e => updateImagePreview(e.target.value.trim()));
|
||
});
|
||
|
||
/* ── CSV Import ── */
|
||
async function importCSVFile(input) {
|
||
const file = input.files[0];
|
||
if (!file) return;
|
||
input.value = '';
|
||
const fd = new FormData();
|
||
fd.append('file', file);
|
||
const btn = document.querySelector('[onclick="document.getElementById(\'csv-file-input\').click()"]');
|
||
if (btn) { btn.disabled = true; btn.textContent = 'Импорт…'; }
|
||
try {
|
||
const { imported, errors } = await LS.importQuestions(fd);
|
||
LS.toast(`Импортировано: ${imported} вопросов${errors.length ? ` (${errors.length} ошибок)` : ''}`, imported > 0 ? 'success' : 'warn', 5000);
|
||
load();
|
||
} catch (e) {
|
||
LS.toast('Ошибка импорта: ' + e.message, 'error');
|
||
} finally {
|
||
if (btn) { btn.disabled = false; btn.innerHTML = '<i data-lucide="upload" style="width:14px;height:14px;vertical-align:-2px"></i> Импорт CSV'; if(window.lucide)lucide.createIcons(); }
|
||
}
|
||
}
|
||
|
||
function downloadCSVTemplate(e) {
|
||
e.preventDefault();
|
||
const header = 'subject_slug;topic;text;difficulty;type;opt1;c1;opt2;c2;opt3;c3;opt4;c4;correct_text;explanation;year';
|
||
const example = [
|
||
'bio;Клетки;Что является «электростанцией» клетки?;2;single;Митохондрия;1;Рибосома;0;Лизосома;0;Ядро;0;;Митохондрии синтезируют АТФ;2024',
|
||
'bio;Клетки;Какие органоиды участвуют в синтезе белка?;2;multi;Рибосома;1;Митохондрия;0;Эндоплазматическая сеть;1;Лизосома;0;;',
|
||
'chem;Кислоты;Формула серной кислоты;1;short_answer;;;;;;;;H2SO4;;',
|
||
].join('\n');
|
||
const blob = new Blob(['' + header + '\n' + example], { type: 'text/csv;charset=utf-8' });
|
||
const a = document.createElement('a');
|
||
a.href = URL.createObjectURL(blob);
|
||
a.download = 'questions_template.csv';
|
||
a.click();
|
||
}
|
||
|
||
// Expose handlers used by onclick (HTML or sibling sections)
|
||
window.onQSubjectChange = onQSubjectChange;
|
||
window.loadQuestions = load;
|
||
window.renderQuestions = renderQuestions;
|
||
window.toggleQDetail = toggleQDetail;
|
||
window.dupQ = dupQ;
|
||
window.deleteQ = deleteQ;
|
||
window.setQType = setQType;
|
||
window.addMatchPair = addMatchPair;
|
||
window.removeMatchPair = removeMatchPair;
|
||
window.ins = ins;
|
||
window.wrapMath = wrapMath;
|
||
window.updateQPreview = updateQPreview;
|
||
window.onCheckChange = onCheckChange;
|
||
window.onRadioChange = onRadioChange;
|
||
window.syncOptText = syncOptText;
|
||
window.addOpt = addOpt;
|
||
window.removeOpt = removeOpt;
|
||
window.openQModal = openQModal;
|
||
window.editQ = editQ;
|
||
window.closeQModal = closeQModal;
|
||
window.loadQModalTopics = loadQModalTopics;
|
||
window.saveQuestion = saveQuestion;
|
||
window.updateImagePreview = updateImagePreview;
|
||
window.clearQuestionImage = clearQuestionImage;
|
||
window.handleImageFileSelect = handleImageFileSelect;
|
||
window.importCSVFile = importCSVFile;
|
||
window.downloadCSVTemplate = downloadCSVTemplate;
|
||
window.updateCharCounter = updateCharCounter;
|
||
window.AdminSections = window.AdminSections || {};
|
||
window.AdminSections.questions = {
|
||
init: async () => { if (inited) return; inited = true; await load(); },
|
||
reload: load,
|
||
openModal: openQModal,
|
||
loadModalTopics: loadQModalTopics,
|
||
};
|
||
})();
|