记录--组件库的 Table 组件表头表体是如何实现同步滚动?

这篇具有很好参考价值的文章主要介绍了记录--组件库的 Table 组件表头表体是如何实现同步滚动?。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助

记录--组件库的 Table 组件表头表体是如何实现同步滚动?

前言

在使用 Vue 3 组件库 Naive UI 的数据表格组件 DataTable 时碰到的问题,NaiveUI 的数据表格组件 DataTable 在固定头部和列的示例中,在键盘操作下表格横向滚动会有问题,本文是记录下解决问题的过程,并最后向 Naive UI 提交 PR。

问题复现步骤:

  1. 鼠标点击表头,此时按键盘左右键,表格横向滚动没问题;
  2. 再把鼠标移入表体,按键盘左右键,会发现表头滚动而表体没动。

记录--组件库的 Table 组件表头表体是如何实现同步滚动?

相关 issue:

  • github.com/tusen-ai/na…
  • github.com/tusen-ai/na…
  • github.com/tusen-ai/na…

Naive UI 中的实现

打开 Chrome 开发者工具,可以看到固定头和列中,表头和表体是由两个 table 元素单独实现,我们遇到的问题可能就是表头表体同步滚动的实现有点问题,具体得看源码中的实现验证下。

记录--组件库的 Table 组件表头表体是如何实现同步滚动?

在 DataTable 组件源码中涉及到滚动的相关文件 src/data-table

  • use-scroll.ts 处理表格滚动事件
  • DataTable.tsx 表格组件
  • tableParts/Header.tsx 表头组件
  • tableParts/Body.tsx 表体组件

我们按照复现步骤的操作来看看滚动的时候做了什么?

大致过程就是表头或表体滚动时会触发 scroll 事件,在监听 scroll 事件的回调中获取 scrollLeft 值,然后设置另一部分的 scrollLeft 来同步滚动。

1. 鼠标点击表头,按键盘左右键,表头表体横向滚动正常

当鼠标移入表头时,表头监听了 mouseenter 事件,会设当前 scrollPartRefhead ;按键盘左右键使表头滚动,触发了表头 scroll 事件,执行 handleTableHeaderScroll 方法,该方法是由 DataTable.tsx 组件提供(provider),在 use-scroll.ts 中的 useScroll 方法导出的;handleTableHeaderScroll 作用主要是来同步表体的滚动及一些样式设置;代码如下:

// use-scroll.ts
function handleTableHeaderScroll (): void {
    // 判断当前滚动的部分是不是表头,scrollPartRef 值为 head 或 body
    if (scrollPartRef.value === 'head') {
    // beforeNextFrameOnce 的作用是每一帧只调用一次传入的回调
    // syncScrollState 的作用是同步滚动表体和一些样式设置
      beforeNextFrameOnce(syncScrollState)
    }
  }

2. 再把鼠标移入表体,按键盘左右键,表头横向滚动正常而表体没动

当将鼠标移入表体时,表体监听了 mouseenter 事件,会设当前 scrollPartRefbody ,在按键盘左右键时表头滚动,执行了表头 scroll 事件回调 handleTableHeaderScroll,但不满足判断条件 scrollPartRef.value === 'head',没有执行 syncScrollState 方法。

问题原因:在移入表体后,此时鼠标焦点依旧在表头,所以按键盘左右键时,仍然是表头滚动及触发 scroll 事件,执行的是 handleTableHeaderScroll 方法,而此时 scrollPartRef 的值为 body ,导致没有执行 syncScrollState 方法来同步表体的 scrollLeft 值,最终表现表体没有跟随表头滚动。

其他组件库中的实现

在解决问题前,观察了一下各组件库表格组件中固定表头和列的示例,看看是否有类似问题,查看之后发现表头和表体都是通过两个 table 元素来单独实现,这就遇到一个问题,因为是两个 table 元素,那怎么实现表头表体同步滚动呢?以及怎么解决在 Naive UI 中遇到的问题?

Element Plus

Element Plus 中,当鼠标点击表头,按键盘左右键是无法横向滚动的,只有鼠标焦点在表体上才能横向滚动;也就是滚动只能由表体滚动带动表头滚动。

源码实现里面它的表格滚动条不像 Naive UI 表头和表体都设为 overflow: scroll 来产生滚动,而是在表体包了一层封装的滚动条组件,表头则没有包直接设为 overflow: hidden 不让滚动;在滚动表体时,获取滚动条组件的 scrollLeft 来同步表头的 scrollLeft 。Table 组件源码点这里

