奇妙的选区绘制
曾经解决的问题,遗忘在了角落
缘起
在做文本时候有这么一个需求。 选中一段文本,可以创建超链接。 具体形式是选中文本,弹出 linkToast(URL 输入弹窗),在 linkToast 里面输入 URL。 当焦点换到切换到 linkToast 时候文本编辑器内的选中状态就会消失,预期结果是那个选中状态还在。 那么问题就冒了出来
Q:
- 为什么切换到另一个输入框的时候,为什么那个选中状态会消失?
- 既然会消失其他产品是怎么实现的?
A:
- 浏览器的焦点只能有一个
- 两个产品流派: 1. 主流:文本用 dom,输入基于 contenteditable 的一众编辑器(主流):飞书,framer,它们会自己去绘制一段区域去占位显示 2. 非主流: 文本用 canvas 画,输入需要自己控制: figma,选区的绘制由自己控制,不受浏览器影响
过程
在原型里面我们要如何绘制这段区域呢? 思路:获取我们选中的内容的 xywh,构造新的 dom 节点,将它们挂载到 body 上,通过 dom 模拟显示选区
- 首先要获取我们选中的内容到底有哪些。浏览器提供了强大的接口去控制。通过拿到 window.getSelection()拿到 range https://zh.javascript.info/selection-range
- 通过 chatXXX 搜索
如何获得选区的所有节点,得到了一堆可以生效的代码。
const getSelectedNodes = () => {
const selection = window.getSelection()
const range = selection.getRangeAt(0)
const commonAncestor = range.commonAncestorContainer
const walker = document.createTreeWalker(commonAncestor, NodeFilter.SHOW_ALL, null)
const selectedNodes = []
while (walker.nextNode()) {
const node = walker.currentNode
if (isNodeInSelection(node, range)) {
selectedNodes.push(node)
}
}
return selectedNodes
}
const isNodeInSelection = (node:Node, range:Range) => {
const nodeRange = document.createRange()
nodeRange.selectNodeContents(node)
return range.intersectsNode(node)
}
- 观察 selectedNodes 的特征,找规律,拿到想要的 rect https://phuoc.ng/collection/html-dom/get-the-bounding-rectangle-of-a-text-node/
- blueArea = document.createElement('div'); blueArea.style = {...xxx}; document.body.append(blueArea); 区域就显示上去了;
观察规律后得到的具体策略:
- 选区 blue area 绘制思路
- 获取选中区域的节点 rect 通过 text 和 div with className 计算 要点:start 和 end 的 rect 需要通过 offset 和 textContent.length 进行二次计算; const percent = (textContent.length - offset)/textContent.length
- 单行:节点里面不会包含 div,有用的是 text 节点,第一个 text 是 start,最后一个 text 是 end,
- 跨两行(有两个 div & className): 过滤掉 div,还是用单行的策略
- 跨多(三行以上)行:掐头去尾使用先获得中间行 div,它们的 rect 可以直接使用,头行的 text 在第二个 div & className 索引前面,尾行的 text 在最后一个 div & className 后面
- 需要考虑从前往后选和从后往前选两种不一样的情况
最终实践下来发现跨多行的方法都能用跨单行去的策略去处理,div 其实是不需要的。但也因为只是针对了选中的元素获取对应 rect ,最终显示效果还是不够好,所以还需要用所属 div 去修改 top,height 来最大限度还原选区。目前的还原情况比 framer 要好
功能思路:脑内模拟 -> 实践发现问题 -> 看看别人怎么做 -> 改 -> 出效果
后续
这段代码实际并没有上线,因为测试之后发现需要管理选中和清空效果的边缘情况有些复杂(时间太过久远,我忘了是什么情况了),离上线节点不远了,和设计确认后,放弃了。
抓大放小