feat(annotator): support editing text range of highlights (#2870)
This commit is contained in:
@@ -0,0 +1,218 @@
|
||||
import clsx from 'clsx';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { BookNote } from '@/types/book';
|
||||
import { Point, TextSelection } from '@/utils/sel';
|
||||
import { useSettingsStore } from '@/store/settingsStore';
|
||||
import { useAnnotationEditor } from '../../hooks/useAnnotationEditor';
|
||||
import { getHighlightColorHex } from '../../utils/annotatorUtil';
|
||||
|
||||
interface HandleProps {
|
||||
position: Point;
|
||||
type: 'start' | 'end';
|
||||
color: string;
|
||||
onDragStart: () => void;
|
||||
onDrag: (point: Point) => void;
|
||||
onDragEnd: () => void;
|
||||
}
|
||||
|
||||
const Handle: React.FC<HandleProps> = ({
|
||||
position,
|
||||
type,
|
||||
color,
|
||||
onDragStart,
|
||||
onDrag,
|
||||
onDragEnd,
|
||||
}) => {
|
||||
const isDragging = useRef(false);
|
||||
|
||||
const handlePointerDown = useCallback(
|
||||
(e: React.PointerEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
isDragging.current = true;
|
||||
onDragStart();
|
||||
(e.target as HTMLElement).setPointerCapture(e.pointerId);
|
||||
},
|
||||
[onDragStart],
|
||||
);
|
||||
|
||||
const handlePointerMove = useCallback(
|
||||
(e: React.PointerEvent) => {
|
||||
if (!isDragging.current) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onDrag({ x: e.clientX, y: e.clientY });
|
||||
},
|
||||
[onDrag],
|
||||
);
|
||||
|
||||
const handlePointerUp = useCallback(
|
||||
(e: React.PointerEvent) => {
|
||||
if (!isDragging.current) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
isDragging.current = false;
|
||||
(e.target as HTMLElement).releasePointerCapture(e.pointerId);
|
||||
onDragEnd();
|
||||
},
|
||||
[onDragEnd],
|
||||
);
|
||||
|
||||
const size = 24;
|
||||
const circleRadius = 8;
|
||||
const stemHeight = 8;
|
||||
|
||||
return (
|
||||
<div
|
||||
className='pointer-events-auto absolute z-50 cursor-grab touch-none active:cursor-grabbing'
|
||||
style={{
|
||||
left: position.x - size / 2,
|
||||
top: type === 'start' ? position.y - size : position.y - size / 2,
|
||||
width: size,
|
||||
height: size + stemHeight,
|
||||
}}
|
||||
onPointerDown={handlePointerDown}
|
||||
onPointerMove={handlePointerMove}
|
||||
onPointerUp={handlePointerUp}
|
||||
onPointerCancel={handlePointerUp}
|
||||
>
|
||||
<svg
|
||||
width={size}
|
||||
height={size + stemHeight}
|
||||
viewBox={`0 0 ${size} ${size + stemHeight}`}
|
||||
className={clsx(type === 'start' && 'rotate-180')}
|
||||
style={{ transform: type === 'start' ? 'rotate(180deg)' : undefined }}
|
||||
>
|
||||
{/* Stem/line */}
|
||||
<line
|
||||
x1={size / 2}
|
||||
y1={0}
|
||||
x2={size / 2}
|
||||
y2={stemHeight}
|
||||
stroke={color}
|
||||
strokeWidth={2}
|
||||
strokeLinecap='round'
|
||||
/>
|
||||
{/* Circle handle */}
|
||||
<circle cx={size / 2} cy={stemHeight + circleRadius} r={circleRadius} fill={color} />
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface AnnotationRangeEditorProps {
|
||||
bookKey: string;
|
||||
annotation: BookNote;
|
||||
selection: TextSelection;
|
||||
getAnnotationText: (range: Range) => Promise<string>;
|
||||
setSelection: React.Dispatch<React.SetStateAction<TextSelection | null>>;
|
||||
onStartEdit: () => void;
|
||||
}
|
||||
|
||||
const AnnotationRangeEditor: React.FC<AnnotationRangeEditorProps> = ({
|
||||
bookKey,
|
||||
annotation,
|
||||
selection,
|
||||
getAnnotationText,
|
||||
setSelection,
|
||||
onStartEdit,
|
||||
}) => {
|
||||
const { settings } = useSettingsStore();
|
||||
const { handlePositions, getHandlePositionsFromRange, handleAnnotationRangeChange } =
|
||||
useAnnotationEditor({ bookKey, annotation, getAnnotationText, setSelection });
|
||||
|
||||
const initializedRef = useRef(false);
|
||||
const handleColor = getHighlightColorHex(settings, annotation.color) ?? '#FFFF00';
|
||||
const draggingRef = useRef<'start' | 'end' | null>(null);
|
||||
const startRef = useRef<Point>({ x: 0, y: 0 });
|
||||
const endRef = useRef<Point>({ x: 0, y: 0 });
|
||||
const [currentStart, setCurrentStart] = useState<Point>({ x: 0, y: 0 });
|
||||
const [currentEnd, setCurrentEnd] = useState<Point>({ x: 0, y: 0 });
|
||||
|
||||
useEffect(() => {
|
||||
if (initializedRef.current) return;
|
||||
initializedRef.current = true;
|
||||
|
||||
const range = selection.range;
|
||||
const positions = getHandlePositionsFromRange(range);
|
||||
if (positions) {
|
||||
setTimeout(() => {
|
||||
setCurrentStart(positions.start);
|
||||
setCurrentEnd(positions.end);
|
||||
}, 0);
|
||||
startRef.current = positions.start;
|
||||
endRef.current = positions.end;
|
||||
}
|
||||
}, [annotation, selection, getHandlePositionsFromRange]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!handlePositions || draggingRef.current) return;
|
||||
setTimeout(() => {
|
||||
setCurrentStart(handlePositions.start);
|
||||
setCurrentEnd(handlePositions.end);
|
||||
}, 0);
|
||||
startRef.current = handlePositions.start;
|
||||
endRef.current = handlePositions.end;
|
||||
}, [handlePositions]);
|
||||
|
||||
const handleStartDragStart = useCallback(() => {
|
||||
draggingRef.current = 'start';
|
||||
onStartEdit();
|
||||
}, [onStartEdit]);
|
||||
|
||||
const handleEndDragStart = useCallback(() => {
|
||||
draggingRef.current = 'end';
|
||||
onStartEdit();
|
||||
}, [onStartEdit]);
|
||||
|
||||
const handleStartDrag = useCallback(
|
||||
(point: Point) => {
|
||||
setCurrentStart(point);
|
||||
startRef.current = point;
|
||||
handleAnnotationRangeChange(point, endRef.current, true);
|
||||
},
|
||||
[handleAnnotationRangeChange],
|
||||
);
|
||||
|
||||
const handleEndDrag = useCallback(
|
||||
(point: Point) => {
|
||||
setCurrentEnd(point);
|
||||
endRef.current = point;
|
||||
handleAnnotationRangeChange(startRef.current, point, true);
|
||||
},
|
||||
[handleAnnotationRangeChange],
|
||||
);
|
||||
|
||||
const handleDragEnd = useCallback(() => {
|
||||
draggingRef.current = null;
|
||||
handleAnnotationRangeChange(startRef.current, endRef.current, false);
|
||||
}, [handleAnnotationRangeChange]);
|
||||
|
||||
if (currentStart.x === 0 && currentStart.y === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='pointer-events-none fixed inset-0 z-40'>
|
||||
<Handle
|
||||
position={currentStart}
|
||||
type='start'
|
||||
color={handleColor}
|
||||
onDragStart={handleStartDragStart}
|
||||
onDrag={handleStartDrag}
|
||||
onDragEnd={handleDragEnd}
|
||||
/>
|
||||
<Handle
|
||||
position={currentEnd}
|
||||
type='end'
|
||||
color={handleColor}
|
||||
onDragStart={handleEndDragStart}
|
||||
onDrag={handleEndDrag}
|
||||
onDragEnd={handleDragEnd}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AnnotationRangeEditor;
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import { RiDeleteBinLine } from 'react-icons/ri';
|
||||
|
||||
import * as CFI from 'foliate-js/epubcfi.js';
|
||||
@@ -30,6 +30,7 @@ import { TransformContext } from '@/services/transformers/types';
|
||||
import { transformContent } from '@/services/transformService';
|
||||
import { getHighlightColorHex } from '../../utils/annotatorUtil';
|
||||
import { annotationToolButtons } from './AnnotationTools';
|
||||
import AnnotationRangeEditor from './AnnotationRangeEditor';
|
||||
import AnnotationPopup from './AnnotationPopup';
|
||||
import WiktionaryPopup from './WiktionaryPopup';
|
||||
import WikipediaPopup from './WikipediaPopup';
|
||||
@@ -72,6 +73,7 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
const [highlightOptionsVisible, setHighlightOptionsVisible] = useState(false);
|
||||
const [showAnnotationNotes, setShowAnnotationNotes] = useState(false);
|
||||
const [annotationNotes, setAnnotationNotes] = useState<BookNote[]>([]);
|
||||
const [editingAnnotation, setEditingAnnotation] = useState<BookNote | null>(null);
|
||||
|
||||
const [selectedStyle, setSelectedStyle] = useState<HighlightStyle>(
|
||||
settings.globalReadSettings.highlightStyle,
|
||||
@@ -80,6 +82,13 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
settings.globalReadSettings.highlightStyles[selectedStyle],
|
||||
);
|
||||
|
||||
const showingPopup =
|
||||
showAnnotPopup ||
|
||||
showWiktionaryPopup ||
|
||||
showWikipediaPopup ||
|
||||
showDeepLPopup ||
|
||||
showProofreadPopup;
|
||||
|
||||
const popupPadding = useResponsiveSize(10);
|
||||
const trianglePadding = popupPadding * 2 + 6;
|
||||
const maxWidth = window.innerWidth - 2 * popupPadding;
|
||||
@@ -150,6 +159,27 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
setSelectedColor(settings.globalReadSettings.highlightStyles[selectedStyle]);
|
||||
}, [settings.globalReadSettings.highlightStyles, selectedStyle]);
|
||||
|
||||
const transformCtx: TransformContext = useMemo(
|
||||
() => ({
|
||||
bookKey,
|
||||
viewSettings: getViewSettings(bookKey)!,
|
||||
userLocale: getLocale(),
|
||||
content: '',
|
||||
transformers: ['punctuation'],
|
||||
reversePunctuationTransform: true,
|
||||
}),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[],
|
||||
);
|
||||
|
||||
const getAnnotationText = useCallback(
|
||||
async (range: Range) => {
|
||||
transformCtx['content'] = getTextFromRange(range, primaryLang.startsWith('ja') ? ['rt'] : []);
|
||||
return await transformContent(transformCtx);
|
||||
},
|
||||
[primaryLang, transformCtx],
|
||||
);
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const handleDismissPopup = useCallback(
|
||||
throttle(() => {
|
||||
@@ -159,24 +189,11 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
setShowWikipediaPopup(false);
|
||||
setShowDeepLPopup(false);
|
||||
setShowProofreadPopup(false);
|
||||
setEditingAnnotation(null);
|
||||
}, 500),
|
||||
[],
|
||||
);
|
||||
|
||||
const transformCtx: TransformContext = {
|
||||
bookKey,
|
||||
viewSettings: getViewSettings(bookKey)!,
|
||||
userLocale: getLocale(),
|
||||
content: '',
|
||||
transformers: ['punctuation'],
|
||||
reversePunctuationTransform: true,
|
||||
};
|
||||
|
||||
const getAnnotationText = async (range: Range) => {
|
||||
transformCtx['content'] = getTextFromRange(range, primaryLang.startsWith('ja') ? ['rt'] : []);
|
||||
return await transformContent(transformCtx);
|
||||
};
|
||||
|
||||
const {
|
||||
isTextSelected,
|
||||
handleScroll,
|
||||
@@ -327,6 +344,7 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
if (isNote) {
|
||||
setShowAnnotationNotes(true);
|
||||
setHighlightOptionsVisible(false);
|
||||
setEditingAnnotation(null);
|
||||
} else {
|
||||
setShowAnnotationNotes(false);
|
||||
setAnnotationNotes([]);
|
||||
@@ -334,6 +352,9 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
setSelectedStyle(style);
|
||||
setSelectedColor(color);
|
||||
}
|
||||
if (style && range) {
|
||||
setEditingAnnotation(annotation);
|
||||
}
|
||||
}
|
||||
setSelection(selection);
|
||||
handleUpToPopup();
|
||||
@@ -342,28 +363,16 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
useFoliateEvents(view, { onLoad, onDrawAnnotation, onShowAnnotation });
|
||||
|
||||
useEffect(() => {
|
||||
handleShowPopup(
|
||||
showAnnotPopup ||
|
||||
showWiktionaryPopup ||
|
||||
showWikipediaPopup ||
|
||||
showDeepLPopup ||
|
||||
showProofreadPopup,
|
||||
);
|
||||
handleShowPopup(showingPopup);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [showAnnotPopup, showWiktionaryPopup, showWikipediaPopup, showDeepLPopup, showProofreadPopup]);
|
||||
}, [showingPopup]);
|
||||
|
||||
// When popups are visible, update their positions on scroll events
|
||||
useEffect(() => {
|
||||
const view = getView(bookKey);
|
||||
if (!view?.renderer) return;
|
||||
const onScroll = () => {
|
||||
if (
|
||||
showAnnotPopup ||
|
||||
showWiktionaryPopup ||
|
||||
showWikipediaPopup ||
|
||||
showDeepLPopup ||
|
||||
showProofreadPopup
|
||||
) {
|
||||
if (showingPopup) {
|
||||
repositionPopups();
|
||||
}
|
||||
};
|
||||
@@ -372,15 +381,7 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
view.renderer.removeEventListener('scroll', onScroll);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
bookKey,
|
||||
showAnnotPopup,
|
||||
showWiktionaryPopup,
|
||||
showWikipediaPopup,
|
||||
showDeepLPopup,
|
||||
showProofreadPopup,
|
||||
repositionPopups,
|
||||
]);
|
||||
}, [bookKey, showingPopup, repositionPopups]);
|
||||
|
||||
useEffect(() => {
|
||||
eventDispatcher.on('export-annotations', handleExportMarkdown);
|
||||
@@ -614,7 +615,7 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
views.forEach((view) => view?.addAnnotation(annotation));
|
||||
} else {
|
||||
annotations[existingIndex]!.deletedAt = Date.now();
|
||||
setShowAnnotPopup(false);
|
||||
handleDismissPopup();
|
||||
}
|
||||
} else {
|
||||
annotations.push(annotation);
|
||||
@@ -689,6 +690,10 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleStartEditAnnotation = useCallback(() => {
|
||||
setShowAnnotPopup(false);
|
||||
}, []);
|
||||
|
||||
// Keyboard shortcuts: trigger actions only if there's an active selection and popup hidden
|
||||
useShortcuts(
|
||||
{
|
||||
@@ -925,6 +930,16 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
onDismiss={handleDismissPopupAndSelection}
|
||||
/>
|
||||
)}
|
||||
{editingAnnotation && editingAnnotation.color && selection && (
|
||||
<AnnotationRangeEditor
|
||||
bookKey={bookKey}
|
||||
annotation={editingAnnotation}
|
||||
selection={selection}
|
||||
getAnnotationText={getAnnotationText}
|
||||
setSelection={setSelection}
|
||||
onStartEdit={handleStartEditAnnotation}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
import { BookNote } from '@/types/book';
|
||||
import { Point, TextSelection } from '@/utils/sel';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import { useSettingsStore } from '@/store/settingsStore';
|
||||
import { useBookDataStore } from '@/store/bookDataStore';
|
||||
|
||||
interface HandlePositions {
|
||||
start: Point;
|
||||
end: Point;
|
||||
}
|
||||
|
||||
interface UseAnnotationEditorProps {
|
||||
bookKey: string;
|
||||
annotation: BookNote;
|
||||
getAnnotationText: (range: Range) => Promise<string>;
|
||||
setSelection: React.Dispatch<React.SetStateAction<TextSelection | null>>;
|
||||
}
|
||||
|
||||
export const useAnnotationEditor = ({
|
||||
bookKey,
|
||||
annotation,
|
||||
getAnnotationText,
|
||||
setSelection,
|
||||
}: UseAnnotationEditorProps) => {
|
||||
const { envConfig } = useEnv();
|
||||
const { settings } = useSettingsStore();
|
||||
const { getConfig, saveConfig, updateBooknotes } = useBookDataStore();
|
||||
const { getView, getViewsById } = useReaderStore();
|
||||
|
||||
const view = getView(bookKey);
|
||||
const editingAnnotationRef = useRef(annotation);
|
||||
const [handlePositions, setHandlePositions] = useState<HandlePositions | null>(null);
|
||||
|
||||
const getHandlePositionsFromRange = useCallback(
|
||||
(range: Range): HandlePositions | null => {
|
||||
const gridFrame = document.querySelector(`#gridcell-${bookKey}`);
|
||||
if (!gridFrame) return null;
|
||||
|
||||
const rects = Array.from(range.getClientRects());
|
||||
if (rects.length === 0) return null;
|
||||
|
||||
const firstRect = rects[0]!;
|
||||
const lastRect = rects[rects.length - 1]!;
|
||||
const frameElement = range.commonAncestorContainer.ownerDocument?.defaultView?.frameElement;
|
||||
const frameRect = frameElement?.getBoundingClientRect() ?? { top: 0, left: 0 };
|
||||
|
||||
return {
|
||||
start: {
|
||||
x: frameRect.left + firstRect.left,
|
||||
y: frameRect.top + firstRect.top,
|
||||
},
|
||||
end: {
|
||||
x: frameRect.left + lastRect.right,
|
||||
y: frameRect.top + lastRect.bottom,
|
||||
},
|
||||
};
|
||||
},
|
||||
[bookKey],
|
||||
);
|
||||
|
||||
const handleAnnotationRangeChange = useCallback(
|
||||
async (startPoint: Point, endPoint: Point, isDragging: boolean) => {
|
||||
if (!editingAnnotationRef.current || !view) return;
|
||||
|
||||
const contents = view.renderer.getContents();
|
||||
if (!contents || contents.length === 0) return;
|
||||
|
||||
const findPositionAtPoint = (doc: Document, x: number, y: number) => {
|
||||
const frameElement = doc.defaultView?.frameElement;
|
||||
const frameRect = frameElement?.getBoundingClientRect() ?? { top: 0, left: 0 };
|
||||
const adjustedX = x - frameRect.left;
|
||||
const adjustedY = y - frameRect.top;
|
||||
|
||||
if (doc.caretPositionFromPoint) {
|
||||
const pos = doc.caretPositionFromPoint(adjustedX, adjustedY);
|
||||
if (pos) return { node: pos.offsetNode, offset: pos.offset };
|
||||
}
|
||||
if (doc.caretRangeFromPoint) {
|
||||
const range = doc.caretRangeFromPoint(adjustedX, adjustedY);
|
||||
if (range) return { node: range.startContainer, offset: range.startOffset };
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
let startPos = null;
|
||||
let endPos = null;
|
||||
let targetDoc: Document | null = null;
|
||||
let targetIndex = 0;
|
||||
|
||||
for (const content of contents) {
|
||||
const { doc, index } = content;
|
||||
if (!doc) continue;
|
||||
|
||||
const sp = findPositionAtPoint(doc, startPoint.x, startPoint.y);
|
||||
const ep = findPositionAtPoint(doc, endPoint.x, endPoint.y);
|
||||
|
||||
if (sp && ep) {
|
||||
startPos = sp;
|
||||
endPos = ep;
|
||||
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);
|
||||
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) {
|
||||
console.warn('Range is collapsed');
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Failed to create range:', e);
|
||||
return;
|
||||
}
|
||||
|
||||
const newPositions = getHandlePositionsFromRange(newRange);
|
||||
if (newPositions) {
|
||||
setHandlePositions(newPositions);
|
||||
}
|
||||
|
||||
const newCfi = view.getCFI(targetIndex, newRange);
|
||||
const newText = await getAnnotationText(newRange);
|
||||
|
||||
if (newCfi && newText) {
|
||||
const config = getConfig(bookKey)!;
|
||||
const { booknotes: annotations = [] } = config;
|
||||
const existingIndex = annotations.findIndex(
|
||||
(a) => a.id === editingAnnotationRef.current.id && !a.deletedAt,
|
||||
);
|
||||
|
||||
if (existingIndex !== -1) {
|
||||
const updatedAnnotation: BookNote = {
|
||||
...annotations[existingIndex]!,
|
||||
cfi: newCfi,
|
||||
text: newText,
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
|
||||
const views = getViewsById(bookKey.split('-')[0]!);
|
||||
views.forEach((v) => v?.addAnnotation(editingAnnotationRef.current, true));
|
||||
views.forEach((v) => v?.addAnnotation(updatedAnnotation));
|
||||
editingAnnotationRef.current = updatedAnnotation;
|
||||
|
||||
if (!isDragging) {
|
||||
annotations[existingIndex] = updatedAnnotation;
|
||||
const updatedConfig = updateBooknotes(bookKey, annotations);
|
||||
if (updatedConfig) {
|
||||
saveConfig(envConfig, bookKey, updatedConfig, settings);
|
||||
}
|
||||
|
||||
setSelection({
|
||||
key: bookKey,
|
||||
annotated: true,
|
||||
text: newText,
|
||||
cfi: newCfi,
|
||||
range: newRange,
|
||||
index: targetIndex,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[bookKey, getHandlePositionsFromRange, getAnnotationText, setSelection],
|
||||
);
|
||||
|
||||
return {
|
||||
handlePositions,
|
||||
setHandlePositions,
|
||||
getHandlePositionsFromRange,
|
||||
handleAnnotationRangeChange,
|
||||
};
|
||||
};
|
||||
@@ -242,7 +242,7 @@ const getLayoutStyles = (
|
||||
}
|
||||
html, body {
|
||||
${writingMode === 'auto' ? '' : `writing-mode: ${writingMode} !important;`}
|
||||
${vertical ? 'font-feature-settings: "vrt2" 1, "vert" 1; text-orientation: upright;' : ''}
|
||||
${writingMode.includes('vertical') ? 'font-feature-settings: "vrt2" 1, "vert" 1; text-orientation: upright;' : ''}
|
||||
text-align: var(--default-text-align);
|
||||
max-height: unset;
|
||||
-webkit-touch-callout: none;
|
||||
|
||||
Reference in New Issue
Block a user