如何使用JavaScript获取位于范围内的节点?

25

我正在尝试获取在一个范围对象内的所有DOM节点,最好的方法是什么?

var selection = window.getSelection(); //what the user has selected
var range = selection.getRangeAt(0); //the first range of the selection
var startNode = range.startContainer;
var endNode = range.endContainer;
var allNodes = /*insert magic*/;

我已经思考了几个小时,想出了以下方法:

var getNextNode = function(node, skipChildren){
    //if there are child nodes and we didn't come from a child node
    if (node.firstChild && !skipChildren) {
        return node.firstChild;
    }
    if (!node.parentNode){
        return null;
    }
    return node.nextSibling 
        || getNextNode(node.parentNode, true);
};

var getNodesInRange = function(range){
    var startNode = range.startContainer.childNodes[range.startOffset]
            || range.startContainer;//it's a text node
    var endNode = range.endContainer.childNodes[range.endOffset]
            || range.endContainer;

    if (startNode == endNode && startNode.childNodes.length === 0) {
        return [startNode];
    };

    var nodes = [];
    do {
        nodes.push(startNode);
    }
    while ((startNode = getNextNode(startNode)) 
            && (startNode != endNode));
    return nodes;
};

然而,当结束节点是起始节点的父节点时,它会返回页面上的所有内容。我确定我漏掉了一些显而易见的东西?或者我可能完全错误地去做了。 MDC/DOM/range

2
var c=getSelection().getRangeAt(0).cloneContents(); c.querySelectorAll('*') - caub
cloneContents() 返回标签中所选部分,例如:<strong>Th|is te|xt</strong> 将返回 <strong>is te</strong>(我用 | 标记了所选内容)。 - Gevorg Hakobyan
11个回答

18

这是我想出来解决此问题的实现方式:

function getNextNode(node)
{
    if (node.firstChild)
        return node.firstChild;
    while (node)
    {
        if (node.nextSibling)
            return node.nextSibling;
        node = node.parentNode;
    }
}

function getNodesInRange(range)
{
    var start = range.startContainer;
    var end = range.endContainer;
    var commonAncestor = range.commonAncestorContainer;
    var nodes = [];
    var node;

    // walk parent nodes from start to common ancestor
    for (node = start.parentNode; node; node = node.parentNode)
    {
        nodes.push(node);
        if (node == commonAncestor)
            break;
    }
    nodes.reverse();

    // walk children and siblings from start until end is found
    for (node = start; node; node = getNextNode(node))
    {
        nodes.push(node);
        if (node == end)
            break;
    }

    return nodes;
}

多好的一段代码啊。虽然Payam Jabbari使用querySelectorAll的方法很巧妙,但他的方法对我来说存在一个根本性问题,即它会克隆节点,也就是从DOM中删除它们,而你的方法则不会,因此提供了直接的DOM操作。非常感谢你的分享。 - Pancho
仍然为我返回完整文档。 - Kirill E.
这很可能总是返回完整的文档。它现在有最多的赞,但是在“从起点向共同祖先遍历父节点”的逻辑上存在问题。 也许应该从结尾开始“遍历父节点”?无论哪种方式,它似乎都不能处理从子节点到共同祖先再到子节点的路径。这里有一些好的代码思路,值得一看。@bob的答案也值得一看,但似乎存在相同的问题,不能处理从子节点到共同祖先再到子节点的路径。 - mikeypie

12
getNextNode会递归跳过您所需的endNode,如果它是父节点。在getNextNode中执行条件断点检查:
var getNextNode = function(node, skipChildren, endNode){
  //if there are child nodes and we didn't come from a child node
  if (endNode == node) {
    return null;
  }
  if (node.firstChild && !skipChildren) {
    return node.firstChild;
  }
  if (!node.parentNode){
    return null;
  }
  return node.nextSibling 
         || getNextNode(node.parentNode, true, endNode); 
};

在 while 语句中:
while (startNode = getNextNode(startNode, false , endNode));

可能需要编辑第二部分,因为它只传递了两个参数并且缺少结束括号。 - AnnanFay

3
Annon,干得好。我修改了原始内容并包含了Stefan的修改,具体如下。
此外,我去掉了对Range的依赖,将函数转换为一个通用算法,以在两个节点之间进行遍历。另外,我将所有内容都包装成了一个单一的函数。
其他解决方案的想法:
- 不希望依赖于jQuery。 - 使用cloneNode将结果提升到一个片段中,这样会阻止在过滤期间进行许多操作。 - 在克隆片段上使用querySelectAll有些奇怪,因为起始或结束节点可能在包装节点内,因此解析器可能没有关闭标签?
示例:
<div>
    <p>A</p>
    <div>
        <p>B</p>
        <div>
            <p>C</p>
        </div>
    </div>
</div>

假设起始节点是“A”段落,终止节点是“C”段落。克隆后的片段将会是:
<p>A</p>
    <div>
        <p>B</p>
        <div>
            <p>C</p>

我们缺少闭合标签吗?这会导致奇怪的DOM结构吗?

不管怎样,这是函数,它包括一个过滤选项,该选项应返回TRUE或FALSE以包含/排除结果。

var getNodesBetween = function(startNode, endNode, includeStartAndEnd, filter){
    if (startNode == endNode && startNode.childNodes.length === 0) {
        return [startNode];
    };

    var getNextNode = function(node, finalNode, skipChildren){
        //if there are child nodes and we didn't come from a child node
        if (finalNode == node) {
            return null;
        }
        if (node.firstChild && !skipChildren) {
            return node.firstChild;
        }
        if (!node.parentNode){
            return null;
        }
        return node.nextSibling || getNextNode(node.parentNode, endNode, true);
    };

    var nodes = [];

    if(includeStartAndEnd){
        nodes.push(startNode);
    }

    while ((startNode = getNextNode(startNode, endNode)) && (startNode != endNode)){
        if(filter){
            if(filter(startNode)){
                nodes.push(startNode);
            }
        } else {
            nodes.push(startNode);
        }
    }

    if(includeStartAndEnd){
        nodes.push(endNode);
    }

    return nodes;
};

3

Rangy库有一个Range.getNodes([Array nodeTypes[, Function filter]])函数。

该函数与此链接相关。

2

我根据MikeB的答案进行了两项额外的修复,以提高所选节点的准确性。

我特别测试了全选操作,除了通过拖动光标跨越多个元素的文本进行范围选择之外的其他操作。

在Firefox中,点击全选(CMD+A)返回一个范围,其中startContainer和endContainer是contenteditable div,不同之处在于startOffset和endOffset分别是第一个和最后一个子节点的索引。

在Chrome中,点击全选(CMD+A)返回一个范围,其中startContainer是contenteditable div的第一个子节点,而endContainer是contenteditable div的最后一个子节点。

我添加的修改解决了两者之间的差异。您可以在代码中查看注释以获得更多解释。

function getNextNode(node) {
    if (node.firstChild)
        return node.firstChild;

    while (node) {
        if (node.nextSibling) return node.nextSibling;
        node = node.parentNode;
    }
}

function getNodesInRange(range) {

    // MOD #1
    // When the startContainer/endContainer is an element, its
    // startOffset/endOffset basically points to the nth child node
    // where the range starts/ends.
    var start = range.startContainer.childNodes[range.startOffset] || range.startContainer;
    var end = range.endContainer.childNodes[range.endOffset] || range.endContainer;
    var commonAncestor = range.commonAncestorContainer;
    var nodes = [];
    var node;

    // walk parent nodes from start to common ancestor
    for (node = start.parentNode; node; node = node.parentNode)
    {
        nodes.push(node);
        if (node == commonAncestor)
            break;
    }
    nodes.reverse();

    // walk children and siblings from start until end is found
    for (node = start; node; node = getNextNode(node))
    {
        // MOD #2
        // getNextNode might go outside of the range
        // For a quick fix, I'm using jQuery's closest to determine
        // when it goes out of range and exit the loop.
        if (!$(node.parentNode).closest(commonAncestor)[0]) break;

        nodes.push(node);
        if (node == end)
            break;
    }

    return nodes;
};

2

我对这个非常古老的问题的2023年答案。希望能帮助到某些人:

问题陈述

我们需要在没有任何额外节点的情况下获取范围内的所有节点

问题

  • 使用cloneContents()或其他内置的范围函数无法解决此问题。
    • 因为commonAncestorContainer是一个父容器,有时它在所选节点之外。例如,范围4不包括<figure>,但其commonAncestorContainer是<figure>
    • 我们需要的是DOM中的元素,而不是这些元素的副本。
  • 我们需要startContainerendContainer之间的所有节点,而不仅仅是它们的血统。
  • 如果我们从startContainer开始遍历树,可能会忽略包装该容器的标记。例如,从范围4开头的文本节点开始遍历将忽略关闭的</b>标记和</p>标记。
  • 我们需要startContainerendContainer的元素,即使它们是文本节点也一样。

节点示例模型

<figure>
  <p>Lorem ipsum dolor sit amet, <b>consectetur</b> adipiscing elit</p>
  <img>
  <ol>
    <li>
      <p>sed do eiusmod tempor incididunt</p>
    </li>
    <li></li>
  </ol>
  <p>ut labore et dolore magna aliqua. Ut <i>enim</i> ad minim veniam</p>
</figure>

模型中不同范围的示例结果

范围1

  ___..............lor sit amet, <b>consectetur</b> adipis........._/__

返回 <p/><b/>

commonAncestorContainer 是 <p/>(包括在内)

范围 2

  ___..............lor sit amet, <b>conse......_/__................_/__

返回 <p/><b/>

commonAncestorContainer 是 <p/>(包括在内)

范围 3

  ___............................___.....ctetur</b> adipis........._/__

返回 <p/><b/>

commonAncestorContainer 是 <p/>(包括在内)

范围 4

  ___...................................ectetur</b> adipiscing elit</p>
  <img>
  <ol>
    <li>
      <p>sed do eiusmod tempor incididunt</p>
    </li>
    <li></li>
  </ol>
  <p>ut labore et dolore magna aliqua. Ut <i>en.._/__................_/__

返回 <p/><b/><img><ol/><li/><p/><li/><p/><i/>

commonAncestorContainer 是 <figure/>(故意不包含)

解决方案

function getElsList(commonAncestor, optionalArgs) {
    const { startNode, endNode } = optionalArgs || {};
    const domEls = [];
    let beforeStart = false;
    let afterEnd = false;

    function getEl(nodeOrEl) {
        if(nodeOrEl?.nodeType === 1) { //type 1 is el
            return nodeOrEl;
        } else {
            return nodeOrEl?.parentElement;
        }
    }
    
    //go backward and out:
    const commonAncestorEl = getEl(commonAncestor);
    let endEl = commonAncestorEl;
    let startEl = commonAncestorEl;
    if(endNode) {
        endEl = getEl(endNode);
    }
    if(startNode) {
        startEl = getEl(startNode);
        beforeStart = true;
    }
    let currentEl = startEl;
    do {
        listEls.push(currentEl);
    } while(currentEl !== commonAncestorEl && (currentEl = currentEl.parentElement));
    if(endEl !== commonAncestorEl && startEl !== commonAncestorEl && endEl !== startEl) {
        listEls.pop();
    }
    listEls.reverse(); //backward and out becomes forward and in

    //go forward and in:
    function walkTrees(branch) {
        const branchNodes = branch.childNodes;
        for(let i = 0; !afterEnd && i < branchNodes.length; i++) {
            let currentNode = branchNodes[i];
            if(currentNode === startNode) {
                beforeStart = false;
            }
            if(!beforeStart && currentNode.nodeType === 1) {
                domEls.push(currentNode);
            }
            if(currentNode === endNode) {
                afterEnd = true;
            } else {
                walkTrees(currentNode);
            }
        }
    }
    walkTrees(commonAncestor);

    return domEls;
}

