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);
}
});