fix: validate grade input and save modal without reload
- require a score and return a clear message for empty/non-numeric values - handle unreadable API request bodies instead of falling back to a bare 400 - submit the grade modal via fetch, show errors in place and update the cell - keep the page scroll position; restore the form feedback tests
This commit is contained in:
@@ -615,8 +615,95 @@ function focusLab() {
|
||||
});
|
||||
}
|
||||
|
||||
/* ---------- Оценка: отправка без перезагрузки страницы ----------
|
||||
* Модалка не закрывается при ошибке, значения остаются на месте,
|
||||
* позиция прокрутки страницы не меняется. */
|
||||
function submitGradeForm(form) {
|
||||
if (form.dataset.saving === '1') return;
|
||||
form.dataset.saving = '1';
|
||||
var btn = form.querySelector('button[type="submit"]');
|
||||
if (btn) btn.disabled = true;
|
||||
|
||||
fetch(form.action, {
|
||||
method: 'POST',
|
||||
body: new FormData(form),
|
||||
headers: { 'X-Requested-With': 'fetch' }
|
||||
})
|
||||
.then(function (r) {
|
||||
if (!r.ok) throw new Error('HTTP ' + r.status);
|
||||
return r.text();
|
||||
})
|
||||
.then(function (html) {
|
||||
var doc = new DOMParser().parseFromString(html, 'text/html');
|
||||
|
||||
// Сервер вернул форму с ошибкой — показываем её без перезагрузки.
|
||||
var feedbackNode = doc.getElementById('form-feedback');
|
||||
if (feedbackNode) {
|
||||
var feedback = null;
|
||||
try {
|
||||
feedback = JSON.parse(feedbackNode.getAttribute('data-feedback') || 'null');
|
||||
} catch (e) {
|
||||
feedback = null;
|
||||
}
|
||||
if (feedback && typeof window.lmsApplyFormFeedback === 'function') {
|
||||
window.lmsApplyFormFeedback(feedback);
|
||||
}
|
||||
var errorToast = doc.querySelector('.toast-item.toast-error .toast-text');
|
||||
showToast(errorToast
|
||||
? errorToast.textContent.trim()
|
||||
: ((feedback && feedback.general) || 'Не удалось сохранить оценку'), 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
var student = form.querySelector('[name="studentId"]').value;
|
||||
var lab = form.querySelector('[name="labId"]').value;
|
||||
var fresh = doc.querySelector('.score-link[data-student="' + student + '"][data-lab="' + lab + '"]');
|
||||
var current = document.querySelector('.score-link[data-student="' + student + '"][data-lab="' + lab + '"]');
|
||||
var notice = doc.querySelector('.toast-item.toast-success .toast-text');
|
||||
var errorWithoutFeedback = doc.querySelector('.toast-item.toast-error .toast-text');
|
||||
|
||||
if (errorWithoutFeedback) {
|
||||
showToast(errorWithoutFeedback.textContent.trim(), 'error');
|
||||
return;
|
||||
}
|
||||
if (!fresh && !notice) {
|
||||
showToast('Не удалось сохранить оценку. Обновите страницу и попробуйте ещё раз.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (fresh && current) {
|
||||
['data-score', 'data-general-comment', 'data-comments', 'title'].forEach(function (attr) {
|
||||
if (fresh.hasAttribute(attr)) current.setAttribute(attr, fresh.getAttribute(attr));
|
||||
else current.removeAttribute(attr);
|
||||
});
|
||||
var freshLabel = fresh.querySelector('span');
|
||||
var currentLabel = current.querySelector('span');
|
||||
if (freshLabel && currentLabel) currentLabel.textContent = freshLabel.textContent;
|
||||
current.classList.toggle('none', fresh.classList.contains('none'));
|
||||
}
|
||||
|
||||
var openDialog = form.closest('dialog');
|
||||
if (openDialog) {
|
||||
var staleToasts = openDialog.querySelector('.toast-host');
|
||||
if (staleToasts) staleToasts.remove();
|
||||
}
|
||||
if (window.closeModal) window.closeModal('modal-grade');
|
||||
showToast(notice ? notice.textContent.trim() : 'Оценка сохранена', 'success');
|
||||
})
|
||||
.catch(function (e) {
|
||||
showToast('Не удалось сохранить оценку: ' + e.message, 'error');
|
||||
})
|
||||
.finally(function () {
|
||||
form.dataset.saving = '';
|
||||
if (btn) btn.disabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('submit', function (ev) {
|
||||
if (ev.target && ev.target.id === 'grade-form') csSerialize();
|
||||
if (!ev.target || ev.target.id !== 'grade-form') return;
|
||||
csSerialize();
|
||||
ev.preventDefault();
|
||||
submitGradeForm(ev.target);
|
||||
}, true);
|
||||
|
||||
window.addEventListener('DOMContentLoaded', function () {
|
||||
|
||||
@@ -193,6 +193,16 @@
|
||||
form.insertBefore(box, form.firstChild);
|
||||
}
|
||||
|
||||
function clearAllErrors(form) {
|
||||
form.querySelectorAll('.' + FIELD_ERROR).forEach(function (node) { node.remove(); });
|
||||
form.querySelectorAll('.' + INVALID).forEach(function (el) {
|
||||
el.classList.remove(INVALID);
|
||||
el.removeAttribute('aria-invalid');
|
||||
});
|
||||
var summary = form.querySelector('.' + SUMMARY);
|
||||
if (summary) summary.remove();
|
||||
}
|
||||
|
||||
function focusFirstInvalid(form) {
|
||||
var el = form.querySelector('.' + INVALID);
|
||||
if (!el || typeof el.focus !== 'function') return;
|
||||
@@ -223,6 +233,7 @@
|
||||
if (!form) return;
|
||||
|
||||
restoreValues(form, feedback.values);
|
||||
clearAllErrors(form);
|
||||
|
||||
if (form.hidden) form.hidden = false;
|
||||
var dialog = form.closest('dialog');
|
||||
@@ -248,6 +259,8 @@
|
||||
function validationMessage(el) {
|
||||
var v = el.validity || {};
|
||||
if (v.valueMissing) {
|
||||
var requiredMessage = el.getAttribute('data-error-required');
|
||||
if (requiredMessage) return requiredMessage;
|
||||
if (el.type === 'file') return 'Выберите файл';
|
||||
if (el.tagName === 'SELECT') return 'Выберите значение';
|
||||
if (el.type === 'checkbox' || el.type === 'radio') return 'Отметьте этот пункт';
|
||||
@@ -285,6 +298,9 @@
|
||||
document.addEventListener('input', function (event) { clearOnEdit(event.target); }, true);
|
||||
document.addEventListener('change', function (event) { clearOnEdit(event.target); }, true);
|
||||
|
||||
// Для AJAX-отправки форм: показать ответ сервера без перезагрузки страницы.
|
||||
window.lmsApplyFormFeedback = applyFeedback;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
// после ui-state.js: сначала восстанавливаем прокрутку, затем открываем модалку
|
||||
setTimeout(function () { applyFeedback(parseFeedback()); }, 0);
|
||||
|
||||
@@ -159,7 +159,8 @@
|
||||
<input type="hidden" name="commentsJson" id="grade-comments" value="[]">
|
||||
<div><label class="label">Студент</label><input class="input" id="grade-who" readonly></div>
|
||||
<div><label class="label" for="grade-score">Балл (0–5)</label>
|
||||
<input class="input" id="grade-score" name="score" type="number" min="0" max="5" step="0.1"></div>
|
||||
<input class="input" id="grade-score" name="score" type="number" min="0" max="5" step="0.1" required
|
||||
data-error-required="Укажите оценку"></div>
|
||||
<div><label class="label" for="grade-general">Общий комментарий по лабораторной</label>
|
||||
<textarea class="input" id="grade-general" name="generalComment" rows="4"
|
||||
placeholder="Итоговый комментарий по работе в целом…"></textarea></div>
|
||||
@@ -345,7 +346,7 @@
|
||||
<div class="dlg-body" id="assign-modal-body"></div>
|
||||
</dialog>
|
||||
|
||||
<script th:src="@{/js/admin-group.js(v='37')}" defer></script>
|
||||
<script th:src="@{/js/admin-group.js(v='38')}" defer></script>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
<script th:src="@{/js/theme-init.js(v='36')}"></script>
|
||||
<script th:src="@{/js/app.js(v='36')}" defer></script>
|
||||
<script th:src="@{/js/ui-state.js(v='36')}" defer></script>
|
||||
<script th:src="@{/js/form-feedback.js(v='37')}" defer></script>
|
||||
<script th:src="@{/js/form-feedback.js(v='38')}" defer></script>
|
||||
<script th:src="@{/js/md-content.js(v='36')}" defer></script>
|
||||
<script th:src="@{/js/submit-guard.js(v='36')}" defer></script>
|
||||
</head>
|
||||
|
||||
Reference in New Issue
Block a user