手写一个文章目录插件。
- 兼容博客园 markdown 和 TinyMCE 编辑器
- 给标题添加活跃样式
- 可选的固定位置
插件的配置
catalog: {
enable: true,
position: 'left',
},
- enable 是否启用
- position 目录固定的位置
- left 固定在左侧
- right 固定在右侧
- sidebar '类似掘金文章目录固定效果的效果'
代码结构
import { pageName, userAgent, hasPostTitle, getClientRect, throttle } from '@tools'
const { enable, position } = opts.catalog
// 在这里写几个 func
function catalog() {
// 在入口处做一些基本的判断
if (conditions) return
// 在这里执行上面的一些 func
}
// 导出插件
export default catalog
import
- pageName 返回当前页面名称,如果不是文章详情页则不必执行代码
- userAgent 返回用户客户端类型,移动端无需文章目录
- hasPostTitle 返回当前文章是否存在文章标题
- getClientRect 返回元素相对与浏览器视口的位置
构建 html
思路:遍历博客园随笔内容子元素 DOM,通过正则表达式获取标题,创建目录 html 元素,并添加锚点链接。由于非 markdown 编辑器的标题没有 id,需要在遍历时添加 id,其值即为标题。有些情况下,非 markdown 编辑器的标题内容可能不直接被 h123 标签所嵌套, 判断处理即可。
function build() {
let $catalogContainer = $(
`<div id="catalog">
<div class='catListTitle'><h3>目录</h3></div>
</div>`
)
const $ulContainer = $('<ul></ul>')
const titleRegExp = /^h[1-3]$/
$('#cnblogs_post_body')
.children()
.each(function () {
if (titleRegExp.test(this.tagName.toLowerCase())) {
let id
let text
if (this.id !== '') {
// 如果没有标题上没有id属性,说明不是markdown编辑器
id = this.id
text = this.childNodes.length === 2 ? this.childNodes[1].nodeValue : this.childNodes[0].nodeValue
} else {
if (this.childNodes.length === 2) {
// 从length === 2 开始判断,因为标题中插入了一个 svg icon
const value = this.childNodes[1].nodeValue
text = value ? value : $(this.childNodes[1]).text()
} else {
const value = this.childNodes[0].nodeValue
text = value ? value : $(this.childNodes[0]).text()
}
id = text.trim()
$(this).attr('id', id)
}
const title = `
<li class='${this.nodeName.toLowerCase()}-list'>
<a href='#${id}'>${text}</a>
</li>
`
$ulContainer.append(title)
}
})
$($catalogContainer.append($ulContainer)).appendTo('#sideBar')
setCatalogPosition()
}
固定目录
接下来根据用户配置的 position,将目录固定在指定位置。
function setCatalogPosition() {
const actions = {
sidebar: () => {
setCatalogToggle()
},
left: () => {
$('#catalog').addClass('catalog-sticky-left')
},
right: () => {
$('#catalog').addClass('catalog-sticky-right')
},
}
actions[position]()
}
可以采用更为简洁的写法, 这里考虑到扩展性。
处理固定在侧栏的情况
目录固定在侧栏时,原来的侧边栏滚动到不可见位置才显示目录,很简单,我们只需要监听滚动事件,获取原侧栏相对于视口的高度,当它超出屏幕,即高度小于0(此处使用小于10),则固定目录。反之,则相反。
function setCatalogToggle() {
if (position !== 'sidebar') return
var p = 0,
t = 0
$(window).scroll(
throttle(
function () {
const bottom = getClientRect(document.querySelector('#sideBarMain')).bottom
if (bottom <= 0) {
$('#catalog').addClass('catalog-sticky')
p = $(this).scrollTop()
t <= p ? $('#catalog').addClass('catalog-scroll-up') : $('#catalog').removeClass('catalog-scroll-up')
setTimeout(function () {
t = p
}, 0)
} else {
$('#catalog').removeClass('catalog-sticky')
}
},
50,
1000 / 60
)
)
}
给标题添加活跃样式
这一步实现思路和处理固定在侧栏的情况基本一致,当一个文章目录超出视口时,我们给对应的标题添加活跃的样式就可以了。
function setActiveCatalogTitle() {
$(window).scroll(
throttle(
function () {
for (let i = $('#catalog ul li').length - 1; i >= 0; i--) {
const titleId = $($('#catalog ul li')[i]).find('a').attr('href').replace(/[#]/g, '')
const postTitle = document.querySelector(`#cnblogs_post_body [id='${titleId}']`)
if (getClientRect(postTitle).top <= 10) {
if ($($('#catalog ul li')[i]).hasClass('catalog-active')) return
$($('#catalog ul li')[i]).addClass('catalog-active')
$($('#catalog ul li')[i]).siblings().removeClass('catalog-active')
return
}
}
},
50,
1000 / 60
)
)
}
如有错误或不足,欢迎指正!