203 lines
6.5 KiB
TypeScript
203 lines
6.5 KiB
TypeScript
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,
|
|
};
|
|
};
|