| @@ -116,7 +116,7 @@ |
| 116 | 116 | function initPageTOC() { |
| 117 | 117 | const tocContainer = document.querySelector('.page-toc'); |
| 118 | 118 | const marker = document.querySelector('.outline-marker'); |
| 119 | | - const tocLinks = document.querySelectorAll('.page-toc a'); |
| 119 | + const tocLinks = document.querySelectorAll('.page-toc a, .bp-local-outline-items a'); |
| 120 | 120 | const headings = Array.from(document.querySelectorAll('h2[id], h3[id], h4[id], h5[id], h6[id]')); |
| 121 | 121 | const scrollContainer = document.querySelector('.BPContent'); |
| 122 | 122 | |
| @@ -144,18 +144,42 @@ function initPageTOC() { |
| 144 | 144 | }; |
| 145 | 145 | } |
| 146 | 146 | |
| 147 | | - // Activate a TOC link by hash |
| 147 | + // Keep the active entry inside whichever outline is currently visible. On |
| 148 | + // long pages the marker used to continue moving below the fixed aside while |
| 149 | + // the aside itself remained at scrollTop 0, making a working scrollspy look |
| 150 | + // completely dead once the reader passed the first screen of headings. |
| 151 | + function keepActiveLinkVisible(activeLink) { |
| 152 | + const outlineViewport = activeLink.closest('.BPDocAside') || activeLink.closest('.bp-local-outline-items'); |
| 153 | + if (!outlineViewport || outlineViewport.clientHeight === 0) return; |
| 154 | + |
| 155 | + const linkRect = activeLink.getBoundingClientRect(); |
| 156 | + const viewportRect = outlineViewport.getBoundingClientRect(); |
| 157 | + const inset = 24; |
| 158 | + |
| 159 | + if (linkRect.top < viewportRect.top + inset) { |
| 160 | + outlineViewport.scrollTop += linkRect.top - viewportRect.top - inset; |
| 161 | + } else if (linkRect.bottom > viewportRect.bottom - inset) { |
| 162 | + outlineViewport.scrollTop += linkRect.bottom - viewportRect.bottom + inset; |
| 163 | + } |
| 164 | + } |
| 165 | + |
| 166 | + // Activate matching links in both the fixed desktop outline and the |
| 167 | + // collapsible in-content outline used at narrower widths. |
| 148 | 168 | function activateLink(hash) { |
| 149 | 169 | // Remove previous active class |
| 150 | 170 | tocLinks.forEach(link => link.classList.remove('active')); |
| 151 | 171 | |
| 152 | 172 | if (hash) { |
| 153 | | - // Find and activate new link |
| 154 | | - const activeLink = tocContainer.querySelector('a[href="' + decodeURIComponent(hash) + '"]'); |
| 173 | + const decodedHash = decodeURIComponent(hash); |
| 174 | + const activeLinks = Array.from(tocLinks).filter(link => link.getAttribute('href') === decodedHash); |
| 175 | + const activeLink = tocContainer.querySelector('a[href="' + decodedHash + '"]'); |
| 155 | 176 | |
| 156 | | - if (activeLink) { |
| 157 | | - activeLink.classList.add('active'); |
| 177 | + activeLinks.forEach(link => { |
| 178 | + link.classList.add('active'); |
| 179 | + keepActiveLinkVisible(link); |
| 180 | + }); |
| 158 | 181 | |
| 182 | + if (activeLink) { |
| 159 | 183 | // Position the marker at the active link |
| 160 | 184 | const linkRect = activeLink.getBoundingClientRect(); |
| 161 | 185 | const containerRect = tocContainer.getBoundingClientRect(); |
| @@ -329,6 +353,16 @@ function initPageTOC() { |
| 329 | 353 | window.__bpTocHashChange = settleActiveLink; |
| 330 | 354 | window.addEventListener('hashchange', settleActiveLink); |
| 331 | 355 | |
| 356 | + // Opening the responsive outline does not move the page and therefore does |
| 357 | + // not emit a scroll event. Re-run once it becomes visible so its active |
| 358 | + // entry and internal scroll position immediately match the current section. |
| 359 | + const inlineOutline = document.querySelector('.bp-local-outline'); |
| 360 | + if (inlineOutline) { |
| 361 | + inlineOutline.addEventListener('toggle', () => { |
| 362 | + if (inlineOutline.open) requestAnimationFrame(setActiveLink); |
| 363 | + }); |
| 364 | + } |
| 365 | + |
| 332 | 366 | // Initial update |
| 333 | 367 | requestAnimationFrame(setActiveLink); |
| 334 | 368 | } |