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
+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;
}