Hey! Hello. 🍰 晓风乾,泪痕残。

2025-02-21 更新 60 阅读

在阿峰那里看到这篇不需要插件,仅仅用 css 与 js 即可实现文章目录功能,收藏备用,兴许以后用得上。#瞎折腾/主题制作

原文:前端实现文章目录功能-link

效果图

512x384-625740122.webp
698x732-2848086689.png

CSS 代码

/** 
    前端文章目录 - css
    author:     阿锋
    link:       https://feng.pub
    version:    0.0.3
*/
.content_toc {
    display: block;
    width: 200px;
    overflow: hidden;
    overflow-y: auto;
    position: fixed;
    bottom: 164px;
    right: 15px;
    padding: 0px;
    margin: 0px;
    background-color: var(--theme-palette-color-8);
    border-radius: 5px;
    box-shadow: 0 0 10px var(--theme-palette-color-1);
    scrollbar-width: none;
    -ms-overflow-style: none;
    z-index: 1;
    transition: all 0.5s;
}

.content_toc::-webkit-scrollbar {
    display: none;
}

.content_toc.less {
    width: 40px;
    height: 40px;
    color: var(--theme-palette-color-1);
    background-color: var(--theme-palette-color-1);
    border-radius: 20px;
    box-shadow: none;
    overflow: hidden;
}

.content_toc.less .content_toc_title h6,
.content_toc.less .content_toc_main {
    display: none;
}

.content_toc_title {
    position: relative;
    top: 0;
    left: 0;
    right: 0;
    height: 40px;
    padding: 0;
    margin: 0;
    transition: all 0.5s;
}

.content_toc_title h6 {
    padding-left: 10px;
    color: var(--theme-palette-color-1);
    font-size: 18px;
    font-weight: 700;
    line-height: 40px;
}

.content_toc_title h6::before {
    content: '|';
    margin-right: 10px;
    color: var(--theme-palette-color-1);
    background: var(--theme-palette-color-1);
    border-radius: 5px;
}

.content_toc_title .btn {
    position: absolute;
    width: 20px;
    height: 20px;
    top: 10px;
    right: 10px;
    line-height: 20px;
    font-weight: bold;
    color: var(--theme-palette-color-7);
    background-color: var(--theme-palette-color-1);
    text-align: center;
    font-size: 12px;
    border-radius: 50%;
    box-shadow: 0 0 10px var(--theme-palette-color-6);
    cursor: pointer;
    transition: all 0.5s;
}

.content_toc_title .btn:hover,
.content_toc_title .btn.close:hover {
    color: var(--theme-palette-color-8);
    background-color: var(--theme-palette-color-2);
    box-shadow: 0 0 10px var(--theme-palette-color-1);
}

.content_toc_title .btn.close {
    top: 0;
    right: 0;
    width: 40px;
    height: 40px;
    line-height: 40px;
    border-radius: 20px;
    color: var(--theme-palette-color-7);
    background-color: var(--theme-palette-color-1);
}

.content_toc_main {
    padding: 0;
    margin: 0;
    overflow-y: auto;
    transition: all 0.5s;
}

.content_toc_main::-webkit-scrollbar {
    display: none;
}

.content_toc_tree {
    padding: 0 10px 10px 10px;
    margin: 0;
    list-style: none;
}

.content_toc_tree li {
    position: relative;
    width: 170px;
    font-size: 14px;
    padding: 0px;
    margin-left: 10px;
    border-radius: 5px;
    color: var(--theme-palette-color-3);
    cursor: pointer;
}

.content_toc_tree li.active,
.content_toc_tree li:hover {
    color: var(--theme-palette-color-8);
    background-color: var(--theme-palette-color-1);
}

.content_toc_tree li::before {
    content: '';
    position: absolute;
    top: 12px;
    left: -10px;
    width: 5px;
    height: 5px;
    background-color: var(--theme-palette-color-6);
    border-radius: 50%;
}

.content_toc_tree li.active::before,
.content_toc_tree li:hover::before {
    border-width: 2px;
    background: var(--theme-palette-color-1);
}

.content_toc_tree li.level_H1 {
    padding-left: 5px;
}

.content_toc_tree li.level_H2 {
    padding-left: 5px;
}

