// Better to have these as constants for minification
const BEFORE_OPEN = 0b1,
AFTER_OPEN = 0b10,
BEFORE_CLOSE = 0b1000,
AFTER_CLOSE = 0b10000,
FILTER_ALL = 0b11011,
FILTER_OPEN = 0b11,
FILTER_CLOSE = 0b11000,
FILTER_BEFORE = 0b1001,
FILTER_AFTER = 0b10010,
FILTER_INSIDE = 0b1010,
FILTER_OUTSIDE = 0b10001,
POSITION_BEFORE = 0b0,
POSITION_INSIDE = 0b100,
POSITION_AFTER = 0b100000;
/** Boundary bit flags. Use these to define and work with a boundary's side. The primary bit flags
* are ordered by their DOM position, so can be used for comparisions. E.g. `BEFORE_OPEN < AFTER_OPEN`.
* To use the filter bitmasks, you need to use bitwise operations, for example:
*
* ```js
* BEFORE_OPEN & FILTER_OPEN // true
* (AFTER_OPEN | BEFORE_CLOSE) & FILTER_OPEN // true
* ```
*
* @see {@link Boundary#side}
* @readonly
* @enum
* @alias BoundaryFlags
*/
const Flags = {
// for Boundary.side; magnitude matches DOM order
/** Denotes a position before the opening boundary of a node (outside the node) */
BEFORE_OPEN,
/** Denotes a position after the opening boundary of a node (inside the node) */
AFTER_OPEN,
/** Denotes a position before the closing boundary of a node (inside the node) */
BEFORE_CLOSE,
/** Denotes a position after the closing boundary of a node (outside the node) */
AFTER_CLOSE,
// for filtering by Boundary.side
/** Bitmask to filter any position */
FILTER_ALL,
/** Bitmask to filter positions relative to a node's opening boundary */
FILTER_OPEN,
/** Bitmask to filter positions relative to a node's closing boundary */
FILTER_CLOSE,
/** Bitmask to filter positions before the opening or closing node boundary */
FILTER_BEFORE,
/** Bitmask to filter positions after the opening or closing node boundary */
FILTER_AFTER,
/** Bitmask to filter positions inside the reference node */
FILTER_INSIDE,
/** Bitmask to filter positions outside the reference node */
FILTER_OUTSIDE,
// for comparing positions relative to a boundary
/** Used to indicate a Boundary that is before a node; `BEFORE_OPEN > POSITION_BEFORE` */
POSITION_BEFORE,
/** Used to indicate a Boundary that is inside a node; `AFTER_OPEN > POSITION_INSIDE > BEFORE_CLOSE` */
POSITION_INSIDE,
/** Used to indicate a Boundary that is after a node; `POSITION_AFTER > AFTER_CLOSE` */
POSITION_AFTER
};
/**
* Encodes a node boundary. Every node has an opening and closing boundary; for HTML, this
* corresponds to the opening/closing tag. There is also an inner and outer half to each boundary,
* denoting the bounds for a node's children and siblings respectively. For example:
*
* ```html
* A<span>B C</span>D
* ```
*
* Each of the letters illustrates a different boundary in reference to the `<span>` node. When defining a
* Boundary, you specify a reference node, and one of four sides:
* - A: {@link BoundaryFlags.BEFORE_OPEN|BEFORE_OPEN}
* - B: {@link BoundaryFlags.AFTER_OPEN|AFTER_OPEN}
* - C: {@link BoundaryFlags.BEFORE_CLOSE|BEFORE_CLOSE}
* - D: {@link BoundaryFlags.AFTER_CLOSE|AFTER_CLOSE}
*
* These are bit flags, so can use bitmasks for filtering. The flags are ordered numerically by
* their DOM position, so you can do comparisons, e.g. `BEFORE_OPEN < AFTER_OPEN`.
*/
class Boundary{
#node;
#side;
/** validate side flag
* @private
*/
static #valid_side(b){
return b == BEFORE_OPEN || b == AFTER_OPEN ||
b == BEFORE_CLOSE || b == AFTER_CLOSE;
}
/** set node and side together
* @private
*/
#set(node, side){
this.#node = node;
this.#side = side;
}
/** Create a new boundary; takes up to three arguments:
* @param args - One of three formats:
* 1. Pass a `Boundary` to copy
* 2. Pass a `Node` and one of {@link BoundaryFlags.BEFORE_OPEN|BEFORE_OPEN}, {@link BoundaryFlags.AFTER_OPEN|AFTER_OPEN},
* {@link BoundaryFlags.BEFORE_CLOSE|BEFORE_CLOSE}, or {@link BoundaryFlags.AFTER_CLOSE|AFTER_CLOSE} flag
* 3. In the manner of the builtin Range interface, pass an anchor `Node`, an offset into that
* anchor, and one of {@link BoundaryFlags.POSITION_BEFORE|POSITION_BEFORE} or {@link BoundaryFlags.POSITION_AFTER|POSITION_AFTER} flag, indicating which side of
* the anchor you wish to get the boundary for. Since the Range interface uses text offsets
* for CharacterData nodes, if the first arg is CharacterData the offset will be ignored,
* instead setting the boundary to be outside. If you want to place the boundary inside a
* CharacterData node, set so directly using syntax #2.
*/
constructor(...args){
this.set(...args);
}
/** Update boundary values. Same arguments as the [constructor]{@link Boundary#Boundary} */
set(...args){
switch (args.length){
case 1:
const o = args[0];
if (!(o instanceof Boundary))
throw TypeError("expected Boundary for first arg");
this.#set(o.#node, o.#side);
break;
case 2: {
const [node, side] = args;
if (!(node instanceof Node || node === null))
throw TypeError("expected Node or null for first arg");
if (!Boundary.#valid_side(side))
throw TypeError("expected a side bit flag for second arg")
this.#set(node, side);
break;
}
case 3: {
let [node, offset, position] = args;
if (!(node instanceof Node))
throw TypeError("expected Node for first arg");
if (!Number.isInteger(offset))
throw TypeError("expected integer for second arg")
if (position != POSITION_BEFORE && position != POSITION_AFTER)
throw TypeError("expected a position bit flag for third arg")
const istxt = node instanceof CharacterData;
if (istxt)
this.#side = position ? AFTER_CLOSE : BEFORE_OPEN;
else{
// left/right side; edges switch to AFTER_OPEN/BEFORE_CLOSE
if (position)
this.#side = offset >= node.childNodes.length ? BEFORE_CLOSE : BEFORE_OPEN;
else this.#side = offset <= 0 ? AFTER_OPEN : AFTER_CLOSE;
// if we are referencing a child node
if (this.#side & FILTER_OUTSIDE)
node = node.childNodes[offset - !position];
}
this.#node = node;
// For text, we first clamp outside, then we clamp again to match the desird `position`;
// no way currently to do an "inclusive" boundary of a CharacterNode using this input syntax
if (istxt)
position ? this.next() : this.previous();
} break;
default:
this.#set(null, BEFORE_OPEN);
break;
}
}
// Property access
/** node whose boundary we reference
* @type {Node}
*/
get node(){ return this.#node; }
set node(node){
if (!(node instanceof Node || node === null))
throw TypeError("node must be a Node or null");
this.#node = node;
}
/** bit flag giving which side of the node our boundary is for; this is one of
* {@link BoundaryFlags}
* @type {Number}
*/
get side(){ return this.#side; }
set side(side){
if (!Boundary.#valid_side(side))
throw TypeError("invalid side bit flag");
this.#side = side;
}
/** Copy this Boundary object
* @returns {Boundary} cloned boundary
*/
clone(){
return new Boundary(this);
}
/** Convert to an anchor, in the manner of the builtin Range/StaticRange interface.
*
* ```js
* const {node, offset} = boundary.toAnchor();
* const range = new Range();
* range.setStart(node, offset);
* ```
*
* @param {boolean} [text=true] The Range interface switches to encoding text offsets for
* CharacterData nodes, instead of encoding a child offset like other node types. We allow a
* boundary inside a CharacterData node though, so these boundaries can't be represented with
* Range.
*
* Set this parameter to `true` to use nearest outside boundary for CharacterData nodes, which
* is what makes more sense for use with Range. Set this to `false` to do no conversion, which
* can be useful if you are not using the anchor with Range.
* @returns {Object} An object with the following members:
* - `node` (`Node`): a reference parent node
* - `offset` (`number`): offset inside the node's childNodes list
*/
toAnchor(text=true){
if (!this.#node)
throw Error("cannot convert null Boundary to anchor");
let node = this.#node, offset = 0;
// calculate offset by finding node's index in parent's child nodes
if (this.#side & FILTER_OUTSIDE || (text && node instanceof CharacterData)){
let child = node;
node = node.parentNode;
// Range offset indexes the previous side (so open boundaries are exclusive)
if (this.#side & FILTER_OPEN)
child = child.previousSibling;
while (child !== null){
child = child.previousSibling
offset++;
}
}
else if (this.#side == BEFORE_CLOSE)
offset = node.childNodes.length;
return {node, offset};
}
/** Compare relative position of two boundaries
* @param {Boundary} other boundary to compare with
* @returns {?number} One of the following:
* - `null` if the boundaries are from different DOM trees or the relative position can't be determined
* - `0` if they are equal (see also [isEqual]{@link Boundary#isEqual} for a faster equality check)
* - `1` if this boundary is after `other`
* - `-1` if this boundary is before `other`
*
* Note, two boundaries that are adjacent, but have differing nodes/boundaries are not
* considered "equal". They have an implicit side to them. Use
* [isAdjacent]{@link Boundary#isAdjacent} to check for this case instead.
*/
compare(other){
if (this.#node && other.#node){
if (this.#node === other.#node)
return Math.sign(this.#side - other.#side);
const p = this.#node.compareDocumentPosition(other.#node);
// handle contained/contains before preceding/following, since they can combine
if (p & Node.DOCUMENT_POSITION_CONTAINED_BY)
return Math.sign(this.#side - POSITION_INSIDE);
if (p & Node.DOCUMENT_POSITION_CONTAINS)
return Math.sign(POSITION_INSIDE - other.#side);
if (p & Node.DOCUMENT_POSITION_PRECEDING)
return 1;
if (p & Node.DOCUMENT_POSITION_FOLLOWING)
return -1;
}
// null boundary, disconnected, or implementation specific
return null;
}
/** See where the boundary sits relative to a Node. This just tells if the boundary is inside,
* before, or after the node. For more detailed comparisons, create a Boundary for `node` to
* compare with instead (see [compare]{@link Boundary#compare}).
* @param {Node} node node to compare with
* @returns {?number} One of the following:
* - `null` if the boundary is null, in a different DOM tree than node, or the relative postiion can't be determined
* - {@link BoundaryFlags.POSITION_BEFORE|POSITION_BEFORE} if the boundary comes before `node` in DOM order
* - {@link BoundaryFlags.POSITION_INSIDE|POSITION_INSIDE} if the boundary is inside `node`
* - {@link BoundaryFlags.POSITION_AFTER|POSITION_AFTER} if the boundary comes after `node` in DOM order
*/
compareNode(node){
if (this.#node){
if (node === this.#node){
if (this.#side & FILTER_INSIDE)
return POSITION_INSIDE;
return this.#side > POSITION_INSIDE ? POSITION_AFTER : POSITION_BEFORE;
}
const p = this.#node.compareDocumentPosition(node);
// handle contained/contains before preceding/following, since they can combine
if (p & Node.DOCUMENT_POSITION_CONTAINED_BY)
return this.#side & FILTER_CLOSE ? POSITION_AFTER : POSITION_BEFORE;
if (p & Node.DOCUMENT_POSITION_CONTAINS)
return POSITION_INSIDE;
if (p & Node.DOCUMENT_POSITION_PRECEDING)
return POSITION_AFTER;
if (p & Node.DOCUMENT_POSITION_FOLLOWING)
return POSITION_BEFORE;
}
// null boundary, disconnected, or implementation specific
return null;
}
/** Check if boundary equals another
* @param {Boundary} other boundary to compare with
* @returns {boolean} true if the boundaries are identical
*/
isEqual(other){
return this.#node === other.#node && this.#side === other.#side;
}
/** Check if this boundary directly precedes another, and is the same DOM insertion point. For
* example, given the following DOM with letters representing boundaries:
*
* ```html
* <main>A B<article>C D<span>E F</span>G H</article>I J</main>
* ```
*
* The pairs (A,B), (C,D), (E,F), (G,H), and (I,J) are considered "adjacent". While they
* represent the same DOM position, they differ in whether they are in reference to the
* preceding or following node. The preceding boundary will always have an "AFTER" side, with
* the adjacent following boundary having a "BEFORE" side (see {@link BoundaryFlags}).
* @param {Boundary} other boundary to compare with
* @returns {boolean} true if `other` is adjacent *and* following `this`
*/
isAdjacent(other){
// before_open <-> after_open are not adjacent since one is outside the node and the other inside
if (!this.#node || !other.#node || this.#side & FILTER_BEFORE || other.#side & FILTER_AFTER)
return false;
return this.clone().next().isEqual(other);
}
/** Check if the boundary node is not set (e.g. null). A null reference node is allowed, and can
* be used to signal the end of DOM traversal or an unset Boundary.
* @returns {boolean} true if boundary is not set
*/
isNull(){ return !this.#node; }
/** Traverses to the nearest boundary point inside the node. For example:
*
* ```html
* A<span>B C</span>D
* ```
*
* A would become B and D would become C.
* @returns {Boundary} modified `this`
*/
inside(){
switch (this.side){
case AFTER_CLOSE:
this.side = BEFORE_CLOSE;
break;
case BEFORE_OPEN:
this.side = AFTER_OPEN;
break;
}
return this;
}
/** Traverses to the nearest boundary point outside the node.
* Performs the inverse of {@link Boundary#inside|inside}
* @see {@link Boundary#inside|inside} for additional details
* @returns {Boundary} modified `this`
*/
outside(){
switch (this.side){
case BEFORE_CLOSE:
this.side = AFTER_CLOSE;
break;
case AFTER_OPEN:
this.side = BEFORE_OPEN;
break;
}
return this;
}
/** Traverses to the next boundary point in the DOM tree. For example:
*
* ```html
* A<span>B C</span>D
* ```
*
* Given a boundary starting at A, traversal would proceed to B, C, D, and finally null
* to signal an end to traversal.
* @returns {Boundary} modified `this`
*/
next(){
if (!this.#node) return;
switch (this.#side){
case AFTER_OPEN:
const c = this.#node.firstChild;
if (c)
this.#set(c, BEFORE_OPEN);
else this.#side = BEFORE_CLOSE;
break;
case AFTER_CLOSE:
const s = this.#node.nextSibling;
if (s)
this.#set(s, BEFORE_OPEN);
else this.#set(this.#node.parentNode, BEFORE_CLOSE);
break;
// before -> after
default:
this.#side >>= 1;
break;
}
return this;
}
/** Traverses to the previous boundary point.
* Performs the inverse of {@link Boundary#next|next}
* @see {@link Boundary#next|next} for additional details
* @returns {Boundary} modified `this`
*/
previous(){
if (!this.#node) return;
switch (this.#side){
case BEFORE_CLOSE:
const c = this.#node.lastChild;
if (c)
this.#set(c, AFTER_CLOSE);
else this.#side = AFTER_OPEN;
break;
case BEFORE_OPEN:
const s = this.#node.previousSibling;
if (s)
this.#set(s, AFTER_CLOSE);
else this.#set(this.#node.parentNode, AFTER_OPEN);
break;
// after -> before
default:
this.#side <<= 1;
break;
}
return this;
}
/** Generator that yields a Boundary for each unique node when traversing in the "next"
* direction. Unlike {@link Boundary#next|next} this method tracks which nodes have been
* visited, and only emits their first boundary encountered. This method is meant to mimic the
* single node traversal of `TreeWalker`, but it yields a node when any of its boundaries is
* crossed. *(Essentially doing a preorder traversal regardless of direction, except when
* traversing an unseen parentNode, which will be postorder).* For example:
*
* ```html
* A <main>B C<article>D E</article>F G</main>H
* ```
*
* Given a boundary starting with...
* - C: yield C, G, null
* - D: yield E, G, null
*
* Note that the yielded {@link Boundary#side|side} will always be {@link BoundaryFlags.BEFORE_OPEN|BEFORE_OPEN}
* or {@link BoundaryFlags.BEFORE_OPEN|BEFORE_CLOSE}. If the current Boundary is one of these types,
* it will be yielded first by default.
*
* @param {boolean} [include_start=true] whether to yield the starting Boundary if it is of type "BEFORE"
* @yields {Boundary} Modified `this`; traversal continues until there is neither sibling or
* parent node. If you need a copy for each iteration, [clone]{@link Boundary#clone} the emitted
* Boundary.
*/
*nextNodes(include_start=true){
// always BEFORE_OPEN or BEFORE_CLOSE; need to convert start bounds to this
const after = this.#side & FILTER_AFTER;
if (after || !include_start){
this.next();
if (!after)
this.next();
}
if (!this.#node) return;
yield this;
let depth = 0, n;
while (true){
// if BEFORE_CLOSE, we've already passed all the children
if (this.#side == BEFORE_OPEN && (n = this.#node.firstChild)){
this.#node = n;
depth++;
yield this;
}
else if (n = this.#node.nextSibling){
this.#set(n, BEFORE_OPEN);
yield this;
}
else if (n = this.#node.parentNode){
this.#set(n, BEFORE_CLOSE);
// while depth non-zero, we've seen this node already
if (!depth)
yield this;
else --depth;
}
else return;
}
}
/** Performs the inverse of {@link Boundary#nextNodes|nextNodes}. Note that when traversing in
* the previous direction, the side will always be one of
* {@link BoundaryFlags.AFTER_OPEN|AFTER_OPEN} or {@link BoundaryFlags.AFTER_CLOSE|AFTER_CLOSE}
* @param {boolean} [include_start=true] whether to yield the starting Boundary if it is of type "AFTER"
* @see {@link Boundary#nextNodes|nextNodes} for additional details
* @yield {Boundary} modified `this`
*/
*previousNodes(include_start=true){
// always AFTER_OPEN or AFTER_CLOSE; need to convert start bounds to this
const before = this.#side & FILTER_BEFORE;
if (before || !include_start){
this.previous();
if (!before)
this.previous();
}
if (!this.#node) return;
yield this;
let depth = 0, n;
while (true){
// if AFTER_OPEN, we've already passed all the children
if (this.#side == AFTER_CLOSE && (n = this.#node.lastChild)){
this.#node = n;
depth++;
yield this;
}
else if (n = this.#node.previousSibling){
this.#set(n, AFTER_CLOSE);
yield this;
}
else if (n = this.#node.parentNode){
this.#set(n, AFTER_OPEN);
// while depth non-zero, we've seen this node already
if (!depth)
yield this;
else --depth;
}
else return;
}
}
/** Insert nodes into the DOM at this boundary position
* @param {Node} nodes the nodes to insert
*/
insert(...nodes){
if (!this.#node)
throw Error("inserting at null Boundary");
switch (this.#side){
case BEFORE_OPEN:
this.#node.before(...nodes);
break;
case AFTER_OPEN:
this.#node.prepend(...nodes);
break;
case BEFORE_CLOSE:
this.#node.append(...nodes);
break;
case AFTER_CLOSE:
this.#node.after(...nodes);
break;
}
}
}
/** Similar to builtin Range or StaticRange interfaces, but encodes the start/end of the range using
* {@link Boundary}. The anchors are not specified as an offset into a parent's children, so the
* range is robust to modifications of the DOM. In particular, you can use this to encode bounds for
* mutations, as DOM changes within the range will not corrupt the range. Conveniently, many
* comparisons and range operations can be performed on the individual start/end anchors via the
* {@link Boundary} class.
*/
class BoundaryRange{
#start;
#end;
/** Create a new range; takes up to two arguments:
* @param {Range|StaticRange|BoundaryRange|Boundary[]} args One of these formats:
* - *empty*: uninitialized range; you should set start/end manually before using the range
* - `Range` or `StaticRange`: converts from a Range, defaulting to an "exclusive" range,
* see {@link BoundaryRange#normalize|normalize}
* - `BoundaryRange`: equivalent to {@link BoundaryRange#cloneRange|cloneRange}
* - `[Boundary, Boundary]`: set the start/end anchors to be a copy of these boundaries
*
* For more control over initialization, leave args empty and use
* {@link BoundaryRange#setStart|setStart} and {@link BoundaryRange#setEnd|setEnd} instead. You
* may also directly manipulate or assign {@link BoundaryRange#start|start} or {@link BoundaryRange#start|end}
* if desired.
*/
constructor(...args){
this.#start = new Boundary();
this.#end = new Boundary();
// optional init
switch (args.length){
case 1:
const o = args[0];
if (o instanceof BoundaryRange){
this.#start.set(o.start);
this.#end.set(o.end);
}
// Range/StaticRange
else{
this.#start.set(o.startContainer, o.startOffset, POSITION_BEFORE);
this.#end.set(o.endContainer, o.endOffset, POSITION_AFTER);
}
break;
case 2:
const [s, e] = args;
this.#start.set(s);
this.#end.set(e);
break;
}
}
/** Starting anchor of the range. You can access or assign this directly as needed
* @type {Boundary}
*/
get start(){ return this.#start; }
set start(b){
if (!(b instanceof Boundary))
throw Error("expected Boundary for start");
}
/** Update {@link BoundaryRange#start|start} anchor; equivalent to `this.start.set()`
* @param args forwarded to {@link Boundary#set}
* @see {@link Boundary#set} for arguments
* @returns {BoundaryRange} modified `this`
*/
setStart(...args){
this.#start.set(...args);
return this;
}
/** Ending anchor of the range. You can access or assign this directly as needed
* @type {Boundary}
*/
get end(){ return this.#end; }
set end(b){
if (!(b instanceof Boundary))
throw Error("expected Boundary for end");
}
/** Update {@link BoundaryRange#end|end} anchor; equivalent to `this.end.set()`
* @param args forwarded to {@link Boundary#set}
* @see {@link Boundary#set} for arguments
* @returns {BoundaryRange} modified `this`
*/
setEnd(...args){
this.#end.set(...args);
return this;
}
/** Make a copy of this range object
* @returns {BoundaryRange} cloned range
*/
cloneRange(){
return new BoundaryRange(this);
}
/** Convert to `Range` interface. Range's end is set last, so if the resulting range's
* anchors would be out of order, it would get collapsed to the end anchor. Boundaries inside
* a CharacterData node are treated as outside for conversion purposes. If the current BoundaryRange
* {@link BoundaryRange#isNull|isNull}, an uninitialized Range will be returned.
* @returns {Range}
*/
toRange(){
const r = new Range();
if (this.isNull())
throw Error("cannot create Range from null BoundaryRange")
// start anchor
const sn = this.#start.node;
let sb = this.#start.side;
if (sn instanceof CharacterData)
sb = sb == AFTER_OPEN ? BEFORE_OPEN : AFTER_CLOSE;
switch (sb){
case BEFORE_OPEN:
r.setStartBefore(sn);
break;
case AFTER_OPEN:
r.setStart(sn, 0);
break;
case BEFORE_CLOSE:
r.setStart(sn, sn.childNodes.length);
break;
case AFTER_CLOSE:
r.setStartAfter(sn);
break;
}
// end anchor
const en = this.#end.node;
let eb = this.#end.side;
if (en instanceof CharacterData)
eb = eb == AFTER_OPEN ? BEFORE_OPEN : AFTER_CLOSE;
switch (eb){
case BEFORE_OPEN:
r.setEndBefore(en);
break;
case AFTER_OPEN:
r.setEnd(en, 0);
break;
case BEFORE_CLOSE:
r.setEnd(en, en.childNodes.length);
break;
case AFTER_CLOSE:
r.setEndAfter(en);
break;
}
return r;
}
/** Convert to `StaticRange` interface. Boundaries inside a CharacterData node are treated as
* outside for conversion purposes. If the current BoundaryRange
* {@link BoundaryRange#isNull|isNull}, an error will be thrown since a `StaticRange` cannot be
* created uninitialized.
* @returns {StaticRange}
*/
toStaticRange(){
if (this.isNull())
throw Error("cannot create StaticRange from null BoundaryRange")
// Range may have side effects from being unordered, so can't reuse toRange for this
const sa = this.#start.toAnchor();
const ea = this.#end.toAnchor();
return new StaticRange({
startContainer: sa.node,
startOffset: sa.offset,
endContainer: ea.node,
endOffset: ea.offset
});
}
/** Check if the range has been fully set, e.g. neither boundary is null
* @see {@link Boundary#isNull}
* @returns {boolean} true if range is not set, or is only partially set
*/
isNull(){
return this.#start.isNull() || this.#end.isNull();
}
/** Check if range exactly matches another
* @param {BoundaryRange} other range to compare with
*/
isEqual(other){
return this.#start.isEqual(other.start) && this.#end.isEqual(other.end);
}
/** Check if the range is collapsed in the current DOM. The start/end boundaries must be equal,
* or start/end must be adjacent to eachother (see {@link Boundary#isEqual} and
* {@link Boundary#isAdjacent}). If the start/end anchors are disconnected or out-of-order, it
* returns false.
* @type {boolean}
*/
get collapsed(){
return this.#start.isEqual(this.#end) || this.#start.isAdjacent(this.#end);
}
/** Collapse the range to one of the boundary points. After calling this method, the start
* anchor will equal the end: `this.start.isEqual(this.end)` (see {@link Boundary#isEqual}). If
* you would like to instead collapse with the start/end anchors *adjacent* (see
* {@link Boundary#isAdjacent}), then follow with a call to
* {@link BoundaryRange#normalize|normalize}.
* @param {boolean} [toStart=false] If true, collapses to the {@link BoundaryRange#start|start};
* otherwise collapses to {@link BoundaryRange#end|end}
* @returns {BoundaryRange} modified `this`
*/
collapse(toStart=false){
if (toStart)
this.#end = this.#start.clone();
else this.#start = this.#end.clone();
return this;
}
/** Extend this range to include the bounds of another BoundaryRange. If the start/end has
* not been set yet, it will simply copy from `other`. Example:
*
* ```html
* <div id='a'></div> <div id='b'></div>
* ```
* ```js
* const ra = (new BoundaryRange()).selectNode(a);
* const rb = (new BoundaryRange()).selectNodeContents(b)
* ra.extend(rb);
* // ra.start == (a, BEFORE_OPEN)
* // ra.end == (b, BEFORE_CLOSE)
* ```
*
* @param {BoundaryRange} other extend bounds to enclose this range
* @returns {BoundaryRange} modified `this`
*/
extend(other){
if (this.#start.isNull())
this.#start = other.start.clone();
else if (this.#start.compare(other.start) == 1)
this.#start.set(other.start);
if (this.#end.isNull())
this.#end = other.end.clone();
else if (this.#end.compare(other.end) == -1)
this.#end.set(other.end);
return this;
}
/** Set range to surround a single node
* @param {Node} node the node to surround
* @param {boolean} [exclusive=true] see {@link BoundaryRange#normalize|normalize}
* @returns {BoundaryRange} modified `this`
*/
selectNode(node, exclusive=false){
this.#start.set(node, BEFORE_OPEN);
this.#end.set(node, AFTER_CLOSE);
if (exclusive){
this.#start.previous();
this.#end.next();
}
return this;
}
/** Set range to surround the contents of a node. Warning, for CharacterData nodes, you probably
* want to use {@link Boundary#selectNode|selectNode} instead, since these nodes cannot have
* children
* @param {Node} node node whose contents to enclose
* @param {boolean} [exclusive=true] see {@link BoundaryRange#normalize|normalize}
* @returns {BoundaryRange} modified `this`
*/
selectNodeContents(node, exclusive=true){
this.#start.set(node, AFTER_OPEN);
this.#end.set(node, BEFORE_CLOSE);
if (!exclusive){
this.#start.next();
this.#end.previous();
}
return this;
}
/** Every boundary has one adjacent boundary at the same position. On one side you have the
* {@link BoundaryFlags.AFTER_OPEN|AFTER_OPEN}/{@link BoundaryFlags.AFTER_CLOSE|AFTER_CLOSE}
* bounds, and following it will be a
* {@link BoundaryFlags.BEFORE_OPEN|BEFORE_OPEN}/{@link BoundaryFlags.BEFORE_CLOSE|BEFORE_CLOSE}
* bounds. See {@link Boundary#isAdjacent}. The start/end anchors can use either boundary and the
* range positions will be equivalent; the main difference is the behavior when the DOM is mutated,
* as the reference nodes will be different. There are two normalization modes:
*
* 1. **exclusive**: start/end anchor boundaries are outside the range; e.g. start boundary is
* AFTER and end boundary is BEFORE type
* 2. **inclusive**: start/end anchor boundaries are inside the range; e.g. start boundary is
* BEFORE and end boundary is AFTER type
*
* For example, if you are encoding a range of mutations, you might want to normalize the range
* to be exclusive; that way, the mutated nodes inside the range will not affect the boundaries.
* @param {boolean} [exclusive=true] true for exclusive bounds, or false for inclusive
* @returns {BoundaryRange} modified `this`
*/
normalize(exclusive=true){
if (exclusive){
if (this.#start.side & FILTER_BEFORE)
this.#start.previous();
if (this.#end.side & FILTER_AFTER)
this.#end.next();
}
else{
if (this.#start.side & FILTER_AFTER)
this.#start.next();
if (this.#end.side & FILTER_BEFORE)
this.#end.previous();
}
return this;
}
// Comparison helper methods
/** Check if this range intersects with another
* @param {BoundaryRange} other the range to compare with
* @param {boolean} [inclusive=false] whether to consider the ranges intersecting if just
* one of their start/end anchors are equal
* @returns {boolean} true if the ranges intersect
*/
intersects(other, inclusive=false){
return (
this.start.compare(other.end) <= (inclusive-1) &&
this.end.compare(other.start) >= (1-inclusive)
);
}
/** Check if this range fully contains `other`
* @param {BoundaryRange} other the range to compare with
* @param {boolean} [inclusive=true] whether to consider the range fully contained if one of
* its start/end anchors equals that of `this`
* @returns {boolean} true if `other` is contained
*/
contains(other, inclusive=true){
return (
this.start.compare(other.start) <= (inclusive-1) &&
this.end.compare(other.end) >= (1-inclusive)
);
}
}
// could maybe rename it to NodeBoundaryXXX
const Flags_readonly = Object.freeze(Flags);
export { Flags_readonly as BoundaryFlags, Boundary, BoundaryRange };