Admin/Public: add sticky admin sidebar and keyboard navigation
This commit is contained in:
parent
bc85fa1644
commit
d220c6c84b
|
|
@ -713,6 +713,7 @@ function nextUniqueCodeName(string $base): string
|
|||
.wrap{max-width:1180px;margin:0 auto;padding:24px}
|
||||
.card{background:#fff;border:1px solid #e5e7eb;border-radius:12px;padding:14px;margin-bottom:14px}
|
||||
.grid{display:grid;gap:12px;grid-template-columns:320px 1fr}
|
||||
.admin-sidebar{position:sticky;top:14px;align-self:start;max-height:calc(100dvh - 28px);overflow:auto}
|
||||
.in{width:100%;padding:8px;border:1px solid #d1d5db;border-radius:8px}
|
||||
.btn{border:0;background:#1f6feb;color:#fff;padding:8px 12px;border-radius:8px;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;white-space:nowrap}
|
||||
.btn-danger{background:#b42318}
|
||||
|
|
@ -762,7 +763,7 @@ function nextUniqueCodeName(string $base): string
|
|||
.comment-search{display:grid;grid-template-columns:minmax(180px,1fr) minmax(180px,1fr) auto auto;gap:8px;align-items:center;margin-bottom:12px}
|
||||
.comment-search .btn{height:36px}
|
||||
@media (max-width:760px){.comment-search{grid-template-columns:1fr}}
|
||||
@media (max-width:960px){.grid{grid-template-columns:1fr}}
|
||||
@media (max-width:960px){.grid{grid-template-columns:1fr}.admin-sidebar{position:static;top:auto;max-height:none;overflow:visible}}
|
||||
</style>
|
||||
</head>
|
||||
<body><div class="wrap">
|
||||
|
|
@ -771,7 +772,7 @@ function nextUniqueCodeName(string $base): string
|
|||
<?php foreach($errors as $e): ?><div class="err"><?= h($e) ?></div><?php endforeach; ?>
|
||||
|
||||
<div class="grid">
|
||||
<aside>
|
||||
<aside class="admin-sidebar">
|
||||
<section class="card">
|
||||
<h3>Меню</h3>
|
||||
<div class="sec">
|
||||
|
|
|
|||
169
index.php
169
index.php
|
|
@ -477,13 +477,13 @@ function outputWatermarked(string $path, string $mime): never
|
|||
|
||||
<h3 style="margin-top:16px">Комментарии</h3>
|
||||
<?php if ($viewer): ?>
|
||||
<form method="post" action="?photo_id=<?= (int)$photo['id'] ?><?= $isTopicMode ? '&topic_id=' . $activeTopicId : '§ion_id=' . (int)$detailSectionId ?><?= $viewerToken!=='' ? '&viewer=' . urlencode($viewerToken) : '' ?>">
|
||||
<form class="js-comment-form" method="post" action="?photo_id=<?= (int)$photo['id'] ?><?= $isTopicMode ? '&topic_id=' . $activeTopicId : '§ion_id=' . (int)$detailSectionId ?><?= $viewerToken!=='' ? '&viewer=' . urlencode($viewerToken) : '' ?>">
|
||||
<input type="hidden" name="action" value="add_comment">
|
||||
<input type="hidden" name="photo_id" value="<?= (int)$photo['id'] ?>">
|
||||
<input type="hidden" name="section_id" value="<?= $isSectionMode ? (int)$detailSectionId : 0 ?>">
|
||||
<input type="hidden" name="topic_id" value="<?= $isTopicMode ? (int)$activeTopicId : 0 ?>">
|
||||
<input type="hidden" name="viewer" value="<?= h($viewerToken) ?>">
|
||||
<textarea name="comment_text" required style="width:100%;min-height:80px;border:1px solid #d1d5db;border-radius:8px;padding:8px"></textarea>
|
||||
<textarea class="js-comment-textarea" name="comment_text" required autofocus style="width:100%;min-height:80px;border:1px solid #d1d5db;border-radius:8px;padding:8px"></textarea>
|
||||
<p><button class="btn" type="submit">Отправить</button></p>
|
||||
</form>
|
||||
<?php else: ?>
|
||||
|
|
@ -498,8 +498,8 @@ function outputWatermarked(string $path, string $mime): never
|
|||
<div class="pager">
|
||||
<div class="muted">Фото <?= (int)$detailIndex ?> из <?= (int)$detailTotal ?><?= $detailLocationLabel !== '' ? ' ' . h($detailLocationLabel) : '' ?></div>
|
||||
<div class="pager-actions">
|
||||
<a class="pager-link<?= $prevPhotoId < 1 ? ' disabled' : '' ?>" href="?photo_id=<?= (int)$prevPhotoId ?><?= $isTopicMode ? '&topic_id=' . $activeTopicId : '§ion_id=' . (int)$detailSectionId ?><?= $viewerToken!=='' ? '&viewer=' . urlencode($viewerToken) : '' ?>">← Предыдущее</a>
|
||||
<a class="pager-link<?= $nextPhotoId < 1 ? ' disabled' : '' ?>" href="?photo_id=<?= (int)$nextPhotoId ?><?= $isTopicMode ? '&topic_id=' . $activeTopicId : '§ion_id=' . (int)$detailSectionId ?><?= $viewerToken!=='' ? '&viewer=' . urlencode($viewerToken) : '' ?>">Следующее →</a>
|
||||
<a class="pager-link js-prev-photo<?= $prevPhotoId < 1 ? ' disabled' : '' ?>" href="?photo_id=<?= (int)$prevPhotoId ?><?= $isTopicMode ? '&topic_id=' . $activeTopicId : '§ion_id=' . (int)$detailSectionId ?><?= $viewerToken!=='' ? '&viewer=' . urlencode($viewerToken) : '' ?>">← Предыдущее</a>
|
||||
<a class="pager-link js-next-photo<?= $nextPhotoId < 1 ? ' disabled' : '' ?>" href="?photo_id=<?= (int)$nextPhotoId ?><?= $isTopicMode ? '&topic_id=' . $activeTopicId : '§ion_id=' . (int)$detailSectionId ?><?= $viewerToken!=='' ? '&viewer=' . urlencode($viewerToken) : '' ?>">Следующее →</a>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
|
@ -514,7 +514,7 @@ function outputWatermarked(string $path, string $mime): never
|
|||
<div class="cards">
|
||||
<?php foreach($photos as $p): ?>
|
||||
<?php $cardCommentCount = (int)($photoCommentCounts[(int)$p['id']] ?? 0); ?>
|
||||
<a class="card" href="?photo_id=<?= (int)$p['id'] ?><?= $isTopicMode ? '&topic_id=' . $activeTopicId : '§ion_id=' . (int)$p['section_id'] ?><?= $viewerToken!=='' ? '&viewer=' . urlencode($viewerToken) : '' ?>" style="text-decoration:none;color:inherit;position:relative">
|
||||
<a class="card js-photo-card" href="?photo_id=<?= (int)$p['id'] ?><?= $isTopicMode ? '&topic_id=' . $activeTopicId : '§ion_id=' . (int)$p['section_id'] ?><?= $viewerToken!=='' ? '&viewer=' . urlencode($viewerToken) : '' ?>" style="text-decoration:none;color:inherit;position:relative">
|
||||
<?php if (!empty($p['before_file_id'])): ?><div class="img-box thumb-img-box"><img src="?action=image&file_id=<?= (int)$p['before_file_id'] ?>" alt="" loading="lazy" decoding="async" fetchpriority="low"></div><?php endif; ?>
|
||||
<div class="card-badges">
|
||||
<?php if ($cardCommentCount > 0): ?><span class="card-badge comments" title="Комментариев: <?= $cardCommentCount ?>">💬 <?= $cardCommentCount ?></span><?php endif; ?>
|
||||
|
|
@ -539,8 +539,8 @@ function outputWatermarked(string $path, string $mime): never
|
|||
<button class="mobile-nav-link js-sidebar-toggle" type="button" aria-controls="sidebar" aria-expanded="false">Меню</button>
|
||||
<a class="mobile-nav-link" href="?<?= $isTopicMode ? 'topic_id=' . $activeTopicId : 'section_id=' . (int)$detailSectionId ?><?= $viewerToken!=='' ? '&viewer=' . urlencode($viewerToken) : '' ?>"><?= $isTopicMode ? 'К тематике' : 'К разделу' ?></a>
|
||||
<div class="mobile-nav-meta">Фото <?= (int)$detailIndex ?> из <?= (int)$detailTotal ?><?= $detailLocationLabel !== '' ? ' ' . h($detailLocationLabel) : '' ?></div>
|
||||
<a class="mobile-nav-link<?= $prevPhotoId < 1 ? ' disabled' : '' ?>" href="?photo_id=<?= (int)$prevPhotoId ?><?= $isTopicMode ? '&topic_id=' . $activeTopicId : '§ion_id=' . (int)$detailSectionId ?><?= $viewerToken!=='' ? '&viewer=' . urlencode($viewerToken) : '' ?>" aria-disabled="<?= $prevPhotoId < 1 ? 'true' : 'false' ?>">←</a>
|
||||
<a class="mobile-nav-link<?= $nextPhotoId < 1 ? ' disabled' : '' ?>" href="?photo_id=<?= (int)$nextPhotoId ?><?= $isTopicMode ? '&topic_id=' . $activeTopicId : '§ion_id=' . (int)$detailSectionId ?><?= $viewerToken!=='' ? '&viewer=' . urlencode($viewerToken) : '' ?>" aria-disabled="<?= $nextPhotoId < 1 ? 'true' : 'false' ?>">→</a>
|
||||
<a class="mobile-nav-link js-prev-photo<?= $prevPhotoId < 1 ? ' disabled' : '' ?>" href="?photo_id=<?= (int)$prevPhotoId ?><?= $isTopicMode ? '&topic_id=' . $activeTopicId : '§ion_id=' . (int)$detailSectionId ?><?= $viewerToken!=='' ? '&viewer=' . urlencode($viewerToken) : '' ?>" aria-disabled="<?= $prevPhotoId < 1 ? 'true' : 'false' ?>">←</a>
|
||||
<a class="mobile-nav-link js-next-photo<?= $nextPhotoId < 1 ? ' disabled' : '' ?>" href="?photo_id=<?= (int)$nextPhotoId ?><?= $isTopicMode ? '&topic_id=' . $activeTopicId : '§ion_id=' . (int)$detailSectionId ?><?= $viewerToken!=='' ? '&viewer=' . urlencode($viewerToken) : '' ?>" aria-disabled="<?= $nextPhotoId < 1 ? 'true' : 'false' ?>">→</a>
|
||||
</nav>
|
||||
<?php elseif ($hasMobileCatalogNav): ?>
|
||||
<nav class="mobile-catalog-nav" aria-label="Навигация по каталогу">
|
||||
|
|
@ -680,6 +680,161 @@ function outputWatermarked(string $path, string $mime): never
|
|||
}
|
||||
}, { passive: true });
|
||||
})();
|
||||
|
||||
(() => {
|
||||
const commentTextarea = document.querySelector('.js-comment-textarea');
|
||||
const commentForm = commentTextarea ? commentTextarea.closest('.js-comment-form') : null;
|
||||
if (commentTextarea) {
|
||||
requestAnimationFrame(() => {
|
||||
commentTextarea.focus();
|
||||
commentTextarea.setSelectionRange(commentTextarea.value.length, commentTextarea.value.length);
|
||||
});
|
||||
|
||||
commentTextarea.addEventListener('keydown', (e) => {
|
||||
if (!e.shiftKey || e.key !== 'Enter' || e.isComposing) {
|
||||
return;
|
||||
}
|
||||
if (!commentForm) {
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
if (typeof commentForm.requestSubmit === 'function') {
|
||||
commentForm.requestSubmit();
|
||||
return;
|
||||
}
|
||||
commentForm.submit();
|
||||
});
|
||||
}
|
||||
|
||||
const isEditableTarget = (target) => {
|
||||
if (!(target instanceof HTMLElement)) {
|
||||
return false;
|
||||
}
|
||||
return target.isContentEditable || !!target.closest('input, textarea, select, [contenteditable="true"]');
|
||||
};
|
||||
|
||||
const enabledNavLink = (selector) => {
|
||||
const links = Array.from(document.querySelectorAll(selector));
|
||||
return links.find((link) => !link.classList.contains('disabled') && link.getAttribute('aria-disabled') !== 'true') || null;
|
||||
};
|
||||
|
||||
const prevPhotoLink = enabledNavLink('.js-prev-photo');
|
||||
const nextPhotoLink = enabledNavLink('.js-next-photo');
|
||||
const photoCards = Array.from(document.querySelectorAll('.js-photo-card'));
|
||||
const catalogLinks = Array.from(document.querySelectorAll('#sidebar .nav-link'));
|
||||
let hoveredCardIndex = -1;
|
||||
|
||||
photoCards.forEach((card, index) => {
|
||||
card.dataset.cardIndex = String(index);
|
||||
card.addEventListener('mouseenter', () => {
|
||||
hoveredCardIndex = index;
|
||||
});
|
||||
card.addEventListener('focus', () => {
|
||||
hoveredCardIndex = index;
|
||||
});
|
||||
});
|
||||
|
||||
const navigatePhotoCards = (direction) => {
|
||||
if (photoCards.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const focusedCard = document.activeElement instanceof HTMLElement
|
||||
? document.activeElement.closest('.js-photo-card')
|
||||
: null;
|
||||
let currentIndex = focusedCard ? Number(focusedCard.dataset.cardIndex || 0) : hoveredCardIndex;
|
||||
|
||||
if (!Number.isInteger(currentIndex) || currentIndex < 0 || currentIndex >= photoCards.length) {
|
||||
currentIndex = direction > 0 ? -1 : photoCards.length;
|
||||
}
|
||||
|
||||
const nextIndex = Math.max(0, Math.min(photoCards.length - 1, currentIndex + direction));
|
||||
if (nextIndex === currentIndex) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const targetCard = photoCards[nextIndex];
|
||||
if (!targetCard || !targetCard.href) {
|
||||
return false;
|
||||
}
|
||||
window.location.href = targetCard.href;
|
||||
return true;
|
||||
};
|
||||
|
||||
const navigateCatalog = (direction) => {
|
||||
if (catalogLinks.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let currentIndex = catalogLinks.findIndex((link) => link.classList.contains('active'));
|
||||
if (currentIndex < 0 && document.activeElement instanceof HTMLElement) {
|
||||
const focusedLink = document.activeElement.closest('#sidebar .nav-link');
|
||||
if (focusedLink) {
|
||||
currentIndex = catalogLinks.indexOf(focusedLink);
|
||||
}
|
||||
}
|
||||
if (currentIndex < 0) {
|
||||
currentIndex = direction > 0 ? -1 : catalogLinks.length;
|
||||
}
|
||||
|
||||
const nextIndex = Math.max(0, Math.min(catalogLinks.length - 1, currentIndex + direction));
|
||||
const link = catalogLinks[nextIndex];
|
||||
if (!link || !link.href) {
|
||||
return false;
|
||||
}
|
||||
if (nextIndex === currentIndex) {
|
||||
return true;
|
||||
}
|
||||
window.location.href = link.href;
|
||||
return true;
|
||||
};
|
||||
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.defaultPrevented || e.isComposing) {
|
||||
return;
|
||||
}
|
||||
|
||||
const target = e.target;
|
||||
if (isEditableTarget(target)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ((e.shiftKey || e.ctrlKey) && (e.key === 'ArrowUp' || e.key === 'ArrowDown')) {
|
||||
const direction = e.key === 'ArrowDown' ? 1 : -1;
|
||||
if (navigateCatalog(direction)) {
|
||||
e.preventDefault();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.key === 'ArrowLeft') {
|
||||
if (prevPhotoLink && prevPhotoLink.href) {
|
||||
e.preventDefault();
|
||||
window.location.href = prevPhotoLink.href;
|
||||
return;
|
||||
}
|
||||
if (navigatePhotoCards(-1)) {
|
||||
e.preventDefault();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.key === 'ArrowRight') {
|
||||
if (nextPhotoLink && nextPhotoLink.href) {
|
||||
e.preventDefault();
|
||||
window.location.href = nextPhotoLink.href;
|
||||
return;
|
||||
}
|
||||
if (navigatePhotoCards(1)) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user