/** * AKSRT Enhancements — 全局 UI 增强 * * 包含:Toast 通知、图片灯箱、滚动方向感知 * * @package AKSRT */ (function () { 'use strict'; /* ============================================================ * 1. Toast 通知系统 * ============================================================ */ var toastContainer = null; function ensureToastContainer() { if (!toastContainer || !document.body.contains(toastContainer)) { toastContainer = document.createElement('div'); toastContainer.className = 'aksrt-toast-container'; document.body.appendChild(toastContainer); } return toastContainer; } /** * 显示 Toast 通知 * * @param {string} message 消息文本 * @param {string} type 类型: success, info, warning, danger/error, loading * @param {number} duration 显示时长(ms),0 表示不自动关闭 * @returns {Object} { dismiss: Function, update: Function } */ function showToast(message, type, duration) { type = type || 'success'; if (type === 'error') type = 'danger'; duration = duration !== undefined ? duration : 3500; if (duration > 0 && duration < 100) duration = duration * 1000; var container = ensureToastContainer(); var icons = { success: 'fas fa-check-circle', info: 'fas fa-bell', warning: 'fas fa-exclamation-circle', danger: 'fas fa-times-circle', loading: 'fas fa-spinner', }; var el = document.createElement('div'); el.className = 'aksrt-toast aksrt-toast--' + type; el.innerHTML = '' + '' + message + ''; container.appendChild(el); var timer = null; function dismiss() { if (timer) clearTimeout(timer); el.classList.add('is-dismissing'); setTimeout(function () { if (el.parentNode) el.remove(); }, 300); } function update(newMessage, newType) { if (newType) { el.className = 'aksrt-toast aksrt-toast--' + newType; var iconEl = el.querySelector('.aksrt-toast__icon i'); if (iconEl) iconEl.className = icons[newType] || icons.info; } var textEl = el.querySelector('.aksrt-toast__text'); if (textEl) textEl.textContent = newMessage; if (newType !== 'loading' && duration > 0) { if (timer) clearTimeout(timer); timer = setTimeout(dismiss, duration); } } el.addEventListener('click', dismiss); if (type !== 'loading' && duration > 0) { timer = setTimeout(dismiss, duration); } return { dismiss: dismiss, update: update }; } // 暴露到全局 window.AksrtToast = showToast; /* ============================================================ * 2. 图片灯箱 — zoom / thumbnails / slideshow / fullscreen / * mousewheel / origin animation / smart download * ============================================================ */ 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 EXCLUDE_SEL = '.no-imgbox,.no-lightbox,.avatar,.img-icon,.avatar-badge,.smilie-icon,.aksrt-avatar,.aksrt-lazy-skeleton'; var TRIGGER_SEL = '.entry-content img, .wp-posts-content img, .aksrt-lightbox-trigger img, [data-aksrt-lightbox] img, .imgbox-container img, .comment-content .box-img img, a[data-imgbox] img'; var CONTAINER_SEL = '.entry-content, .wp-posts-content, .aksrt-lightbox-trigger, [data-aksrt-lightbox], .imgbox-container, .comment-content'; var MAX_ZOOM_DESKTOP = 2; var MAX_ZOOM_MOBILE = 3; function lightboxFlag(key, fallback) { var w = window._win || (window.aksrtData && window.aksrtData.imgbox) || {}; if (w[key] === undefined || w[key] === null || w[key] === '') return fallback; return w[key] === true || w[key] === 1 || w[key] === '1'; } function lightboxEnabled() { return lightboxFlag('imgbox', true); } function resolveFullUrl(img) { var full = img.getAttribute('data-full-url') || img.getAttribute('data-src') || ''; if (full) return full; var src = img.src || ''; // thumbnail → original: strip -NNNxNNN suffix return src.replace(/-\d+x\d+(\.[a-z]+)$/i, '$1'); } function isNoLightbox(img) { if (img.closest(EXCLUDE_SEL)) return true; // parent anchor that does NOT point to an image var parentA = img.closest('a'); if (parentA) { var href = parentA.getAttribute('href') || ''; if (href && !/\.(jpe?g|png|gif|webp|avif|svg|bmp)(\?|$)/i.test(href) && !parentA.hasAttribute('data-imgbox')) { return true; } } return false; } function createLightbox() { if (!lightboxEnabled()) return; if (lightboxEl) return; lightboxEl = document.createElement('div'); lightboxEl.className = 'aksrt-lightbox'; 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'); // Apply window._win imgbox feature flags var zoomBtn = lightboxEl.querySelector('.toggle-zoom'); var thumbsBtn = lightboxEl.querySelector('.toggle-thumbs'); var playBtn = lightboxEl.querySelector('.toggle-play'); var fullBtn = lightboxEl.querySelector('.toggle-full'); var downBtn = lightboxEl.querySelector('.aksrt-lightbox__download'); if (zoomBtn) zoomBtn.hidden = !lightboxFlag('imgbox_zoom', true); if (thumbsBtn) thumbsBtn.hidden = !lightboxFlag('imgbox_thumbs', true); if (playBtn) playBtn.hidden = !lightboxFlag('imgbox_play', false); if (fullBtn) fullBtn.hidden = !lightboxFlag('imgbox_full', true); if (downBtn) downBtn.hidden = !lightboxFlag('imgbox_down', false); // Close lightboxEl.querySelector('.aksrt-lightbox__close').addEventListener('click', function (e) { e.stopPropagation(); closeLightbox(); }); lightboxEl.addEventListener('click', function (e) { if (e.target === lightboxEl || e.target.classList.contains('aksrt-lightbox__stage')) closeLightbox(); }); // Nav lightboxEl.querySelector('.aksrt-lightbox__nav--prev').addEventListener('click', function (e) { e.stopPropagation(); navigateLightbox(-1); }); lightboxEl.querySelector('.aksrt-lightbox__nav--next').addEventListener('click', function (e) { e.stopPropagation(); navigateLightbox(1); }); // Keyboard document.addEventListener('keydown', function (e) { if (!lightboxEl.classList.contains('is-open')) return; if (e.key === 'Escape') closeLightbox(); if (e.key === 'ArrowLeft') navigateLightbox(-1); if (e.key === 'ArrowRight') navigateLightbox(1); }); // Mousewheel nav lightboxEl.addEventListener('wheel', function (e) { if (!lightboxEl.classList.contains('is-open')) return; if (currentZoom > 1) return; // don't nav when zoomed e.preventDefault(); if (e.deltaY > 0) navigateLightbox(1); else navigateLightbox(-1); }, { passive: false }); // Image click → toggle zoom lightboxImg.addEventListener('click', function (e) { e.stopPropagation(); toggleZoom(); }); // Double-click → zoom in lightboxImg.addEventListener('dblclick', function (e) { e.stopPropagation(); var maxZoom = window.innerWidth < 768 ? MAX_ZOOM_MOBILE : MAX_ZOOM_DESKTOP; if (currentZoom < maxZoom) setZoom(maxZoom); else setZoom(1); }); // Pinch zoom (mobile) var pinchStart = 0; lightboxEl.addEventListener('touchstart', function (e) { if (e.touches.length === 2) { pinchStart = Math.hypot( e.touches[1].clientX - e.touches[0].clientX, e.touches[1].clientY - e.touches[0].clientY ); } }, { passive: true }); lightboxEl.addEventListener('touchmove', function (e) { if (e.touches.length === 2 && pinchStart > 0) { var dist = Math.hypot( e.touches[1].clientX - e.touches[0].clientX, e.touches[1].clientY - e.touches[0].clientY ); var maxZoom = window.innerWidth < 768 ? MAX_ZOOM_MOBILE : MAX_ZOOM_DESKTOP; var newZoom = Math.min(maxZoom, Math.max(1, currentZoom * (dist / pinchStart))); setZoom(newZoom); pinchStart = dist; } }, { passive: true }); lightboxEl.addEventListener('touchend', function () { pinchStart = 0; }, { passive: true }); // Touch swipe/pull-dismiss (skip when zoomed) var touchStartY = 0, touchStartX = 0, isSwiping = false; lightboxEl.addEventListener('touchstart', function (e) { if (e.touches.length !== 1 || currentZoom > 1.05) return; touchStartY = e.touches[0].clientY; touchStartX = e.touches[0].clientX; isSwiping = false; }, { passive: true }); lightboxEl.addEventListener('touchmove', function (e) { if (e.touches.length !== 1 || currentZoom > 1.05) return; var dy = e.touches[0].clientY - touchStartY; var dx = e.touches[0].clientX - touchStartX; if (Math.abs(dy) > Math.abs(dx) && dy > 30) { isSwiping = true; var opacity = Math.max(0.2, (150 - dy) / 80); lightboxImg.style.transform = 'translate3d(0, ' + dy + 'px, 0) scale(' + currentZoom + ')'; lightboxImg.style.opacity = String(Math.min(1, opacity)); } }, { passive: true }); lightboxEl.addEventListener('touchend', function () { if (isSwiping) { closeLightbox(); lightboxImg.style.transform = ''; lightboxImg.style.opacity = ''; } isSwiping = false; }, { passive: true }); // Download lightboxEl.querySelector('.aksrt-lightbox__download').addEventListener('click', function (e) { e.stopPropagation(); var item = currentGallery[currentIndex]; if (!item || !item.src) return; var a = document.createElement('a'); var fullSrc = item.fullSrc || item.src; a.href = fullSrc; a.download = fullSrc.substring(fullSrc.lastIndexOf('/') + 1) || 'image'; document.body.appendChild(a); a.click(); a.remove(); }); // Zoom button lightboxEl.querySelector('.toggle-zoom').addEventListener('click', function (e) { e.stopPropagation(); toggleZoom(); }); // Thumbnails toggle lightboxEl.querySelector('.toggle-thumbs').addEventListener('click', function (e) { e.stopPropagation(); lightboxThumbsWrap.hidden = !lightboxThumbsWrap.hidden; }); // Slideshow toggle lightboxEl.querySelector('.toggle-play').addEventListener('click', function (e) { e.stopPropagation(); toggleAutoplay(); }); // Fullscreen lightboxEl.querySelector('.toggle-full').addEventListener('click', function (e) { e.stopPropagation(); toggleFullscreen(); }); document.addEventListener('fullscreenchange', function () { var icon = lightboxEl.querySelector('.toggle-full i'); if (document.fullscreenElement) { lightboxEl.classList.add('full-screen'); icon.className = 'fas fa-compress'; } else { lightboxEl.classList.remove('full-screen'); icon.className = 'fas fa-expand'; } }); } function setZoom(z) { currentZoom = z; lightboxImg.style.transform = 'scale(' + z + ')'; lightboxEl.classList.toggle('is-zoomed', z > 1); } function toggleZoom() { var maxZoom = window.innerWidth < 768 ? MAX_ZOOM_MOBILE : MAX_ZOOM_DESKTOP; setZoom(currentZoom > 1 ? 1 : maxZoom); } function toggleAutoplay() { var icon = lightboxEl.querySelector('.toggle-play i'); if (isAutoplay) { clearInterval(autoplayTimer); isAutoplay = false; icon.className = 'fas fa-play'; } else { if (currentGallery.length < 2) return; isAutoplay = true; icon.className = 'fas fa-pause'; autoplayTimer = setInterval(function () { navigateLightbox(1); }, 3000); } } function toggleFullscreen() { var rfs = lightboxEl.requestFullscreen || lightboxEl.webkitRequestFullscreen || lightboxEl.msRequestFullscreen; if (document.fullscreenElement || document.webkitFullscreenElement) { (document.exitFullscreen || document.webkitExitFullscreen || document.msExitFullscreen).call(document); } else if (rfs) { rfs.call(lightboxEl); } } function buildThumbnails() { if (!lightboxThumbsWrap) return; lightboxThumbsWrap.innerHTML = ''; currentGallery.forEach(function (item, i) { var thumb = document.createElement('img'); thumb.src = item.src; thumb.className = 'aksrt-lightbox__thumb' + (i === currentIndex ? ' is-active' : ''); thumb.addEventListener('click', function (e) { e.stopPropagation(); showLightboxImage(i); }); lightboxThumbsWrap.appendChild(thumb); }); } function syncThumbActive() { if (!lightboxThumbsWrap) return; var thumbs = lightboxThumbsWrap.querySelectorAll('.aksrt-lightbox__thumb'); thumbs.forEach(function (t, i) { t.classList.toggle('is-active', i === currentIndex); }); var active = thumbs[currentIndex]; if (active) active.scrollIntoView({ inline: 'center', block: 'nearest', behavior: 'smooth' }); } function openLightbox(src, caption, gallery, index, originEl) { createLightbox(); currentGallery = gallery || [{ src: src, caption: caption || '' }]; currentIndex = index || 0; currentZoom = 1; // Origin animation if (originEl) { 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'); } showLightboxImage(currentIndex); buildThumbnails(); lightboxEl.classList.add('is-open'); document.body.style.overflow = 'hidden'; lightboxEl.dispatchEvent(new CustomEvent('aksrt:lightbox-open', { bubbles: true })); } function showLightboxImage(index) { 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.style.display = item.caption ? '' : 'none'; if (currentGallery.length > 1) { lightboxCounter.textContent = (currentIndex + 1) + ' / ' + currentGallery.length; lightboxCounter.style.display = ''; } else { lightboxCounter.style.display = 'none'; } syncThumbActive(); } function navigateLightbox(dir) { showLightboxImage(currentIndex + dir); } function closeLightbox() { if (!lightboxEl) return; if (isAutoplay) toggleAutoplay(); lightboxEl.classList.remove('is-open', 'is-zoomed', 'from-origin'); document.body.style.overflow = ''; lightboxThumbsWrap.hidden = true; if (document.fullscreenElement) { (document.exitFullscreen || document.webkitExitFullscreen || document.msExitFullscreen).call(document); } } // Auto-bind: expanded trigger scope + exclusion rules function initLightbox() { document.addEventListener('click', function (e) { // Also support a[data-imgbox] var anchor = e.target.closest('a[data-imgbox]'); if (anchor) { e.preventDefault(); var href = anchor.getAttribute('href') || anchor.getAttribute('data-imgbox'); openLightbox(href, '', [{ src: href, fullSrc: href, caption: '' }], 0, anchor); return; } var img = e.target.closest(TRIGGER_SEL); if (!img) return; if (img.width < 100 || img.height < 100) return; if (!lightboxEnabled() || isNoLightbox(img)) return; e.preventDefault(); var container = img.closest(CONTAINER_SEL); if (!container) return; var allImages = container.querySelectorAll('img'); var gallery = []; var foundIndex = 0; allImages.forEach(function (el) { if (el.width < 100 || el.height < 100) return; if (isNoLightbox(el)) return; var full = resolveFullUrl(el); var caption = el.alt || ''; var figcaption = el.closest('figure') ? el.closest('figure').querySelector('figcaption') : null; if (figcaption) caption = figcaption.textContent; gallery.push({ src: el.dataset.src || el.src, fullSrc: full, caption: caption }); if (el === img) foundIndex = gallery.length - 1; }); if (gallery.length > 0) { openLightbox(img.dataset.src || img.src, img.alt, gallery, foundIndex, img); } }); } // 暴露到全局 window.AksrtLightbox = { open: openLightbox, close: closeLightbox, }; /* ============================================================ * 3. 滚动方向感知 * ============================================================ */ // Scroll direction is now handled by aksrt-runtime.js body scroll bus // (body.scroll-down / body.scroll-up / body.body-scroll). // This stub syncs the legacy aksrt-* prefixed classes for backward compat. 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); } function onScroll() { requestAnimationFrame(sync); } window.addEventListener('scroll', onScroll, { passive: true }); } /* ============================================================ * 4. 导航溢出折叠 * 由 assets/js/core/nav.js(zibll 1:1 navFolding)负责。 * 此处不再重复实现,避免两套逻辑争抢 .aksrt-nav-more 节点 * 触发 NotFoundError: insertBefore (moreLi 已被对方 remove)。 * ============================================================ */ /* ============================================================ * 初始化入口 * ============================================================ */ document.addEventListener('DOMContentLoaded', function () { initLightbox(); initScrollDirection(); }); })();