Ant Design Vue

Ant Design Vue 的表现同 Element Plus,表头无法滚动,只能由表体滚动带动表头滚动。

源码实现原理跟 Element Plus 差不多,它的表格表头也是设为 oveflow: hidden 无法滚动,表体设为 overflow: auto scroll 来滚动,然后监听表体的滚动事件 scroll获取 scrollLeft 来同步表头 scrollLeft 。Table 组件源码点这里

问题解决过程

问题复现

根据 Naive UI DataTable 源码中固定头和列时同步滚动的实现方式,搞一个 demo 复现问题。

代码实现思路:滚动分为表头、表体两个部分,监听各自的滚动事件 scroll ,滚动某一个部分时,在 scroll 事件处理函数中通过设置另一部分 scrollLeft 来同步滚动,因为在设置 scrollLeft 时也会触发 scroll 事件,这样就会造成死循环,所以需要判断当前滚动的是哪个部分,这里用 scrollPartRef 变量来记录,在鼠标移入表头时设 scrollPartRef'head',在鼠标移出表头或移入表体时设 scrollPartRef‘body’,然后在滚动事件处理回调 handleHeaderScroll / handleBodyScroll 方法中,判断 scrollPartRef 是不是为对应的 'head' / 'body' ,是的话才会执行 syncScrollState 方法来同步另一部分的 scrollLeft

具体代码如下:

Demo 在线地址:[Bug] NaiveUI-DataTable-scrolling-sync (demo) - codesandbox

/**
 * Naive UI DataTable 组件滚动同步 demo 实现
 */
<template>
  <div class="wrap">
    <p>scrollPart:{{ scrollPartRef }}</p>
    <div
      ref="headerRef"
      class="header"
      @mouseenter="handleHeaderMouseenter"
      @mouseleave="handleHeaderMouseleave"
      @scroll="handleHeaderScroll"
    >
      <div class="content" tabIndex="-1">
        head-head-head-head-head-head-head-head-head-head-head-head-head-head-head-head-head-head-head-head-head-head-head
      </div>
    </div>
    <div
      ref="bodyRef"
      class="body"
      @mouseenter="handleBodyMouseenter"
      @scroll="handleBodyScroll"
    >
      <div class="content" tabIndex="-1">
        body-body-body-body-body-body-body-body-body-body-body-body-body-body-body-body-body-body-body-body-body-body-body
      </div>
    </div>
  </div>
</template>
<script setup lang="ts">
import { defineComponent, ref } from "vue";

const scrollPartRef = ref(); // 当前滚动部分,值为 'head' 或 'body'
const headerRef = ref();
const bodyRef = ref();

function handleHeaderScroll() {
  if (scrollPartRef.value === "head") {
    syncScrollState();
  }
}

function handleBodyScroll() {
  if (scrollPartRef.value === "body") {
    syncScrollState();
  }
}
// 同步滚动
let scrollLeft = 0;
function syncScrollState() {
  if (scrollPartRef.value === "head") {
    scrollLeft = headerRef.value.scrollLeft;
    bodyRef.value.scrollLeft = scrollLeft;
  } else {
    scrollLeft = bodyRef.value.scrollLeft;
    headerRef.value.scrollLeft = scrollLeft;
  }
}

function handleHeaderMouseenter() {
  scrollPartRef.value = "head";
}
function handleHeaderMouseleave() {
  scrollPartRef.value = "body";
}
function handleBodyMouseenter() {
  scrollPartRef.value = "body";
}
</script>
<style>
.wrap {
  width: 600px;
}
.content {
  width: 1000px;
  height: 80px;
  background-color: lightblue;
}
.header {
  padding: 10px;
  background-color: lightgray;
  overflow: auto;
  margin-bottom: 20px;
}
.body {
  padding: 10px;
  background-color: lightgray;
  overflow: auto;
}
.content {
  border: 1px solid orange;
}
</style>

问题分析

想下使浏览器原生滚动条滚动的交互操作有几种:

  1. 触控板手势滚动
  2. 鼠标按住滚动条拖动
  3. 键盘 shift 键 + 鼠标滚轮滚动
  4. 鼠标焦点在滚动内容上,接着用键盘左右键滚动
  5. 还有么?

概括下来就是分为三种:触控板、鼠标、键盘,作者实现的时候可能没考虑到键盘操作的场景。

