t_html_toc
Version:
toc html_toc word js paragraph
280 lines (272 loc) • 11 kB
JavaScript
const Default_selectors = ['h1', 'h2', 'h3', 'h5', 'h6']// 默认的选择器
const LevelKey = '_html_toc_level'// dom节点以及生成的数据项 保存层级的字段名
const nodeKey = '_html_toc_node'// 生成的数据保存原始dom节点的字段名
const parentKey = '_html_toc_parent'// 生成的数据,树形保存父级节点的字段名
const containerActiveTocItemKey = '_html_old_active_toc'// 挂载toc的容器上保存上一次高亮的toc dom元素的字段名
const containerClickKey = '_htmlClick'// 挂载toc的容器上保存点击事件的字段名
const tocItemClassPre = 'html_toc_node html_toc_node_level_'// 生成的toc 元素节点所包含的className的前缀,
const tocNodeKey = '_html_toc_node_data'//toc 元素上保存 生成的toc数据的字段名
const DefaultOptions = { // 生成数据的相关配置 new HtmlToc(root,OPTION) 这里的option
titleKey: "title",// 生成数据保存原始节点文本内容的字段
nodeToTitle: node => node.innerText,// 匹配到节点,获取toc 文本内容的函数,可以自有获取,默认是获取innerText
childrenKey: "children",// 树形数据保存子节点的字段
selecters: Default_selectors,// 指定 生成toc需要抽取的dom选择器列表,列表顺序即是最终生成的level层级,只支持 .xxx tag id 不支持嵌套
clearEmptyChildren: true,// 树形数据是否去掉 children=[] 是的children字段
clearParent: false// 树形数据是否保留parent,用以规避在某些外部插件生成树时内存溢出,目前测试的 jq的jsTree需要去掉parent
}
const DefaultMountTocOptions = {// 生成toc的相关配置 mountToc(container,OPTION) 中的option
scrollbehavior: 'smooth',// 内部是调用的dom的 scrollIntoView实现页面滚动,该字段是传递给此函数的 behavior
scrollParams: null,// 内部是调用的dom的 scrollIntoView实现页面滚动,该字段是传递给此函数的 option
isChildrenHiddenKey: "hiddenChildren",// 当前toc的子级toc是否处于隐藏状态的属性
isHiddenKey: "hidden",//toc节点自身是否隐藏的属性
isActiveKey: "active",// toc节点是否激活的属性
autoToggleChildren: false,// 是否启动子节点的显示隐藏操作
clickHanle: null// toc的点击事件滚动处理函数,传递了就不会自动处理了
}
class HtmlToc {
constructor(root, options) {
options = Object.assign({}, DefaultOptions, options)
const { titleKey, nodeToTitle, childrenKey, clearEmptyChildren, clearParent } = options
this.$options = options
this.$root = this.initRoot(root)
this.$selectors = this.initSelectors()
this.$titleKey = titleKey
this.$nodeToTitle = nodeToTitle
this.$childrenKey = childrenKey
this.$clearEmptyChildren = clearEmptyChildren
this.$clearParent = clearParent
this.updateData()
}
initRoot(root) {
return this.parseSelector(root)
}
parseSelector(selecter) {
if (selecter.nodeType === 1) return selecter
if (/^\#/.test(selecter)) return document.getElementById(selecter.slice(1))
if (/\^\./.test(selecter)) return document.getElementsByClassName(selecter.slice(1))[0]
return document.getElementsByTagName(selecter)[0]
}
initSelectors() {
const selecters = (this.$options.selecters || Default_selectors).filter(Boolean)
return selecters.map(i => {
const temp = {
id: "",
className: "",
tag: ""
}
if (/^\./.test(i)) temp.className = i.slice(1)
else if (/^\#/.test(i)) temp.id = i.slice(1)
else temp.tag = i.toUpperCase()
return temp
})
}
loopChild(node) {
if (!node) return
const isTarget = this.isTargetNode(node)
if (isTarget) this.$targetList.push(node)
let children = [...node.children]
children.forEach(n => this.loopChild(n))
}
isTargetNode(node) {
let isTarget = false, _htmlTocLevel = -1
for (let i = 0; i < this.$selectors.length; i++) {
const { id, className, tag } = this.$selectors[i]
if ((id && id === node.id) || (tag && tag === node.tagName) || (className && node.className && node.className.includes(className))) {
isTarget = true
_htmlTocLevel = i
}
if (isTarget) {
node._isHtmlToc = true
node[LevelKey] = _htmlTocLevel
return true
}
}
return false
}
getPlatData() {
return this.createPlatData()
}
getTreeData() {
return this.createTreeData()
}
//从新获取文章内容的标题节点
updateData() {
if (!this.$root) {
console.log('root不能为空',)
return
}
this.$targetList = []
this.loopChild(this.$root)
}
// 这两个函数是给 匹配到的文章内容的对应标题节点添加和移除事件
addEvent(eventObj = {}) {
this.$targetList.forEach(node => {
Object.keys(eventObj).forEach(key => {
node.addEventListener(key, eventObj[key], false)
})
})
}
removeEvent(eventObj = {}) {
this.$targetList.forEach(node => {
Object.keys(eventObj).forEach(key => {
node.removeEventListener(key, eventObj[key], false)
})
})
}
createPlatData() {
return this.$targetList.map(node => ({ [LevelKey]: node[LevelKey], [nodeKey]: node, [this.$titleKey]: this.$nodeToTitle(node) }))
}
createTreeData() {
let rootList = []
let plathtmlTocNodes = []
let tmpNode = null
this.$targetList.forEach(node => {
let nodeLevel = node[LevelKey]
let curNode = { [LevelKey]: nodeLevel, [this.$titleKey]: this.$nodeToTitle(node), [nodeKey]: node, [this.$childrenKey]: [] }
plathtmlTocNodes.push(curNode)
if (!tmpNode) {
tmpNode = curNode
rootList.push(tmpNode)
} else {
if (nodeLevel === tmpNode[LevelKey]) {
// 层级相同,往共同的父级添加子节点即可
if (tmpNode[parentKey]) {
// 如果父级存在
tmpNode[parentKey][this.$childrenKey].push(curNode)
tmpNode = tmpNode[parentKey]
curNode[parentKey] = tmpNode
} else {
// 父级不存在则存到顶级节点中
rootList.push(curNode)
tmpNode = rootList[rootList.length - 1]
}
// 如果层级比前一个高 那么就是其子节点
} else if (nodeLevel > tmpNode[LevelKey]) {
tmpNode[this.$childrenKey].push(curNode)
curNode[parentKey] = tmpNode
}
// 如果层级比前一个低,则不是其子节点,需要向上找父节点
else if (nodeLevel < tmpNode[LevelKey]) {
while (tmpNode && tmpNode[LevelKey] >= nodeLevel) {
tmpNode = tmpNode[parentKey]
}
if (!tmpNode) {
tmpNode = curNode
rootList.push(tmpNode)
} else {
// 如果存在 那么此时 tmpNode的层级 < curNode的层级, tmpNode是父节点
tmpNode[this.$childrenKey].push(curNode)
}
}
}
})
plathtmlTocNodes.forEach(node => {
if (this.$clearEmptyChildren && !node[this.$childrenKey] || node[this.$childrenKey].length === 0) {
delete node[this.$childrenKey]
}
if (this.$clearParent) {
delete node[parentKey]
}
})
return rootList
}
mountToc(container, options = {}) {
this.containers = this.containers || []
const con = this.parseSelector(container)
this.generateToc(con, Object.assign({}, DefaultMountTocOptions, options))
this.containers.push(con)
}
destory() {
if (this.containers) {
this.containers.forEach(con => {
con.innerHTML = null
con.removeEventListener('click', con[containerClickKey], false)
delete con[containerClickKey]
delete con[containerActiveTocItemKey]
})
}
}
generateToc(container, options) {
container.innerHTML = null
let nodes = this.getPlatData()
nodes.forEach(node => {
const div = document.createElement('div')
div.innerText = node[this.$titleKey]
div.className = `${tocItemClassPre}${node[LevelKey]}`
div[tocNodeKey] = node
container.appendChild(div)
})
const { scrollbehavior,
isChildrenHiddenKey,
isHiddenKey,
isActiveKey, autoToggleChildren, clickHanle, scrollParams } = options
function containerClick(e) {
if (!e.target[tocNodeKey] || !e.target[tocNodeKey][nodeKey]) {
// toc上都有这个字段,没有的说明不是toc
return
}
try {
const tocNode = e.target// 指向点击的toc节点
const target = e.target[tocNodeKey][nodeKey]// 指向toc所对应的 文章内容的标题实际dom节点
const userClickHandle = clickHanle
if (userClickHandle) {
userClickHandle(tocNode, target)
} else {
target.scrollIntoView(scrollParams || {
behavior: scrollbehavior
})
}
if (autoToggleChildren) {
const hiddenChild = toggleAttr(tocNode, isChildrenHiddenKey)
let nextToc = tocNode
const curLevel = getTocLevel(tocNode)
while (nextToc && nextToc.nextSibling) {
nextToc = nextToc.nextSibling
const nextLevel = getTocLevel(nextToc)
// 为0 说明是顶级toc 为null说明不是toc 否符合终止循环条件
if (!nextLevel) break
if (nextLevel > curLevel) {
updateAttr(nextToc, isHiddenKey, hiddenChild)
} else if (nextLevel <= curLevel) {
break
}
}
}
// 如果存在 高亮节点,则先取消其高亮状态,在激活当前点击的toc高亮 当前设计只允许同时高亮一个toc节点
if (container[containerActiveTocItemKey]) {
updateAttr(container[containerActiveTocItemKey], isActiveKey, null)
}
updateAttr(tocNode, isActiveKey, true)
// 存储当前toc为高亮节点
container[containerActiveTocItemKey] = tocNode
} catch (error) {
console.log('自动处理节点出错', error)
}
}
container.addEventListener('click', containerClick, false)
container[containerClickKey] = containerClick
}
}
function getTocLevel(node) {
try {
return node[tocNodeKey][LevelKey]
} catch (error) {
return null
}
}
function toggleAttr(node, key) {
let has = node.hasAttribute(key)
newVal = !has
updateAttr(node, key, newVal)
return newVal
}
function updateAttr(node, key, val) {
if (val) {
node.setAttribute(key, val)
} else {
node.removeAttribute(key)
}
}
if (typeof module !== "undefined") {
module.exports = HtmlToc
}