Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
a1b3bf2
Added rects normalization utils
oleksandr-danylchenko Jul 24, 2024
405dfee
Added `getAnnotationRects` exposure from the store
oleksandr-danylchenko Jul 24, 2024
f631f5f
Added popup positioning using the spatial tree bounds
oleksandr-danylchenko Jul 24, 2024
b59bed6
Added conversion to dom rect list
oleksandr-danylchenko Jul 25, 2024
5eaef4e
Added repositioning on target update
oleksandr-danylchenko Aug 27, 2024
e4a5620
Added reference removal upon closing the popup
oleksandr-danylchenko Aug 27, 2024
eae94ed
Added reference cleanup when the annotation is removed
oleksandr-danylchenko Aug 27, 2024
bbfd82d
Merge branch 'main' into spatial-tree-positioning
oleksandr-danylchenko Aug 30, 2024
3fdc61a
Merge branch 'main' into spatial-tree-positioning
oleksandr-danylchenko Sep 2, 2024
9650d3c
Deduped re-exports
oleksandr-danylchenko Sep 2, 2024
8071b73
Merge branch 'fix-testing-component-popup-rendering' into spatial-tre…
oleksandr-danylchenko Sep 2, 2024
34dd7b3
Merge branch 'main' into spatial-tree-positioning
oleksandr-danylchenko Sep 3, 2024
f811c9b
Merge branch 'main' into spatial-tree-positioning
oleksandr-danylchenko Sep 10, 2024
565bb6d
Aligned `getIntersecting` type
oleksandr-danylchenko Sep 10, 2024
d924f42
Merge branch 'main' into spatial-tree-positioning
oleksandr-danylchenko Sep 26, 2024
5d5218d
Fixed array iterator type
oleksandr-danylchenko Sep 26, 2024
fb76657
Fixed renamed generic use
oleksandr-danylchenko Sep 26, 2024
a004ee4
Merge branch 'main' into spatial-tree-positioning
oleksandr-danylchenko Sep 30, 2024
c6d36b6
Merged `main` into `spatial-tree-positioning`
oleksandr-danylchenko Sep 30, 2024
8a051e3
Merge branch 'main' into spatial-tree-positioning
oleksandr-danylchenko Oct 7, 2024
1515f0b
Merge branch 'main' into spatial-tree-positioning
oleksandr-danylchenko Oct 7, 2024
a5cef01
Fixed imports order
oleksandr-danylchenko Oct 7, 2024
9aa433f
Merge branch 'main' into spatial-tree-positioning
oleksandr-danylchenko Oct 29, 2024
182f6cb
Added `undefined` bounds support
oleksandr-danylchenko Oct 29, 2024
0752c62
Added empty index handling
oleksandr-danylchenko Oct 30, 2024
81c76c2
Merge branch 'main' into spatial-tree-positioning
oleksandr-danylchenko Nov 4, 2024
e427223
Merge branch 'main' into spatial-tree-positioning
oleksandr-danylchenko Nov 11, 2024
2ee2f96
Added opening on the bound's presence
oleksandr-danylchenko Nov 18, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import { ReactNode, useEffect, useMemo, useRef, useState } from 'react';
import { useAnnotator, useSelection } from '@annotorious/react';
import { isRevived, NOT_ANNOTATABLE_CLASS, TextAnnotation, TextAnnotator } from '@recogito/text-annotator';
import {
NOT_ANNOTATABLE_CLASS,
denormalizeRectWithOffset,
toDomRectList,
type TextAnnotation,
type TextAnnotator,
} from '@recogito/text-annotator';

import { isMobile } from './isMobile';
import {
arrow,
Expand Down Expand Up @@ -64,8 +71,8 @@ export const TextAnnotationPopup = (props: TextAnnotationPopupProps) => {
shift({ crossAxis: true, padding: 10 })
];

return props.arrow
? [...m, arrow({ element: arrowRef }) ]
return props.arrow
? [...m, arrow({ element: arrowRef }) ]
: m;
}, [props.arrow]);

