在很多时候,数据必须以表格的形式呈现,考虑到在未知单元格内容量的情况下,要达到左右高度对齐,必须使用<table>元素来展现。但如果此时需要仅能在其中的一列(或若干列)中选择并复制文字,则需要一些额外的工作。
固定单列可选
一个典型的场景是代码的展示,我们不希望在复制代码的时候把行号也一起复制进去:

这种固定某一个列不能选可以使用纯CSS来实现,其逻辑是:
- 默认情况下,选中文字并不会包含
::before和::after生成的伪元素。 - CSS的
content属性支持使用attr来引用某一个属性的内容。
那么综合下来,我们不要将行号写在<td>元素内,而是作为一个自定义属性,再使用::before伪元素去获取即可,代码类似于:
<table>
<tbody>
<tr>
<th data-line-number="1"></th>
<td>if (foo) {</td>
</tr>
<tr>
<th data-line-number="2"></th>
<td> bar();</td>
</tr>
<tr>
<th data-line-number="3"></th>
<td>}</td>
</tr>
</tbody>
</table>配合相应的CSS:
table {
width: 100%;
font-family: monospace;
}
th {
width: 2ch;
}
th::before {
content: attr(data-line-number);
}
td {
white-space: pre;
}可以在此处看到示例:http://jsfiddle.net/owb2uk89/4/
非固定列可选
一种更复杂的情况是,我们可选择的列并不是固定的,一个经典的场景是代码的diff展示:

可以看到在Github的diff展示中,虽然对行号进行了处理,但依旧会同时选中左右两列的代码,复制出来的内容是不可用的。
在这个场景下,我们需要的是通过鼠标的点击来判断需要选中的是哪一块,于此同时动态地控制DOM或选中区域来保持仅一边的代码选中。
我们首先想到的是自然是Selection API,如果根据鼠标的按下、移动、放开的轨迹来判断包含的代码区块,再使用Selection来控制选中多个<td>元素,自然可以实现这一功能。
但不幸地是,在查看了相关的API文档后,我们得到这样的描述:
As the Selection API specification notes, the Selection API was initially created by Netscape and used multiple ranges, for instance, to allow the user to select a column from a ``. However browsers other than Gecko did not implement multiple ranges, and the specification also requires the selection to always have a single range.
只有Firefox具备同时设置多个选中区域的能力,而其它浏览器仅能提供一个Range对象。在DOM结构上,一个选中区域包含右侧的2个单元格时,一定会带入一个左侧的单元格,因此使用Selection的方式并不可行。
于是我们寻找不同的方案,将目标瞄准到了user-select这个CSS样式上。从所周知,这个样式用于控制元素的内容是否可被选中,那么我们就可以在鼠标按下时将另一侧的单元格置为user-select: none;即可,这相对来说比较容易,大致的代码如下:
$('table').on(
'mousedown',
({target}) => {
// 仅按在代码区域才起效果
if (target.nodeName !== 'TD') {
return;
}
const table = $(target).closest('table');
// 能选的2个列的序号分别是1和3
const currentSide = $(target).index();
const otherSide = currentSide === 3 ? 1 : 3;
table.find(`td:nth-child(${currentSide + 1})`).css('user-select', 'auto');
table.find(`td:nth-child(${otherSide + 1})`).css('user-select', 'none');
}
);这样就能简单实现单边可选的效果,但是在一边选完,回到另一边时,会有一个跳动:

这是由于在切换user-select样式的时候,左侧的选中区域还没有消除,右侧变为user-select: auto的一瞬间会造成选中区域的错乱。最简单地解决方式是在修改选择状态前先消除掉选中区域:
$('table').on(
'mousedown',
({target}) => {
// 仅按在代码区域才起效果
if (target.nodeName !== 'TD') {
return;
}
window.getSelection().removeAllRanges();
// ...
}
);这样做也有一些缺点,最明显的是会让“键盘按住SHIFT后2次点击选中区块”的功能失效。我们可以对这一行代码做进一步的细化,例如点击在上一次同侧时,不做任何的额外逻辑。
可以在此处看到相关的示例:http://jsfiddle.net/56Ljp3ot/4/。这个方式对性能是有较大的考验的,因此不建议在很大的表格上使用。
我们在自研的react-diff-view代码diff展示组件上实现了这一功能,使其的交互体验比Github的更为优秀:
