Add support of popover footnotes

This commit is contained in:
chrox
2024-12-10 14:14:34 +01:00
parent 6efdbb78e5
commit 0cbb950c37
9 changed files with 160 additions and 21 deletions
@@ -12,6 +12,7 @@ import Ribbon from './Ribbon';
import SettingsDialog from './settings/SettingsDialog';
import Annotator from './annotator/Annotator';
import { useBookDataStore } from '@/store/bookDataStore';
import FootnotePopup from './FootnotePopup';
interface BooksGridProps {
bookKeys: string[];
@@ -60,6 +61,7 @@ const BooksGrid: React.FC<BooksGridProps> = ({ bookKeys, onCloseBook }) => {
onSetSettingsDialogOpen={setFontLayoutSettingsDialogOpen}
/>
<FoliateViewer bookKey={bookKey} bookDoc={bookDoc} config={config} />
<FootnotePopup bookKey={bookKey} bookDoc={bookDoc} />
{viewSettings.scrolled ? null : (
<>
<SectionInfo section={sectionLabel} gapLeft={marginGap} />
@@ -0,0 +1,132 @@
import React, { useEffect, useRef, useState } from 'react';
import { BookDoc } from '@/libs/document';
import { useReaderStore } from '@/store/readerStore';
import { useFoliateEvents } from '../hooks/useFoliateEvents';
import { useTheme } from '@/hooks/useTheme';
import { getStyles } from '@/utils/style';
import { getPopupPosition, getPosition, Position } from '@/utils/sel';
import { FoliateView } from './FoliateViewer';
import { FootnoteHandler } from 'foliate-js/footnotes.js';
import Popup from '@/components/Popup';
interface FootnotePopupProps {
bookKey: string;
bookDoc: BookDoc;
}
const popupWidth = 360;
const popupHeight = 88;
const popupPadding = 10;
const FootnotePopup: React.FC<FootnotePopupProps> = ({ bookKey, bookDoc }) => {
const footnoteRef = useRef<HTMLDivElement>(null);
const footnoteViewRef = useRef<FoliateView | null>(null);
const [trianglePosition, setTrianglePosition] = useState<Position | null>();
const [popupPosition, setPopupPosition] = useState<Position | null>();
const [showPopup, setShowPopup] = useState(false);
const { getView, getViewSettings } = useReaderStore();
const { themeCode } = useTheme();
const view = getView(bookKey);
const footnoteHandler = new FootnoteHandler();
useEffect(() => {
const handleBeforeRender = (e: Event) => {
const detail = (e as CustomEvent).detail;
const { view } = detail;
footnoteViewRef.current = view;
footnoteRef.current?.replaceChildren(view);
const { renderer } = view;
renderer.setAttribute('flow', 'scrolled');
renderer.setAttribute('margin', '0px');
renderer.setAttribute('gap', '5%');
const viewSettings = getViewSettings(bookKey)!;
renderer.setStyles?.(getStyles(viewSettings, themeCode));
};
const handleRender = (e: Event) => {
const detail = (e as CustomEvent).detail;
console.log('render footnote', detail);
setShowPopup(true);
};
footnoteHandler.addEventListener('before-render', handleBeforeRender);
footnoteHandler.addEventListener('render', handleRender);
return () => {
footnoteHandler.removeEventListener('before-render', handleBeforeRender);
footnoteHandler.removeEventListener('render', handleRender);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [view]);
const docLinkHandler = async (event: Event) => {
const detail = (event as CustomEvent).detail;
console.log('doc link click', detail);
const gridFrame = document.querySelector(`#gridcell-${bookKey}`);
if (!gridFrame) return;
const rect = gridFrame.getBoundingClientRect();
const triangPos = getPosition(detail.a, rect);
const popupPos = getPopupPosition(triangPos, rect, popupWidth, popupHeight, popupPadding);
setTrianglePosition(triangPos);
setPopupPosition(popupPos);
footnoteHandler.handle(bookDoc, event)?.catch((err) => {
console.warn(err);
const detail = (event as CustomEvent).detail;
view?.goTo(detail.href);
});
};
const closePopup = () => {
const view = footnoteRef.current?.querySelector('foliate-view') as FoliateView;
view?.close();
view?.remove();
};
const handleDismissPopup = () => {
closePopup();
setPopupPosition(null);
setTrianglePosition(null);
setShowPopup(false);
};
useFoliateEvents(view, {
onLinkClick: docLinkHandler,
});
useEffect(() => {
if (footnoteViewRef.current) {
footnoteRef.current?.replaceChildren(footnoteViewRef.current);
}
}, [footnoteRef]);
return (
<div>
{showPopup && (
<div
className='fixed inset-0'
onClick={handleDismissPopup}
onContextMenu={handleDismissPopup}
/>
)}
<Popup
width={popupWidth}
height={popupHeight}
position={showPopup ? popupPosition! : undefined}
trianglePosition={showPopup ? trianglePosition! : undefined}
className='select-text overflow-y-auto'
>
<div
className=''
ref={footnoteRef}
style={{
width: `${popupWidth}px`,
height: `${popupHeight}px`,
}}
></div>
</Popup>
</div>
);
};
export default FootnotePopup;
@@ -120,8 +120,7 @@ const DeepLPopup: React.FC<DeepLPopupProps> = ({
width={popupWidth}
height={popupHeight}
position={position}
className='bg-neutral select-text'
triangleClassName='text-neutral'
className='select-text'
>
<div className='text-neutral-content relative h-[50%] overflow-y-auto border-b border-neutral-400/75 p-4 font-sans'>
<div className='mb-2 flex items-center justify-between'>
@@ -107,8 +107,7 @@ const WikipediaPopup: React.FC<WikipediaPopupProps> = ({
height={popupHeight}
position={position}
trianglePosition={trianglePosition}
className='bg-neutral select-text overflow-y-auto'
triangleClassName='text-neutral'
className='select-text overflow-y-auto'
>
<main className='p-2 font-sans'></main>
<footer className='hidden data-[state=loaded]:block data-[state=error]:hidden data-[state=loading]:hidden'>
@@ -163,8 +163,7 @@ const WiktionaryPopup: React.FC<WiktionaryPopupProps> = ({
width={popupWidth}
height={popupHeight}
position={position}
className='bg-neutral select-text overflow-y-auto'
triangleClassName='text-neutral'
className='select-text overflow-y-auto'
>
<main className='p-4 font-sans' />
<footer className='hidden data-[state=loaded]:block data-[state=error]:hidden data-[state=loading]:hidden'>
@@ -104,7 +104,7 @@ export const handleClick = (bookKey: string, event: MouseEvent) => {
if (Date.now() - lastClickTime >= doubleClickThreshold) {
let element: HTMLElement | null = event.target as HTMLElement;
while (element) {
if (['a', 'audio', 'video'].includes(element.tagName.toLowerCase())) {
if (['sup', 'a', 'audio', 'video'].includes(element.tagName.toLowerCase())) {
return;
}
element = element.parentElement;