问题原因上面分析过,在鼠标点击表头后移入表体,scrollPartRef 会被设为 'body' ,而此时鼠标焦点依旧在表头上,当操作键盘方向键,滚动的是表头,执行它的 handleHeaderScroll 方法,但不满足判断条件 scrollPartRef.value === 'head',没有执行 syncScrollState 方法,导致表体没有同步滚动。

现在能想到的解决方案有两种:一种就是参考其他组件库中的方案,不让表头能主动滚动,只能由表体滚动带动表头滚动;一种就是修复复现操作下的问题。

我认为现在的实现方式太复杂了,需要监听表头、表体的鼠标事件 mouseentermouseleave来预设当前滚动部分 scrollPartRef ,如果按照这种思路,要修复键盘操作下的问题,是不是还要监听当前焦点focus 事件然后做判断,有没有更简单的方式?

解决思路

我的想法是只监听 scroll 事件能不能做到同步滚动,现在有表头、表体两个滚动部分,那么可分为主动滚动和被动滚动;在未滚动前,我们不预设主动滚动是哪部分(即不设置 scrollPartRef),等到真正滚动的时候,如果我们能知道主动滚动的是哪部分,这样就能获取主动滚动部分的 scrollLeft ,去设置被动滚动部分的 scrollLeft ,以此实现同步滚动。如果大家有更好的解决思路,欢迎讨论!

怎么判断主动滚动的是哪部分?

当时给 Naive UI 提 PR 的时候想到的是第一种思路,但是我觉得第二种思路更好一点,后续重新提交一个。

第一种思路:在每次滚动中取表头或表体的 scrollLeft 和上一次滚动记录下的 lastScrollLeft (初始为 0)比较来判断当前主动滚动部分是哪个,这里取表头部分的 scrollLeft,如果差值不为 0,说明当前主动滚动部分为表头(即 scrollPartRef = ‘head’),否则为表体。

const scrollPartRef = ref(); // 当前主动滚动部分
const headerRef = ref();
const bodyRef = ref();

function handleHeaderScroll() {
  if (scrollPartRef.value !== "body") {
    syncScrollState();
  } else {
  // 每次滚动结束,置空
    scrollPartRef.value = undefined;
  }
}

function handleBodyScroll() {
  if (scrollPartRef.value !== "head") {
    syncScrollState();
  } else {
  // 每次滚动结束,置空
    scrollPartRef.value = undefined;
  }
}

let lastScrollLeft = 0;
function syncScrollState() {
 if (!scrollPart.value) {
   // 取 header 的 scrollLeft 跟上一次滚动记录的 scrollLeft 比较
   const directionHead = lastScrollLeft - headerRef.value.scrollLeft;
  // 不为 0 说明 header 滚动了,主动滚动即为 head,否则为 body
   scrollPart.value = directionHead !== 0 ? "head" : "body";
  }
  if (scrollPart.value === "head") {
    lastScrollLeft = headerRef.value.scrollLeft;
    bodyRef.value.scrollLeft = lastScrollLeft;
  } else {
    lastScrollLeft = bodyRef.value.scrollLeft;
    headerRef.value.scrollLeft = lastScrollLeft;
  }
}
第二种思路:主动滚动部分肯定会先触发滚动事件,所以可以在表头或表体的 scroll 事件处理函数中判断 scrollPartRef 是否存在,不存在则将 scrollPartRef 设为对应的 'head' \ 'body'(即为主动滚动部分),然后调用syncScrollState同步被动滚动部分的scrollLeft。代码如下:
function handleHeaderScroll() {
 if(!scrollPart.value) {
  scrollPartRef.value = 'head'
 }
 if (scrollPartRef.value === "head") {
    syncScrollState();
  } else {
  // 每次滚动结束,置空
  scrollPartRef.value = undefined
 }
}

function handleBodyScroll() {
 if(!scrollPart.value) {
  scrollPartRef.value = 'body'
 }
  if (scrollPartRef.value === "body") {
    syncScrollState();
  } else {
  // 每次滚动结束,置空
  scrollPartRef.value = undefined
 }
}