.content_toc_tree li.level_H3 {
    padding-left: 1em;
}

.content_toc_tree li.level_H4 {
    padding-left: 2em;
}

.content_toc_tree li.level_H5 {
    padding-left: 3em;
}

.content_toc_tree li.level_H6 {
    padding-left: 4em;
}

@media screen and (max-width: 992px) {
    .content_toc {
        width: 170px;
    }

    .content_toc_tree {
        padding: 0 5px 10px 5px;
    }

    .content_toc_tree li {
        width: 150px;
    }
}

js 代码

/**
 * 前端文章目录 - js
 * 
 * @author 阿锋
 * @link https://feng.pub
 * @version 0.0.3
 */
document.addEventListener("DOMContentLoaded", () => {
    // 配置项
    // 获取内容的标题级别
    const levelTOC = 'h1, h2, h3, h4'
    // 获取到标题个数大于该数时才显示文章目录
    const toShowNum = 3
    // 文章目录块的高度偏移量(可视窗口高度减去该高度为文章目录块的高度)
    const tocOffsetHeight = 235
    // 滚动顶部偏移(滚动窗口时内容标题顶部偏移高度,防止顶部浮动导航遮挡标题)
    const topTOCOffsetHeight = 65
    // 关闭文章菜单后,圆球的高度(根据 css 样式设定)
    const lessHeight = 40
    // 菜单默认状态(1:打开状态;0:关闭状态)
    const defaultState = 1
    // 移动端菜单默认状态
    const mobileDefaultState = 0

    // 当前状态
    const initTOCState = (document.documentElement.clientWidth <= 992) ? mobileDefaultState : defaultState
    // 获取文章内容元素
    const entryContent = document.querySelector('.entry-content')
    // 获取元素中 H1、H2、H3、H4、H5、H6
    const contentHeadings = entryContent.querySelectorAll(levelTOC)
    if (contentHeadings.length >= toShowNum) {
        // 文章目录 HTML 结构
        // TOC
        const contentTOCDiv = document.createElement('div')
        if (initTOCState === 1) {
            contentTOCDiv.className = 'content_toc'
        } else {
            contentTOCDiv.classList = 'content_toc less'
        }
        // TOC title
        const tocTitleDiv = document.createElement('div')
        tocTitleDiv.className = 'content_toc_title'
        contentTOCDiv.appendChild(tocTitleDiv)
        // TOC title h6
        const tocTitle = document.createElement('h6')
        tocTitle.innerText = '文档目录'
        tocTitleDiv.appendChild(tocTitle)
        // TOC title btn
        const contentTOCBtn = document.createElement('div')
        if (initTOCState === 1) {
            contentTOCBtn.className = 'btn'
            contentTOCBtn.innerText = '×'
            contentTOCBtn.setAttribute('title', '关闭文档目录')
        } else {
            contentTOCBtn.classList = 'btn close'
            contentTOCBtn.innerText = '目录'
            contentTOCBtn.setAttribute('title', '打开文档目录')
        }
        tocTitleDiv.appendChild(contentTOCBtn)
        // TOC main
        const contentTOCTreeDiv = document.createElement('div')
        contentTOCTreeDiv.className = 'content_toc_main'
        contentTOCDiv.appendChild(contentTOCTreeDiv)
        // TOC main tree
        const contentTOCTree = document.createElement('ul')
        contentTOCTree.className = 'content_toc_tree'
        contentHeadings.forEach((e, k) => {
            // TOC main tree li
            const toc = document.createElement('li')
            toc.className = 'level_' + e.tagName
            toc.innerText = e.textContent
            toc.dataset.toc = k
            contentTOCTree.appendChild(toc)
        });
        contentTOCTreeDiv.appendChild(contentTOCTree)
        // 追加到 body
        document.querySelector('body').appendChild(contentTOCDiv)

        let currentState = initTOCState
        // 是否需要重设高度
        let needSetHeight = 0

        const action = {
            // 设置文档目录的高度
            setTOCHeight: () => {
                // 标题块高度
                const tocTitleHeight = tocTitleDiv.clientHeight
                // 文档可视高度
                const clientHeight = document.documentElement.clientHeight
                // 文档目录树高度
                const tocTreeHeight = contentTOCTree.clientHeight
                // 文档目录块最大高度
                const maxTOCHeight = clientHeight - tocOffsetHeight
                // 文档目录块初始高度
                const tocHeight = tocTreeHeight + tocTitleHeight > maxTOCHeight ? maxTOCHeight : tocTreeHeight + tocTitleHeight
                // 文档目录块高度带单位 px
                const tocStyleHeight = tocHeight + 'px'
                contentTOCDiv.dataset.height = tocStyleHeight
                contentTOCDiv.style.height = tocStyleHeight
                // 设置文档目录树块的高度
                const tocMainHeight = tocHeight - tocTitleHeight
                contentTOCTreeDiv.dataset.height = tocMainHeight + 'px'
                contentTOCTreeDiv.style.height = tocMainHeight + 'px'
            },
            // 文档目录打开、关闭切换
            toggleTOC: (to) => {
                if (currentState === 0) {
                    // 当前是关闭状态
                    if (to == 'open' || to == undefined) {
                        contentTOCBtn.innerText = '×'
                        contentTOCBtn.setAttribute('title', '关闭文章目录')
                        contentTOCDiv.style.height = contentTOCDiv.dataset.height
                        contentTOCDiv.classList.toggle('less')
                        contentTOCBtn.classList.toggle('close')
                        // 更改状态
                        currentState = 1
                        if (needSetHeight === 1) action.setTOCHeight()
                    }
                } else {
                    // 当前是打开状态
                    if (to == 'close' || to == undefined) {
                        contentTOCBtn.innerText = '目录'
                        contentTOCBtn.setAttribute('title', '打开文章目录')
                        contentTOCDiv.style.height = lessHeight + 'px'
                        contentTOCDiv.classList.toggle('less')
                        contentTOCBtn.classList.toggle('close')
                        // 更改状态
                        currentState = 0
                    }
                }
            }
        }

        // 默认打开状态时需设置高度
        if (initTOCState === 1) {
            action.setTOCHeight()
        } else {
            // 默认关闭状态需要重设高度
            needSetHeight = 1
        }

        // 监听窗口变化需要重新计算高度
        window.addEventListener('resize', () => {
            if (currentState === 1) {
                // 当前是打开状态,直接重设高度
                action.setTOCHeight()
            } else {
                // 记录当前窗口已发生变化,需要重设高度
                needSetHeight = 1
            }
        })

        // 绑定打开/关闭目录事件
        contentTOCBtn.addEventListener('click', e => {
            action.toggleTOC()
        })

        // 点击文章目录事件
        contentTOCTree.addEventListener('click', e => {
            if (e.target.tagName == 'LI') {
                const activeToc = contentTOCTree.querySelector('.active')
                if (activeToc) activeToc.classList.remove('active')
                e.target.classList.add('active')
                const TOCIndex = e.target.dataset.toc
                window.scrollTo({ top: contentHeadings[TOCIndex].offsetTop - topTOCOffsetHeight, left: 0, behavior: "smooth" })
            }
        })

        // 监听页面滚动
        document.addEventListener('scroll', () => {
            const scrollTop = document.documentElement.scrollTop
            const activeToc = contentTOCTree.querySelector('.active')
            if (scrollTop < entryContent.offsetTop || scrollTop > (entryContent.offsetHeight + entryContent.offsetTop)) {
                if (activeToc) activeToc.classList.remove('active')
            } else {
                const contentTOCLis = contentTOCTree.querySelectorAll('li')
                contentHeadings.forEach((e, k) => {
                    if (scrollTop >= (e.offsetTop - topTOCOffsetHeight) && scrollTop < (contentHeadings[k + 1] ? contentHeadings[k + 1].offsetTop - topTOCOffsetHeight : entryContent.offsetHeight + entryContent.offsetTop)) {
                        if (activeToc) activeToc.classList.remove('active')
                        contentTOCLis[k].classList.add('active')
                    }
                })
            }
        })
    }
});

使用方法

将 CSS 代码及 JS 代码复制后添加到主题模板相应的 css、js 文件内即可。某些程序或主题可通过后台添加 css、js 代码,直接在后台添加也可以。

瞎折腾/主题制作

Life is like a Design.
0