antd/fusion表格增加圈选复制功能

背景介绍

我们存在着大量在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);
};<Tablestyle={{ 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 = {};
};<Tablestyle={{ userSelect: 'none' }}onMouseDown={onMouseDown}onMouseMove={onMouseMove}onMouseUp={onMouseUp}></Table>

到这一步,就已经实现了鼠标圈选功能。

在这里插入图片描述

复制功能

表格圈选的交互效果其实是为复制功能做准备。

鼠标右键复制

原表格在选中元素时鼠标右键会出现【复制】按钮,点击后复制的效果是图中圈选到的元素每一个都换行展示,圈选行为不能满足使用需求,复制的内容也无法按照页面中展示的行列格式。

在这里插入图片描述

而当我们实现圈选功能之后,因为使用 css 属性 “user-select: none” 禁止用户选择文本,此时鼠标右键已经不会出现复制按钮。

在这里插入图片描述

为了实现鼠标右键出现复制按钮,我们需要覆盖原鼠标右键事件,自定义复制功能。

1、为表格绑定鼠标右键事件 contextMenu

<Tablestyle={{ 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 函数中

 <Tablestyle={{ 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 两个版本,欢迎大家来点个 star。

总结

表格圈选复制功能的实现主要是以下五步

  • mousedown 按下鼠标,记录初始坐标
  • mousemove 滑动鼠标,计算所形成的四边形区域
  • mouseup 松开鼠标,清空初始坐标
  • contextmenu 自定义鼠标右键事件,定位上下文事件
  • keydown 监听键盘按下位置,判断是否为复制操作

集合了较多的鼠标、键盘事件,以及 javascript 获取属性、元素。

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

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

相关文章

Spring之bean的生命周期源码解析

Spring最重要的功能就是帮助程序员创建对象&#xff08;也就是IOC&#xff09;&#xff0c;而启动Spring就是为创建Bean对象做准备&#xff0c;所以我们先明白Spring到底是怎么去创建Bean的&#xff0c;也就是先弄明白Bean的生命周期。 Bean的生命周期就是指&#xff1a;在Spr…

Nodejs 相关知识

Nodejs是一个js运行环境&#xff0c;可以让js开发后端程序&#xff0c;实现几乎其他后端语言实现的所有功能&#xff0c;能够让js与其他后端语言平起平坐。 nodejs是基于v8引擎&#xff0c;v8是Google发布的开源js引擎&#xff0c;本身就是用于chrome浏览器的js解释部分&#…

帆软FineReport决策报表之页面布局

最近在用帆软决策报表绘制首页大屏&#xff0c;记录使用过程&#xff0c;方便查看。 版本&#xff1a;FineReport10.0 第一步、页面布局 页面布局其实就是组件的排列组合&#xff0c;决策报表主区域body有两种布局方式&#xff1a;自适应布局和绝对布局。 1&#xff09;自适应…

第一百五十三回 如何实现滑动窗口

文章目录 概念介绍实现方法示例代码 我们在上一章回中介绍了自定义组件实现游戏摇杆相关的内容&#xff0c;本章回中将介绍 如何实现滑动窗口.闲话休提&#xff0c;让我们一起Talk Flutter吧。 概念介绍 我们在本章回中介绍的滑动窗口表示在屏幕底部向上滑动时弹出一个窗口&a…

【Unity3D赛车游戏制作】开始界面场景搭建

&#x1f468;‍&#x1f4bb;个人主页&#xff1a;元宇宙-秩沅 &#x1f468;‍&#x1f4bb; hallo 欢迎 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! &#x1f468;‍&#x1f4bb; 本文由 秩沅 原创 &#x1f468;‍&#x1f4bb; 收录于专栏&#xff1a;Uni…

SpringSecurity授权

目录 一、RABC的介绍 二、权限表设计 三、编写权限控制方法 &#xff08;1&#xff09;mapper接口 &#xff08;2&#xff09;映射文件 &#xff08;3&#xff09;修改认证逻辑 四、配置类访问资源 五、自定义访问控制逻辑 &#xff08;1&#xff09;自定义 &#xf…

LeakCanary 源码详解(3)

上一篇&#xff1a;LeakCanary源码详解&#xff08;2&#xff09; 如果你是直接刷到这篇的&#xff0c;建议还是从1开始看&#xff0c;然后2&#xff0c;然后是这篇3&#xff0c;如果你只关注这篇的重点hprof 文件定位泄漏位置的感兴趣&#xff0c;可以试试直接读这篇&#xff…

23种设计模式

目录 一、设计模式学前导读 1、代码质量好坏如何评价 ? 2、编程方法论 二、设计模式概述 设计模式的产生 UML图 三、六大设计原则 1、单一职责原则&#xff08;Single Responsibitity Principle&#xff09; 2、开放封闭原则&#xff08;Open Close Principle&#x…

利用亚马逊 云服务器 EC2 和S3免费套餐搭建私人网盘

网盘是一种在线存储服务&#xff0c;提供文件存储&#xff0c;访问&#xff0c;备份&#xff0c;贡献等功能&#xff0c;是我们日常中不可或缺的一种服务。很多互联网公司都为个人和企业提供免费的网盘服务。但这些免费服务都有一些限制&#xff0c;比如限制下载速度&#xff0…

win10系统 C++环境 安装编译GRPC

第一步 下载源码、更新、cmake编译&#xff1a; 为了依赖的成功安装&#xff0c;采用gitee进行下载与更新。记得需要安装git软件。 安装命令&#xff1a; 在自己指定的目录下&#xff0c;鼠标右键&#xff0c;选择 git Bash Here 打开命令行 git clone -b v1.34.0 https://gi…

中秋国庆内卷之我爱学习C++

文章目录 前言Ⅰ. 内联函数0x00 内联函数和宏的比较0x01 内联函数的概念0x02 内联函数的特性 Ⅱ. auto&#xff08;C 11)0x00 auto的概念0x01 auto的用途 Ⅲ. 范围for循环(C11)0x00 基本用法0x01 范围for循环(C11)的使用条件 Ⅳ. 指针空值nullptr(C11)0x00 概念 前言 亲爱的夏…

Flutter实现PS钢笔工具,实现高精度抠图的效果。

演示&#xff1a; 代码&#xff1a; import dart:ui;import package:flutter/material.dart hide Image; import package:flutter/services.dart; import package:flutter_screenutil/flutter_screenutil.dart; import package:kq_flutter_widgets/widgets/animate/stack.dart…

【Vue】快速入门和生命周期

目录 前言 一、vue的介绍 1. Vue.js是什么&#xff1f; 2. 库和框架的区别 3.基本概念和用法&#xff1a; 二、MVVM的介绍 1. 什么是MVVM&#xff1f; 2. MVVM的组成部分 3. MVVM的工作流程 4. MVVM的优势 5. MVVM的应用场景 三、vue实例 1.模板语法&#xff1a; …

智慧公厕是提升公共厕所管理服务能力的创新举措

在城市化进程加速的今天&#xff0c;公共厕所的管理问题成为让人头疼的难题。随着智慧科技的发展&#xff0c;智慧公厕应运而生&#xff0c;为提升公共厕所综合管理服务能力提供了新思路和解决方案。本文将以智慧公厕领先厂家广州中期科技有限公司&#xff0c;大量精品案例项目…

卡尔曼滤波(Kalman Filter)原理浅析-数学理论推导-4

目录 前言数学理论推导1. 直观理解与二维实例2. EKF3. 补充知识-线性化结语参考 前言 最近项目需求涉及到目标跟踪部分&#xff0c;准备从 DeepSORT 多目标跟踪算法入手。DeepSORT 中涉及的内容有点多&#xff0c;以前也就对其进行了简单的了解&#xff0c;但是真正去做发现总是…

搞定ESD(一):静电放电测试标准解析

文章目录 一、基本术语与定义1.1 基本定义1.2 重要基本术语 二、静电放电发生器介绍2.1 静电放电发生器的特性&#xff1a;通用规范【GB/T17626.2-2018 标准】2.2 ESD 放电发生器电极规格要求&#xff1a;通用规范【GB/T17626.2-2018 标准】2.3 放电回路电缆的要求&#xff1a;…

黑马JVM总结(十四)

&#xff08;1&#xff09;分代回收_1 Java虚拟机都是结合前面几种算法&#xff0c;让他们协同工作&#xff0c;具体实现是虚拟机里面一个叫做分代的垃圾回收机制&#xff0c;把我们堆内存大的区域划分为两块新生代、老年代 新生代有划分为伊甸园、幸存区Form、幸存区To 为什…

进程同步与互斥

目录 进程同步与互斥&#xff08;1&#xff09; 第一节、进程间相互作用 一、相关进程和无关进程 二、与时间有关的错误 第二节、进程同步与互斥 一、进程的同步 二、进程的互斥 三、临界区 进程同步与互斥&#xff08;2&#xff09; 三、信号量与P、V操作的物理含义…

防泄密软件推荐(数据防泄漏软件好用榜前五名)

在当今的数字化时代&#xff0c;数据已经成为企业最宝贵的资产之一。企业需要依赖数据来驱动业务决策、提高运营效率和创新产品。然而&#xff0c;随着数据量的不断增长&#xff0c;数据安全问题也日益凸显。企业需要采取有效的措施来保护敏感数据&#xff0c;防止信息泄露给竞…

算法通关村第16关【青铜】| 滑动窗口思想

1. 滑动窗口的基本思想 一句话概括就是两个快慢指针维护的一个会移动的区间 固定大小窗口&#xff1a;求哪个窗口元素最大、最小、平均值、和最大、和最小 可变大小窗口&#xff1a;求一个序列里最大、最小窗口是什么 2. 两个入门题 &#xff08;1&#xff09;子数组最大平…