No inline JS/styles in templates: app.js delegation (data-action/data-modal/data-confirm/data-auto-submit), B3 confirm/alert dialogs, theme-init file

This commit is contained in:
2026-10-02 18:41:12 +03:00
parent aee1672058
commit 6cb2e558ca
18 changed files with 382 additions and 209 deletions

View File

@@ -33,8 +33,7 @@
th:attr="data-refresh=${refreshUrl}">
<input type="hidden" name="_csrf" th:value="${csrfToken}">
<input type="hidden" name="studentId" th:value="${row.studentId}">
<select class="input" name="labLevelId" th:disabled="${#lists.isEmpty(lab.levels)}"
onchange="this.closest('form').requestSubmit()">
<select class="input" name="labLevelId" th:disabled="${#lists.isEmpty(lab.levels)}" data-auto-submit>
<option th:each="lv : ${lab.levels}" th:value="${lv.id}"
th:selected="${lv.id == row.levelId}" th:text="${lv.title}">Уровень</option>
</select>
@@ -46,7 +45,7 @@
th:attr="data-refresh=${refreshUrl}">
<input type="hidden" name="_csrf" th:value="${csrfToken}">
<input type="hidden" name="studentId" th:value="${row.studentId}">
<select class="input" name="variantId" onchange="confirmAssignVariant(this)">
<select class="input js-variant-select" name="variantId">
<option value="">— авто —</option>
<optgroup th:each="lv : ${lab.levels}" th:label="${lv.title}">
<option th:each="v : ${lv.variants}" th:value="${v.id}"

View File

