diff --git a/themes/Ying/assets/css/main.css b/themes/Ying/assets/css/main.css index 6e11c663..0b43384d 100644 --- a/themes/Ying/assets/css/main.css +++ b/themes/Ying/assets/css/main.css @@ -4776,7 +4776,7 @@ html[data-theme="light"] .atk-loading-spinner svg circle { left: 0; width: 100%; height: 100%; - background: rgba(0,0,0,0.6); + /* background: rgba(0,0,0,0.6); */ z-index: 9999; display: flex; justify-content: center; diff --git a/themes/Ying/assets/js/modules/floating-tools.js b/themes/Ying/assets/js/modules/floating-tools.js index 49bae919..42f0039c 100644 --- a/themes/Ying/assets/js/modules/floating-tools.js +++ b/themes/Ying/assets/js/modules/floating-tools.js @@ -33,41 +33,57 @@ window.initFloatingTools = function() { } } - // 1. Back to Top + // 1. Back to Top (Optimized with IntersectionObserver) var backToTopButton = document.getElementById('back-to-top'); if (backToTopButton) { - var ticking = false; - - function update() { - var btn = document.getElementById('back-to-top'); - if (!btn) return; - - // Only show if NOT on homepage (redundant if container is hidden, but good for safety) - if (window.scrollY > 300) { - if (!btn.classList.contains('show')) { - btn.classList.add('show'); - } - } else { - if (btn.classList.contains('show')) { - btn.classList.remove('show'); - } - } - ticking = false; + + // Clean up old scroll handler if exists (from previous Pjax or version) + if (window.backToTopScrollHandler) { + window.removeEventListener('scroll', window.backToTopScrollHandler); + window.backToTopScrollHandler = null; } - // Global Scroll Handler (Singleton) - if (!window.backToTopScrollHandler) { + // Create or reuse sentinel for IntersectionObserver + var sentinelId = 'back-to-top-sentinel'; + var sentinel = document.getElementById(sentinelId); + if (!sentinel) { + sentinel = document.createElement('div'); + sentinel.id = sentinelId; + // Sentinel covers the top 300px. When it exits viewport, we show button. + sentinel.style.cssText = 'position: absolute; top: 0; left: 0; width: 1px; height: 300px; pointer-events: none; visibility: hidden; z-index: -9999;'; + document.body.appendChild(sentinel); + } + + if (window.IntersectionObserver) { + var observer = new IntersectionObserver(function(entries) { + entries.forEach(function(entry) { + if (!entry.isIntersecting) { + // Sentinel is out of view (scrolled down past 300px) + backToTopButton.classList.add('show'); + } else { + // Sentinel is in view (near top) + backToTopButton.classList.remove('show'); + } + }); + }); + observer.observe(sentinel); + } else { + // Fallback for very old browsers + var ticking = false; + var update = function() { + if (window.scrollY > 300) { + backToTopButton.classList.add('show'); + } else { + backToTopButton.classList.remove('show'); + } + ticking = false; + }; window.backToTopScrollHandler = function() { - // Performance Optimization: Check scroll less frequently? - // No, rAF is best. But we can add a check if the button is even in DOM/Visible? - // But checking DOM layout is expensive. - // Keep rAF but ensure 'update' is light. if (!ticking) { window.requestAnimationFrame(update); ticking = true; } }; - // Use passive listener to prevent scroll blocking window.addEventListener('scroll', window.backToTopScrollHandler, { passive: true }); } @@ -78,9 +94,6 @@ window.initFloatingTools = function() { backToTopButton = newBtn; } - // Initialize state immediately - update(); - backToTopButton.addEventListener('click', function(e) { e.preventDefault(); window.scrollTo({ diff --git a/themes/Ying/assets/js/modules/lazyload.js b/themes/Ying/assets/js/modules/lazyload.js index 6f1e9984..d8b48ef1 100644 --- a/themes/Ying/assets/js/modules/lazyload.js +++ b/themes/Ying/assets/js/modules/lazyload.js @@ -36,8 +36,8 @@ if (entry.isIntersecting) { var img = entry.target; - // Add to queue instead of loading immediately - addToQueue(img); + // Load immediately without queue + loadImage(img); // Stop observing this image once it's triggered self.unobserve(img); @@ -51,29 +51,7 @@ observeImages(); } - function addToQueue(img) { - // Avoid duplicates - if (!imageQueue.includes(img)) { - imageQueue.push(img); - processQueue(); - } - } - - function processQueue() { - if (isProcessingQueue) return; - if (imageQueue.length === 0) return; - - isProcessingQueue = true; - - var img = imageQueue.shift(); - loadImage(img, function() { - // After this image starts showing (or fails), process next after delay - setTimeout(function() { - isProcessingQueue = false; - processQueue(); - }, defaults.queueInterval); - }); - } + // Removed addToQueue and processQueue functions as we want instant loading function observeImages() { var images = document.querySelectorAll('img.lazyload, .post-content img:not(.lazy-loaded)'); diff --git a/themes/Ying/assets/js/modules/reward.js b/themes/Ying/assets/js/modules/reward.js index 2b9a6642..af8dce92 100644 --- a/themes/Ying/assets/js/modules/reward.js +++ b/themes/Ying/assets/js/modules/reward.js @@ -9,6 +9,24 @@ if (!btn || !modal) return; + // Global Click Handler + // We attach this to document only when modal is open to handle "click anywhere outside content" + // This is more robust than attaching to the modal container, which might have size/z-index issues. + + const handleGlobalClick = (e) => { + const activeModal = document.getElementById('reward-modal'); + if (!activeModal || !activeModal.classList.contains('show')) return; + + // If click is inside the content, ignore + if (e.target.closest('.reward-content')) return; + + // If click is on the open button, ignore (it has its own handler, or we might double-toggle) + if (e.target.closest('#reward-btn')) return; + + // Otherwise, close it + hideModal(); + }; + // Helper to show modal const showModal = (e) => { if(e) { @@ -17,12 +35,18 @@ } modal.classList.add('show'); document.body.style.overflow = 'hidden'; // Prevent scrolling + + // Attach global click listener with a slight delay to avoid current click triggering it + setTimeout(() => { + document.addEventListener('click', handleGlobalClick); + }, 10); }; // Helper to hide modal const hideModal = () => { modal.classList.remove('show'); document.body.style.overflow = ''; + document.removeEventListener('click', handleGlobalClick); }; // Button click @@ -30,27 +54,55 @@ // Close button click if (close) { - close.addEventListener('click', hideModal); - } - - // Click outside modal content - modal.addEventListener('click', (e) => { - if (e.target === modal) { + close.addEventListener('click', (e) => { + e.stopPropagation(); // Prevent bubbling to global handler (though redundant as handler checks) hideModal(); - } - }); + }); + } + + // Remove old direct listeners if any (cleaner approach replaces them) // Escape key to close if (!window.rewardKeyHandlerAttached) { - document.addEventListener('keydown', function(e) { + window.rewardKeyHandler = function(e) { const activeModal = document.getElementById('reward-modal'); if (e.key === 'Escape' && activeModal && activeModal.classList.contains('show')) { - activeModal.classList.remove('show'); - document.body.style.overflow = ''; + hideModal(); // Use the same hide function to clean up listeners } - }); + }; + document.addEventListener('keydown', window.rewardKeyHandler); window.rewardKeyHandlerAttached = true; } + + // Expose hideModal for destroy function if needed? + // We can just rely on the fact that destroyReward queries DOM. + // But we need to make sure destroyReward removes the global listener too. + // Since handleGlobalClick is local scope, we need a way to reference it or just define it outside? + // Or better: make handleGlobalClick a named function available to destroyReward via a shared scope or window. + // Let's attach it to window for safety in this module pattern. + window.rewardGlobalClickHandler = handleGlobalClick; + } + + function destroyReward() { + // Cleanup keydown listener + if (window.rewardKeyHandlerAttached && window.rewardKeyHandler) { + document.removeEventListener('keydown', window.rewardKeyHandler); + window.rewardKeyHandlerAttached = false; + window.rewardKeyHandler = null; + } + + // Cleanup global click listener + if (window.rewardGlobalClickHandler) { + document.removeEventListener('click', window.rewardGlobalClickHandler); + window.rewardGlobalClickHandler = null; + } + + // Hide modal if open (restore body scroll) + const modal = document.getElementById('reward-modal'); + if (modal && modal.classList.contains('show')) { + modal.classList.remove('show'); + document.body.style.overflow = ''; + } } // Initialize on load @@ -58,7 +110,11 @@ // Initialize on Pjax complete document.addEventListener('pjax:complete', initReward); + + // Destroy on Pjax send + document.addEventListener('pjax:send', destroyReward); // Export window.initReward = initReward; + window.destroyReward = destroyReward; })();