Commit e0592c09 authored by Robert Knight's avatar Robert Knight

Add `TextPosition.relativeTo` method

Add a method that converts a text position within an element to one
where the offset is relative to some ancestor element.

This operation will be needed for replacing existing Range => (element,
start, end) conversions with a single one based on `TextRange` and
`TextPosition`.
parent 9fe2c104
......@@ -88,6 +88,38 @@ describe('annotator/anchoring/text-range', () => {
});
});
describe('#relativeTo', () => {
it("throws an error if argument is not an ancestor of position's element", () => {
const el = document.createElement('div');
el.append('One');
const pos = TextPosition.fromPoint(el.firstChild, 0);
assert.throws(() => {
pos.relativeTo(document.body);
}, 'Parent is not an ancestor of current element');
});
it('returns a TextPosition with offset relative to the given parent', () => {
const grandparent = document.createElement('div');
const parent = document.createElement('div');
const child = document.createElement('span');
grandparent.append('a', parent);
parent.append('bc', child);
child.append('def');
const childPos = TextPosition.fromPoint(child.firstChild, 3);
const parentPos = childPos.relativeTo(parent);
assert.equal(parentPos.element, parent);
assert.equal(parentPos.offset, 5);
const grandparentPos = childPos.relativeTo(grandparent);
assert.equal(grandparentPos.element, grandparent);
assert.equal(grandparentPos.offset, 6);
});
});
describe('fromPoint', () => {
it('returns TextPosition for offset in Text node', () => {
const el = document.createElement('div');
......@@ -155,13 +187,27 @@ describe('annotator/anchoring/text-range', () => {
assert.equal(range.toString(), 'two');
});
it('throws if start or end points cannot be resolved', () => {
it('throws if start point cannot be resolved', () => {
const el = document.createElement('div');
el.textContent = 'one two three';
const textRange = new TextRange(
new TextPosition(el, 4),
new TextPosition(el, 20)
new TextPosition(el, 100),
new TextPosition(el, 5)
);
assert.throws(() => {
textRange.toRange();
}, 'Offset exceeds text length');
});
it('throws if end point cannot be resolved', () => {
const el = document.createElement('div');
el.textContent = 'one two three';
const textRange = new TextRange(
new TextPosition(el, 5),
new TextPosition(el, 100)
);
assert.throws(() => {
......
......@@ -32,12 +32,35 @@ export class TextPosition {
throw new Error('Offset is invalid');
}
/** Element that `offset` is relative to. */
this.element = element;
/** Character offset from the start of the element's `textContent`. */
this.offset = offset;
}
/**
* Return a copy of this position with offset relative to a given ancestor
* element.
*
* @param {Element} parent - Ancestor of `this.element`
* @return {TextPosition}
*/
relativeTo(parent) {
if (!parent.contains(this.element)) {
throw new Error('Parent is not an ancestor of current element');
}
let el = this.element;
let offset = this.offset;
while (el !== parent) {
offset += previousSiblingsTextLength(el);
el = /** @type {Element} */ (el.parentElement);
}
return new TextPosition(el, offset);
}
/**
* Resolve the position to a specific text node and offset within that node.
*
......@@ -141,6 +164,10 @@ export class TextRange {
/**
* Resolve the `TextRange` to a DOM range.
*
* The resulting DOM Range will always start and end in a `Text` node.
* Hence `TextRange.fromRange(range).toRange()` can be used to "shrink" a
* range to the text it contains.
*
* May throw if the `start` or `end` positions cannot be resolved to a range.
*
* @return {Range}
......
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