js實(shí)現(xiàn)文章目錄索引導(dǎo)航(table of content)
什么叫TOC呢?table of content。
具體什么效果呢?可以隨便找個(gè)hexo博客中體驗(yàn)一下,例如這個(gè)。
好了,實(shí)現(xiàn)它有2個(gè)要點(diǎn):
點(diǎn)目錄跳到段落:通過<a>標(biāo)簽的錨點(diǎn)實(shí)現(xiàn),其原理在這里。
滾動(dòng)觸發(fā)目錄變換:通過js監(jiān)聽滾動(dòng)事件,判定當(dāng)前所處段落,令對(duì)應(yīng)目錄項(xiàng)高亮。
我寫了一個(gè)簡(jiǎn)單的demo來演示這個(gè)效果,
源碼地址:https://github.com/owenliang/js-toc
在線體驗(yàn):http://owenliang.github.io/js-toc
實(shí)現(xiàn)分析
#toc是左側(cè)的目錄,#content是右側(cè)的文章正文。
<div id="toc"> <ul> </ul> </div> <div id="content"> <a name="seg-1" class="seg-begin"><h1>第1章節(jié)</h1></a> <div class="seg-content"></div> <a name="seg-2" class="seg-begin"><h1>第2章節(jié)</h1></a> <div class="seg-content"></div> <a name="seg-3" class="seg-begin"><h1>第3章節(jié)</h1></a> <div class="seg-content"></div> <a name="seg-4" class="seg-begin"><h1>第4章節(jié)</h1></a> <div class="seg-content"></div> </div>
利用css控制#toc靠左,當(dāng)前目錄高亮為紅色,正文則靠右填滿屏幕:
#toc {
width: 200px;
position: fixed;
left: 0;
top: 0;
}
#toc a.active {
color: red;
}
#content {
margin-left: 200px;
}
在上面的靜態(tài)頁面中,目錄暫時(shí)為空,因?yàn)樾枰肑S動(dòng)態(tài)生成。
正文中需要人工埋點(diǎn)段落起始標(biāo)識(shí),也就是a.seg-begin這樣的錨點(diǎn),每個(gè)段落的錨點(diǎn)name唯一,而錨點(diǎn)之后緊隨段落的內(nèi)容。
在JS中,我首先按錨點(diǎn)的出現(xiàn)次序收集所有的a.seg-begin保存到segs數(shù)組中,其順序就是文章自上而下的閱讀順序,按照其<h1>中的段落標(biāo)題建出#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事件進(jìn)行監(jiān)聽,每次滾動(dòng)就判斷最近一個(gè)滾出屏幕頂部的a.seg-begin節(jié)點(diǎn),它就是當(dāng)前正在閱讀的段落:
$(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())
}
})
后續(xù)
這里目錄的生成是在前端JS里根據(jù)正文的錨點(diǎn)動(dòng)態(tài)生成的,為了SEO可以在后端提交文章正文時(shí)匹配出這些錨點(diǎn),直接保存為目錄。
完整代碼
<!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>一個(gè)段落而已</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章節(jié)</h1></a>
<div class="seg-content"></div>
<a name="seg-2" class="seg-begin"><h1>第2章節(jié)</h1></a>
<div class="seg-content"></div>
<a name="seg-3" class="seg-begin"><h1>第3章節(jié)</h1></a>
<div class="seg-content"></div>
<a name="seg-4" class="seg-begin"><h1>第4章節(jié)</h1></a>
<div class="seg-content"></div>
</div>
</body>
</html>
另外,這里沒有實(shí)現(xiàn)嵌套的目錄結(jié)構(gòu),我特意觀察了一下hexo的做法,是通過h1,h2,h3來表達(dá)層級(jí)的,這樣在each遍歷生成目錄的時(shí)候可以基于這個(gè)信息完成嵌套層級(jí)的標(biāo)記,問題迎刃而解。
相關(guān)文章
js+html+css實(shí)現(xiàn)簡(jiǎn)單電子時(shí)鐘
這篇文章主要為大家詳細(xì)介紹了js+html+css實(shí)現(xiàn)簡(jiǎn)單電子時(shí)鐘,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-06-06
Javascript 獲取鼠標(biāo)當(dāng)前的位置實(shí)現(xiàn)方法
這篇文章主要介紹了Javascript 獲取鼠標(biāo)當(dāng)前的位置實(shí)現(xiàn)方法的相關(guān)資料,需要的朋友可以參考下2016-10-10
基于BootStrap Metronic開發(fā)框架經(jīng)驗(yàn)小結(jié)【五】Bootstrap File Input文件上傳插件的用法
本文主要基于我自己的框架代碼案例,介紹其中文件上傳插件File Input的使用,非常具有參考借鑒價(jià)值,感興趣的朋友一起學(xué)習(xí)吧2016-05-05
原生javascript單例模式的應(yīng)用實(shí)例分析
這篇文章主要介紹了原生javascript單例模式的應(yīng)用,結(jié)合實(shí)例形式分析了JavaScript單例模式的基本功能、原理、應(yīng)用及操作注意事項(xiàng),需要的朋友可以參考下2020-02-02
淺談TypeScript3.7中值得注意的3個(gè)新特性
這篇文章主要介紹了TypeScript3.7中值得注意的3個(gè)新特性,對(duì)TS感興趣的同學(xué),可以參考下2021-05-05

