突破编程_前端_JS编程实例(目录导航)

1 开发目标

目录导航组件旨在提供一个滚动目录导航功能,使得用户可以方便地通过点击目录条目快速定位到对应的内容标题位置,同时也能够随着滚动条的移动动态显示当前位置在目录中的位置:

在这里插入图片描述

2 详细需求

2.1 标题提取与目录生成

  • 组件需要能够自动提取网页内容中的所有标题元素(如 h1, h2, h3 等)。
  • 提取的标题需要按照其在网页中的层级关系(如 h1 后面跟着的 h2 是其子章节)进行组织,形成一个目录容器。
  • 目录容器需以清晰、直观的方式展示给用户,允许用户通过点击目录条目进行导航。

2.2 滚动定位

  • 当用户在目录容器中点击某个目录条目时,网页的滚动条需要动态移动到对应的标题位置,使得该标题出现在页面的最上方。
  • 滚动过程应该平滑且快速,提升用户体验。

2.3 滚动条与目录条目交互

  • 当用户滚动网页的滚动条时,目录容器中的对应目录条目应该能够实时更新状态,以指示当前所在位置。
  • 当滚动条经过某个标题时,对应的目录条目应改变颜色(如高亮显示),以提醒用户当前的位置。

2.4 无滚动条情况处理

  • 如果网页内容较少,没有出现滚动条,那么点击目录条目时不应触发任何滚动动作。
  • 这种情况下,目录容器仍应正常显示,以供用户浏览网页内容的结构。

3 代码实现

首先创建一个 neat_directory.js 文件,该文件用于本组件的工具类、目录处理函数的代码构建。

(1)在具体的业务代码编写之前,先实现一个工具类以及一些工具方法,方便后面调用:

class CommonUtil {

    // 设置 DIV 中的文字为垂直居中
    static centerYTextInDiv(container) {
        container.style.display = 'flex';
        container.style.justifyContent = 'center';
        container.style.flexDirection = 'column';
    }

    // 判断 DIV 有无垂直滚动条
    static hasScrollbar(container) {
        var divStyle = window.getComputedStyle(container);
        var isOverflowing = container.scrollHeight > container.clientHeight;
        var isScrollbarVisible = isOverflowing &&
            (divStyle.overflow === 'scroll' || divStyle.overflow === 'auto' || divStyle.overflowY === 'scroll' || divStyle.overflowY === 'auto');

        return isScrollbarVisible;
    }
}

(2)接下来,开始定义目录节点类型,目录节点显示在目录区域:

class DirectoryNode {
    static LEVEL_OFFSET = 20;     // 每个级别的目录节点偏移像素
    static NODE_HEIGHT = '30px';     // 目录节点高度
    static NODE_NAME_FONTSIZE = '14px';     // 默认目录标题字符串的字体大小
    static NODE_NAME_COLOR = '#000';     // 默认目录标题字符串字体颜色
    static NODE_NAME_ACTIVE_COLOR = 'red';     // 默认目录标题在激活情况下字符串字体颜色

    constructor(container, para) {
        this.container = container;                 // 本目录节点的容器
        this.para = para;                           // 配置参数,包含页面内容的容器、标题容器以及标题等级等

        this.init();
    }

上面代码定义了 DirectoryNode 的一些默认属性与成员变量,并且创建构造函数,该函数接收调用者传入的 DIV 容器,并且调用 render 方法。
在 render 方法中,需要渲染当前目录节点,并且还要定义点击事件:

	render() {
        this.container.style.width = '100%';
        this.container.style.height = this.para.height ?? DirectoryNode.NODE_HEIGHT;
        this.container.style.fontSize = this.para.fontSize ?? DirectoryNode.NODE_NAME_FONTSIZE;
        this.container.style.color = this.para.color ?? DirectoryNode.NODE_NAME_COLOR;
        this.container.innerText = this.para.name;
        if (this.para.level > 1) {      // 设置目录节点偏移
            this.container.style.paddingLeft = ((this.para.level - 1) * DirectoryNode.LEVEL_OFFSET) + 'px';
        }
        this.container.style.cursor = 'pointer';

        // 点击事件
        let that = this;
        this.container.onclick = function () {
            that.para.onClick.call(that.para.onClickObj, that);
        }
    }

然后需要对目录节点的激活与非激活状态以及目录跳转逻辑做实现:

