From 5905be0d210a888c1ac558ae02d037d470280d19 Mon Sep 17 00:00:00 2001 From: Oleksandr Danylchenko Date: Mon, 4 Nov 2024 18:28:39 +0200 Subject: [PATCH 01/11] Moved `TextAnnotatorPopup` under the `TextAnnotator` --- packages/text-annotator-react/test/App.tsx | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/packages/text-annotator-react/test/App.tsx b/packages/text-annotator-react/test/App.tsx index 74849e2c..773137e5 100644 --- a/packages/text-annotator-react/test/App.tsx +++ b/packages/text-annotator-react/test/App.tsx @@ -187,13 +187,10 @@ export const App: FC = () => { to pity him except Neptune, who still persecuted him without ceasing and would not let him get home.

- - () - } - /> + ()} /> + + From c07d8a257fd4fb8325779515093ffc94c9262d91 Mon Sep 17 00:00:00 2001 From: Oleksandr Danylchenko Date: Mon, 4 Nov 2024 18:40:53 +0200 Subject: [PATCH 02/11] Added not annotatable class constant usage --- .../src/TextAnnotatorPopup/TextAnnotatorPopup.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/text-annotator-react/src/TextAnnotatorPopup/TextAnnotatorPopup.tsx b/packages/text-annotator-react/src/TextAnnotatorPopup/TextAnnotatorPopup.tsx index 6d1d0aac..126a84b4 100644 --- a/packages/text-annotator-react/src/TextAnnotatorPopup/TextAnnotatorPopup.tsx +++ b/packages/text-annotator-react/src/TextAnnotatorPopup/TextAnnotatorPopup.tsx @@ -1,6 +1,6 @@ import { PointerEvent, ReactNode, useCallback, useEffect, useMemo, useState } from 'react'; import { useAnnotator, useSelection } from '@annotorious/react'; -import { isRevived, TextAnnotation, TextAnnotator } from '@recogito/text-annotator'; +import { isRevived, NOT_ANNOTATABLE_CLASS, TextAnnotation, TextAnnotator } from '@recogito/text-annotator'; import { isMobile } from './isMobile'; import { autoUpdate, @@ -128,7 +128,7 @@ export const TextAnnotatorPopup = (props: TextAnnotationPopupProps) => { returnFocus={false} initialFocus={initialFocus}>
Date: Mon, 4 Nov 2024 18:42:19 +0200 Subject: [PATCH 03/11] Added `isIdling` state to the `SelectionHandler` --- .../text-annotator/src/SelectionHandler.ts | 74 +++++++++++++------ 1 file changed, 50 insertions(+), 24 deletions(-) diff --git a/packages/text-annotator/src/SelectionHandler.ts b/packages/text-annotator/src/SelectionHandler.ts index 84d45071..e0faf867 100644 --- a/packages/text-annotator/src/SelectionHandler.ts +++ b/packages/text-annotator/src/SelectionHandler.ts @@ -44,6 +44,8 @@ export const SelectionHandler = ( const { store, selection } = state; + let isIdling: boolean | undefined; + let currentTarget: TextAnnotationTarget | undefined; let isLeftClick: boolean | undefined; @@ -51,31 +53,38 @@ export const SelectionHandler = ( let lastDownEvent: Selection['event'] | undefined; const onSelectStart = (evt: Event) => { - if (isLeftClick === false) - return; + if (isLeftClick === false) return; /** - * Make sure we don't listen to selection changes that were - * not started on the container, or which are not supposed to + * Make sure to not listen to selection changes that weren't + * started on the container, or which aren't supposed to * be annotatable (like a component popup). - * Note that Chrome/iOS will sometimes return the root doc as target! + * Note that Chrome/iOS will sometimes return the root doc as a target! */ - currentTarget = isNotAnnotatable(evt.target as Node) - ? undefined - : { - annotation: uuidv4(), - selector: [], - creator: currentUser, - created: new Date() - }; + if (isNotAnnotatable(evt.target as Node)) { + currentTarget = undefined; + return; + } + + isIdling = false; + + currentTarget = { + annotation: uuidv4(), + creator: currentUser, + created: new Date(), + selector: [] + }; }; const onSelectionChange = debounce((evt: Event) => { const sel = document.getSelection(); - // This is to handle cases where the selection is "hijacked" by another element - // in a not-annotatable area. A rare case in theory. But rich text editors - // will like Quill do it... + /** + * This is to handle cases where the selection is "hijacked" + * by another element in a not-annotatable area. + * A rare case in theory. + * But rich text editors will, like the Quill does it. + */ if (isNotAnnotatable(sel.anchorNode)) { currentTarget = undefined; return; @@ -133,6 +142,8 @@ export const SelectionHandler = ( if (!hasChanged) return; + isIdling = false; + currentTarget = { ...currentTarget, selector: annotatableRanges.map(r => rangeToSelector(r, container, offsetReferenceSelector)), @@ -140,8 +151,8 @@ export const SelectionHandler = ( }; /** - * During mouse selection on the desktop, annotation won't usually exist while the selection is being edited. - * But it will be typical during keyboard or mobile handlebars selection! + * During mouse selection on the desktop, the annotation won't usually exist while the selection is being edited. + * But it'll be typical during keyboard or mobile handlebars' selection. */ if (store.getAnnotation(currentTarget.annotation)) { store.updateTarget(currentTarget, Origin.LOCAL); @@ -152,7 +163,7 @@ export const SelectionHandler = ( }); /** - * Select events don't carry information about the mouse button + * Select events don't carry information about the mouse button. * Therefore, to prevent right-click selection, we need to listen * to the initial pointerdown event and remember the button */ @@ -224,15 +235,17 @@ export const SelectionHandler = ( clickSelect(); } else if (currentTarget && currentTarget.selector.length > 0) { selection.clear(); + upsertCurrentTarget(); selection.userSelect(currentTarget.annotation, clonePointerEvent(evt)); } + + isIdling = true; }); } const onContextMenu = (evt: PointerEvent) => { const sel = document.getSelection(); - if (sel?.isCollapsed) return; // When selecting the initial word, Chrome Android fires `contextmenu` @@ -242,8 +255,9 @@ export const SelectionHandler = ( } upsertCurrentTarget(); - selection.userSelect(currentTarget.annotation, clonePointerEvent(evt)); + + isIdling = true; } const onKeyup = (evt: KeyboardEvent) => { @@ -252,8 +266,11 @@ export const SelectionHandler = ( if (!sel.isCollapsed) { selection.clear(); + upsertCurrentTarget(); selection.userSelect(currentTarget.annotation, cloneKeyboardEvent(evt)); + + isIdling = true; } } } @@ -275,7 +292,9 @@ export const SelectionHandler = ( document.removeEventListener('selectionchange', onSelected); - // Sigh... this needs a delay to work. But doesn't seem reliable. + isIdling = true; + + // This needs a delay to work. But doesn't seem reliable. }, 100); // Listen to the change event that follows @@ -300,8 +319,8 @@ export const SelectionHandler = ( * * It should be handled only on: * - the annotatable `container`, where the text is. - * - the `body`, where the focus goes when user closes the popup, - * or clicks the button that gets unmounted, e.g. "Close" + * - the `body`, where the focus goes when the user closes the popup, + * or clicks the button that gets unmounted, for example, "Close" */ const handleArrowKeyPress = (evt: KeyboardEvent) => { if ( @@ -311,7 +330,9 @@ export const SelectionHandler = ( return; } + isIdling = true; currentTarget = undefined; + selection.clear(); }; @@ -328,6 +349,11 @@ export const SelectionHandler = ( } const destroy = () => { + isIdling = undefined; + currentTarget = undefined; + lastDownEvent = undefined; + isLeftClick = undefined; + container.removeEventListener('pointerdown', onPointerDown); document.removeEventListener('pointerup', onPointerUp); document.removeEventListener('contextmenu', onContextMenu); From 1912741ccce9ab702b56133ff2b50d98eefc1d45 Mon Sep 17 00:00:00 2001 From: Oleksandr Danylchenko Date: Tue, 5 Nov 2024 12:22:30 +0200 Subject: [PATCH 04/11] Revert "Added `isIdling` state to the `SelectionHandler`" This reverts commit 16a3660ed2a4b77e4efeb2e798e5f8265e441a0e. --- .../text-annotator/src/SelectionHandler.ts | 74 ++++++------------- 1 file changed, 24 insertions(+), 50 deletions(-) diff --git a/packages/text-annotator/src/SelectionHandler.ts b/packages/text-annotator/src/SelectionHandler.ts index e0faf867..84d45071 100644 --- a/packages/text-annotator/src/SelectionHandler.ts +++ b/packages/text-annotator/src/SelectionHandler.ts @@ -44,8 +44,6 @@ export const SelectionHandler = ( const { store, selection } = state; - let isIdling: boolean | undefined; - let currentTarget: TextAnnotationTarget | undefined; let isLeftClick: boolean | undefined; @@ -53,38 +51,31 @@ export const SelectionHandler = ( let lastDownEvent: Selection['event'] | undefined; const onSelectStart = (evt: Event) => { - if (isLeftClick === false) return; + if (isLeftClick === false) + return; /** - * Make sure to not listen to selection changes that weren't - * started on the container, or which aren't supposed to + * Make sure we don't listen to selection changes that were + * not started on the container, or which are not supposed to * be annotatable (like a component popup). - * Note that Chrome/iOS will sometimes return the root doc as a target! + * Note that Chrome/iOS will sometimes return the root doc as target! */ - if (isNotAnnotatable(evt.target as Node)) { - currentTarget = undefined; - return; - } - - isIdling = false; - - currentTarget = { - annotation: uuidv4(), - creator: currentUser, - created: new Date(), - selector: [] - }; + currentTarget = isNotAnnotatable(evt.target as Node) + ? undefined + : { + annotation: uuidv4(), + selector: [], + creator: currentUser, + created: new Date() + }; }; const onSelectionChange = debounce((evt: Event) => { const sel = document.getSelection(); - /** - * This is to handle cases where the selection is "hijacked" - * by another element in a not-annotatable area. - * A rare case in theory. - * But rich text editors will, like the Quill does it. - */ + // This is to handle cases where the selection is "hijacked" by another element + // in a not-annotatable area. A rare case in theory. But rich text editors + // will like Quill do it... if (isNotAnnotatable(sel.anchorNode)) { currentTarget = undefined; return; @@ -142,8 +133,6 @@ export const SelectionHandler = ( if (!hasChanged) return; - isIdling = false; - currentTarget = { ...currentTarget, selector: annotatableRanges.map(r => rangeToSelector(r, container, offsetReferenceSelector)), @@ -151,8 +140,8 @@ export const SelectionHandler = ( }; /** - * During mouse selection on the desktop, the annotation won't usually exist while the selection is being edited. - * But it'll be typical during keyboard or mobile handlebars' selection. + * During mouse selection on the desktop, annotation won't usually exist while the selection is being edited. + * But it will be typical during keyboard or mobile handlebars selection! */ if (store.getAnnotation(currentTarget.annotation)) { store.updateTarget(currentTarget, Origin.LOCAL); @@ -163,7 +152,7 @@ export const SelectionHandler = ( }); /** - * Select events don't carry information about the mouse button. + * Select events don't carry information about the mouse button * Therefore, to prevent right-click selection, we need to listen * to the initial pointerdown event and remember the button */ @@ -235,17 +224,15 @@ export const SelectionHandler = ( clickSelect(); } else if (currentTarget && currentTarget.selector.length > 0) { selection.clear(); - upsertCurrentTarget(); selection.userSelect(currentTarget.annotation, clonePointerEvent(evt)); } - - isIdling = true; }); } const onContextMenu = (evt: PointerEvent) => { const sel = document.getSelection(); + if (sel?.isCollapsed) return; // When selecting the initial word, Chrome Android fires `contextmenu` @@ -255,9 +242,8 @@ export const SelectionHandler = ( } upsertCurrentTarget(); - selection.userSelect(currentTarget.annotation, clonePointerEvent(evt)); - isIdling = true; + selection.userSelect(currentTarget.annotation, clonePointerEvent(evt)); } const onKeyup = (evt: KeyboardEvent) => { @@ -266,11 +252,8 @@ export const SelectionHandler = ( if (!sel.isCollapsed) { selection.clear(); - upsertCurrentTarget(); selection.userSelect(currentTarget.annotation, cloneKeyboardEvent(evt)); - - isIdling = true; } } } @@ -292,9 +275,7 @@ export const SelectionHandler = ( document.removeEventListener('selectionchange', onSelected); - isIdling = true; - - // This needs a delay to work. But doesn't seem reliable. + // Sigh... this needs a delay to work. But doesn't seem reliable. }, 100); // Listen to the change event that follows @@ -319,8 +300,8 @@ export const SelectionHandler = ( * * It should be handled only on: * - the annotatable `container`, where the text is. - * - the `body`, where the focus goes when the user closes the popup, - * or clicks the button that gets unmounted, for example, "Close" + * - the `body`, where the focus goes when user closes the popup, + * or clicks the button that gets unmounted, e.g. "Close" */ const handleArrowKeyPress = (evt: KeyboardEvent) => { if ( @@ -330,9 +311,7 @@ export const SelectionHandler = ( return; } - isIdling = true; currentTarget = undefined; - selection.clear(); }; @@ -349,11 +328,6 @@ export const SelectionHandler = ( } const destroy = () => { - isIdling = undefined; - currentTarget = undefined; - lastDownEvent = undefined; - isLeftClick = undefined; - container.removeEventListener('pointerdown', onPointerDown); document.removeEventListener('pointerup', onPointerUp); document.removeEventListener('contextmenu', onContextMenu); From dd37d124361c0dc618ffc92626dc484f5f0b7e62 Mon Sep 17 00:00:00 2001 From: Oleksandr Danylchenko Date: Wed, 6 Nov 2024 16:50:18 +0200 Subject: [PATCH 05/11] Formatted contextmenu comment --- packages/text-annotator/src/SelectionHandler.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/text-annotator/src/SelectionHandler.ts b/packages/text-annotator/src/SelectionHandler.ts index 84d45071..cf656a1b 100644 --- a/packages/text-annotator/src/SelectionHandler.ts +++ b/packages/text-annotator/src/SelectionHandler.ts @@ -235,8 +235,10 @@ export const SelectionHandler = ( if (sel?.isCollapsed) return; - // When selecting the initial word, Chrome Android fires `contextmenu` - // before selectionChanged. + /** + * When selecting the initial word, Chrome Android + * fires the `contextmenu` before the `selectionchange` + */ if (!currentTarget || currentTarget.selector.length === 0) { onSelectionChange(evt); } From bc1c35260b56bd412e50a7db810c25eaaeae6e85 Mon Sep 17 00:00:00 2001 From: Oleksandr Danylchenko Date: Wed, 6 Nov 2024 16:53:10 +0200 Subject: [PATCH 06/11] Added annotation idling timeout before showing the popup --- .../TextAnnotatorPopup/TextAnnotatorPopup.tsx | 14 ++++--- .../text-annotator-react/src/hooks/index.ts | 1 + .../src/hooks/useAnnotationTargetIdling.ts | 40 +++++++++++++++++++ 3 files changed, 50 insertions(+), 5 deletions(-) create mode 100644 packages/text-annotator-react/src/hooks/index.ts create mode 100644 packages/text-annotator-react/src/hooks/useAnnotationTargetIdling.ts diff --git a/packages/text-annotator-react/src/TextAnnotatorPopup/TextAnnotatorPopup.tsx b/packages/text-annotator-react/src/TextAnnotatorPopup/TextAnnotatorPopup.tsx index 126a84b4..6c0db835 100644 --- a/packages/text-annotator-react/src/TextAnnotatorPopup/TextAnnotatorPopup.tsx +++ b/packages/text-annotator-react/src/TextAnnotatorPopup/TextAnnotatorPopup.tsx @@ -1,7 +1,8 @@ import { PointerEvent, ReactNode, useCallback, useEffect, useMemo, useState } from 'react'; + import { useAnnotator, useSelection } from '@annotorious/react'; import { isRevived, NOT_ANNOTATABLE_CLASS, TextAnnotation, TextAnnotator } from '@recogito/text-annotator'; -import { isMobile } from './isMobile'; + import { autoUpdate, flip, @@ -16,6 +17,8 @@ import { useRole } from '@floating-ui/react'; +import { useAnnotationTargetIdling } from '../hooks'; +import { isMobile } from './isMobile'; import './TextAnnotatorPopup.css'; interface TextAnnotationPopupProps { @@ -41,9 +44,10 @@ export const TextAnnotatorPopup = (props: TextAnnotationPopupProps) => { const r = useAnnotator(); const { selected, event } = useSelection(); - const annotation = selected[0]?.annotation; + const isAnnotationIdling = useAnnotationTargetIdling(annotation?.id); + const [isOpen, setOpen] = useState(selected?.length > 0); const { refs, floatingStyles, update, context } = useFloating({ @@ -72,8 +76,8 @@ export const TextAnnotatorPopup = (props: TextAnnotationPopupProps) => { useEffect(() => { const annotationSelector = annotation?.target.selector; - setOpen(annotationSelector?.length > 0 ? isRevived(annotationSelector) : false); - }, [annotation]); + setOpen(isAnnotationIdling && annotationSelector?.length > 0 ? isRevived(annotationSelector) : false); + }, [annotation?.target?.selector, isAnnotationIdling]); useEffect(() => { if (isOpen && annotation) { @@ -147,4 +151,4 @@ export const TextAnnotatorPopup = (props: TextAnnotationPopupProps) => { ) : null; -} +}; diff --git a/packages/text-annotator-react/src/hooks/index.ts b/packages/text-annotator-react/src/hooks/index.ts new file mode 100644 index 00000000..a6407b4e --- /dev/null +++ b/packages/text-annotator-react/src/hooks/index.ts @@ -0,0 +1 @@ +export { useAnnotationTargetIdling } from './useAnnotationTargetIdling'; diff --git a/packages/text-annotator-react/src/hooks/useAnnotationTargetIdling.ts b/packages/text-annotator-react/src/hooks/useAnnotationTargetIdling.ts new file mode 100644 index 00000000..31868543 --- /dev/null +++ b/packages/text-annotator-react/src/hooks/useAnnotationTargetIdling.ts @@ -0,0 +1,40 @@ +import { Origin, useAnnotationStore } from '@annotorious/react'; +import { useEffect, useState } from 'react'; +import { Ignore } from '@annotorious/core'; + +export const useAnnotationTargetIdling = ( + annotationId: string | undefined, + options: { timeout: number } = { timeout: 500 } +) => { + const store = useAnnotationStore(); + + const [isIdling, setIdling] = useState(true); + + useEffect(() => { + if (!annotationId) return; + + let idlingTimeout: ReturnType; + + const scheduleSetIdling = () => { + setIdling(false); + + clearTimeout(idlingTimeout); + idlingTimeout = setTimeout(() => setIdling(true), options.timeout); + }; + + store.observe( + scheduleSetIdling, + { + annotations: annotationId, + ignore: Ignore.BODY_ONLY, + origin: Origin.LOCAL + } + ); + return () => { + clearTimeout(idlingTimeout); + store.unobserve(scheduleSetIdling); + }; + }, [annotationId]); + + return isIdling; +}; From 718ebfb80cc22707bed7b8b7111140238a9641d1 Mon Sep 17 00:00:00 2001 From: Oleksandr Danylchenko Date: Wed, 6 Nov 2024 16:54:21 +0200 Subject: [PATCH 07/11] Added hooks re-export --- packages/text-annotator-react/src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/text-annotator-react/src/index.ts b/packages/text-annotator-react/src/index.ts index 645d635f..1c1ab44b 100644 --- a/packages/text-annotator-react/src/index.ts +++ b/packages/text-annotator-react/src/index.ts @@ -1,4 +1,5 @@ export * from './tei'; +export * from './hooks'; export * from './TextAnnotator'; export * from './TextAnnotatorPopup'; export * from './TextAnnotatorPlugin'; From 6b19bbce5b78da742b90c7c860bf9a00992a05c7 Mon Sep 17 00:00:00 2001 From: Oleksandr Danylchenko Date: Mon, 18 Nov 2024 14:51:57 +0200 Subject: [PATCH 08/11] Added quote comparison for the idling --- package-lock.json | 3 +- packages/text-annotator-react/package.json | 5 +- .../TextAnnotationPopup.tsx | 8 +-- .../text-annotator-react/src/hooks/index.ts | 2 +- .../src/hooks/useAnnotationQuoteIdling.ts | 60 +++++++++++++++++++ .../src/hooks/useAnnotationTargetIdling.ts | 40 ------------- 6 files changed, 70 insertions(+), 48 deletions(-) create mode 100644 packages/text-annotator-react/src/hooks/useAnnotationQuoteIdling.ts delete mode 100644 packages/text-annotator-react/src/hooks/useAnnotationTargetIdling.ts diff --git a/package-lock.json b/package-lock.json index 728fe47b..162ce7ea 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4415,7 +4415,8 @@ "@floating-ui/react": "^0.26.27", "@recogito/text-annotator": "3.0.0-rc.52", "@recogito/text-annotator-tei": "3.0.0-rc.52", - "CETEIcean": "^1.9.3" + "CETEIcean": "^1.9.3", + "dequal": "^2.0.3" }, "devDependencies": { "@types/react-dom": "^18.3.1", diff --git a/packages/text-annotator-react/package.json b/packages/text-annotator-react/package.json index 5d30334f..6a0863ea 100644 --- a/packages/text-annotator-react/package.json +++ b/packages/text-annotator-react/package.json @@ -49,6 +49,7 @@ "@floating-ui/react": "^0.26.27", "@recogito/text-annotator": "3.0.0-rc.52", "@recogito/text-annotator-tei": "3.0.0-rc.52", - "CETEIcean": "^1.9.3" + "CETEIcean": "^1.9.3", + "dequal": "^2.0.3" } -} \ No newline at end of file +} diff --git a/packages/text-annotator-react/src/TextAnnotationPopup/TextAnnotationPopup.tsx b/packages/text-annotator-react/src/TextAnnotationPopup/TextAnnotationPopup.tsx index a7304618..b24f33e2 100644 --- a/packages/text-annotator-react/src/TextAnnotationPopup/TextAnnotationPopup.tsx +++ b/packages/text-annotator-react/src/TextAnnotationPopup/TextAnnotationPopup.tsx @@ -20,7 +20,7 @@ import { useRole } from '@floating-ui/react'; -import { useAnnotationTargetIdling } from '../hooks'; +import { useAnnotationQuoteIdling } from '../hooks'; import { isMobile } from './isMobile'; import './TextAnnotationPopup.css'; @@ -53,7 +53,7 @@ export const TextAnnotationPopup = (props: TextAnnotationPopupProps) => { const { selected, event } = useSelection(); const annotation = selected[0]?.annotation; - const isAnnotationIdling = useAnnotationTargetIdling(annotation?.id); + const isAnnotationQuoteIdling = useAnnotationQuoteIdling(annotation?.id); const [isOpen, setOpen] = useState(selected?.length > 0); @@ -94,8 +94,8 @@ export const TextAnnotationPopup = (props: TextAnnotationPopupProps) => { useEffect(() => { const annotationSelector = annotation?.target.selector; - setOpen(isAnnotationIdling && annotationSelector?.length > 0 ? isRevived(annotationSelector) : false); - }, [annotation?.target?.selector, isAnnotationIdling]); + setOpen(isAnnotationQuoteIdling && annotationSelector?.length > 0 ? isRevived(annotationSelector) : false); + }, [annotation?.target?.selector, isAnnotationQuoteIdling]); useEffect(() => { if (isOpen && annotation) { diff --git a/packages/text-annotator-react/src/hooks/index.ts b/packages/text-annotator-react/src/hooks/index.ts index a6407b4e..4e8b7cca 100644 --- a/packages/text-annotator-react/src/hooks/index.ts +++ b/packages/text-annotator-react/src/hooks/index.ts @@ -1 +1 @@ -export { useAnnotationTargetIdling } from './useAnnotationTargetIdling'; +export { useAnnotationQuoteIdling } from './useAnnotationQuoteIdling'; diff --git a/packages/text-annotator-react/src/hooks/useAnnotationQuoteIdling.ts b/packages/text-annotator-react/src/hooks/useAnnotationQuoteIdling.ts new file mode 100644 index 00000000..e05b3bc5 --- /dev/null +++ b/packages/text-annotator-react/src/hooks/useAnnotationQuoteIdling.ts @@ -0,0 +1,60 @@ +import { useEffect, useState } from 'react'; +import { dequal } from 'dequal/lite'; + +import { Origin, useAnnotationStore } from '@annotorious/react'; +import { Ignore, type StoreChangeEvent } from '@annotorious/core'; +import type { TextAnnotation, TextAnnotationStore } from '@recogito/text-annotator'; + +export const useAnnotationQuoteIdling = ( + annotationId: string | undefined, + options: { timeout: number } = { timeout: 500 } +) => { + const store = useAnnotationStore(); + + const [isIdling, setIdling] = useState(true); + + useEffect(() => { + if (!store || !annotationId) return; + + let idlingTimeout: ReturnType; + + const scheduleSetIdling = (event: StoreChangeEvent) => { + const { changes: { updated } } = event; + + const hasChanged = updated?.some((update) => { + const { targetUpdated } = update; + if (!targetUpdated) return false; + + const { + oldTarget: { selector: oldSelector }, + newTarget: { selector: newSelector } + } = targetUpdated; + + const oldQuotes = oldSelector.map(({ quote }) => quote); + const newQuotes = newSelector.map(({ quote }) => quote); + + return dequal(oldQuotes, newQuotes); + }); + + if (!hasChanged) return; + + setIdling(false); + + clearTimeout(idlingTimeout); + idlingTimeout = setTimeout(() => setIdling(true), options.timeout); + }; + + store.observe(scheduleSetIdling, { + annotations: annotationId, + ignore: Ignore.BODY_ONLY, + origin: Origin.LOCAL + }); + + return () => { + clearTimeout(idlingTimeout); + store.unobserve(scheduleSetIdling); + }; + }, [store, annotationId, options.timeout]); + + return isIdling; +}; diff --git a/packages/text-annotator-react/src/hooks/useAnnotationTargetIdling.ts b/packages/text-annotator-react/src/hooks/useAnnotationTargetIdling.ts deleted file mode 100644 index 31868543..00000000 --- a/packages/text-annotator-react/src/hooks/useAnnotationTargetIdling.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { Origin, useAnnotationStore } from '@annotorious/react'; -import { useEffect, useState } from 'react'; -import { Ignore } from '@annotorious/core'; - -export const useAnnotationTargetIdling = ( - annotationId: string | undefined, - options: { timeout: number } = { timeout: 500 } -) => { - const store = useAnnotationStore(); - - const [isIdling, setIdling] = useState(true); - - useEffect(() => { - if (!annotationId) return; - - let idlingTimeout: ReturnType; - - const scheduleSetIdling = () => { - setIdling(false); - - clearTimeout(idlingTimeout); - idlingTimeout = setTimeout(() => setIdling(true), options.timeout); - }; - - store.observe( - scheduleSetIdling, - { - annotations: annotationId, - ignore: Ignore.BODY_ONLY, - origin: Origin.LOCAL - } - ); - return () => { - clearTimeout(idlingTimeout); - store.unobserve(scheduleSetIdling); - }; - }, [annotationId]); - - return isIdling; -}; From e4af5e565d3ebc4928072233431784fdace1ced6 Mon Sep 17 00:00:00 2001 From: Oleksandr Danylchenko Date: Mon, 18 Nov 2024 14:56:50 +0200 Subject: [PATCH 09/11] Added ts warning explanation --- .../src/hooks/useAnnotationQuoteIdling.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/text-annotator-react/src/hooks/useAnnotationQuoteIdling.ts b/packages/text-annotator-react/src/hooks/useAnnotationQuoteIdling.ts index e05b3bc5..090f31cd 100644 --- a/packages/text-annotator-react/src/hooks/useAnnotationQuoteIdling.ts +++ b/packages/text-annotator-react/src/hooks/useAnnotationQuoteIdling.ts @@ -30,7 +30,11 @@ export const useAnnotationQuoteIdling = ( newTarget: { selector: newSelector } } = targetUpdated; + // The generic type support in the `Update` was added in https://github.com/annotorious/annotorious/pull/476 + + // @ts-expect-error: requires generic `TextSelector` type support const oldQuotes = oldSelector.map(({ quote }) => quote); + // @ts-expect-error: requires generic `TextSelector` type support const newQuotes = newSelector.map(({ quote }) => quote); return dequal(oldQuotes, newQuotes); From 40fb398fc16149ceffcac1b1b601394cc0f86ebf Mon Sep 17 00:00:00 2001 From: Oleksandr Danylchenko Date: Mon, 18 Nov 2024 15:28:40 +0200 Subject: [PATCH 10/11] Added `hasTargetQuoteChanged` utility --- .../src/hooks/useAnnotationQuoteIdling.ts | 42 ++++++++++--------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/packages/text-annotator-react/src/hooks/useAnnotationQuoteIdling.ts b/packages/text-annotator-react/src/hooks/useAnnotationQuoteIdling.ts index 090f31cd..62898c9a 100644 --- a/packages/text-annotator-react/src/hooks/useAnnotationQuoteIdling.ts +++ b/packages/text-annotator-react/src/hooks/useAnnotationQuoteIdling.ts @@ -3,7 +3,7 @@ import { dequal } from 'dequal/lite'; import { Origin, useAnnotationStore } from '@annotorious/react'; import { Ignore, type StoreChangeEvent } from '@annotorious/core'; -import type { TextAnnotation, TextAnnotationStore } from '@recogito/text-annotator'; +import type { TextAnnotation, TextAnnotationStore, TextAnnotationTarget } from '@recogito/text-annotator'; export const useAnnotationQuoteIdling = ( annotationId: string | undefined, @@ -23,29 +23,21 @@ export const useAnnotationQuoteIdling = ( const hasChanged = updated?.some((update) => { const { targetUpdated } = update; - if (!targetUpdated) return false; + if (targetUpdated) { + const { oldTarget, newTarget } = targetUpdated; - const { - oldTarget: { selector: oldSelector }, - newTarget: { selector: newSelector } - } = targetUpdated; - - // The generic type support in the `Update` was added in https://github.com/annotorious/annotorious/pull/476 - - // @ts-expect-error: requires generic `TextSelector` type support - const oldQuotes = oldSelector.map(({ quote }) => quote); - // @ts-expect-error: requires generic `TextSelector` type support - const newQuotes = newSelector.map(({ quote }) => quote); - - return dequal(oldQuotes, newQuotes); + // The generic type support in the `Update` was added in https://github.com/annotorious/annotorious/pull/476 + // @ts-expect-error: requires generic `TextAnnotationTarget` type support + return hasTargetQuoteChanged(oldTarget, newTarget); + } }); - if (!hasChanged) return; + if (hasChanged) { + setIdling(false); - setIdling(false); - - clearTimeout(idlingTimeout); - idlingTimeout = setTimeout(() => setIdling(true), options.timeout); + clearTimeout(idlingTimeout); + idlingTimeout = setTimeout(() => setIdling(true), options.timeout); + } }; store.observe(scheduleSetIdling, { @@ -62,3 +54,13 @@ export const useAnnotationQuoteIdling = ( return isIdling; }; + +const hasTargetQuoteChanged = (oldValue: TextAnnotationTarget, newValue: TextAnnotationTarget) => { + const { selector: oldSelector } = oldValue; + const oldQuotes = oldSelector.map(({ quote }) => quote); + + const { selector: newSelector } = newValue; + const newQuotes = newSelector.map(({ quote }) => quote); + + return !dequal(oldQuotes, newQuotes); +}; From 3e92bd0479d6ca76c0e0deec847cc52659f3577f Mon Sep 17 00:00:00 2001 From: Oleksandr Danylchenko Date: Thu, 21 Nov 2024 17:38:56 +0200 Subject: [PATCH 11/11] Removed redundant `expect-error` --- .../text-annotator-react/src/hooks/useAnnotationQuoteIdling.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/text-annotator-react/src/hooks/useAnnotationQuoteIdling.ts b/packages/text-annotator-react/src/hooks/useAnnotationQuoteIdling.ts index 62898c9a..8fb0887e 100644 --- a/packages/text-annotator-react/src/hooks/useAnnotationQuoteIdling.ts +++ b/packages/text-annotator-react/src/hooks/useAnnotationQuoteIdling.ts @@ -25,9 +25,6 @@ export const useAnnotationQuoteIdling = ( const { targetUpdated } = update; if (targetUpdated) { const { oldTarget, newTarget } = targetUpdated; - - // The generic type support in the `Update` was added in https://github.com/annotorious/annotorious/pull/476 - // @ts-expect-error: requires generic `TextAnnotationTarget` type support return hasTargetQuoteChanged(oldTarget, newTarget); } });