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:
@@ -21,86 +21,11 @@
|
||||
<script th:src="@{/js/highlight.min.js(v='10')}" defer></script>
|
||||
<script th:src="@{/js/table-scroll.js(v='10')}" defer></script>
|
||||
<script th:src="@{/js/tablekit.js(v='10')}" defer></script>
|
||||
<script th:src="@{/js/theme-init.js(v='10')}"></script>
|
||||
<script th:src="@{/js/app.js(v='10')}" defer></script>
|
||||
<script th:src="@{/js/ui-state.js(v='10')}" defer></script>
|
||||
<script th:src="@{/js/md-content.js(v='10')}" defer></script>
|
||||
<script th:src="@{/js/submit-guard.js(v='10')}" defer></script>
|
||||
<script>
|
||||
(function(){
|
||||
var t = localStorage.getItem('lms-theme') || 'light';
|
||||
document.documentElement.setAttribute('data-theme', t);
|
||||
window.__hljsSet = function(theme){
|
||||
var l = document.getElementById('hljs-light'), d = document.getElementById('hljs-dark');
|
||||
if (l && d) { l.disabled = theme !== 'light'; d.disabled = theme !== 'dark'; }
|
||||
};
|
||||
window.__hljsAll = function(){
|
||||
if (!window.hljs) return;
|
||||
document.querySelectorAll('pre code').forEach(function(el){ window.hljs.highlightElement(el); });
|
||||
};
|
||||
})();
|
||||
document.addEventListener('DOMContentLoaded', function(){ window.__hljsSet(document.documentElement.getAttribute('data-theme')); window.__hljsAll(); syncModalScrollLock(); initNavActive(); initSections(); });
|
||||
/* Активный пункт меню — как в макете: бордовый цвет у текущего раздела. */
|
||||
function initNavActive(){
|
||||
var path = window.location.pathname;
|
||||
document.querySelectorAll('#top-nav .nav-link').forEach(function (a) {
|
||||
var href = a.getAttribute('href') || '';
|
||||
if (path === href || (href !== '/' && path.indexOf(href + '/') === 0)) a.classList.add('active');
|
||||
});
|
||||
}
|
||||
function toggleTheme(){
|
||||
var e=document.documentElement; var t=e.getAttribute('data-theme')==='dark'?'light':'dark';
|
||||
e.setAttribute('data-theme',t); localStorage.setItem('lms-theme',t); window.__hljsSet(t);
|
||||
}
|
||||
function openModal(id){ var d=document.getElementById(id); if(d) d.showModal(); syncModalScrollLock(); }
|
||||
function closeModal(id){ var d=document.getElementById(id); if(d) d.close(); syncModalScrollLock(); }
|
||||
function openModalData(btn){ openModal(btn.dataset.modal); }
|
||||
function closeModalData(btn){ closeModal(btn.dataset.modal); }
|
||||
/* Пока открыта хоть одна модалка — блокируем прокрутку страницы, сохраняя позицию скролла
|
||||
(внутри .modal-body прокрутка остаётся). */
|
||||
var modalScrollY = 0;
|
||||
function syncModalScrollLock(){
|
||||
var root = document.documentElement;
|
||||
if (document.querySelector('dialog[open]')) {
|
||||
if (root.classList.contains('modal-open')) return;
|
||||
modalScrollY = window.scrollY || window.pageYOffset || 0;
|
||||
var sw = window.innerWidth - root.clientWidth;
|
||||
root.style.setProperty('--modal-scrollbar', (sw > 0 ? sw : 0) + 'px');
|
||||
root.style.setProperty('--modal-top', (-modalScrollY) + 'px');
|
||||
root.classList.add('modal-open');
|
||||
} else if (root.classList.contains('modal-open')) {
|
||||
root.classList.remove('modal-open');
|
||||
root.style.removeProperty('--modal-scrollbar');
|
||||
root.style.removeProperty('--modal-top');
|
||||
window.scrollTo(0, modalScrollY);
|
||||
}
|
||||
}
|
||||
document.addEventListener('toggle', function (e) {
|
||||
if (e.target && e.target.tagName === 'DIALOG') syncModalScrollLock();
|
||||
}, true);
|
||||
function sectionStateKey(id){ return 'lms-section-open:' + id; }
|
||||
function applySectionState(acc){
|
||||
var id = acc.getAttribute('data-section-id');
|
||||
var stored = null;
|
||||
try { stored = id ? localStorage.getItem(sectionStateKey(id)) : null; } catch (e) { /* приватный режим */ }
|
||||
var open = stored === '1' ? true
|
||||
: stored === '0' ? false
|
||||
: acc.getAttribute('data-default-open') === 'true';
|
||||
acc.classList.toggle('collapsed', !open);
|
||||
var btn = acc.querySelector('.sub-toggle');
|
||||
if (btn) btn.setAttribute('aria-expanded', open ? 'true' : 'false');
|
||||
}
|
||||
function initSections(){
|
||||
document.querySelectorAll('.sub-acc[data-section-id]').forEach(applySectionState);
|
||||
}
|
||||
function toggleSection(btn){
|
||||
var acc = btn.closest('.sub-acc');
|
||||
if (!acc) return;
|
||||
var collapsed = acc.classList.toggle('collapsed');
|
||||
btn.setAttribute('aria-expanded', collapsed ? 'false' : 'true');
|
||||
var id = acc.getAttribute('data-section-id');
|
||||
if (id) { try { localStorage.setItem(sectionStateKey(id), collapsed ? '0' : '1'); } catch (e) { /* приватный режим */ } }
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', initSections);
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="page">
|
||||
@@ -122,8 +47,8 @@
|
||||
<a th:href="@{/admin}" th:if="${currentUser.admin}" class="nav-user" th:text="${currentUser.name}" title="Личный кабинет">—</a>
|
||||
<a th:href="@{/student}" th:unless="${currentUser.admin}" class="nav-user" th:text="${currentUser.name}" title="Личный кабинет">—</a>
|
||||
</th:block>
|
||||
<a th:if="${currentUser == null}" th:href="@{/login}" class="btn btn-secondary btn-sm">Войти</a>
|
||||
<button type="button" onclick="toggleTheme()" class="btn btn-secondary btn-sm theme-btn"
|
||||
<a th:if="${currentUser == null}" th:href="@{/login}" class="btn">Войти</a>
|
||||
<button type="button" class="btn btn-secondary theme-btn"
|
||||
aria-label="Сменить тему" title="Сменить тему">
|
||||
<svg class="theme-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
|
||||
<circle cx="12" cy="12" r="5" fill="none" stroke="currentColor" stroke-width="1.6"/>
|
||||
@@ -141,7 +66,7 @@
|
||||
</svg>
|
||||
</button>
|
||||
<form th:if="${currentUser != null}" th:action="@{/logout}" method="post">
|
||||
<button class="btn btn-secondary btn-sm" type="submit">Выйти</button>
|
||||
<button class="btn" type="submit">Выйти</button>
|
||||
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
||||
</form>
|
||||
</div>
|
||||
@@ -151,39 +76,38 @@
|
||||
<th:block th:replace="${content}" />
|
||||
</main>
|
||||
<footer class="page-footer">
|
||||
LMS v0.2.0
|
||||
LMS v1.1.0
|
||||
</footer>
|
||||
</div>
|
||||
<div class="toast-host" id="toast-host" aria-live="polite">
|
||||
<div th:if="${notice}" class="toast-item toast-success" data-toast>
|
||||
<span class="toast-text" th:text="${notice}">…</span>
|
||||
<button class="toast-close" type="button" aria-label="Закрыть" onclick="this.parentElement.remove()">✕</button>
|
||||
<button class="toast-close" type="button" aria-label="Закрыть">✕</button>
|
||||
</div>
|
||||
<div th:if="${error}" class="toast-item toast-error" data-toast>
|
||||
<span class="toast-text" th:text="${error}">…</span>
|
||||
<button class="toast-close" type="button" aria-label="Закрыть" onclick="this.parentElement.remove()">✕</button>
|
||||
<button class="toast-close" type="button" aria-label="Закрыть">✕</button>
|
||||
</div>
|
||||
<div th:if="${resetPassword}" class="toast-item toast-warning" data-toast>
|
||||
<span class="toast-text">
|
||||
Новый пароль<span th:if="${resetFor}" th:text="| для ${resetFor}|"> для Иванов И.</span>:
|
||||
<code class="code font-mono" th:text="${resetPassword}">password</code>
|
||||
</span>
|
||||
<button class="toast-close" type="button" aria-label="Закрыть" onclick="this.parentElement.remove()">✕</button>
|
||||
<button class="toast-close" type="button" aria-label="Закрыть">✕</button>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
document.querySelectorAll('[data-toast]').forEach(function (toast) {
|
||||
var timer = setTimeout(function () { toast.remove(); }, 30000);
|
||||
var close = toast.querySelector('.toast-close');
|
||||
if (close) {
|
||||
close.addEventListener('click', function () {
|
||||
clearTimeout(timer);
|
||||
toast.remove();
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<dialog id="modal-confirm" class="modal">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title" id="confirm-title">Подтверждение</h2>
|
||||
<button class="modal-close" type="button" aria-label="Закрыть">✕</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p class="confirm-text" id="confirm-text">—</p>
|
||||
<div class="flex justify-end gap-2 mt-4">
|
||||
<button class="btn btn-secondary" type="button" id="confirm-cancel">Отмена</button>
|
||||
<button class="btn btn-primary" type="button" id="confirm-ok">ОК</button>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user