【问题标题】:Javascript selected text highlighting probJavascript选择的文本突出显示问题
【发布时间】:2015-04-27 11:44:16
【问题描述】:

我有一个带有文本内容的 html 页面。在选择任何文本并按下突出显示按钮时,我可以更改所选文本的样式以突出显示相同的内容。为了实现这个功能,我写了下面的方法。

sel = window.getSelection();
var range = sel.getRangeAt(0);
var span = document.createElement('span');
span.className = "highlight" + color;
range.surroundContents(span);

如果您选择没有 html 标记的文本,这可以正常工作,但是当文本之间有任何 html 标记时,它会报错

无法在“Range”上执行“surroundContents”:Range 部分选择了非文本节点。

如何解决这个问题。是否可以为每个部分分别突出显示相同的部分(以html标签划分)?

【问题讨论】:

  • 这会很困难。如果你仔细想想,你最终不会得到像<div>unselected here<span>selected</div><div>selected here</span> more unselected at the end</div> 这样的东西,因为那会破坏 DOM 树。您需要在基节点处的文本中放置一个跨度,然后在目标节点处。如果您也想要它们之间的任何节点,但我不确定它们是否可以通过选择 API 访问

标签: javascript getselection


【解决方案1】:

Range.extractContents:

document.getElementById('execute').addEventListener('click', function() {
    var range = window.getSelection().getRangeAt(0),
        span = document.createElement('span');

    span.className = 'highlight';
    span.appendChild(range.extractContents());
    range.insertNode(span);
});
.highlight { background-color: yellow; }
<div id="test">
    Select any part of <b>this text and</b> then click 'Run'.
</div>

<button id="execute">Run</button>

【讨论】:

  • 放一个兄弟姐妹而不是一个孩子,它会中断:jsfiddle.net/sL7joxhs - 突出显示来自test1test2 的一些文本,它将两个 div 包装在一个跨度中并破坏结构跨度>
  • 是的,span.appendChild(range.extractContents()) 如果所选文本在多个段落中,则会中断段落。如何解决这个问题?
  • @RGraham @dev_android 它只是从视觉上“损坏”了——检查标记并自己验证 DOM 结构是否有效。它看起来是错误的,因为混合了块元素和内联元素(即divspan)——Range API 不应该归咎于此。 Replace the divs with spans and you'll see that nothing looks broken.
  • @AndréDion 看到这里不仅仅是 HTML 有效性问题,它实际上改变了树的结构。曾经你有&lt;span&gt;asd&lt;/span&gt;&lt;span&gt;asd&lt;/span&gt;,你希望变成:&lt;span&gt;as&lt;span class="highlight"&gt;d&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="highlight"&gt;a&lt;/span&gt;sd&lt;/span&gt;来保持结构,你现在有&lt;span&gt;as&lt;/span&gt;&lt;span class="highlight"&gt;&lt;span&gt;d&lt;/span&gt;&lt;span&gt;....等等,这会破坏你基于该结构运行的任何查询。希望这是有道理的。很难在评论中解释!
  • 不可能在所有情况下都用 span 替换 DIV。而且它不仅可以是DIV,还可以是

    等等。这个问题的一般出路是什么?

【解决方案2】:

我不会重新发明*,而是使用Rangy 的突出显示功能。

我已经分叉了 RGraham created 的小提琴并创建了一个 new fiddle 来展示它是如何工作的。它是这样完成的:

var applier = rangy.createClassApplier("highlight");
var highlighter = rangy.createHighlighter();
highlighter.addClassApplier(applier);

document.getElementById('execute').addEventListener('click', function() {
    highlighter.removeAllHighlights();
    highlighter.highlightSelection("highlight");
});

这样做是创建一个荧光笔,它将highlight 类设置在完全位于选择范围内的元素上,并根据需要为跨越选择的元素创建具有highlight 类的跨度。当点击 ID 为 execute 的按钮时,旧的高亮将被移除并应用新的高亮。

