feat: LS.modal — общий компонент модалок + миграция /exam9 + /my-students
Новый общий компонент LS.modal (api.js) — companion к LS.confirm.
Универсальная form/content-модалка с консистентным поведением:
LS.modal({
title, content, size: 'sm'|'md'|'lg',
actions: [{label, primary, danger, onClick}],
onClose,
});
// Returns { close, root, body, setBody, setActions, setError }
Стандартное поведение:
- ESC и backdrop-click закрывают (опциональный dismissible:false)
- z-index 9000 (тот же что LS.confirm — без конфликтов)
- Auto-focus первого input/select/textarea/button в body
- prevFocus restore при закрытии
- Анимация scale+translateY .22s
- Адаптив: на мобилках padding уменьшается
CSS-классы .ls-mov / .ls-mod / .ls-mod-hdr / .ls-mod-body / .ls-mod-act
впрыскиваются один раз из api.js (id=ls-modal-style), как и стили
toast/confirm.
Миграция exam9 «Назначить вариант»:
- Убран inline <div class="ex-overlay" id="assign-overlay">…</div>
- Убраны .ax-actions, .ax-btn, .ax-btn-primary, .ax-error, .ax-success
CSS (теперь в общих .ls-mod-* стилях)
- openAssignModal → LS.modal({ title, content: form, actions: [...] })
- Удалены closeAssignModal/onAssignOverlayClick/onAssignEsc — теперь
handle'ит LS.modal
- Удалена unused переменная assignVariantNum (closure теперь над varNum)
exam9.html: −53 строк (CSS + HTML модалки)
app.js: переписан 90 строк → 70 строк
Миграция my-students «Убрать ученика»:
- native confirm() → LS.confirm() с danger-стилизацией
- alert() → LS.toast() для согласованности
Сохранён классroom-овский «ex-overlay»/«ex-panel» CSS (используется
для picker'а вариантов в exam9). Не трогаем classroom.html — у него
своя ecosystem cr-*-overlay.
Дальше — postupенная миграция модалок в textbooks/classes/admin
по мере касания этих страниц. Шаблон установлен.
This commit is contained in:
+61
-80
@@ -176,7 +176,6 @@ function selectVariant(num) {
|
||||
}
|
||||
|
||||
/* ── Assignment modal ───────────────────────────────────────────── */
|
||||
let assignVariantNum = null;
|
||||
|
||||
async function loadTeacherClasses() {
|
||||
if (teacherClasses) return teacherClasses;
|
||||
@@ -189,94 +188,76 @@ async function loadTeacherClasses() {
|
||||
return teacherClasses;
|
||||
}
|
||||
|
||||
async function openAssignModal(variantNum) {
|
||||
if (!variantTests[variantNum]) return;
|
||||
assignVariantNum = variantNum;
|
||||
document.getElementById('assign-title').textContent = `Назначить «Вариант ${variantNum}» как ДЗ`;
|
||||
document.getElementById('ax-error').classList.remove('visible');
|
||||
document.getElementById('ax-success').classList.remove('visible');
|
||||
document.getElementById('ax-deadline').value = '';
|
||||
document.getElementById('ax-submit').disabled = false;
|
||||
document.getElementById('ax-submit').textContent = 'Назначить';
|
||||
|
||||
const listEl = document.getElementById('ax-classes-list');
|
||||
listEl.textContent = 'Загрузка…';
|
||||
const classes = await loadTeacherClasses();
|
||||
if (!classes.length) {
|
||||
listEl.innerHTML = '<div style="padding:14px;color:var(--text-3);font-size:.85rem">У вас пока нет классов. Создайте класс на странице «Классы».</div>';
|
||||
} else {
|
||||
listEl.innerHTML = classes.map(c => `
|
||||
<label class="ax-class">
|
||||
<input type="checkbox" name="cls" value="${c.id}" />
|
||||
<span class="ax-cname">${escapeHtml(c.name)}</span>
|
||||
<span class="ax-cmeta">${c.member_count || 0} учеников</span>
|
||||
</label>`).join('');
|
||||
}
|
||||
|
||||
document.getElementById('assign-overlay').classList.add('visible');
|
||||
document.addEventListener('keydown', onAssignEsc);
|
||||
}
|
||||
|
||||
function closeAssignModal() {
|
||||
document.getElementById('assign-overlay').classList.remove('visible');
|
||||
document.removeEventListener('keydown', onAssignEsc);
|
||||
assignVariantNum = null;
|
||||
}
|
||||
|
||||
function onAssignOverlayClick(e) {
|
||||
if (e.target === document.getElementById('assign-overlay')) closeAssignModal();
|
||||
}
|
||||
function onAssignEsc(e) { if (e.key === 'Escape') closeAssignModal(); }
|
||||
|
||||
function escapeHtml(s) {
|
||||
return String(s || '').replace(/[&<>"']/g, c => ({ '&':'&', '<':'<', '>':'>', '"':'"', "'":''' }[c]));
|
||||
}
|
||||
|
||||
async function submitAssign() {
|
||||
const errorEl = document.getElementById('ax-error');
|
||||
const successEl = document.getElementById('ax-success');
|
||||
const submitBtn = document.getElementById('ax-submit');
|
||||
errorEl.classList.remove('visible');
|
||||
successEl.classList.remove('visible');
|
||||
async function openAssignModal(variantNum) {
|
||||
if (!variantTests[variantNum]) return;
|
||||
const testId = variantTests[variantNum];
|
||||
|
||||
const checked = Array.from(document.querySelectorAll('#ax-classes-list input[name="cls"]:checked'))
|
||||
.map(el => Number(el.value));
|
||||
if (!checked.length) {
|
||||
errorEl.textContent = 'Выберите хотя бы один класс';
|
||||
errorEl.classList.add('visible');
|
||||
return;
|
||||
}
|
||||
// Build body with shared LS.modal helper
|
||||
const classes = await loadTeacherClasses();
|
||||
const classesHtml = classes.length
|
||||
? classes.map(c => `
|
||||
<label class="ax-class">
|
||||
<input type="checkbox" name="cls" value="${c.id}" />
|
||||
<span class="ax-cname">${escapeHtml(c.name)}</span>
|
||||
<span class="ax-cmeta">${c.member_count || 0} учеников</span>
|
||||
</label>`).join('')
|
||||
: '<div style="padding:14px;color:var(--text-3);font-size:.85rem">У вас пока нет классов. Создайте класс на странице «Классы».</div>';
|
||||
|
||||
const testId = variantTests[assignVariantNum];
|
||||
const deadline = document.getElementById('ax-deadline').value || null;
|
||||
if (!testId) { errorEl.textContent = 'Вариант не в банке вопросов'; errorEl.classList.add('visible'); return; }
|
||||
const body = `
|
||||
<form class="ax-form" id="assign-form" onsubmit="event.preventDefault()">
|
||||
<div class="ax-field">
|
||||
<label>Классы</label>
|
||||
<div class="ax-classes" id="ax-classes-list">${classesHtml}</div>
|
||||
</div>
|
||||
<div class="ax-field">
|
||||
<label>Срок сдачи (опционально)</label>
|
||||
<input type="datetime-local" class="ax-input" id="ax-deadline" />
|
||||
</div>
|
||||
</form>`;
|
||||
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.textContent = 'Назначаю…';
|
||||
const m = LS.modal({
|
||||
title: `Назначить «Вариант ${variantNum}» как ДЗ`,
|
||||
content: body,
|
||||
size: 'sm',
|
||||
actions: [
|
||||
{ label: 'Отмена', onClick: () => m.close() },
|
||||
{
|
||||
label: 'Назначить', primary: true,
|
||||
onClick: async () => {
|
||||
const checked = Array.from(m.body.querySelectorAll('input[name="cls"]:checked')).map(el => Number(el.value));
|
||||
if (!checked.length) { m.setError('Выберите хотя бы один класс'); return; }
|
||||
|
||||
try {
|
||||
const r = await LS.api('/api/assignments/bulk', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
title: `Экзамен 9 — Вариант ${assignVariantNum}`,
|
||||
class_ids: checked,
|
||||
mode: 'exam',
|
||||
count: 10,
|
||||
test_id: testId,
|
||||
deadline: deadline,
|
||||
is_homework: 1,
|
||||
const btns = m.root.querySelectorAll('.ls-mod-btn');
|
||||
btns.forEach(b => b.disabled = true);
|
||||
btns[1].textContent = 'Назначаю…';
|
||||
|
||||
try {
|
||||
const r = await LS.api('/api/assignments/bulk', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
title: `Экзамен 9 — Вариант ${variantNum}`,
|
||||
class_ids: checked,
|
||||
mode: 'exam', count: 10,
|
||||
test_id: testId,
|
||||
deadline: m.body.querySelector('#ax-deadline').value || null,
|
||||
is_homework: 1,
|
||||
},
|
||||
});
|
||||
LS.toast(`Назначено в ${r.count || checked.length} класс(ах)`, 'success');
|
||||
m.close();
|
||||
} catch (e) {
|
||||
m.setError(e.message || 'Не удалось создать задание');
|
||||
btns.forEach(b => b.disabled = false);
|
||||
btns[1].textContent = 'Назначить';
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
successEl.textContent = `Назначено в ${r.count || checked.length} классе(ах)`;
|
||||
successEl.classList.add('visible');
|
||||
submitBtn.textContent = 'Готово';
|
||||
setTimeout(closeAssignModal, 1500);
|
||||
} catch (e) {
|
||||
errorEl.textContent = e.message || 'Не удалось создать задание';
|
||||
errorEl.classList.add('visible');
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.textContent = 'Назначить';
|
||||
}
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
/* ── Boot ───────────────────────────────────────────────────────── */
|
||||
|
||||
Reference in New Issue
Block a user