function syncScrollState() {
  if (scrollPart.value === "head") {
    lastScrollLeft = headerRef.value.scrollLeft;
    bodyRef.value.scrollLeft = lastScrollLeft;
  } else {
    lastScrollLeft = bodyRef.value.scrollLeft;
    headerRef.value.scrollLeft = lastScrollLeft;
}

怎么判断滚动结束?

我们需要在每一次滚动结束后置空 scrollPartRef,否则同步会出问题。被动滚动部分在被设置 scrollLeft 时也会触发 scroll 事件,而被动滚动部分的 scroll 事件会晚于主动滚动的 scroll 事件触发,所以可以认为被动滚动事件执行完滚动就结束了,在它的事件回调处理中的 else 分支置空 scrollPartRef ,如上代码所示。

但是这样判断结束在 Safari 中还是有点有问题的,详见下面遗留问题。

完整代码

Demo 在线地址:[Fix] NaiveUI-DataTable-scrolling-sync (demo) - codesandbox

<template>
  <div class="wrap">
    <p>scrollPart:{{ scrollPart }}</p>
    <div ref="headerRef" class="header" @scroll="handleHeaderScroll">
      <div class="content" tabIndex="-1">
        head-head-head-head-head-head-head-head-head-head-head-head-head-head-head-head-head-head-head-head-head-head-head
      </div>
    </div>
    <div ref="bodyRef" class="body" @scroll="handleBodyScroll">
      <div class="content" tabIndex="-1">
        body-body-body-body-body-body-body-body-body-body-body-body-body-body-body-body-body-body-body-body-body-body-body
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'

const scrollPart = ref() // 当前主动滚动部分
const headerRef = ref()

function handleHeaderScroll() {
  if (scrollPart.value !== 'body') {
    console.log('<<< head scroll start >>>')
    syncScrollState()
  } else {
    scrollPart.value = undefined
    console.log('<<< body scroll end >>>')
    console.log('\n')
  }
}

const bodyRef = ref()
function handleBodyScroll() {
  if (scrollPart.value !== 'head') {
    console.log('<<< body scroll start >>>')
    syncScrollState()
  } else {
    scrollPart.value = undefined
    console.log('<<< head scroll end >>>')
    console.log('\n')
  }
}

let lastScrollLeft = 0
function syncScrollState() {
 // 取 header 的 scrollLeft 跟上一次滚动记录的 scrollLeft 比较
  const directionHead = lastScrollLeft - headerRef.value.scrollLeft
 // 不为 0 说明 header 滚动了,主动滚动即为 head,否则为 body
  scrollPart.value = directionHead !== 0 ? 'head' : 'body'
  if (scrollPart.value === 'head') {
    lastScrollLeft = headerRef.value.scrollLeft
    bodyRef.value.scrollLeft = lastScrollLeft
  } else {
    lastScrollLeft = bodyRef.value.scrollLeft
    headerRef.value.scrollLeft = lastScrollLeft
  }
  console.log('final scrollLeft', lastScrollLeft)
}
</script>

<style>
.wrap {
  width: 600px;
}
.content {
  width: 1000px;
  height: 80px;
  background-color: lightblue;
}
.header {
  padding: 10px;
  background-color: lightgray;
  overflow: auto;
  margin-bottom: 20px;
}
.body {
  padding: 10px;
  background-color: lightgray;
  overflow: auto;
}
.content {
  border: 1px solid orange;
}
</style>

PR

github.com/tusen-ai/na…

遗留问题

虽然解决了键盘操作的问题,但是后面发现在 Safari 浏览器中使用触控板快速滑动会有点小问题,这是由于在 Safari 中,滚动会有弹性效果导致的,复现步骤:

  1. 鼠标先点击表头,让焦点在表头上;
  2. 鼠标移入表体,使用触控板快速滑动,有弹性效果;
  3. 按键盘左键,第一下表头滚动,表体没动。

记录--组件库的 Table 组件表头表体是如何实现同步滚动?

原因是因为快速滑动表体(主动滚动部分)时,表头(被动滚动部分)到达边界后就不会再触发 scroll 事件了,而表体因为弹性效果依旧在触发 scroll 事件,导致 scrollPartRef 一直为 'body',未被清空,后面再按键盘滚动表头时,事件处理中条件 scrollPartRef.value === 'head' 不满足,未执行 syncScrollState 方法,导致了表体未同步滚动。根本原因就是我们没法正确判断滚动什么时候结束,如果能知道什么时候滚动结束,那么在滚动结束时重置 scrollPartRef 就不会有问题了。

听说现在有了 scrollend ,但是兼容性不行。

记录--组件库的 Table 组件表头表体是如何实现同步滚动?

本文转载于:

https://juejin.cn/post/7251786381483376695

如果对您有所帮助,欢迎您点个关注,我会定时更新技术文档,大家一起讨论学习,一起进步。

 记录--组件库的 Table 组件表头表体是如何实现同步滚动?文章来源地址https://www.toymoban.com/news/detail-519005.html

到了这里,关于记录--组件库的 Table 组件表头表体是如何实现同步滚动?的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 前端基础(Element、vxe-table组件库的使用)

    前言:在前端项目中,实际上,会用到组件库里的很多组件,本博客主要介绍Element、vxe-table这两个组件如何使用。 目录 Element 引入element 使用组件的步骤 使用对话框的示例代码 效果展示  vxe-table 引入vxe-table 成果展示 总结 官网地址 Button 按钮 | Element Plus (element-plus.org) 在m

    2024年02月10日
    浏览(36)
  • vue el-table实现动态表头

    众所周知,element-ui中有一个表格组件el-table,用于展示多条结构类似的数据。之前遇到过一个需求,要手动控制el-table的表头显示。就是假如table表格一共有10列数据,可以通过设置勾选,决定显示多少列。 为了代码的复用性,将配置页面单独抽成了组件,所以代码中会有组件

    2024年02月12日
    浏览(50)
  • Vue+Element Ui实现el-table自定义表头下拉选择表头筛选

    用vue+element ui开发管理系统时,使用el-table做表格,当表格列过多的时候,想要做成可选表头的,实现表格列的筛选显示,效果如下: 代码文件结构: 废话不多说,直接上代码: 第一步:新建名为 TableHeaderRender.vue 的文件 template   el-popover     placement=\\\"bottom\\\"     width=\\\"2

    2024年02月02日
    浏览(61)
  • 伯俊ERP与金蝶云星空对接集成表头表体组合查询连通分布式调出单新增(调拨出库对接分布式调出(KD调拨)6月)

    伯俊科技,依托在企业信息化建设方面的领先技术与实践积累,致力于帮助企业实现全渠道一盘货。伯俊提供数字经营的咨询与系统实施,助力企业信息化升级、加速数字化转型,覆盖零售市场中时尚鞋服、母婴内衣、珠宝饰品、食品酒水等多个行业,为5000+家机构及企业提

    2024年02月09日
    浏览(32)
  • vue 解决el-table 表体数据发生变化时,未重新渲染问题

     父组件中数量改变后总数重新计算 父组件代码  WmsTable 是子组件名  WmsTable 中的数量字段quantity修改时重新计算合计

    2024年01月20日
    浏览(35)
  • Vue 3 中 Ant Design Vue 如何自定义表格 Table 的表头(列头)内容?

    项目用到的是 Ant Design Vue (2.2.8) 组件库,开发中遇到一个如下图的表格,有些表头文本后面会有一些自定义图标,鼠标移入图标时显示对应的审批时间提示。当前列如果没有审批时间就会隐藏图标,只展示列头文本。 使用 Ant Design Vue 基础的 Table 组件是无法满足这个场景的

    2024年02月16日
    浏览(43)
  • el-table实现动态表头,自定义斑马纹等功能

    需求:1.根据选择的日期时间,实现表头的动态显示功能            2.修改默认表头灰色样式,            3.斑马纹偶数灰色改为奇数为灰色            4.表格某一行加分割线区分 1. height: 表格的高度设置,内容超出后会显示滚动条,高度固定 2. :row-class-name: 行类名,

    2024年02月16日
    浏览(41)
  • el-table 实现动态表头 静态内容 根据数据显示动态输入框

    直接放代码了

    2024年02月12日
    浏览(45)
  • 记录--前端如何优雅导出多表头xlsx

    xlsx导出是比较前后端开发过程中都比较常见的一个功能。但传统的二维表格可能很难能满足我们对业务的需求,因为当数据的维度和层次比较多时,二维表格很难以清晰和压缩的方式展现所有的信息,所以我们也就经常能碰到多级表头开发了。 每当我们新使用一个插件的时候

    2024年02月09日
    浏览(41)
  • Vue结合el-table实现合并单元格(以及高亮单元表头和指定行)

    实现效果如下: 思路: 1.首先使用动态表头表格。 2.其次实现动态计算合并单元格。(计算规则 传递需要合并的字段) 3.然后封装公共的计算单元格方法 export导出供多个页面使用。 4.同时需要封装成公共的组件供多个页面使用。 5.组件内写指定表头行高亮颜色以及指定行高

    2024年04月10日
    浏览(45)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包