Skip to content

Commit

Permalink
Merge branch 'main' into recogito#172-fix-popup-flickering-selection
Browse files Browse the repository at this point in the history
# Conflicts:
#	package-lock.json
#	packages/text-annotator-react/package.json
#	packages/text-annotator-react/src/TextAnnotationPopup/TextAnnotationPopup.tsx
  • Loading branch information
oleksandr-danylchenko committed Nov 21, 2024
2 parents 40fb398 + f2341c3 commit b854356
Show file tree
Hide file tree
Showing 17 changed files with 343 additions and 286 deletions.
418 changes: 213 additions & 205 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@recogito/text-annotator-monorepo",
"version": "3.0.0-rc.52",
"version": "3.0.0-rc.53",
"description": "Recogito Text Annotator monorepo",
"author": "Rainer Simon",
"repository": {
Expand Down
4 changes: 2 additions & 2 deletions packages/extension-tei/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@recogito/text-annotator-tei",
"version": "3.0.0-rc.52",
"version": "3.0.0-rc.53",
"description": "Recogito Text Annotator TEI extension",
"author": "Rainer Simon",
"license": "BSD-3-Clause",
Expand Down Expand Up @@ -33,6 +33,6 @@
},
"peerDependencies": {
"@annotorious/core": "^3.0.12",
"@recogito/text-annotator": "3.0.0-rc.52"
"@recogito/text-annotator": "3.0.0-rc.53"
}
}
10 changes: 5 additions & 5 deletions packages/text-annotator-react/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@recogito/react-text-annotator",
"version": "3.0.0-rc.52",
"version": "3.0.0-rc.53",
"description": "Recogito Text Annotator React bindings",
"author": "Rainer Simon",
"license": "BSD-3-Clause",
Expand Down Expand Up @@ -31,7 +31,7 @@
"typescript": "5.6.3",
"vite": "^5.4.11",
"vite-plugin-dts": "^4.3.0",
"vite-tsconfig-paths": "^5.1.2"
"vite-tsconfig-paths": "^5.1.3"
},
"peerDependencies": {
"openseadragon": "^3.0.0 || ^4.0.0 || ^5.0.0",
Expand All @@ -46,9 +46,9 @@
"dependencies": {
"@annotorious/core": "^3.0.12",
"@annotorious/react": "^3.0.12",
"@floating-ui/react": "^0.26.27",
"@recogito/text-annotator": "3.0.0-rc.52",
"@recogito/text-annotator-tei": "3.0.0-rc.52",
"@floating-ui/react": "^0.26.28",
"@recogito/text-annotator": "3.0.0-rc.53",
"@recogito/text-annotator-tei": "3.0.0-rc.53",
"CETEIcean": "^1.9.3",
"dequal": "^2.0.3"
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
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,
toDomRectList,
type TextAnnotation,
type TextAnnotator,
} from '@recogito/text-annotator';

import {
arrow,
Expand Down Expand Up @@ -46,6 +51,12 @@ export interface TextAnnotationPopupContentProps {

}

const toViewportBounds = (annotationBounds: DOMRect, container: HTMLElement): DOMRect => {
const { left, top, right, bottom } = annotationBounds;
const containerBounds = container.getBoundingClientRect();
return new DOMRect(left + containerBounds.left, top + containerBounds.top, right - left, bottom - top);
}

export const TextAnnotationPopup = (props: TextAnnotationPopupProps) => {

const r = useAnnotator<TextAnnotator>();
Expand All @@ -59,20 +70,6 @@ export const TextAnnotationPopup = (props: TextAnnotationPopupProps) => {

const arrowRef = useRef(null);

// Conditional floating-ui middleware
const middleware = useMemo(() => {
const m = [
inline(),
offset(10),
flip({ crossAxis: true }),
shift({ crossAxis: true, padding: 10 })
];

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

const { refs, floatingStyles, update, context } = useFloating({
placement: isMobile() ? 'bottom' : 'top',
open: isOpen,
Expand All @@ -82,7 +79,13 @@ export const TextAnnotationPopup = (props: TextAnnotationPopupProps) => {
r?.cancelSelected();
}
},
middleware,
middleware: [
inline(),
offset(10),
flip({ crossAxis: true }),
shift({ crossAxis: true, padding: 10 }),
arrow({ element: arrowRef })
],
whileElementsMounted: autoUpdate
});

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

useEffect(() => {
const annotationSelector = annotation?.target.selector;
setOpen(isAnnotationQuoteIdling && annotationSelector?.length > 0 ? isRevived(annotationSelector) : false);
}, [annotation?.target?.selector, isAnnotationQuoteIdling]);
if (annotation?.id && isAnnotationQuoteIdling) {
const bounds = r?.state.store.getAnnotationBounds(annotation.id);
setOpen(Boolean(bounds));
} else {
setOpen(false);
}
}, [annotation?.id, isAnnotationQuoteIdling, 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: () => {
// Annotation bounds are relative to the document element
const bounds = r.state.store.getAnnotationBounds(annotation.id);
return bounds
? toViewportBounds(bounds, r.element)
: new DOMRect();
},
getClientRects: () => {
const rects = r.state.store.getAnnotationRects(annotation.id);
const viewportRects = rects.map(rect => toViewportBounds(rect, r.element));
return toDomRectList(viewportRects);
}
});
} 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
10 changes: 8 additions & 2 deletions packages/text-annotator-react/src/tei/CETEIcean.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import CETEI from 'CETEIcean';

interface CETEIceanProps {

initArgs?: any;

tei?: string;

onLoad?(element: Element): void;
Expand Down Expand Up @@ -55,7 +57,7 @@ export const CETEIcean = (props: CETEIceanProps) => {

useEffect(() => {
if (props.tei) {
const ceteicean = new CETEI();
const ceteicean = new CETEI(props.initArgs);

ceteicean.addBehaviors({
...PRESET_BEHAVIORS,
Expand All @@ -71,7 +73,11 @@ export const CETEIcean = (props: CETEIceanProps) => {
props.onLoad(el.current);
});
}
}, [props.tei]);

return () => {
el.current.innerHTML = '';
}
}, [props.tei, JSON.stringify(props.initArgs), props.onLoad]);

return (
<div
Expand Down
6 changes: 3 additions & 3 deletions packages/text-annotator-react/src/tei/TEIAnnotator.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Children, ReactElement, ReactNode, cloneElement, useContext, useEffect } from 'react';
import { Children, ReactElement, ReactNode, cloneElement, useCallback, useContext, useEffect } from 'react';
import { AnnotoriousContext, Filter } from '@annotorious/react';
import { TEIPlugin } from '@recogito/text-annotator-tei';
import { createTextAnnotator, HighlightStyleExpression } from '@recogito/text-annotator';
Expand All @@ -22,10 +22,10 @@ export const TEIAnnotator = (props: TEIAnnotatorProps) => {

const { anno, setAnno } = useContext(AnnotoriousContext);

const onLoad = (element: HTMLElement) => {
const onLoad = useCallback((element: HTMLElement) => {
const anno = TEIPlugin(createTextAnnotator(element, opts));
setAnno(anno);
}
}, []);

useEffect(() => {
if (!anno)
Expand Down
13 changes: 8 additions & 5 deletions packages/text-annotator-react/test/tei/App.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import React, { FC, useEffect, useRef, useState } from 'react';

import { AnnotationBody, Annotorious, useAnnotationStore, useAnnotator } from '@annotorious/react';
import { TextAnnotation, TextAnnotator as VanillaTextAnnotator } from '@recogito/text-annotator';

import { TEIAnnotator, CETEIcean, TextAnnotatorPopup, type TextAnnotationPopupContentProps } from '../../src';
import {
TEIAnnotator,
CETEIcean,
type TextAnnotationPopupContentProps,
TextAnnotationPopup
} from '../../src';

const TestPopup: FC<TextAnnotationPopupContentProps> = (props) => {

Expand All @@ -22,7 +25,7 @@ const TestPopup: FC<TextAnnotationPopupContentProps> = (props) => {
};

const onClick = () => {
store.addBody(body);
store?.addBody(body);
r.cancelSelected();
};

Expand Down Expand Up @@ -78,7 +81,7 @@ export const App: FC = () => {
<TEIAnnotator>
<CETEIcean tei={tei} />

<TextAnnotatorPopup
<TextAnnotationPopup
popup={props => (
<TestPopup {...props} />
)}
Expand Down
10 changes: 7 additions & 3 deletions packages/text-annotator-react/test/tei/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import React, { createRoot } from 'react-dom/client';
import React from 'react';
import ReactDOM from 'react-dom/client';

import { App } from './App';

const root = createRoot(document.getElementById('root') as Element);
const root = ReactDOM.createRoot(document.getElementById('root') as Element);
root.render(
<App />
<React.StrictMode>
<App />
</React.StrictMode>
)
4 changes: 2 additions & 2 deletions packages/text-annotator/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@recogito/text-annotator",
"version": "3.0.0-rc.52",
"version": "3.0.0-rc.53",
"description": "A JavaScript text annotation library",
"author": "Rainer Simon",
"license": "BSD-3-Clause",
Expand Down Expand Up @@ -32,7 +32,7 @@
"typescript": "5.6.3",
"vite": "^5.4.11",
"vite-plugin-dts": "^4.3.0",
"vitest": "^2.1.4"
"vitest": "^2.1.5"
},
"dependencies": {
"@annotorious/core": "^3.0.12",
Expand Down
19 changes: 15 additions & 4 deletions packages/text-annotator/src/SelectionHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { v4 as uuidv4 } from 'uuid';
import hotkeys from 'hotkeys-js';
import type { TextAnnotatorState } from './state';
import type { TextAnnotation, TextAnnotationTarget } from './model';
import type { TextAnnotatorOptions } from './TextAnnotatorOptions';
import {
clonePointerEvent,
cloneKeyboardEvent,
Expand All @@ -15,7 +16,6 @@ import {
trimRangeToContainer,
isNotAnnotatable
} from './utils';
import type { TextAnnotatorOptions } from './TextAnnotatorOptions';

const CLICK_TIMEOUT = 300;

Expand Down Expand Up @@ -75,6 +75,19 @@ export const SelectionHandler = (
const onSelectionChange = debounce((evt: Event) => {
const sel = document.getSelection();

/**
* In iOS when a user clicks on a button, the `selectionchange` event is fired.
* However, the generated selection is empty and the `anchorNode` is `null`.
* That doesn't give us information about whether the selection is in the annotatable area
* or whether the previously selected text was dismissed.
* Therefore - we should bail out from such a range processing.
*
* @see https://github.com/recogito/text-annotator-js/pull/164#issuecomment-2416961473
*/
if (!sel?.anchorNode) {
return;
}

/**
* This is to handle cases where the selection is "hijacked"
* by another element in a not-annotatable area.
Expand Down Expand Up @@ -189,7 +202,7 @@ export const SelectionHandler = (
const currentIds = new Set(selected.map(s => s.id));
const nextIds = Array.isArray(hovered) ? hovered.map(a => a.id) : [hovered.id];

const hasChanged =
const hasChanged =
currentIds.size !== nextIds.length ||
!nextIds.every(id => currentIds.has(id));

Expand Down Expand Up @@ -219,7 +232,6 @@ export const SelectionHandler = (
currentTarget = undefined;
clickSelect();
} else if (currentTarget && currentTarget.selector.length > 0) {
selection.clear();
upsertCurrentTarget();
selection.userSelect(currentTarget.annotation, clonePointerEvent(evt));
}
Expand Down Expand Up @@ -249,7 +261,6 @@ export const SelectionHandler = (
const sel = document.getSelection();

if (!sel.isCollapsed) {
selection.clear();
upsertCurrentTarget();
selection.userSelect(currentTarget.annotation, cloneKeyboardEvent(evt));
}
Expand Down
12 changes: 6 additions & 6 deletions packages/text-annotator/src/model/w3c/W3CTextFormatAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,17 @@ import type { TextAnnotation, TextAnnotationTarget, TextSelector } from '../core
import type { W3CTextAnnotation, W3CTextAnnotationTarget, W3CTextSelector } from '../w3c';
import { getQuoteContext } from '../../utils';

export type W3CTextFormatAdapter = FormatAdapter<TextAnnotation, W3CTextAnnotation>;
export type W3CTextFormatAdapter<I extends TextAnnotation = TextAnnotation, E extends W3CTextAnnotation = W3CTextAnnotation> = FormatAdapter<I, E>;

/**
* @param source - the IRI of the annotated content
* @param container - the HTML container of the annotated content,
* Required to locate the content's `range` within the DOM
*/
export const W3CTextFormat = (
export const W3CTextFormat = <E extends W3CTextAnnotation = W3CTextAnnotation>(
source: string,
container: HTMLElement
): W3CTextFormatAdapter => ({
): W3CTextFormatAdapter<TextAnnotation, E> => ({
parse: (serialized) => parseW3CTextAnnotation(serialized),
serialize: (annotation) => serializeW3CTextAnnotation(annotation, source, container)
});
Expand Down Expand Up @@ -119,11 +119,11 @@ export const parseW3CTextAnnotation = (

};

export const serializeW3CTextAnnotation = (
export const serializeW3CTextAnnotation = <E extends W3CTextAnnotation = W3CTextAnnotation>(
annotation: TextAnnotation,
source: string,
container: HTMLElement
): W3CTextAnnotation => {
): E => {
const { bodies, target, ...rest } = annotation;

const {
Expand Down Expand Up @@ -171,6 +171,6 @@ export const serializeW3CTextAnnotation = (
created: created?.toISOString(),
modified: updated?.toISOString(),
target: w3cTargets
};
} as E;

};
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): DOMRect | undefined;

getAnnotationRects(id: string): DOMRect[];

Expand Down
Loading

0 comments on commit b854356

Please sign in to comment.