1、效果描述
在富文本编辑器中将某段落设置成H1-H6任意标题时,实时在左侧【目录导航】中生成目录,取消任意H1-H6标签时,同步更新左侧目录导航。此目录只为做快速定位跳转至富文本标题位置处 (若需要生成类似doc目录树,则需要标准的目录录入限制或者有标识确定嵌套关系) --- 仅供相互学习,讨论
2、实现思路
- 利用监听dom节点变化的
- 把我们需要观察的节点(H1-H6)筛选出来
- 分别给筛选出来的节点(H1-H6)添加上自定义属性,方面定位该元素的位置
- 把节点列表渲染到左侧目录导航
- 点击目录导航时回传自定义属性到富文本中,进行滚动定位到节点位置
3、实现步骤流程
(1)vue 中实例化时创建观察器
// 获取iframe的Document 对象
get iframeDocument() {
const iframe = (document.querySelector('#tinymce_ifr') as any)
const iframeDoc = iframe.contentDocument.querySelector('#tinymce')
return iframeDoc
}
mounted() {
// 创建节点变化观察器
this.mutationObserverInstall()
// 初始化闭包函数,用于生成唯一的数字作为自定义属性
this.catalogUniqueNumber = this.generateUniqueNumber()
// 初始化时,先全文查找h1-h6标签并设置自定义属性
setTimeout(() => {
this.findH1ToH6Nodes()
}, 500)
}
// 生成唯一的数字
generateUniqueNumber() {
var num = 0
return function() {
num += 1
return num
}
}
(2)vue组件更新时触发观察器
// 页面更新时
updated() {
// 配置观察选项
const config = {
// 观察 attributes 改变
attributes: true,
// 指定观察属性
attributeFilter: ['id'],
// 观察节点增删
childList: true,
// 观察文本改变
characterData: true,
// 观察子节点树
subtree: true
}
// 关闭观察器
this.observer.disconnect()
// 观察节点变化
this.observer.observe(this.iframeDocument, config)
}
(2.1)监听dom节点变化函数
// 创建观察器
mutationObserverInstall() {
const list = ['H1', 'H2', 'H3', 'H4', 'H5', 'H6']
let catalogList: any[] = []
this.observer = new MutationObserver((mutations) => {
mutations.forEach((mutation: any) => {
// console.log('mutation', mutation)
// 过滤出 h1-h6
const childrens = _.filter(mutation.target.children, item => _.includes(list, item.nodeName))
// 移除 h1-h6 中的一个
const removedNodes = _.some(mutation.removedNodes, item => _.includes(list, item.nodeName))
// childrens.length > 1 ,此时会全量更新h1-h6的节点
if (!_.isEmpty(mutation.addedNodes) && !_.isEmpty(childrens)) {
// 先清空
catalogList = []
console.log('childrens', childrens)
// 给元素添加自定义属性
_.forEach(childrens, child => {
const foundName = _.find(child.attributes, { name: 'catalogname' })
// 未添加自定义属性的目录
if (!foundName) {
const name = `catalog_${this.catalogUniqueNumber()}`
child.setAttribute('catalogName', name)
catalogList.push({
catalogName: name,
nodeName: child.nodeName,
innerHTML: child.innerHTML,
})
}
// 已经添加了自定义属性
else {
catalogList.push({
catalogName: foundName.value,
nodeName: child.nodeName,
innerHTML: child.innerHTML,
})
}
})
this.$emit('showCatalog', catalogList)
}
// childrens.length = 0 , 检查到全文中没有h1-h6, 则清空目录列表
if (removedNodes && _.isEmpty(childrens)) {
const catalogs = this.iframeDocument.querySelectorAll('h1,h2,h3,h4,h5,h6')
// 清空目录列表
if (_.isEmpty(catalogs)) this.$emit('showCatalog', [])
}
})
})
}
(4)在富文本中找到对应的节点及滚动到对应的位置
// 目录id
@Watch('activeCatalogName')
onactiveCatalogNameChange() {
const titleNode = this.iframeDocument.querySelector(`[catalogname=${this.activeCatalogName}]`)
console.log('titleNode', titleNode)
const top = titleNode.offsetTop
// 设置滚动条滚动到对应的位置
// iframe的window对象
;(document.getElementById('tinymce_ifr') as any).contentWindow.scrollTo(0, top)
}
(5)查找h1-h6标签并设置自定义属性函数
// 查找所有h1-h6标题节点
findH1ToH6Nodes() {
// 获取h1-h6的所有节点
const childrens = this.iframeDocument.querySelectorAll('h1,h2,h3,h4,h5,h6')
if (_.isEmpty(childrens)) return
const catalogList: any[] = []
// 保留只有id锚点的节点
_.forEach(childrens, (child: any) => {
const name = `catalog_${this.catalogUniqueNumber()}`
child.setAttribute('catalogName', name)
catalogList.push({
catalogName: name,
nodeName: child.nodeName,
innerHTML: child.innerHTML,
})
})
if (!_.isEmpty(catalogList)) this.$emit('showCatalog', catalogList)
}