diff --git a/packages/text-annotator/src/utils/index.ts b/packages/text-annotator/src/utils/index.ts index 5d536dd2..16f629b0 100644 --- a/packages/text-annotator/src/utils/index.ts +++ b/packages/text-annotator/src/utils/index.ts @@ -1,5 +1,4 @@ export * from './cancelSingleClickEvents'; -export * from './cloneEvents'; export * from './device'; export * from './programmaticallyFocusable'; export * from './debounce'; @@ -14,4 +13,5 @@ export * from './reviveSelector'; export * from './reviveTarget'; export * from './splitAnnotatableRanges'; export * from './trimRangeToContainer'; - +export * from './cloneEvents'; +export * from './rangeContains'; diff --git a/packages/text-annotator/src/utils/rangeContains.ts b/packages/text-annotator/src/utils/rangeContains.ts new file mode 100644 index 00000000..f0ebec47 --- /dev/null +++ b/packages/text-annotator/src/utils/rangeContains.ts @@ -0,0 +1,24 @@ +/** + * Need to manually iterate over the cloned node's children + * to check if the target node is contained within. + * Unfortunately, we cannot use `.contains` method, + * because the cloned node is detached from the DOM. + */ +const clonedNodeContains = (clonedNode: Node, targetNode: Node) => { + if (clonedNode.isEqualNode(targetNode)) { + return true; + } + + for (let child of clonedNode.childNodes) { + if (clonedNodeContains(child, targetNode)) { + return true; + } + } + + return false; +}; + +export const rangeContains = (range: Range, node: T) => { + const rangeContents = range.cloneContents(); + return clonedNodeContains(rangeContents, node); +}; diff --git a/packages/text-annotator/src/utils/trimRangeToContainer.ts b/packages/text-annotator/src/utils/trimRangeToContainer.ts index 8a419a4b..94a043ff 100644 --- a/packages/text-annotator/src/utils/trimRangeToContainer.ts +++ b/packages/text-annotator/src/utils/trimRangeToContainer.ts @@ -1,16 +1,40 @@ +import { rangeContains } from './rangeContains'; + export const trimRangeToContainer = ( range: Range, container: HTMLElement ): Range => { const trimmedRange = range.cloneRange(); - // If the start is outside the container - set it to the start of the container - if (!container.contains(trimmedRange.startContainer)) { + const containsRangeStart = container.contains(trimmedRange.startContainer); + const containsRangeEnd = container.contains(trimmedRange.endContainer); + + /** + * If both range's edges are not within the container (i.e. the selection is done outside) + * and the range doesn't cover the container itself (i.e. "Select All" was pressed) -> + * collapse it as irrelevant + */ + if (!containsRangeStart && !containsRangeEnd) { + const containedWithinRange = rangeContains(trimmedRange, container); + if (!containedWithinRange) { + trimmedRange.collapse(); + return trimmedRange; + } + } + + /** + * If the range starts outside the container - + * trim it to the start of the container + */ + if (!containsRangeStart) { trimmedRange.setStart(container, 0); } - // If the end is outside the container - set it to the end of the container - if (!container.contains(trimmedRange.endContainer)) { + /** + * If the range ends outside the container - + * trim it to the end of the container + */ + if (!containsRangeEnd) { trimmedRange.setEnd(container, container.childNodes.length); }