function initScrollbox(box) { const area = box.querySelector("[data-scroll-area]"); const track = box.querySelector("[data-scroll-track]"); const thumb = box.querySelector("[data-scroll-thumb]"); const inset = () => parseFloat(getComputedStyle(thumb).left) || 0; const update = () => { const pad = inset(); const room = track.clientHeight - pad * 2; const maxScroll = area.scrollHeight - area.clientHeight; const ratio = Math.min(1, area.clientHeight / area.scrollHeight); const height = Math.max(room * ratio, thumb.clientWidth); const progress = maxScroll > 0 ? area.scrollTop / maxScroll : 0; thumb.style.height = `${height}px`; thumb.style.top = `${pad + progress * (room - height)}px`; }; thumb.addEventListener("pointerdown", (e) => { e.preventDefault(); thumb.setPointerCapture(e.pointerId); const startY = e.clientY; const startTop = parseFloat(thumb.style.top) || 0; const move = (ev) => { const pad = inset(); const room = track.clientHeight - pad * 2 - thumb.offsetHeight; const top = Math.min( Math.max(startTop - pad + ev.clientY - startY, 0), room, ); const progress = room > 0 ? top / room : 0; area.scrollTop = progress * (area.scrollHeight - area.clientHeight); }; const stop = () => { thumb.removeEventListener("pointermove", move); thumb.removeEventListener("pointerup", stop); thumb.removeEventListener("pointercancel", stop); }; thumb.addEventListener("pointermove", move); thumb.addEventListener("pointerup", stop); thumb.addEventListener("pointercancel", stop); }); area.addEventListener("scroll", update, { passive: true }); const observer = new ResizeObserver(update); observer.observe(area); for (const child of area.children) observer.observe(child); update(); } document.querySelectorAll("[data-scrollbox]").forEach(initScrollbox);