荧光笔功能是 Rangy 版本的一部分,被认为是“alpha”版本。 然而,几年来我一直在使用 Rangy 的 alpha 版本,但我发现我的应用程序存在可以追溯的问题非常非常给兰吉。几次我发现 Rangy 的问题时,Tim Down(它的作者)反应非常迅速。

【讨论】:

  • 1.可以使用 rangy 突出显示/取消突出显示现有范围(不是选择)吗? 2. 如果是,它会以幂等的方式执行此操作(没有副作用。IOW,它是否会像在该对操作之前找到它一样离开 dom)?
【解决方案3】:

我的解决方案突出显示所有选定的节点。

function highlight() {
  const sel = window.getSelection();
  const range = sel.getRangeAt(0);
  const {
    commonAncestorContainer,
    startContainer,
    endContainer,
    startOffset,
    endOffset,
  } = range;
  const nodes = [];

  console.group("range");

  console.log("range", range);
  console.log("commonAncestorContainer", commonAncestorContainer);
  console.log("startContainer", startContainer);
  console.log("endContainer", endContainer);
  console.log("startOffset", startOffset);
  console.log("endOffset", endOffset);
  console.log("startContainer.parentNode", startContainer.parentNode);
  console.groupEnd();

  if (startContainer === endContainer) {
    const span = document.createElement("span");
    span.className = "highlight";
    range.surroundContents(span);
    return;
  }

  // get all posibles selected nodes
  function getNodes(childList) {
    console.group("***** getNode: ", childList);
    childList.forEach((node) => {
      console.log("node:", node, "nodoType", node.nodeType);

      const nodeSel = sel.containsNode(node, true);
      console.log("nodeSel", nodeSel);

      // if is not selected
      if (!nodeSel) return;

      const tempStr = node.nodeValue;
      console.log("nodeValue:", tempStr);

      if (node.nodeType === 3 && tempStr.replace(/^\s+|\s+$/gm, "") !== "") {
        console.log("nodo agregado");
        nodes.push(node);
      }

      if (node.nodeType === 1) {
        if (node.childNodes) getNodes(node.childNodes);
      }
    });
    console.groupEnd();
  }

  getNodes(commonAncestorContainer.childNodes);

  console.log(nodes);

  nodes.forEach((node, index, listObj) => {
    const { nodeValue } = node;
    let text, prevText, nextText;

    if (index === 0) {
      prevText = nodeValue.substring(0, startOffset);
      text = nodeValue.substring(startOffset);
    } else if (index === listObj.length - 1) {
      text = nodeValue.substring(0, endOffset);
      nextText = nodeValue.substring(endOffset);
    } else {
      text = nodeValue;
    }

    const span = document.createElement("span");
    span.className = "highlight";
    span.append(document.createTextNode(text));
    const { parentNode } = node;

    parentNode.replaceChild(span, node);

    if (prevText) {
      const prevDOM = document.createTextNode(prevText);
      parentNode.insertBefore(prevDOM, span);
    }

    if (nextText) {
      const nextDOM = document.createTextNode(nextText);
      parentNode.insertBefore(nextDOM, span.nextSibling);
    }
  });

  sel.removeRange(range);
}

例如https://codesandbox.io/s/api-selection-multiple-with-nodes-gx2is?file=/index.html

【讨论】:

  • 这应该是选择的答案。
  • 有没有办法取消高亮文本?
  • @ryan yes 类似,不用插入 你可以先获取然后删除。
  • 谢谢,这正是我想要的
【解决方案4】:

试试这个:

newNode.appendChild(range.extractContents())

根据MDN

部分选择的节点被克隆以包含父标签 使文档片段有效所必需的。

Range.surroundContents:

但是,如果 Range 拆分非文本,则会引发异常 只有一个边界点的节点。也就是说,不同于 上面的替代方案,如果有部分选择的节点,它们将 不会被克隆,而是操作会失败。

没有测试,但是...

