Javascript 选中的文字高亮概率

Javascript selected text highlighting prob

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

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

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

Failed to execute 'surroundContents' on 'Range': The Range has partially selected a non-Text node.

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

试试这个:

newNode.appendChild(range.extractContents())

根据MDN

Partially selected nodes are cloned to include the parent tags necessary to make the document fragment valid.

Range.surroundContents:

An exception will be thrown, however, if the Range splits a non-Text node with only one of its boundary points. That is, unlike the alternative above, if there are partially selected nodes, they will not be cloned and instead the operation will fail.

没有测试,但是...

参见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>

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

我已经分叉了 fiddle 和 and created a 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 class 设置在完全位于选择范围内的元素上,并使用 highlight class 创建跨度根据跨越选择的元素的需要。单击 ID 为 execute 的按钮时,旧的突出显示将被删除并应用新的突出显示。

荧光笔功能是 Rangy 版本的一部分,被认为是 "alpha"。 但是,几年来我一直在使用 Rangy 的 alpha 版本,但是极其我很少发现我的问题我可以追溯到 Rangy 的应用程序。有几次我发现 Rangy 有问题,Tim Down(它的作者)反应非常迅速。

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]);
                }
           }

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

当你仔细观察我们通过调用

获得的选择对象时
window.getSelection().getRangeAt(0)

您会看到有 4 个属性:startContainerstartOffsetendContainerendOffset

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

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

的节点

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

解决方法真的很棘手。 我以某种方式找到了解决方法。看我的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]);
    }
}

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

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