Forms: return to previous scroll position after submit (no highlight/flicker); themed scrollbars everywhere
This commit is contained in:
@@ -403,7 +403,6 @@ class AdminLabController(
|
||||
redirect: RedirectAttributes,
|
||||
): String =
|
||||
try {
|
||||
val detail =
|
||||
api.saveVariant(
|
||||
token(session),
|
||||
labId,
|
||||
@@ -411,16 +410,7 @@ class AdminLabController(
|
||||
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"
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
/* Состояние страницы при перезагрузке: открытые <details> и позиция прокрутки.
|
||||
Нужно, чтобы после сохранения формы или перерисовки страница возвращалась
|
||||
туда же, а не «схлопывала» разделы и не сбрасывала прокрутку в начало. */
|
||||
/* Состояние страницы при перезагрузке/возврате после отправки формы:
|
||||
позиция прокрутки и раскрытые <details>. Возврат — мгновенный, без анимации.
|
||||
Позиция фиксируется «на лету», пока страница не заблокирована модалкой
|
||||
(при открытой модалке 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);
|
||||
})();
|
||||
|
||||
@@ -342,7 +342,7 @@
|
||||
<div class="dlg-body" id="assign-modal-body"></div>
|
||||
</dialog>
|
||||
|
||||
<script th:src="@{/js/admin-group.js(v='20')}" defer></script>
|
||||
<script th:src="@{/js/admin-group.js(v='21')}" defer></script>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -196,7 +196,7 @@
|
||||
</dialog>
|
||||
</th:block>
|
||||
</div>
|
||||
<script th:src="@{/js/admin-lab.js(v='20')}" defer></script>
|
||||
<script th:src="@{/js/admin-lab.js(v='21')}" defer></script>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -17,18 +17,18 @@
|
||||
<!-- v=4: сброс кэша стилей после редизайна; при изменении css поднимать версию -->
|
||||
<link rel="stylesheet" th:href="@{/css/bootstrap.min.css}">
|
||||
<link rel="stylesheet" th:href="@{/css/bootstrap-icons.min.css}">
|
||||
<link rel="stylesheet" th:href="@{/css/app.css(v='20')}">
|
||||
<link id="hljs-light" rel="stylesheet" th:href="@{/css/hljs-github.css(v='20')}">
|
||||
<link id="hljs-dark" rel="stylesheet" th:href="@{/css/hljs-github-dark.css(v='20')}" disabled>
|
||||
<script th:src="@{/js/highlight.min.js(v='20')}" defer></script>
|
||||
<script th:src="@{/js/table-scroll.js(v='20')}" defer></script>
|
||||
<script th:src="@{/js/tablekit.js(v='20')}" defer></script>
|
||||
<link rel="stylesheet" th:href="@{/css/app.css(v='21')}">
|
||||
<link id="hljs-light" rel="stylesheet" th:href="@{/css/hljs-github.css(v='21')}">
|
||||
<link id="hljs-dark" rel="stylesheet" th:href="@{/css/hljs-github-dark.css(v='21')}" disabled>
|
||||
<script th:src="@{/js/highlight.min.js(v='21')}" defer></script>
|
||||
<script th:src="@{/js/table-scroll.js(v='21')}" defer></script>
|
||||
<script th:src="@{/js/tablekit.js(v='21')}" defer></script>
|
||||
<script th:src="@{/js/bootstrap.bundle.min.js}" defer></script>
|
||||
<script th:src="@{/js/theme-init.js(v='20')}"></script>
|
||||
<script th:src="@{/js/app.js(v='20')}" defer></script>
|
||||
<script th:src="@{/js/ui-state.js(v='20')}" defer></script>
|
||||
<script th:src="@{/js/md-content.js(v='20')}" defer></script>
|
||||
<script th:src="@{/js/submit-guard.js(v='20')}" defer></script>
|
||||
<script th:src="@{/js/theme-init.js(v='21')}"></script>
|
||||
<script th:src="@{/js/app.js(v='24')}" defer></script>
|
||||
<script th:src="@{/js/ui-state.js(v='24')}" defer></script>
|
||||
<script th:src="@{/js/md-content.js(v='21')}" defer></script>
|
||||
<script th:src="@{/js/submit-guard.js(v='21')}" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="page d-flex flex-column min-vh-100">
|
||||
|
||||
@@ -124,7 +124,7 @@
|
||||
</div>
|
||||
|
||||
|
||||
<script th:src="@{/js/mdeditor.js(v='20')}" defer></script>
|
||||
<script th:src="@{/js/mdeditor.js(v='21')}" defer></script>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -183,7 +183,7 @@
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<script th:src="@{/js/content-sort.js(v='20')}" defer></script>
|
||||
<script th:src="@{/js/content-sort.js(v='21')}" defer></script>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -236,8 +236,8 @@
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<script th:src="@{/js/mdeditor.js(v='20')}" defer></script>
|
||||
<script th:src="@{/js/content-sort.js(v='20')}" defer></script>
|
||||
<script th:src="@{/js/mdeditor.js(v='21')}" defer></script>
|
||||
<script th:src="@{/js/content-sort.js(v='21')}" defer></script>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -143,7 +143,7 @@
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<script th:src="@{/js/content-sort.js(v='20')}" defer></script>
|
||||
<script th:src="@{/js/content-sort.js(v='21')}" defer></script>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -107,7 +107,7 @@
|
||||
<p class="text-sm text-muted">Ожидайте преподавателя с проверкой.</p>
|
||||
</div>
|
||||
</div>
|
||||
<script th:src="@{/js/defense-timer.js(v='20')}" defer></script>
|
||||
<script th:src="@{/js/defense-timer.js(v='21')}" defer></script>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user