【问题标题】:How To Wrap / Surround Highlighted Text With An Element如何用元素包裹/环绕突出显示的文本
【发布时间】:2011-09-13 19:07:50
【问题描述】:

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

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

【问题讨论】:

    标签: javascript dom selection


    【解决方案1】:

    以下跨多个 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>

    【讨论】:

      【解决方案2】:

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

      首先,这是一个非常通用的函数,用于在 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')
      

      【讨论】:

        【解决方案3】:

        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 Fiddle.

        【讨论】:

        • 这很好,只要选择不跨越多个块级元素,只要你不介意潜在的嵌套跨度。此外,如果链接页面不起作用,则仅包含 jsFiddle 链接的答案将毫无用处,因此最好至少在答案中描述该方法。
        【解决方案4】:

        请发现下面的代码将有助于为所有类型的标签包装 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';
                            }
                        }
                    }
                }
            }
        }
        

        【讨论】:

        • 欢迎来到 StackOverflow!一个很好的答案解释了代码在做什么,以便人们可以从中学习,而不是简单地成为一个巨大的代码块。您可以通过突出显示重要部分来改进您的答案。
        【解决方案5】:

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

        此外,在 IE Range 和 window.getSelection()。对于这些浏览器,您需要再次使用另一种方法。您可以使用诸如我自己的Rangy 之类的库来规范浏览器行为,您可能会发现class applier module 对这个问题很有用。

        简单的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);
                }
            }
        }
        

        【讨论】:

        • 如果所选文本跨越节点边界,我们如何用 span 包裹所选文本?任何解决方案(无论多么复杂)都将不胜感激。
        • @JoshGrinberg:这是不可能的,因为你的标签不匹配(例如&lt;b&gt;foo&lt;i&gt;bar&lt;/b&gt;baz&lt;/i&gt;)。可能,您首先必须操作已经存在的标签,以便您要包装的区域仅包含在一个节点中。
        • @Tim Down 我正在使用您的 rangy 库为所选文本创建注释。我几乎完成了,但遇到了一个问题,即我在所选文本之前显示一个点,但是在多个元素的情况下,当我尝试在其上创建注释时,它会多次显示点,因为创建多个跨度标签是由于多个元素。我如何才能在选定文本的第一个节点上应用任何一个类?
        • @Tim Down 可以显示选定的文本吗?例如使用 innerHrml?
        【解决方案6】:

        surroundContents 仅在您的选择仅包含文本且不包含 HTML 时才有效。这是一个更灵活的跨浏览器解决方案。这将插入一个像这样的跨度:

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

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

        【讨论】:

        • 如果您想包围一个不包含 HTML 元素的选择,那么公认的解决方案效果最好。此解决方案将通过在目标元素之前放置标记跨度来帮助您识别包含所选文本的现有节点。
        【解决方案7】:

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

        【讨论】:

          猜你喜欢
          • 2013-07-24
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2011-06-05
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多