前端冷知识 Selection 和 Range

介绍开发歌词标注编辑器过程中用到的一些知识

Hex 为您分享 / Github: @hex-ci

目录


  • 预热
  • 学习 Range
  • 学习 Selection
  • 总结

预热

Selection 和 Range 可以做什么?


可以获取现有选中内容,选择/取消全部或部分选中内容,
从文档中删除所选部分,或将其包装到一个标签(如 span)中等。


  • 歌词标注编辑器(划掉)
  • 划词翻译
  • 网页批注

学习 Range

Range 是什么?

本质上是一对“边界点”:范围起点和范围终点

考虑以下 HTML 片段:


              <p id="p">Example: <i>italic</i> and <b>bold</b>

让我们来选择 Example: <i>italic</i>,它是 <p> 的前两个子节点

学习 Range

我们先来创建一个 Range


            let range = new Range();
          

range.setStart(node, offset)range.setEnd(node, offset) 来设置边界


              range.setStart(p, 0); // 将起点设置为 <p> 的第 0 个子节点(即文本节点 Example:)
              range.setEnd(p, 2); // 覆盖范围至(但不包括)<p> 的第 2 个子节点(即文本节点 and,但由于不包括末节点,所以最后选择的节点是 <i>)
            

选择文本节点的一部分

让我们选择部分文本,像这样:

  • <p> 的第一个子节点的位置 2 开始(选择 Example: 中除前两个字母外的所有字母)
  • <b> 的第一个子节点的位置 3 结束(选择 bold 的前三个字母)

              

Example: italic and bold

<script> let range = new Range(); range.setStart(p.firstChild, 2); range.setEnd(p.querySelector('b').firstChild, 3); alert(range); // ample: italic and bol // 使用此范围进行选择 window.getSelection().addRange(range); </script>

运行代码3

选择文本节点的一部分

range 对象具有以下属性:

  • startContainerstartOffset:起始节点和偏移量
  • endContainerendOffset:结束节点和偏移量
  • collapsed:布尔值,如果范围在同一点上开始和结束则为 true
  • commonAncestorContainer:在范围内的所有节点中最近的共同祖先节点

Range 的方法

设置范围的起点:

  • setStart(node, offset):将起点设置在 node 中的位置 offset
  • setStartBefore(node):将起点设置在 node 前面
  • setStartAfter(node):将起点设置在 node 后面


设置范围的终点:

  • setEnd(node, offset):将终点设置为 node 中的位置 offset
  • setEndBefore(node):将终点设置为 node 前面
  • setEndAfter(node):将终点设置为 node 后面

node 既可以是文本节点,也可以是元素节点
对于文本节点,offset 偏移的是字符数
对于元素节点则是子节点数

Range 的方法


其他:

  • selectNode(node):设置范围以选择整个 node
  • selectNodeContents(node):设置范围以选择整个 node 的内容
  • collapse(toStart):如果 toStart=true 则设置 end=start
    否则设置 start=end,从而折叠范围
  • cloneRange():创建一个具有相同起点/终点的新范围

Range 的方法

操纵范围内的内容:

  • deleteContents():从文档中删除范围内容
  • extractContents():从文档中删除范围内容,并将删除的内容作为 DocumentFragment 返回
  • cloneContents():复制范围内容,并将复制的内容作为 DocumentFragment 返回
  • insertNode(node):在范围的起始处将 node 插入文档
  • surroundContents(node):使用 node 将所选范围内容包裹起来
    要使此操作有效则该范围必须包含其中所有元素的开始和结束标签
    不能像 <i>abc 这样的部分范围

例子4

学习 Selection

Range 是用于管理选择范围的对象
他们在视觉上不会选择任何内容

在视觉上选择文档需要用到 Selection 对象
通过 window.getSelection() 来获取

理论上,一个选择可以包括零个或多个范围

实际上,只有 Firefox 允许在文档中选择多个范围

