js实现文章目录索引导航(table of content)


Posted in Javascript onMay 10, 2020

什么叫TOC呢?table of content。

具体什么效果呢?可以随便找个hexo博客中体验一下,例如这个。

好了,实现它有2个要点:

点目录跳到段落:通过<a>标签的锚点实现,其原理在这里。
滚动触发目录变换:通过js监听滚动事件,判定当前所处段落,令对应目录项高亮。
我写了一个简单的demo来演示这个效果,

源码地址:https://github.com/owenliang/js-toc

在线体验:http://owenliang.github.io/js-toc

实现分析

#toc是左侧的目录,#content是右侧的文章正文。

<div id="toc">
  <ul>

  </ul>
 </div>
 <div id="content">
  <a name="seg-1" class="seg-begin"><h1>第1章节</h1></a>
  <div class="seg-content"></div>
  <a name="seg-2" class="seg-begin"><h1>第2章节</h1></a>
  <div class="seg-content"></div>
  <a name="seg-3" class="seg-begin"><h1>第3章节</h1></a>
  <div class="seg-content"></div>
  <a name="seg-4" class="seg-begin"><h1>第4章节</h1></a>
  <div class="seg-content"></div>
 </div>

利用css控制#toc靠左,当前目录高亮为红色,正文则靠右填满屏幕:

#toc {
   width: 200px;
   position: fixed;
   left: 0;
   top: 0;
  }
  #toc a.active {
   color: red;
  }
  #content {
   margin-left: 200px;
  }

在上面的静态页面中,目录暂时为空,因为需要用JS动态生成。

正文中需要人工埋点段落起始标识,也就是a.seg-begin这样的锚点,每个段落的锚点name唯一,而锚点之后紧随段落的内容。

在JS中,我首先按锚点的出现次序收集所有的a.seg-begin保存到segs数组中,其顺序就是文章自上而下的阅读顺序,按照其<h1>中的段落标题建出#toc中的<ul>列表:

var segs = [];
    $(".seg-begin").each(function (idx, node) {
     segs.push(node)
     var link = $("<a></a>").attr("href", "#" + $(node).attr("name")).html($(node).children("h1").html())
     if (!idx) {
      link.addClass("active")
     }
     var row = $("<li></li>").append(link)
     $("#toc ul").append(row)
    })

然后绑定浏览器的scroll事件进行监听,每次滚动就判断最近一个滚出屏幕顶部的a.seg-begin节点,它就是当前正在阅读的段落:

$(window).bind("scroll", function() {
     var scrollTop = $(this).scrollTop()
     var topSeg = null
     for (var idx in segs) {
      var seg = segs[idx]
      if (seg.offsetTop > scrollTop) {
       continue
      }
      if (!topSeg) {
       topSeg = seg
      } else if (seg.offsetTop >= topSeg.offsetTop) {
       topSeg = seg
      }
     }
     if (topSeg) {
      $("#toc a").removeClass("active")
      var link = "#" + $(topSeg).attr("name")
      console.log('#toc a[href="' + link + '" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" ]')
      $('#toc a[href="' + link + '" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" ]').addClass("active")
      // console.log($(topSeg).children("h1").text())
     }
    })

后续

这里目录的生成是在前端JS里根据正文的锚点动态生成的,为了SEO可以在后端提交文章正文时匹配出这些锚点,直接保存为目录。

完整代码

<!DOCTYPE html>
<html lang="en">
<head>
 <meta charset="UTF-8">
 <style>
  * {
   margin: 0;
   padding: 0;
   word-break: break-all;
  }
  #toc {
   width: 200px;
   position: fixed;
   left: 0;
   top: 0;
  }
  #toc a.active {
   color: red;
  }
  #content {
   margin-left: 200px;
  }
 </style>
 <script src="https://cdn.bootcss.com/jquery/3.2.1/jquery.min.js"></script>
 <script>
  $(document).ready(function () {
   for (var i = 0; i < 50; ++i) {
    $(".seg-content").append("<p>一个段落而已</p>")
   }

   (function () {
    var segs = [];
    $(".seg-begin").each(function (idx, node) {
     segs.push(node)

     var link = $("<a></a>").attr("href", "#" + $(node).attr("name")).html($(node).children("h1").html())
     if (!idx) {
      link.addClass("active")
     }
     var row = $("<li></li>").append(link)
     $("#toc ul").append(row)
    })

    $(window).bind("scroll", function() {
     var scrollTop = $(this).scrollTop()

     var topSeg = null
     for (var idx in segs) {
      var seg = segs[idx]
      if (seg.offsetTop > scrollTop) {
       continue
      }
      if (!topSeg) {
       topSeg = seg
      } else if (seg.offsetTop >= topSeg.offsetTop) {
       topSeg = seg
      }
     }
     if (topSeg) {
      $("#toc a").removeClass("active")

      var link = "#" + $(topSeg).attr("name")
      console.log('#toc a[href="' + link + '" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" ]')
      $('#toc a[href="' + link + '" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" ]').addClass("active")
      // console.log($(topSeg).children("h1").text())
     }
    })
   })()
  })
 </script>
</head>
<body>
 <div id="toc">
  <ul>

  </ul>
 </div>
 <div id="content">
  <a name="seg-1" class="seg-begin"><h1>第1章节</h1></a>
  <div class="seg-content"></div>
  <a name="seg-2" class="seg-begin"><h1>第2章节</h1></a>
  <div class="seg-content"></div>
  <a name="seg-3" class="seg-begin"><h1>第3章节</h1></a>
  <div class="seg-content"></div>
  <a name="seg-4" class="seg-begin"><h1>第4章节</h1></a>
  <div class="seg-content"></div>
 </div>
</body>
</html>

另外,这里没有实现嵌套的目录结构,我特意观察了一下hexo的做法,是通过h1,h2,h3来表达层级的,这样在each遍历生成目录的时候可以基于这个信息完成嵌套层级的标记,问题迎刃而解。

Javascript 相关文章推荐
指定位置如果有图片显示图片,无图片显示广告的JS
Jun 05 Javascript
javascript学习笔记(六) Date 日期类型
Jun 19 Javascript
理运用命名空间让js不产生冲突避免全局变量的泛滥
Jun 15 Javascript
JavaScript中使用arguments获得函数传参个数实例
Aug 27 Javascript
JavaScript函数使用的基本教程
Jun 04 Javascript
jquery实现最简单的滑动菜单效果代码
Sep 12 Javascript
javascript多物体运动实现方法分析
Jan 08 Javascript
js中class的点击事件没有效果的解决方法
Oct 13 Javascript
vue项目中axios请求网络接口封装的示例代码
Dec 18 Javascript
JS使用数组实现的队列功能示例
Mar 04 Javascript
Vue CLI 3.x 自动部署项目至服务器的方法
Apr 02 Javascript
Vue全局loading及错误提示的思路与实现
Aug 09 Javascript
文章或博客自动生成章节目录索引(支持三级)的实现代码
May 10 #Javascript
vue滑动吸顶及锚点定位的示例代码
May 10 #Javascript
webpack+vue.js构建前端工程化的详细教程
May 10 #Javascript
JS实现单张或多张图片持续无缝滚动的示例代码
May 10 #Javascript
Layer UI表格列日期格式化及取消自动填充日期的实现方法
May 10 #Javascript
angula中使用iframe点击后不执行变更检测的问题
May 10 #Javascript
JavaScript 装逼指南(js另类写法)
May 10 #Javascript
You might like
PHP 删除文件与文件夹操作 unlink()与rmdir()这两个函数的使用
2011/07/17 PHP
PHP实现简单数字分页效果
2015/07/26 PHP
IE浏览器打印的页眉页脚设置解决方法
2009/12/08 Javascript
一些常用且实用的原生JavaScript函数
2010/09/08 Javascript
jQuery EasyUI API 中文文档 - Form表单
2011/10/06 Javascript
document.getElementBy(&quot;id&quot;)与$(&quot;#id&quot;)有什么区别
2013/09/22 Javascript
JS模拟实现Select效果代码
2015/09/24 Javascript
每天一篇javascript学习小结(面向对象编程)
2015/11/20 Javascript
阿里云ecs服务器中安装部署node.js的步骤
2016/10/08 Javascript
BootStrap tooltip提示框使用小结
2016/10/26 Javascript
JS区分Object与Aarry的六种方法总结
2017/02/27 Javascript
angularjs实现上拉加载和下拉刷新数据功能
2017/06/12 Javascript
JavaScript中三个等号和两个等号你了解多少
2017/07/04 Javascript
layui之select的option叠加问题的解决方法
2018/03/08 Javascript
JS的函数调用栈stack size的计算方法
2018/06/24 Javascript
代码实例ajax实现点击加载更多数据图片
2018/10/12 Javascript
vue+element实现表单校验功能
2019/05/20 Javascript
layui表格设计以及数据初始化详解
2019/10/26 Javascript
微信小程序 (地址选择1)--选取搜索地点并显示效果
2019/12/17 Javascript
uniapp实现横向滚动选择日期
2020/10/21 Javascript
Django项目中包含多个应用时对url的配置方法
2018/05/30 Python
在scrapy中使用phantomJS实现异步爬取的方法
2018/12/17 Python
pytorch 自定义参数不更新方式
2020/01/06 Python
python实现图像拼接功能
2020/03/23 Python
python利用tkinter实现图片格式转换的示例
2020/09/28 Python
selenium设置浏览器为headless无头模式(Chrome和Firefox)
2021/01/08 Python
River Island美国官网:英国高街时尚品牌
2018/09/04 全球购物
以太网Ethernet IEEE802.3
2013/08/05 面试题
竞聘医务工作人员的自我评价分享
2013/11/04 职场文书
商务助理求职信范文
2014/04/20 职场文书
就业协议书怎么填
2014/09/15 职场文书
2014年小学语文工作总结
2014/12/20 职场文书
2019消防宣传标语!
2019/07/10 职场文书
Pytorch中TensorBoard及torchsummary的使用详解
2021/05/12 Python
python基础之类属性和实例属性
2021/10/24 Python
Pytorch中expand()的使用(扩展某个维度)
2022/07/15 Python