17

When implementing a rich text editor in javascript, I need to apply some changes to every text node in selected range. Range object provides interface to get startContainer, endContainer, startOffset, endOffset for the selected range. How can I iterate over every DOM node in between?

var selection = window.getSelection();
var range = selection.getRange(0);
// How can I iterate over every node within the range?
NeoWang
  • 17,361
  • 24
  • 78
  • 126

2 Answers2

16

As suggested, you can use NodeIterator to walk inside range.commonAncestorContainer.

Here's a snippet:

var _iterator = document.createNodeIterator(
    range.commonAncestorContainer,
    NodeFilter.SHOW_ALL, // pre-filter
    {
        // custom filter
        acceptNode: function (node) {
            return NodeFilter.FILTER_ACCEPT;
        }
    }
);

var _nodes = [];
while (_iterator.nextNode()) {
    if (_nodes.length === 0 && _iterator.referenceNode !== range.startContainer) continue;
    _nodes.push(_iterator.referenceNode);
    if (_iterator.referenceNode === range.endContainer) break;
}

You should use NodeFilter.SHOW_ALL because your range can contain multiple nodeTypes. If you know what you are selecting, you can check this reference to properly choose NodeFilter.


Edit: I also want to point out document.createTreeWalker().

The key difference is that document.createTreeWalker() allow your acceptNode filter to return both NodeFilter.FILTER_REJECT and NodeFilter.FILTER_SKIP with real differences.

Quote from NodeFilter docs:

FILTER_REJECT:

Value to be returned by the NodeFilter.acceptNode() method when a node should be rejected. For TreeWalker, child nodes are also rejected. For NodeIterator, this flag is synonymous with FILTER_SKIP.

Ps: the NodeFilter.acceptNode() documentation for NodeFilter.FILTER_REJECT is incorrect.

James Tan
  • 1,336
  • 1
  • 14
  • 32
raphaelbs
  • 440
  • 4
  • 13
  • I might be wrong, but I think not using NodeFilter.SHOW_ALL would potentially result in the first condition always evaluating to false, as referenceNode might never equal startContainer if it was filtered away. – Benny Bottema Nov 18 '20 at 20:04
5

range.commonAncestorContainer will give you the node that encompasses the range. If it gives you a text node, then that's the only node in your range.

If it gives you an element, you can use NodeIterator, or el.querySelectorAll('*') to get the nodes within.

Not all of these will be inside your range, so use range.intersectsNode(el) to confirm.

JaffaTheCake
  • 13,895
  • 4
  • 51
  • 54
  • `Range.intersectsNode()` has no support on IE (up to and including 11) :( Likewise `Selection.containsNode()`. – simon Jul 22 '17 at 07:00