@@ -19,14 +19,14 @@
<a class="btn btn-secondary" th:href="@{/admin/groups/{id}/submissions.zip(id=${group.id})}"
title="ZIP со всеми последними решениями группы по всем лабораторным">Скачать работы</a>
<a class="btn btn-secondary" th:href="@{/admin/groups/{id}/credentials(id=${group.id}, format='xlsx')}">Скачать пароли</a>
<button class="btn" type="button" onclick="openModal('modal-add-student')">+ Студент</button>
<button class="btn" type="button" data-modal="modal-add-student">+ Студент</button>
</div>
</div>
<div class="toolbar">
<input class="input table-search w-260" data-target="group-table"
placeholder="Поиск по студенту…" aria-label="Поиск по студенту">
<select class="input" id="lab-focus" onchange="focusLab()">
<select class="input" id="lab-focus">
<option value="">все лабораторные</option>
<option th:each="lab : ${labs}" th:value="${lab.id}" th:text="|ЛР ${lab.number}|">—</option>
</select>
@@ -47,8 +47,7 @@
<span class="lab-col-title" th:text="${lab.title}">—</span>
<div class="lab-head-actions">
<button class="mini-btn" type="button"
th:attr="data-url=@{/admin/groups/{id}/assign-modal(id=${group.id}, labId=${lab.id})},data-lab-name=|ЛР ${lab.number}|"
onclick="openLabAssign(this)"
th:attr="data-url=@{/admin/groups/{id}/assign-modal(id=${group.id}, labId=${lab.id})},data-lab-name=|ЛР ${lab.number}|" data-action="assign"
title="Выдача вариантов: уровни, варианты, автокраспределение по группе">
<svg class="cell-ico" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M4 6h16M4 12h16M4 18h16"/></svg>
</button>
@@ -89,16 +88,14 @@
<div class="cell-main">
<button class="variant-btn" type="button"
th:classappend="${cell.variantManual} ? 'manual' : ''"
th:attr="data-student=${row.studentId},data-student-name=${row.fullName},data-lab=${cell.labId},data-lab-name='ЛР ' + ${cell.labNumber},data-variant=${cell.variantId}"
onclick="openVariant(this)"
th:attr="data-student=${row.studentId},data-student-name=${row.fullName},data-lab=${cell.labId},data-lab-name='ЛР ' + ${cell.labNumber},data-variant=${cell.variantId}" data-action="variant"
title="Назначить вариант вручную или выдать случайный">
<span th:if="${cell.variantNumber}" th:text="'В-' + ${cell.variantNumber}">В-1</span>
<span th:if="${cell.variantNumber == null}" class="cell-none">выдать</span>
</button>
<button class="score-link" type="button"
th:classappend="${cell.score == null} ? 'none' : ''"
th:attr="data-student=${row.studentId},data-student-name=${row.fullName},data-lab=${cell.labId},data-lab-name='ЛР ' + ${cell.labNumber},data-score=${cell.score},data-comments=${row.commentsJson},data-ci=${iterStat.index}"
onclick="openGradeEditor(this)"
th:attr="data-student=${row.studentId},data-student-name=${row.fullName},data-lab=${cell.labId},data-lab-name='ЛР ' + ${cell.labNumber},data-score=${cell.score},data-comments=${row.commentsJson},data-ci=${iterStat.index}" data-action="grade"
th:title="${cell.score} ? 'Оценка ' + ${cell.score} + ' — изменить' : 'Оценки нет — выставить балл и комментарии'">
<span th:text="${cell.score} ?: '—'">—</span>
</button>
@@ -113,8 +110,7 @@
<span th:unless="${cell.submissionId}" class="sub-dim">не сдано</span>
<button class="cell-btn st js-defense-cell" type="button"
th:classappend="${cell.defenseStatus == null} ? 'dim' : (${cell.defenseStatus == 'ready'} ? 'ok' : 'warn')"
th:attr="data-student=${row.studentId},data-student-name=${row.fullName},data-lab=${cell.labId},data-lab-name='ЛР ' + ${cell.labNumber},data-md=${cell.defenseAssignmentMd},data-session=${cell.defenseSessionId},data-status=${cell.defenseStatus},data-seconds=${cell.defenseSecondsLeft},data-code=${cell.defenseCode},data-reason=${cell.defenseReadyReason}"
onclick="openDefense(this)"
th:attr="data-student=${row.studentId},data-student-name=${row.fullName},data-lab=${cell.labId},data-lab-name='ЛР ' + ${cell.labNumber},data-md=${cell.defenseAssignmentMd},data-session=${cell.defenseSessionId},data-status=${cell.defenseStatus},data-seconds=${cell.defenseSecondsLeft},data-code=${cell.defenseCode},data-reason=${cell.defenseReadyReason}" data-action="defense"
th:title="${cell.defenseStatus == null} ? 'Защита не назначена — нажмите, чтобы задать задание' : 'Защита: задание и действия'">
<span class="cell-dim">защита:</span>
<span class="js-def-label"
@@ -132,15 +128,13 @@
</td>
<td class="text-right actions-cell">
<div class="actions-col">
<form th:action="@{/admin/groups/{gid}/students/{sid}/reset-password(gid=${group.id}, sid=${row.studentId})}" method="post"
onsubmit="return confirm('Сбросить пароль студенту? Текущий пароль перестанет работать, новый покажется один раз.')">
<form th:action="@{/admin/groups/{gid}/students/{sid}/reset-password(gid=${group.id}, sid=${row.studentId})}" method="post" data-confirm="Сбросить пароль студенту? Текущий пароль перестанет работать, новый покажется один раз.">
<input type="hidden" name="_csrf" th:value="${csrfToken}">
<button class="btn btn-secondary btn-sm" type="submit"
title="Сгенерировать новый пароль (покажется один раз, потребуется смена при входе)">Сбросить</button>
</form>
<form th:action="@{/admin/groups/{gid}/students/{sid}/unenroll(gid=${group.id}, sid=${row.studentId})}"
method="post"
onsubmit="return confirm('Отчислить этого студента из группы? Аккаунт, сдача и оценки сохранятся; студент сможет войти и будет без группы.')">
method="post" data-confirm="Отчислить этого студента из группы? Аккаунт, сдача и оценки сохранятся; студент сможет войти и будет без группы.">
<input type="hidden" name="_csrf" th:value="${csrfToken}">
<button class="btn btn-sm btn-danger" type="submit">Отчислить</button>
</form>
@@ -157,7 +151,7 @@
<dialog id="modal-grade" class="modal modal-lg">
<div class="modal-header">
<h2 class="modal-title" id="grade-title">Оценка и комментарии</h2>
<button class="modal-close" type="button" aria-label="Закрыть" onclick="closeModal('modal-grade')">✕</button>
<button class="modal-close" type="button" aria-label="Закрыть">✕</button>
</div>
<div class="modal-body">
<form class="form-stack" id="grade-form" th:action="@{/admin/groups/{id}/grades(id=${group.id})}" method="post">
@@ -170,7 +164,7 @@
<input class="input" id="grade-score" name="score" type="number" min="0" max="5" step="0.1"></div>
<div id="cs-rows"></div>
<div class="flex gap-2">
<button class="btn btn-secondary" type="button" onclick="csRow('','neutral')">+ комментарий</button>
<button class="btn btn-secondary" type="button" data-action="comment-add">+ комментарий</button>
<button class="btn btn-primary" type="submit">Сохранить</button>
</div>
</form>
@@ -182,7 +176,7 @@
<dialog id="modal-variant" class="modal modal-lg">
<div class="modal-header">
<h2 class="modal-title">Назначить вариант</h2>
<button class="modal-close" type="button" aria-label="Закрыть" onclick="closeModal('modal-variant')">✕</button>
<button class="modal-close" type="button" aria-label="Закрыть">✕</button>
</div>
<div class="modal-body form-stack">
<p class="text-sm text-muted" id="variant-who">—</p>
@@ -218,7 +212,7 @@
<dialog id="modal-defense" class="modal">
<div class="modal-header">
<h2 class="modal-title">Индивидуальное задание</h2>
<button class="modal-close" type="button" aria-label="Закрыть" onclick="closeModal('modal-defense')">✕</button>
<button class="modal-close" type="button" aria-label="Закрыть">✕</button>
</div>
<div class="modal-body">
<form class="form-stack" id="defense-form" th:action="@{/admin/defenses/save}" method="post">
@@ -247,8 +241,7 @@
<input type="hidden" name="groupId" th:value="${group.id}">
<button class="btn btn-secondary" type="submit">Выдать код</button>
</form>
<form class="inline-form" id="defense-reset-form" method="post"
onsubmit="return confirm('Открыть защиту заново для нового (дополнительного) задания?')">
<form class="inline-form" id="defense-reset-form" method="post" data-confirm="Открыть защиту заново для нового (дополнительного) задания?">
<input type="hidden" name="_csrf" th:value="${csrfToken}">
<input type="hidden" name="labId" id="defense-reset-lab">
<input type="hidden" name="groupId" th:value="${group.id}">
@@ -267,7 +260,7 @@
<dialog id="modal-add-student" class="modal">
<div class="modal-header">
<h2 class="modal-title">Добавить студента</h2>
<button class="modal-close" type="button" aria-label="Закрыть" onclick="closeModal('modal-add-student')">✕</button>
<button class="modal-close" type="button" aria-label="Закрыть">✕</button>
</div>
<div class="modal-body">
<form class="form-stack" id="enroll-form" method="post"
@@ -346,7 +339,7 @@
<dialog id="modal-lab-assign" class="modal modal-lg">
<div class="modal-header">
<h2 class="modal-title" id="assign-modal-title">Выдача вариантов</h2>
<button class="modal-close" type="button" aria-label="Закрыть" onclick="closeModal('modal-lab-assign')">✕</button>
<button class="modal-close" type="button" aria-label="Закрыть">✕</button>
</div>
<div class="modal-body" id="assign-modal-body"></div>
</dialog>

