Public: switch to CSS-only image placeholders

Replace JS-driven loading behavior with a pure CSS skeleton background so gallery cards remain fully native links. Drop click interception and deferred JS loaders to keep photo-page navigation immediate while images are still loading.
This commit is contained in:
Alexander Andreev 2026-02-20 19:10:24 +03:00
parent 0eefcf6b04
commit a0d7e0131d

View File

@ -143,13 +143,12 @@ function outputWatermarked(string $path, string $mime): never
.stack{display:grid;gap:12px;grid-template-columns:1fr} .stack{display:grid;gap:12px;grid-template-columns:1fr}
.cmt{border-top:1px solid #eee;padding:8px 0} .cmt{border-top:1px solid #eee;padding:8px 0}
.muted{color:#6b7280;font-size:13px} .muted{color:#6b7280;font-size:13px}
.img-box{position:relative;display:block;background:#f3f4f6} .img-box{position:relative;display:block;overflow:hidden;background:linear-gradient(110deg,#eef2f7 8%,#f8fafc 18%,#eef2f7 33%);background-size:200% 100%;animation:skeleton 1.2s linear infinite}
.img-box::before{content:'';position:absolute;left:50%;top:50%;width:28px;height:28px;margin:-14px 0 0 -14px;border:3px solid #cbd5e1;border-top-color:#1f6feb;border-radius:50%;opacity:0;pointer-events:none} .img-box img{display:block;position:relative;z-index:1}
.img-box.is-loading::before{opacity:1;animation:spin .75s linear infinite}
.img-box.is-loading img{opacity:.38}
.img-box img{transition:opacity .2s ease}
.thumb-img-box{height:130px} .thumb-img-box{height:130px}
@keyframes spin{to{transform:rotate(360deg)}} .detail .img-box{min-height:200px;border-radius:10px;border:1px solid #e5e7eb}
.detail .img-box img{max-width:100%;height:auto;border:0;border-radius:0}
@keyframes skeleton{to{background-position:-200% 0}}
.sidebar-head{display:flex;align-items:center;justify-content:space-between;gap:10px} .sidebar-head{display:flex;align-items:center;justify-content:space-between;gap:10px}
.sidebar-head h3{margin:0} .sidebar-head h3{margin:0}
.sidebar-toggle{display:none} .sidebar-toggle{display:none}
@ -207,8 +206,8 @@ function outputWatermarked(string $path, string $mime): never
<h2><?= h((string)$photo['code_name']) ?></h2> <h2><?= h((string)$photo['code_name']) ?></h2>
<p class="muted"><?= h((string)($photo['description'] ?? '')) ?></p> <p class="muted"><?= h((string)($photo['description'] ?? '')) ?></p>
<div class="stack"> <div class="stack">
<?php if (!empty($photo['before_file_id'])): ?><div><div class="muted">До обработки</div><div class="img-box is-loading"><img class="js-public-image" src="?action=image&file_id=<?= (int)$photo['before_file_id'] ?>" alt="" decoding="async" fetchpriority="high"></div></div><?php endif; ?> <?php if (!empty($photo['before_file_id'])): ?><div><div class="muted">До обработки</div><div class="img-box"><img src="?action=image&file_id=<?= (int)$photo['before_file_id'] ?>" alt="" decoding="async" fetchpriority="high"></div></div><?php endif; ?>
<?php if (!empty($photo['after_file_id'])): ?><div><div class="muted">После обработки (watermark)</div><div class="img-box is-loading"><img class="js-public-image" src="?action=image&file_id=<?= (int)$photo['after_file_id'] ?>" alt="" decoding="async" fetchpriority="high"></div></div><?php endif; ?> <?php if (!empty($photo['after_file_id'])): ?><div><div class="muted">После обработки (watermark)</div><div class="img-box"><img src="?action=image&file_id=<?= (int)$photo['after_file_id'] ?>" alt="" decoding="async" fetchpriority="high"></div></div><?php endif; ?>
</div> </div>
<h3 style="margin-top:16px">Комментарии</h3> <h3 style="margin-top:16px">Комментарии</h3>
@ -239,7 +238,7 @@ function outputWatermarked(string $path, string $mime): never
<div class="cards"> <div class="cards">
<?php foreach($photos as $p): ?> <?php foreach($photos as $p): ?>
<a class="card" href="?photo_id=<?= (int)$p['id'] ?>&section_id=<?= (int)$activeSectionId ?><?= $viewerToken!=='' ? '&viewer=' . urlencode($viewerToken) : '' ?>" style="text-decoration:none;color:inherit;position:relative"> <a class="card" href="?photo_id=<?= (int)$p['id'] ?>&section_id=<?= (int)$activeSectionId ?><?= $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 is-loading"><img class="js-public-image js-card-image" src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw==" data-src="?action=image&file_id=<?= (int)$p['before_file_id'] ?>" alt="" loading="lazy" decoding="async" fetchpriority="low"></div><?php endif; ?> <?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; ?>
<?php if (!empty($p['after_file_id'])): ?><span title="Есть обработанная версия" style="position:absolute;top:8px;right:8px;background:rgba(31,111,235,.92);color:#fff;font-size:11px;line-height:1;padding:6px 7px;border-radius:999px">AI</span><?php endif; ?> <?php if (!empty($p['after_file_id'])): ?><span title="Есть обработанная версия" style="position:absolute;top:8px;right:8px;background:rgba(31,111,235,.92);color:#fff;font-size:11px;line-height:1;padding:6px 7px;border-radius:999px">AI</span><?php endif; ?>
<div class="cap"><strong><?= h((string)$p['code_name']) ?></strong><br><span class="muted"><?= h((string)($p['description'] ?? '')) ?></span></div> <div class="cap"><strong><?= h((string)$p['code_name']) ?></strong><br><span class="muted"><?= h((string)($p['description'] ?? '')) ?></span></div>
</a> </a>
@ -257,73 +256,10 @@ function outputWatermarked(string $path, string $mime): never
</div> </div>
<script> <script>
(() => { (() => {
const loadCardImage = (img) => {
const src = img.dataset.src;
if (!src) {
return;
}
img.src = src;
img.removeAttribute('data-src');
};
const deferred = document.querySelectorAll('.js-card-image[data-src]');
if ('IntersectionObserver' in window) {
const io = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (!entry.isIntersecting) {
return;
}
loadCardImage(entry.target);
io.unobserve(entry.target);
});
}, { rootMargin: '240px 0px' });
deferred.forEach((img) => io.observe(img));
} else {
deferred.forEach((img) => loadCardImage(img));
}
document.querySelectorAll('img').forEach((img) => { document.querySelectorAll('img').forEach((img) => {
img.addEventListener('contextmenu', (e) => e.preventDefault()); img.addEventListener('contextmenu', (e) => e.preventDefault());
img.addEventListener('dragstart', (e) => e.preventDefault()); img.addEventListener('dragstart', (e) => e.preventDefault());
}); });
document.querySelectorAll('.js-public-image').forEach((img) => {
const box = img.closest('.img-box');
if (!box) {
return;
}
const clearLoading = () => {
box.classList.remove('is-loading');
};
img.addEventListener('load', clearLoading, { once: true });
img.addEventListener('error', clearLoading, { once: true });
if (img.complete && !img.dataset.src) {
clearLoading();
}
});
document.querySelectorAll('a.card').forEach((link) => {
link.addEventListener('click', (e) => {
if (e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {
return;
}
const href = link.getAttribute('href');
if (!href) {
return;
}
e.preventDefault();
window.stop();
window.location.assign(href);
});
});
})(); })();
(() => { (() => {