記錄--組件庫的 Table 組件表頭表體是如何實現同步滾動?

来源:https://www.cnblogs.com/smileZAZ/archive/2023/07/04/17526732.html
-Advertisement-
Play Games

這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 前言 在使用 Vue 3 組件庫 Naive UI 的數據表格組件 DataTable 時碰到的問題,NaiveUI 的數據表格組件 DataTable 在固定頭部和列的示例中,在鍵盤操作下表格橫向滾動會有問題,本文是記錄下解決問題的過程 ...


這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助

前言

在使用 Vue 3 組件庫 Naive UI 的數據表格組件 DataTable 時碰到的問題,NaiveUI 的數據表格組件 DataTable 在固定頭部和列的示例中,在鍵盤操作下表格橫向滾動會有問題,本文是記錄下解決問題的過程,並最後向 Naive UI 提交 PR。

問題復現步驟:

  1. 滑鼠點擊表頭,此時按鍵盤左右鍵,表格橫向滾動沒問題;
  2. 再把滑鼠移入表體,按鍵盤左右鍵,會發現表頭滾動而表體沒動。

相關 issue:

Naive UI 中的實現

打開 Chrome 開發者工具,可以看到固定頭和列中,表頭和表體是由兩個 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. 按鍵盤左鍵,第一下表頭滾動,表體沒動。

原因是因為快速滑動表體(主動滾動部分)時,表頭(被動滾動部分)到達邊界後就不會再觸發 scroll 事件了,而表體因為彈性效果依舊在觸發 scroll 事件,導致 scrollPartRef 一直為 'body',未被清空,後面再按鍵盤滾動表頭時,事件處理中條件 scrollPartRef.value === 'head' 不滿足,未執行 syncScrollState 方法,導致了表體未同步滾動。根本原因就是我們沒法正確判斷滾動什麼時候結束,如果能知道什麼時候滾動結束,那麼在滾動結束時重置 scrollPartRef 就不會有問題了。

聽說現在有了 scrollend ,但是相容性不行。

本文轉載於:

https://juejin.cn/post/7251786381483376695

如果對您有所幫助,歡迎您點個關註,我會定時更新技術文檔,大家一起討論學習,一起進步。

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • Linux下PAM認證詳解(以centos7為例) PAM簡介(Pluggable Authentication Modules,可插拔認證模塊) Sun公司於1995年開發的一種與認證相關的通用框架機制:PAM(可插拔認證模塊)是實現認證工作的一個模塊。 因為每個服務都用到不同的認證方式,所以就需 ...
  • 使用expdp/impdp導出導入數據時,遇到ORA-2000錯誤,如下所示: Processing object type SCHEMA_EXPORT/TABLE/GRANT/OWNER_GRANT/OBJECT_GRANTProcessing object type SCHEMA_EXPORT/ ...
  • 文章中包含我所遇到的錯誤,進行了HDFS錯誤整改,以及後面有操作創建“遠程客戶端操作hdfs創建文件夾”,驗證環境是否配置成功的過程。 ...
  • 摘要:本文主要為大家講解在數倉性能調優過程中,關於大寬表關聯MERGE性能優化過程。 本文分享自華為雲社區《GaussDB(DWS)性能調優:大寬表關聯MERGE性能優化》,作者:譡里個檔。 【業務背景】 如下MERGE語句執行耗時長達2034s MERGE INTO sdifin.hah_ae_l ...
  • # 1、環境 Windows 11 Docker 20.0.2 # 2、拉取鏡像 我選擇 ubuntu20.04: ```powershell docker pull ubuntu:20.04 ``` ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/6d91edc5 ...
  • 數據倉庫是資料庫的下一代產品形態 —— 如何對數字化轉型過程中涌現的數據集合進行有效的存儲、分析和利用,繼而幫忙企業進行運營決策優化甚至創造出新的獲客模式和商業模式形成競爭力,是企業主們亟需解決的問題。在數據價值爆發的時代背景中,數據倉庫在千行百業中都有著相應的應用場景。 ...
  • 原文地址: [Android BottomNavigation底部導航欄使用 - Stars-One的雜貨小窩](https://stars-one.site/2023/07/04/android-bottomnavigation-use) ## 基本使用 本文側重點記錄一些特殊的樣式設置,所以基本 ...
  • ### 動機 在進行移動端全球化的時候,我們需要根據語言類型準備格式相同,文本不同的好多個文件,如果一個一個翻譯顯然很浪費時間,如果整篇複製到Google翻譯通常翻譯出來的文本是沒辦法直接用的,所以我通過有道雲API實現了一個翻譯iOS全球化文件的工具類。Android可以重寫文本匹配的部分。 ## ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...