node-boundary
A better interface for DOM anchors and ranges:
- Allows working with anchors directly, rather than using a collapsed
Range
- Anchors and ranges don't change positions with DOM mutations
- Clean interfaces for comparison and traversal
- Interoperable with
Range
andStaticRange
API documentation | npm package | GitHub source code
Installation
npm i node-boundary
This project uses ES 2015+ class features. A Babel transpiled and minified version is provided as boundary.compat.min.js
, with exports under NodeBoundary
; though I highly recommend building a bundle yourself to customize the target and reduce code size. A plain minified version is provided as boundary.min.js
.
Usage
// an alias is recommended for BoundaryFlags
import { Boundary, BoundaryRange, BoundaryFlags as F} from "node-boundary";
If not using a bundler, you'll need to provide a path to the actual source file, e.g. ./node_modules/node-boundary/boundary.mjs
.
Boundary
Use a Boundary
object to represent a position, or anchor, inside the DOM. A position in the DOM is given by a reference node
, and a side
relative to that node. It is called a "boundary" because the position is tied to a node's inner/outer bounds. For example:
A<main>B <article> </article> C</main>D
const main = document.querySelector("main");
const A = new Boundary(main, BoundaryFlags.BEFORE_OPEN);
const B = new Boundary(main, BoundaryFlags.AFTER_OPEN);
const C = new Boundary(main, BoundaryFlags.BEFORE_CLOSE);
const D = new Boundary(main, BoundaryFlags.AFTER_CLOSE);
The letters A-D give the possible positions relative to the main
reference node:
- A:
BEFORE_OPEN
outside the node, immediately preceding - B:
AFTER_OPEN
inside the node, before any child nodes - C:
BEFORE_CLOSE
inside the node, after any child nodes - D:
AFTER_CLOSE
outside the node, immediately following
Two boundaries can have the same position, but differ in the reference node they are attached to. See Boundary.isAdjacent
. For example:
<main>A B<article> </article> </main>
const main = document.querySelector("main");
const article = document.querySelector("article");
const A = new Boundary(main, BoundaryFlags.AFTER_OPEN);
const B = new Boundary(article, BoundaryFlags.BEFORE_OPEN);
A.isAdjacent(B); // true
Contrast this encoding with using a collapsed Range
; a Range
specifies a position as a relative offset into a node's childNodes
list. There is no way to encode "before a node" or "at the end of a node", since added/removed children will invalidate the position. The position given by a Boundary
on the other hand will not change with DOM mutations.
BoundaryRange
Use a BoundaryRange
object to represent a range between two positions. Internally, this is represented by a starting and ending Boundary
. You may access the start/end boundary directly and perform operations on it, which conveniently simplifies many of the operations that you use on Range
.
A BoundaryRange
is akin to a StaticRange
, in that it does not validate that the start/end anchors belong to the same DOM tree or that end follows start. However, unlike StaticRange
, you are still able to operate and modify the range as needed. The idea is the range can continue to be used despite any DOM mutations. Some of the comparison and update operations will access the current DOM (e.g. extend
, toRange
, normalize
), so just be wary of this when dealing with mutating DOM's.
While many of the Range
interfaces methods have been implemented on BoundaryRange
, some more computationally heavy ones have not. For these, you can always convert to a Range
to perform the operation, provided the start/end anchors are properly ordered. For example:
const range = boundary.toRange();
range.extractContents();
range.getClientRects();
Examples
Inserting a span
before every node:
<main><div></div><div></div></main>
const b = new Boundary(document.body, BoundaryFlags.AFTER_OPEN);
for (const _ of b.nextNodes()){
if (b.side === BoundaryFlags.BEFORE_OPEN)
b.insert(document.createElement("span"));
}
Iterating all element boundaries within a node:
<main>
<div></div>
<div></div>
</main>
const r = new BoundaryRange();
r.selectNodeContents(document.querySelector("main"));
while (true) {
if (r.start.node.nodeType == Node.ELEMENT_NODE)
console.log(r.start.node, r.end.side);
if (r.start.isEqual(r.end))
break;
r.start.next();
}
Getting the combined extent of two ranges:
<main>
<aside></aside>
<article></article>
</main>
const main = document.querySelector("main");
const aside = main.firstElementChild;
const article = main.lastElementChild;
const r1 = new BoundaryRange();
r1.setStart(main, BoundaryFlags.AFTER_OPEN).setEnd(aside, BoundaryFlags.BEFORE_CLOSE);
const r2 = new BoundaryRange();
r2.selectNode(article);
r1.extend(r2);
// result is a combination of the two ranges
r1.start.isEqual(new Boundary(main, BoundaryFlags.AFTER_OPEN)); // true
r1.end.isEqual(new Boundary(article, BoundaryFlags.AFTER_CLOSE)); // true
Checking if a position is inside a range:
<main>Lorem ipsum</main>
const main = document.querySelector("main");
const txt = main.firstChild;
const r = (new BoundaryRange()).selectNode(main);
const b = new Boundary(txt, BoundaryFlags.BEFORE_OPEN);
if (b.compare(r.start) >= 0 && b.compare(r.end) <= 0)
console.log("inside range");
Robustness of range to DOM modifications:
<main></main>
const main = document.querySelector("main");
const r = (new BoundaryRange()).selectNodeContents(main);
// add some nodes
for (let i=0; i<5; i++)
main.appendChild(document.createElement("span"))
// extracted contents will contain all spans
console.log(r.toRange().extractContents());