mirror of
https://github.com/mwisnowski/mtg_python_deckbuilder.git
synced 2025-12-17 08:00:13 +01:00
feat: add collapsible analytics, click-to-pin chart tooltips, and extended virtualization
This commit is contained in:
parent
3877890889
commit
20b9e8037c
10 changed files with 1036 additions and 202 deletions
|
|
@ -798,9 +798,8 @@
|
|||
// --- Lightweight virtualization (feature-flagged via data-virtualize) ---
|
||||
function initVirtualization(root){
|
||||
try{
|
||||
var body = document.body || document.documentElement;
|
||||
var DIAG = !!(body && body.getAttribute('data-diag') === '1');
|
||||
// Global diagnostics aggregator
|
||||
var body = document.body || document.documentElement;
|
||||
var DIAG = !!(body && body.getAttribute('data-diag') === '1');
|
||||
var GLOBAL = (function(){
|
||||
if (!DIAG) return null;
|
||||
if (window.__virtGlobal) return window.__virtGlobal;
|
||||
|
|
@ -821,7 +820,6 @@
|
|||
el.style.zIndex = '50';
|
||||
el.style.boxShadow = '0 4px 12px rgba(0,0,0,.35)';
|
||||
el.style.cursor = 'default';
|
||||
// Hidden by default; toggle with 'v'
|
||||
el.style.display = 'none';
|
||||
document.body.appendChild(el);
|
||||
store.summaryEl = el;
|
||||
|
|
@ -837,7 +835,7 @@
|
|||
visible += (g[i].end||0) - (g[i].start||0);
|
||||
lastMs = Math.max(lastMs, g[i].lastMs||0);
|
||||
}
|
||||
el.textContent = 'virt sum: grids '+g.length+' • visible '+visible+'/'+total+' • last '+lastMs.toFixed ? lastMs.toFixed(1) : String(lastMs)+'ms';
|
||||
el.textContent = 'virt sum: grids '+g.length+' • visible '+visible+'/'+total+' • last '+(lastMs.toFixed ? lastMs.toFixed(1) : String(lastMs))+'ms';
|
||||
}
|
||||
function register(gridId, ref){
|
||||
store.grids.push({ id: gridId, ref: ref });
|
||||
|
|
@ -852,48 +850,66 @@
|
|||
}
|
||||
update();
|
||||
},
|
||||
toggle: function(){ var el = ensure(); el.style.display = (el.style.display === 'none' ? '' : 'none'); }
|
||||
toggle: function(){
|
||||
var el = ensure();
|
||||
el.style.display = (el.style.display === 'none' ? '' : 'none');
|
||||
}
|
||||
};
|
||||
}
|
||||
window.__virtGlobal = { register: register, toggle: function(){ var el = ensure(); el.style.display = (el.style.display === 'none' ? '' : 'none'); } };
|
||||
window.__virtGlobal = {
|
||||
register: register,
|
||||
toggle: function(){
|
||||
var el = ensure();
|
||||
el.style.display = (el.style.display === 'none' ? '' : 'none');
|
||||
}
|
||||
};
|
||||
return window.__virtGlobal;
|
||||
})();
|
||||
// Support card grids and other scroll containers (e.g., #owned-box)
|
||||
var grids = (root || document).querySelectorAll('.card-grid[data-virtualize="1"], #owned-box[data-virtualize="1"]');
|
||||
|
||||
var scope = root || document;
|
||||
if (!scope || !scope.querySelectorAll) return;
|
||||
var grids = scope.querySelectorAll('[data-virtualize]');
|
||||
if (!grids.length) return;
|
||||
|
||||
grids.forEach(function(grid){
|
||||
if (grid.__virtBound) return;
|
||||
grid.__virtBound = true;
|
||||
// Basic windowing: assumes roughly similar tile heights; uses sentinel measurements.
|
||||
if (!grid || grid.__virtBound) return;
|
||||
var attrVal = (grid.getAttribute('data-virtualize') || '').trim();
|
||||
if (!attrVal || /^0|false$/i.test(attrVal)) return;
|
||||
|
||||
var container = grid;
|
||||
container.style.position = container.style.position || 'relative';
|
||||
var wrapper = document.createElement('div');
|
||||
wrapper.className = 'virt-wrapper';
|
||||
// Ensure wrapper itself is a grid to preserve multi-column layout inside
|
||||
// when the container (e.g., .card-grid) is virtualized.
|
||||
wrapper.style.display = 'grid';
|
||||
// Move children into a fragment store (for owned, children live under UL)
|
||||
|
||||
var mode = attrVal.toLowerCase();
|
||||
var minItemsAttr = parseInt(grid.getAttribute('data-virtualize-min') || (grid.dataset ? grid.dataset.virtualizeMin : ''), 10);
|
||||
var rowAttr = parseInt(grid.getAttribute('data-virtualize-row') || (grid.dataset ? grid.dataset.virtualizeRow : ''), 10);
|
||||
var colAttr = parseInt(grid.getAttribute('data-virtualize-columns') || (grid.dataset ? grid.dataset.virtualizeColumns : ''), 10);
|
||||
var maxHeightAttr = grid.getAttribute('data-virtualize-max-height') || (grid.dataset ? grid.dataset.virtualizeMaxHeight : '');
|
||||
var overflowAttr = grid.getAttribute('data-virtualize-overflow') || (grid.dataset ? grid.dataset.virtualizeOverflow : '');
|
||||
|
||||
var source = container;
|
||||
// If this is the owned box, use the UL inside as the source list
|
||||
var ownedGrid = container.id === 'owned-box' ? container.querySelector('#owned-grid') : null;
|
||||
if (ownedGrid) { source = ownedGrid; }
|
||||
if (!source || !source.children || !source.children.length) return;
|
||||
|
||||
var all = Array.prototype.slice.call(source.children);
|
||||
// Threshold: skip virtualization for small grids to avoid scroll jitter at end-of-list.
|
||||
// Empirically flicker was reported when reaching the bottom of short grids (e.g., < 80 tiles)
|
||||
// due to dynamic height adjustments (image loads + padding recalcs). Keeping full DOM
|
||||
// is cheaper than the complexity for small sets.
|
||||
var MIN_VIRT_ITEMS = 80;
|
||||
if (all.length < MIN_VIRT_ITEMS){
|
||||
// Mark as processed so we don't attempt again on HTMX swaps.
|
||||
return; // children remain in place; no virtualization applied.
|
||||
}
|
||||
all.forEach(function(node, idx){ try{ node.__virtIndex = idx; }catch(_){ } });
|
||||
var minItems = !isNaN(minItemsAttr) ? Math.max(0, minItemsAttr) : 80;
|
||||
if (all.length < minItems) return;
|
||||
|
||||
grid.__virtBound = true;
|
||||
|
||||
var store = document.createElement('div');
|
||||
store.style.display = 'none';
|
||||
all.forEach(function(n){ store.appendChild(n); });
|
||||
all.forEach(function(node){ store.appendChild(node); });
|
||||
|
||||
var padTop = document.createElement('div');
|
||||
var padBottom = document.createElement('div');
|
||||
padTop.style.height = '0px'; padBottom.style.height = '0px';
|
||||
// For owned, keep the UL but render into it; otherwise append wrapper to container
|
||||
padTop.style.height = '0px';
|
||||
padBottom.style.height = '0px';
|
||||
|
||||
var wrapper = document.createElement('div');
|
||||
wrapper.className = 'virt-wrapper';
|
||||
|
||||
if (ownedGrid){
|
||||
ownedGrid.innerHTML = '';
|
||||
ownedGrid.appendChild(padTop);
|
||||
|
|
@ -901,17 +917,34 @@
|
|||
ownedGrid.appendChild(padBottom);
|
||||
ownedGrid.appendChild(store);
|
||||
} else {
|
||||
container.appendChild(padTop);
|
||||
container.appendChild(wrapper);
|
||||
container.appendChild(padBottom);
|
||||
container.appendChild(store);
|
||||
}
|
||||
var rowH = container.id === 'owned-box' ? 160 : 240; // estimate tile height
|
||||
var perRow = 1;
|
||||
// Optional diagnostics overlay
|
||||
|
||||
if (maxHeightAttr){
|
||||
container.style.maxHeight = maxHeightAttr;
|
||||
} else if (!container.style.maxHeight){
|
||||
container.style.maxHeight = '70vh';
|
||||
}
|
||||
if (overflowAttr){
|
||||
container.style.overflow = overflowAttr;
|
||||
} else if (!container.style.overflow){
|
||||
container.style.overflow = 'auto';
|
||||
}
|
||||
|
||||
var baseRow = container.id === 'owned-box' ? 160 : (mode.indexOf('list') > -1 ? 110 : 240);
|
||||
var minRowH = !isNaN(rowAttr) && rowAttr > 0 ? rowAttr : baseRow;
|
||||
var rowH = minRowH;
|
||||
var explicitCols = (!isNaN(colAttr) && colAttr > 0) ? colAttr : null;
|
||||
var perRow = explicitCols || 1;
|
||||
|
||||
var diagBox = null; var lastRenderAt = 0; var lastRenderMs = 0;
|
||||
var renderCount = 0; var measureCount = 0; var swapCount = 0;
|
||||
var gridId = (container.id || container.className || 'grid') + '#' + Math.floor(Math.random()*1e6);
|
||||
var globalReg = DIAG && GLOBAL ? GLOBAL.register(gridId, container) : null;
|
||||
|
||||
function fmt(n){ try{ return (Math.round(n*10)/10).toFixed(1); }catch(_){ return String(n); } }
|
||||
function ensureDiag(){
|
||||
if (!DIAG) return null;
|
||||
|
|
@ -928,8 +961,7 @@
|
|||
diagBox.style.fontSize = '12px';
|
||||
diagBox.style.margin = '0 0 .35rem 0';
|
||||
diagBox.style.color = '#cbd5e1';
|
||||
diagBox.style.display = 'none'; // hidden until toggled
|
||||
// Controls
|
||||
diagBox.style.display = 'none';
|
||||
var controls = document.createElement('div');
|
||||
controls.style.display = 'flex';
|
||||
controls.style.gap = '.35rem';
|
||||
|
|
@ -937,107 +969,204 @@
|
|||
controls.style.marginBottom = '.25rem';
|
||||
var title = document.createElement('div'); title.textContent = 'virt diag'; title.style.fontWeight = '600'; title.style.fontSize = '11px'; title.style.color = '#9ca3af';
|
||||
var btnCopy = document.createElement('button'); btnCopy.type = 'button'; btnCopy.textContent = 'Copy'; btnCopy.className = 'btn small';
|
||||
btnCopy.addEventListener('click', function(){ try{ var payload = {
|
||||
id: gridId, rowH: rowH, perRow: perRow, start: start, end: end, total: total,
|
||||
renderCount: renderCount, measureCount: measureCount, swapCount: swapCount,
|
||||
lastRenderMs: lastRenderMs, lastRenderAt: lastRenderAt
|
||||
}; navigator.clipboard.writeText(JSON.stringify(payload, null, 2)); btnCopy.textContent = 'Copied'; setTimeout(function(){ btnCopy.textContent = 'Copy'; }, 1200); }catch(_){ }
|
||||
btnCopy.addEventListener('click', function(){
|
||||
try{
|
||||
var payload = {
|
||||
id: gridId,
|
||||
rowH: rowH,
|
||||
perRow: perRow,
|
||||
start: start,
|
||||
end: end,
|
||||
total: total,
|
||||
renderCount: renderCount,
|
||||
measureCount: measureCount,
|
||||
swapCount: swapCount,
|
||||
lastRenderMs: lastRenderMs,
|
||||
lastRenderAt: lastRenderAt,
|
||||
};
|
||||
navigator.clipboard.writeText(JSON.stringify(payload, null, 2));
|
||||
btnCopy.textContent = 'Copied';
|
||||
setTimeout(function(){ btnCopy.textContent = 'Copy'; }, 1200);
|
||||
}catch(_){ }
|
||||
});
|
||||
var btnHide = document.createElement('button'); btnHide.type = 'button'; btnHide.textContent = 'Hide'; btnHide.className = 'btn small';
|
||||
btnHide.addEventListener('click', function(){ diagBox.style.display = 'none'; });
|
||||
controls.appendChild(title); controls.appendChild(btnCopy); controls.appendChild(btnHide);
|
||||
controls.appendChild(title);
|
||||
controls.appendChild(btnCopy);
|
||||
controls.appendChild(btnHide);
|
||||
diagBox.appendChild(controls);
|
||||
var text = document.createElement('div'); text.className = 'virt-diag-text'; diagBox.appendChild(text);
|
||||
var host = (container.id === 'owned-box') ? container : container.parentElement || container;
|
||||
host.insertBefore(diagBox, host.firstChild);
|
||||
return diagBox;
|
||||
}
|
||||
|
||||
function measure(){
|
||||
try {
|
||||
measureCount++;
|
||||
// create a temp tile to measure if none
|
||||
var probe = store.firstElementChild || all[0];
|
||||
if (probe){
|
||||
var fake = probe.cloneNode(true);
|
||||
fake.style.position = 'absolute'; fake.style.visibility = 'hidden'; fake.style.pointerEvents = 'none';
|
||||
fake.style.position = 'absolute';
|
||||
fake.style.visibility = 'hidden';
|
||||
fake.style.pointerEvents = 'none';
|
||||
(ownedGrid || container).appendChild(fake);
|
||||
var rect = fake.getBoundingClientRect();
|
||||
rowH = Math.max(120, Math.ceil(rect.height) + 16);
|
||||
rowH = Math.max(minRowH, Math.ceil(rect.height) + 16);
|
||||
(ownedGrid || container).removeChild(fake);
|
||||
}
|
||||
// Estimate perRow via computed styles of grid
|
||||
var style = window.getComputedStyle(ownedGrid || container);
|
||||
var cols = style.getPropertyValue('grid-template-columns');
|
||||
// Mirror grid settings onto the wrapper so its children still flow in columns
|
||||
try {
|
||||
var displayMode = style.getPropertyValue('display');
|
||||
if (displayMode && displayMode.trim()){
|
||||
wrapper.style.display = displayMode;
|
||||
} else if (!wrapper.style.display){
|
||||
wrapper.style.display = 'grid';
|
||||
}
|
||||
if (cols && cols.trim()) wrapper.style.gridTemplateColumns = cols;
|
||||
var gap = style.getPropertyValue('gap') || style.getPropertyValue('grid-gap');
|
||||
if (gap && gap.trim()) wrapper.style.gap = gap;
|
||||
// Inherit justify/align if present
|
||||
var ji = style.getPropertyValue('justify-items');
|
||||
if (ji && ji.trim()) wrapper.style.justifyItems = ji;
|
||||
var ai = style.getPropertyValue('align-items');
|
||||
if (ai && ai.trim()) wrapper.style.alignItems = ai;
|
||||
} catch(_) {}
|
||||
perRow = Math.max(1, (cols && cols.split ? cols.split(' ').filter(function(x){return x && (x.indexOf('px')>-1 || x.indexOf('fr')>-1 || x.indexOf('minmax(')>-1);}).length : 1));
|
||||
} catch(_){}
|
||||
} catch(_){ }
|
||||
var derivedCols = (cols && cols.split ? cols.split(' ').filter(function(x){
|
||||
return x && (x.indexOf('px')>-1 || x.indexOf('fr')>-1 || x.indexOf('minmax(')>-1);
|
||||
}).length : 0);
|
||||
if (explicitCols){
|
||||
perRow = explicitCols;
|
||||
} else if (derivedCols){
|
||||
perRow = Math.max(1, derivedCols);
|
||||
} else {
|
||||
perRow = Math.max(1, perRow);
|
||||
}
|
||||
} catch(_){ }
|
||||
}
|
||||
|
||||
measure();
|
||||
var total = all.length;
|
||||
var start = 0, end = 0;
|
||||
|
||||
function render(){
|
||||
var t0 = DIAG ? performance.now() : 0;
|
||||
var scroller = container;
|
||||
var vh = scroller.clientHeight || window.innerHeight;
|
||||
var scrollTop = scroller.scrollTop;
|
||||
// If container isn’t scrollable, use window scroll offset
|
||||
var top = scrollTop || (scroller.getBoundingClientRect().top < 0 ? -scroller.getBoundingClientRect().top : 0);
|
||||
var rowsInView = Math.ceil(vh / rowH) + 2; // overscan
|
||||
var rowStart = Math.max(0, Math.floor(top / rowH) - 1);
|
||||
var rowEnd = Math.min(Math.ceil((top / rowH)) + rowsInView, Math.ceil(total / perRow));
|
||||
var newStart = rowStart * perRow;
|
||||
var newEnd = Math.min(total, rowEnd * perRow);
|
||||
if (newStart === start && newEnd === end) return; // no change
|
||||
start = newStart; end = newEnd;
|
||||
// Padding
|
||||
var beforeRows = Math.floor(start / perRow);
|
||||
var afterRows = Math.ceil((total - end) / perRow);
|
||||
var vh, scrollTop, top;
|
||||
|
||||
if (useWindowScroll) {
|
||||
// Window-scroll mode: measure relative to viewport
|
||||
vh = window.innerHeight;
|
||||
var rect = container.getBoundingClientRect();
|
||||
top = Math.max(0, -rect.top);
|
||||
scrollTop = window.pageYOffset || document.documentElement.scrollTop || 0;
|
||||
} else {
|
||||
// Container-scroll mode: measure relative to container
|
||||
vh = scroller.clientHeight || window.innerHeight;
|
||||
scrollTop = scroller.scrollTop;
|
||||
top = scrollTop || (scroller.getBoundingClientRect().top < 0 ? -scroller.getBoundingClientRect().top : 0);
|
||||
}
|
||||
|
||||
var rowsInView = Math.ceil(vh / Math.max(1, rowH)) + 2;
|
||||
var rowStart = Math.max(0, Math.floor(top / Math.max(1, rowH)) - 1);
|
||||
var rowEnd = Math.min(Math.ceil(top / Math.max(1, rowH)) + rowsInView, Math.ceil(total / Math.max(1, perRow)));
|
||||
var newStart = rowStart * Math.max(1, perRow);
|
||||
var newEnd = Math.min(total, rowEnd * Math.max(1, perRow));
|
||||
if (newStart === start && newEnd === end) return;
|
||||
start = newStart;
|
||||
end = newEnd;
|
||||
var beforeRows = Math.floor(start / Math.max(1, perRow));
|
||||
var afterRows = Math.ceil((total - end) / Math.max(1, perRow));
|
||||
padTop.style.height = (beforeRows * rowH) + 'px';
|
||||
padBottom.style.height = (afterRows * rowH) + 'px';
|
||||
// Render visible children
|
||||
wrapper.innerHTML = '';
|
||||
for (var i = start; i < end; i++) {
|
||||
for (var i = start; i < end; i++){
|
||||
var node = all[i];
|
||||
if (node) wrapper.appendChild(node);
|
||||
}
|
||||
if (DIAG){
|
||||
var box = ensureDiag();
|
||||
if (box){
|
||||
var dt = performance.now() - t0; lastRenderMs = dt; renderCount++; lastRenderAt = Date.now();
|
||||
var vis = end - start; var rowsTotal = Math.ceil(total / perRow);
|
||||
var dt = performance.now() - t0;
|
||||
lastRenderMs = dt;
|
||||
renderCount++;
|
||||
lastRenderAt = Date.now();
|
||||
var vis = end - start;
|
||||
var rowsTotal = Math.ceil(total / Math.max(1, perRow));
|
||||
var textEl = box.querySelector('.virt-diag-text');
|
||||
var msg = 'range ['+start+'..'+end+') of '+total+' • vis '+vis+' • rows ~'+rowsTotal+' • perRow '+perRow+' • rowH '+rowH+'px • render '+fmt(dt)+'ms • renders '+renderCount+' • measures '+measureCount+' • swaps '+swapCount;
|
||||
textEl.textContent = msg;
|
||||
// Health hint
|
||||
var bad = (dt > 33) || (vis > 300);
|
||||
var warn = (!bad) && ((dt > 16) || (vis > 200));
|
||||
box.style.borderColor = bad ? '#ef4444' : (warn ? '#f59e0b' : 'var(--border)');
|
||||
box.style.boxShadow = bad ? '0 0 0 1px rgba(239,68,68,.35)' : (warn ? '0 0 0 1px rgba(245,158,11,.25)' : 'none');
|
||||
if (globalReg && globalReg.set){ globalReg.set({ total: total, start: start, end: end, lastMs: dt }); }
|
||||
if (globalReg && globalReg.set){
|
||||
globalReg.set({ total: total, start: start, end: end, lastMs: dt });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onScroll(){ render(); }
|
||||
function onResize(){ measure(); render(); }
|
||||
container.addEventListener('scroll', onScroll);
|
||||
|
||||
// Support both container-scroll (default) and window-scroll modes
|
||||
var scrollMode = overflowAttr || container.style.overflow || 'auto';
|
||||
var useWindowScroll = (scrollMode === 'visible' || scrollMode === 'window');
|
||||
|
||||
if (useWindowScroll) {
|
||||
// Window-scroll mode: listen to window scroll events
|
||||
window.addEventListener('scroll', onScroll, { passive: true });
|
||||
} else {
|
||||
// Container-scroll mode: listen to container scroll events
|
||||
container.addEventListener('scroll', onScroll, { passive: true });
|
||||
}
|
||||
window.addEventListener('resize', onResize);
|
||||
// Initial size; ensure container is scrollable for our logic
|
||||
if (!container.style.maxHeight) container.style.maxHeight = '70vh';
|
||||
container.style.overflow = container.style.overflow || 'auto';
|
||||
|
||||
render();
|
||||
// Re-render after filters resort or HTMX swaps
|
||||
document.addEventListener('htmx:afterSwap', function(ev){ if (container.contains(ev.target)) { swapCount++; all = Array.prototype.slice.call(store.children).concat(Array.prototype.slice.call(wrapper.children)); total = all.length; measure(); render(); } });
|
||||
// Keyboard toggle for overlays: 'v'
|
||||
|
||||
// Track cleanup for disconnected containers
|
||||
grid.__virtCleanup = function(){
|
||||
try {
|
||||
if (useWindowScroll) {
|
||||
window.removeEventListener('scroll', onScroll);
|
||||
} else {
|
||||
container.removeEventListener('scroll', onScroll);
|
||||
}
|
||||
window.removeEventListener('resize', onResize);
|
||||
} catch(_){}
|
||||
};
|
||||
|
||||
document.addEventListener('htmx:afterSwap', function(ev){
|
||||
if (!container.isConnected) return;
|
||||
if (!container.contains(ev.target)) return;
|
||||
swapCount++;
|
||||
var merged = Array.prototype.slice.call(store.children).concat(Array.prototype.slice.call(wrapper.children));
|
||||
var known = new Map();
|
||||
all.forEach(function(node, idx){
|
||||
var index = (typeof node.__virtIndex === 'number') ? node.__virtIndex : idx;
|
||||
known.set(node, index);
|
||||
});
|
||||
var nextIndex = known.size;
|
||||
merged.forEach(function(node){
|
||||
if (!known.has(node)){
|
||||
node.__virtIndex = nextIndex;
|
||||
known.set(node, nextIndex);
|
||||
nextIndex++;
|
||||
}
|
||||
});
|
||||
merged.sort(function(a, b){
|
||||
var ia = known.get(a);
|
||||
var ib = known.get(b);
|
||||
return (ia - ib);
|
||||
});
|
||||
merged.forEach(function(node, idx){ node.__virtIndex = idx; });
|
||||
all = merged;
|
||||
total = all.length;
|
||||
measure();
|
||||
render();
|
||||
});
|
||||
|
||||
if (DIAG && !window.__virtHotkeyBound){
|
||||
window.__virtHotkeyBound = true;
|
||||
document.addEventListener('keydown', function(e){
|
||||
|
|
@ -1045,9 +1174,11 @@
|
|||
if (e.target && (/input|textarea|select/i).test(e.target.tagName)) return;
|
||||
if (e.key && e.key.toLowerCase() === 'v'){
|
||||
e.preventDefault();
|
||||
// Toggle all virt-diag boxes and the global summary
|
||||
var shown = null;
|
||||
document.querySelectorAll('.virt-diag').forEach(function(b){ if (shown === null) shown = (b.style.display === 'none'); b.style.display = shown ? '' : 'none'; });
|
||||
document.querySelectorAll('.virt-diag').forEach(function(b){
|
||||
if (shown === null) shown = (b.style.display === 'none');
|
||||
b.style.display = shown ? '' : 'none';
|
||||
});
|
||||
if (GLOBAL && GLOBAL.toggle) GLOBAL.toggle();
|
||||
}
|
||||
}catch(_){ }
|
||||
|
|
@ -1198,4 +1329,61 @@
|
|||
});
|
||||
}catch(_){ }
|
||||
});
|
||||
|
||||
// --- Lazy-loading analytics accordions ---
|
||||
function initLazyAccordions(root){
|
||||
try {
|
||||
var scope = root || document;
|
||||
if (!scope || !scope.querySelectorAll) return;
|
||||
|
||||
scope.querySelectorAll('.analytics-accordion[data-lazy-load]').forEach(function(details){
|
||||
if (!details || details.__lazyBound) return;
|
||||
details.__lazyBound = true;
|
||||
|
||||
var loaded = false;
|
||||
|
||||
details.addEventListener('toggle', function(){
|
||||
if (!details.open || loaded) return;
|
||||
loaded = true;
|
||||
|
||||
// Mark as loaded to prevent re-initialization
|
||||
var content = details.querySelector('.analytics-content');
|
||||
if (!content) return;
|
||||
|
||||
// Remove placeholder if present
|
||||
var placeholder = content.querySelector('.analytics-placeholder');
|
||||
if (placeholder) {
|
||||
placeholder.remove();
|
||||
}
|
||||
|
||||
// Content is already rendered in the template, just need to initialize any scripts
|
||||
// Re-run virtualization if needed
|
||||
try {
|
||||
initVirtualization(content);
|
||||
} catch(_){}
|
||||
|
||||
// Re-attach chart interactivity if this is mana overview
|
||||
var type = details.getAttribute('data-analytics-type');
|
||||
if (type === 'mana') {
|
||||
try {
|
||||
// Tooltip and highlight logic is already in the template scripts
|
||||
// Just trigger a synthetic event to re-attach if needed
|
||||
var event = new CustomEvent('analytics:loaded', { detail: { type: 'mana' } });
|
||||
details.dispatchEvent(event);
|
||||
} catch(_){}
|
||||
}
|
||||
|
||||
// Send telemetry
|
||||
telemetry.send('analytics.accordion_expand', {
|
||||
type: type || 'unknown',
|
||||
accordion: details.id || 'unnamed',
|
||||
});
|
||||
});
|
||||
});
|
||||
} catch(_){}
|
||||
}
|
||||
|
||||
// Initialize on load and after HTMX swaps
|
||||
document.addEventListener('DOMContentLoaded', function(){ initLazyAccordions(); });
|
||||
document.addEventListener('htmx:afterSwap', function(e){ initLazyAccordions(e.target); });
|
||||
})();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue