构建动态交互式H5导航栏:滑动高亮、吸顶和锚点导航技巧详解

news2024/11/19 18:45:39

功能描述

产品要求在h5页面实现集锚点、吸顶及滑动高亮为一体的功能,如下图展示的一样。当页面滑动时,内容区域对应的选项卡高亮。当点击选项卡时,内容区域自动滑动到选项卡正下方。

在这里插入图片描述

布局设计

css 布局

为了更清晰的描述各功能实现的方式,将页面布局进行了如下的拆分。

★ 最外层的元素定义为 contentWrap,是使用 Intersection 定义的观察根元素
★ 所有可纵向滑动的元素包裹在 vertScrollWrap 中,也是粘性定位需要找到的父元素。
★ 横向可滑动的导航栏是 horiScrollWrap ,实现吸顶功能需要设置粘性定位。
★ observerWrap 用来包裹可观察的元素,observerItem 用来形容每一个可观察的子元素。

在这里插入图片描述

数据结构

导航栏的数据结构为数组,里面包括了选项卡需要显示的文案,对应的值,以及唯一值 key 。

const list =  {
  label: "选项卡一",
  value: "1",
  key: "1",
  height: 150, // 模拟使用,真实场景并不需要,数据会自动将盒子撑开
}]

在我们真实的业务场景中,导航栏的标题来源于后端接口,内容区域也需要根据标题类型结合数据展示不同的内容,在获取接口数据后,我会为每一条数据增加一个随机的 key(非索引值,不会重复的8位哈希值) ,在选项卡内容区域增加自定义属性,如 data-tab-item-id,这样可以精准的获取到所需要的 dom 元素。

在这里插入图片描述

选项卡吸顶

按照这个场景,首先把选项卡横向滚动吸顶的功能实现。这里代码语法很简单,通过 position: sticky 就能实现,但需要注意的是,这里的 dom 元素布局很重要,父元素需要包裹滑动时无需展示的中间区域,以及选项卡、及里面的内容区域。

在这里插入图片描述

具体代码如下,这样就能实现向上滑动时,选项卡一整行固定在头部区域和内容区域之间。

// 父元素
.vertScrollWrap {
  position: relative;
  overflow: scroll;
  height: calc(100vh - 100px);
}

// 子元素 
.horiScrollWrap {
  position: sticky
  top: 0
}

滑动导航高亮

当手指触摸页面滑动时,我们需要知道当前出现在可视区域的内容区域是哪些,传统方案可以通过绑定 scroll 方法,这里我使用的是 IntersectionObserver,通过观察元素与父元素的交叉状态,注意⚠️ 这个api有一定的浏览器版本要求。

map 保存 dom 结构信息

在页面滑动时,需要知道每个内容区域距离父元素顶部的距离,找出距离顶部最近的元素,才能高亮对应的选项卡。当选项卡点击时,我们希望知道每个内容区域的高度,高度计算后,滚动整体到指定的高度,让选项卡对应的内容元素放在选项卡的最下方。

根据以上逻辑,需要每个内容模块的属性,这里我使用map来保存这些数据,key 为 dom 元素,value 值为对象,其中包含是否与父元素相交、距离顶部元素、元素高度等属性。

// 初始化map
domMap = new Map();

// 设置map属性
setDomMap = (dom, obj) => {
  const element = this.domMap.get(dom);
  const value = {
    key: element?.key,
    top: element?.top,
    height: element?.height,
    index: element?.index,
    isIntersecting: element?.isIntersecting,
    ...obj,
  };
  this.domMap.set(dom, value);
};
IntersectionObserver 观察相交状态

使用 new IntersectionObserver(callback[, options]) 来定义观察逻辑。

在这里插入图片描述

初始化 domMap

在组件挂载时,初始化map数据,遍历所有的内容区域元素。

const prefix = "nav";
const blockId = `${prefix}-block-id`;
// 每一个 observerItem 绑定 nav-block-id 的属性, 为了保存其 key 值
const observerNodes = [
  ...contentWrap.querySelectorAll(`[${blockId}^="${prefix}-"]`),
];

observerNodes.forEach((el, index) => {
  this.observer.observe(el);
  const attr = el.getAttribute(blockId);
  const key = attr?.split("-")?.[1];
  this.setDomMap(el, {
    isIntersecting: false,
    key,
    index,
    top: -1,
    height: -1,
  });
});
callback 定义相交规则
this.observer = new IntersectionObserver((entries) => {
  entries.forEach((entry) => {
    // 更新 isIntersecting 属性,是否相交
    this.setDomMap(entry.target, { isIntersecting: entry.isIntersecting });
  });

  // 遍历所有属性,更新距离顶部高度
  Array.from(this.domMap.keys()).forEach((dom) => {
    const rect = dom.getBoundingClientRect();
    this.setDomMap(dom, { top: rect.top, height: rect.height });
  });

  let min = 1000;
  let key = null;

  // 遍历domMap,根据每个dom元素存储的top值,找到距离父元素最近的一个dom元素 
  for (const [, value] of this.domMap) {
    if (value.isIntersecting) {
      if (value.top < min) {
        min = value.top;
        key = value.key;
      }
    }
  }

  // 找到这个key值后,设置选项卡高亮,saveInfo.clickFlag 这里是判断当前操作是滑动还是手动点击了选项卡,如果手动点击选项卡后执行的滚动逻辑,则不再这里重复复制
  if (key && !saveInfo.clickFlag) {
    this.setActiveKey(key);
  }
  saveInfo.clickFlag = false;
}, options);
options 中定义文档视口的属性
const options = {
  root: contentWrap, // 监听元素的祖先DOM元素
  rootMargin: `-${marginTop}px 0px 0px 0px`, // 计算交叉值时添加至根的边界盒中的一组偏移量,marginTop 是头部区域+选项卡的高度
  threshold: 0, // 规定了一个监听目标与边界盒交叉区域的比例值
};
设置选项卡高亮

设置选项卡高亮只需要通过 state 来绑定一个变量,这里需要注意两个逻辑⚠️。

  1. 当需要高亮的选项卡不在当前可视区域内,需要将整个选项卡整体向左边滑动,露出高亮的选项卡。
  2. 当页面已经滑到底时,高亮的选项卡仍然可视区域内最靠近选项卡的那一个,比如下图的选项卡六。

在这里插入图片描述

判断选项卡是否在可视区域

首先是判断需高亮的选项卡是否在可视区域内,如果在可视区域内也就不需要再左滑了。

isInViewport = (element) => {
  const rect = element.getBoundingClientRect();
  return (
    rect.top >= 0 &&
    rect.left >= 0 &&
    rect.bottom <=
      (window.innerHeight || document.documentElement.clientHeight) &&
    rect.right <= (window.innerWidth || document.documentElement.clientWidth)
  );
};
计算左滑的距离

可以通过即将高亮的选项卡dom元素来计算,如果每滑动一次都要进行dom计算会比较的耗费性能,更建议一开始就将每一个选项卡元素距离左边的x轴距离保存起来,在组件初始化的时候使用一个对象保存起来。

calcTabsLeft() {
  this.tabsObj = {};
  // 为所有选项卡元素都绑定一个属性,格式为 data-tab-item-id={`${prefix}-${item.key}`}
  const tabs = document.querySelectorAll(`[data-tab-item-id]`);
  tabs.forEach((tab) => {
    const rect = tab.getBoundingClientRect();
    // 拆分出每个元素绑定在 dom 上的 key 值
    const key = tab.getAttribute("data-tab-item-id");
    this.tabsObj[key] = rect.x;
  });
}
判断当前展示内容是否已滑动到底部
canElementScrollDown = () => {
  // vertScrollWrap 是上图所标记出来的,滑动元素的父级
  return vertScrollWrap.scrollTop < vertScrollWrap.scrollHeight - vertScrollWrap.clientHeight;
};
导航栏横向滑动

为每一个 horiScrollItem 定义了 data-tab-item-id 属性,用于记录其 key 值。

在这里插入图片描述

navScroll() {
  const { activeKey } = this.state;
  // 可横向滚动选项卡父级
  const scrollTab = document.querySelector('[data-tab="tab"]');
  // 需滑动的选项卡元素
  const horiScrollItem = scrollTab?.querySelector(
    `[data-tab-item-id=${prefix}-${activeKey}]`
  );

  // 如果选项卡元素存在并且不在可视区域内,才滑动
 if (horiScrollItem && !this.isInViewport(horiScrollItem)) {
    const navDataId = `${prefix}-${activeKey}`;
    const elementX = this.tabsObj[navDataId] - 12;
    scrollTab.scrollTo(elementX, 0);
  }
}

接着就可以定义高亮选项卡的方法

setActiveKey = (key) => {
  // 如果已经滑动到底部,则不继续设置高亮选项卡
  if (!this.canElementScrollDown()) return;
  this.setState(
    {
      activeKey: key,
    },
    () => {
      // 判断选项卡是否在可视区域内,如果不是,则滑动到可视区域内
      this.navScroll();
    }
  );
};

锚点跳转

在点击选项卡的时候,通过选项卡自定义属性上的 key 值找到对应内容区域的 dom 元素,再计算出它和父元素的距离,将对应的 vertScrollItem 滑动到可视区域即可。

这里需要注意⚠️的是,锚点元素已经完全出现在可视区域或者已经滑到底部时,内容区域不会再向上滑动。比如下图中,点击选项卡七选项卡八展示的页面形式是一样的,因为他们对应的内容区域已经完全展示出来了。如果设计为向上滑动,则会页面底部很大一片空白。

在这里插入图片描述

计算内容区域与父级的距离
getTop = (key) => {
  let scrollTop = 0;
  Array.from(this.domMap.keys()).forEach((dom) => {
    const domValue = this.domMap.get(dom);
    if (domValue.key === key) {
      scrollTop = dom.offsetTop;
    }
  });
  return scrollTop;
};
点击锚点后滑动到可视区域
 onClickTabItem = (key) => {
    const vertScrollWrap = document.querySelector(".vertScrollWrap");
    // 导航栏高度 + 距离父元素高度
    const tabs = document.querySelector(".horiScrollWrap");
    const tabsHeight = tabs.getBoundingClientRect().height;
    const top = this.getTop(key) - tabsHeight;

    const observerItem = vertScrollWrap.querySelector(
      `[${blockId}="${prefix}-${key}"]`
    );
    if (observerItem) {
      // 将 clickFlag 定义为 true 时,不会在 intersectionObserver 处因为滑动导致不相交时而再次更新选项卡高亮的值
      saveInfo.clickFlag = true;
      const options = {
        left: 0,
        top,
      };
      vertScrollWrap.scroll(options);
    }

    this.setState({
      activeKey: key,
    });
  };

完整代码

以上便是滑动高亮+吸顶+锚点跳转的H5导航栏功能的分布解析,完整代码我放在了 github 上,戳 H5导航栏 anchor-sticky-nav 可查看,欢迎大家点个 star~

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

CentOS 7安装Nginx

说明&#xff1a;本文介绍如何在CentOS 7操作系统中安装Nginx 下载安装 首先&#xff0c;去官网上下载Nginx压缩包&#xff0c;官网地址&#xff1a;https://nginx.org/en/download.html&#xff0c;我这里下载稳定版1.24.0&#xff1b; 上传到云服务器上&#xff0c;解压&am…

数据分析案例(三):基于RFM分析的客户分群

实验2 基于RFM分析的客户分群 Tips&#xff1a;"分享是快乐的源泉&#x1f4a7;&#xff0c;在我的博客里&#xff0c;不仅有知识的海洋&#x1f30a;&#xff0c;还有满满的正能量加持&#x1f4aa;&#xff0c;快来和我一起分享这份快乐吧&#x1f60a;&#xff01; 喜欢…

读书笔记:高效能人士的七个习惯

前言 恐惧感和不安全感 现代社会&#xff0c;太多的人饱受恐惧感的折磨。他们恐惧将来&#xff0c;恐惧失业&#xff0c;恐惧无力养家。这种弱点&#xff0c;常常助长了一种倾向&#xff1a;无论在工作时&#xff0c;还是回到家中&#xff0c;都倾向于零风险的生活&#xff0…

CMMI认证是什么?如何确定CMMI认证的目标和范围

CMMI&#xff08;Capability Maturity Model Integration&#xff09;认证是一种用于评估和改进组织软件和项目管理过程的框架。它由美国国防部软件工程所&#xff08;SEI&#xff09;开发&#xff0c;旨在帮助组织提高其软件和项目管理的成熟度水平。 CMMI认证的意义在于&…

Python求利率

要求 编写程序计算在给定利率、指定年数的情况下投资的未来值。这个计算公式如下。 使用文本域输入投资额、年份和利率。当用户单击“calculate”按钮时&#xff0c;在文本域中显示未来的投资值&#xff0c;如图所示。 代码实现 import tkinter as tkdef calculate():amou…

求圆、圆球和圆柱的面积和体积(C语言)

一、运行结果&#xff1b; 二、源代码&#xff1b; # define _CRT_SECURE_NO_WARNINGS # include <stdio.h> //定义π常量的值&#xff1b; # define π 3.141526int main() {//初始化变量值&#xff1b;float r, h, S1, S2, P1, V1, V2;int judge 0;//提示用户&#x…

【Spring进阶系列丨第九篇】基于XML的面向切面编程(AOP)详解

文章目录 一、基于XML的AOP1.1、打印日志案例1.1.1、beans.xml中添加aop的约束1.1.2、定义Bean 1.2、定义记录日志的类【切面】1.3、导入AOP的依赖1.4、主配置文件中配置AOP1.5、测试1.6、切入点表达式1.6.1、访问修饰符可以省略1.6.2、返回值可以使用通配符&#xff0c;表示任…

设计模式之大话西游

8年前深究设计模式&#xff0c;现如今再次回锅&#xff5e; 还是大话设计模式 这本书还是可以的 大话西游经典的台词&#xff1a;“曾经有一份真挚的爱情摆在我面前,我没有珍惜,等我失去的时候,我才后悔莫及,人世间最痛苦的事莫过于此。如果上天能够给我一个再来一次的机会,我会…

内网通如何去除广告,内网通免广告生成器

公司使用内网通内部传输确实方便&#xff01;但是会有广告弹窗推送&#xff01;这个很烦恼&#xff01;那么如何去除广告呢&#xff01; 下载&#xff1a; 链接&#xff1a;https://pan.baidu.com/s/1CVVdWexliF3tBaFgN1W9aw?pwdhk7m 提取码&#xff1a;hk7m ID&#xff1a;…

大数据产品有哪些分类?各类里知名大数据产品都有哪些?

随着互联网技术的持续进步和全球数字化转型的推进&#xff0c;我们正处于一个数据爆炸的时代。在这样的大背景下&#xff0c;大数据已经逐渐崭露头角&#xff0c;成为了推动各行各业发展的关键因素和核心资源。大数据不仅仅是指数据的规模巨大&#xff0c;更重要的是它蕴含的价…

消除 BEV 空间中的跨模态冲突,实现 LiDAR 相机 3D 目标检测

Eliminating Cross-modal Conflicts in BEV Space for LiDAR-Camera 3D Object Detection 消除 BEV 空间中的跨模态冲突&#xff0c;实现 LiDAR 相机 3D 目标检测 摘要Introduction本文方法Single-Modal BEV Feature ExtractionSemantic-guided Flow-based AlignmentDissolved…

每日一题(leetcode238):除自身以外数组的乘积--前缀和

不进阶是创建两个数组&#xff1a; class Solution { public:vector<int> productExceptSelf(vector<int>& nums) {int nnums.size();vector<int> left(n);vector<int> right(n);int mul1;for(int i0;i<n;i){mul*nums[i];left[i]mul;}mul1;for…

阿迪Akamai 逆向 第二部分

声明&#xff1a; 本文章中所有内容仅供学习交流使用&#xff0c;不用于其他任何目的&#xff0c;抓包内容、敏感网址、数据接口等均已做脱敏处理&#xff0c;严禁用于商业用途和非法用途&#xff0c;否则由此产生的一切后果均与作者无关&#xff01;wx a15018601872 …

利用vip.item_get API接口,唯品会电商发展再上新台阶,VIP商品详情尽在掌握

唯品会作为中国领先的折扣电商平台&#xff0c;一直致力于为消费者提供高品质的商品和优质的服务。近年来&#xff0c;随着技术的不断发展和电商行业的竞争加剧&#xff0c;唯品会不断寻求创新&#xff0c;以满足消费者的多样化需求。在这个过程中&#xff0c;vip.item_get API…

深入理解大型项目服务器部署与数据库优化策略

我们先了解一下大概的数据量&#xff1a; 复杂的数据库语句&#xff1a;Mysql每秒大概可以执行100-200个 一台服务器就可以做到每秒几十个并发&#xff08;配置&#xff1a;4核8G&#xff09; 一万块钱的服务器的配置&#xff1a;16核32g内存4T硬盘 如果是微型的服务&#…

Elasticsearch中父子文档的关联:利用Join类型赋予文档的层级关系

码到三十五 &#xff1a; 个人主页 心中有诗画&#xff0c;指尖舞代码&#xff0c;目光览世界&#xff0c;步履越千山&#xff0c;人间尽值得 ! Elasticsearch是一个强大的搜索引擎&#xff0c;它提供了丰富的功能来满足复杂的搜索需求。其中&#xff0c;父子索引类型的join功…

离谱!奇安信人事总监透露:Web安全不会岗位这些就别投简历了

有人的地方就有江湖&#xff0c;有互联网安全的地方&#xff0c;就必然有Web安全工程师的身影。但其实Web安全是近几年才备受关注的&#xff0c;从事这方面的专业人员并不多&#xff0c;这就导致整个市场Web安全研究员的供求严重不平衡。 这种供求不平衡直接反映在Web安全研究…

Windows沙盒:sandboxie-plus工作原理及安装使用指导

文章目录 1、简介2、工作原理3、主要功能4、安装4.1、下载安装文件4.2、运行安装程序4.3、选择安装路径 5、使用方法6、总结 1、简介 Sandboxie是一款安全软件&#xff0c;也被称为沙箱。它的主要功能是创建一个隔离的虚拟环境&#xff0c;用户能够在其中运行或安装应用程序&a…

mmdetection模型使用mmdeploy部署在windows上的c++部署流程【详细全面版】

0. 前置说明: 该文档适用于:已经使用mmdetection训练好了模型,并且完成了模型转换。要进行模型部署了。 1. 概述 MMDeploy 定义的模型部署流程,如下图所示: 模型转换【待撰写,敬请期待…】 主要功能是:把输入的模型格式,转换为目标设备的推理引擎所要求的模型格式…

973: 统计利用先序遍历创建的二叉树叶结点的个数

解法&#xff1a; #include<iostream> #include<queue> using namespace std; // 定义二叉树结点 struct TreeNode {char val;TreeNode* left;TreeNode* right;TreeNode(char x) :val(x), left(NULL), right(NULL) {}; }; // 先序递归遍历建立二叉树 TreeNode* bu…