Skip to content

Commit

Permalink
Merge branch 'spatial-tree-positioning' into stagin-candidate-2
Browse files Browse the repository at this point in the history
  • Loading branch information
oleksandr-danylchenko committed Oct 9, 2024
2 parents a659473 + a5cef01 commit f0c62e6
Show file tree
Hide file tree
Showing 7 changed files with 73 additions and 29 deletions.
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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 {
Expand Down Expand Up @@ -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 };
Expand Down Expand Up @@ -154,4 +166,4 @@ export const TextAnnotatorPopup = (props: TextAnnotationPopupProps) => {
</FloatingPortal>
) : null;

}
}
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 @@ -117,7 +117,7 @@ export const createTextAnnotatorState = <I extends TextAnnotation = TextAnnotati
return filtered.length > 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;

Expand All @@ -132,6 +132,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 @@ -160,8 +167,8 @@ export const createTextAnnotatorState = <I extends TextAnnotation = TextAnnotati
bulkUpsertAnnotations,
getAnnotationBounds,
getAnnotationRects,
getIntersecting,
getAt,
getIntersecting: tree.getIntersecting,
recalculatePositions,
updateTarget
},
Expand Down
18 changes: 11 additions & 7 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
1 change: 1 addition & 0 deletions packages/text-annotator/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,5 @@ 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);
}

0 comments on commit f0c62e6

Please sign in to comment.