概览
本文主要是关于最近开发文章附带目录的开发思路分析记录。常见参考案例
开发过程主要分为以下三部分:
- 点击转跳的实现。
- 高亮跟随的实现。
- 目录数据结构的构建。
一、点击转跳的实现
- 常见锚点实现参考案例
2. js转跳 Element.scrollintoView()(让指定元素滚动到可视区域)
// 接受一个布尔值或者一个对象
el.scrollIntoView() // true-可见区域顶部,false-可见区域底部
// 或者
el.scrollIntoView(options)
{
behavior // 过渡动画 instant smooth auto(默认)
block // 可选 start center end nearest(默认)
inline // 可选 start center end nearest(默认)
}
- jq动画实现
// 元素距离文档顶部顶部偏移量
console.log('$(headerId).offset().top===', $(headerId).offset().top)
// 文章包裹元素滚动到指定位置
$('.course_content').animate(
{ scrollTop: $(headerId).offset().top },
{ duration: duration, easing: 'swing' }
)
tips: 如果有包裹元素注意需要处理包裹元素的滚动偏移量
// 把包裹区的滚动条垂直偏移量算上
const top = $('.course_content').scrollTop()
// 元素距离文档顶部顶部偏移量
console.log('$(headerId).offset().top===', $(headerId).offset().top)
$('.course_content').animate(
{ scrollTop: $(headerId).offset().top + top },
{ duration: duration, easing: 'swing' }
)
二、高亮跟随的实现
1、api介绍
element.getBoundingClientRect() 返回当前元素在浏览器中距离可视区域左上角的距离以及自身的width和height,如下
2、实现分享
监听文章包裹区的滚动,同时循环判断各个标题元素是否进入了自定义的高亮范围
$('#article').scroll(() => {
const headers = $('#article').find(':header')
const headersLength = headers.length
for (let i = 0; i < headersLength; i++) {
const id = $(headers.get(i)).attr('id')
const el = document.getElementById(id)
const top = el.getBoundingClientRect().top
// 自定义高亮范围
if (top < 130 && top > 80) {
setCatalogueSelectedKey([id]) // 激活目录点亮的标题
return
}
}
})
三、目录数据结构的构建
最重要的是构建由各个标题标签元素按标题层级构成的嵌套数组,如:
代码实现如下:
import React, { FC, ReactElement, useCallback, useEffect, useState, useReducer } from 'react'
export const Home: FC = (): ReactElement => {
// 获取h*元素
function selectHeadings () {
const contentElement = document.querySelector('.js-toc-content')
var selectors = 'h1, h2, h3, h4, h5, h6'
const nodes = contentElement.querySelectorAll(selectors)
return nodes
}
function getLastItem(array: any) {
return array[array.length - 1]
}
function getHeadingObject (node: any) {
const obj = {
nodeName: node.nodeName,
headingLevel: +node.nodeName.toUpperCase().replace('H', ''),
textContent: node.innerHTML,
children: []
}
return obj
}
// 当前数组最后一个元素比较等级差,同等或大于则插入,小于则在最后元素的children继续寻找
const addNode = (obj: any, nest: any[]) => {
var array = nest
var level = obj.headingLevel // 当前节点等级
var lastItem = getLastItem(array) // 获得当前数组最后一个节点信息
var lastItemLevel = lastItem
? lastItem.headingLevel
: 0
var counter = level - lastItemLevel // 等级差
// counter > 0 低等级 不断寻找lastItem以及其children中插入
while (counter > 0) {
lastItem = getLastItem(array)
if (lastItem && level === lastItem.headingLevel) {
break
} else if (lastItem && lastItem.children !== undefined) {
array = lastItem.children // 更新被插入的数组,存在lastItem时,children默认[]
}
counter--
}
// counter <= 0 同级插入
array.push(obj)
return array
}
function nestHeadingsArray (headingsArray: any) {
// array.reduce(function(prev, currentValue, currentIndex, arr), initialValue)
// prev 上一次调用的返回值
const reduce = [].reduce
return reduce.call(headingsArray, function reducer (nestArr: any, cur: any) {
var currentHeading = getHeadingObject(cur)
if (currentHeading) {
addNode(currentHeading, nestArr)
}
return nestArr
}, [])
}
useEffect(() => {
// 获取所有标题节点
const headers = selectHeadings()
// 构建嵌套数组
const arr = nestHeadingsArray(headers)
console.log(arr)
}, [])
return (
<div>
<h1>1 主标题</h1>
<h2>1.1 痛点333</h2>
<h2>1.2 价值333</h2>
<h3>1.2.1 痛点444</h3>
<h2>1.3 价值444</h2>
</div>
)
}
获得嵌套数组后可以使用antd的Tree树形控件实现目录结构,如下: