How to get nodes lying inside a range with javascript?

20,240

Solution 1

The getNextNode will skip your desired endNode recursively if its a parent node.

Perform the conditional break check inside of the getNextNode instead:

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

and in while statement:

while (startNode = getNextNode(startNode, false , endNode));

Solution 2

Here's an implementation I came up with to solve this:

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

Solution 3

The Rangy library has a Range.getNodes([Array nodeTypes[, Function filter]]) function.

Solution 4

I made 2 additional fixes based on MikeB's answer to improve the accuracy of the selected nodes.

I'm particularly testing this on select all operations, other than range selection made by dragging the cursor along text spanning across multiple elements.

In Firefox, hitting select all (CMD+A) returns a range where it's startContainer & endContainer is the contenteditable div, the difference is in the startOffset & endOffset where it's respectively the index of the first and the last child node.

In Chrome, hitting select all (CMD+A) returns a range where it's startContainer is the first child node of the contenteditable div, and the endContainer is the last child node of the contenteditable div.

The modifications I've added work around the discrepancies between the two. You can see the comments in the code for additional explanation.

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

Solution 5

I wrote the perfect code for this and it works 100% for every node :

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
}

here are some helper functions

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
}
Share:
20,240
AnnanFay
Author by

AnnanFay

I am an experienced developer looking to solve interesting problems and create insightful programs. My recent expertise is in using Python to investigate algorithmic efficiency, optimisation problems and visualise data.

Updated on August 18, 2020

Comments

  • AnnanFay
    AnnanFay over 3 years

    I'm trying to get all the DOM nodes that are within a range object, what's the best way to do this?

    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*/;
    

    I've been been thinking of a way for the last few hours and came up with this:

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

    However when the end node is the parent of the start node it returns everything on the page. I'm sure I'm overlooking something obvious? Or maybe going about it in totally the wrong way.

    MDC/DOM/range

    • caub
      caub over 8 years
      var c=getSelection().getRangeAt(0).cloneContents(); c.querySelectorAll('*')
  • AnnanFay
    AnnanFay about 15 years
    Thanks :) Might want to edit the second bit though, it's only passing in two parameters and missing the ending bracket.
  • Pancho
    Pancho over 9 years
    what a great piece of code. While payam jabbari's below use of querySelectorAll is neat, the fundamental problem with his approach for me is that it clones the nodes ie. removes them from the dom, whereas yours doesn't and thus provides direct dom manipulation. Thanks very much for this.
  • Kirill E.
    Kirill E. over 3 years
    Still returns full document for me.