const sel = window.getSelection();
const range = sel.getRangeAt(0);
const rangeEls = getElsList(range.commonAncestorContainer, { startNode: range.startContainer, endNode: range.endContainer })
console.log("els", rangeEls)

说明

getElsList() 之外的所有内容都是为了创建一个可工作的示例。在此示例中,我们根据所选文本获取范围。但是,选择文本并获取范围是可选的,因为该函数接受节点。

getElsList() 需要一个节点进行遍历。然后它执行以下操作:

如果提供了startNode,getElsList()将首先遍历该节点的谱系。如果“主”(共同祖先)节点不是选择的一部分,则它将从列表末尾弹出。结果被反转,以使列表顺序与DOM顺序匹配。
注意: - do...while确保我们收集起始节点 - do...while结合设置currentEl.parentElement和检查null(谱系的末端)
getElsList()调用walkTrees(),收集起始节点和结束节点之间的所有节点。如果没有提供起始节点,则会收集起始节点。如果提供了起始节点,则在上一步中已经收集到。总是收集结束节点。
如果提供了endNode,getElsList()将停止遍历该节点。

1
以下代码解决了您的问题。
    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>payam jabbari</title>
<script src="http://code.jquery.com/jquery-2.0.2.min.js" type="text/javascript"></script>
<script type="text/javascript">

$(document).ready(function(){
    var startNode = $('p.first').contents().get(0);
var endNode = $('span.second').contents().get(0);
var range = document.createRange();
range.setStart(startNode, 0);
range.setEnd(endNode, 5);
var selection = document.getSelection();
selection.addRange(range);
// below code return all nodes in selection range. this code work in all browser
var nodes = range.cloneContents().querySelectorAll("*");
for(var i=0;i<nodes.length;i++)
{
   alert(nodes[i].innerHTML);
}
});
</script>
</head>

<body>
<div>

<p class="first">Even a week ago, the idea of a Russian military intervention in Ukraine seemed far-fetched if not totally alarmist. But the arrival of Russian troops in Crimea over the weekend has shown that he is not averse to reckless adventures, even ones that offer little gain. In the coming days and weeks</p>

<ol>
    <li>China says military will respond to provocations.</li>
    <li >This Man Has Served 20 <span class="second"> Years—and May Die—in </span> Prison for Marijuana.</li>
    <li>At White House, Israel's Netanyahu pushes back against Obama diplomacy.</li>
</ol>
</div>
</body>
</html>

1
我为此编写了完美的代码,并且它可以在每个节点上100%地工作:
function getNodesInSelection() {
    
var range = window.getSelection().getRangeAt(0);
var node = range.startContainer;

var ranges = []
var nodes = []
        
while (node != null) {        
    
    var r = document.createRange();
    r.selectNode(node)
    
    if(node == range.startContainer){
        r.setStart(node, range.startOffset)
    }
    
    if(node == range.endContainer){
        r.setEnd(node, range.endOffset)
    }
    
    
    ranges.push(r)
    nodes.push(node)
    
    node = getNextElementInRange(node, range)
}
     
// do what you want with ranges and nodes
}

这里是一些辅助函数。
function getClosestUncle(node) {

var parent = node.parentElement;

while (parent != null) {
    var uncle = parent.nextSibling;
    if (uncle != null) {
        return uncle;
    }
    
    uncle = parent.nextElementSibling;
    if (uncle != null) {
        return uncle;
    }
    
    parent = parent.parentElement
}

return null
}
 

                    
function getFirstChild(_node) {

var deep = _node

while (deep.firstChild != null) {
    
    deep = deep.firstChild
}

return deep
}
  

                    
function getNextElementInRange(currentNode, range) {

var sib = currentNode.nextSibling;

if (sib != null && range.intersectsNode(sib)) {
    return getFirstChild(sib)
}
        
var sibEl = currentNode.nextSiblingElemnent;

if (sibEl != null && range.intersectsNode(sibEl)) {
    return getFirstChild(sibEl)
}

var uncle = getClosestUncle(currentNode);
var nephew = getFirstChild(uncle)

if (nephew != null && range.intersectsNode(nephew)) {
    return nephew
}

return null
}