	// 目录节点激活并跳转对应目录位置
    activate() {
        this.container.style.color = DirectoryNode.NODE_NAME_ACTIVE_COLOR;
    }

    // 目录节点非激活
    deactivate() {
        this.container.style.color = this.para.color ?? DirectoryNode.NODE_NAME_COLOR;
    }

    // 目录跳转
    jump() {
        // 计算目标元素相对于父元素的位置  
        let targetElementRect = this.para.titleContainer.getBoundingClientRect();
        let parentRect = this.para.contentContainer.getBoundingClientRect();

        // 滚动到目标元素的顶部
        let offset = targetElementRect.top - parentRect.top + this.para.contentContainer.scrollTop;
        this.para.contentContainer.scrollTop = offset;
    }

    // 获取在页面内容的容器中,当前目录节点所对应的标题元素离顶部的距离
    getTopOffset() {
        let targetElementRect = this.para.titleContainer.getBoundingClientRect();
        let parentRect = this.para.contentContainer.getBoundingClientRect();
        return targetElementRect.top + parentRect.top;
    }
}

(3)在完成 DirectoryNode 的实现以后,开始创建目录类型 Directory :

class Directory {

    constructor(container, para) {
        this.container = container;                 // 传入的目录容器,用于渲染提取生成的目录
        this.para = para;                           // 配置参数,包含页面内容的容器
        this.nodes = [];                            // 目录节点集合
        this.jumpFlag=false;                        // 当前是否处于点击目录节点进行跳转的状态

        this.render();
    }

目录类型 Directory 的渲染函数 render 主要是获取页面内容中所有节点,遍历处理标题元素,然后创捷目录节点。此后,还需要定义页面内容的容器在滚动滚动轴时,触发目录变化的逻辑:

	render() {
        // 清空目录容器
        this.container.innerHTML = '';

        // 获取页面内容中所有节点,遍历处理标题元素
        let containerNodes = this.para.contentContainer.childNodes;
        containerNodes.forEach(element => {
            if (!element.tagName) {
                return;
            }
            let tagName = element.tagName.toUpperCase();
            if (2 == tagName.length) {
                let tagName1 = tagName.slice(0, 1);
                let tagName2 = tagName.slice(1, 2);
                if ('H' == tagName1 && !isNaN(tagName2)) {
                    let level = parseInt(tagName2);     // 标题等级
                    let directoryNodeContainer = document.createElement('div');
                    this.container.appendChild(directoryNodeContainer);
                    let nodePara = {
                        "name": element.innerText,
                        "level": level,
                        "titleContainer": element,
                        "contentContainer": this.para.contentContainer,
                        "onClick": this.jumpTo,
                        "onClickObj": this,
                    }
                    let node = new DirectoryNode(directoryNodeContainer, nodePara);
                    this.nodes.push(node);
                }
            }
        });

        // 页面内容的容器在滚动滚动轴时,触发目录变化
        let that = this;
        this.para.contentContainer.addEventListener('scroll', function () {
            // 如果网页内容较少,没有出现滚动条,那么页面内容的容器在滚动滚动轴时,不做任何触发
            // 如果当前是处于点击目录节点进行跳转的状态,则不做处理
            if (!CommonUtil.hasScrollbar(that.para.contentContainer) || that.jumpFlag) {
                return;
            }

            // 判断当前内容属于哪一个目录节点
            let activeNode=null;
            for (let index = 0; index < that.nodes.length; index++) {
                const node = that.nodes[index];
                if(node.getTopOffset()<0 && index+1<that.nodes.length && that.nodes[index+1].getTopOffset()>0){
                    activeNode = node;
                    break;
                }
            }
            if(null == activeNode && that.nodes.length>0){
                activeNode = that.nodes[0];
            }

            that.nodes.forEach(element => {
                element.deactivate();
            });
            activeNode.activate();
        });
    }

在完成渲染函数 render 的实现后,即要实现点击后目录跳转的功能,注意:如果网页内容较少,没有出现滚动条,那么点击目录条目时不应触发任何滚动动作:

