Как получить узлы, лежащие внутри диапазона, с помощью javascript?

StackOverflow https://stackoverflow.com/questions/667951

Вопрос

Я пытаюсь получить все узлы DOM, которые находятся в пределах объекта range, каков наилучший способ сделать это?

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/диапазон

Это было полезно?

Решение

getNextNode рекурсивно пропустит ваш желаемый конечный узел, если это родительский узел.

Вместо этого выполните проверку условного разрыва внутри 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));

Другие советы

Вот реализация, которую я придумал, чтобы решить эту проблему:

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

приведенный ниже код решит вашу проблему

    <!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>

Я внес 2 дополнительных исправления на основе ответа Майкеба, чтобы повысить точность выбранных узлов.

Я особенно тестирую это при выполнении операций выбора всех, кроме выбора диапазона, выполняемого путем перетаскивания курсора вдоль текста, охватывающего несколько элементов.

В Firefox нажатие select all (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;
};

Аннон, отличная работа.Я изменил оригинал, а также включил изменения Стефана в следующее.

Кроме того, я убрал зависимость от 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;
};

боб.функция возвращает только начальный и конечный узлы.узлы, находящиеся между ними, не помещаются в массив.

похоже, цикл while возвращает null для getNextNode(), следовательно, этот блок никогда не выполняется.

вот функция, возвращающая вам массив поддиапазонов

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;

}

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top