文章目录跟随文章内容滚动
2023年6月10日 2024年2月13日
说明
- 监听滚动事件, 触发定时器, 到时实现文章目录滚动跟随
- 浏览器支持
- docs-toc scrollTop my-toc scrollTop 文章目录跳转 Safari O X - Firefox O X - Chrome O O docs-toc需要定时器, 延时要求大于文章目录跳转最大耗时
滚动监听
- window添加事件监听
1window.addEventListener('scroll', () => { 2 // 处理 3 });
- window注册滚动处理
1window.onscroll = function () { 2 // 处理 3};
进入判断
1document.addEventListener('DOMContentLoaded', () => { 2 const myToc = document.querySelector('.my-toc'); 3 const fullToc = document.querySelector('.docs-toc'); 4 if (!myToc || !fullToc) return; 5 6 // 后续处理 7});
滚动计算
为标题排序, 厘清高亮标题在文章目录中的位置
1let i = 0; 2myToc.querySelectorAll('a').forEach(entry => { 3 entry.setAttribute('scrollIdx', i++); 4});
文章目录结构
.my-toc嵌套在.docs-toc中, .docs-toc多一个"文章目录"提示
- 根据.my-toc的滚动高度和其容纳的标题个数计算单个标题的高度
1function computeHeadingHeight() { 2 const toc = document.querySelector('.my-toc'); 3 return toc.scrollHeight / toc.querySelectorAll('a').length; 4}
- 计算窗口能容纳的标题数,经过测试,取其1/6,可以使高亮标题始终处于文章目录偏上位置
1function computeUpIdx() { 2 const fullToc = document.querySelector('.docs-toc'); 3 const myToc = document.querySelector('.my-toc'); 4 const offset = fullToc.scrollHeight - myToc.scrollHeight; 5 const max = parseInt((window.innerHeight - offset) / HeadingHeight); 6 return parseInt(max / 6); 7}
- 添加全局变量, 保存单个标题高度和高亮标题理想位置
1let HeadingHeight, UpIdx; 2 3HeadingHeight = computeHeadingHeight(); 4UpIdx = computeUpIdx();
注册监听处理:重启定时器, 延时滚动文章目录
1let FollowTimer = null; // 全局 2const FollowTimerInterval = 300; 3 4window.onscroll = function () { 5 clearTimeout(FollowTimer); 6 FollowTimer = setTimeout(scrollFollow, FollowTimerInterval); 7};
实现文章目录滚动
无高亮标题, 不作处理;多个标题高亮时, 基于第一个标题计算滚动偏移
1function scrollFollow() { 2 const activeHeadings = document.querySelector('.my-toc').querySelectorAll('a.active'); 3 if (activeHeadings.length > 0) { 4 const heading = activeHeadings.item(0); 5 const idx = heading.getAttribute('scrollIdx'); 6 const scrollTarget = idx - upIdx; 7 document.querySelector('.docs-toc').scrollTop = HeadingHeight * scrollTarget; 8 } 9}
完整JavaScript代码
1let HeadingHeight, UpIdx; 2let FollowTimer = null; 3const FollowTimerInterval = 300; 4 5function computeHeadingHeight() { 6 const toc = document.querySelector('.my-toc'); 7 return toc.scrollHeight / toc.querySelectorAll('a').length; 8} 9 10function computeUpIdx() { 11 const fullToc = document.querySelector('.docs-toc'); 12 const myToc = document.querySelector('.my-toc'); 13 const offset = fullToc.scrollHeight - myToc.scrollHeight; 14 const max = parseInt((window.innerHeight - offset) / HeadingHeight); 15 return parseInt(max / 6); 16} 17 18function scrollFollow() { 19 const activeHeadings = document.querySelector('.my-toc').querySelectorAll('a.active'); 20 if (activeHeadings.length > 0) { 21 const heading = activeHeadings.item(0); 22 const idx = heading.getAttribute('scrollIdx'); 23 const scrollTarget = idx - UpIdx; 24 document.querySelector('.docs-toc').scrollTop = HeadingHeight * scrollTarget; 25 } 26} 27 28document.addEventListener('DOMContentLoaded', () => { 29 const myToc = document.querySelector('.my-toc'); 30 const fullToc = document.querySelector('.docs-toc'); 31 if (!myToc || !fullToc) return; 32 33 let i = 0; 34 myToc.querySelectorAll('a').forEach(entry => { 35 entry.setAttribute('scrollIdx', i++); 36 }); 37 38 HeadingHeight = computeHeadingHeight(); 39 UpIdx = computeUpIdx(); 40 41 window.onscroll = function () { 42 clearTimeout(FollowTimer); 43 FollowTimer = setTimeout(scrollFollow, FollowTimerInterval); 44 }; 45});