View File

@@ -8,8 +8,8 @@
<div class="flex gap-2">
<input class="input table-search" data-target="groups-table"
placeholder="Поиск по названию…" aria-label="Поиск по группам">
<button class="btn btn-secondary" type="button" onclick="openModal('modal-import-group')">Импорт</button>
<button class="btn btn-primary" type="button" onclick="openModal('modal-create-group')">+ Новая группа</button>
<button class="btn btn-secondary" type="button" data-modal="modal-import-group">Импорт</button>
<button class="btn btn-primary" type="button" data-modal="modal-create-group">+ Новая группа</button>
</div>
</div>
@@ -30,7 +30,7 @@
<dialog id="modal-create-group" class="modal">
<div class="modal-header">
<h2 class="modal-title">Новая группа</h2>
<button class="modal-close" type="button" aria-label="Закрыть" onclick="closeModal('modal-create-group')">✕</button>
<button class="modal-close" type="button" aria-label="Закрыть">✕</button>
</div>
<div class="modal-body">
<form class="form-stack" th:action="@{/admin/groups}" method="post">
@@ -45,7 +45,7 @@
<dialog id="modal-import-group" class="modal">
<div class="modal-header">
<h2 class="modal-title">Импорт студентов</h2>
<button class="modal-close" type="button" aria-label="Закрыть" onclick="closeModal('modal-import-group')">✕</button>
<button class="modal-close" type="button" aria-label="Закрыть">✕</button>
</div>
<div class="modal-body">
<form class="form-stack" th:action="@{/admin/groups/import}" method="post" enctype="multipart/form-data">