	// 目录跳转
    jumpTo(node) {
        // 如果网页内容较少,没有出现滚动条,那么点击目录条目时不应触发任何滚动动作
        if (!CommonUtil.hasScrollbar(this.para.contentContainer)) {
            return;
        }

        this.jumpFlag=true; 
        this.nodes.forEach(element => {
            element.deactivate();
        });

        node.activate();
        node.jump();

        // 延迟一段时间
        let that = this;
        setTimeout(function() {  
            that.jumpFlag=false; 
        }, 100); 
    }
}

至此,整个目录导航功能的组件构建结束。

(4)完成目录导航功能的组件的代码编写后,可以创建 neat_directory.html 文件,调用该组件:

<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>header tab</title>
    <style>
        html {
            height: 100%;
        }

        body {
            margin: 0;
            height: 100%;
        }
    </style>
</head>

<body>
    <div id="divMain" style="height: 100%;width: 100%;display: flex;">
        <div id="divDirectory" style="margin:10px;height: 500px;width: 300px;border: 1px solid #aaa;padding: 10px;">
        
        </div>
        <div id="divContent" style="margin:10px;height: 500px;width: 600px;border: 1px solid #aaa;padding: 10px;overflow-y: auto;">
            <h1>1 第一章</h1>
            <h2>1.1 第一章 第一节 </h2>
            <span>组件需要能够自动提取网页内容中的所有标题元素(如 h1, h2, h3 等)。</span>
            <span>提取的标题需要按照其在网页中的层级关系(如 h1 后面跟着的 h2 是其子章节)进行组织,形成一个目录容器。</span>
            <span>目录容器需以清晰、直观的方式展示给用户,允许用户通过点击目录条目进行导航。</span>
            <h3>1.1.1 第一章 第一节 第一段</h3>
            <span>组件需要能够自动提取网页内容中的所有标题元素(如 h1, h2, h3 等)。</span>
            <span>提取的标题需要按照其在网页中的层级关系(如 h1 后面跟着的 h2 是其子章节)进行组织,形成一个目录容器。</span>
            <span>目录容器需以清晰、直观的方式展示给用户,允许用户通过点击目录条目进行导航。</span>
            <h3>1.1.2 第一章 第一节 第二段</h3>
            <span>组件需要能够自动提取网页内容中的所有标题元素(如 h1, h2, h3 等)。</span>
            <span>提取的标题需要按照其在网页中的层级关系(如 h1 后面跟着的 h2 是其子章节)进行组织,形成一个目录容器。</span>
            <span>目录容器需以清晰、直观的方式展示给用户,允许用户通过点击目录条目进行导航。</span>
            <h1>2 第二章</h1>
            <h2>2.1 第二章 第一节 </h2>
            <span>组件需要能够自动提取网页内容中的所有标题元素(如 h1, h2, h3 等)。</span>
            <span>提取的标题需要按照其在网页中的层级关系(如 h1 后面跟着的 h2 是其子章节)进行组织,形成一个目录容器。</span>
            <span>目录容器需以清晰、直观的方式展示给用户,允许用户通过点击目录条目进行导航。</span>
            <h3>2.1.1 第二章 第一节 第一段</h3>
            <span>组件需要能够自动提取网页内容中的所有标题元素(如 h1, h2, h3 等)。</span>
            <span>提取的标题需要按照其在网页中的层级关系(如 h1 后面跟着的 h2 是其子章节)进行组织,形成一个目录容器。</span>
            <span>目录容器需以清晰、直观的方式展示给用户,允许用户通过点击目录条目进行导航。</span>
            <h3>2.1.2 第二章 第一节 第二段</h3>
            <span>组件需要能够自动提取网页内容中的所有标题元素(如 h1, h2, h3 等)。</span>
            <span>提取的标题需要按照其在网页中的层级关系(如 h1 后面跟着的 h2 是其子章节)进行组织,形成一个目录容器。</span>
            <span>目录容器需以清晰、直观的方式展示给用户,允许用户通过点击目录条目进行导航。</span>
        </div>
    </div>
</body>
<script src="./neat_directory.js"></script>
<script>
    let para = {
        "contentContainer":document.getElementById('divContent'),
    }
    let directory = new Directory(document.getElementById('divDirectory'),para);

</script>

</html>

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/446132.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Transformer之多角度解读

Transformer 文章目录 Transformer  &#x1f449;引言&#x1f48e; 一、 自注意力机制 &#xff1a; 主要用于 长距离依赖捕捉和转换序列二、 Encoder&#xff1a;2.1 多头注意力机制&#xff1a;2.2 残差连接&#xff1a; 三、 Decoder&#xff1a;3.1 Decoder 多头注意力…

SMART PLC自适应低通滤波器(收放卷线速度滤波)

一阶低通滤波器更多内容请参考信号处理专栏相关文章,常用链接如下: 1、SMART PLC 低通滤波器和模拟量采集应用 https://rxxw-control.blog.csdn.net/article/details/136595982https://rxxw-control.blog.csdn.net/article/details/1365959822、SMART PLC双线性变换和后向差…

腾讯云服务器99元一年购买链接来了,续费也是99元

良心腾讯云推出99元一年服务器&#xff0c;新用户和老用户均可以购买&#xff0c;续费不涨价&#xff0c;续费也是99元&#xff0c;配置为轻量2核2G4M、50GB SSD盘、300GB月流量、4M带宽&#xff1a;优惠价格99元一年&#xff0c;续费99元&#xff0c;官方活动页面 txybk.com/g…

【STM32】STM32F4中USART的使用方法和Printf的重定义(基于CubeMX和Keil)

文章目录 一、前言二、STM32CubeMX生成代码2.1 选择芯片2.2 配置相关模式2.3 生成代码 三、Keil重定义Printf3.1 勾选“UseMicroLIB”3.2 添加头文件和修改fputc和fgetc 四、测试Printf的效果4.1 字符串测试4.2 格式化输出测试 五、存在问题的解决方法5.1 检查串口号是否一致5.…

由于找不到vcruntime140.dll无法继续执行的多种解决方法

最近&#xff0c;我在安装Adobe Premiere Pro&#xff08;以下简称PR&#xff09;时遇到了一个问题&#xff0c;即无法找到vcruntime140.dll文件。这可能导致某些应用程序无法正常启动或运行&#xff0c;因为vcruntime140.dll是许多基于Microsoft Visual C编译的应用程序所必需…

【中医】康复科治疗与中医养生(针灸、理疗、足浴)

程序员生活指南之 【中医】康复治疗与中医养生&#xff08;针灸、理疗、足浴&#xff09; 文章目录 1、康复科室2、中医与养生3、中医康复技术 1、康复科室 什么是康复科&#xff1f; 大部分医院都有康复科&#xff0c;但很多人都不知其具体是干什么的。其实&#xff0c;康复…

考研常识 | 专业硕士与学术硕士的11个区别

专业硕士与学术硕士的11个区别 对于考研学子而言&#xff0c;了解专业学位与学术学位的区别&#xff0c;是报考的第一步。学术学位研究生一般都是全日制的&#xff0c;而专业学位研究生的学习方式还分为即全日制与非全日制两种。这篇文章将带大家认识全日制专业学位与全日制学术…

LCR 131. 砍竹子 I

解题思路&#xff1a;&#xff08;与砍竹子II的区别是&#xff0c;这里的竹子长度数量级较小&#xff09; 数学推导或贪心 切分规则&#xff1a; 等长&#xff0c;且尽量为3 b0时&#xff0c;pow(3,a) b1时&#xff0c;pow(3,a-1)*4 少一段3&#xff0c;并入b生成一…

【数据结构】Map的常用方法

文章目录 一、搜索1.概念 二、Map的使用1.概念&#xff1a;2.Map的常用方法&#xff1a;1.V put(K Key ,V Value )2.V get(Object key)3.V getOrDefault(Object key, V defaultValue)4.V remove(Object key)5.Set<K> keySet()6.Collection<V> values()7.Set<Map…

连锁门店终端如何高效IT运维?向日葵助力服装行业数字化升级

服装行业作为典型的传统行业&#xff0c;因供应逐渐饱和、产能相对过剩以及消费结构升级&#xff0c;其销售端的数字化转型需求是最为迫切的。 为此&#xff0c;某知名时装品牌紧抓数字化转型机遇&#xff0c;在2016年起就开始了数字化变革&#xff0c;并在两年多的时间里完成…

配置与管理DNS服务器

配置与管理DNS服务器 **1&#xff0c;什么是DNS&#xff1f;**负责将域名转换成实际想对应的ip地址&#xff0c;这个过程交域名解析。 **2&#xff0c;域名解析的方法&#xff1a;**分布式&#xff0c;层次结构的数据库系统。根域&#xff0c;顶级域&#xff0c;二级域&#…

MyBatis是纸老虎吗?(二)

从二月二十六号开始&#xff0c;我就要求自己出一期与MyBatis有关的文章&#xff0c;直到三月三号那天才发表第一篇文章。这速度&#xff0c;这质量&#xff0c;着实堪忧。经过这件事&#xff0c;我也深刻认识到自己性格上的缺陷——懒惰。为了克服这个坏毛病&#xff0c;我决定…

使用Julia语言和R语言实现K-均值

K-均值算法基础 K-均值聚类算法属于一种无监督学习的方法&#xff0c;通过迭代的方式将数据划分为K个不重叠的子集&#xff08;簇&#xff09;&#xff0c;每个子集由其内部数据点的平均值来表示。计算方法大体如下&#xff1a; 1.初始化簇中心 选择K个数据点作为初始的簇中心…

LLM RAG系统中消除数据幻觉的几个绝招-OPENAI公司内称的“大招”

前言-什么是数据幻觉&#xff1f;它到底有什么危害呢 我们直接来举例&#xff1a; 我是金银花一区的&#xff0c;附近有什么小学&#xff1f; 此时RAG带出如下信息&#xff1a; 金银花小区一区、二区、三区附近教育资源有&#xff1a;银树大学、建设小学金银花校区、金树高…

IMX8MM -- Yocto构建遇见的错误及解决方法:

IMX8MM Yocto构建遇见的错误及解决方法&#xff1a; 1 bison-3.0.4 error2 Opencv BB_NO_NETWORK Error &#xff1a;3 Yocto构建时出现U-boot 问题4 Yocto构建时出现Linux kernel编译问题5 wayland-native6 cross-localedef-native7 wayland-protocols8 mesa 硬件&#xff1a;…

React Navite环境搭建

React Navite官网地址 React Native 中文网 使用React来编写原生应用的框架 创建React Navite项目命令&#xff08;目录必须是英文&#xff09; npx react-nativelatest init AwesomeProject 如果你是想把 React Native 集成到现有的原生项目中&#xff0c;则步骤完全不同…

多项式回归算法模拟

import numpy as np import matplotlib.pyplot as plt from sklearn.linear_model import LinearRegression from sklearn.preprocessing import PolynomialFeatures# 生成随机数作为x变量&#xff0c;范围在-5到5之间&#xff0c;共500个样本 x np.random.uniform(-5, 5, siz…

Java开发从入门到精通(一):Java的进阶语法知识

Java大数据开发和安全开发 Java的方法1.1 方法是什么1.1.1 方法的定义1.1.2 方法如何执行?1.1.3 方法定义时注意点1.1.4 使用方法的好处是? 1.2 方法的多种形式1.2.1 无参数 无返回值1.2.2 有参数 无返回值 1.3 方法使用时的常见问题1.4 方法的设计案例1.4.1 计算1-n的和1.4.…

[C/C++]string类常用接口介绍及模拟实现string类

一&#xff1a;Cstring类的由来 在C语言中&#xff0c;字符串是以\0结尾的一些字符的集合&#xff0c;为了操作方便&#xff0c;C标准库中提供了一些str系列的库函数&#xff0c;但是这些库函数与字符串是分离开的&#xff0c;不太符合OOP的思想&#xff0c;而且底层空间需要用…

【CSS面试题】高度塌陷问题及解决

什么情况下产生 (when 父盒子没有定义高度&#xff0c;但是子元素有高度&#xff0c;希望用子盒子撑起父盒子的高度&#xff0c;但是子盒子添加了浮动属性之后&#xff0c;父盒子高度为0 <template><div class"father"><div class"son"&…