盈凸月

奇妙的选区绘制

曾经解决的问题,遗忘在了角落

缘起

在做文本时候有这么一个需求。 选中一段文本,可以创建超链接。 具体形式是选中文本,弹出 linkToast(URL 输入弹窗),在 linkToast 里面输入 URL。 当焦点换到切换到 linkToast 时候文本编辑器内的选中状态就会消失,预期结果是那个选中状态还在。 那么问题就冒了出来

Q:

  1. 为什么切换到另一个输入框的时候,为什么那个选中状态会消失?
  2. 既然会消失其他产品是怎么实现的?

A:

  1. 浏览器的焦点只能有一个
  2. 两个产品流派: 1. 主流:文本用 dom,输入基于 contenteditable 的一众编辑器(主流):飞书,framer,它们会自己去绘制一段区域去占位显示 2. 非主流: 文本用 canvas 画,输入需要自己控制: figma,选区的绘制由自己控制,不受浏览器影响

过程

在原型里面我们要如何绘制这段区域呢? 思路:获取我们选中的内容的 xywh,构造新的 dom 节点,将它们挂载到 body 上,通过 dom 模拟显示选区

  1. 首先要获取我们选中的内容到底有哪些。浏览器提供了强大的接口去控制。通过拿到 window.getSelection()拿到 range https://zh.javascript.info/selection-range
  2. 通过 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)
}
  1. 观察 selectedNodes 的特征,找规律,拿到想要的 rect https://phuoc.ng/collection/html-dom/get-the-bounding-rectangle-of-a-text-node/
  2. 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 要好

功能思路:脑内模拟 -> 实践发现问题 -> 看看别人怎么做 -> 改 -> 出效果

后续

这段代码实际并没有上线,因为测试之后发现需要管理选中和清空效果的边缘情况有些复杂(时间太过久远,我忘了是什么情况了),离上线节点不远了,和设计确认后,放弃了。

抓大放小

← 返回文章列表