Commit 7d196444 authored by Robert Knight's avatar Robert Knight

Optimize and fix a bug in `isNodeInRange` if node had no parent

`isNodeInRange` in range would throw an exception if the passed node had
no parent because `range.selectNode(...)` requires its argument to have
a parent.

This commit rewrites `isNodeInRange` to use `range.comparePoint` which
avoids creating a temporary live range and handle exceptions that
`comparePoint` may throw. It also updates the JSDoc to more accurately
describe what the function does.
parent 6a162268
...@@ -14,41 +14,36 @@ export function isSelectionBackwards(selection) { ...@@ -14,41 +14,36 @@ export function isSelectionBackwards(selection) {
} }
/** /**
* Returns true if `node` lies within a range. * Returns true if any part of `node` lies within `range`.
*
* This is a simplified version of `Range.isPointInRange()` for compatibility
* with IE.
* *
* @param {Range} range * @param {Range} range
* @param {Node} node * @param {Node} node
*/ */
export function isNodeInRange(range, node) { export function isNodeInRange(range, node) {
if (node === range.startContainer || node === range.endContainer) { try {
return true; const length = node.nodeValue?.length ?? node.childNodes.length;
return (
// Check start of node is before end of range.
range.comparePoint(node, 0) <= 0 &&
// Check end of node is after start of range.
range.comparePoint(node, length) >= 0
);
} catch (e) {
// `comparePoint` may fail if the `range` and `node` do not share a common
// ancestor or `node` is a doctype.
return false;
} }
const nodeRange = /** @type {Document} */ (node.ownerDocument).createRange();
nodeRange.selectNode(node);
const isAtOrBeforeStart =
range.compareBoundaryPoints(Range.START_TO_START, nodeRange) <= 0;
const isAtOrAfterEnd =
range.compareBoundaryPoints(Range.END_TO_END, nodeRange) >= 0;
nodeRange.detach();
return isAtOrBeforeStart && isAtOrAfterEnd;
} }
/** /**
* Iterate over all Node(s) in `range` in document order and invoke `callback` * Iterate over all Node(s) which overlap `range` in document order and invoke
* for each of them. * `callback` for each of them.
* *
* @param {Range} range * @param {Range} range
* @param {(n: Node) => any} callback * @param {(n: Node) => any} callback
*/ */
function forEachNodeInRange(range, callback) { function forEachNodeInRange(range, callback) {
const root = range.commonAncestorContainer; const root = range.commonAncestorContainer;
// The `whatToShow`, `filter` and `expandEntityReferences` arguments are
// mandatory in IE although optional according to the spec.
const nodeIter = /** @type {Document} */ (root.ownerDocument).createNodeIterator( const nodeIter = /** @type {Document} */ (root.ownerDocument).createNodeIterator(
root, root,
NodeFilter.SHOW_ALL NodeFilter.SHOW_ALL
...@@ -56,7 +51,6 @@ function forEachNodeInRange(range, callback) { ...@@ -56,7 +51,6 @@ function forEachNodeInRange(range, callback) {
let currentNode; let currentNode;
while ((currentNode = nodeIter.nextNode())) { while ((currentNode = nodeIter.nextNode())) {
// eslint-disable-line no-cond-assign
if (isNodeInRange(range, currentNode)) { if (isNodeInRange(range, currentNode)) {
callback(currentNode); callback(currentNode);
} }
......
...@@ -44,26 +44,39 @@ describe('annotator.range-util', function () { ...@@ -44,26 +44,39 @@ describe('annotator.range-util', function () {
selection.addRange(range); selection.addRange(range);
} }
describe('#isNodeInRange', function () { describe('#isNodeInRange', () => {
it('is true for a node in the range', function () { it('returns true for a node in the range', () => {
const rng = createRange(testNode, 0, 1); const range = createRange(testNode, 0, 1);
assert.equal(rangeUtil.isNodeInRange(rng, testNode.firstChild), true); assert.isTrue(rangeUtil.isNodeInRange(range, testNode.firstChild));
}); });
it('is false for a node before the range', function () { it('returns false for a node before the range', () => {
testNode.innerHTML = 'one <b>two</b> three'; testNode.innerHTML = 'one <b>two</b> three';
const rng = createRange(testNode, 1, 2); const range = createRange(testNode, 1, 2);
assert.equal(rangeUtil.isNodeInRange(rng, testNode.firstChild), false); assert.isFalse(rangeUtil.isNodeInRange(range, testNode.firstChild));
}); });
it('is false for a node after the range', function () { it('returns false for a node after the range', () => {
testNode.innerHTML = 'one <b>two</b> three'; testNode.innerHTML = 'one <b>two</b> three';
const rng = createRange(testNode, 1, 2); const range = createRange(testNode, 1, 2);
assert.equal( assert.isFalse(
rangeUtil.isNodeInRange(rng, testNode.childNodes.item(2)), rangeUtil.isNodeInRange(range, testNode.childNodes.item(2))
false
); );
}); });
it('can test a node with no parent', () => {
const node = document.createElement('span');
const range = new Range();
range.setStart(node, 0);
range.setEnd(node, 0);
assert.isTrue(rangeUtil.isNodeInRange(range, node));
});
it('can test a node against an empty range', () => {
const node = document.createElement('span');
const range = new Range();
assert.isFalse(rangeUtil.isNodeInRange(range, node));
});
}); });
describe('#getTextBoundingBoxes', function () { describe('#getTextBoundingBoxes', function () {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment