范围、选择

范围Range

一、选择的基本概念是Range:本质上是一对“边界点”:范围起点和范围终点。
二、每个点都被表示为一个带有相对于起点的相对偏移(offset)的父DOM节点。如果父节点是元素节点,则偏移量是子节点的编号,对于文本节点,则是文本中的位置。

选择的范围示例

选择一些东西

一、创建一个范围(构造器没有参数)

  1. let range = new Range()

二、可以使用range.setStart(node, offset)和range.setEnd(node, offset)来设置选择边界。
1、代码片段

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

2、DOM结构
image.png
三、选择 “Example: italic“。它是

的前两个子节点(文本节点也算在内)
(1)图例
image.png
(2)代码

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

<script>
  let range = new Range();

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

  // 范围的 toString 以文本形式返回其内容(不带标签)
  alert(range); // Example: italic

  // 将此范围应用于文档选择(后文有解释)
  document.getSelection().addRange(range);
</script>

选择文本节点的一部分

一、这需要将起点和终点设置为文本节点的相对偏移量即可。
二、1、图例
image.png
2、代码

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

<script>
  let range = new Range();

  range.setStart(p.firstChild, 2); // 从<p>的第一个子节点的位置2开始(选择“Eample”:中除前两个字母外的所有字母)
  range.setEnd(p.querySelector('b').firstChild, 3); // 到<b>的第一个子节点的位置3结束(选择“bold”的前三个字母,就这些)

  alert(range); // ample: italic and bol

  // 使用此范围进行选择(后文有解释)
  window.getSelection().addRange(range);
</script>

Range

一、Range 对象:范围的起点不能在终点之后

Range对象属性

一、
image.png
二、range对象的属性:
1、startContainer, startOffset:起始节点和偏移量。
(1)上例中,

中的第一个文本节点, 2
2、endContainer, endOffset:结束节点和偏移量。
(1)上例中,中的第一个文本节点,3
3、collapsed:布尔值,如果范围在同一点上开始和结束(所以范围内没有内容)则为true
(1)上例中,false
4、commonAncestorContainer:在范围内的所有节点中最近的共同祖先节点。
(1)上例中,

Range方法

一、见:https://www.yuque.com/tqpuuk/yrrefz/ixdth3
二、

点击按钮运行所选内容上的方法,点击 "resetExample" 进行重置。

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

<p id="result"></p>
<script>
  let range = new Range();

  // 下面演示了上述的每个方法:
  let methods = {
    deleteContents() {
      range.deleteContents()
    },
    extractContents() {
      let content = range.extractContents();
      result.innerHTML = "";
      result.append("extracted: ", content);
    },
    cloneContents() {
      let content = range.cloneContents();
      result.innerHTML = "";
      result.append("cloned: ", content);
    },
    insertNode() {
      let newNode = document.createElement('u');
      newNode.innerHTML = "NEW NODE";
      range.insertNode(newNode);
    },
    surroundContents() {
      let newNode = document.createElement('u');
      try {
        range.surroundContents(newNode);
      } catch(e) { alert(e) }
    },
    resetExample() {
      p.innerHTML = `Example: <i>italic</i> and <b>bold</b>`;
      result.innerHTML = "";

      range.setStart(p.firstChild, 2);
      range.setEnd(p.querySelector('b').firstChild, 3);

      window.getSelection().removeAllRanges();
      window.getSelection().addRange(range);
    }
  };

  for(let method in methods) {
    document.write(`<div><button onclick="methods.${method}()">${method}</button></div>`);
  }

  methods.resetExample();
</script>

选择Selection

一、Range是用于管理选择范围的通用对象。我们可能会创建此类对象,并传递它们:它们在视觉上不会自动选择任何内容。
二、文档选择是由Selection对象表示的,可通过window.getSelection()或document.getSelection()来获取。
三、一个选择可以包括零个或多个范围。不过实际上,只有Firefox允许使用Ctrol + click在文档中选择多个范围。其他浏览器最多支持1个范围。

选择属性

一、选择的起点称为“锚点(anchor)”,终点称为“焦点(focus)”
二、见:https://www.yuque.com/tqpuuk/yrrefz/ixdth3
三、在文档中,选择的终点可能在起点之前
1、如果在文档中,选择的起点(anchor)在终点(focus)之前,则称此选择具有 “forward” 方向
【示例1】如果用户使用鼠标从 “Example” 开始选择到 “italic”
image.png
【示例2】如果是从 “italic” 的末尾开始选择到 “Example”,则所选内容将被定向为 “backward”,其焦点(focus)将在锚点(anchor)之前
image.png

选择事件

一、跟踪选择的事件:
1、elem.onselectstart—— 当选择从elem上开始时,例如,用户按下鼠标键并开始移动鼠标。
(1)阻止默认行为会使选择无法开始。
2、document.onselectionchange—— 当选择变动时。
(2)请注意:此处理程序只能在 document 上设置。
【示例1】随更改动态显示选择边界

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

From <input id="from" disabled> – To <input id="to" disabled>
<script>
  document.onselectionchange = function() {
    let {anchorNode, anchorOffset, focusNode, focusOffset} = document.getSelection();

    from.value = `${anchorNode && anchorNode.data}:${anchorOffset}`;
    to.value = `${focusNode && focusNode.data}:${focusOffset}`;
  };
</script>

二、要获取整个选择
1、作为文本:只需调用document.getSelection().toString()
2、作为DOM节点:获取底层的(underlying)范围,并调用它们的 cloneContents() 方法(如果我们不支持 Firefox 多选的话,则仅取第一个范围
【示例1】选择内容获取为文本、DOM节点

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

Cloned: <span id="cloned"></span>
<br>
As text: <span id="astext"></span>

<script>
  document.onselectionchange = function() {
    let selection = document.getSelection();

    cloned.innerHTML = astext.innerHTML = "";

    // 从范围复制 DOM 节点(这里我们支持多选)
    for (let i = 0; i < selection.rangeCount; i++) {
      cloned.append(selection.getRangeAt(i).cloneContents());
    }

    // 获取为文本形式
    astext.innerHTML += selection;
  };
</script>

选择方法

一、见:https://www.yuque.com/tqpuuk/yrrefz/ixdth3
二、对于许多任务,我们可以调用Selection方法,而无需访问底层的(underlying)Range对象。
三、设置选择的方式
【示例1】选择段落

的全部内容

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

<script>
  // 从 <p> 的第 0 个子节点选择到最后一个子节点
  document.getSelection().setBaseAndExtent(p, 0, p, p.childNodes.length);
</script>

【示例2】使用范围来完成同一个操作

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

<script>
  let range = new Range();
  range.selectNodeContents(p); // 或者也可以使用 selectNode(p) 来选择 <p> 标签

  document.getSelection().removeAllRanges(); // 清除现有选择(如果有的话)
  document.getSelection().addRange(range);
</script>

四、如果选择已存在,则首先使用removeAllRange()来将其清空。然后添加范围。否则,除Firefox外的浏览器都将忽略新范围。
1、某些选择方法例外,它们会替换现有的选择,如setBaseAndExtent

表单控件中的选择

一、诸如input, textarea等表单元素提供了专用的选择API,没有Selection或Range对象。由于输入值是纯文本而不是HTML,因此不需要此类对象。
二、属性、事件、方法。见:https://www.yuque.com/tqpuuk/yrrefz/ixdth3

跟踪选择

一、【示例1】使用onselect事件来跟踪选择

<textarea id="area" style="width:80%;height:60px">
Selecting in this text updates values below.
</textarea>
<br>
From <input id="from" disabled> – To <input id="to" disabled>

<script>
  area.onselect = function() {
    from.value = area.selectionStart;
    to.value = area.selectionEnd;
  };
</script>

1、onselect 是在某项被选择时触发,而在选择被删除时不触发。
2、根据 规范,发表单控件内的选择不应该触发 document.onselectionchange 事件,因为它与 document 选择和范围不相关。一些浏览器会生成它,但我们不应该依赖它。

移动光标

一、我们可以更改 selectionStart 和 selectionEnd,二者设定了选择。
1、一个重要的边界情况是 selectionStart 和 selectionEnd 彼此相等。那正是光标位置。或者,换句话说,当未选择任何内容时,选择会折叠在光标位置。
二、通过将 selectionStart 和 selectionEnd 设置为相同的值,我们可以移动光标。
【示例1】

<textarea id="area" style="width:80%;height:60px">
Focus on me, the cursor will be at position 10.
</textarea>

<script>
  area.onfocus = () => {
    // 设置零延迟 setTimeout 以在浏览器 "focus" 行为完成后运行
    setTimeout(() => {
      // 我们可以设置任何选择
      // 如果 start=end,则光标就会在该位置
      area.selectionStart = area.selectionEnd = 10;
    });
  };
</script>

修改选择

一、如要修改选择的内容,有如下两种方案
1、方案1: input.setRangeText() 方法。
(1)优点:setRangeText 功能更强大,通常更方便。
2、方案2:读取 selectionStart/End,并在了解选择的情况下更改 value 的相应子字符串
(1)缺点:这是一个有点复杂的方法。
【示例1】使用其最简单的单参数形式,它可以替换用户选择的范围并删除该选择。这里的用户的选择将被包装在 中:

<input id="input" style="width:200px" value="Select here and click the button">
<button id="button">Wrap selection in stars *...*</button>

<script>
button.onclick = () => {
  if (input.selectionStart == input.selectionEnd) {
    return; // 什么都没选
  }

  let selected = input.value.slice(input.selectionStart, input.selectionEnd);
  input.setRangeText(`*${selected}*`);
};
</script>

【示例2】使用更多参数,我们可以设置范围 start 和 end。
在下面这个示例中,我们在输入文本中找到 “THIS”,将其替换,并保持替换文本的选中状态:

<input id="input" style="width:200px" value="Replace THIS in text">
<button id="button">Replace THIS</button>

<script>
button.onclick = () => {
  let pos = input.value.indexOf("THIS");
  if (pos >= 0) {
    input.setRangeText("*THIS*", pos, pos + 4, "select");
    input.focus(); // 聚焦(focus),以使选择可见
  }
};
</script>

在光标处插入

一、如果未选择任何内容,或者我们在 setRangeText 中使用了相同的 start 和 end,则仅插入新文本,不会删除任何内容。
二、也可以使用 setRangeText 在“光标处”插入一些东西。
【示例1】这是一个按钮,按下后会在光标位置插入 “HELLO”,然后光标紧随其后。如果选择不为空,则将其替换(我们可以通过比较 selectionStart!=selectionEnd 来进行检查,为空则执行其他操作):

<input id="input" style="width:200px" value="Text Text Text Text Text">
<button id="button">Insert "HELLO" at cursor</button>

<script>
  button.onclick = () => {
    input.setRangeText("HELLO", input.selectionStart, input.selectionEnd, "end");
    input.focus();
  };
</script>

使不可选

一、要使某些内容不可选,有三种方式
1、使用CSS属性user-select:none

<style>
#elem {
  user-select: none;
}
</style>
<div>Selectable <div id="elem">Unselectable</div> Selectable</div>

(1)这样不允许从elem开始。但是用户可以在其他地方开始选择,并将elem包含在内。
然后 elem 将成为 document.getSelection() 的一部分,因此选择实际发生了,但是在复制粘贴中,其内容通常会被忽略。
2、防止 onselectstart 或 mousedown 事件中的默认行为。

<div>Selectable <div id="elem">Unselectable</div> Selectable</div>

<script>
  elem.onselectstart = () => false;
</script>

(1)这样可以防止在 elem 上开始选择,但是访问者可以在另一个元素上开始选择,然后扩展到 elem。
(2)当同一行为上有另一个事件处理程序触发选择时(例如 mousedown),这会很方便。因此我们禁用选择以避免冲突,仍然允许复制 elem 内容。
3、我们还可以使用 document.getSelection().empty() 来在选择发生后清除选择。
(1)很少使用这种方法,因为这会在选择项消失时导致不必要的闪烁。