Expand All @@ -89,26 +96,37 @@ export const TextAnnotationPopup = (props: TextAnnotationPopupProps) => {
const { getFloatingProps } = useInteractions([dismiss, role]);

useEffect(() => {
const annotationSelector = annotation?.target.selector;
setOpen(annotationSelector?.length > 0 ? isRevived(annotationSelector) : false);
}, [annotation]);
if (annotation?.id) {
const bounds = r?.state.store.getAnnotationBounds(annotation.id);
setOpen(Boolean(bounds));
} else {
setOpen(false);
}
}, [annotation?.id, r?.state.store]);

useEffect(() => {
if (isOpen && annotation) {
const {
target: {
selector: [{ range }]
}
} = annotation;
if (!r) return;

if (isOpen && annotation?.id) {
refs.setPositionReference({
getBoundingClientRect: () => range.getBoundingClientRect(),
getClientRects: () => range.getClientRects()
getBoundingClientRect: () => {
const bounds = r.state.store.getAnnotationBounds(annotation.id);
return bounds
? denormalizeRectWithOffset(bounds, r.element.getBoundingClientRect())
: new DOMRect();
},
getClientRects: () => {
const rects = r.state.store.getAnnotationRects(annotation.id);
const denormalizedRects = rects.map((rect) =>
denormalizeRectWithOffset(rect, r.element.getBoundingClientRect())
);
return toDomRectList(denormalizedRects);
}
});
} else {
refs.setPositionReference(null);
}
}, [isOpen, annotation, refs]);
}, [isOpen, annotation?.id, annotation?.target, r]);

useEffect(() => {
const config: MutationObserverInit = { attributes: true, childList: true, subtree: true };
Expand Down Expand Up @@ -151,9 +169,9 @@ export const TextAnnotationPopup = (props: TextAnnotationPopupProps) => {
})}

{props.arrow && (
<FloatingArrow
<FloatingArrow
ref={arrowRef}
context={context}
context={context}
{...(props.arrowProps || {})} />
)}

Expand Down
4 changes: 3 additions & 1 deletion packages/text-annotator/src/state/TextAnnotationStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ export interface TextAnnotationStore<T extends TextAnnotation = TextAnnotation>

bulkUpsertAnnotations(annotations: T[], origin?: Origin): T[];

getAnnotationBounds(id: string, hintX?: number, hintY?: number, buffer?: number): DOMRect;
getAnnotationRects(id: string): DOMRect[];

getAnnotationBounds(id: string, hintX?: number, hintY?: number, buffer?: number): DOMRect | undefined;

getAnnotationRects(id: string): DOMRect[];

Expand Down
13 changes: 10 additions & 3 deletions packages/text-annotator/src/state/TextAnnotatorState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import type {
} from '@annotorious/core';
import { createSpatialTree } from './spatialTree';
import type { TextAnnotation, TextAnnotationTarget } from '../model';
import type { TextAnnotationStore } from './TextAnnotationStore';
import type { AnnotationRects, TextAnnotationStore } from './TextAnnotationStore';
import { isRevived, reviveAnnotation, reviveTarget } from '../utils';

export interface TextAnnotatorState<I extends TextAnnotation = TextAnnotation, E extends unknown = TextAnnotation> extends AnnotatorState<I, E> {
Expand Down Expand Up @@ -114,7 +114,7 @@ export const createTextAnnotatorState = <I extends TextAnnotation = TextAnnotati
return all ? filtered : filtered[0];
}

const getAnnotationBounds = (id: string, x?: number, y?: number, buffer = 5): DOMRect => {
const getAnnotationBounds = (id: string, x?: number, y?: number, buffer = 5): DOMRect | undefined => {
const rects = tree.getAnnotationRects(id);
if (rects.length === 0) return;

Expand All @@ -129,6 +129,13 @@ export const createTextAnnotatorState = <I extends TextAnnotation = TextAnnotati
return tree.getAnnotationBounds(id);
}

const getIntersecting = (
minX: number,
minY: number,
maxX: number,
maxY: number,
): AnnotationRects<I>[] => tree.getIntersecting(minX, minY, maxX, maxY);

const getAnnotationRects = (id: string): DOMRect[] => tree.getAnnotationRects(id);

const recalculatePositions = () => tree.recalculate();
Expand Down Expand Up @@ -157,8 +164,8 @@ export const createTextAnnotatorState = <I extends TextAnnotation = TextAnnotati
bulkUpsertAnnotations,
getAnnotationBounds,
getAnnotationRects,
getIntersecting,
getAt,
getIntersecting: tree.getIntersecting,
recalculatePositions,
updateTarget
},
Expand Down
20 changes: 12 additions & 8 deletions packages/text-annotator/src/state/spatialTree.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import RBush from 'rbush';
import type { Store } from '@annotorious/core';
import type { TextAnnotation, TextAnnotationTarget } from '../model';
import { isRevived, mergeClientRects } from '../utils';
import { reviveSelector } from '../utils';
import {
isRevived,
reviveSelector,
mergeClientRects,
normalizeRectWithOffset
} from '../utils';
import type { AnnotationRects } from './TextAnnotationStore';

