/** * AKSRT Enhancements — 全局 UI 增强 * * 包含:图片灯箱、滚动方向感知 * * @package AKSRT */ (function () { 'use strict'; var lightboxEl = null; var lightboxImg = null; var lightboxCaption = null; var lightboxCounter = null; var lightboxThumbsWrap = null; var currentGallery = []; var currentIndex = 0; var currentZoom = 1; var autoplayTimer = null; var isAutoplay = false; var lastTrigger = null; var previousBodyOverflow = ''; var EXCLUDE_SEL = '.no-imgbox,.no-lightbox,.avatar,.img-icon,.avatar-badge,.aksrt-avatar,[data-no-lightbox],[data-aksrt-lightbox="0"]'; var TRIGGER_SEL = '[data-aksrt-lightbox="1"] img,.aksrt-lightbox-trigger img,.imgbox-container img,.comment-content .box-img'; var ANCHOR_TRIGGER_SEL = 'a[data-aksrt-lightbox-trigger]'; var GROUP_SEL = '[data-aksrt-lightbox-group],.content-gallery[data-aksrt-lightbox="1"],.aksrt-carousel-block[data-aksrt-lightbox="1"],.imgbox-container,.comment-content,.post_front_content[data-aksrt-lightbox="1"],.wp-posts-content[data-aksrt-lightbox="1"],.entry-thumb[data-aksrt-lightbox="1"],.entry-content[data-aksrt-lightbox="1"],.aksrt-lightbox-trigger,[data-aksrt-lightbox="1"]'; var IMAGE_URL_RE = /\.(?:avif|bmp|gif|ico|jpe?g|png|svg|tiff?|webp)(?:[?#].*)?$/i; var MAX_ZOOM_DESKTOP = 2; var MAX_ZOOM_MOBILE = 3; function lightboxFlag(key, fallback) { var config = window._win || {}; if (config[key] === undefined || config[key] === null || config[key] === '') return fallback; return config[key] === true || config[key] === 1 || config[key] === '1'; } function lightboxEnabled() { return lightboxFlag('imgbox', false); } function normalizeFullUrl(url) { if (!url || url.indexOf('data:') === 0) return url || ''; return url .replace(/-\d+x\d+(\.[a-z0-9]+)(?=([?#]|$))/i, '$1') .replace(/([?&])x-oss-process=[^#&]*&?/i, '$1') .replace(/[?&]$/, ''); } function resolveFullUrl(img) { var explicitFull = img.getAttribute('data-full-url') || ''; if (explicitFull) return normalizeFullUrl(explicitFull); var anchor = img.closest('a[href]'); var href = anchor ? anchor.getAttribute('href') || '' : ''; if (IMAGE_URL_RE.test(href)) return normalizeFullUrl(href); return normalizeFullUrl( img.getAttribute('data-src') || img.getAttribute('data-lazy-src') || img.currentSrc || img.getAttribute('src') || '' ); } function resolveThumbUrl(img) { return img.currentSrc || img.getAttribute('src') || img.getAttribute('data-src') || img.getAttribute('data-lazy-src') || img.getAttribute('data-full-url') || ''; } function resolveCaption(img) { var anchor = img.closest('a'); var explicit = anchor ? anchor.getAttribute('data-caption') || '' : ''; if (explicit.trim()) return explicit.trim(); var figure = img.closest('figure'); var figcaption = figure ? figure.querySelector('figcaption') : null; if (figcaption && figcaption.textContent.trim()) return figcaption.textContent.trim(); return (img.getAttribute('alt') || '').trim(); } function isNoLightbox(img) { if (!img || img.closest(EXCLUDE_SEL)) return true; var parentA = img.closest('a[href]'); if (parentA) { var href = parentA.getAttribute('href') || ''; if (href && !IMAGE_URL_RE.test(href) && !parentA.hasAttribute('data-aksrt-lightbox-trigger')) { return true; } } return resolveFullUrl(img) === ''; } function createLightbox() { if (!lightboxEnabled()) return null; if (lightboxEl) return lightboxEl; lightboxEl = document.createElement('div'); lightboxEl.className = 'aksrt-lightbox'; lightboxEl.setAttribute('role', 'dialog'); lightboxEl.setAttribute('aria-modal', 'true'); lightboxEl.setAttribute('aria-hidden', 'true'); lightboxEl.setAttribute('aria-label', '图片预览'); lightboxEl.setAttribute('tabindex', '-1'); lightboxEl.innerHTML = '
' + '' + '' + '' + '' + '' + '' + '' + '
' + '' + '
' + '' + '
' + ''; document.body.appendChild(lightboxEl); lightboxImg = lightboxEl.querySelector('.aksrt-lightbox__img'); lightboxCaption = lightboxEl.querySelector('.aksrt-lightbox__caption'); lightboxCounter = lightboxEl.querySelector('.aksrt-lightbox__counter'); lightboxThumbsWrap = lightboxEl.querySelector('.aksrt-lightbox__thumbs'); lightboxEl.querySelector('.aksrt-lightbox__close').addEventListener('click', function (event) { event.stopPropagation(); closeLightbox(); }); lightboxEl.addEventListener('click', function (event) { if (event.target === lightboxEl || event.target.classList.contains('aksrt-lightbox__stage')) closeLightbox(); }); lightboxEl.querySelector('.aksrt-lightbox__nav--prev').addEventListener('click', function (event) { event.stopPropagation(); navigateLightbox(-1); }); lightboxEl.querySelector('.aksrt-lightbox__nav--next').addEventListener('click', function (event) { event.stopPropagation(); navigateLightbox(1); }); document.addEventListener('keydown', function (event) { if (!lightboxEl || !lightboxEl.classList.contains('is-open')) return; if (event.key === 'Escape') closeLightbox(); if (event.key === 'ArrowLeft') navigateLightbox(-1); if (event.key === 'ArrowRight') navigateLightbox(1); if (event.key === 'Tab') trapFocus(event); }); lightboxEl.addEventListener('wheel', function (event) { if (!lightboxEl.classList.contains('is-open') || currentZoom > 1 || currentGallery.length < 2) return; event.preventDefault(); navigateLightbox(event.deltaY > 0 ? 1 : -1); }, { passive: false }); lightboxImg.addEventListener('click', function (event) { event.stopPropagation(); if (lightboxFlag('imgbox_zoom', true)) toggleZoom(); }); lightboxImg.addEventListener('dblclick', function (event) { if (!lightboxFlag('imgbox_zoom', true)) return; event.stopPropagation(); var maxZoom = window.innerWidth < 768 ? MAX_ZOOM_MOBILE : MAX_ZOOM_DESKTOP; setZoom(currentZoom < maxZoom ? maxZoom : 1); }); bindTouchGestures(); lightboxEl.querySelector('.toggle-zoom').addEventListener('click', function (event) { event.stopPropagation(); toggleZoom(); }); lightboxEl.querySelector('.toggle-thumbs').addEventListener('click', function (event) { event.stopPropagation(); lightboxThumbsWrap.hidden = !lightboxThumbsWrap.hidden; }); lightboxEl.querySelector('.toggle-play').addEventListener('click', function (event) { event.stopPropagation(); toggleAutoplay(); }); lightboxEl.querySelector('.toggle-full').addEventListener('click', function (event) { event.stopPropagation(); toggleFullscreen(); }); document.addEventListener('fullscreenchange', syncFullscreenState); document.addEventListener('webkitfullscreenchange', syncFullscreenState); return lightboxEl; } function bindTouchGestures() { var pinchStart = 0; var touchStartY = 0; var touchStartX = 0; var pullDistance = 0; lightboxEl.addEventListener('touchstart', function (event) { if (event.touches.length === 2 && lightboxFlag('imgbox_zoom', true)) { pinchStart = Math.hypot( event.touches[1].clientX - event.touches[0].clientX, event.touches[1].clientY - event.touches[0].clientY ); return; } if (event.touches.length === 1 && currentZoom <= 1.05) { touchStartY = event.touches[0].clientY; touchStartX = event.touches[0].clientX; pullDistance = 0; } }, { passive: true }); lightboxEl.addEventListener('touchmove', function (event) { if (event.touches.length === 2 && pinchStart > 0 && lightboxFlag('imgbox_zoom', true)) { var distance = Math.hypot( event.touches[1].clientX - event.touches[0].clientX, event.touches[1].clientY - event.touches[0].clientY ); var maxZoom = window.innerWidth < 768 ? MAX_ZOOM_MOBILE : MAX_ZOOM_DESKTOP; setZoom(Math.min(maxZoom, Math.max(1, currentZoom * (distance / pinchStart)))); pinchStart = distance; return; } if (event.touches.length !== 1 || currentZoom > 1.05) return; var dy = event.touches[0].clientY - touchStartY; var dx = event.touches[0].clientX - touchStartX; if (Math.abs(dy) > Math.abs(dx) && dy > 30) { pullDistance = dy; lightboxImg.style.transform = 'translate3d(0, ' + dy + 'px, 0) scale(' + currentZoom + ')'; lightboxImg.style.opacity = String(Math.max(0.2, Math.min(1, (150 - dy) / 80))); } }, { passive: true }); lightboxEl.addEventListener('touchend', function () { pinchStart = 0; if (pullDistance > 100) { closeLightbox(); } else if (pullDistance > 0) { setZoom(1); } lightboxImg.style.opacity = ''; pullDistance = 0; }, { passive: true }); } function trapFocus(event) { var focusable = Array.prototype.filter.call( lightboxEl.querySelectorAll('a[href],button:not([hidden]):not([disabled]),[tabindex]:not([tabindex="-1"])'), function (element) { return element.offsetParent !== null; } ); if (!focusable.length) { event.preventDefault(); lightboxEl.focus(); return; } var first = focusable[0]; var last = focusable[focusable.length - 1]; if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last.focus(); } else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first.focus(); } } function setZoom(zoom) { if (!lightboxImg || !lightboxEl) return; currentZoom = zoom; lightboxImg.style.transform = 'scale(' + zoom + ')'; lightboxEl.classList.toggle('is-zoomed', zoom > 1); } function toggleZoom() { if (!lightboxFlag('imgbox_zoom', true)) return; var maxZoom = window.innerWidth < 768 ? MAX_ZOOM_MOBILE : MAX_ZOOM_DESKTOP; setZoom(currentZoom > 1 ? 1 : maxZoom); } function toggleAutoplay() { if (!lightboxEl) return; var icon = lightboxEl.querySelector('.toggle-play i'); if (isAutoplay) { clearInterval(autoplayTimer); autoplayTimer = null; isAutoplay = false; icon.className = 'fa fas fa-play'; return; } if (currentGallery.length < 2) return; isAutoplay = true; icon.className = 'fa fas fa-pause'; autoplayTimer = setInterval(function () { navigateLightbox(1); }, 3000); } function toggleFullscreen() { if (!lightboxEl || !lightboxFlag('imgbox_full', true)) return; var fullscreenElement = document.fullscreenElement || document.webkitFullscreenElement; if (fullscreenElement) { var exit = document.exitFullscreen || document.webkitExitFullscreen; if (exit) exit.call(document); return; } var request = lightboxEl.requestFullscreen || lightboxEl.webkitRequestFullscreen; if (request) request.call(lightboxEl); } function syncFullscreenState() { if (!lightboxEl) return; var active = Boolean(document.fullscreenElement || document.webkitFullscreenElement); var icon = lightboxEl.querySelector('.toggle-full i'); lightboxEl.classList.toggle('full-screen', active); icon.className = active ? 'fa fas fa-compress' : 'fa fas fa-expand'; } function buildThumbnails() { if (!lightboxThumbsWrap) return; lightboxThumbsWrap.innerHTML = ''; currentGallery.forEach(function (item, index) { var button = document.createElement('button'); var thumb = document.createElement('img'); button.type = 'button'; button.className = 'aksrt-lightbox__thumb' + (index === currentIndex ? ' is-active' : ''); button.setAttribute('aria-label', '查看第 ' + (index + 1) + ' 张图片'); thumb.src = item.thumbSrc || item.src; thumb.alt = ''; button.appendChild(thumb); button.addEventListener('click', function (event) { event.stopPropagation(); showLightboxImage(index); }); lightboxThumbsWrap.appendChild(button); }); } function syncThumbActive() { if (!lightboxThumbsWrap) return; var thumbs = lightboxThumbsWrap.querySelectorAll('.aksrt-lightbox__thumb'); thumbs.forEach(function (thumb, index) { thumb.classList.toggle('is-active', index === currentIndex); }); var active = thumbs[currentIndex]; if (active) active.scrollIntoView({ inline: 'center', block: 'nearest' }); } function syncControls() { if (!lightboxEl) return; var multiple = currentGallery.length > 1; var fullscreenAvailable = Boolean( lightboxEl.requestFullscreen || lightboxEl.webkitRequestFullscreen || document.fullscreenEnabled || document.webkitFullscreenEnabled ); var thumbsEnabled = multiple && lightboxFlag('imgbox_thumbs', true); lightboxEl.querySelector('.toggle-zoom').hidden = !lightboxFlag('imgbox_zoom', true); lightboxEl.querySelector('.toggle-thumbs').hidden = !thumbsEnabled; lightboxEl.querySelector('.toggle-play').hidden = !multiple || !lightboxFlag('imgbox_play', false); lightboxEl.querySelector('.toggle-full').hidden = !lightboxFlag('imgbox_full', true) || !fullscreenAvailable; lightboxEl.querySelector('.aksrt-lightbox__download').hidden = !lightboxFlag('imgbox_down', false); lightboxEl.querySelector('.aksrt-lightbox__nav--prev').hidden = !multiple; lightboxEl.querySelector('.aksrt-lightbox__nav--next').hidden = !multiple; lightboxThumbsWrap.hidden = !thumbsEnabled; } function openLightbox(src, caption, gallery, index, originEl) { if (!lightboxEnabled() || !createLightbox()) return false; currentGallery = gallery && gallery.length ? gallery : [{ src: src, fullSrc: src, thumbSrc: src, caption: caption || '' }]; currentIndex = Number.isInteger(index) ? index : 0; currentZoom = 1; lastTrigger = originEl || document.activeElement; if (originEl && typeof originEl.getBoundingClientRect === 'function') { var rect = originEl.getBoundingClientRect(); lightboxEl.style.setProperty('--imgbox-origin-top', (rect.top + rect.height / 2) + 'px'); lightboxEl.style.setProperty('--imgbox-origin-left', (rect.left + rect.width / 2) + 'px'); lightboxEl.classList.add('from-origin'); } else { lightboxEl.classList.remove('from-origin'); } buildThumbnails(); syncControls(); showLightboxImage(currentIndex); lightboxEl.classList.add('is-open'); lightboxEl.setAttribute('aria-hidden', 'false'); previousBodyOverflow = document.body.style.overflow; document.body.style.overflow = 'hidden'; document.body.classList.add('aksrt-lightbox-open'); lightboxEl.querySelector('.aksrt-lightbox__close').focus(); lightboxEl.dispatchEvent(new CustomEvent('aksrt:lightbox-open', { bubbles: true })); return true; } function showLightboxImage(index) { if (!currentGallery.length || !lightboxImg) return; if (index < 0) index = currentGallery.length - 1; if (index >= currentGallery.length) index = 0; currentIndex = index; setZoom(1); var item = currentGallery[currentIndex]; lightboxImg.src = item.fullSrc || item.src; lightboxImg.alt = item.caption || ''; lightboxCaption.textContent = item.caption || ''; lightboxCaption.hidden = !item.caption; if (currentGallery.length > 1) { lightboxCounter.textContent = (currentIndex + 1) + ' / ' + currentGallery.length; lightboxCounter.hidden = false; } else { lightboxCounter.textContent = ''; lightboxCounter.hidden = true; } var download = lightboxEl.querySelector('.aksrt-lightbox__download'); var fullSrc = item.fullSrc || item.src; download.href = fullSrc; download.download = fullSrc.substring(fullSrc.lastIndexOf('/') + 1).split(/[?#]/)[0] || 'image'; syncThumbActive(); } function navigateLightbox(direction) { if (currentGallery.length < 2) return; showLightboxImage(currentIndex + direction); } function closeLightbox() { if (!lightboxEl || !lightboxEl.classList.contains('is-open')) return; if (isAutoplay) toggleAutoplay(); lightboxEl.classList.remove('is-open', 'is-zoomed', 'from-origin'); lightboxEl.setAttribute('aria-hidden', 'true'); document.body.style.overflow = previousBodyOverflow; document.body.classList.remove('aksrt-lightbox-open'); lightboxThumbsWrap.hidden = true; lightboxImg.src = ''; lightboxImg.alt = ''; var fullscreenElement = document.fullscreenElement || document.webkitFullscreenElement; var exit = document.exitFullscreen || document.webkitExitFullscreen; if (fullscreenElement && exit) exit.call(document); if (lastTrigger && typeof lastTrigger.focus === 'function' && document.contains(lastTrigger)) { lastTrigger.focus({ preventScroll: true }); } lastTrigger = null; } function buildGallery(img) { var container = img.closest(GROUP_SEL); if (!container || container.closest('[data-no-lightbox],[data-aksrt-lightbox="0"]')) return null; var gallery = []; var foundIndex = -1; container.querySelectorAll('img').forEach(function (candidate) { if (!candidate.matches(TRIGGER_SEL) || isNoLightbox(candidate)) return; var fullSrc = resolveFullUrl(candidate); var thumbSrc = resolveThumbUrl(candidate) || fullSrc; if (!fullSrc) return; if (candidate === img) foundIndex = gallery.length; gallery.push({ src: thumbSrc, thumbSrc: thumbSrc, fullSrc: fullSrc, caption: resolveCaption(candidate), }); }); if (foundIndex < 0 || !gallery.length) return null; return { items: gallery, index: foundIndex }; } function openImage(img) { if (isNoLightbox(img)) return false; var gallery = buildGallery(img); if (!gallery) return false; var item = gallery.items[gallery.index]; return openLightbox(item.fullSrc, item.caption, gallery.items, gallery.index, img); } function initLightbox() { document.addEventListener('click', function (event) { if (!lightboxEnabled() || !(event.target instanceof Element)) return; var anchor = event.target.closest(ANCHOR_TRIGGER_SEL); if (anchor && !anchor.closest(EXCLUDE_SEL)) { var anchorImage = anchor.querySelector('img'); if (anchorImage && openImage(anchorImage)) { event.preventDefault(); return; } var href = anchor.getAttribute('href') || ''; if (IMAGE_URL_RE.test(href) && openLightbox(href, anchor.getAttribute('data-caption') || '', null, 0, anchor)) { event.preventDefault(); } return; } var img = event.target.closest(TRIGGER_SEL); if (img && openImage(img)) event.preventDefault(); }); } window.AksrtLightbox = { open: openLightbox, close: closeLightbox, }; // Scroll direction is handled by aksrt-runtime.js. Keep the prefixed // classes in sync for the remaining CSS consumers. function initScrollDirection() { var body = document.body; function sync() { body.classList.toggle('aksrt-body-scroll', body.classList.contains('body-scroll')); body.classList.toggle('aksrt-scroll-down', body.classList.contains('scroll-down')); body.classList.toggle('aksrt-scroll-up', body.classList.contains('scroll-up')); body.classList.add('aksrt-scrolling'); clearTimeout(body._aksrtScrollTimer); body._aksrtScrollTimer = setTimeout(function () { body.classList.remove('aksrt-scrolling'); }, 300); } window.addEventListener('scroll', function () { requestAnimationFrame(sync); }, { passive: true }); } document.addEventListener('DOMContentLoaded', function () { initLightbox(); initScrollDirection(); }); })();