演示地址http://81.68.192.120/blog/88
以前的目录只是用JQuery拿到了所有mavon-editor内容里的h2标签,把这些标签的内容显示在了页面上,这样肯定是比较简陋的。一篇文章的目录结构应该是树形的。mavon-editor提供了两种拿到目录的方法,但是这两种都不能在组件外进行目录显示,一种是在文章顶部,和文章成为一部分,第二种是直接嵌入了文章内部。这两种都是比较差的选择,目录最好还是脱离文章,同时位置固定。
这几天接触了一下el-tree,突然想到这个树形结构其实用来做目录是再合适不过。不过问题在于,从mavon-editor拿到的标签虽然有顺序,但是是没有子父层级关系的。所以,需要手动去构建这种关系。简单来说就是构建一个数组,数组里有标签的对象,对象里有个属性就是它自己这个类型的对象。这个算法应该实现起来各有千秋,但是大多都是递归。我的做法也是,不过,因为考虑到文中不会用一级标题(一级标题一般作为文章的题目,文章的小标题一般是二级标题开始),所以我是如此实现的:
tocAndCli() { this.$nextTick(() => { const aArr1 = $( ".v-note-wrapper a" ).toArray(); let aArr = [] aArr1.forEach(item => { if (item.id) { aArr.push(item) } }) //给数据赋值,保存元素的id和其距顶部的距离 this.tocAndDist(aArr) let toc = []; aArr.forEach((item, index) => { let href = $(item).attr("id"); let name = $(item).parent().text(); let prop = $(item).parent().prop('nodeName'); let children = this.getChildren(aArr, item, index) if (href && prop === 'H2') { // 这里判断是因为我们只需要有id的内容,没有id的则过滤掉。 toc.push({ id: href.substring(href.lastIndexOf("_") + 1), href: "#" + href, name, prop, children }); this.catalog = true } }); this.toc = toc }); clipboard = new Clipboard(".copy-btn"); this.$nextTick(() => { // 复制成功失败的提示 clipboard.on("success", () => { this.$message.success("复制成功"); }); clipboard.on("error", () => { this.$message.error("复制失败"); }); }); }, tocAndDist(arr) { arr.forEach(item => { if ($(item).attr("id")) { let id = item.id.substring(item.id.lastIndexOf("_") + 1) let dist = $('#' + item.id).offset().top; this.catalogue.push({id, dist}) } }) },
首先用JQuery找到组件里的所有a标签,再过滤一次,只要有id的a标签。然后算出标签距浏览器顶部的距离,保存JSON对象形如{id: 1, dist: 100}这种形式到data里的一个数组里。id就是el-tree的节点id,我这里是取了a标签id的后几位,在最后一个“_”以后的部分,这样可以保证不重复。
关键在于第25行这个children的设置:
getChildren(aArr, item, index) { let out = [] if (index === aArr.length - 1) { return [] } let nodeName = $(aArr[index]).parent().prop('nodeName') let level = parseInt(nodeName.substring(1, 2).charAt(0)) if ($(aArr[index + 1]).parent().prop('nodeName') === nodeName) { return [] } for (let i = index + 1; i < aArr.length; i++) { let name = $(aArr[i]).parent().prop('nodeName') if (level + 1 === parseInt(name.substring(1, 2))) { //构建孩子 let href = $(aArr[i]).attr("id"); let name = $(aArr[i]).parent().text(); let children = this.getChildren(aArr, aArr[i], i) out.push({ //lastIndexOf是因为有些标签有多个_,取最后为id id: href.substring(href.lastIndexOf("_") + 1), href: "#" + href, name, children }) } else if (level < parseInt(name.substring(1, 2)) - 1) { continue } else { break } } return out },
核心判断条件就是,例如,如果h2的下一个标签是h3,那么h3就是他的孩子节点,但是如果h3的下一个是h4,这个h4就不是h2的孩子节点,而是h3是h2的孩子,h4是h3的孩子,如此构建。如果h2后面还是一个h2,那么这个h2就没有孩子节点。第26行是一个递归,要找孩子节点的孩子节点,老生常谈。
最后构建好数据以后,就可以放到el-tree标签:
<el-tree :data="toc" :props="defaultProps" highlight-current accordion :indent="10" node-key="id" @node-click="scrollToPosition" ref="menuTree"> </el-tree>
标签的单击事件是跳转到对应的标题:
scrollToPosition(data) { let id = data.href const position = $(id).offset(); position.top = position.top - 40; $("html,body").animate({ scrollTop: position.top }, 500); },
有JQuery以后,这种平滑的跳转就很好实现。但是,在Vue里集成JQuery,框架里套框架,再操作dom,看起来并不是一个很轻的选择。
除了这些,考虑到gitbook类型的书一般都会有一个目录,这个目录是会随着浏览器的滚动自动折叠和展开的,也会自动标亮现在在阅读的章节。这个如何实现呢?想到gitbook其实每个小标题都是有锚点的,我这个小标题虽说也手动标记了锚点,但是毕竟和gitbook用的完全不同的技术,我也没有研究过其源码的(研究了估计也短时间看不懂),重要的是想到思路。
要做自动展开和折叠,一是要考虑el-tree的展开、折叠、标亮这三个事件,二是要考虑监听浏览器的滚动,浏览器滚动的时候去执行展开、折叠、标亮对应的方法。查了一些资料以后,形成了一个思路,如下:
之前已经构建了小标题距离浏览器顶部的距离和节点id的关系数组,用浏览器目前顶端位置距离浏览器顶部的距离依次减去这个数组距离的每一项,第一项为正的,就是节点对应的章节。
于是代码就是这样:
mounted() { window.addEventListener('scroll', this.roll) },
在mounted()里进行滚动监听,一旦浏览器滚动就执行roll方法:
roll() { let scrolled = document.documentElement.scrollTop || document.body.scrollTop let arrCopy = JSON.parse(JSON.stringify(this.catalogue)) arrCopy.forEach(item => { item.dist = scrolled - item.dist }) let temp arrCopy.forEach((item, index) => { if (item.dist > 0) { temp = item return } }) try { let nodes = this.$refs.menuTree.store._getAllNodes(); for (let i in nodes) { if (nodes[i].data.id === temp.id) { nodes[i].expanded = true this.$refs.menuTree.setCurrentKey(nodes[i].data.id) this.expand(nodes[i]) } else { nodes[i].expanded = false } } } catch (e) { } },
通过this.$refs.menuTree.store._getAllNodes()可以拿到el-tree节点的相关信息,这个数组每一项里的data属性里包含了是自行设置的id,这个id和选择出的节点id相等的时候,就调用nodes[i].expanded = true将该节点展开。但是,在el-tree里,如果只展开了子节点,没有展开父节点,那也是展不开的,这时候需要调用方法把这个节点的所有父节点都展开,对应上面的29行expand():
expand(node) { if (node && node.parent) { node.parent.expanded = true } if (node.parent) this.expand(node.parent) }
递归实现。同时,滚动的时候由于只展开了节点和对应的父节点,其他节点都主动进行了折叠,也就是进行了必要的考虑。