0

这是一个返回子范围数组的函数

function getSafeRanges(range) {

var doc = document;

var commonAncestorContainer = range.commonAncestorContainer;
var startContainer = range.startContainer;
var endContainer = range.endContainer;
var startArray = new Array(0),
    startRange = new Array(0);
var endArray = new Array(0),
    endRange = new Array(0);
// @@@@@ If start container and end container is same
if (startContainer == endContainer) {
    return [range];
} else {
    for (var i = startContainer; i != commonAncestorContainer; i = i.parentNode) {
        startArray.push(i);
    }
    for (var i = endContainer; i != commonAncestorContainer; i = i.parentNode) {
        endArray.push(i);
    }
}
if (0 < startArray.length) {
    for (var i = 0; i < startArray.length; i++) {
        if (i) {
            var node = startArray[i - 1];
            while ((node = node.nextSibling) != null) {
                startRange = startRange.concat(getRangeOfChildNodes(node));
            }
        } else {
            var xs = doc.createRange();
            var s = startArray[i];
            var offset = range.startOffset;
            var ea = (startArray[i].nodeType == Node.TEXT_NODE) ? startArray[i] : startArray[i].lastChild;
            xs.setStart(s, offset);
            xs.setEndAfter(ea);
            startRange.push(xs);
        }
    }
}
if (0 < endArray.length) {
    for (var i = 0; i < endArray.length; i++) {
        if (i) {
            var node = endArray[i - 1];
            while ((node = node.previousSibling) != null) {
                endRange = endRange.concat(getRangeOfChildNodes(node));
            }
        } else {
            var xe = doc.createRange();
            var sb = (endArray[i].nodeType == Node.TEXT_NODE) ? endArray[i] : endArray[i].firstChild;
            var end = endArray[i];
            var offset = range.endOffset;
            xe.setStartBefore(sb);
            xe.setEnd(end, offset);
            endRange.unshift(xe);
        }
    }
}
var topStartNode = startArray[startArray.length - 1];
var topEndNode = endArray[endArray.length - 1];
var middleRange = getRangeOfMiddleElements(topStartNode, topEndNode);
startRange = startRange.concat(middleRange);
response = startRange.concat(endRange);
return response;

}


0
使用生成器和document.createTreeWalker
function *getNodeInRange(range) {
  let [start, end] = [range.startContainer, range.endContainer]
  if (start.nodeType < Node.TEXT_NODE || Node.COMMENT_NODE < start.nodeType) {
    start = start.childNodes[range.startOffset]
  }
  if (end.nodeType < Node.TEXT_NODE || Node.COMMENT_NODE < end.nodeType) {
    end = end.childNodes[range.endOffset-1]
  }
  const relation = start.compareDocumentPosition(end)
  if (relation & Node.DOCUMENT_POSITION_PRECEDING) {
      [start, end] = [end, start]
  }

  const walker = document.createTreeWalker(
    document, NodeFilter.SHOW_ALL
  )
  walker.currentNode = start
  yield start
  while (walker.parentNode()) yield walker.currentNode

  if (!start.isSameNode(end)) {
    walker.currentNode = start
    while (walker.nextNode()) {
      yield walker.currentNode
      if (walker.currentNode.isSameNode(end)) break
    }
  }
  
  const subWalker = document.createTreeWalker(
    end, NodeFilter.SHOW_ALL
  )
  while (subWalker.nextNode()) yield subWalker.currentNode
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接