diff --git a/pytorrent/static/app.js b/pytorrent/static/app.js index d968a78..4256bd0 100644 --- a/pytorrent/static/app.js +++ b/pytorrent/static/app.js @@ -24,6 +24,8 @@ const hasActiveProfile = !!window.PYTORRENT?.activeProfile; let firstRunSetupShown = false; const activeOperations = new Map(); + // Note: Keeps live filter tooltips stable while the pointer is over a filter button. + const filterTooltipState = new WeakMap(); function toast(msg, type="secondary") { const h=$('toastHost'); if(!h) return; const el=document.createElement('div'); el.className=`toast-item text-bg-${type}`; el.innerHTML=esc(msg); h.appendChild(el); setTimeout(()=>el.remove(),3500); } function setBusy(on){ pendingBusy += on ? 1 : -1; if(pendingBusy<0) pendingBusy=0; $('globalLoader')?.classList.toggle('d-none', pendingBusy===0); $('busyBadge')?.classList.toggle('d-none', pendingBusy===0); } @@ -93,6 +95,41 @@ } return lines.join('\n'); } + function applyFilterTooltip(button, tooltip, ariaLabel){ + if(tooltip){ + button.title = tooltip; + button.setAttribute('aria-label', ariaLabel); + } else { + button.removeAttribute('title'); + button.removeAttribute('aria-label'); + } + } + function ensureStableFilterTooltip(button){ + if(filterTooltipState.has(button)) return filterTooltipState.get(button); + const state = {hovering:false, pending:null}; + filterTooltipState.set(button, state); + button.addEventListener('mouseenter', () => { + state.hovering = true; + state.pending = null; + }); + button.addEventListener('mouseleave', () => { + state.hovering = false; + if(state.pending){ + applyFilterTooltip(button, state.pending.tooltip, state.pending.ariaLabel); + state.pending = null; + } + }); + return state; + } + // Note: Freezes tooltip content during hover; the next hover receives the newest live summary. + function setStableFilterTooltip(button, tooltip, ariaLabel){ + const state = ensureStableFilterTooltip(button); + if(state.hovering){ + state.pending = {tooltip, ariaLabel}; + return; + } + applyFilterTooltip(button, tooltip, ariaLabel); + } function setFilterSummary(type){ const el=$(FILTER_COUNT_IDS[type]); if(!el) return; @@ -102,13 +139,8 @@ el.innerHTML=`${esc(bucket.count||0)}${meta?`${esc(meta)}`:''}`; const button=el.closest('.filter'); if(button){ - if(tooltip){ - button.title=tooltip; - button.setAttribute('aria-label', `${button.dataset.filter || type}: ${tooltip.replace(/\n/g, ', ')}`); - } else { - button.removeAttribute('title'); - button.removeAttribute('aria-label'); - } + const ariaLabel = tooltip ? `${button.dataset.filter || type}: ${tooltip.replace(/\n/g, ', ')}` : ''; + setStableFilterTooltip(button, tooltip, ariaLabel); } } function labelNames(value){ return String(value||'').split(/[,;|]+/).map(x=>x.trim()).filter(Boolean).filter((x,i,a)=>a.indexOf(x)===i); }