forked from akai/readest
feat(annotator): instant highlighting with mouse, touch or pen (#2881)
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import clsx from 'clsx';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { BookNote } from '@/types/book';
|
||||
import { BookNote, HighlightColor } from '@/types/book';
|
||||
import { Point, TextSelection } from '@/utils/sel';
|
||||
import { useSettingsStore } from '@/store/settingsStore';
|
||||
import { useAnnotationEditor } from '../../hooks/useAnnotationEditor';
|
||||
@@ -105,6 +105,7 @@ interface AnnotationRangeEditorProps {
|
||||
bookKey: string;
|
||||
annotation: BookNote;
|
||||
selection: TextSelection;
|
||||
handleColor: HighlightColor;
|
||||
getAnnotationText: (range: Range) => Promise<string>;
|
||||
setSelection: React.Dispatch<React.SetStateAction<TextSelection | null>>;
|
||||
onStartEdit: () => void;
|
||||
@@ -114,6 +115,7 @@ const AnnotationRangeEditor: React.FC<AnnotationRangeEditorProps> = ({
|
||||
bookKey,
|
||||
annotation,
|
||||
selection,
|
||||
handleColor,
|
||||
getAnnotationText,
|
||||
setSelection,
|
||||
onStartEdit,
|
||||
@@ -123,7 +125,7 @@ const AnnotationRangeEditor: React.FC<AnnotationRangeEditorProps> = ({
|
||||
useAnnotationEditor({ bookKey, annotation, getAnnotationText, setSelection });
|
||||
|
||||
const initializedRef = useRef(false);
|
||||
const handleColor = getHighlightColorHex(settings, annotation.color) ?? '#FFFF00';
|
||||
const handleColorHex = getHighlightColorHex(settings, handleColor) ?? '#FFFF00';
|
||||
const draggingRef = useRef<'start' | 'end' | null>(null);
|
||||
const startRef = useRef<Point>({ x: 0, y: 0 });
|
||||
const endRef = useRef<Point>({ x: 0, y: 0 });
|
||||
@@ -198,7 +200,7 @@ const AnnotationRangeEditor: React.FC<AnnotationRangeEditorProps> = ({
|
||||
<Handle
|
||||
position={currentStart}
|
||||
type='start'
|
||||
color={handleColor}
|
||||
color={handleColorHex}
|
||||
onDragStart={handleStartDragStart}
|
||||
onDrag={handleStartDrag}
|
||||
onDragEnd={handleDragEnd}
|
||||
@@ -206,7 +208,7 @@ const AnnotationRangeEditor: React.FC<AnnotationRangeEditorProps> = ({
|
||||
<Handle
|
||||
position={currentEnd}
|
||||
type='end'
|
||||
color={handleColor}
|
||||
color={handleColorHex}
|
||||
onDragStart={handleEndDragStart}
|
||||
onDrag={handleEndDrag}
|
||||
onDragEnd={handleDragEnd}
|
||||
|
||||
@@ -198,9 +198,12 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
isTextSelected,
|
||||
handleScroll,
|
||||
handleTouchStart,
|
||||
handleTouchMove,
|
||||
handleTouchEnd,
|
||||
handlePointerdown,
|
||||
handlePointerup,
|
||||
handlePointerDown,
|
||||
handlePointerMove,
|
||||
handlePointerCancel,
|
||||
handlePointerUp,
|
||||
handleSelectionchange,
|
||||
handleShowPopup,
|
||||
handleUpToPopup,
|
||||
@@ -217,10 +220,12 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
const detail = (event as CustomEvent).detail;
|
||||
const { doc, index } = detail;
|
||||
|
||||
const handleTouchmove = () => {
|
||||
const handleTouchmove = (ev: TouchEvent) => {
|
||||
// Available on iOS, on Android not fired
|
||||
// To make the popup not follow the selection while dragging
|
||||
setShowAnnotPopup(false);
|
||||
setEditingAnnotation(null);
|
||||
handleTouchMove(ev);
|
||||
};
|
||||
|
||||
const handleNativeTouch = (event: CustomEvent) => {
|
||||
@@ -229,7 +234,7 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
handleTouchStart();
|
||||
} else if (ev.type === 'touchend') {
|
||||
handleTouchEnd();
|
||||
handlePointerup(doc, index);
|
||||
handlePointerUp(doc, index);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -245,13 +250,14 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
view?.renderer?.addEventListener('scroll', () => {
|
||||
repositionPopups();
|
||||
});
|
||||
detail.doc?.addEventListener('touchstart', handleTouchStart);
|
||||
detail.doc?.addEventListener('touchmove', handleTouchmove);
|
||||
const opts = { passive: false };
|
||||
detail.doc?.addEventListener('touchstart', handleTouchStart, opts);
|
||||
detail.doc?.addEventListener('touchmove', handleTouchmove, opts);
|
||||
detail.doc?.addEventListener('touchend', handleTouchEnd);
|
||||
detail.doc?.addEventListener('pointerdown', handlePointerdown);
|
||||
detail.doc?.addEventListener('pointerup', (ev: PointerEvent) =>
|
||||
handlePointerup(doc, index, ev),
|
||||
);
|
||||
detail.doc?.addEventListener('pointerdown', handlePointerDown.bind(null, doc, index), opts);
|
||||
detail.doc?.addEventListener('pointermove', handlePointerMove.bind(null, doc, index), opts);
|
||||
detail.doc?.addEventListener('pointercancel', handlePointerCancel.bind(null, doc, index));
|
||||
detail.doc?.addEventListener('pointerup', handlePointerUp.bind(null, doc, index));
|
||||
detail.doc?.addEventListener('selectionchange', () => handleSelectionchange(doc));
|
||||
|
||||
// For PDF selections, enable right-click context menu to directly open translator popup.
|
||||
@@ -935,6 +941,7 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
bookKey={bookKey}
|
||||
annotation={editingAnnotation}
|
||||
selection={selection}
|
||||
handleColor={selectedColor}
|
||||
getAnnotationText={getAnnotationText}
|
||||
setSelection={setSelection}
|
||||
onStartEdit={handleStartEditAnnotation}
|
||||
|
||||
@@ -67,6 +67,7 @@ export const useAnnotationEditor = ({
|
||||
const contents = view.renderer.getContents();
|
||||
if (!contents || contents.length === 0) return;
|
||||
|
||||
// the point is from viewport, need to adjust to each content's coordinate
|
||||
const findPositionAtPoint = (doc: Document, x: number, y: number) => {
|
||||
const frameElement = doc.defaultView?.frameElement;
|
||||
const frameRect = frameElement?.getBoundingClientRect() ?? { top: 0, left: 0 };
|
||||
@@ -102,22 +103,11 @@ export const useAnnotationEditor = ({
|
||||
targetDoc = doc;
|
||||
targetIndex = index ?? 0;
|
||||
break;
|
||||
} else if (sp && !startPos) {
|
||||
startPos = sp;
|
||||
targetDoc = doc;
|
||||
targetIndex = index ?? 0;
|
||||
} else if (ep && !endPos) {
|
||||
endPos = ep;
|
||||
if (!targetDoc) {
|
||||
targetDoc = doc;
|
||||
targetIndex = index ?? 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!startPos || !endPos || !targetDoc) return;
|
||||
|
||||
// Create a new range from the positions
|
||||
const newRange = targetDoc.createRange();
|
||||
try {
|
||||
const positionComparison = startPos.node.compareDocumentPosition(endPos.node);
|
||||
|
||||
@@ -0,0 +1,302 @@
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { BookNote } from '@/types/book';
|
||||
import { Point, TextSelection } from '@/utils/sel';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useSettingsStore } from '@/store/settingsStore';
|
||||
import { useBookDataStore } from '@/store/bookDataStore';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import { uniqueId } from '@/utils/misc';
|
||||
|
||||
interface UseInstantAnnotationProps {
|
||||
bookKey: string;
|
||||
getAnnotationText: (range: Range) => Promise<string>;
|
||||
setSelection: React.Dispatch<React.SetStateAction<TextSelection | null>>;
|
||||
}
|
||||
|
||||
export const useInstantAnnotation = ({ bookKey, getAnnotationText }: UseInstantAnnotationProps) => {
|
||||
const { envConfig } = useEnv();
|
||||
const { settings } = useSettingsStore();
|
||||
const { getConfig, saveConfig, updateBooknotes } = useBookDataStore();
|
||||
const { getView, getViewsById, getViewSettings } = useReaderStore();
|
||||
|
||||
const startPointRef = useRef<Point | null>(null);
|
||||
const startDocRef = useRef<Document | null>(null);
|
||||
const startIndexRef = useRef<number>(0);
|
||||
const previewAnnotationRef = useRef<BookNote | null>(null);
|
||||
const annotationIdRef = useRef<string>(uniqueId());
|
||||
|
||||
const isInstantAnnotationEnabled = useCallback(() => {
|
||||
const viewSettings = getViewSettings(bookKey);
|
||||
return (
|
||||
viewSettings?.enableAnnotationQuickActions &&
|
||||
viewSettings?.annotationQuickAction === 'highlight'
|
||||
);
|
||||
}, [bookKey, getViewSettings]);
|
||||
|
||||
const clearPreviewAnnotation = useCallback(() => {
|
||||
if (previewAnnotationRef.current) {
|
||||
const views = getViewsById(bookKey.split('-')[0]!);
|
||||
views.forEach((v) => v?.addAnnotation(previewAnnotationRef.current!, true));
|
||||
previewAnnotationRef.current = null;
|
||||
}
|
||||
}, [bookKey, getViewsById]);
|
||||
|
||||
const findPositionAtPoint = useCallback((doc: Document, x: number, y: number) => {
|
||||
if (doc.caretPositionFromPoint) {
|
||||
const pos = doc.caretPositionFromPoint(x, y);
|
||||
if (pos) return { node: pos.offsetNode, offset: pos.offset };
|
||||
}
|
||||
if (doc.caretRangeFromPoint) {
|
||||
const range = doc.caretRangeFromPoint(x, y);
|
||||
if (range) return { node: range.startContainer, offset: range.startOffset };
|
||||
}
|
||||
return null;
|
||||
}, []);
|
||||
|
||||
const isSelectableContent = useCallback(
|
||||
(doc: Document, x: number, y: number): boolean => {
|
||||
const pos = findPositionAtPoint(doc, x, y);
|
||||
if (!pos) return false;
|
||||
|
||||
// Must be a text node
|
||||
if (pos.node.nodeType !== Node.TEXT_NODE) return false;
|
||||
|
||||
const textNode = pos.node as Text;
|
||||
const textLength = textNode.length;
|
||||
if (textLength === 0) return false;
|
||||
|
||||
// Create a range around the caret position to get the character bounds
|
||||
const range = doc.createRange();
|
||||
try {
|
||||
// Get bounds of character at or after the caret position
|
||||
const startOffset = Math.min(pos.offset, textLength - 1);
|
||||
const endOffset = Math.min(pos.offset + 1, textLength);
|
||||
range.setStart(textNode, startOffset);
|
||||
range.setEnd(textNode, endOffset);
|
||||
|
||||
const rects = range.getClientRects();
|
||||
for (const rect of rects) {
|
||||
const tolerance = 20;
|
||||
if (
|
||||
x >= rect.left - tolerance &&
|
||||
x <= rect.right + tolerance &&
|
||||
y >= rect.top - tolerance &&
|
||||
y <= rect.bottom + tolerance
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
[findPositionAtPoint],
|
||||
);
|
||||
|
||||
const createRangeFromPoints = useCallback(
|
||||
(doc: Document, startPoint: Point, endPoint: Point) => {
|
||||
const startPos = findPositionAtPoint(doc, startPoint.x, startPoint.y);
|
||||
const endPos = findPositionAtPoint(doc, endPoint.x, endPoint.y);
|
||||
|
||||
if (!startPos || !endPos) return null;
|
||||
|
||||
const newRange = doc.createRange();
|
||||
try {
|
||||
const positionComparison = startPos.node.compareDocumentPosition(endPos.node);
|
||||
const needsSwap =
|
||||
positionComparison & Node.DOCUMENT_POSITION_PRECEDING ||
|
||||
(startPos.node === endPos.node && startPos.offset > endPos.offset);
|
||||
|
||||
if (needsSwap) {
|
||||
newRange.setStart(endPos.node, endPos.offset);
|
||||
newRange.setEnd(startPos.node, startPos.offset);
|
||||
} else {
|
||||
newRange.setStart(startPos.node, startPos.offset);
|
||||
newRange.setEnd(endPos.node, endPos.offset);
|
||||
}
|
||||
|
||||
if (newRange.collapsed) {
|
||||
return null;
|
||||
}
|
||||
return newRange;
|
||||
} catch (e) {
|
||||
console.warn('Failed to create range:', e);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
[findPositionAtPoint],
|
||||
);
|
||||
|
||||
const createAnnotation = useCallback((cfi: string, text?: string) => {
|
||||
const style = settings.globalReadSettings.highlightStyle;
|
||||
const color = settings.globalReadSettings.highlightStyles[style];
|
||||
const annotation: BookNote = {
|
||||
id: annotationIdRef.current,
|
||||
type: 'annotation',
|
||||
cfi,
|
||||
style,
|
||||
color,
|
||||
text: text ?? '',
|
||||
note: '',
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
return annotation;
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const handleInstantAnnotationPointerDown = useCallback(
|
||||
(doc: Document, index: number, ev: PointerEvent) => {
|
||||
if (!isInstantAnnotationEnabled()) return false;
|
||||
|
||||
// Only handle primary button (left click / touch / stylus)
|
||||
if (ev.button !== 0) return false;
|
||||
|
||||
if (!isSelectableContent(doc, ev.clientX, ev.clientY)) return false;
|
||||
|
||||
startPointRef.current = { x: ev.clientX, y: ev.clientY };
|
||||
startDocRef.current = doc;
|
||||
startIndexRef.current = index;
|
||||
previewAnnotationRef.current = null;
|
||||
annotationIdRef.current = uniqueId();
|
||||
return true;
|
||||
},
|
||||
[isInstantAnnotationEnabled, isSelectableContent],
|
||||
);
|
||||
|
||||
const handleInstantAnnotationPointerMove = useCallback(
|
||||
(doc: Document, index: number, ev: PointerEvent) => {
|
||||
if (!isInstantAnnotationEnabled()) return false;
|
||||
|
||||
const view = getView(bookKey);
|
||||
if (!startPointRef.current || !startDocRef.current || !view) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const endPoint: Point = { x: ev.clientX, y: ev.clientY };
|
||||
const startPoint = startPointRef.current;
|
||||
|
||||
const deltaX = Math.abs(endPoint.x - startPoint.x);
|
||||
const deltaY = Math.abs(endPoint.y - startPoint.y);
|
||||
const distance = Math.sqrt(Math.pow(deltaX, 2) + Math.pow(deltaY, 2));
|
||||
// need a longer horizontal or vertical drag to avoid accidental selections
|
||||
if (distance < 20 || (deltaX / deltaY < 5 && deltaY / deltaX < 5 && distance < 30)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const newRange = createRangeFromPoints(doc, startPoint, endPoint);
|
||||
if (!newRange) return false;
|
||||
|
||||
const cfi = view.getCFI(index, newRange);
|
||||
if (!cfi) return false;
|
||||
|
||||
clearPreviewAnnotation();
|
||||
const annotation = createAnnotation(cfi);
|
||||
const views = getViewsById(bookKey.split('-')[0]!);
|
||||
views.forEach((v) => v?.addAnnotation(annotation));
|
||||
previewAnnotationRef.current = annotation;
|
||||
|
||||
return true;
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[isInstantAnnotationEnabled, createRangeFromPoints],
|
||||
);
|
||||
|
||||
const handleInstantAnnotationPointerCancel = useCallback(() => {
|
||||
if (!isInstantAnnotationEnabled()) return false;
|
||||
|
||||
startPointRef.current = null;
|
||||
startDocRef.current = null;
|
||||
clearPreviewAnnotation();
|
||||
return true;
|
||||
}, [isInstantAnnotationEnabled, clearPreviewAnnotation]);
|
||||
|
||||
const handleInstantAnnotationPointerUp = useCallback(
|
||||
async (doc: Document, index: number, ev: PointerEvent) => {
|
||||
if (!isInstantAnnotationEnabled()) return false;
|
||||
|
||||
const view = getView(bookKey);
|
||||
if (!startPointRef.current || !view) {
|
||||
startPointRef.current = null;
|
||||
startDocRef.current = null;
|
||||
clearPreviewAnnotation();
|
||||
return false;
|
||||
}
|
||||
|
||||
const endPoint: Point = { x: ev.clientX, y: ev.clientY };
|
||||
const startPoint = startPointRef.current;
|
||||
|
||||
startPointRef.current = null;
|
||||
startDocRef.current = null;
|
||||
|
||||
const distance = Math.sqrt(
|
||||
Math.pow(endPoint.x - startPoint.x, 2) + Math.pow(endPoint.y - startPoint.y, 2),
|
||||
);
|
||||
if (distance < 10) {
|
||||
clearPreviewAnnotation();
|
||||
return false;
|
||||
}
|
||||
|
||||
const newRange = createRangeFromPoints(doc, startPoint, endPoint);
|
||||
if (!newRange) {
|
||||
clearPreviewAnnotation();
|
||||
return false;
|
||||
}
|
||||
|
||||
const text = await getAnnotationText(newRange);
|
||||
const cfi = view.getCFI(index, newRange);
|
||||
|
||||
if (!text || !cfi || text.trim().length === 0) {
|
||||
clearPreviewAnnotation();
|
||||
return false;
|
||||
}
|
||||
|
||||
clearPreviewAnnotation();
|
||||
const annotation = createAnnotation(cfi, text);
|
||||
const views = getViewsById(bookKey.split('-')[0]!);
|
||||
views.forEach((v) => v?.addAnnotation(annotation));
|
||||
|
||||
const config = getConfig(bookKey)!;
|
||||
const { booknotes: annotations = [] } = config;
|
||||
const existingIndex = annotations.findIndex(
|
||||
(a) => a.cfi === cfi && a.type === 'annotation' && a.style && !a.deletedAt,
|
||||
);
|
||||
|
||||
if (existingIndex !== -1) {
|
||||
annotations[existingIndex] = {
|
||||
...annotations[existingIndex]!,
|
||||
...annotation,
|
||||
id: annotations[existingIndex]!.id,
|
||||
};
|
||||
} else {
|
||||
annotations.push(annotation);
|
||||
}
|
||||
|
||||
const updatedConfig = updateBooknotes(bookKey, annotations);
|
||||
if (updatedConfig) {
|
||||
saveConfig(envConfig, bookKey, updatedConfig, settings);
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[isInstantAnnotationEnabled, createRangeFromPoints, getAnnotationText, clearPreviewAnnotation],
|
||||
);
|
||||
|
||||
const cancelInstantAnnotation = useCallback(() => {
|
||||
startPointRef.current = null;
|
||||
startDocRef.current = null;
|
||||
clearPreviewAnnotation();
|
||||
}, [clearPreviewAnnotation]);
|
||||
|
||||
return {
|
||||
isInstantAnnotationEnabled,
|
||||
handleInstantAnnotationPointerDown,
|
||||
handleInstantAnnotationPointerMove,
|
||||
handleInstantAnnotationPointerCancel,
|
||||
handleInstantAnnotationPointerUp,
|
||||
cancelInstantAnnotation,
|
||||
};
|
||||
};
|
||||
@@ -4,6 +4,7 @@ import { useReaderStore } from '@/store/readerStore';
|
||||
import { getOSPlatform } from '@/utils/misc';
|
||||
import { eventDispatcher } from '@/utils/event';
|
||||
import { isPointerInsideSelection, TextSelection } from '@/utils/sel';
|
||||
import { useInstantAnnotation } from './useInstantAnnotation';
|
||||
|
||||
export const useTextSelector = (
|
||||
bookKey: string,
|
||||
@@ -22,6 +23,16 @@ export const useTextSelector = (
|
||||
const isTouchStarted = useRef(false);
|
||||
const selectionPosition = useRef<number | null>(null);
|
||||
const lastPointerType = useRef<string>('mouse');
|
||||
const isInstantAnnotating = useRef(false);
|
||||
const isInstantAnnotated = useRef(false);
|
||||
|
||||
const {
|
||||
isInstantAnnotationEnabled,
|
||||
handleInstantAnnotationPointerDown,
|
||||
handleInstantAnnotationPointerMove,
|
||||
handleInstantAnnotationPointerCancel,
|
||||
handleInstantAnnotationPointerUp,
|
||||
} = useInstantAnnotation({ bookKey, getAnnotationText, setSelection });
|
||||
|
||||
const isValidSelection = (sel: Selection) => {
|
||||
return sel && sel.toString().trim().length > 0 && sel.rangeCount > 0;
|
||||
@@ -64,10 +75,65 @@ export const useTextSelector = (
|
||||
}, 30);
|
||||
};
|
||||
|
||||
const handlePointerdown = (e: PointerEvent) => {
|
||||
lastPointerType.current = e.pointerType;
|
||||
const startInstantAnnotating = (ev: PointerEvent) => {
|
||||
isInstantAnnotating.current = true;
|
||||
isInstantAnnotated.current = false;
|
||||
if (view) view.renderer.scrollLocked = true;
|
||||
(ev.target as HTMLElement).style.userSelect = 'none';
|
||||
};
|
||||
const handlePointerup = (doc: Document, index: number, ev?: PointerEvent) => {
|
||||
|
||||
const stopInstantAnnotating = (ev: PointerEvent) => {
|
||||
isInstantAnnotating.current = false;
|
||||
isInstantAnnotated.current = false;
|
||||
if (view) view.renderer.scrollLocked = false;
|
||||
(ev.target as HTMLElement).style.userSelect = '';
|
||||
};
|
||||
|
||||
const handlePointerDown = (doc: Document, index: number, ev: PointerEvent) => {
|
||||
lastPointerType.current = ev.pointerType;
|
||||
|
||||
if (isInstantAnnotationEnabled()) {
|
||||
const handled = handleInstantAnnotationPointerDown(doc, index, ev);
|
||||
if (handled) {
|
||||
ev.preventDefault();
|
||||
startInstantAnnotating(ev);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handlePointerMove = (doc: Document, index: number, ev: PointerEvent) => {
|
||||
if (isInstantAnnotating.current) {
|
||||
ev.preventDefault();
|
||||
isInstantAnnotated.current = handleInstantAnnotationPointerMove(doc, index, ev);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePointerCancel = (_doc: Document, _index: number, ev: PointerEvent) => {
|
||||
if (isInstantAnnotating.current) {
|
||||
stopInstantAnnotating(ev);
|
||||
handleInstantAnnotationPointerCancel();
|
||||
}
|
||||
};
|
||||
|
||||
const handlePointerUp = async (doc: Document, index: number, ev?: PointerEvent) => {
|
||||
if (isInstantAnnotating.current && ev) {
|
||||
stopInstantAnnotating(ev);
|
||||
const handled = await handleInstantAnnotationPointerUp(doc, index, ev);
|
||||
if (handled) {
|
||||
return;
|
||||
} else {
|
||||
// If instant annotation was not created, we let the event propagate
|
||||
// as an iframe click event which relies on a mousedown event
|
||||
(ev.target as Element)?.dispatchEvent(
|
||||
new MouseEvent('mousedown', {
|
||||
...ev,
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Available on iOS and Desktop, fired at touchend or mouseup
|
||||
// Note that on Android, we mock pointer events with native touch events
|
||||
const sel = doc.getSelection() as Selection;
|
||||
@@ -86,6 +152,11 @@ export const useTextSelector = (
|
||||
const handleTouchStart = () => {
|
||||
isTouchStarted.current = true;
|
||||
};
|
||||
const handleTouchMove = (ev: TouchEvent) => {
|
||||
if (isInstantAnnotating.current && isInstantAnnotated.current) {
|
||||
ev.preventDefault();
|
||||
}
|
||||
};
|
||||
const handleTouchEnd = () => {
|
||||
isTouchStarted.current = false;
|
||||
};
|
||||
@@ -173,9 +244,12 @@ export const useTextSelector = (
|
||||
isTextSelected,
|
||||
handleScroll,
|
||||
handleTouchStart,
|
||||
handleTouchMove,
|
||||
handleTouchEnd,
|
||||
handlePointerdown,
|
||||
handlePointerup,
|
||||
handlePointerDown,
|
||||
handlePointerMove,
|
||||
handlePointerCancel,
|
||||
handlePointerUp,
|
||||
handleSelectionchange,
|
||||
handleShowPopup,
|
||||
handleUpToPopup,
|
||||
|
||||
@@ -48,6 +48,7 @@ export interface FoliateView extends HTMLElement {
|
||||
};
|
||||
renderer: {
|
||||
scrolled?: boolean;
|
||||
scrollLocked: boolean;
|
||||
size: number; // current page height
|
||||
viewSize: number; // whole document view height
|
||||
start: number;
|
||||
|
||||
+1
-1
Submodule packages/foliate-js updated: 4eb5ca42a4...ffb8248449
Reference in New Issue
Block a user