My 2023 answer to this very old question. Hope this helps someone:
Problem statement
We need all the nodes in a range without any extra nodes
Issues
- Using
cloneContents()
or other built-in range functions does not answer this problem
- Because
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>
- We want the elements in the DOM, not copies of those elements
- We need all the nodes between
startContainer
and endContainer
, not just their lineage
- If we start walking the tree from
startContainer
, 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.
- We need the elements of
startContainer
and endContainer
, even if they are text nodes
Example model of 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>
Example results for various ranges in the model
Range 1
___..............lor sit amet, <b>consectetur</b> adipis........._/__
returns <p/>
, <b/>
commonAncestorContainer is <p/>
(included)
Range 2
___..............lor sit amet, <b>conse......_/__................_/__
returns <p/>
, <b/>
commonAncestorContainer is <p/>
(included)
Range 3
___............................___.....ctetur</b> adipis........._/__
returns <p/>
, <b/>
commonAncestorContainer is <p/>
(included)
Range 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.._/__................_/__
returns <p/>
, <b/>
, <img>
, <ol/>
, <li/>
, <p/>
, <li/>
, <p/>
, <i/>
commonAncestorContainer is <figure/>
(purposefully not included)
Solution
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)
Explanation
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:
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