antd/fusion表格增加圈選複製功能

来源:https://www.cnblogs.com/vigourice/archive/2023/09/24/17724258.html
-Advertisement-
Play Games

背景介紹 我們存在著大量在PC頁面通過表格看數據業務場景,表格又分為兩種,一種是 antd / fusion 這種基於 dom 元素的表格,另一種是通過 canvas 繪製的類似 excel 的表格。 基於 dom 的表格功能豐富較為美觀,能實現多表頭、合併單元格和各種自定義渲染(如表格中渲染圖形 ...


背景介紹

我們存在著大量在PC頁面通過表格看數據業務場景,表格又分為兩種,一種是 antd / fusion 這種基於 dom 元素的表格,另一種是通過 canvas 繪製的類似 excel 的表格。

基於 dom 的表格功能豐富較為美觀,能實現多表頭、合併單元格和各種自定義渲染(如表格中渲染圖形 / 按鈕 / 進度條 / 單選框 / 輸入框),以展示為主,不提供圈選、整列複製等功能。

canvas 繪製的類 excel 外表朴素更為實用,大量數據渲染不卡頓,操作類似 excel,能行/列選中,圈選、複製等功能。

兩者使用場景有所差異,各有利弊,但業務方不希望一套系統中出現兩種類型的交互,期望能將兩種表格的優缺點進行融合,在美觀的dom表格中增加圈選、複製的功能。

圈選效果

業務方所期望的圈選效果和excel類似,滑鼠按下即選中元素,然後滑動滑鼠,滑鼠所經過形成的四邊形就是選中區域,此時滑鼠右鍵點擊複製按鈕,或者鍵盤按下 ctrl + c 複製文本。

而dom表格經過如上操作,會把一整行數據都選上,不符合業務同學的使用預期。

實現過程

去除預設樣式

我們需要自行定義滑鼠事件、元素樣式,需要先將無用的預設樣式清除,包括上圖中的 hover 和選中元素的背景色。

  • 禁用表格本身的滑鼠點擊選擇功能,設置css,userSelect: none
<Table style={{ userSelect: 'none' }} ></Table>
  • 去除 hover 樣式(這裡使用的是 fusion 組件)
.next-table-row:hover {
  background-color: transparent !important;
}

滑鼠按下,記錄選中元素

為表格綁定滑鼠按鍵時觸發事件 mousedown

當滑鼠按下時,這個元素就是中心元素,無論是向哪個方向旋轉,所形成的區域一定會包含初始選中的元素。

getBoundingClientRect() 用於獲得頁面中某個元素的上下左右分別相對瀏覽器視窗的位置。

const onMouseDown = (event) => {
  const rect = event.target.getBoundingClientRect();

  // funsion 判斷點擊是否為表頭元素,為否時才繼續後面的邏輯。antd 不需要判斷,因為點擊表頭不會觸發該事件
  const isHeaderNode = event.target?.parentNode?.getAttribute('class')?.indexOf('next-table-header-node') > -1;
  if (isHeaderNode) return;

  originDir = {
    top: rect.top,
    left: rect.left,
    right: rect.right,
    bottom: rect.bottom,
  };
  // 渲染
  renderNodes(originDir);
};

<Table style={{ userSelect: 'none' }} onMouseDown={onMouseDown}></Table>

滑鼠滑過

為表格綁定滑鼠滑過時觸發事件 mousemove

根據滑動元素的上下左右距離與滑鼠按下時的位置進行判斷,圈選元素存在四個方向,以第一次選中的元素為中心位置。滑動時元素位於滑鼠按下的右下、左下、右上、左上方,根據不同的情況來設置四個角的方位。

const onMouseMove = (event) => {
  if (!originDir.top) return;
  const rect = event.target.getBoundingClientRect();

  let coordinates = {};

  // 滑鼠按下後往右下方拖動
  if (
    rect.top <= originDir.top &&
    rect.left <= originDir.left &&
    rect.right <= originDir.left &&
    rect.bottom <= originDir.top
  ) {
    coordinates = {
      top: rect.top,
      left: rect.left,
      right: originDir.right,
      bottom: originDir.bottom,
    };
  }

  // 滑鼠按下後往左下方拖動
  if (
    rect.top >= originDir.top &&
    rect.left <= originDir.left &&
    rect.right <= originDir.right &&
    rect.bottom >= originDir.bottom
  ) {
    coordinates = {
      top: originDir.top,
      left: rect.left,
      right: originDir.right,
      bottom: rect.bottom,
    };
  }
  
  
// 滑鼠按下後往右上方拖動
   if (
    rect.top <= originDir.top &&
    rect.left >= originDir.left &&
    rect.right >= originDir.right &&
    rect.bottom <= originDir.bottom
    ) {
     coordinates = {
        top: rect.top,
        left: originDir.left,
        right: rect.right,
        bottom: originDir.bottom,
    };
}

  // 滑鼠按下後往左上方拖動
  if (
    rect.top >= originDir.top &&
    rect.left >= originDir.left &&
    rect.right >= originDir.right &&
    rect.bottom >= originDir.bottom
  ) {
    coordinates = {
      top: originDir.top,
      left: originDir.left,
      right: rect.right,
      bottom: rect.bottom,
    };
  }

  renderNodes(coordinates);
};

<Table
    style={{ userSelect: 'none' }}
    onMouseDown={onMouseDown}
    onMouseMove={onMouseMove}
></Table>

渲染/清除樣式

遍歷表格中 dom 元素,如果該元素在圈選的區域內,為其添加選中的背景色,再為四邊形區域增加邊框。

這裡無論是直接設置 style 還是添加 classname 都不是很好。直接添加 classname 時,antd 會在 hover 操作時重置 classname,原來設置的 classname 會被覆蓋。直接設置 style 可能存在和其他設置衝突的情況,並且最後獲取所有圈選元素時比較麻煩。

以上兩種方法都嘗試過,最後選擇了直接往 dom 元素上面添加屬性,分別用5個屬性保存是否圈選,上下左右邊框,這裡沒有進行合併是因為一個dom元素可能同時存在這五個屬性。

const renderNodes = (coordinates) => {
  const nodes = document.querySelectorAll('.next-table-cell-wrapper');
  nodes.forEach((item) => {
    const target = item?.getBoundingClientRect();
    clearStyle(item);
    if (
      target?.top >= coordinates.top &&
      target?.right <= coordinates.right &&
      target?.left >= coordinates.left &&
      target?.bottom <= coordinates.bottom
    ) {
      item.setAttribute('data-brush', 'true');

      if (target.top === coordinates.top) {
        item.setAttribute('brush-border-top', 'true');
      }
      if (target.right === coordinates.right) {
        item.setAttribute('brush-border-right', 'true');
      }
      if (target.left === coordinates.left) {
        item.setAttribute('brush-border-left', 'true');
      }
      if (target.bottom === coordinates.bottom) {
        item.setAttribute('brush-border-bottom', 'true');
      }
    }
  });
};

const clearStyle = (item) => {
  item.hasAttribute('data-brush') && item.removeAttribute('data-brush');
  item.hasAttribute('brush-border-top') && item.removeAttribute('brush-border-top');
  item.hasAttribute('brush-border-right') && item.removeAttribute('brush-border-right');
  item.hasAttribute('brush-border-left') && item.removeAttribute('brush-border-left');
  item.hasAttribute('brush-border-bottom') && item.removeAttribute('brush-border-bottom');
};

使用 fusion 的 table 需要為每一個元素添加上透明的邊框,不然會出現佈局抖動的情況。(antd 不用)

 /* 為解決設置樣式抖動而設置 */
 .next-table td .next-table-cell-wrapper {
  border: 1px solid transparent;
 }

[brush-border-top="true"] {
  border-top: 1px solid #b93d06 !important;
}
[brush-border-right="true"] {
  border-right: 1px solid #b93d06 !important;
}
[brush-border-left="true"] {
  border-left: 1px solid #b93d06 !important;
}
[brush-border-bottom="true"] {
  border-bottom: 1px solid #b93d06 !important;
}
[data-brush="true"] {
  background-color: #f5f5f5 !important;
}

.next-table-row:hover {
  background-color: transparent !important;
}

滑鼠鬆開

為表格綁定滑鼠鬆開時觸發事件 mouseup

從滑鼠按下,到滑動,最後鬆開,是一整個圈選流程,在滑鼠按下時保存了初始的方位,滑動時判斷是否存在方位再進行計算,鬆開時將初始方位置空。

const onMouseUp = () => {
  originDir = {};
};

 <Table
    style={{ userSelect: 'none' }}
    onMouseDown={onMouseDown}
    onMouseMove={onMouseMove}
    onMouseUp={onMouseUp}
    ></Table>

到這一步,就已經實現了滑鼠圈選功能。

複製功能

表格圈選的交互效果其實是為複製功能做準備。

滑鼠右鍵複製

原表格在選中元素時滑鼠右鍵會出現【複製】按鈕,點擊後複製的效果是圖中圈選到的元素每一個都換行展示,圈選行為不能滿足使用需求,複製的內容也無法按照頁面中展示的行列格式。

而當我們實現圈選功能之後,因為使用 css 屬性 "user-select: none" 禁止用戶選擇文本,此時滑鼠右鍵已經不會出現複製按鈕。

為了實現滑鼠右鍵出現複製按鈕,我們需要覆蓋原滑鼠右鍵事件,自定義複製功能。

1、為表格綁定滑鼠右鍵事件 contextMenu

<Table
    style={{ userSelect: 'none' }}
    onMouseDown={onMouseDown}
    onMouseMove={onMouseMove}
    onMouseUp={onMouseUp}
    onContextMenu={onContextMenu}
></Table>

2、創建一個包含複製按鈕的自定義上下文菜單

<div id="contextMenu" className="context-menu" style={{ cursor: 'pointer' }}>
<div onClick={onClickCopy}>複製</div>
</div>

3、阻止預設的右鍵菜單彈出,將自定義上下文菜單添加到頁面中,並定位在滑鼠右鍵點擊的位置。

const onContextMenu = (event) => {
  event.preventDefault(); // 阻止預設右鍵菜單彈出

  const contextMenu = document.getElementById('contextMenu');

  // 定位上下文菜單的位置
  contextMenu.style.left = `${event.clientX}px`;
  contextMenu.style.top = `${event.clientY}px`;

  // 顯示上下文菜單
  contextMenu.style.display = 'block';
};

這裡複製按鈕沒有調整樣式,可根據自己項目情況進行一些美化。

4、點擊複製按鈕時,保存當前行列格式執行複製操作。

複製仍然保留表格的樣式,這裡想了很久,一直在想通過保存dom元素的樣式來實現,這種方案存在兩個問題,一是保存html樣式的api,document.execCommand('copy') 不被瀏覽器支持,二是表格元素都是行內元素,即使複製了樣式,也和頁面上看到的佈局不一樣。

最後採取的方案還是自己對是否換行進行處理,遍歷元素時判斷當前元素的 top 屬性和下一個點距離,如果相同則添加空字元串,不同則添加換行符 \n 。

const onClickCopy = () => {
    const contextMenu = document.getElementById('contextMenu');
    const copyableElements = document.querySelectorAll('[data-brush=true]');

    // 遍歷保存文本
    let copiedContent = '';
    copyableElements.forEach((element, index) => {
       let separator = ' ';
       if (index < copyableElements.length - 1) {
          const next = copyableElements?.[index + 1];
          if (next?.getBoundingClientRect().top !== element.getBoundingClientRect().top) {
              separator = '\n';
           }
        }
        copiedContent += `${element.innerHTML}${separator}`;
    });

    // 執行複製操作
    navigator.clipboard.writeText(copiedContent).then(() => {
       console.log('已複製內容:', copiedContent);
    }) .catch((error) => {
        console.error('複製失敗:', error);
    });

    // 隱藏上下文菜單
    contextMenu.style.display = 'none';
};

5、對滑鼠按下事件 onMouseDown 的處理

  • 滑鼠點擊右鍵也會觸發 onMouseDown ,這時會造成選中區域錯亂,需要通過 event.button 判斷當前事件觸發的滑鼠位置。
  • 滑鼠右鍵後如果沒有點擊複製按鈕而是滑走或者使用滑鼠左鍵選中,這時候相當於執行取消複製操作,複製按鈕的上下文需要清除。
const onMouseDown = (event) => {
  //  0:表示滑鼠左鍵。2:表示滑鼠右鍵。1:表示滑鼠中鍵或滾輪按鈕
  if (event.button !== 0) return;
  
  // 隱藏複製按鈕
  const contextMenu = document.getElementById('contextMenu');
  contextMenu.style.display = 'none';
};

到這裡,就已經實現了圈選滑鼠右鍵複製的功能。

ctrl+s / command+s 複製

使用 event.ctrlKey 來檢查 Ctrl 鍵是否按下,使用 event.metaKey 來檢查 Command 鍵是否按下,並使用 event.key 來檢查按下的鍵是否是 c 鍵。

useEffect(() => {
    const clickSave = (event) => {
      if ((event.ctrlKey || event.metaKey) && event.key === 'c') {
        onClickCopy();
        event.preventDefault(); // 阻止預設的保存操作
      }
    };

    document.addEventListener('keydown', clickSave);

    return () => {
      document.removeEventListener('keydown', clickSave);
    };
}, []);

antd 也可以使用

以上功能是在 fusion design 中實現的,在 antd 中也可以使用,語法稍有不同。

表格中滑鼠事件需要綁定在 onRow 函數中

 <Table
  style={{ userSelect: 'none' }}
  onRow={() => {
    return {
      onContextMenu,
      onMouseDown,
      onMouseMove,
      onMouseUp,
    };
  }}
>

獲取所有表格 dom 元素的類名替換一下

 const nodes = document.querySelectorAll('.ant-table-cell');

覆蓋表格 hover 時樣式

 .ant-table-cell-row-hover {
    background: transparent;
  }

  .ant-table-wrapper .ant-table .ant-table-tbody > tr.ant-table-row:hover > td,
  .ant-table-wrapper .ant-table .ant-table-tbody > tr > td.ant-table-cell-row-hover {
    background: transparent;
  }

實現效果是這樣的

完整代碼

完整代碼在這裡 table-brush-copy,包括 fusion design 和 ant design 兩個版本。

總結

表格圈選複製功能的實現主要是以下五步

  • mousedown 按下滑鼠,記錄初始坐標
  • mousemove 滑動滑鼠,計算所形成的四邊形區域
  • mouseup 鬆開滑鼠,清空初始坐標
  • contextmenu 自定義滑鼠右鍵事件,定位上下文事件
  • keydown 監聽鍵盤按下位置,判斷是否為複製操作

集合了較多的滑鼠、鍵盤事件,以及 javascript 獲取屬性、元素。


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

-Advertisement-
Play Games
更多相關文章
  • 問題代碼: xmal:一個按鈕+一個顯示框 1 <Button Width="100" Height="50" Margin="10" Click="Button_Click">test</Button> 2 <TextBox x:Name="display" Width="300" Height= ...
  • 大家好,我是沙漠盡頭的狼。 本文首發於Dotnet9,介紹使用Lib.Harmony庫攔截第三方.NET庫方法,達到不修改其源碼並能實現修改方法邏輯、預期行為的效果,並且不限於只攔截public訪問修飾的類及方法,行文目錄: 什麼是方法攔截? 示常式序攔截 非public方法怎麼攔截? 總結 1. ...
  • 周末,寫點簡單的水一下。 新版本的vs創建項目的時候可以選擇自帶一個swagger。然而這隻是基本的swagger功能。 幾個介面無所謂啦,隨著介面越來越多,就這麼丟給你,一時間也會懵逼,所以這篇文章要做的有兩個功能。 給swagger文檔添加註釋 給swagger添加切換“版本”的功能(也可以理解 ...
  • 痞子衡嵌入式半月刊: 第 81 期 這裡分享嵌入式領域有用有趣的項目/工具以及一些熱點新聞,農曆年分二十四節氣,希望在每個交節之日準時發佈一期。 本期刊是開源項目(GitHub: JayHeng/pzh-mcu-bi-weekly),歡迎提交 issue,投稿或推薦你知道的嵌入式那些事兒。 上期回顧 ...
  • Redis 命令工具 redis-server Redis 伺服器啟動命令 redis-cli shutdown 停止服務 redis-benchmark:性能測試工具,用於檢測 Redis 在本機的運行效率 redis-check-aof:修複有問題的 AOF 持久化文件 redis-check- ...
  • Vue-router路由 什麼是vue-router? 服務端路由指的是伺服器根據用戶訪問的 URL 路徑返回不同的響應結果。當我們在一個傳統的服務端渲染的 web 應用中點擊一個鏈接時,瀏覽器會從服務端獲得全新的 HTML,然後重新載入整個頁面。 然而,在單頁面應用中,客戶端的 JavaScrip ...
  • React和Vue是前端開發中的兩大熱門框架,各自都有著強大的功能和豐富的生態系統。然而,你有沒有想過,在一個項目中同時使用React和Vue?是的,你沒有聽錯,可以在同一個項目中混用這兩個框架!本文就來分享 3 個用於混合使用 React 和 Vue 的工具! Veaury Veaury 是一個基 ...
  • UI組件庫提供了各種常見的 UI 元素,比如按鈕、輸入框、菜單等,只需要調用相應的組件並按照需求進行配置,就能夠快速構建出一個功能完善的 UI。 雖然市面上有許多不同的UI組件庫可供選擇,但在2023年底也並沒有出現一兩個明確的解決方案能夠適用於所有情況。因為不同的前端框架(例如React、Angu ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...