Support left / right popover on vertical writing documents

This commit is contained in:
chrox
2024-12-10 17:34:47 +01:00
parent 811f377dc0
commit b57fd8bd3d
12 changed files with 114 additions and 27 deletions
+2 -2
View File
@@ -78,12 +78,12 @@ Stay tuned for continuous improvements and updates! Contributions and suggestion
![Annotations](./data/screenshots/annotations.png)
![Wikipedia](./data/screenshots/wikipedia.png)
![DeepL](./data/screenshots/deepl.png)
![Footnote](./data/screenshots/footnote_popover.png)
![Wikipedia](./data/screenshots/wikipedia_vertical.png)
![Dark Mode](./data/screenshots/dark_mode.png)
---
@@ -1,5 +1,5 @@
import React, { useEffect, useRef } from 'react';
import { BookDoc } from '@/libs/document';
import { BookDoc, getDirection } from '@/libs/document';
import { BookConfig, BookNote, BookSearchConfig, BookSearchResult } from '@/types/book';
import { useReaderStore } from '@/store/readerStore';
import { useParallelViewStore } from '@/store/parallelViewStore';
@@ -76,7 +76,7 @@ const FoliateViewer: React.FC<{
const viewRef = useRef<FoliateView | null>(null);
const isViewCreated = useRef(false);
const { getView, setView: setFoliateView, setProgress, getViewSettings } = useReaderStore();
const { hoveredBookKey, setHoveredBookKey } = useReaderStore();
const { hoveredBookKey, setHoveredBookKey, setViewSettings } = useReaderStore();
const { getParallels } = useParallelViewStore();
const { themeCode } = useTheme();
@@ -120,7 +120,10 @@ const FoliateViewer: React.FC<{
const detail = (event as CustomEvent).detail;
console.log('doc loaded:', detail);
if (detail.doc) {
const writingDir = viewRef.current?.renderer.setStyles && getDirection(detail.doc);
const viewSettings = getViewSettings(bookKey)!;
viewSettings.vertical = writingDir?.vertical || false;
setViewSettings(bookKey, viewSettings);
if (viewSettings.scrolled && shouldAutoHideScrollbar) {
handleScrollbarAutoHide(detail.doc);
}
@@ -65,7 +65,8 @@ const FootnotePopup: React.FC<FootnotePopupProps> = ({ bookKey, bookDoc }) => {
const gridFrame = document.querySelector(`#gridcell-${bookKey}`);
if (!gridFrame) return;
const rect = gridFrame.getBoundingClientRect();
const triangPos = getPosition(detail.a, rect);
const viewSettings = getViewSettings(bookKey)!;
const triangPos = getPosition(detail.a, rect, viewSettings.vertical);
const popupPos = getPopupPosition(triangPos, rect, popupWidth, popupHeight, popupPadding);
setTrianglePosition(triangPos);
setPopupPosition(popupPos);
@@ -30,13 +30,14 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
const { envConfig } = useEnv();
const { settings } = useSettingsStore();
const { getConfig, saveConfig, getBookData, updateBooknotes } = useBookDataStore();
const { getProgress, getView, getViewsById } = useReaderStore();
const { getProgress, getView, getViewsById, getViewSettings } = useReaderStore();
const { isNotebookPinned, isNotebookVisible } = useNotebookStore();
const { setNotebookVisible, setNotebookNewAnnotation } = useNotebookStore();
const config = getConfig(bookKey)!;
const progress = getProgress(bookKey)!;
const bookData = getBookData(bookKey)!;
const view = getView(bookKey);
const viewSettings = getViewSettings(bookKey)!;
const isShowingPopup = useRef(false);
const isTextSelected = useRef(false);
@@ -48,6 +49,7 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
const [trianglePosition, setTrianglePosition] = useState<Position>();
const [annotPopupPosition, setAnnotPopupPosition] = useState<Position>();
const [dictPopupPosition, setDictPopupPosition] = useState<Position>();
const [translatorPopupPosition, setTranslatorPopupPosition] = useState<Position>();
const [toastMessage, setToastMessage] = useState('');
const [highlightOptionsVisible, setHighlightOptionsVisible] = useState(false);
@@ -59,7 +61,9 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
);
const dictPopupWidth = 480;
const dictPopupHeight = 360;
const dictPopupHeight = 300;
const transPopupWidth = 480;
const transPopupHeight = 360;
const annotPopupWidth = 280;
const annotPopupHeight = 44;
const popupPadding = 10;
@@ -148,7 +152,7 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
const gridFrame = document.querySelector(`#gridcell-${bookKey}`);
if (!gridFrame) return;
const rect = gridFrame.getBoundingClientRect();
const triangPos = getPosition(selection.range, rect);
const triangPos = getPosition(selection.range, rect, viewSettings.vertical);
const annotPopupPos = getPopupPosition(
triangPos,
rect,
@@ -163,13 +167,22 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
dictPopupHeight,
popupPadding,
);
const transPopupPos = getPopupPosition(
triangPos,
rect,
transPopupWidth,
transPopupHeight,
popupPadding,
);
if (triangPos.point.x == 0 || triangPos.point.y == 0) return;
setShowAnnotPopup(true);
setAnnotPopupPosition(annotPopupPos);
setDictPopupPosition(dictPopupPos);
setTranslatorPopupPosition(transPopupPos);
setTrianglePosition(triangPos);
isShowingPopup.current = true;
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selection, bookKey]);
useEffect(() => {
@@ -357,13 +370,13 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
popupHeight={dictPopupHeight}
/>
)}
{showDeepLPopup && trianglePosition && dictPopupPosition && (
{showDeepLPopup && trianglePosition && translatorPopupPosition && (
<DeepLPopup
text={selection?.text as string}
position={dictPopupPosition}
position={translatorPopupPosition}
trianglePosition={trianglePosition}
popupWidth={dictPopupWidth}
popupHeight={dictPopupHeight}
popupWidth={transPopupWidth}
popupHeight={transPopupHeight}
/>
)}
{showAnnotPopup && trianglePosition && annotPopupPosition && (
@@ -96,7 +96,8 @@ const WikipediaPopup: React.FC<WikipediaPopupProps> = ({
}
};
const langCode = typeof lang === 'string' ? lang : lang?.[0];
const bookLang = typeof lang === 'string' ? lang : lang?.[0];
const langCode = bookLang ? bookLang.split('-')[0]! : 'en';
fetchSummary(text, langCode);
}, [text, lang]);
+40 -7
View File
@@ -23,13 +23,46 @@ const Popup = ({
<div
className={`triangle text-base-200 absolute z-10 ${triangleClassName}`}
style={{
left: `${trianglePosition ? trianglePosition.point.x : -999}px`,
top: `${trianglePosition ? trianglePosition.point.y : -999}px`,
borderLeft: '6px solid transparent',
borderRight: '6px solid transparent',
borderBottom: trianglePosition && trianglePosition.dir === 'up' ? 'none' : `6px solid`,
borderTop: trianglePosition && trianglePosition.dir === 'up' ? `6px solid` : 'none',
transform: 'translateX(-50%)',
left:
trianglePosition?.dir === 'left'
? `${trianglePosition.point.x}px`
: trianglePosition?.dir === 'right'
? `${trianglePosition.point.x}px`
: `${trianglePosition ? trianglePosition.point.x : -999}px`,
top:
trianglePosition?.dir === 'up'
? `${trianglePosition.point.y}px`
: trianglePosition?.dir === 'down'
? `${trianglePosition.point.y}px`
: `${trianglePosition ? trianglePosition.point.y : -999}px`,
borderLeft:
trianglePosition?.dir === 'right'
? 'none'
: trianglePosition?.dir === 'left'
? `6px solid`
: '6px solid transparent',
borderRight:
trianglePosition?.dir === 'left'
? 'none'
: trianglePosition?.dir === 'right'
? `6px solid`
: '6px solid transparent',
borderTop:
trianglePosition?.dir === 'down'
? 'none'
: trianglePosition?.dir === 'up'
? `6px solid`
: '6px solid transparent',
borderBottom:
trianglePosition?.dir === 'up'
? 'none'
: trianglePosition?.dir === 'down'
? `6px solid`
: '6px solid transparent',
transform:
trianglePosition?.dir === 'left' || trianglePosition?.dir === 'right'
? 'translateY(-50%)'
: 'translateX(-50%)',
}}
/>
<div
+9
View File
@@ -166,3 +166,12 @@ export class DocumentLoader {
return { book, format } as { book: BookDoc; format: BookFormat };
}
}
export const getDirection = (doc: Document) => {
const { defaultView } = doc;
const { writingMode, direction } = defaultView!.getComputedStyle(doc.body);
console.log('direction', writingMode, direction);
const vertical = writingMode === 'vertical-rl' || writingMode === 'vertical-lr';
const rtl = doc.body.dir === 'rtl' || direction === 'rtl' || doc.documentElement.dir === 'rtl';
return { vertical, rtl };
};
@@ -42,6 +42,7 @@ export const DEFAULT_BOOK_LAYOUT: BookLayout = {
maxInlineSize: 720,
maxBlockSize: 1440,
animated: false,
vertical: false,
};
export const DEFAULT_BOOK_STYLE: BookStyle = {
+1
View File
@@ -42,6 +42,7 @@ export interface BookLayout {
maxInlineSize: number;
maxBlockSize: number;
animated: boolean;
vertical: boolean;
}
export interface BookStyle {
+32 -7
View File
@@ -15,7 +15,7 @@ export interface Point {
y: number;
}
export type PositionDir = 'up' | 'down';
export type PositionDir = 'up' | 'down' | 'left' | 'right';
export interface Position {
point: Point;
@@ -64,8 +64,7 @@ const getIframeElement = (nodeElement: Range | Element): HTMLIFrameElement | nul
return null;
};
export const getPosition = (target: Range | Element, rect: Rect) => {
// TODO: vertical text
export const getPosition = (target: Range | Element, rect: Rect, isVertical: boolean = false) => {
const frameElement = getIframeElement(target);
const transform = frameElement ? getComputedStyle(frameElement).transform : '';
const match = transform.match(/matrix\((.+)\)/);
@@ -75,6 +74,22 @@ export const getPosition = (target: Range | Element, rect: Rect) => {
const rects = Array.from(target.getClientRects());
const first = frameRect(frame, rects[0] as Rect, sx, sy);
const last = frameRect(frame, rects.at(-1) as Rect, sx, sy);
if (isVertical) {
const leftSpace = first.left - rect.left;
const rightSpace = rect.right - first.right;
const dir = leftSpace > rightSpace ? 'left' : 'right';
const position = {
point: {
x: dir === 'left' ? first.left - rect.left - 6 : first.right - rect.left + 6,
y: (first.top + first.bottom) / 2 - rect.top,
},
dir,
} as Position;
const inView = pointIsInView(position.point);
return inView ? position : ({ point: { x: 0, y: 0 }, dir } as Position);
}
const start = {
point: { x: (first.left + first.right) / 2 - rect.left, y: first.top - rect.top - 12 },
dir: 'up',
@@ -98,10 +113,20 @@ export const getPopupPosition = (
popupHeightPx: number,
popupPaddingPx: number,
) => {
const popupPoint = {
x: position.point.x - popupWidthPx / 2,
y: position.dir === 'up' ? position.point.y - popupHeightPx : position.point.y + 6,
};
const popupPoint = { x: 0, y: 0 };
if (position.dir === 'up') {
popupPoint.x = position.point.x - popupWidthPx / 2;
popupPoint.y = position.point.y - popupHeightPx;
} else if (position.dir === 'down') {
popupPoint.x = position.point.x - popupWidthPx / 2;
popupPoint.y = position.point.y + 6;
} else if (position.dir === 'left') {
popupPoint.x = position.point.x - popupWidthPx;
popupPoint.y = position.point.y - popupHeightPx / 2;
} else if (position.dir === 'right') {
popupPoint.x = position.point.x + 6;
popupPoint.y = position.point.y - popupHeightPx / 2;
}
if (popupPoint.x < popupPaddingPx) {
popupPoint.x = popupPaddingPx;
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB