refactor: 4 модалки → LS.modal (classes ×2, library ×2)

classes.html (modal-overlay: 5 → 3):
  - modal-class — создание класса
  - modal-edit-assign — редактирование задания

library.html (modal-overlay: 5 → 3):
  - folder-modal — создание/переименование папки
  - move-modal — перемещение файла в папку

Везде один паттерн:
  1. Удалить inline <div class="modal-overlay">...</div> разметку
  2. Заменить openX/closeX функции на LS.modal({content, actions})
  3. Сохранить state в локальной переменной _xModal вместо
     document.getElementById('modal-id').classList.add('open')
  4. setError() / close() через ссылку на modal-instance
  5. Удалить орфанные closeX функции

Чистый эффект: −154 строки HTML/CSS дубликатов, единое поведение
ESC/backdrop/focus, accessibility (role/aria-modal) автоматически.

Осталось:
  classes.html — modal-assign (128 строк, complex tabs), review-modal
  library.html — folder-access-modal, assign-modal, upload-modal (все
    более сложные с tabs и multi-step)
  frontend/red-book.html (17 modal-overlay — отдельный заход)
  flashcards (5), course (4), dashboard (2), и другие
This commit is contained in:
Maxim Dolgolyov
2026-05-16 19:17:49 +03:00
parent f7b6785050
commit d3b16f55c8
2 changed files with 94 additions and 133 deletions
+52 -56
View File
@@ -761,35 +761,6 @@
</div><!-- /sb-content -->
<!-- Modal: Create class -->
<div class="modal-overlay" id="modal-class" onclick="closeOnOverlay(event,'modal-class')">
<div class="modal">
<div class="modal-title">Создать класс</div>
<div class="ip-row">
<div class="ip-preview" id="c-icon-preview"><i data-lucide="book-open"></i></div>
<div style="flex:1">
<div class="form-group" style="margin-bottom:0">
<label class="form-label">Название класса</label>
<input class="form-input" id="c-name" placeholder="11А · Биология" />
</div>
</div>
</div>
<div class="form-group">
<div class="icon-picker-label">Иконка класса</div>
<div class="icon-picker" id="c-icon-picker"></div>
<div class="icon-picker-label" style="margin-top:10px">Цвет</div>
<div class="color-picker" id="c-color-picker"></div>
</div>
<div class="form-group">
<label class="form-label">Описание (необязательно)</label>
<textarea class="form-textarea" id="c-desc" rows="2" placeholder="Подготовка к ЦТ 2026"></textarea>
</div>
<div class="modal-footer">
<button class="btn-cancel" onclick="closeModal('modal-class')">Отмена</button>
<button class="btn-save" id="btn-save-class" onclick="saveClass()">Создать</button>
</div>
</div>
</div>
<!-- Modal: Create assignment -->
<div class="modal-overlay" id="modal-assign" onclick="closeOnOverlay(event,'modal-assign')">
<div class="modal">
@@ -977,25 +948,6 @@
</div>
</div>
<!-- Modal: Edit assignment -->
<div class="modal-overlay" id="modal-edit-assign" onclick="closeOnOverlay(event,'modal-edit-assign')">
<div class="modal">
<div class="modal-title">Редактировать задание</div>
<div class="form-group">
<label class="form-label">Название</label>
<input class="form-input" id="ea-title" />
</div>
<div class="form-group">
<label class="form-label">Дедлайн</label>
<input class="form-input" id="ea-deadline" type="datetime-local" />
</div>
<div class="modal-footer">
<button class="btn-cancel" onclick="closeModal('modal-edit-assign')">Отмена</button>
<button class="btn-save" id="btn-save-edit-assign" onclick="saveEditAssignment()">Сохранить</button>
</div>
</div>
</div>
<!-- Review submission modal -->
<div class="modal-overlay" id="review-modal" onclick="if(event.target===this)closeReviewModal()">
<div class="modal">
@@ -1216,15 +1168,42 @@
/* ══ Create class ══ */
function openCreateClass() {
document.getElementById('c-name').value = '';
document.getElementById('c-desc').value = '';
_selectedIcon = 'book-open';
_selectedColor = '#9B5DE5';
const body = `
<div class="ip-row">
<div class="ip-preview" id="c-icon-preview"><i data-lucide="book-open"></i></div>
<div style="flex:1">
<div class="form-group" style="margin-bottom:0">
<label class="form-label">Название класса</label>
<input class="form-input" id="c-name" placeholder="11А · Биология" />
</div>
</div>
</div>
<div class="form-group">
<div class="icon-picker-label">Иконка класса</div>
<div class="icon-picker" id="c-icon-picker"></div>
<div class="icon-picker-label" style="margin-top:10px">Цвет</div>
<div class="color-picker" id="c-color-picker"></div>
</div>
<div class="form-group">
<label class="form-label">Описание (необязательно)</label>
<textarea class="form-textarea" id="c-desc" rows="2" placeholder="Подготовка к ЦТ 2026"></textarea>
</div>`;
_classModal = LS.modal({
title: 'Создать класс',
content: body,
size: 'md',
actions: [
{ label: 'Отмена', onClick: () => _classModal.close() },
{ label: 'Создать', primary: true, id: 'btn-save-class', onClick: saveClass },
],
});
renderIconPreview('c-icon-preview');
renderIconPicker('c-icon-picker', 'c-icon-preview');
renderColorPicker('c-color-picker', 'c-icon-preview');
openModal('modal-class');
}
let _classModal = null;
/* ══ Icon picker (Lucide SVG) ══════════════════════════════════════ */
const CLASS_ICONS = [
// Биология / Химия / Наука
@@ -1326,7 +1305,7 @@
btn.disabled = true;
try {
const c = await LS.createClass({ name, description, cover_emoji: encodeIconValue(_selectedIcon, _selectedColor) });
closeModal('modal-class');
_classModal?.close();
toast('Класс создан! Код: ' + c.invite_code);
await loadClasses();
openClass(c.id);
@@ -1721,13 +1700,30 @@
/* ══ Edit assignment ══ */
let _editingAssign = null;
let _editModal = null;
function openEditAssignment(id) {
_editingAssign = _classAssignments.find(x => x.id === id);
if (!_editingAssign) return;
document.getElementById('ea-title').value = _editingAssign.title;
const dl = _editingAssign.deadline;
document.getElementById('ea-deadline').value = dl ? dl.replace(' ', 'T').slice(0, 16) : '';
openModal('modal-edit-assign');
const dlVal = dl ? dl.replace(' ', 'T').slice(0, 16) : '';
const body = `
<div class="form-group">
<label class="form-label">Название</label>
<input class="form-input" id="ea-title" value="${LS.esc(_editingAssign.title)}" />
</div>
<div class="form-group">
<label class="form-label">Дедлайн</label>
<input class="form-input" id="ea-deadline" type="datetime-local" value="${dlVal}" />
</div>`;
_editModal = LS.modal({
title: 'Редактировать задание',
content: body,
size: 'sm',
actions: [
{ label: 'Отмена', onClick: () => _editModal.close() },
{ label: 'Сохранить', primary: true, id: 'btn-save-edit-assign', onClick: saveEditAssignment },
],
});
}
async function saveEditAssignment() {
if (!_editingAssign) return;
@@ -1745,7 +1741,7 @@
count: _editingAssign.count || 25,
test_id: _editingAssign.test_id || null,
});
closeModal('modal-edit-assign');
_editModal?.close();
toast('Изменения сохранены');
await openClass(currentClass.id);
document.querySelector('[data-tab="assign"]').click();
+42 -77
View File
@@ -245,40 +245,7 @@
</div>
<!-- ─── Folder Modal (create / rename) ─────────────────────────────── -->
<div class="modal-overlay" id="folder-modal" onclick="if(event.target===this)closeFolderModal()">
<div class="modal-box" style="max-width:420px">
<div class="modal-title" id="fm-title">Новая папка</div>
<div class="form-row">
<label class="form-label">Название папки</label>
<input type="text" class="form-ctrl" id="fm-name" placeholder="Например: Биология 10 класс" maxlength="80" />
</div>
<div class="form-error" id="fm-error"></div>
<div class="modal-footer">
<button class="btn-cancel" onclick="closeFolderModal()">Отмена</button>
<button class="btn-save" id="fm-save" onclick="saveFolderModal()">Создать</button>
</div>
</div>
</div>
<!-- ─── Move File Modal ─────────────────────────────────────────────── -->
<div class="modal-overlay" id="move-modal" onclick="if(event.target===this)closeMoveModal()">
<div class="modal-box" style="max-width:400px">
<div class="modal-title">Переместить файл</div>
<div class="form-row">
<label class="form-label">Папка назначения</label>
<select class="form-ctrl" id="mv-folder">
<option value="">— Корневая папка —</option>
</select>
</div>
<div class="form-error" id="mv-error"></div>
<div class="modal-footer">
<button class="btn-cancel" onclick="closeMoveModal()">Отмена</button>
<button class="btn-save" id="mv-save" onclick="doMoveFile()">Переместить</button>
</div>
</div>
</div>
<!-- ─── Folder Access Modal ─────────────────────────────────────────── -->
<!-- ─── Folder Access Modal ─────────────────────────────────────────── -->
<div class="modal-overlay" id="folder-access-modal" onclick="if(event.target===this)closeFolderAccess()">
<div class="modal-box">
<div class="modal-title">
@@ -700,37 +667,37 @@
/* ─── Folder modal (create / rename) ─── */
let _editingFolderId = null;
let _folderModal = null;
function openFolderModal() {
_editingFolderId = null;
document.getElementById('fm-title').textContent = 'Новая папка';
document.getElementById('fm-save').textContent = 'Создать';
document.getElementById('fm-name').value = '';
document.getElementById('fm-error').textContent = '';
document.getElementById('folder-modal').classList.add('open');
setTimeout(() => document.getElementById('fm-name').focus(), 80);
function openFolderModal(opts = {}) {
_editingFolderId = opts.id || null;
const isRename = !!_editingFolderId;
const body = `
<div class="form-row">
<label class="form-label">Название папки</label>
<input type="text" class="form-ctrl" id="fm-name" placeholder="Например: Биология 10 класс" maxlength="80" value="${LS.esc(opts.name || '')}" />
</div>`;
_folderModal = LS.modal({
title: isRename ? 'Переименовать папку' : 'Новая папка',
content: body, size: 'sm',
actions: [
{ label: 'Отмена', onClick: () => _folderModal.close() },
{ label: isRename ? 'Сохранить' : 'Создать', primary: true, id: 'fm-save', onClick: saveFolderModal },
],
});
// Enter to submit
const nameEl = _folderModal.body.querySelector('#fm-name');
nameEl.addEventListener('keydown', e => { if (e.key === 'Enter') saveFolderModal(); });
if (isRename) setTimeout(() => nameEl.select(), 50);
}
function openRenameFolder(id, name) {
_editingFolderId = id;
document.getElementById('fm-title').textContent = 'Переименовать папку';
document.getElementById('fm-save').textContent = 'Сохранить';
document.getElementById('fm-name').value = name;
document.getElementById('fm-error').textContent = '';
document.getElementById('folder-modal').classList.add('open');
setTimeout(() => { const n = document.getElementById('fm-name'); n.focus(); n.select(); }, 80);
}
function closeFolderModal() {
document.getElementById('folder-modal').classList.remove('open');
openFolderModal({ id, name });
}
async function saveFolderModal() {
const name = document.getElementById('fm-name').value.trim();
const errEl = document.getElementById('fm-error');
errEl.textContent = '';
if (!name) { errEl.textContent = 'Введите название папки'; return; }
if (!name) { _folderModal?.setError('Введите название папки'); return; }
const btn = document.getElementById('fm-save');
btn.disabled = true;
try {
@@ -738,7 +705,6 @@
await LS.renameFolder(_editingFolderId, name);
const fo = allFolders.find(f => f.id === _editingFolderId);
if (fo) fo.name = name;
// update breadcrumb if we're inside this folder
if (currentFolder === _editingFolderId) {
document.getElementById('lib-bc-name').textContent = name;
}
@@ -746,22 +712,15 @@
const { id } = await LS.createFolder(name);
allFolders.push({ id, name, file_count: 0 });
}
closeFolderModal();
_folderModal?.close();
render();
} catch (e) {
errEl.textContent = e.message;
_folderModal?.setError(e.message);
} finally {
btn.disabled = false;
}
}
// Allow Enter key in folder name input
document.addEventListener('DOMContentLoaded', () => {
document.getElementById('fm-name').addEventListener('keydown', e => {
if (e.key === 'Enter') saveFolderModal();
});
});
async function doDeleteFolder(id, name) {
if (!await LS.confirm(`Удалить папку «${name}»? Файлы внутри переместятся в корневую папку.`, { title: 'Удалить папку', confirmText: 'Удалить' })) return;
try {
@@ -925,19 +884,25 @@
/* ─── Move file modal ─── */
let _movingFileId = null;
let _moveModal = null;
function openMoveModal(fileId) {
_movingFileId = fileId;
document.getElementById('mv-error').textContent = '';
const sel = document.getElementById('mv-folder');
const currentFolderId = allFiles.find(f => f.id === fileId)?.folder_id || '';
sel.innerHTML = `<option value="">— Корневая папка —</option>` +
const options = `<option value="">— Корневая папка —</option>` +
allFolders.map(fo => `<option value="${fo.id}" ${fo.id === currentFolderId ? 'selected' : ''}>${esc(fo.name)}</option>`).join('');
document.getElementById('move-modal').classList.add('open');
}
function closeMoveModal() {
document.getElementById('move-modal').classList.remove('open');
const body = `
<div class="form-row">
<label class="form-label">Папка назначения</label>
<select class="form-ctrl" id="mv-folder">${options}</select>
</div>`;
_moveModal = LS.modal({
title: 'Переместить файл', content: body, size: 'sm',
actions: [
{ label: 'Отмена', onClick: () => _moveModal.close() },
{ label: 'Переместить', primary: true, id: 'mv-save', onClick: doMoveFile },
],
});
}
async function doMoveFile() {
@@ -948,11 +913,11 @@
await LS.moveFile(_movingFileId, folderId ? Number(folderId) : null);
const f = allFiles.find(f => f.id === _movingFileId);
if (f) f.folder_id = folderId ? Number(folderId) : null;
closeMoveModal();
_moveModal?.close();
render();
LS.toast('Файл перемещён', 'success');
} catch (e) {
document.getElementById('mv-error').textContent = e.message;
_moveModal?.setError(e.message);
} finally {
btn.disabled = false;
}