文章页面目录自动生成方案
一、前言
前两天项目遇到一个需要给页面添加大纲导航的功能,要求把页面中的特定标签加入到大纲导航中。类似这样:
二、实现思路
1、需求分析
竟然是给标题元素加了一个带有id属性的a标签的子节点。不过它生成id的方式比较简单,单纯的"字符串_编号"而已,想来并不是那么可靠(难于保证编辑器外有相同id的元素)。
既然是对于任意页面都可用,那可以遍历DOM树,寻找需要导航的标签,然后把相关节点位置信息存储起来。这里也可一类似mavon-editor给dom树中插入一个元素作为一个锚点。遍历DOM树的方法应该与DOM渲染后从上到下的顺序一致,即采用深度优先的先序遍历方法(先序遍历即先检查根元素,再检查子元素;后序遍历则相反;如果是二叉树,还有中序遍历)。
在所有页面中,并不能单纯根据h1,h2等标签名来判别一个元素是否要导航,所以想到了用选择器来确定,同时添加根据选择器来排除一些例外的元素。
最终的导航应该是一个树形结构,并且每一个节点对应一个插入的锚点,即每一个树节点应该包含一个锚点信息。
2、实现思路
一个列表selectors:列表中的每一项是一层导航对应的选择器,比如下标为0的元素是第一级导航,通常可以用选择器'h1',下标为1的元素是第二级导航,通常可以用选择器'h2';
一个字符串exceptSelector,用于排除例外元素的选择器;
一个回调函数callback,用于接收生成的导航树形数据。
三、具体实现
1、锚点生成函数
import uuidv4 from 'uuid/v4'
let ATTR_NAME = 'navigation_anchor'
function createLinkElement (dom) {
let id = uuidv4()
let element = document.createElement('a')
element.setAttribute('id', id)
element.setAttribute(ATTR_NAME, true)
dom.parentNode.insertBefore(element, dom)
return id
}
2、锚点清理函数
function clearLinkElement (dom) {
dom = dom || document
let domList = dom.querySelectorAll(`a[${ATTR_NAME}]`)
for (let idx = domList.length - 1; idx > -1; idx--) {
let element = domList[idx]
element.parentNode.removeChild(element)
}
}
3、生成树形导航数据函数
function generateNavTree (dom, selectors, exceptSelector) {
clearLinkElement(dom)
let list = []
if (exceptSelector) {
let exceptList = dom.querySelectorAll(exceptSelector)
exceptList.forEach(element => {
element.__nav_except = true
})
}
for (let idx in selectors) {
let elementList = dom.querySelectorAll(selectors[idx])
elementList.forEach(element => {
if (element.__nav_except || element.offsetParent === null) return
element.__nav_level = idx
})
}
let selector = selectors.join(',')
let domList = dom.querySelectorAll(selector)
for (let element of domList) {
if (!element.__nav_level) {
delete element.__nav_except
continue
}
let pushList = list
while (element.__nav_level > 0) {
pushList = pushList.length ? pushList[pushList.length - 1].children : null
if (!pushList) break
element.__nav_level--
}
let data = {
title: element.textContent,
children: [],
id: createLinkElement(element)
}
pushList && pushList.push(data)
delete element.__nav_level
}
return list
}
4、调用导航数据生成函数并通过回调传给组件。
指令部分代码如下:
export default {
bind (el, binding, vNode) {
el.__navigationGenerateFunction = () => {
if (el.__generating) return
let selectors = binding.value.selectors || ['h1', 'h2']
let exceptSelector = binding.value.exceptSelector
el.__generating = true
let list = []
generateNavTree(el, selectors, exceptSelector, list)
binding.value.callback(list)
vNode.context.$nextTick(() => {
delete el.__generating
})
}
},
inserted (el, binding, vNode) {
el.__navigationGenerateFunction && el.__navigationGenerateFunction()
},
componentUpdated (el, binding, vNode) {
el.__navigationGenerateFunction && el.__navigationGenerateFunction()
},
unbind (el, binding, vNode) {
clearLinkElement()
if (el.__navigationGenerateFunction) {
delete el.__navigationGenerateFunction
}
}
}
5、导航数据的展示
这里我就使用这个组件来展示,下面是一个完整的示例:
<template>
<div class="hello">
<div v-outline="{
callback: refreshNavTree,
selectors: ['h1', 'h2'],
exceptSelector: '[un-nav]'
}" class="content">
<div>
<h1>一级标题1h1>
<div :style="{ margin: '.5rem 2rem' }">内容不出现在导航div>
<h2>二级标题h2>
<div :style="{ margin: '.5rem 2rem' }">内容不出现在导航div>
div>
div>
<div class="navigation">
<div class="title">导航目录div>
<simple-tree
:treeData="navTree"
:expand="false"
class="tree">
<div slot-scope="{ data, parentData }">
<div
class="node-render-content"
@click.stop="jumpToAnchor(data.id)">
{{ data.title }}
div>
div>
simple-tree>
div>
div>
template>
<script>
export default {
data () {
return {
navTree: []
}
},
methods: {
refreshNavTree (treeData) {
this.navTree = treeData
},
jumpToAnchor (id) {
let element = document.getElementById(id)
if (element) {
element.scrollIntoView({ behavior: 'smooth', block: 'start', inline: 'nearest' })
}
}
}
}
script>
四、npm插件
评论