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
bob. the function only returns the startNode and endNode. the nodes in between do not get pushed to the array.
seems the while loop returns null on getNextNode() hence that block never gets executed.
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;
}
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;
};