文章页面目录自动生成方案

一、前言
前两天项目遇到一个需要给页面添加大纲导航的功能,要求把页面中的特定标签加入到大纲导航中。类似这样:

二、实现思路
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 || documentlet 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) returnelement.__nav_level = idx})}let selector = selectors.join(',')let domList = dom.querySelectorAll(selector)for (let element of domList) {if (!element.__nav_level) {delete element.__nav_exceptcontinue}let pushList = listwhile (element.__nav_level > 0) {pushList = pushList.length ? pushList[pushList.length - 1].children : nullif (!pushList) breakelement.__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) returnlet selectors = binding.value.selectors || ['h1', 'h2']let exceptSelector = binding.value.exceptSelectorel.__generating = truelet 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 }"><divclass="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插件
最后
欢迎加我微信(winty230),拉你进技术群,长期交流学习...
欢迎关注「前端Q」,认真学前端,做个专业的技术人...


评论