【讨论】:

    【解决方案5】:

    这个解决方案有点棘手,但我觉得足够了

    当你仔细看到我们通过调用得到的选择对象时

    window.getSelection().getRangeAt(0)
    

    您会发现有 4 个属性:startContainerstartOffsetendContainerendOffset

    所以现在您需要从startContainerstartOffset 开始,然后从那里开始放置必要的跨度节点。

    如果现在endContainer 是不同的节点,那么您需要开始遍历从startContainerendContainer 的节点

    对于遍历,您需要检查可以从 DOM 对象中获取的子节点和兄弟节点。所以首先遍历startContainer,遍历它的所有子节点,检查子节点是否是内联元素,然后在它周围应用span标签,然后你需要为各种极端情况编写一些代码。

    【讨论】:

      【解决方案6】:

      解决方案真的很棘手。 我以某种方式找到了解决方法。见我的fiddle

      function highlight() {
          var range = window.getSelection().getRangeAt(0),
              parent = range.commonAncestorContainer,
              start = range.startContainer,
              end = range.endContainer;
          var startDOM = (start.parentElement == parent) ? start.nextSibling : start.parentElement;
          var currentDOM = startDOM.nextElementSibling;
          var endDOM = (end.parentElement == parent) ? end : end.parentElement;
          //Process Start Element
          highlightText(startDOM, 'START', range.startOffset);
          while (currentDOM != endDOM && currentDOM != null) {
              highlightText(currentDOM);
              currentDOM = currentDOM.nextElementSibling;
          }
          //Process End Element
          highlightText(endDOM, 'END', range.endOffset);
      }
      
      function highlightText(elem, offsetType, idx) {
          if (elem.nodeType == 3) {
              var span = document.createElement('span');
              span.setAttribute('class', 'highlight');
              var origText = elem.textContent, text, prevText, nextText;
              if (offsetType == 'START') {
                  text = origText.substring(idx);
                  prevText = origText.substring(0, idx);
              } else if (offsetType == 'END') {
                  text = origText.substring(0, idx);
                  nextText = origText.substring(idx);
              } else {
                  text = origText;
              }
              span.textContent = text;
      
              var parent = elem.parentElement;
              parent.replaceChild(span, elem);
              if (prevText) { 
                  var prevDOM = document.createTextNode(prevText);
                  parent.insertBefore(prevDOM, span);
              }
              if (nextText) {
                  var nextDOM = document.createTextNode(nextText);
                  parent.appendChild(nextDOM);
              }
              return;
          }
          var childCount = elem.childNodes.length;
          for (var i = 0; i < childCount; i++) {
              if (offsetType == 'START' && i == 0) 
                  highlightText(elem.childNodes[i], 'START', idx);
              else if (offsetType == 'END' && i == childCount - 1)
                  highlightText(elem.childNodes[i], 'END', idx);
              else
                  highlightText(elem.childNodes[i]);
          }
      }
      

      【讨论】:

      【解决方案7】:
      if (window.getSelection) {
                      var sel = window.getSelection();
                      if (!sel) {
                          return;
                      }
                      var range = sel.getRangeAt(0);
                      var start = range.startContainer;
                      var end = range.endContainer;
                      var commonAncestor = range.commonAncestorContainer;
                      var nodes = [];
                      var node;
      
                      for (node = start.parentNode; node; node = node.parentNode){
                         var tempStr=node.nodeValue;
                         if(node.nodeValue!=null &&    tempStr.replace(/^\s+|\s+$/gm,'')!='')
                           nodes.push(node);
                         if (node == commonAncestor)
                           break;
                      }
                      nodes.reverse();
      
                      for (node = start; node; node = getNextNode(node)){
                         var tempStr=node.nodeValue;
                         if(node.nodeValue!=null &&  tempStr.replace(/^\s+|\s+$/gm,'')!='')
                           nodes.push(node);
                         if (node == end)
                          break;
                      }
      
                      for(var i=0 ; i<nodes.length ; i++){
      
                         var sp1 = document.createElement("span");
                         sp1.setAttribute("class", "highlight"+color );
                         var sp1_content = document.createTextNode(nodes[i].nodeValue);
                         sp1.appendChild(sp1_content);
                         var parentNode = nodes[i].parentNode;
                         parentNode.replaceChild(sp1, nodes[i]);
                      }
                 }
      

      【讨论】: