REC

使用JavaScript动态生成网站文章目录及内容滚动跟随效果

易航
1年前发布 /正在检测是否收录...

由于之前使用的文章导读插件性能过于臃肿,导致资源消耗高且难以维护,因此我决定自行开发一个轻量级的文章导读功能。参考了郄郄私语的 案例文章,我实现了以下四个主要功能:

  1. 根据文章内容自动生成目录
  2. 点击目录跳转至相应章节
  3. 滚动阅读时,目录随当前章节自动切换
  4. 目录跟随文章内容滚动

目标

本文旨在实现以下功能:

  • 自动生成文章目录
  • 点击目录跳转至对应章节
  • 目录根据当前阅读章节自动高亮
  • 目录随文章内容滚动

根据文章内容自动生成目录

实现自动生成目录的前提是文章内容须有清晰的层次结构,这需要使用 h1h6 标签来定义标题。通过 JavaScript,我们可以提取这些标题来生成目录:

var articleTitleList = $('.joe_detail__article').find('h1, h2, h3, h4, h5, h6');
articleTitleList.each(function() {
    const headingLevel = $(this).prop("tagName").toLowerCase();
    const headingName = $(this).text().trim();
    console.log(headingLevel, headingName);
    $('#catalogs').append(`<div class="catalog catalog-${headingLevel}">${headingName}</div>`);
});

在样式表中为目录项定义缩进样式:

.catalog-h2 { margin-left: 1em; }
.catalog-h3 { margin-left: 2em; }
.catalog-h4 { margin-left: 3em; }
.catalog-h5 { margin-left: 4em; }
.catalog-h6 { margin-left: 5em; }

点击目录跳转至对应章节

点击目录跳转至相应章节可以通过为每个标题添加锚点。我们将在 h1h6 标签上添加 id

articleTitleList.each(function() {
    const headingName = $(this).text().trim();
    const enHeadingName = encodeURIComponent(headingName);
    $(this).attr('id', `${enHeadingName}`);
    $('.posts-nav-lists>ul').append(`<li><a href="#${enHeadingName}">${headingName}</a></li>`);
});

目录自动切换至当前章节

为了让目录随着阅读位置自动切换,我们将监控用户的滚动位置并依据此更新目录的高亮条目:

const catalogTrack = () => {
    let $currentHeading = $('h1');
    articleTitleList.each(function() {
        const $heading = $(this);
        if ($heading.offset().top - $(document).scrollTop() > 20) return false; // 退出条件
        $currentHeading = $heading;
    });
    
    // 更新目录状态
    const anchorName = $currentHeading.attr('id');
    $('.posts-nav-lists>ul>li').removeClass('active');
    $(`#title-${anchorName}`).parent().addClass('active');
};

目录随文章内容滚动

为确保当前章节始终可见,我们控制目录区域的滚动:

if ($catalog.length > 0) {
    $('.posts-nav-box').scrollTop($catalog[0].offsetTop - 50); // 确保可见
}

性能优化

为了避免在滚动时频繁调用 catalogTrack 导致性能降低,我们可以使用函数节流技术:

function throttle(fn, wait) {
    let lastTime = 0;
    return function() {
        const now = Date.now();
        if (now - lastTime >= wait) {
            fn.apply(this, arguments);
            lastTime = now;
        }
    };
}

window.addEventListener('scroll', throttle(catalogTrack, 500)); // 每500毫秒调用一次

自定义跳转行为

考虑到固定导航条可能遮挡目标章节,我们可以自定义跳转行为,确保目标元素不会被遮挡:

document.querySelectorAll('.posts-nav-lists>ul>li>a').forEach(link => {
    link.addEventListener('click', (event) => {
        event.preventDefault(); // 阻止默认跳转
        const targetId = link.getAttribute('href').substring(1);
        const targetElement = document.getElementById(targetId);
        const headerHeight = document.querySelector('.joe_header').offsetHeight;
        const scrollTop = $(targetElement).offset().top - headerHeight - 10;
        window.scrollTo({ top: scrollTop, behavior: 'smooth' });
    });
});

总结

本文记录了生成和控制博客文章目录的思路与方法,针对不同需求可能会有其他解决方案。欢迎大家在评论区讨论和分享建议,感谢阅读!

完整代码示例

HTML 部分

<section class="joe_aside__item posts-nav-box">
    <div class="joe_aside__item-title"><i class="fa fa-list-ul"></i><span class="text">文章目录</span><span class="line"></span></div>
    <div class="joe_aside__item-contain">
        <div class="posts-nav-lists"><ul class="bl nav"></ul></div>
    </div>
</section>

JavaScript 部分

