纯JS实现WordPress简单文章目录功能
导语
文章目录这种东西应该是每个(WordPress)站点都具备的功能,尤其是阅读长文章的时候,在用户触手可及的地方提供一个目录,既可以方便用户快捷跳转,又可以快速了解文章概要,是一件一举两得的事情。然而现实情况却不尽人意,大部分CMS只关注文章本身,而把其他部分交给主题和用户来定制,导致这些”其他部分”一直没有一个统一的标准,主题和插件实现的五花八门。加之中外审美的巨大差异,找一个和自己主题搭配且好用的文章目录方案,确实是一件十分困难的事情。
相信折腾到最后,大部分人都会选择使用带文章目录功能的主题,或是文章目录插件。我自己找了一圈,发现大部分插件不是审美严重不同(违和感极强),就是价格昂贵,十分划不来,并且我个人也不喜欢使用不必要插件。
最终,我决定使用纯JavaScript实现一个文章目录功能。这样既不会影响后续更新,也免去了使用不搭配的插件带来的一系列烦恼,实现效果可以参考右侧侧边栏。
设计思路
在页面加载时通过JavaScript提取页面中的标题信息,随后创建并插入文章目录。
大致分为三个步骤
- 判断是否为文章页面
- 获取标题信息
- 生成文章目录
文章标题的结构
通常情况下我们使用h1-6标签显示标题,h1标签显示文章标题,因此我们在正文使用的标签就从h2开始。一般标题分级不会超过三级,h2、h3、h4再往后就几乎不用了。观察维基百科的目录结构,一般是显示三级,但我们通常不会写那么长的大型文章,分二级显示就足够了。也就是说,我们只需要获取文章页面的h2、h3标签。
获取标题信息
大部分WordPress主题的文章主体部分通常包含在一个div元素中,div里包含着各种文章元素,标题h2 h3、段落p、图片img等等,不过这个div的class是什么由开发者决定。按下你浏览器的F12键
,打开审查元素
,可以轻松找到该div元素。
判断是否为文章页面
由于WordPress的限制,我们在不修改模板/使用插件的前提下插入JS代码时只能插入到整个网站。不过解决方法也很简单,只要检索页面中有没有文章主体的div即可判断出当前页面是不是文章页面,进而自适应生成目录。
let articleContent = document.getElementsByClassName('entry-inner'); if (articleContent.length !== 1) { return null; }
寻找并记录标题
遍历这个容器的所有元素,找出二级标题h2和三级标题h3即可。为了实现目录层次,我们使用一个数组来记录一个二级标题下的所有三级标题。为了实现跳转功能,我们为每个标题标签分配唯一id。
let catalog = []; let header = {}; let elements = articleContent[0].childNodes; // 遍历所有元素 for (let i = 0; i < elements.length; i++) { if (elements[i].nodeName === 'H2') { // 为二级标题分配ID以供锚点跳转,下同 elements[i].id = 'h2-' + catalog.length; // 记录此二级标题和其所有的三级子标题 header = { name: elements[i].innerText, childHeaders: [] }; catalog.push(header); } else if (elements[i].nodeName === 'H3') { elements[i].id = 'h2-' + (catalog.length - 1) + '-h3-' + header.childHeaders.length; // 记录此三级标题到二级标题下 header.childHeaders.push(elements[i].innerText); } }
生成文章目录
有了目录信息,并且在上一步已经为标签添加了ID,只需要简单的拼接即可实现自动生成文章目录
let catalog = '<div style="text-align: center; margin-top: 10px;">文章目录</div>'; for (let i = 0; i < catalogData.length; i++) { let target = '#h2-' + i; // 跳转目标 let index = (i + 1) + '. '; // 标题索引 let name = catalogData[i].name; // 标题 catalog += '<a href="' + target + '">' + index + name + '</a><br/>'; for (let i2 = 0; i2 < catalogData[i].childHeaders.length; i2++) { target = '#h2-' + i + '-h3-' + i2; index = (i + 1) + '.' + (i2 + 1) + '. '; name = catalogData[i].childHeaders[i2]; catalog += ' <a href="' + target + '">' + index + name + '</a><br/>' } }
完整代码
使用时记得根据你的实际情况修改dynamic-wrapper
、entry-inner
两处,目录的样式随意发挥。
为了平滑跳转,可以为html和body设置 scroll-behavior: smooth
已添加详细注释,方便修改使用
<script> /* Author: Azure99 WebSite: https://www.rainng.com/ GitHub: https://github.com/Azure99 */ let catalogData = getArticleCatalog(); // 自适应文章目录 if (catalogData != null) { // dynamic-wrapper换成你的目录容器 let wrapper = document.getElementById('dynamic-wrapper'); wrapper.innerHTML = generateCatalog(catalogData); } // 获取本页面的文章目录信息 function getArticleCatalog() { // entry-inner换成你使用主题的文章容器 let articleContent = document.getElementsByClassName('entry-inner'); if (articleContent.length !== 1) { return null; } let catalog = []; let header = {}; let elements = articleContent[0].childNodes; // 遍历所有元素 for (let i = 0; i < elements.length; i++) { if (elements[i].nodeName === 'H2') { // 为二级标题分配ID以供锚点跳转,下同 elements[i].id = 'h2-' + catalog.length; // 记录此二级标题和其所有的三级子标题 header = { name: elements[i].innerText, childHeaders: [] }; catalog.push(header); } else if (elements[i].nodeName === 'H3') { elements[i].id = 'h2-' + (catalog.length - 1) + '-h3-' + header.childHeaders.length; // 记录此三级标题到二级标题下 header.childHeaders.push(elements[i].innerText); } } return catalog; } // 根据目录信息生成文章目录代码 function generateCatalog(catalogData) { let catalog = '<div style="text-align: center; margin-top: 10px;">文章目录</div>'; for (let i = 0; i < catalogData.length; i++) { let target = '#h2-' + i; // 跳转目标 let index = (i + 1) + '. '; // 标题索引 let name = catalogData[i].name; // 标题 catalog += '<a href="' + target + '">' + index + name + '</a><br/>'; for (let i2 = 0; i2 < catalogData[i].childHeaders.length; i2++) { target = '#h2-' + i + '-h3-' + i2; index = (i + 1) + '.' + (i2 + 1) + '. '; name = catalogData[i].childHeaders[i2]; catalog += ' <a href="' + target + '">' + index + name + '</a><br/>' } } return catalog; } </script>
有个wordpress网站,文章容器是嵌套起来的,像下面这样,有办法解决吗?直接用大佬的模板实现不了..
<div data-sek-level="module" data-sek-id="_nimble_422" "class="sek-row sa-dflu"
…
…
…
……
非常不错,感谢分享。不过在我添加此HTML代码到自定义的HTML小工具里没法实现,不知道原因。尝试多次修改了对应主题的dynamic-wrapper、entry-inner,也可能是我找的不对导致的。另外我还使用了构建器来编辑文章和页面的,也可能和这个有关。
非常好用,谢谢分享。另外说一句,如果顶部有固定导航,要考虑到锚点偏移,不然会被导航遮盖住。
等有时间着加个暗锚
可以实现渐变颜色吗?我发现阿里云的文档是有这个效果 的。
可以是可以,现在想到的一个思路:计算出所有标题的绝对高度,然后监听屏幕位置。不过实现起来应该比较啰嗦,有空写一下。