HTML 元素(主要是文本)能否被选中,是由 user-select css 属性控制的,若设置为 none 则不可选中,更多属性值参考 MDN.
HTML 页面的默认选中方式是行选择模式,即鼠标从按下到释放中间经过的所有行都会被选中。若要实现列选中模式或是任意选中模式,基本思路是:将表格所有单元格设置为不可选中,在鼠标经过时,将对应的单元格设置可选中,即可实现任意选择的模式。 以上思路有几点需要注意的:
浏览器适配:完整的设置不可选中的样式为: -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none;不可选中的元素:不一定是给单元格 td 设置不可选中,而应该给直接包裹文字的元素设置(如下例中是 td 中 class 为 cell的 div)。框选模式:该思路只能直线涂抹选中,即鼠标经过的 cell 会被选中。若想实现画对角线进行框选,还需要添加逻辑。事件:会涉及的事件:mousedown,mousemove,mouseup。若使用 jquery 则可以很方便的进行事件注册和 DOM操作,若使用 vue 则可以通过自定义指令 directives 得到需要操作的 DOM元素。示例代码(Vue + elementUI):
const selectDisableStyle = `-webkit-user-select:none; -moz-user-select: none; -ms-user-select: none; user-select: none;` ... directives: { areaSelect: { // 在需要自定义选择的元素上添加 v-areaSelect inserted: (el, binding, vnode) => { let randIds = new Map() let mouseDownFlag = false let mouseUpFlag = false let cells = [] el.addEventListener('mousedown', function (event) { mouseDownFlag = true mouseUpFlag = false cells = [] el.querySelectorAll('tr').forEach(tr => { let row = tr.querySelectorAll('td div.cell') row.length > 0 && cells.push(row) }) cells.forEach((tdRow, idy) => { tdRow.forEach((tdCol, idx) => { const style = tdCol.getAttribute('style') if (style.indexOf(selectDisableStyle) < 0) { tdCol.setAttribute('style', style + selectDisableStyle) } // 若表格有 rowIndex ,cellIndex 则可不设 id tdCol.setAttribute('id', `${idy + 1}_${idx + 1}`) }) }) // 选中点击的 cell removeStyle(event) }) function mouseMove(evt) { if (mouseUpFlag || !mouseDownFlag) { return } // 缓存经过的 cell id randIds.set(evt.target.id, evt.target.id) // 选中 removeStyle(evt) } el.addEventListener('mousemove', mouseMove) el.addEventListener('mouseup', function (evt) { mouseUpFlag = true mouseDownFlag = false // 框选逻辑 let posList = Array.from(randIds).filter(v => v[0]).map(v => v[0]).map(v => v.split('_')) let posYList = posList.map(v => v[0]) let posXList = posList.map(v => v[1]) let minX = Math.min(...posXList), minY = Math.min(...posYList) let maxX = Math.max(...posXList), maxY = Math.max(...posYList) cells.forEach(cellRow => { cellRow.forEach(cell => { let [idy, idx] = cell.id.split('_').map(v => Number(v)) if (idx >= minX && idx <= maxX && idy >= minY && idy <= maxY) { removeStyle(cell) } }) }) // 重置 randIds = new Map() cells = [] }) } } } // 清除禁止选中的样式,同时选中 function removeStyle(evt) { let target = evt.target || evt let style = target.getAttribute('style') || selectDisableStyle let reg = new RegExp(selectDisableStyle, 'g') target.setAttribute('style', style.replace(reg, '')) }该方法虽然可实现任意区域框选,但复制的操作仍然不理想,复制到 Excel 中仍然会复制整行(可能是 ElementUI 的行为),复制到文本编辑器,多行多列的内容也会被合并为一列(单元格内容被换行或制表符分割)。
第二种思路 不去依赖浏览器的默认复制操作,而是自动将被复制内容写入剪切板。依然可以借鉴上一方法中对各种事件的监听,以及区域框选算法。只是对鼠标经过的单元格,不是设置 user-select:none 之类的样式,而是将单元格添加边框,以示选中。执行区域选中之后,程序是可以知道哪些单元格被选中的,此时可以将这些单元格的内容以想要的格式写入剪切板。
写入剪切板的思路:利用一个不可见 input 元素(若需要多行内容可以使用 textarea),将要复制的文本写入,再执行 setSelectionRange 选中,然后执行 document.execCommand('copy'),将 value 写入系统剪切板。
操作方式:按住 Ctrl 再使用鼠标选择,鼠标释放时自动框选,并将内容复制到剪切板。若不按 Ctrl 则仍旧可以使用浏览器自身的行选择模式。
const selectStyle = 'border: 1px solid rgb(51,144,255); box-shadow: 0px 0px 5px 1px rgb(51,144,255);' ... mounted() { // 按下 control 键 // isCtrlPressed => this.ctrlPress > 0 document.onkeydown = (e) => { if (e.keyCode === 17) { this.ctrlPress += 1 } } document.onkeyup = (e) => { if (e.keyCode === 17) { this.ctrlPress = 0 } } }, directives: { areaSelect: { inserted: (el, binding, vnode) => { let randIds = new Map() let mouseDownFlag = false let mouseUpFlag = false const vm = vnode.context // 获取当前组件的 Vue 实例 let cells = [] // 表格中所有 cell let selectedCells = [] // 最终选中的 cell // 复制之后清除选中样式,单击会与现有事件冲突,改为双击 document.addEventListener('dblclick', function () { if (!el) { // 该事件不好注销,故加此判断 return } el.querySelectorAll('tr').forEach(tr => { let row = tr.querySelectorAll('td div.cell') row.forEach(tdCol => { tdCol.setAttribute('style', "") }) }) }) el.addEventListener('mousedown', function (event) { if (!vm.isCtrlPressed) { return } mouseDownFlag = true mouseUpFlag = false cells = [] el.querySelectorAll('tr').forEach(tr => { let row = tr.querySelectorAll('td div.cell') row.length > 0 && cells.push(row) }) cells.forEach((tdRow, idy) => { tdRow.forEach((tdCol, idx) => { const style = tdCol.getAttribute('style') // 为了界面简洁明了,选择过程中仍然禁止浏览器自身选中行为 if (style.indexOf(selectDisableStyle) < 0) { tdCol.setAttribute('style', style + selectDisableStyle) } tdCol.setAttribute('id', `${idy + 1}_${idx + 1}`) }) }) // 选中点击的 cell selectCell(event) }) el.addEventListener('mousemove', function mouseMove(evt) { if (!vm.isCtrlPressed) { return } if (mouseUpFlag || !mouseDownFlag) { return } // 缓存经过的 cell id randIds.set(evt.target.id, evt.target.id) selectCell(evt) }) el.addEventListener('mouseup', function (evt) { if (!vm.isCtrlPressed) { return } mouseUpFlag = true mouseDownFlag = false let posList = Array.from(randIds).filter(v => v[0]).map(v => v[0]).map(v => v.split('_')) let posYList = posList.map(v => v[0]) let posXList = posList.map(v => v[1]) let minX = Math.min(...posXList), minY = Math.min(...posYList) let maxX = Math.max(...posXList), maxY = Math.max(...posYList) cells.forEach(cellRow => { let selectedRow = [] cellRow.forEach(cell => { let [idy, idx] = cell.id.split('_').map(v => Number(v)) if (idx >= minX && idx <= maxX && idy >= minY && idy <= maxY) { selectCell(cell) selectedRow.push(cell) } // 去除禁止选择的样式,仍然支持浏览器自身的行选择模式 removeStyle(cell) }) selectedRow.length > 0 && selectedCells.push(selectedRow) }) // WPS 默认单元格以 \t 分割,行以 \n 分割 copyToClipboard(selectedCells.map(v => v).map(row => row.map(cell => cell.innerText).join("\t")).join("\n")) vm.$message.success("内容已复制到剪切板!") selectedCells = [] randIds = new Map() cells = [] }) } } } function removeStyle(evt) { let target = evt.target || evt let style = target.getAttribute('style') || selectDisableStyle let reg = new RegExp(selectDisableStyle, 'g') target.setAttribute('style', style.replace(reg, '')) } function selectCell(evt) { let target = evt.target || evt // 可能会有其他元素进入,导致样式不美观 if (target.getAttribute('class').indexOf('cell') < 0) { return } const style = target.getAttribute('style') if (style.indexOf(selectStyle) < 0) { target.setAttribute('style', style + ';' + selectStyle) } } function copyToClipboard(text) { const input = document.createElement('TEXTAREA'); input.style.opacity = 0; input.style.position = 'absolute'; input.style.left = '-100000px'; document.body.appendChild(input); input.value = text; input.select(); input.setSelectionRange(0, text.length); document.execCommand('copy'); document.body.removeChild(input); }效果: