Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

⚠️ Fixed processing of the selection range made totally outside of the container ⚠️ #150

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
4 changes: 2 additions & 2 deletions packages/text-annotator/src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
export * from './cancelSingleClickEvents';
export * from './cloneEvents';
export * from './device';
export * from './programmaticallyFocusable';
export * from './debounce';
Expand All @@ -14,4 +13,5 @@ export * from './reviveSelector';
export * from './reviveTarget';
export * from './splitAnnotatableRanges';
export * from './trimRangeToContainer';

export * from './cloneEvents';
export * from './rangeContains';
24 changes: 24 additions & 0 deletions packages/text-annotator/src/utils/rangeContains.ts
Original file line number Diff line number Diff line change
@@ -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 = <T extends Node>(range: Range, node: T) => {
const rangeContents = range.cloneContents();
return clonedNodeContains(rangeContents, node);
};
32 changes: 28 additions & 4 deletions packages/text-annotator/src/utils/trimRangeToContainer.ts
Original file line number Diff line number Diff line change
@@ -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);
}

Expand Down