antd/fusion表格增加圈選復(fù)制功能思路詳解
背景介紹
我們存在著大量在PC頁面通過表格看數(shù)據(jù)業(yè)務(wù)場(chǎng)景,表格又分為兩種,一種是 antd / fusion 這種基于 dom 元素的表格,另一種是通過 canvas 繪制的類似 excel 的表格。
基于 dom 的表格功能豐富較為美觀,能實(shí)現(xiàn)多表頭、合并單元格和各種自定義渲染(如表格中渲染圖形 / 按鈕 / 進(jìn)度條 / 單選框 / 輸入框),以展示為主,不提供圈選、整列復(fù)制等功能。
canvas 繪制的類 excel 外表樸素更為實(shí)用,大量數(shù)據(jù)渲染不卡頓,操作類似 excel,能行/列選中,圈選、復(fù)制等功能。
兩者使用場(chǎng)景有所差異,各有利弊,但業(yè)務(wù)方不希望一套系統(tǒng)中出現(xiàn)兩種類型的交互,期望能將兩種表格的優(yōu)缺點(diǎn)進(jìn)行融合,在美觀的dom表格中增加圈選、復(fù)制的功能。
圈選效果
業(yè)務(wù)方所期望的圈選效果和excel類似,鼠標(biāo)按下即選中元素,然后滑動(dòng)鼠標(biāo),鼠標(biāo)所經(jīng)過形成的四邊形就是選中區(qū)域,此時(shí)鼠標(biāo)右鍵點(diǎn)擊復(fù)制按鈕,或者鍵盤按下 ctrl + c 復(fù)制文本。
而dom表格經(jīng)過如上操作,會(huì)把一整行數(shù)據(jù)都選上,不符合業(yè)務(wù)同學(xué)的使用預(yù)期。
實(shí)現(xiàn)過程
去除默認(rèn)樣式
我們需要自行定義鼠標(biāo)事件、元素樣式,需要先將無用的默認(rèn)樣式清除,包括上圖中的 hover 和選中元素的背景色。
禁用表格本身的鼠標(biāo)點(diǎn)擊選擇功能,設(shè)置css,userSelect: none
<Table style={{ userSelect: 'none' }} ></Table>
去除 hover 樣式(這里使用的是 fusion 組件)
.next-table-row:hover { background-color: transparent !important; }
鼠標(biāo)按下,記錄選中元素
為表格綁定鼠標(biāo)按鍵時(shí)觸發(fā)事件 mousedown
。
當(dāng)鼠標(biāo)按下時(shí),這個(gè)元素就是中心元素,無論是向哪個(gè)方向旋轉(zhuǎn),所形成的區(qū)域一定會(huì)包含初始選中的元素。
getBoundingClientRect()
用于獲得頁面中某個(gè)元素的上下左右分別相對(duì)瀏覽器視窗的位置。
const onMouseDown = (event) => { const rect = event.target.getBoundingClientRect(); // funsion 判斷點(diǎn)擊是否為表頭元素,為否時(shí)才繼續(xù)后面的邏輯。antd 不需要判斷,因?yàn)辄c(diǎn)擊表頭不會(huì)觸發(fā)該事件 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>
鼠標(biāo)滑過
為表格綁定鼠標(biāo)滑過時(shí)觸發(fā)事件 mousemove
。
根據(jù)滑動(dòng)元素的上下左右距離與鼠標(biāo)按下時(shí)的位置進(jìn)行判斷,圈選元素存在四個(gè)方向,以第一次選中的元素為中心位置。滑動(dòng)時(shí)元素位于鼠標(biāo)按下的右下、左下、右上、左上方,根據(jù)不同的情況來設(shè)置四個(gè)角的方位。
const onMouseMove = (event) => { if (!originDir.top) return; const rect = event.target.getBoundingClientRect(); let coordinates = {}; // 鼠標(biāo)按下后往右下方拖動(dòng) 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, }; } // 鼠標(biāo)按下后往左下方拖動(dòng) 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, }; } // 鼠標(biāo)按下后往右上方拖動(dòng) 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, }; } // 鼠標(biāo)按下后往左上方拖動(dòng) 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 元素,如果該元素在圈選的區(qū)域內(nèi),為其添加選中的背景色,再為四邊形區(qū)域增加邊框。
這里無論是直接設(shè)置 style 還是添加 classname 都不是很好。直接添加 classname 時(shí),antd 會(huì)在 hover 操作時(shí)重置 classname,原來設(shè)置的 classname 會(huì)被覆蓋。直接設(shè)置 style 可能存在和其他設(shè)置沖突的情況,并且最后獲取所有圈選元素時(shí)比較麻煩。
以上兩種方法都嘗試過,最后選擇了直接往 dom 元素上面添加屬性,分別用5個(gè)屬性保存是否圈選,上下左右邊框,這里沒有進(jìn)行合并是因?yàn)橐粋€(gè)dom元素可能同時(shí)存在這五個(gè)屬性。
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 需要為每一個(gè)元素添加上透明的邊框,不然會(huì)出現(xiàn)布局抖動(dòng)的情況。(antd 不用)
/* 為解決設(shè)置樣式抖動(dòng)而設(shè)置 */ .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; }
鼠標(biāo)松開
為表格綁定鼠標(biāo)松開時(shí)觸發(fā)事件 mouseup
。
從鼠標(biāo)按下,到滑動(dòng),最后松開,是一整個(gè)圈選流程,在鼠標(biāo)按下時(shí)保存了初始的方位,滑動(dòng)時(shí)判斷是否存在方位再進(jìn)行計(jì)算,松開時(shí)將初始方位置空。
const onMouseUp = () => { originDir = {}; }; <Table style={{ userSelect: 'none' }} onMouseDown={onMouseDown} onMouseMove={onMouseMove} onMouseUp={onMouseUp} ></Table>
到這一步,就已經(jīng)實(shí)現(xiàn)了鼠標(biāo)圈選功能。
復(fù)制功能
表格圈選的交互效果其實(shí)是為復(fù)制功能做準(zhǔn)備。
鼠標(biāo)右鍵復(fù)制
原表格在選中元素時(shí)鼠標(biāo)右鍵會(huì)出現(xiàn)【復(fù)制】按鈕,點(diǎn)擊后復(fù)制的效果是圖中圈選到的元素每一個(gè)都換行展示,圈選行為不能滿足使用需求,復(fù)制的內(nèi)容也無法按照頁面中展示的行列格式。
而當(dāng)我們實(shí)現(xiàn)圈選功能之后,因?yàn)槭褂?css 屬性 "user-select: none" 禁止用戶選擇文本,此時(shí)鼠標(biāo)右鍵已經(jīng)不會(huì)出現(xiàn)復(fù)制按鈕。
為了實(shí)現(xiàn)鼠標(biāo)右鍵出現(xiàn)復(fù)制按鈕,我們需要覆蓋原鼠標(biāo)右鍵事件,自定義復(fù)制功能。
1、為表格綁定鼠標(biāo)右鍵事件 contextMenu
<Table style={{ userSelect: 'none' }} onMouseDown={onMouseDown} onMouseMove={onMouseMove} onMouseUp={onMouseUp} onContextMenu={onContextMenu} ></Table>
2、創(chuàng)建一個(gè)包含復(fù)制按鈕的自定義上下文菜單
<div id="contextMenu" className="context-menu" style={{ cursor: 'pointer' }}> <div onClick={onClickCopy}>復(fù)制</div> </div>
3、阻止默認(rèn)的右鍵菜單彈出,將自定義上下文菜單添加到頁面中,并定位在鼠標(biāo)右鍵點(diǎn)擊的位置。
const onContextMenu = (event) => { event.preventDefault(); // 阻止默認(rèn)右鍵菜單彈出 const contextMenu = document.getElementById('contextMenu'); // 定位上下文菜單的位置 contextMenu.style.left = `${event.clientX}px`; contextMenu.style.top = `${event.clientY}px`; // 顯示上下文菜單 contextMenu.style.display = 'block'; };
這里復(fù)制按鈕沒有調(diào)整樣式,可根據(jù)自己項(xiàng)目情況進(jìn)行一些美化。
4、點(diǎn)擊復(fù)制按鈕時(shí),保存當(dāng)前行列格式執(zhí)行復(fù)制操作。
復(fù)制仍然保留表格的樣式,這里想了很久,一直在想通過保存dom元素的樣式來實(shí)現(xiàn),這種方案存在兩個(gè)問題,一是保存html樣式的api,document.execCommand('copy') 不被瀏覽器支持,二是表格元素都是行內(nèi)元素,即使復(fù)制了樣式,也和頁面上看到的布局不一樣。
最后采取的方案還是自己對(duì)是否換行進(jìn)行處理,遍歷元素時(shí)判斷當(dāng)前元素的 top 屬性和下一個(gè)點(diǎn)距離,如果相同則添加空字符串,不同則添加換行符 \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}`; }); // 執(zhí)行復(fù)制操作 navigator.clipboard.writeText(copiedContent).then(() => { console.log('已復(fù)制內(nèi)容:', copiedContent); }) .catch((error) => { console.error('復(fù)制失敗:', error); }); // 隱藏上下文菜單 contextMenu.style.display = 'none'; };
5、對(duì)鼠標(biāo)按下事件 onMouseDown 的處理
- 鼠標(biāo)點(diǎn)擊右鍵也會(huì)觸發(fā) onMouseDown ,這時(shí)會(huì)造成選中區(qū)域錯(cuò)亂,需要通過 event.button 判斷當(dāng)前事件觸發(fā)的鼠標(biāo)位置。
- 鼠標(biāo)右鍵后如果沒有點(diǎn)擊復(fù)制按鈕而是滑走或者使用鼠標(biāo)左鍵選中,這時(shí)候相當(dāng)于執(zhí)行取消復(fù)制操作,復(fù)制按鈕的上下文需要清除。
const onMouseDown = (event) => { // 0:表示鼠標(biāo)左鍵。2:表示鼠標(biāo)右鍵。1:表示鼠標(biāo)中鍵或滾輪按鈕 if (event.button !== 0) return; // 隱藏復(fù)制按鈕 const contextMenu = document.getElementById('contextMenu'); contextMenu.style.display = 'none'; };
到這里,就已經(jīng)實(shí)現(xiàn)了圈選鼠標(biāo)右鍵復(fù)制的功能。
ctrl+s / command+s 復(fù)制
使用 event.ctrlKey
來檢查 Ctrl 鍵是否按下,使用 event.metaKey
來檢查 Command 鍵是否按下,并使用 event.key
來檢查按下的鍵是否是 c 鍵。
useEffect(() => { const clickSave = (event) => { if ((event.ctrlKey || event.metaKey) && event.key === 'c') { onClickCopy(); event.preventDefault(); // 阻止默認(rèn)的保存操作 } }; document.addEventListener('keydown', clickSave); return () => { document.removeEventListener('keydown', clickSave); }; }, []);
antd 也可以使用
以上功能是在 fusion design 中實(shí)現(xiàn)的,在 antd 中也可以使用,語法稍有不同。
表格中鼠標(biāo)事件需要綁定在 onRow 函數(shù)中
<Table style={{ userSelect: 'none' }} onRow={() => { return { onContextMenu, onMouseDown, onMouseMove, onMouseUp, }; }} >
獲取所有表格 dom 元素的類名替換一下
const nodes = document.querySelectorAll('.ant-table-cell');
覆蓋表格 hover 時(shí)樣式
.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; }
實(shí)現(xiàn)效果是這樣的
完整代碼
完整代碼在這里 table-brush-copy,包括 fusion design 和 ant design 兩個(gè)版本。
總結(jié)
表格圈選復(fù)制功能的實(shí)現(xiàn)主要是以下五步
- mousedown 按下鼠標(biāo),記錄初始坐標(biāo)
- mousemove 滑動(dòng)鼠標(biāo),計(jì)算所形成的四邊形區(qū)域
- mouseup 松開鼠標(biāo),清空初始坐標(biāo)
- contextmenu 自定義鼠標(biāo)右鍵事件,定位上下文事件
- keydown 監(jiān)聽鍵盤按下位置,判斷是否為復(fù)制操作
集合了較多的鼠標(biāo)、鍵盤事件,以及 javascript 獲取屬性、元素。
到此這篇關(guān)于antd/fusion表格增加圈選復(fù)制功能的文章就介紹到這了,更多相關(guān)antd/fusion表格圈選復(fù)制內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
JavaScript設(shè)計(jì)模式之構(gòu)造器模式(生成器模式)定義與用法實(shí)例分析
這篇文章主要介紹了JavaScript設(shè)計(jì)模式之構(gòu)造器模式(生成器模式)定義與用法,結(jié)合實(shí)例形式分析了javascript構(gòu)造器模式的概念、原理、與工廠模式的區(qū)別以及相關(guān)使用方法,需要的朋友可以參考下2018-07-07JS注釋所產(chǎn)生的bug 即使注釋也會(huì)執(zhí)行
寫js時(shí)出現(xiàn)個(gè)bug一直提示我JAVA類中的一個(gè)屬性沒有,可是明明注釋掉了,后來才知道,JS里即使注釋也會(huì)執(zhí)行2013-11-11Javascript 更新 JavaScript 數(shù)組的 uniq 方法
2008-01-01詳解webpack-dev-server使用http-proxy解決跨域問題
這篇文章主要介紹了詳解webpack-dev-server使用http-proxy解決跨域問題,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-01-01JavaScript HTML DOM 元素 (節(jié)點(diǎn))新增,編輯,刪除操作實(shí)例分析
這篇文章主要介紹了JavaScript HTML DOM 元素 (節(jié)點(diǎn))新增,編輯,刪除操作,結(jié)合實(shí)例形式分析了JavaScript針對(duì)HTML DOM 元素 (節(jié)點(diǎn))的新增,編輯,刪除相關(guān)操作技巧與使用注意事項(xiàng),需要的朋友可以參考下2020-03-03