interface IndexedHighlightRect {
Expand Down Expand Up @@ -38,11 +42,11 @@ export const createSpatialTree = <T extends TextAnnotation>(store: Store<T>, con
return Array.from(revivedRange.getClientRects());
});

const merged = mergeClientRects(rects)
// Offset the merged client rects so that coords
// are relative to the parent container
.map(({ left, top, right, bottom }) =>
new DOMRect(left - offset.left, top - offset.top, right - left, bottom - top));
/**
* Offset the merged client rects so that coords
* are relative to the parent container
*/
const merged = mergeClientRects(rects).map(rect => normalizeRectWithOffset(rect, offset));

return merged.map(rect => {
const { x, y, width, height } = rect;
Expand Down Expand Up @@ -145,7 +149,7 @@ export const createSpatialTree = <T extends TextAnnotation>(store: Store<T>, con

const getAnnotationRects = (id: string): DOMRect[] => {
const indexed = index.get(id);
if (indexed) {
if (indexed[0]) {
// Reminder: *each* IndexedHighlightRect stores *all*
// DOMRects for the annotation for convenience
return indexed[0].annotation.rects;
Expand Down
3 changes: 2 additions & 1 deletion packages/text-annotator/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,6 @@ export * from './reviveSelector';
export * from './reviveTarget';
export * from './splitAnnotatableRanges';
export * from './trimRangeToContainer';

export * from './cloneEvents';
export * from './normalizeRects';

15 changes: 12 additions & 3 deletions packages/text-annotator/src/utils/mergeClientRects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ const union = (a: DOMRect, b: DOMRect): DOMRect => {
return new DOMRect(left, top, right - left, bottom - top);
}

export const mergeClientRects = (rects: DOMRect[]) => rects.reduce((merged, rectA) => {
export const mergeClientRects = (rects: DOMRect[]) => rects.reduce<DOMRect[]>((merged, rectA) => {
// Some browser report empty rects - discard
if (rectA.width === 0 || rectA.height === 0)
return merged;
Expand Down Expand Up @@ -102,7 +102,16 @@ export const mergeClientRects = (rects: DOMRect[]) => rects.reduce((merged, rect
}

return wasMerged ? next : [ ...next, rectA ];
}, [] as DOMRect[]);
}, []);

export const toDomRectList = (rects: DOMRect[]): DOMRectList => ({
length: rects.length,
item: (index) => rects[index],
[Symbol.iterator]: function* (): ArrayIterator<DOMRect> {
for (let i = 0; i < this.length; i++)
yield this.item(i)!;
}
})

/* Pixels that rects can be apart vertically while still
// being considered to be on the same line.
Expand Down Expand Up @@ -142,7 +151,7 @@ export const mergeClientRects = (rects: DOMRect[]) => {
}).filter(r => r.height > 0 && r.width > 0);

// Checks if the given rect contains any other rects
const containsOthers = (rect: DOMRect) => mergedRects.some(other =>
const containsOthers = (rect: DOMRect) => mergedRects.some(other =>
other !== rect &&
other.left >= rect.left &&
other.right <= rect.right &&
Expand Down
9 changes: 9 additions & 0 deletions packages/text-annotator/src/utils/normalizeRects.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export const normalizeRectWithOffset = (rect: DOMRect, offset: DOMRect): DOMRect => {
const { left, top, right, bottom } = rect;
return new DOMRect(left - offset.left, top - offset.top, right - left, bottom - top);
};

export const denormalizeRectWithOffset = (rect: DOMRect, offset: DOMRect): DOMRect => {
const { left, top, right, bottom } = rect;
return new DOMRect(left + offset.left, top + offset.top, right - left, bottom - top);
}