View File

@@ -46,8 +46,7 @@
</td>
<td class="text-right">
<button class="btn btn-danger" type="submit" name="action"
th:value="|delete-level-${level.id}|"
onclick="return confirm('Удалить уровень? Несохранённые правки остальных полей формы будут потеряны. Варианты на уровне должны быть пустыми.')">
th:value="|delete-level-${level.id}|" data-confirm="Удалить уровень? Несохранённые правки остальных полей формы будут потеряны. Варианты на уровне должны быть пустыми.">
удалить</button>
</td>
</tr>
@@ -82,8 +81,7 @@
<div><label class="label">Условие (markdown)</label>
<textarea class="input" name="variantStatements" rows="6" th:text="${v.statementMd}"></textarea></div>
<button class="btn btn-danger" type="submit" name="action"
th:value="|delete-variant-${v.id}|"
onclick="return confirm('Удалить вариант? Несохранённые правки остальных полей формы будут потеряны.')">Удалить вариант</button>
th:value="|delete-variant-${v.id}|" data-confirm="Удалить вариант? Несохранённые правки остальных полей формы будут потеряны.">Удалить вариант</button>
</div>
</div>

View File

@@ -11,8 +11,7 @@
Статус: <span class="badge"
th:classappend="${lab.status == 'published'} ? 'badge-ok' : 'badge-warn'"
th:text="${lab.status == 'published'} ? 'Опубликована' : (${lab.status == 'archived'} ? 'В архиве' : 'Черновик')">draft</span>
<form class="inline-form" th:action="@{/admin/labs/{id}/toggle-status(id=${lab.id})}" method="post"
onsubmit="return confirm('Сменить статус лабораторной? Опубликованную увидят студенты, черновик — перестанут.')">
<form class="inline-form" th:action="@{/admin/labs/{id}/toggle-status(id=${lab.id})}" method="post" data-confirm="Сменить статус лабораторной? Опубликованную увидят студенты, черновик — перестанут.">
<input type="hidden" name="_csrf" th:value="${csrfToken}">
<button class="btn btn-secondary" type="submit"
th:text="${lab.status == 'published'} ? 'Скрыть' : 'Опубликовать'">Опубликовать</button>

View File

