Reputation: 9759
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.
Upvotes: 26
Views: 24633
Reputation: 149
My 2023 answer to this very old question. Hope this helps someone:
We need all the nodes in a range without any extra nodes
cloneContents()
or other built-in range functions does not answer this problem
commonAncestorContainer
is a parent container, it is sometimes outside of the selected nodes. For example, Range 4 does not include <figure>
, but its commonAncestorContainer is <figure>
startContainer
and endContainer
, not just their lineagestartContainer
, we might not walk the tags that wrap that container. For example, walking from the text node at the start of Range 4 would ignore the closing </b>
tag and </p>
tag.startContainer
and endContainer
, even if they are text nodes<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>
___..............lor sit amet, <b>consectetur</b> adipis........._/__
returns <p/>
, <b/>
commonAncestorContainer is <p/>
(included)
___..............lor sit amet, <b>conse......_/__................_/__
returns <p/>
, <b/>
commonAncestorContainer is <p/>
(included)
___............................___.....ctetur</b> adipis........._/__
returns <p/>
, <b/>
commonAncestorContainer is <p/>
(included)
___...................................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.._/__................_/__
returns <p/>
, <b/>
, <img>
, <ol/>
, <li/>
, <p/>
, <li/>
, <p/>
, <i/>
commonAncestorContainer is <figure/>
(purposefully not included)
function getElsList(commonAncestor, optionalArgs) {
const { startNode, endNode } = optionalArgs || {};
const listEls = [];
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) {
listEls.push(currentNode);
}
if(currentNode === endNode) {
afterEnd = true;
} else {
walkTrees(currentNode);
}
}
}
walkTrees(commonAncestor);
return listEls;
}
const sel = window.getSelection();
const range = sel.getRangeAt(0);
const rangeEls = getElsList(range.commonAncestorContainer, { startNode: range.startContainer, endNode: range.endContainer })
console.log("els", rangeEls)
Everything outside of getElsList()
is for making a working example. In this example, we get a range based on the selected text. However, selecting text and getting a range is optional because the function accepts nodes
getElsList()
requires a node to walk. It then does the following:
If a startNode is provided, getElsList()
will first walk the lineage of that node. If the 'main' (common ancestor) node is not part of the selection, it is popped off the end of the list. The result is reversed so that the list order matches the DOM order
NOTES:
The do...while
ensures that we gather the start node
The do...while
combines setting currentEl.parentElement
and checking for null
(end of lineage)
getElsList()
calls walkTrees()
which gathers all the nodes between start and end. If no start node was provided, it gathers the start node. If a start node was provided, it was gathered in the previous step. The end node is always gathered
walkTrees()
recursively calls itself to accumulate the full set of trees between the start and end nodes
If an endNode is provided, getElsList()
will stop walking at that node
Upvotes: 2
Reputation: 91
With generator and 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
}
Upvotes: 0
Reputation: 3222
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
}
Upvotes: 1
Reputation: 1302
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));
Upvotes: 12
Reputation: 1317
here is function return you array of sub-ranges
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;
}
Upvotes: 0
Reputation: 310
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.
Upvotes: 0
Reputation: 7543
The Rangy library has a Range.getNodes([Array nodeTypes[, Function filter]])
function.
Upvotes: 3
Reputation: 8005
Annon, great work. I've modified the original plus included Stefan's modifications in the following.
In addition, I removed the reliance on Range, which converts the function into an generic algorithm to walk between two nodes. Plus, I've wrapped everything into a single function.
Thoughts on other solutions:
Example:
<div>
<p>A</p>
<div>
<p>B</p>
<div>
<p>C</p>
</div>
</div>
</div>
Assume start node is the "A" paragraph, and the end node is the "C" paragraph . The resulting cloned fragment would be:
<p>A</p>
<div>
<p>B</p>
<div>
<p>C</p>
and we're missing closing tags? resulting in funky DOM structure?
Anyway, here's the function, which includes a filter option, which should return TRUE or FALSE to include/exclude from results.
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;
};
Upvotes: 3
Reputation: 61
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;
};
Upvotes: 2
Reputation: 19
below code solve your problem
<!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>
Upvotes: 1
Reputation: 181
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;
}
Upvotes: 18