学习 Selection

Selection 的起点称为 anchor,终点称为 focus

在文档中,Selection 的终点可能在起点之前

如果用户使用鼠标从 “Example” 开始选择到 “italic”
称此选择具有 “forward” 方向:

如果是从 “italic” 的末尾开始选择到 “Example”
则所选内容将被定向为 “backward”

学习 Selection


Selection 对象主要属性:

  • anchorNode:选择的起始节点
  • anchorOffset:选择开始的 anchorNode 中的偏移量
  • focusNode:选择的结束节点
  • focusOffset:选择开始处 focusNode 的偏移量
  • isCollapsed:如果未选择任何内容(空范围)或不存在,则为 true
  • rangeCount:选择中的范围数,除 Firefox 外,其他浏览器最多为 1

Selection 事件


有一些事件可以跟踪选择:

  • elem.onselectstart:当选择从 elem 上开始时
    例如,用户按下鼠标键并开始移动鼠标
    阻止默认行为会使选择无法开始
  • document.onselectionchange:当选择变动时
    注意:此事件只能在 document 上监听


例子5

获取选中的内容


  • 作为文本:只需调用 document.getSelection().toString()
  • 作为 DOM 节点:获取所有 Range 实例
    并调用它们的 cloneContents() 方法


例子6

Selection 方法


添加/移除范围的方法:

  • getRangeAt(i):获取从 0 开始的第 i 个范围
    在除 Firefox 之外的所有浏览器中,仅使用 0
  • addRange(range):将 range 添加到选择中
    如果选择已有关联的范围
    则除 Firefox 外的所有浏览器都将忽略该调用
  • removeRange(range):从选择中删除 range
  • removeAllRanges():删除所有范围
  • empty()removeAllRanges 的别名

Selection 方法

还有一些方法可以直接操作选择范围,而无需使用 Range

  • collapse(node, offset):用一个新的范围替换选定的范围
    该新范围从给定的 node 处开始,到偏移 offset 处结束
  • setPosition(node, offset)collapse 的别名
  • collapseToStart():折叠(替换为空范围)到选择起点
  • collapseToEnd():折叠到选择终点
  • extend(node, offset):将选择的 focus 移到给定的 node,位置偏移 offset
  • setBaseAndExtent(anchorNode, anchorOffset, focusNode, focusOffset)
    用给定的起点和终点来替换选择范围,选中它们之间的所有内容
  • selectAllChildren(node):选择 node 的所有子节点
  • deleteFromDocument():从文档中删除所选择的内容
  • containsNode(node, allowPartialContainment = false)
    检查选择中是否包含 node(特别是如果第二个参数是 true 的话)

例子7例子8

如要选择,请先移除现有的选择


如果选择已存在,则首先使用 removeAllRanges() 将其清空,
然后添加范围,否则除 Firefox 外的所有浏览器都将忽略新范围

某些选择方法例外,它们会替换现有的选择,例如 setBaseAndExtent

总结

今天主要介绍了 Selection 和 Range 对象,常用的方案是:

  1. 获取选择:
    
                    let selection = document.getSelection();
    
                    let cloned = /* 要将所选的节点克隆到的元素 */;
    
                    // 然后将 Range 方法应用于 selection.getRangeAt(0)
                    // 或者,像这样,用于所有范围,以支持多选
                    for (let i = 0; i < selection.rangeCount; i++) {
                      cloned.append(selection.getRangeAt(i).cloneContents());
                    }
                  
  2. 设置选择:
    
                    let selection = document.getSelection();
    
                    // 直接:
                    selection.setBaseAndExtent(...from...to...);
    
                    // 或者我们可以创建一个范围并:
                    selection.removeAllRanges();
                    selection.addRange(range);
                  

THE END

谢谢!







参考资料:

  1. MDN Range 文档
  2. MDN Selection 文档
  3. Can I use Range
  4. Can I use Selection