@@ -8,8 +8,8 @@
<div class="flex gap-2">
<input class="input table-search" data-target="labs-table"
placeholder="Поиск по названию…" aria-label="Поиск по лабораторным">
<button class="btn btn-secondary" type="button" onclick="openModal('modal-import-lab')">Импорт</button>
<button class="btn btn-primary" type="button" onclick="openModal('modal-create-lab')">+ Создать</button>
<button class="btn btn-secondary" type="button" data-modal="modal-import-lab">Импорт</button>
<button class="btn btn-primary" type="button" data-modal="modal-create-lab">+ Создать</button>
</div>
</div>
@@ -27,8 +27,7 @@
th:text="${lab.status == 'published'} ? 'Опубликована' : (${lab.status == 'archived'} ? 'В архиве' : 'Черновик')">—</span>
</td>
<td class="text-center">
<form th:action="@{/admin/labs/{id}/toggle-status(id=${lab.id})}" method="post"
onsubmit="return confirm('Сменить статус лабораторной? Опубликованную увидят студенты, черновик — перестанут.')">
<form th:action="@{/admin/labs/{id}/toggle-status(id=${lab.id})}" method="post" data-confirm="Сменить статус лабораторной? Опубликованную увидят студенты, черновик — перестанут.">
<input type="hidden" name="_csrf" th:value="${csrfToken}">
<input type="hidden" name="back" th:value="@{/admin/labs}">
<button class="btn"
@@ -37,8 +36,7 @@
th:text="${lab.status == 'published'} ? 'Скрыть' : 'Опубликовать'">—</button>
</form>
</td>
<td class="text-center"><form th:action="@{/admin/labs/{id}/delete(id=${lab.id})}" method="post"
onsubmit="return confirm('Удалить лабораторную вместе с вариантами и назначениями? Действие необратимо.')"><input type="hidden" name="_csrf" th:value="${csrfToken}"><button class="btn btn-danger" type="submit">Удалить</button></form></td>
<td class="text-center"><form th:action="@{/admin/labs/{id}/delete(id=${lab.id})}" method="post" data-confirm="Удалить лабораторную вместе с вариантами и назначениями? Действие необратимо."><input type="hidden" name="_csrf" th:value="${csrfToken}"><button class="btn btn-danger" type="submit">Удалить</button></form></td>
</tr>
<tr th:if="${#lists.isEmpty(labs)}"><td colspan="6" class="empty">Лабораторных пока нет.</td></tr>
</tbody>
@@ -49,7 +47,7 @@
<dialog id="modal-import-lab" class="modal">
<div class="modal-header">
<h2 class="modal-title">Импорт лабораторной</h2>
<button class="modal-close" type="button" aria-label="Закрыть" onclick="closeModal('modal-import-lab')">✕</button>
<button class="modal-close" type="button" aria-label="Закрыть">✕</button>
</div>
<div class="modal-body">
<form class="form-stack" th:action="@{/admin/labs/import}" method="post" enctype="multipart/form-data">
@@ -72,7 +70,7 @@
<dialog id="modal-create-lab" class="modal">
<div class="modal-header">
<h2 class="modal-title">Новая лабораторная</h2>
<button class="modal-close" type="button" aria-label="Закрыть" onclick="closeModal('modal-create-lab')">✕</button>
<button class="modal-close" type="button" aria-label="Закрыть">✕</button>
</div>
<div class="modal-body">
<form class="form-stack" th:action="@{/admin/labs/create}" method="post">

View File

@@ -30,8 +30,7 @@
<td class="font-mono" th:text="${u.studentNo}">—</td>
<td><a th:if="${u.groupId}" th:href="@{/admin/groups/{id}(id=${u.groupId})}" th:text="${u.groupName}">—</a><span th:unless="${u.groupId}" class="text-muted">без группы</span></td>
<td class="text-right">
<form class="inline-form" th:action="@{/admin/users/{id}/delete(id=${u.id})}" method="post"
onsubmit="return confirm('Удалить пользователя безвозвратно? Будут удалены аккаунт, группа, оценки с комментариями, сдачи с файлами и задания защит.');">
<form class="inline-form" th:action="@{/admin/users/{id}/delete(id=${u.id})}" method="post" data-confirm="Удалить пользователя безвозвратно? Будут удалены аккаунт, группа, оценки с комментариями, сдачи с файлами и задания защит.">
<input type="hidden" name="_csrf" th:value="${csrfToken}">
<input type="hidden" name="q" th:value="${query}">
<button class="btn btn-danger" type="submit">Удалить</button>