文章出自個人博客https://knightyun.github.io/2020/01/14/website-add-category,轉載請申明
概述
之前有寫文章探索如何給個人博客網站添加文章搜索功能,可以方便的通過關鍵詞檢索相關文章,現在再來探索一下另一個功能,即給文章添加目錄導航;對於篇幅較短的文章,目錄的有無影響不大,但是當文章篇幅過長時,一個能提供預覽和跳轉的目錄結構預覽,就顯得意義重大了,接下來就來一步步將它實現出來;
原理
樣式
實現功能必先思考其原理,目錄預覽其實就是一塊內容,包含當前頁面不同級別的標題的組合,並結構化的展示出來,首先我們可以參考一些網站的做法,比如 CSDN 的博客文章就有配置目錄插件,下面就是我的某篇文章的目錄預覽圖:
它們的目錄插件就是右邊側欄的一個按鈕,鼠標放上去就會顯示一個側欄,內容就是當前文章的小標題的集合,不同級別的標題對應這不同程度的縮進,並且點擊每個標題都會有相應的頁面跳轉,這也基本是我們常見而熟悉的目錄形式,那麼我們就以此爲參考來實現;
目錄獲取
想要生成這麼一個目錄之前,當然是要先獲取目錄的內容,前面講過,目錄的內容就是當前文章的所有標題的集合,而我們知道,在 HTML 中標題相關的標籤是 h1, h2, h3, h4, h5, h6
這幾個,所以直接獲取它們就行了,比如:
// 獲取所有的標籤名爲 h1 的元素
document.querySelectorAll('h1');
// 獲取所有的標籤名爲 h1 - h6 的元素
document.querySelectorAll('h1, h2, h3, h4, h5, h6');
獲取內容是一個數組,包含所有標題節點;接下來的問題就是考慮如何結構化存儲,這樣便於理解的同時又方便後期的讀取,所謂結構化,即目錄本身就是一類 樹 結構,比如,目錄包含多個一級標題,同時某些以及標題可能還有多個二級標題,甚至再向下延伸出三級標題等等,類似下面的結構:
├─ 一級標題 1
│ └─ 二級標題 1
│ └─ 三級標題 1
│ └─ 四級標題
├─ 一級標題 2
│ └─ 二級標題 1
├─ 一級標題 3
│ ├─ 二級標題 1
│ ├─ 二級標題 2
│ ├─ 二級標題 3
│ │ ├─ 三級標題 1
│ │ ├─ 三級標題 2
... ...
理論的做法就是以樹結構保存獲取的標題,類似於下面這種:
[{
node: 'h1Node', // 一級標題 1 對應的節點
child: [{
node: 'h2Node', // 二級標題 1
child: [{
node: 'h3Node', // 三級標題 1
child: []
}]
},
{
node: 'h2Node', // 二級標題 2
child: []
}]
},
{
node: 'h1Node', // 一級標題 2
child: [{
node: 'h2Node', // 二級標題 1
child: []
}]
},
{
node: 'h1Node', // 一級標題 3
child: []
}]
看着還是比較複雜的,耗費的空間也較大,需要遞歸式獲取,最後也要遞歸式的輸出,一般文章目錄包含的標題數量也是較少的,所以暫且不用這種結構來保存,可以換一種簡單的思路,即我們最後生成該目錄時可以選擇一行一行遞進的輸出,即設計如下結構:
[
'h1Node', // 一級標題 1 對應的元素節點
'h2Node', // 二級標題 1 (隸屬於一級標題 1)
'h3Node', // 三級標題 1 (隸屬於二級標題 1)
'h2Node', // 二級標題 2 (隸屬於一級標題 1)
'h1Node', // 一級標題 2
'h2Node', // 二級標題 1 (隸屬於一級標題 2)
'h1Node' // 一級標題 3
]
因爲我們只要求最終能輸出一列格式化的目錄,即挨個依次輸出,所以只需以此存儲即可,這樣佔用的空間和複雜度都有所減少;
目錄生成
最終展示效果設定爲最上面的樣圖所示,按照之前設計的存儲結構,遍歷該數組一行一行打印出來即可;關於不用級別的標題應用不同程度的縮進,可以巧妙利用一下元素節點的 nodeName
這個屬性,比如元素節點 <h1></h1>
對應的 nodeName
就是 H1
,h2
就對應 H2
,以此類推,我們就利用該值最後的那個數字,乘以一個固定縮進值,這樣級別遞增的標題節點也就擁有了遞增的縮進值,最後樣式部分就可以利用 padding-left
來實現縮進,js 代碼的實現思路如下:
// node 爲標題節點,32 是標題級別增加而多縮進的值
node.style.paddingLeft = node.nodeName.slice(-1) * 32 + 'px';
對於點擊標題跳轉到文章對應標題所在位置這個功能,實現也比較簡單,設置對應 錨點 即可,也就是標題元素需要設置一個 id
屬性值,然後給點擊的 <a>
標籤的 href
屬性也設置爲這個 id
值即可,例如:
<h1 id="my-h1">一級標題</h1>
<a href="my-h1">點我跳轉到一級標題</a>
具體實現
第三方庫
避免重複造輪子,一些基礎的格式化樣式就交給第三方庫去解決吧,這裏使用的是 Materialize 這個庫,安裝和引用教程去官網:https://materializecss.com 查看;
HTML部分
具體參考代碼與說明如下:
<!-- 引用第三方庫 -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/css/materialize.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/js/materialize.min.js"></script>
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<!-- 固定於屏幕右下方的一個懸浮按鈕 -->
<div class="fixed-action-btn">
<a class="btn-floating btn-large blue z-depth-4">
<i class="large material-icons">apps</i>
</a>
<ul>
<!-- 文章目錄按鈕 -->
<li class="category-btn hide">
<a class="sidenav-trigger btn-floating blue lighten-2" data-target="category">
<i class="material-icons">format_list_bulleted</i>
</a>
</li>
<!-- 下面也可以添加其他按鈕,如返回文章頂部等-->
<li>
<a class="btn-floating blue lighten-2" href="javascript: scrollTo(0, 0);">
<i class="material-icons">publish</i>
</a>
</li>
</ul>
</div>
<!-- 文章目錄側欄 -->
<ul id="category" class="hide sidenav grey lighten-4 grey-text text-darken-3">
<li><p class="center-align">目錄</p></li>
</ul>
<!-- 下面的元素中存放文章內容 -->
<div id="post-content">
<!-- 文章內容,需要注意的只有,爲不同的標題元素設置不同的 id 屬性以實現跳轉 -->
<!-- 以下爲示例內容 -->
<h1 id="t1">Title 1</h1>
<p>Hello World!</p>
<p>Hello World!</p>
<h2 id="t11">Title 1</h2>
<p>Hello World!</p>
<p>Hello World!</p>
<p>Hello World!</p>
<p>Hello World!</p>
<p>Hello World!</p>
<h2 id="t12">Title 1</h2>
<h3 id="t121">Title 1</h3>
<p>Hello World!</p>
<p>Hello World!</p>
<p>Hello World!</p>
<h1 id="t2">Title 2</h1>
<p>Hello World!</p>
<p>Hello World!</p>
<p>Hello World!</p>
<p>Hello World!</p>
<p>Hello World!</p>
<h1 id="t3">Title 3</h1>
<p>Hello World!</p>
<h2 id="t31">Title 3</h2>
<p>Hello World!</p>
<p>Hello World!</p>
<p>Hello World!</p>
<p>Hello World!</p>
<p>Hello World!</p>
<p>Hello World!</p>
<p>Hello World!</p>
</div>
CSS部分
樣式部分因人而異,可以自行設計調整,以下爲參考:
#category li a:before { /* 添加一個摺疊符號,爲了好看 */
content: "∟";
position: absolute;
left: 10px;
bottom: 5px;
font-size: 12px;
}
JavaScript部分
該部分就是核心所在了,對應上面的 HTML 和 CSS 部分,實現如下:
// 初始化第三方庫的插件
M.AutoInit();
document.addEventListener('DOMContentLoaded', function () {
var elemCategory = document.querySelector('#category');
M.Sidenav.init(elemCategory, {
'edge': 'right' // right 表示在右側欄顯示,left 則表示在左邊顯示
});
});
var postContent = document.querySelector('#post-content');
if (postContent) { // 存在文章內容
var categories = postContent.querySelectorAll('h1, h2, h3, h4, h5, h6');
if (categories.length > 0) { // 文章存在標題
var category = document.querySelector('#category'),
categoryBtn = document.querySelector('.category-btn');
var li = document.createElement('li'),
a = document.createElement('a');
a.className = 'waves-effect';
// 存在目錄則顯示目錄按鈕和側欄
category.classList.remove('hide');
categoryBtn.classList.remove('hide');
categories.forEach(node => {
// 每次 cloneNode 取代 createElement
// 因爲克隆一個元素快於創建一個元素
var _li = li.cloneNode(false),
_a = a.cloneNode(false);
_a.innerText = node.innerText;
// 爲標題設置跳轉鏈接
_a.href = '#' + node.id;
_li.appendChild(_a);
// 爲不同級別標題應用不同的縮進
_li.style.paddingLeft = node.nodeName.slice(-1) * 32 + 'px';
category.appendChild(_li);
})
}
}
效果
最後附上幾張本人博客網站實現的最終效果圖,也歡迎點擊 https://knightyun.github.io 前往訪問 ^_^