()
| 20 | const headingElements = new Map<string, Element>() |
| 21 | |
| 22 | const setupObserver = () => { |
| 23 | // Clean up previous observer |
| 24 | if (observer) { |
| 25 | observer.disconnect() |
| 26 | } |
| 27 | headingElements.clear() |
| 28 | |
| 29 | // Find all heading elements that match TOC IDs |
| 30 | const ids = toc.value.map(item => item.id) |
| 31 | if (ids.length === 0) return |
| 32 | |
| 33 | for (const id of ids) { |
| 34 | const el = document.getElementById(id) |
| 35 | if (el) { |
| 36 | headingElements.set(id, el) |
| 37 | } |
| 38 | } |
| 39 | |
| 40 | if (headingElements.size === 0) return |
| 41 | |
| 42 | // Create observer that triggers when headings cross the top 20% of viewport |
| 43 | observer = new IntersectionObserver( |
| 44 | entries => { |
| 45 | // Get all visible headings sorted by their position |
| 46 | const visibleHeadings: { id: string; top: number }[] = [] |
| 47 | |
| 48 | for (const entry of entries) { |
| 49 | if (entry.isIntersecting) { |
| 50 | visibleHeadings.push({ |
| 51 | id: entry.target.id, |
| 52 | top: entry.boundingClientRect.top, |
| 53 | }) |
| 54 | } |
| 55 | } |
| 56 | |
| 57 | // If there are visible headings, pick the one closest to the top |
| 58 | if (visibleHeadings.length > 0) { |
| 59 | visibleHeadings.sort((a, b) => a.top - b.top) |
| 60 | activeId.value = visibleHeadings[0]?.id ?? null |
| 61 | } else { |
| 62 | // No headings visible in intersection zone - find the one just above viewport |
| 63 | const headingsWithPosition: { id: string; top: number }[] = [] |
| 64 | for (const [id, el] of headingElements) { |
| 65 | const rect = el.getBoundingClientRect() |
| 66 | headingsWithPosition.push({ id, top: rect.top }) |
| 67 | } |
| 68 | |
| 69 | // Find the heading that's closest to (but above) the viewport top |
| 70 | const aboveViewport = headingsWithPosition |
| 71 | .filter(h => h.top < 100) // Allow some buffer |
| 72 | .sort((a, b) => b.top - a.top) // Sort descending (closest to top first) |
| 73 | |
| 74 | if (aboveViewport.length > 0) { |
| 75 | activeId.value = aboveViewport[0]?.id ?? null |
| 76 | } |
| 77 | } |
| 78 | }, |
| 79 | { |
nothing calls this directly
no test coverage detected