diff --git a/apps/readest-app/src/app/reader/components/annotator/AnnotationPopup.tsx b/apps/readest-app/src/app/reader/components/annotator/AnnotationPopup.tsx new file mode 100644 index 00000000..089ebc85 --- /dev/null +++ b/apps/readest-app/src/app/reader/components/annotator/AnnotationPopup.tsx @@ -0,0 +1,88 @@ +import React from 'react'; +import PopupButton from './PopupButton'; +import HighlightOptions from './HighlightOptions'; +import { Position } from '@/utils/sel'; +import { HighlightColor, HighlightStyle } from '@/types/book'; + +interface AnnotationPopupProps { + buttons: Array<{ tooltipText: string; Icon: React.ElementType; onClick: () => void }>; + position: Position; + trianglePosition: Position; + highlightOptionsVisible: boolean; + selectedStyle: HighlightStyle; + selectedColor: HighlightColor; + popupWidth: number; + popupHeight: number; + onHighlight: (update?: boolean) => void; +} + +const highlightOptionsHeightPx = 28; +const highlightOptionsPaddingPx = 16; + +const AnnotationPopup: React.FC = ({ + buttons, + position, + trianglePosition, + highlightOptionsVisible, + selectedStyle, + selectedColor, + popupWidth, + popupHeight, + onHighlight, +}) => { + const isPopupAbove = trianglePosition.dir === 'up'; + return ( +
+
+
+
+ {buttons.map((button, index) => ( + + ))} +
+
+ {highlightOptionsVisible && ( + + )} +
+ ); +}; + +export default AnnotationPopup; diff --git a/apps/readest-app/src/app/reader/components/annotator/Annotator.tsx b/apps/readest-app/src/app/reader/components/annotator/Annotator.tsx index 85ec6488..e86ec927 100644 --- a/apps/readest-app/src/app/reader/components/annotator/Annotator.tsx +++ b/apps/readest-app/src/app/reader/components/annotator/Annotator.tsx @@ -4,17 +4,17 @@ import { FiCopy } from 'react-icons/fi'; import { PiHighlighterFill } from 'react-icons/pi'; import { FaWikipediaW } from 'react-icons/fa'; import { BsPencilSquare } from 'react-icons/bs'; -import { RiDeleteBinFill } from 'react-icons/ri'; +import { RiDeleteBinLine } from 'react-icons/ri'; +import { Overlayer } from 'foliate-js/overlayer.js'; import { useEnv } from '@/context/EnvContext'; import { BookNote, HighlightColor, HighlightStyle } from '@/types/book'; import { useReaderStore } from '@/store/readerStore'; import { useFoliateEvents } from '../../hooks/useFoliateEvents'; -import { getPosition, Position } from '@/utils/sel'; +import { getPopupPosition, getPosition, Position } from '@/utils/sel'; +import Toast from '@/components/Toast'; import useOutsideClick from '@/hooks/useOutsideClick'; -import PopupButton from './PopupButton'; -import HighlightOptions from './HighlightOptions'; -import { Overlayer } from 'foliate-js/overlayer.js'; +import AnnotationPopup from './AnnotationPopup'; interface TextSelection { annotated?: boolean; @@ -34,7 +34,6 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => { const [selection, setSelection] = useState(); const [showPopup, setShowPopup] = useState(false); - const [isPopupAbove, setIsPopupAbove] = useState(false); const [trianglePosition, setTrianglePosition] = useState(); const [popupPosition, setPopupPosition] = useState(); const [toastMessage, setToastMessage] = useState(''); @@ -47,28 +46,23 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => { globalReadSettings.highlightStyles[selectedStyle], ); - const popupWidthPx = 240; - const popupHeightPx = 44; - const popupPaddingPx = 10; - const highlightOptionsHeightPx = 28; - const highlightOptionsPaddingPx = 16; + const popupWidth = 240; + const popupHeight = 44; + const popupPadding = 10; - const docLoadHandler = (event: Event) => { + const onLoad = (event: Event) => { const detail = (event as CustomEvent).detail; const { doc, index } = detail; - const handlePointerup = () => { const sel = doc.getSelection(); if (sel && sel.toString().trim().length > 0) { setSelection({ text: sel.toString(), range: sel.getRangeAt(0), index }); } }; - if (detail.doc) { - detail.doc.addEventListener('pointerup', handlePointerup); - } + detail.doc?.addEventListener('pointerup', handlePointerup); }; - const drawAnnotationHandler = (event: Event) => { + const onDrawAnnotation = (event: Event) => { const detail = (event as CustomEvent).detail; const { draw, annotation, doc, range } = detail; const { style, color } = annotation as BookNote; @@ -83,33 +77,20 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => { } }; - const showAnnotationHandler = (event: Event) => { + const onShowAnnotation = (event: Event) => { const detail = (event as CustomEvent).detail; const { value: cfi, index, range } = detail; const { booknotes = [] } = config; const annotations = booknotes.filter((booknote) => booknote.type === 'annotation'); const annotation = annotations.find((annotation) => annotation.cfi === cfi); if (!annotation) return; - const selection = { - annotated: true, - text: annotation.text, - range, - index, - }; + const selection = { annotated: true, text: annotation.text, range, index }; setSelectedStyle(annotation.style!); setSelectedColor(annotation.color!); setSelection(selection as TextSelection); }; - useFoliateEvents( - view, - { - onLoad: docLoadHandler, - onDrawAnnotation: drawAnnotationHandler, - onShowAnnotation: showAnnotationHandler, - }, - [bookState], - ); + useFoliateEvents(view, { onLoad, onDrawAnnotation, onShowAnnotation }, [bookState]); const popupRef = useOutsideClick(() => { setShowPopup(false); @@ -121,42 +102,12 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => { if (selection && selection.text.trim().length > 0) { const gridFrame = document.querySelector(`#gridcell-${bookKey}`); if (!gridFrame) return; - const offset = { - x: gridFrame.getBoundingClientRect().left, - y: gridFrame.getBoundingClientRect().top, - }; - const gridFrameRect = gridFrame.getBoundingClientRect(); - const position = getPosition(selection.range, offset); - - if (position.dir === 'up') { - position.point.y -= 12; - setIsPopupAbove(true); - } else { - position.point.y += 0; - setIsPopupAbove(false); - } - - const popupPoint = { - x: position.point.x - popupWidthPx / 2, - y: position.dir === 'up' ? position.point.y - popupHeightPx : position.point.y + 6, - }; - - if (popupPoint.x + popupWidthPx > gridFrameRect.right - offset.x - popupPaddingPx) { - popupPoint.x = gridFrameRect.right - offset.x - popupPaddingPx - popupWidthPx; - } - if (popupPoint.x < gridFrameRect.left - offset.x + popupPaddingPx) { - popupPoint.x = gridFrameRect.left - offset.x + popupPaddingPx; - } - if (popupPoint.y + popupHeightPx > gridFrameRect.bottom - offset.y - popupPaddingPx) { - popupPoint.y = gridFrameRect.bottom - offset.y - popupPaddingPx - popupHeightPx; - } - if (popupPoint.y < gridFrameRect.top - offset.y + popupPaddingPx) { - popupPoint.y = gridFrameRect.top - offset.y + popupPaddingPx; - } - - setPopupPosition({ point: popupPoint }); - setTrianglePosition(position); + const rect = gridFrame.getBoundingClientRect(); + const triangPos = getPosition(selection.range, rect); + const popupPos = getPopupPosition(triangPos, rect, popupWidth, popupHeight, popupPadding); setShowPopup(true); + setPopupPosition(popupPos); + setTrianglePosition(triangPos); } }, [selection, bookKey]); @@ -178,16 +129,13 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => { const { tocHref: href } = progress; const cfi = view?.getCFI(selection.index, selection.range); if (!cfi) return; - const annotation: BookNote = { - type: 'annotation', - cfi, - href, - style: globalReadSettings.highlightStyle, - color: globalReadSettings.highlightStyles[globalReadSettings.highlightStyle], - text: selection.text, - note: '', - created: Date.now(), - }; + const style = globalReadSettings.highlightStyle; + const color = globalReadSettings.highlightStyles[style]; + const text = selection.text; + const type = 'annotation'; + const created = Date.now(); + const note = ''; + const annotation: BookNote = { type, cfi, href, style, color, text, note, created }; const existingIndex = annotations.findIndex( (annotation) => annotation.cfi === cfi && annotation.type === 'annotation', ); @@ -203,14 +151,12 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => { } else { annotations.push(annotation); view?.addAnnotation(annotation); + setSelection({ ...selection, annotated: true }); } const dedupedAnnotations = Array.from( - new Map( - annotations.map((annotation) => [`${annotation.type}-${annotation.cfi}`, annotation]), - ).values(), + new Map(annotations.map((item) => [`${item.type}-${item.cfi}`, item])).values(), ); - const updatedConfig = updateBooknotes(bookKey, dedupedAnnotations); if (updatedConfig) { saveConfig(envConfig, bookKey, updatedConfig, settings); @@ -225,7 +171,7 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => { { tooltipText: 'Copy', Icon: FiCopy, onClick: handleCopy }, { tooltipText: selectionAnnotated ? 'Delete Highlight' : 'Highlight', - Icon: selectionAnnotated ? RiDeleteBinFill : PiHighlighterFill, + Icon: selectionAnnotated ? RiDeleteBinLine : PiHighlighterFill, onClick: handleHighlight, }, { tooltipText: 'Annotate', Icon: BsPencilSquare, onClick: handleAnnotate }, @@ -236,64 +182,19 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => { return (
{showPopup && trianglePosition && popupPosition && ( -
-
-
-
- {buttons.map((button, index) => ( - - ))} -
-
- {highlightOptionsVisible && ( - - )} -
- )} - {toastMessage && ( -
-
- {toastMessage} -
-
+ )} + {toastMessage && }
); }; diff --git a/apps/readest-app/src/app/reader/components/annotator/PopupButton.tsx b/apps/readest-app/src/app/reader/components/annotator/PopupButton.tsx index d89648f9..3a5d814d 100644 --- a/apps/readest-app/src/app/reader/components/annotator/PopupButton.tsx +++ b/apps/readest-app/src/app/reader/components/annotator/PopupButton.tsx @@ -14,7 +14,10 @@ const PopupButton: React.FC = ({ tooltipText, Icon, onClick }) }; return (
-
diff --git a/apps/readest-app/src/components/Toast.tsx b/apps/readest-app/src/components/Toast.tsx new file mode 100644 index 00000000..172f042f --- /dev/null +++ b/apps/readest-app/src/components/Toast.tsx @@ -0,0 +1,11 @@ +import React from 'react'; + +const Toast: React.FC<{ message: string }> = ({ message }) => ( +
+
+ {message} +
+
+); + +export default Toast; diff --git a/apps/readest-app/src/utils/sel.ts b/apps/readest-app/src/utils/sel.ts index 3cffdffd..06446e01 100644 --- a/apps/readest-app/src/utils/sel.ts +++ b/apps/readest-app/src/utils/sel.ts @@ -49,7 +49,7 @@ const getIframeElement = (range: Range): HTMLIFrameElement | null => { return null; }; -export const getPosition = (target: Range, offset = { x: 0, y: 0 }) => { +export const getPosition = (target: Range, rect: Rect) => { // TODO: vertical text const frameElement = getIframeElement(target); const transform = frameElement ? getComputedStyle(frameElement).transform : ''; @@ -61,11 +61,11 @@ export const getPosition = (target: Range, offset = { x: 0, y: 0 }) => { const first = frameRect(frame, rects[0] as Rect, sx, sy); const last = frameRect(frame, rects.at(-1) as Rect, sx, sy); const start = { - point: { x: (first.left + first.right) / 2 - offset.x, y: first.top - offset.y }, + point: { x: (first.left + first.right) / 2 - rect.left, y: first.top - rect.top - 12 }, dir: 'up', } as Position; const end = { - point: { x: (last.left + last.right) / 2 - offset.x, y: last.bottom - offset.y }, + point: { x: (last.left + last.right) / 2 - rect.left, y: last.bottom - rect.top }, dir: 'down', } as Position; const startInView = pointIsInView(start.point); @@ -75,3 +75,31 @@ export const getPosition = (target: Range, offset = { x: 0, y: 0 }) => { if (!endInView) return start; return start.point.y > window.innerHeight - end.point.y ? start : end; }; + +export const getPopupPosition = ( + position: Position, + boundingReact: Rect, + popupWidthPx: number, + popupHeightPx: number, + popupPaddingPx: number, +) => { + const popupPoint = { + x: position.point.x - popupWidthPx / 2, + y: position.dir === 'up' ? position.point.y - popupHeightPx : position.point.y + 6, + }; + + if (popupPoint.x < popupPaddingPx) { + popupPoint.x = popupPaddingPx; + } + if (popupPoint.y < popupPaddingPx) { + popupPoint.y = popupPaddingPx; + } + if (popupPoint.x + popupWidthPx > boundingReact.right - boundingReact.left - popupPaddingPx) { + popupPoint.x = boundingReact.right - boundingReact.left - popupPaddingPx - popupWidthPx; + } + if (popupPoint.y + popupHeightPx > boundingReact.bottom - boundingReact.top - popupPaddingPx) { + popupPoint.y = boundingReact.bottom - boundingReact.top - popupPaddingPx - popupHeightPx; + } + + return { point: popupPoint, dir: position.dir } as Position; +};