var articleTitleList = $('.joe_detail__article').find('h1, h2, h3, h4, h5, h6');
if (articleTitleList.length > 0) {
    // 目录生成及事件绑定
    (function () {
        for (let heading of articleTitleList) {
            const headingLevel = heading.tagName.toUpperCase();
            const $heading = $(heading);
            // console.log($heading);
            const headingName = $heading.text().trim();
            const enHeadingName = encodeURIComponent(headingName);
            $heading.attr('id', `${enHeadingName}`);
            $('.posts-nav-lists>ul').append(`<li class="n-${headingLevel}"><a id="title-${enHeadingName}" href="#${enHeadingName}">${headingName}</a></li>`);
            // const anchorName = $heading.attr('id');
            // console.log(headingLevel, headingName);
        }

        if (Joe.IS_MOBILE) {
            $('.joe_action').append(`<div class="joe_action_item posts-nav-switcher"><i class="fa fa-list-ul"></i></div>`)
            let joe_aside = $('.joe_aside .joe_aside__item.posts-nav-box .joe_aside__item-contain').html();
            $('.joe_aside .joe_aside__item.posts-nav-box').remove();
            let html = document.createElement('div');
            html.className = 'posts-nav-box';
            html.innerHTML = joe_aside;
            // console.log(html);
            $('.posts-nav-switcher').append(html);
            $('.joe_action_item .posts-nav-box').css({
                'position': 'absolute',
                'display': 'none',
                'right': '50px',
                'bottom': '0px',
                'padding': '15px',
                'border-radius': 'var(--radius-wrap)',
                'box-shadow': '0 0 10px 8px var(--main-shadow)',
                'overflow': 'auto',
                'max-height': '50vh',
                'max-width': '80vw'
            })
            $('.joe_action_item.posts-nav-switcher').click(() => {
                $('.joe_action_item.posts-nav-switcher .posts-nav-box').fadeToggle(200);
            });
        }

        const catalogTrack = () => {
            // console.log('页面滚动标题监听');
            let $currentHeading = $('h1');
            for (let heading of articleTitleList) {
                const $heading = $(heading);
                if ($heading.offset().top - $(document).scrollTop() > $('.joe_header').height()) {
                    break;
                }
                $currentHeading = $heading;
                const anchorName = $currentHeading.attr('id');
                const $catalog = $(document.getElementById(`title-${anchorName}`)).parent();
                if (!$catalog.hasClass('active')) {
                    $('.posts-nav-lists>ul>li').removeClass('active');
                    $catalog.addClass('active');
                }
                if ($catalog.length > 0) {
                    if ($('.posts-nav-box .joe_aside__item-contain').length > 0) {
                        $('.posts-nav-box .joe_aside__item-contain').scrollTop($catalog[0].offsetTop - 50);
                    } else {
                        $('.posts-nav-box').scrollTop($catalog[0].offsetTop - 50);
                    }
                } else {
                    $('.posts-nav-lists').scrollTop(0);
                }
            }
        };

        /**
         * 函数节流,时间戳方案
         * @param {*} fn 
         * @param {*} wait 
         * @returns 
         */
        function throttle(fn, wait) {
            var pre = Date.now();
            return function () {
                var context = this;
                var args = arguments;
                var now = Date.now();
                if (now - pre >= wait) {
                    fn.apply(context, args);
                    pre = Date.now();
                }
            }
        }

        window.addEventListener('scroll', throttle(() => {
            catalogTrack();
        }, 500));

        // 监听文章目录a标签点击
        document.querySelectorAll('.posts-nav-lists>ul>li>a').forEach(link => {
            link.addEventListener('click', (event) => {
                event.preventDefault(); // 阻止默认跳转行为
                // 获取目标元素 ID
                const targetId = link.getAttribute('href').substring(1);
                const targetElement = document.getElementById(targetId);
                if (targetElement) {
                    // 获取目标元素距离页面顶部的距离
                    const targetTop = $(targetElement).offset().top;
                    // console.log(targetTop);
                    // 获取顶栏高度,考虑动态高度变化和内部元素
                    const headerHeight = document.querySelector('.joe_header').offsetHeight;
                    // 计算滚动位置
                    const scrollTop = (targetTop - headerHeight) - 10; // 预留10px的美观距离
                    window.scrollTo({
                        top: scrollTop,
                        behavior: 'smooth'
                    });
                } else {
                    console.error('目标元素未找到:', targetId);
                }
            });
        });

    }())
} else {
    $('.joe_aside__item.posts-nav-box').remove();
}

通过以上修改,文章的可读性、结构性和实用性得以提升,使读者能够更好地理解和使用所提供的功能。

© 版权声明
本站用户发帖仅代表本站用户个人观点,并不代表本站赞同其观点和对其真实性负责。
转载本网站任何内容,请按照转载方式正确书写本站原文地址。
THE END
喜欢就支持一下吧
点赞 1 分享 赞赏
评论 抢沙发
取消 登录评论