diff --git a/packages/text-annotator-react/src/TextAnnotatorPopup/TextAnnotatorPopup.tsx b/packages/text-annotator-react/src/TextAnnotatorPopup/TextAnnotatorPopup.tsx index 3a3272e0..4b04d1c4 100644 --- a/packages/text-annotator-react/src/TextAnnotatorPopup/TextAnnotatorPopup.tsx +++ b/packages/text-annotator-react/src/TextAnnotatorPopup/TextAnnotatorPopup.tsx @@ -1,6 +1,5 @@ import { PointerEvent, ReactNode, useCallback, useEffect, useMemo, useState } from 'react'; -import { useAnnotator, useSelection } from '@annotorious/react'; -import type { TextAnnotation, TextAnnotator } from '@recogito/text-annotator'; + import { isMobile } from './isMobile'; import { autoUpdate, @@ -16,6 +15,14 @@ import { useRole } from '@floating-ui/react'; +import { useAnnotator, useSelection } from '@annotorious/react'; +import { + denormalizeRectWithOffset, + toDomRectList, + type TextAnnotation, + type TextAnnotator, +} from '@recogito/text-annotator'; + import './TextAnnotatorPopup.css'; interface TextAnnotationPopupProps { @@ -73,31 +80,36 @@ export const TextAnnotatorPopup = (props: TextAnnotationPopupProps) => { useEffect(() => { setOpen( // Selected annotation exists and has a selector? - annotation?.target.selector && + annotation?.target.selector && // Selector not empty? (Annotations from plugins, general defensive programming) - annotation.target.selector.length > 0 && - // Range not collapsed? (E.g. lazy loading PDFs. Note that this will have to + annotation.target.selector.length > 0 && + // Range not collapsed? (E.g. lazy loading PDFs. Note that this will have to // change if we switch from ranges to pre-computed bounds!) !annotation.target.selector[0].range.collapsed ); }, [annotation]); 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: () => denormalizeRectWithOffset( + r.state.store.getAnnotationBounds(annotation.id), + r.element.getBoundingClientRect() + ), + 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 }; @@ -154,4 +166,4 @@ export const TextAnnotatorPopup = (props: TextAnnotationPopupProps) => { ) : null; -} \ No newline at end of file +} diff --git a/packages/text-annotator/src/state/TextAnnotationStore.ts b/packages/text-annotator/src/state/TextAnnotationStore.ts index 7b81e2b1..b03da8ad 100644 --- a/packages/text-annotator/src/state/TextAnnotationStore.ts +++ b/packages/text-annotator/src/state/TextAnnotationStore.ts @@ -11,7 +11,9 @@ export interface TextAnnotationStore 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[]; diff --git a/packages/text-annotator/src/state/TextAnnotatorState.ts b/packages/text-annotator/src/state/TextAnnotatorState.ts index 710f22ff..5cb5a4ab 100644 --- a/packages/text-annotator/src/state/TextAnnotatorState.ts +++ b/packages/text-annotator/src/state/TextAnnotatorState.ts @@ -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 extends AnnotatorState { @@ -117,7 +117,7 @@ export const createTextAnnotatorState = 0 ? filtered[0] : undefined; } - 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; @@ -132,6 +132,13 @@ export const createTextAnnotatorState = [] => tree.getIntersecting(minX, minY, maxX, maxY); + const getAnnotationRects = (id: string): DOMRect[] => tree.getAnnotationRects(id); const recalculatePositions = () => tree.recalculate(); @@ -160,8 +167,8 @@ export const createTextAnnotatorState = (store: Store, 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; diff --git a/packages/text-annotator/src/utils/index.ts b/packages/text-annotator/src/utils/index.ts index fe84a63b..4b09af3b 100644 --- a/packages/text-annotator/src/utils/index.ts +++ b/packages/text-annotator/src/utils/index.ts @@ -14,4 +14,5 @@ export * from './reviveTarget'; export * from './splitAnnotatableRanges'; export * from './trimRangeToContainer'; export * from './cloneEvents'; +export * from './normalizeRects'; diff --git a/packages/text-annotator/src/utils/mergeClientRects.ts b/packages/text-annotator/src/utils/mergeClientRects.ts index cd7bed62..bb08631f 100644 --- a/packages/text-annotator/src/utils/mergeClientRects.ts +++ b/packages/text-annotator/src/utils/mergeClientRects.ts @@ -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((merged, rectA) => { // Some browser report empty rects - discard if (rectA.width === 0 || rectA.height === 0) return merged; @@ -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 { + 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. @@ -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 && diff --git a/packages/text-annotator/src/utils/normalizeRects.ts b/packages/text-annotator/src/utils/normalizeRects.ts new file mode 100644 index 00000000..461f222f --- /dev/null +++ b/packages/text-annotator/src/utils/normalizeRects.ts @@ -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); +}