乐趣区

关于前端:antdfusion表格增加圈选复制功能

背景介绍

咱们存在着大量在 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 两个版本,欢送大家来点个 star。

总结

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

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

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

退出移动版