From 299d11c1d0e1553d8ffd404b07d5280c805a5007 Mon Sep 17 00:00:00 2001 From: AIIrondev Date: Mon, 18 May 2026 17:06:46 +0200 Subject: [PATCH] feat: implement DOM virtualization for item cards in main and admin templates --- Web/templates/main.html | 50 ++++++++++++++++++++++++++++++++++ Web/templates/main_admin.html | 51 +++++++++++++++++++++++++++++++++++ 2 files changed, 101 insertions(+) diff --git a/Web/templates/main.html b/Web/templates/main.html index 56da176..c69fd13 100755 --- a/Web/templates/main.html +++ b/Web/templates/main.html @@ -751,6 +751,7 @@ let mainItemsSentinel = null; let mainItemsLoadingIndicator = null; let mainItemsScrollPrefetchBound = false; + let cardVirtualizationObserver = null; function ensureMainItemsLoadingIndicator() { const itemsContainer = document.querySelector('#items-container'); @@ -1189,6 +1190,55 @@ mainItemsScrollPrefetchBound = true; } + // Setup DOM virtualization for items + if (!cardVirtualizationObserver) { + cardVirtualizationObserver = new IntersectionObserver((entries) => { + entries.forEach(entry => { + const card = entry.target; + if (entry.isIntersecting) { + if (card.dataset.deloaded === 'true') { + card.innerHTML = card.dataset.savedHtml; + card.dataset.deloaded = 'false'; + card.classList.remove('is-deloaded'); + const favBtn = card.querySelector('.bookmark-btn'); + if (favBtn) { + favBtn.addEventListener('click', (ev) => { + ev.stopPropagation(); + toggleFavorite(card.dataset.itemId, favBtn, card); + }); + } + const cardContent = card.querySelector('.card-content'); + if (cardContent) { + cardContent.addEventListener('click', function(e) { + if (e.target.tagName === 'BUTTON' || e.target.tagName === 'A' || e.target.tagName === 'INPUT' || + e.target.closest('button') || e.target.closest('a') || e.target.closest('.image-container')) return; + e.stopPropagation(); + openItemQuick(card.dataset.itemId); + }); + } + } + } else { + if (card.dataset.deloaded !== 'true') { + const rect = card.getBoundingClientRect(); + if (rect.height > 50) { + card.style.minHeight = rect.height + 'px'; + card.dataset.itemId = card.querySelector('.card-content')?.dataset.itemId || ''; + card.dataset.savedHtml = card.innerHTML; + card.innerHTML = ''; + card.dataset.deloaded = 'true'; + card.classList.add('is-deloaded'); + } + } + } + }); + }, { + rootMargin: isMobileLayout ? '1500px 0px 1500px 0px' : '0px 2500px 0px 2500px' + }); + } + + // Observe all valid cards + document.querySelectorAll('.item-card').forEach(card => cardVirtualizationObserver.observe(card)); + if (!mainItemsHasMore) return; mainItemsObserver = new IntersectionObserver(entries => { diff --git a/Web/templates/main_admin.html b/Web/templates/main_admin.html index 2859f68..4172815 100755 --- a/Web/templates/main_admin.html +++ b/Web/templates/main_admin.html @@ -3357,6 +3357,7 @@ document.addEventListener('DOMContentLoaded', ()=>{ let mainAdminItemsLoadingIndicator = null; let mainAdminItemsScrollPrefetchBound = false; let totalItemsInSystem = 0; + let cardVirtualizationObserver = null; function ensureMainAdminItemsLoadingIndicator() { const itemsContainer = document.querySelector('#items-container'); @@ -3784,6 +3785,56 @@ document.addEventListener('DOMContentLoaded', ()=>{ mainAdminItemsScrollPrefetchBound = true; } + // Setup DOM virtualization for items + if (!cardVirtualizationObserver) { + cardVirtualizationObserver = new IntersectionObserver((entries) => { + entries.forEach(entry => { + const card = entry.target; + if (entry.isIntersecting) { + if (card.dataset.deloaded === 'true') { + card.innerHTML = card.dataset.savedHtml; + card.dataset.deloaded = 'false'; + card.classList.remove('is-deloaded'); + const favBtn = card.querySelector('.bookmark-btn'); + if (favBtn) { + favBtn.addEventListener('click', (ev) => { + ev.stopPropagation(); + toggleFavorite(card.dataset.itemId, favBtn, card); + }); + } + const cardContent = card.querySelector('.card-content'); + if (cardContent) { + cardContent.addEventListener('click', function(e) { + if (e.target.tagName === 'BUTTON' || e.target.tagName === 'A' || e.target.tagName === 'INPUT' || + e.target.closest('button') || e.target.closest('a') || e.target.closest('.image-container') || + e.target.closest('.bulk-delete-toggle') || e.target.closest('label')) return; + e.stopPropagation(); + openItemQuick(card.dataset.itemId); + }); + } + } + } else { + if (card.dataset.deloaded !== 'true') { + const rect = card.getBoundingClientRect(); + if (rect.height > 50) { + card.style.minHeight = rect.height + 'px'; + card.dataset.itemId = card.querySelector('.card-content')?.dataset.itemId || ''; + card.dataset.savedHtml = card.innerHTML; + card.innerHTML = ''; + card.dataset.deloaded = 'true'; + card.classList.add('is-deloaded'); + } + } + } + }); + }, { + rootMargin: isMobileLayout ? '1500px 0px 1500px 0px' : '0px 2500px 0px 2500px' + }); + } + + // Observe all valid cards + document.querySelectorAll('.item-card').forEach(card => cardVirtualizationObserver.observe(card)); + if (!mainAdminItemsHasMore) return; mainAdminItemsObserver = new IntersectionObserver(entries => {