文章目录构建思路分享

189 阅读2分钟

概览

本文主要是关于最近开发文章附带目录的开发思路分析记录。常见参考案例

开发过程主要分为以下三部分:

  1. 点击转跳的实现。
  2. 高亮跟随的实现。
  3. 目录数据结构的构建。

一、点击转跳的实现

  1. 常见锚点实现参考案例

image.png 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(默认) 
}
  1. 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,如下

image.png 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
        }
    }
})

三、目录数据结构的构建

最重要的是构建由各个标题标签元素按标题层级构成的嵌套数组,如:

image.png 代码实现如下:

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树形控件实现目录结构,如下:

image.png