diff --git a/package-lock.json b/package-lock.json index a432e9ac..237e39dc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2518,6 +2518,18 @@ "dev": true, "license": "MIT" }, + "node_modules/debounce": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/debounce/-/debounce-2.1.1.tgz", + "integrity": "sha512-+xRWxgel9LgTC4PwKlm7TJUK6B6qsEK77NaiNvXmeQ7Y3e6OVVsBC4a9BSptS/mAYceyAz37Oa8JTTuPRft7uQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/debug": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", @@ -5481,6 +5493,7 @@ "dependencies": { "@annotorious/core": "^3.0.14", "colord": "^2.9.3", + "debounce": "^2.1.1", "dequal": "^2.0.3", "hotkeys-js": "^3.13.9", "rbush": "^4.0.1", diff --git a/packages/text-annotator-react/src/TextAnnotator.tsx b/packages/text-annotator-react/src/TextAnnotator.tsx index e24b2f83..80bcf090 100644 --- a/packages/text-annotator-react/src/TextAnnotator.tsx +++ b/packages/text-annotator-react/src/TextAnnotator.tsx @@ -27,8 +27,8 @@ export const TextAnnotator = (null); const { className, children, ...opts } = props; - - const { style, filter, user } = opts; + + const { style, filter, user, annotatingEnabled } = opts; const { anno, setAnno } = useContext(AnnotoriousContext); @@ -49,10 +49,12 @@ export const TextAnnotator = anno?.setUser(user), [anno, user]); + useEffect(() => anno?.setAnnotatingEnabled(annotatingEnabled), [anno, annotatingEnabled]); + return (
{children}
- ) + ); } diff --git a/packages/text-annotator/package.json b/packages/text-annotator/package.json index 4e684700..9c021f98 100644 --- a/packages/text-annotator/package.json +++ b/packages/text-annotator/package.json @@ -37,6 +37,7 @@ "dependencies": { "@annotorious/core": "^3.0.14", "colord": "^2.9.3", + "debounce": "^2.1.1", "dequal": "^2.0.3", "hotkeys-js": "^3.13.9", "rbush": "^4.0.1", diff --git a/packages/text-annotator/src/SelectionHandler.ts b/packages/text-annotator/src/SelectionHandler.ts index d6451432..6b6f99d7 100644 --- a/packages/text-annotator/src/SelectionHandler.ts +++ b/packages/text-annotator/src/SelectionHandler.ts @@ -1,14 +1,15 @@ -import { Origin } from '@annotorious/core'; -import type { Filter, Selection, User } from '@annotorious/core'; +import debounce from 'debounce'; import { v4 as uuidv4 } from 'uuid'; import hotkeys from 'hotkeys-js'; + +import { Origin, type Filter, type Selection, type User } from '@annotorious/core'; + import type { TextAnnotatorState } from './state'; import type { TextAnnotation, TextAnnotationTarget } from './model'; import type { TextAnnotatorOptions } from './TextAnnotatorOptions'; import { clonePointerEvent, cloneKeyboardEvent, - debounce, splitAnnotatableRanges, rangeToSelector, isMac, @@ -28,12 +29,14 @@ const SELECTION_KEYS = [ SELECT_ALL ]; -export const SelectionHandler = ( +export const createSelectionHandler = ( container: HTMLElement, state: TextAnnotatorState, options: TextAnnotatorOptions ) => { + const { store, selection } = state; + let currentUser: User | undefined; const { annotatingEnabled, offsetReferenceSelector, selectionMode } = options; @@ -44,17 +47,29 @@ export const SelectionHandler = ( const setFilter = (filter?: Filter) => currentFilter = filter; - const { store, selection } = state; - let currentTarget: TextAnnotationTarget | undefined; let isLeftClick: boolean | undefined; let lastDownEvent: Selection['event'] | undefined; + let currentAnnotatingEnabled = annotatingEnabled; + + const setAnnotatingEnabled = (enabled: boolean) => { + currentAnnotatingEnabled = enabled; + onSelectionChange.clear(); + + if (!enabled) { + currentTarget = undefined; + isLeftClick = undefined; + lastDownEvent = undefined; + } + }; + const onSelectStart = (evt: Event) => { - if (isLeftClick === false) - return; + if (!currentAnnotatingEnabled) return; + + if (isLeftClick === false) return; /** * Make sure we don't listen to selection changes that were @@ -73,6 +88,8 @@ export const SelectionHandler = ( }; const onSelectionChange = debounce((evt: Event) => { + if (!currentAnnotatingEnabled) return; + const sel = document.getSelection(); /** @@ -166,7 +183,7 @@ export const SelectionHandler = ( // Proper lifecycle management: clear the previous selection first... selection.clear(); } - }); + }, 10); /** * Select events don't carry information about the mouse button. @@ -243,18 +260,20 @@ export const SelectionHandler = ( if (sel?.isCollapsed) return; - // When selecting the initial word, Chrome Android fires `contextmenu` + // When selecting the initial word, Chrome Android fires `contextmenu` // before selectionChanged. if (!currentTarget || currentTarget.selector.length === 0) { onSelectionChange(evt); } - + upsertCurrentTarget(); selection.userSelect(currentTarget.annotation, clonePointerEvent(evt)); } const onKeyup = (evt: KeyboardEvent) => { + if (!currentAnnotatingEnabled) return; + if (evt.key === 'Shift' && currentTarget) { const sel = document.getSelection(); @@ -351,13 +370,17 @@ export const SelectionHandler = ( document.addEventListener('pointerup', onPointerUp); document.addEventListener('contextmenu', onContextMenu); - if (annotatingEnabled) { - container.addEventListener('keyup', onKeyup); - container.addEventListener('selectstart', onSelectStart); - document.addEventListener('selectionchange', onSelectionChange); - } + container.addEventListener('keyup', onKeyup); + container.addEventListener('selectstart', onSelectStart); + document.addEventListener('selectionchange', onSelectionChange); const destroy = () => { + currentTarget = undefined; + isLeftClick = undefined; + lastDownEvent = undefined; + + onSelectionChange.clear(); + container.removeEventListener('pointerdown', onPointerDown); document.removeEventListener('pointerup', onPointerUp); document.removeEventListener('contextmenu', onContextMenu); @@ -372,7 +395,8 @@ export const SelectionHandler = ( return { destroy, setFilter, - setUser + setUser, + setAnnotatingEnabled } } diff --git a/packages/text-annotator/src/TextAnnotator.ts b/packages/text-annotator/src/TextAnnotator.ts index b391b512..9be39f88 100644 --- a/packages/text-annotator/src/TextAnnotator.ts +++ b/packages/text-annotator/src/TextAnnotator.ts @@ -1,14 +1,25 @@ -import { createAnonymousGuest, createLifecycleObserver, createBaseAnnotator, createUndoStack } from '@annotorious/core'; -import type { Filter } from '@annotorious/core'; +import { + createAnonymousGuest, + createLifecycleObserver, + createBaseAnnotator, + createUndoStack, + type Filter, +} from '@annotorious/core'; import type { Annotator, User, PresenceProvider } from '@annotorious/core'; -import { createCanvasRenderer, createHighlightsRenderer, createSpansRenderer, type HighlightStyleExpression } from './highlight'; + +import { + createCanvasRenderer, + createHighlightsRenderer, + createSpansRenderer, + type HighlightStyleExpression +} from './highlight'; import { createPresencePainter } from './presence'; import { scrollIntoView } from './api'; import { type TextAnnotationStore, type TextAnnotatorState, createTextAnnotatorState } from './state'; import type { TextAnnotation } from './model'; import { cancelSingleClickEvents, programmaticallyFocusable } from './utils'; import { fillDefaults, type RendererType, type TextAnnotatorOptions } from './TextAnnotatorOptions'; -import { SelectionHandler } from './SelectionHandler'; +import { createSelectionHandler } from './SelectionHandler'; import './TextAnnotator.css'; @@ -23,6 +34,8 @@ export interface TextAnnotator void; + state: TextAnnotatorState; } @@ -65,8 +78,8 @@ export const createTextAnnotator = currentUser; + const setAnnotatingEnabled = (enabled?: boolean) => { + selectionHandler.setAnnotatingEnabled( + enabled === undefined ? true : enabled + ); + }; + const setFilter = (filter?: Filter) => { highlightRenderer.setFilter(filter); selectionHandler.setFilter(filter); @@ -132,6 +152,7 @@ export const createTextAnnotator = ( opts: TextAnnotatorOptions, defaults: TextAnnotatorOptions -): TextAnnotatorOptions => { - - return { - ...opts, - annotatingEnabled: opts.annotatingEnabled ?? defaults.annotatingEnabled, - user: opts.user || defaults.user - }; - -}; +): TextAnnotatorOptions => ({ + ...opts, + annotatingEnabled: opts.annotatingEnabled ?? defaults.annotatingEnabled, + user: opts.user || defaults.user +}); diff --git a/packages/text-annotator/src/highlight/baseRenderer.ts b/packages/text-annotator/src/highlight/baseRenderer.ts index 43bdf001..429f1338 100644 --- a/packages/text-annotator/src/highlight/baseRenderer.ts +++ b/packages/text-annotator/src/highlight/baseRenderer.ts @@ -1,6 +1,8 @@ +import debounce from 'debounce'; + import type { Filter, ViewportState } from '@annotorious/core'; + import type { TextAnnotatorState } from '../state'; -import { debounce } from '../utils'; import { type ViewportBounds, getViewportBounds, trackViewport } from './viewport'; import type { HighlightPainter } from './HighlightPainter'; import type { Highlight } from './Highlight'; @@ -135,11 +137,10 @@ export const createBaseRenderer = { store.recalculatePositions(); - if (currentPainter) - currentPainter.reset(); + currentPainter?.reset(); redraw(); - }); + }, 10); window.addEventListener('resize', onResize); @@ -169,6 +170,7 @@ export const createBaseRenderer = { }); }); - const onResize = debounce(() => { - resetCanvas(canvas); - }); + const onResize = debounce(() => resetCanvas(canvas), 10); window.addEventListener('resize', onResize); @@ -130,6 +130,7 @@ const createRenderer = (container: HTMLElement): RendererImplementation => { const destroy = () => { canvas.remove(); + onResize.clear(); window.removeEventListener('resize', onResize); } diff --git a/packages/text-annotator/src/utils/debounce.ts b/packages/text-annotator/src/utils/debounce.ts deleted file mode 100644 index c20a566c..00000000 --- a/packages/text-annotator/src/utils/debounce.ts +++ /dev/null @@ -1,8 +0,0 @@ -export const debounce = void>(func: T, delay = 10): T => { - let timeoutId: ReturnType; - - return ((...args: any[]) => { - clearTimeout(timeoutId); - timeoutId = setTimeout(() => func.apply(this, args), delay); - }) as T; -} diff --git a/packages/text-annotator/src/utils/index.ts b/packages/text-annotator/src/utils/index.ts index 5d536dd2..7a438d7c 100644 --- a/packages/text-annotator/src/utils/index.ts +++ b/packages/text-annotator/src/utils/index.ts @@ -2,7 +2,6 @@ export * from './cancelSingleClickEvents'; export * from './cloneEvents'; export * from './device'; export * from './programmaticallyFocusable'; -export * from './debounce'; export * from './getQuoteContext'; export * from './isNotAnnotatable'; export * from './isRevived';