From d7c64308b313a4782c01c6861528295f75ab36ba Mon Sep 17 00:00:00 2001 From: Vladislav Kopnin Date: Thu, 8 Oct 2026 00:54:51 +0300 Subject: [PATCH] Forms: return to previous scroll position after submit (no highlight/flicker); themed scrollbars everywhere --- .../ysu/lms/web/admin/AdminLabController.kt | 24 ++++------- web/src/main/resources/static/css/app.css | 5 +-- web/src/main/resources/static/js/app.js | 5 +++ web/src/main/resources/static/js/ui-state.js | 41 ++++++++++++++++--- .../main/resources/templates/admin/group.html | 2 +- .../main/resources/templates/admin/lab.html | 2 +- .../resources/templates/fragments/base.html | 22 +++++----- .../resources/templates/learn/article.html | 2 +- .../resources/templates/learn/chapter.html | 2 +- .../main/resources/templates/learn/index.html | 4 +- .../resources/templates/learn/section.html | 2 +- .../main/resources/templates/student/lab.html | 2 +- 12 files changed, 68 insertions(+), 45 deletions(-) diff --git a/web/src/main/kotlin/ru/ysu/lms/web/admin/AdminLabController.kt b/web/src/main/kotlin/ru/ysu/lms/web/admin/AdminLabController.kt index aaeb2ff..91df8cc 100644 --- a/web/src/main/kotlin/ru/ysu/lms/web/admin/AdminLabController.kt +++ b/web/src/main/kotlin/ru/ysu/lms/web/admin/AdminLabController.kt @@ -403,24 +403,14 @@ class AdminLabController( redirect: RedirectAttributes, ): String = try { - val detail = - api.saveVariant( - token(session), - labId, - form.variantId, - VariantSavePayload(form.labLevelId, form.number, form.title, form.statementMd), - ) + api.saveVariant( + token(session), + labId, + form.variantId, + VariantSavePayload(form.labLevelId, form.number, form.title, form.statementMd), + ) redirect.addFlashAttribute("notice", "Вариант сохранён") - // вернуться к отредактированному/созданному варианту на странице лабы - val anchor = - form.variantId - ?.takeIf { it.isNotBlank() } - ?: detail.levels - .firstOrNull { it.id == form.labLevelId } - ?.variants - ?.firstOrNull { it.number == form.number.trim() } - ?.id - if (anchor.isNullOrBlank()) "redirect:/admin/labs/$labId" else "redirect:/admin/labs/$labId#variant-$anchor" + "redirect:/admin/labs/$labId" } catch (ex: ApiException) { redirect.addFlashAttribute("error", ex.message) "redirect:/admin/labs/$labId" diff --git a/web/src/main/resources/static/css/app.css b/web/src/main/resources/static/css/app.css index 7a48e6f..95b8911 100644 --- a/web/src/main/resources/static/css/app.css +++ b/web/src/main/resources/static/css/app.css @@ -89,7 +89,6 @@ a:not(.btn):hover { color: var(--c-accent-hover); } .user { display: flex; align-items: center; gap: 0.75rem; font-size: 0.84375rem; margin-left: auto; } .nav-user { font-weight: 500; color: var(--c-text); } .score-big { display: inline-block; margin-left: 0.5rem; font-size: 1.75rem; font-weight: 700; line-height: 1; color: var(--c-tone-pos); font-variant-numeric: tabular-nums; vertical-align: middle; } -.variant-block:target { outline: 2px solid var(--c-accent); outline-offset: 4px; border-radius: var(--radius); } .variant-name { font-weight: 600; } .variant-name b { font-variant-numeric: tabular-nums; } p { margin-top: 0; } @@ -179,8 +178,8 @@ code { --bs-code-color: var(--c-text); color: var(--c-text); } a { --bs-link-color: var(--c-accent); --bs-link-hover-color: var(--c-accent-hover); text-decoration: none; } -/* ---- Полосы прокрутки в стиле приложения ---- */ -html { scrollbar-width: thin; scrollbar-color: var(--c-border-strong) var(--c-bg); } +/* ---- Полосы прокрутки в стиле приложения (везде: страница, модалки, таблицы, pre) ---- */ +* { scrollbar-width: thin; scrollbar-color: var(--c-border-strong) var(--c-bg); } ::-webkit-scrollbar { width: 10px; height: 10px; } ::-webkit-scrollbar-track { background: var(--c-bg); } ::-webkit-scrollbar-thumb { background: var(--c-border-strong); border: 2px solid var(--c-bg); border-radius: 8px; } diff --git a/web/src/main/resources/static/js/app.js b/web/src/main/resources/static/js/app.js index 10497f3..f0b3886 100644 --- a/web/src/main/resources/static/js/app.js +++ b/web/src/main/resources/static/js/app.js @@ -42,6 +42,8 @@ if (document.querySelector('dialog[open]')) { if (root.classList.contains('modal-open')) return; modalScrollY = window.scrollY || window.pageYOffset || 0; + // запоминаем позицию до блокировки — её использует ui-state.js при сохранении формы + window.__pageScrollY = modalScrollY; var sw = window.innerWidth - root.clientWidth; root.style.setProperty('--modal-scrollbar', (sw > 0 ? sw : 0) + 'px'); root.style.setProperty('--modal-top', (-modalScrollY) + 'px'); @@ -51,6 +53,9 @@ root.style.removeProperty('--modal-scrollbar'); root.style.removeProperty('--modal-top'); window.scrollTo(0, modalScrollY); + // повторно после пересчёта разметки, иначе прокрутка может «зажаться» в 0 + requestAnimationFrame(function () { window.scrollTo(0, modalScrollY); }); + delete window.__pageScrollY; } } document.addEventListener('toggle', function (e) { diff --git a/web/src/main/resources/static/js/ui-state.js b/web/src/main/resources/static/js/ui-state.js index 76fc62e..05659ae 100644 --- a/web/src/main/resources/static/js/ui-state.js +++ b/web/src/main/resources/static/js/ui-state.js @@ -1,8 +1,10 @@ -/* Состояние страницы при перезагрузке: открытые
и позиция прокрутки. - Нужно, чтобы после сохранения формы или перерисовки страница возвращалась - туда же, а не «схлопывала» разделы и не сбрасывала прокрутку в начало. */ +/* Состояние страницы при перезагрузке/возврате после отправки формы: + позиция прокрутки и раскрытые
. Возврат — мгновенный, без анимации. + Позиция фиксируется «на лету», пока страница не заблокирована модалкой + (при открытой модалке body зафиксирован и window.scrollY = 0 — его нельзя терять). */ (function () { var KEY = 'lms-ui-state'; + var lastY = window.scrollY || window.pageYOffset || 0; function keyOf(element, index) { return element.getAttribute('data-chapter-id') @@ -11,8 +13,28 @@ || ('details-' + index); } + // ключ страницы — только путь: после поисковых GET-форм добавляется ?q=, + // но это та же страница, и позицию нужно восстановить function currentUrl() { - return location.pathname + location.search; + return location.pathname; + } + + function locked() { + return document.documentElement.classList.contains('modal-open'); + } + + // запоминаем позицию до блокировки скролла модалкой + window.addEventListener('scroll', function () { + if (!locked()) lastY = window.scrollY || window.pageYOffset || 0; + }, { passive: true }); + + function scrollY() { + // позиция, надёжная при любом состоянии: window.scrollY может быть 0 + // из-за блокировки модалкой или зажатия прокрутки при снятии блокировки, + // поэтому берём максимум из текущей, последней до блокировки и сохранённой app.js + var y = window.scrollY || window.pageYOffset || 0; + if (typeof window.__pageScrollY === 'number') y = Math.max(y, window.__pageScrollY); + return Math.max(y, lastY); } function save() { @@ -23,7 +45,7 @@ try { sessionStorage.setItem(KEY, JSON.stringify({ url: currentUrl(), - y: window.scrollY || window.pageYOffset || 0, + y: scrollY(), open: open })); } catch (e) { /* приватный режим — просто пропускаем */ } @@ -49,11 +71,18 @@ if (typeof state.y === 'number' && state.y > 0) { var y = state.y; + // мгновенно, без плавной прокрутки; повторно — после полной загрузки, + // если разметка «доразрослась» и позиция сбилась + window.scrollTo(0, y); requestAnimationFrame(function () { window.scrollTo(0, y); }); - setTimeout(function () { window.scrollTo(0, y); }, 80); + window.addEventListener('load', function () { window.scrollTo(0, y); }); } } + // сохраняем перед любой отправкой формы (в т.ч. из модалок) и при уходе со страницы + document.addEventListener('submit', save, true); window.addEventListener('beforeunload', save); + // возврат по Back/Forward из кэша браузера + window.addEventListener('pageshow', function (event) { if (event.persisted) restore(); }); document.addEventListener('DOMContentLoaded', restore); })(); diff --git a/web/src/main/resources/templates/admin/group.html b/web/src/main/resources/templates/admin/group.html index 5474cb7..91f02f8 100644 --- a/web/src/main/resources/templates/admin/group.html +++ b/web/src/main/resources/templates/admin/group.html @@ -342,7 +342,7 @@
- + diff --git a/web/src/main/resources/templates/admin/lab.html b/web/src/main/resources/templates/admin/lab.html index c69a00a..fa4f984 100644 --- a/web/src/main/resources/templates/admin/lab.html +++ b/web/src/main/resources/templates/admin/lab.html @@ -196,7 +196,7 @@ - + diff --git a/web/src/main/resources/templates/fragments/base.html b/web/src/main/resources/templates/fragments/base.html index ac92de5..286bc6b 100644 --- a/web/src/main/resources/templates/fragments/base.html +++ b/web/src/main/resources/templates/fragments/base.html @@ -17,18 +17,18 @@ - - - - - - + + + + + + - - - - - + + + + +
diff --git a/web/src/main/resources/templates/learn/article.html b/web/src/main/resources/templates/learn/article.html index b06427a..b715911 100644 --- a/web/src/main/resources/templates/learn/article.html +++ b/web/src/main/resources/templates/learn/article.html @@ -124,7 +124,7 @@
- + diff --git a/web/src/main/resources/templates/learn/chapter.html b/web/src/main/resources/templates/learn/chapter.html index f2a9113..49601c4 100644 --- a/web/src/main/resources/templates/learn/chapter.html +++ b/web/src/main/resources/templates/learn/chapter.html @@ -183,7 +183,7 @@ - + diff --git a/web/src/main/resources/templates/learn/index.html b/web/src/main/resources/templates/learn/index.html index 2cb9a9f..d43a216 100644 --- a/web/src/main/resources/templates/learn/index.html +++ b/web/src/main/resources/templates/learn/index.html @@ -236,8 +236,8 @@ - - + + diff --git a/web/src/main/resources/templates/learn/section.html b/web/src/main/resources/templates/learn/section.html index d8e0633..30f26a4 100644 --- a/web/src/main/resources/templates/learn/section.html +++ b/web/src/main/resources/templates/learn/section.html @@ -143,7 +143,7 @@ - + diff --git a/web/src/main/resources/templates/student/lab.html b/web/src/main/resources/templates/student/lab.html index 6baff56..dd6c4b3 100644 --- a/web/src/main/resources/templates/student/lab.html +++ b/web/src/main/resources/templates/student/lab.html @@ -107,7 +107,7 @@

Ожидайте преподавателя с проверкой.

- +