如何用元素包装/环绕突出显示的文本

How To Wrap / Surround Highlighted Text With An Element

本文关键字:显示 文本 何用 元素 包装      更新时间:2023-09-26

我想将选定的文本包装在带有span的div容器中,可以吗?

用户将选择一个文本并单击一个按钮,在按钮单击事件中,我想用 span 元素包装该所选文本。我可以使用window.getSelection()获取所选文本,但是如何知道它在DOM结构中的确切位置?

如果所选内容完全包含在单个文本节点中,则可以使用从所选内容获取的范围的surroundContents()方法执行此操作。但是,这是非常脆弱的:如果选择在逻辑上不能包含在单个元素中(通常,如果范围跨越节点边界,尽管这不是精确的定义(,则它不起作用。要在一般情况下执行此操作,您需要一种更复杂的方法。

此外,IE <9 不支持 DOM Rangewindow.getSelection()。对于这些浏览器,您将再次需要另一种方法。您可以使用像我自己的 Rangy 这样的库来规范浏览器行为,您可能会发现类应用程序模块对这个问题很有用。

简单surroundContents()示例 jsFiddle: http://jsfiddle.net/VRcvn/

法典:

function surroundSelection(element) {
    if (window.getSelection) {
        var sel = window.getSelection();
        if (sel.rangeCount) {
            var range = sel.getRangeAt(0).cloneRange();
            range.surroundContents(element);
            sel.removeAllRanges();
            sel.addRange(range);
        }
    }
}

function wrapSelectedText() {       
    var selection= window.getSelection().getRangeAt(0);
    var selectedText = selection.extractContents();
    var span= document.createElement("span");
    span.style.backgroundColor = "yellow";
    span.appendChild(selectedText);
    selection.insertNode(span);
}
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam rhoncus  gravida magna, quis interdum magna mattis quis. Fusce tempor sagittis  varius. Nunc at augue at erat suscipit bibendum id nec enim. Sed eu odio  quis turpis hendrerit sagittis id sit amet justo. Cras ac urna purus,  non rutrum nunc. Aenean nec vulputate ante. Morbi scelerisque sagittis  hendrerit. Pellentesque habitant morbi tristique senectus et netus et  malesuada fames ac turpis egestas. Nulla tristique ligula fermentum  tortor semper at consectetur erat aliquam. Sed gravida consectetur  sollicitudin. 
<input type="button" onclick="wrapSelectedText();" value="Highlight" />

JS小提琴。

下面是对允许跨元素边界的通用解决方案的尝试。可能不能很好地与图像等混合,也不能很好地与非从左到右的文本混合,但对于简单的情况应该没问题。

首先,这是一个非常通用的函数,用于在由 Range 对象定义的子树中从左到右遍历文本节点。这将为我们提供所需的所有文本:

function walkRange(range) {
    let ranges = [];
    
    let el = range.startContainer;
    let elsToVisit = true;
    while (elsToVisit) {
        let startOffset = el == range.startContainer ? range.startOffset : 0;
        let endOffset = el == range.endContainer ? range.endOffset : el.textContent.length;
        let r = document.createRange();
        r.setStart(el, startOffset);
        r.setEnd(el, endOffset);
        ranges.push(r);
        
        
        /// Move to the next text container in the tree order
        elsToVisit = false;
        while (!elsToVisit && el != range.endContainer) {
            let nextEl = getFirstTextNode(el.nextSibling);
            if (nextEl) {
                el = nextEl;
                elsToVisit = true;
            }
            else {
                if (el.nextSibling)      el = el.nextSibling;
                else if (el.parentNode)  el = el.parentNode;
                else                     break;
            }
        }
    }
    
    return ranges;
}

这利用这个实用程序函数来获取子树中的第一个(最左边的(文本节点:

function getFirstTextNode(el) {
    /// Degenerate cases: either el is null, or el is already a text node
    if (!el)               return null;
    if (el.nodeType == 3)  return el;
    
    for (let child of el.childNodes) {
        if (child.nodeType == 3) {
            return child;
        }
        else {
            let textNode = getFirstTextNode(child);
            if (textNode !== null) return textNode;
        }
    }
    
    return null;
}

调用walkRanges后,您只需在返回的内容上使用surroundContents即可实际进行突出显示/标记。下面是一个函数:

function highlight(range, className) {
    range = range.getRangeAt ? range.getRangeAt(0) : range;
    for (let r of walkRange(range)) {
        let mark = document.createElement('mark');
        mark.className = className;
        r.surroundContents(mark);
    }
}

以及取消突出显示(假设您使用了唯一的类名作为突出显示(:

function unhighlight(sel) {
    document.querySelectorAll(sel).forEach(el => el.replaceWith(...el.childNodes));
}

用法示例:

highlight(document.getSelection(), 'mySelectionClassName');
unhighlight('.mySelectionClassName')
这是

可能的。您需要使用 range API 和 Range.surroundContent(( 方法。它将内容包装的节点放在指定范围的开头。见 https://developer.mozilla.org/en/DOM/range.surroundContents

surroundContents 仅在您的选择仅包含文本而不包含 HTML 时才有效。这是一个更灵活以及跨浏览器的解决方案。这将插入如下所示的跨度:

<span id="new_selection_span"><!--MARK--></span>

范围插入到所选内容之前,在最近的开始 HTML 标记的前面。

var span = document.createElement("span");
span.id = "new_selection_span";
span.innerHTML = '<!--MARK-->';
if (window.getSelection) { //compliant browsers
    //obtain the selection
    sel = window.getSelection();
    if (sel.rangeCount) {
        //clone the Range object
        var range = sel.getRangeAt(0).cloneRange();
        //get the node at the start of the range
        var node = range.startContainer;
        //find the first parent that is a real HTML tag and not a text node
        while (node.nodeType != 1) node = node.parentNode;
        //place the marker before the node
        node.parentNode.insertBefore(span, node);
        //restore the selection
        sel.removeAllRanges();
        sel.addRange(range);
    }
} else { //IE8 and lower
    sel = document.selection.createRange();
    //place the marker before the node
    var node = sel.parentElement();
    node.parentNode.insertBefore(span, node);
    //restore the selection
    sel.select();
}

以下工作跨多个 dom 元素

function highlightSelection() {       
    let selection= window.getSelection().getRangeAt(0);
    let selectedContent = selection.extractContents();
    var span= document.createElement("span");
    span.style.backgroundColor = "lightpink";
    span.appendChild(selectedContent);
    selection.insertNode(span);
}
Make your <b>selection across multiple</b> elements <strike>and then click highlight</strike> button.
<button onclick="highlightSelection();">Highlight</button>

请找到以下代码将有助于包装所有类型标签的 span 标签。请仔细阅读代码并将逻辑用于您的实现。

getSelectedText(this);
addAnnotationElement(this, this.parent);
function getSelectedText(this) {
    this.range = window.getSelection().getRangeAt(0);
    this.parent = this.range.commonAncestorContainer;
    this.frag = this.range.cloneContents();
    this.clRange = this.range.cloneRange();
    this.start = this.range.startContainer;
    this.end = this.range.endContainer;
}

function addAnnotationElement(this, elem) {
    var text, textParent, origText, prevText, nextText, childCount,
        annotationTextRange,
        span = this.htmlDoc.createElement('span');
    if (elem.nodeType === 3) {
        span.setAttribute('class', this.annotationClass);
        span.dataset.name = this.annotationName;
        span.dataset.comment = '';
        span.dataset.page = '1';
        origText = elem.textContent;            
        annotationTextRange = validateTextRange(this, elem);
        if (annotationTextRange == 'textBeforeRangeButIntersect') {
            text = origText.substring(0, this.range.endOffset);
            nextText = origText.substring(this.range.endOffset);
        } else if (annotationTextRange == 'textAfterRangeButIntersect') {
            prevText = origText.substring(0, this.range.startOffset);
            text = origText.substring(this.range.startOffset);
        } else if (annotationTextRange == 'textExactlyInRange') {
            text = origText
        } else if (annotationTextRange == 'textWithinRange') {
            prevText = origText.substring(0, this.range.startOffset);
            text = origText.substring(this.range.startOffset,this.range.endOffset);
            nextText = origText.substring(this.range.endOffset);
        } else if (annotationTextRange == 'textNotInRange') {
            return;
        }
        span.textContent = text;
        textParent = elem.parentElement;
        textParent.replaceChild(span, elem);
        if (prevText) {
            var prevDOM = this.htmlDoc.createTextNode(prevText);
            textParent.insertBefore(prevDOM, span);
        }
        if (nextText) {
            var nextDOM = this.htmlDoc.createTextNode(nextText);
            textParent.insertBefore(nextDOM, span.nextSibling);
        }
        return;
    }
    childCount = elem.childNodes.length;
    for (var i = 0; i < childCount; i++) {
        var elemChildNode = elem.childNodes[i];
        if( Helper.isUndefined(elemChildNode.tagName) ||
            ! ( elemChildNode.tagName.toLowerCase() === 'span' &&
            elemChildNode.classList.contains(this.annotationClass) ) ) {
            addAnnotationElement(this, elem.childNodes[i]);
        }
        childCount = elem.childNodes.length;
    }
}
  function validateTextRange(this, elem) {
    var textRange = document.createRange();
    textRange.selectNodeContents (elem);
    if (this.range.compareBoundaryPoints (Range.START_TO_END, textRange) <= 0) {
        return 'textNotInRange';
    }
    else {
        if (this.range.compareBoundaryPoints (Range.END_TO_START, textRange) >= 0) {
            return 'textNotInRange';
        }
        else {
            var startPoints = this.range.compareBoundaryPoints (Range.START_TO_START, textRange),
                endPoints = this.range.compareBoundaryPoints (Range.END_TO_END, textRange);
            if (startPoints < 0) {
                if (endPoints < 0) {
                    return 'textBeforeRangeButIntersect';
                }
                else {
                    return "textExactlyInRange";
                }
            }
            else {
                if (endPoints > 0) {
                    return 'textAfterRangeButIntersect';
                }
                else {
                    if (startPoints === 0 && endPoints === 0) {
                        return "textExactlyInRange";
                    }
                    else {
                        return 'textWithinRange';
                    }
                }
            }
        }
    }
}

以下代码将当前选定内容中的所有文本节点包装在 span-elements 中。即使所选内容跨越多个深度嵌套元素并忽略非文本节点,它也能正常工作。

这个答案使用了几种在以前的答案时尚不可用的技术,最值得注意的是一种有效解析所有文本节点的nodeIterator

它保留了选择并支持 Firefox 选择多个独立文本范围的功能。

function wrapSelectedTextNodes(id, className) {
    getSelectedTextNodes().forEach((selection, index) => {
        selection.forEach((textNode, nodeNumber) => {
            let span = document.createElement('span');
            if (nodeNumber==0) span.id=id+"-"+index;
            else span.setAttribute("for",id+"-"+index);
            span.classList.add(className);
            textNode.before(span);
            span.appendChild(textNode);
        });
    });
}
function getSelectedTextNodes() {
    let returnArray = new Array();
    let selection = window.getSelection();
    for (let rangeNumber = selection.rangeCount-1; rangeNumber >= 0; rangeNumber--) {
        let rangeNodes = new Array();
        let range = selection.getRangeAt(rangeNumber);
        if (range.startContainer === range.endContainer && range.endContainer.nodeType === Node.TEXT_NODE) {
            range.startContainer.splitText(range.endOffset);
            let textNode = range.startContainer.splitText(range.startOffset);
            rangeNodes.push(textNode);
        } else {
            let textIterator = document.createNodeIterator(range.commonAncestorContainer, NodeFilter.SHOW_TEXT, (node) => (node.compareDocumentPosition(range.startContainer)==Node.DOCUMENT_POSITION_PRECEDING && node.compareDocumentPosition(range.endContainer)==Node.DOCUMENT_POSITION_FOLLOWING) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT );
            while (node = textIterator.nextNode()) { if (node.textContent.trim()!="") rangeNodes.push(node);}
            if (range.endContainer.nodeType === Node.TEXT_NODE) {
                range.endContainer.splitText(range.endOffset);
                rangeNodes.push(range.endContainer);
            }
            if (range.startContainer.nodeType === Node.TEXT_NODE) {
                rangeNodes.unshift(range.startContainer.splitText(range.startOffset));
            }
        }
        returnArray.unshift(rangeNodes);
    }
    return returnArray;
}
.selection { background-color:pink }
table { border-collapse:collapse }
td { border:1px solid black; padding:0.3em}
<p>Select any text (or in Firefox, multiple passages using Ctrl) across the different elements below. Then hit the "Highlight Selection" button.</p>
<p>This will wrap the selected text ins span-nodes and color it pink. It works for partially selected nodes and will keep the remaining dom-layout intact</p>
<ul>
  <li>it will work on lists</li>
  <li>and won't mess them up</li>
</ul>
<ol>
  <li>even if they are themselves nested around other complex elements</li>
  <li>
  <table>
      <tr>
        <td>such</td><td>as</td><td>tables</td>
        <td colspan=2>with weird</td><td>layout</td>
      </tr>
    </table>
  </li>
</ol>
<p>or <strong>with <i>deeply <u>nested</u> elements</i> like </strong>this.</p>
<button onclick="wrapSelectedTextNodes('mySelectionId','selection')">Highlight Selection</button>

大部分工作是在函数getSelectedTextNodes中完成的,函数可以独立使用。它会拆分部分选定的文本节点,因此结果将不再具有部分选定的文本节点。

包装器函数wrapSelectedTextNodes相当偏向于我自己的用法。您可能需要调整它(属性和 ID 可能对您没有用(。但是,请保留我的方法将文本节点包装在新的 span 节点中,因为这是唯一保持选择不变的方法。