fix in mobile
This commit is contained in:
@@ -276,3 +276,13 @@ html {
|
|||||||
.flipbook-container.flipbook-magnifier-active .stf__item {
|
.flipbook-container.flipbook-magnifier-active .stf__item {
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* جلوگیری از اسکرول/ورقخوردن پیشفرض مرورگر هنگام لمس با ذرهبین فعال */
|
||||||
|
.flipbook-container.flipbook-magnifier-active {
|
||||||
|
touch-action: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* در حین انیمیشن ورق، hit-test ذرهبین روی صفحات در حال حرکت انجام نشود */
|
||||||
|
.flipbook-flipping .flipbook-container.flipbook-magnifier-active .stf__item {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|||||||
@@ -139,6 +139,7 @@ const BookViewer: FC<BookViewerProps> = ({ pages, catalogSize, documentSettings
|
|||||||
isBookFlippingRef.current = flipping;
|
isBookFlippingRef.current = flipping;
|
||||||
flipbookWrapperRef.current?.classList.toggle("relative", flipping);
|
flipbookWrapperRef.current?.classList.toggle("relative", flipping);
|
||||||
flipbookWrapperRef.current?.classList.toggle("z-10", flipping);
|
flipbookWrapperRef.current?.classList.toggle("z-10", flipping);
|
||||||
|
flipbookWrapperRef.current?.classList.toggle("flipbook-flipping", flipping);
|
||||||
spineOverlayRef.current?.classList.toggle("z-50", !flipping);
|
spineOverlayRef.current?.classList.toggle("z-50", !flipping);
|
||||||
spineOverlayRef.current?.classList.toggle("z-0", flipping);
|
spineOverlayRef.current?.classList.toggle("z-0", flipping);
|
||||||
}, []);
|
}, []);
|
||||||
@@ -231,6 +232,8 @@ const BookViewer: FC<BookViewerProps> = ({ pages, catalogSize, documentSettings
|
|||||||
const pagePixelWidth = viewportFit.w;
|
const pagePixelWidth = viewportFit.w;
|
||||||
const pagePixelHeight = viewportFit.h;
|
const pagePixelHeight = viewportFit.h;
|
||||||
const contentScale = viewportFit.contentScale;
|
const contentScale = viewportFit.contentScale;
|
||||||
|
const flipDisabledOnMobile = !isDesktop && magnifierEnabled;
|
||||||
|
const magnifierLockedPageId = usePortrait ? (pages[currentPage]?.id ?? null) : null;
|
||||||
|
|
||||||
const flipKey = `${usePortrait ? "portrait" : "spread"}-${pagePixelWidth}-${pagePixelHeight}-${flipIndexCount}`;
|
const flipKey = `${usePortrait ? "portrait" : "spread"}-${pagePixelWidth}-${pagePixelHeight}-${flipIndexCount}`;
|
||||||
|
|
||||||
@@ -399,6 +402,12 @@ const BookViewer: FC<BookViewerProps> = ({ pages, catalogSize, documentSettings
|
|||||||
[flipIndexCount, scheduleEntranceForIndices, getVisibleLogicalIndices, toLogicalIndex],
|
[flipIndexCount, scheduleEntranceForIndices, getVisibleLogicalIndices, toLogicalIndex],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const beginPageNavigation = useCallback(() => {
|
||||||
|
if (flipDisabledOnMobile) return false;
|
||||||
|
setBookFlippingVisual(true);
|
||||||
|
return true;
|
||||||
|
}, [flipDisabledOnMobile, setBookFlippingVisual]);
|
||||||
|
|
||||||
/** بعد از اتمام انیمیشن ورق، ایندکس واقعی کتاب را میگیریم و انیمیشن ورود را یکبار شروع میکنیم */
|
/** بعد از اتمام انیمیشن ورق، ایندکس واقعی کتاب را میگیریم و انیمیشن ورود را یکبار شروع میکنیم */
|
||||||
const handleChangeState = useCallback(
|
const handleChangeState = useCallback(
|
||||||
(e: PageFlipStateEvent) => {
|
(e: PageFlipStateEvent) => {
|
||||||
@@ -415,23 +424,25 @@ const BookViewer: FC<BookViewerProps> = ({ pages, catalogSize, documentSettings
|
|||||||
|
|
||||||
const goToNextPage = useCallback(() => {
|
const goToNextPage = useCallback(() => {
|
||||||
if (currentPage >= pages.length - 1) return;
|
if (currentPage >= pages.length - 1) return;
|
||||||
|
if (!beginPageNavigation()) return;
|
||||||
const api = bookRef.current?.pageFlip() as PageFlipWithFlipController | undefined;
|
const api = bookRef.current?.pageFlip() as PageFlipWithFlipController | undefined;
|
||||||
if (isRtlBook) {
|
if (isRtlBook) {
|
||||||
triggerFlipPrev(api);
|
triggerFlipPrev(api);
|
||||||
} else {
|
} else {
|
||||||
api?.flipNext();
|
api?.flipNext();
|
||||||
}
|
}
|
||||||
}, [currentPage, pages.length, isRtlBook]);
|
}, [currentPage, pages.length, isRtlBook, beginPageNavigation]);
|
||||||
|
|
||||||
const goToPrevPage = useCallback(() => {
|
const goToPrevPage = useCallback(() => {
|
||||||
if (currentPage <= 0) return;
|
if (currentPage <= 0) return;
|
||||||
|
if (!beginPageNavigation()) return;
|
||||||
const api = bookRef.current?.pageFlip() as PageFlipWithFlipController | undefined;
|
const api = bookRef.current?.pageFlip() as PageFlipWithFlipController | undefined;
|
||||||
if (isRtlBook) {
|
if (isRtlBook) {
|
||||||
api?.flipNext();
|
api?.flipNext();
|
||||||
} else {
|
} else {
|
||||||
triggerFlipPrev(api);
|
triggerFlipPrev(api);
|
||||||
}
|
}
|
||||||
}, [currentPage, isRtlBook]);
|
}, [currentPage, isRtlBook, beginPageNavigation]);
|
||||||
|
|
||||||
const stopAutoPlayByUser = useCallback(() => {
|
const stopAutoPlayByUser = useCallback(() => {
|
||||||
if (isAutoPlayActive) {
|
if (isAutoPlayActive) {
|
||||||
@@ -459,10 +470,17 @@ const BookViewer: FC<BookViewerProps> = ({ pages, catalogSize, documentSettings
|
|||||||
return () => window.removeEventListener("keydown", handleKeyDown);
|
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||||
}, [magnifierEnabled]);
|
}, [magnifierEnabled]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (flipDisabledOnMobile && isAutoPlayActive) {
|
||||||
|
setIsAutoPlayActive(false);
|
||||||
|
}
|
||||||
|
}, [flipDisabledOnMobile, isAutoPlayActive]);
|
||||||
|
|
||||||
const navigateToPageIndex = useCallback(
|
const navigateToPageIndex = useCallback(
|
||||||
(targetIndex: number) => {
|
(targetIndex: number) => {
|
||||||
const pageFlipAPI = bookRef.current?.pageFlip();
|
const pageFlipAPI = bookRef.current?.pageFlip();
|
||||||
if (!pageFlipAPI) return;
|
if (!pageFlipAPI) return;
|
||||||
|
if (!beginPageNavigation()) return;
|
||||||
|
|
||||||
const logicalIdx = Math.max(0, Math.min(targetIndex, pages.length - 1));
|
const logicalIdx = Math.max(0, Math.min(targetIndex, pages.length - 1));
|
||||||
const flipIdx = toFlipIndex(logicalIdx);
|
const flipIdx = toFlipIndex(logicalIdx);
|
||||||
@@ -470,7 +488,7 @@ const BookViewer: FC<BookViewerProps> = ({ pages, catalogSize, documentSettings
|
|||||||
lastFlipPageRef.current = logicalIdx;
|
lastFlipPageRef.current = logicalIdx;
|
||||||
scheduleEntranceForIndices(getVisibleLogicalIndices(flipIdx));
|
scheduleEntranceForIndices(getVisibleLogicalIndices(flipIdx));
|
||||||
},
|
},
|
||||||
[pages.length, scheduleEntranceForIndices, getVisibleLogicalIndices, toFlipIndex],
|
[pages.length, scheduleEntranceForIndices, getVisibleLogicalIndices, toFlipIndex, beginPageNavigation],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleLinkClick = useCallback(
|
const handleLinkClick = useCallback(
|
||||||
@@ -479,6 +497,7 @@ const BookViewer: FC<BookViewerProps> = ({ pages, catalogSize, documentSettings
|
|||||||
stopAutoPlayByUser();
|
stopAutoPlayByUser();
|
||||||
|
|
||||||
if (!linkUrl.startsWith("page://")) return;
|
if (!linkUrl.startsWith("page://")) return;
|
||||||
|
if (flipDisabledOnMobile) return;
|
||||||
|
|
||||||
const action = linkUrl.replace("page://", "");
|
const action = linkUrl.replace("page://", "");
|
||||||
|
|
||||||
@@ -497,7 +516,7 @@ const BookViewer: FC<BookViewerProps> = ({ pages, catalogSize, documentSettings
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[pages.length, goToNextPage, goToPrevPage, navigateToPageIndex, stopAutoPlayByUser],
|
[pages.length, goToNextPage, goToPrevPage, navigateToPageIndex, stopAutoPlayByUser, flipDisabledOnMobile],
|
||||||
);
|
);
|
||||||
|
|
||||||
const prevFlipKeyRef = useRef(flipKey);
|
const prevFlipKeyRef = useRef(flipKey);
|
||||||
@@ -518,7 +537,7 @@ const BookViewer: FC<BookViewerProps> = ({ pages, catalogSize, documentSettings
|
|||||||
|
|
||||||
// پخش خودکار
|
// پخش خودکار
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isAutoPlayActive || pages.length <= 1) return;
|
if (!isAutoPlayActive || pages.length <= 1 || flipDisabledOnMobile) return;
|
||||||
const timer = setInterval(() => {
|
const timer = setInterval(() => {
|
||||||
if (currentPage >= pages.length - 1) {
|
if (currentPage >= pages.length - 1) {
|
||||||
bookRef.current?.pageFlip()?.turnToPage?.(toFlipIndex(0));
|
bookRef.current?.pageFlip()?.turnToPage?.(toFlipIndex(0));
|
||||||
@@ -534,7 +553,7 @@ const BookViewer: FC<BookViewerProps> = ({ pages, catalogSize, documentSettings
|
|||||||
}
|
}
|
||||||
}, AUTO_PLAY_INTERVAL_MS);
|
}, AUTO_PLAY_INTERVAL_MS);
|
||||||
return () => clearInterval(timer);
|
return () => clearInterval(timer);
|
||||||
}, [isAutoPlayActive, currentPage, pages.length, isRtlBook, toFlipIndex]);
|
}, [isAutoPlayActive, currentPage, pages.length, isRtlBook, toFlipIndex, flipDisabledOnMobile]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center gap-4 md:gap-6 w-full h-full max-md:min-h-0 max-md:max-w-full max-md:overflow-x-hidden max-md:pb-[env(safe-area-inset-bottom,0px)]" dir="rtl">
|
<div className="flex flex-col items-center gap-4 md:gap-6 w-full h-full max-md:min-h-0 max-md:max-w-full max-md:overflow-x-hidden max-md:pb-[env(safe-area-inset-bottom,0px)]" dir="rtl">
|
||||||
@@ -560,6 +579,7 @@ const BookViewer: FC<BookViewerProps> = ({ pages, catalogSize, documentSettings
|
|||||||
className="max-w-full flex justify-center shrink-0 rounded-sm"
|
className="max-w-full flex justify-center shrink-0 rounded-sm"
|
||||||
style={{
|
style={{
|
||||||
cursor: magnifierEnabled ? "zoom-in" : undefined,
|
cursor: magnifierEnabled ? "zoom-in" : undefined,
|
||||||
|
touchAction: magnifierEnabled ? "none" : undefined,
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
width: usePortrait ? pagePixelWidth : pagePixelWidth * 2,
|
width: usePortrait ? pagePixelWidth : pagePixelWidth * 2,
|
||||||
height: pagePixelHeight,
|
height: pagePixelHeight,
|
||||||
@@ -583,12 +603,12 @@ const BookViewer: FC<BookViewerProps> = ({ pages, catalogSize, documentSettings
|
|||||||
autoSize={false}
|
autoSize={false}
|
||||||
maxShadowOpacity={legacyIOS ? 0 : isDesktop ? 0.55 : 0.32}
|
maxShadowOpacity={legacyIOS ? 0 : isDesktop ? 0.55 : 0.32}
|
||||||
showCover={hasMultiplePages}
|
showCover={hasMultiplePages}
|
||||||
mobileScrollSupport={!usePortrait}
|
mobileScrollSupport={!usePortrait && !magnifierEnabled}
|
||||||
clickEventForward={true}
|
clickEventForward={true}
|
||||||
useMouseEvents={hasMultiplePages && !magnifierEnabled}
|
useMouseEvents={hasMultiplePages && !magnifierEnabled}
|
||||||
swipeDistance={hasMultiplePages ? (usePortrait ? 48 : 30) : 9999}
|
swipeDistance={flipDisabledOnMobile || !hasMultiplePages ? 9999 : usePortrait ? 48 : 30}
|
||||||
startZIndex={1}
|
startZIndex={1}
|
||||||
showPageCorners={hasMultiplePages}
|
showPageCorners={hasMultiplePages && !flipDisabledOnMobile}
|
||||||
disableFlipByClick={true}
|
disableFlipByClick={true}
|
||||||
className={clx(
|
className={clx(
|
||||||
"flipbook-container mx-auto max-w-full",
|
"flipbook-container mx-auto max-w-full",
|
||||||
@@ -626,6 +646,9 @@ const BookViewer: FC<BookViewerProps> = ({ pages, catalogSize, documentSettings
|
|||||||
enabled={magnifierEnabled}
|
enabled={magnifierEnabled}
|
||||||
containerRef={bookAreaRef}
|
containerRef={bookAreaRef}
|
||||||
interactionRef={flipbookWrapperRef}
|
interactionRef={flipbookWrapperRef}
|
||||||
|
isFlippingRef={isBookFlippingRef}
|
||||||
|
lockedPageId={magnifierLockedPageId}
|
||||||
|
blockFlipTouch={flipDisabledOnMobile}
|
||||||
pages={displayPages}
|
pages={displayPages}
|
||||||
pageWidth={pagePixelWidth}
|
pageWidth={pagePixelWidth}
|
||||||
pageHeight={pagePixelHeight}
|
pageHeight={pagePixelHeight}
|
||||||
@@ -651,9 +674,13 @@ const BookViewer: FC<BookViewerProps> = ({ pages, catalogSize, documentSettings
|
|||||||
goToPrevPage();
|
goToPrevPage();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
disabled={leftToRightFlip ? currentPage >= pages.length - 1 : currentPage <= 0}
|
disabled={
|
||||||
|
flipDisabledOnMobile ||
|
||||||
|
(leftToRightFlip ? currentPage >= pages.length - 1 : currentPage <= 0)
|
||||||
|
}
|
||||||
className="p-2 md:p-2 min-h-11 min-w-11 md:min-h-0 md:min-w-0 inline-flex items-center justify-center rounded-full hover:bg-gray-100 active:bg-gray-200 disabled:opacity-30 disabled:cursor-not-allowed transition-all"
|
className="p-2 md:p-2 min-h-11 min-w-11 md:min-h-0 md:min-w-0 inline-flex items-center justify-center rounded-full hover:bg-gray-100 active:bg-gray-200 disabled:opacity-30 disabled:cursor-not-allowed transition-all"
|
||||||
aria-label={leftToRightFlip ? "صفحه بعد" : "صفحه قبل"}
|
aria-label={leftToRightFlip ? "صفحه بعد" : "صفحه قبل"}
|
||||||
|
title={flipDisabledOnMobile ? "برای ورق زدن ابتدا ذرهبین را خاموش کنید" : undefined}
|
||||||
>
|
>
|
||||||
<ArrowRight2 color="black" size={20} className="text-gray-700 md:w-6 md:h-6" />
|
<ArrowRight2 color="black" size={20} className="text-gray-700 md:w-6 md:h-6" />
|
||||||
</button>
|
</button>
|
||||||
@@ -680,9 +707,13 @@ const BookViewer: FC<BookViewerProps> = ({ pages, catalogSize, documentSettings
|
|||||||
goToNextPage();
|
goToNextPage();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
disabled={leftToRightFlip ? currentPage <= 0 : currentPage >= pages.length - 1}
|
disabled={
|
||||||
|
flipDisabledOnMobile ||
|
||||||
|
(leftToRightFlip ? currentPage <= 0 : currentPage >= pages.length - 1)
|
||||||
|
}
|
||||||
className="p-2 md:p-2 min-h-11 min-w-11 md:min-h-0 md:min-w-0 inline-flex items-center justify-center rounded-full hover:bg-gray-100 active:bg-gray-200 disabled:opacity-30 disabled:cursor-not-allowed transition-all"
|
className="p-2 md:p-2 min-h-11 min-w-11 md:min-h-0 md:min-w-0 inline-flex items-center justify-center rounded-full hover:bg-gray-100 active:bg-gray-200 disabled:opacity-30 disabled:cursor-not-allowed transition-all"
|
||||||
aria-label={leftToRightFlip ? "صفحه قبل" : "صفحه بعد"}
|
aria-label={leftToRightFlip ? "صفحه قبل" : "صفحه بعد"}
|
||||||
|
title={flipDisabledOnMobile ? "برای ورق زدن ابتدا ذرهبین را خاموش کنید" : undefined}
|
||||||
>
|
>
|
||||||
<ArrowLeft2 color="black" size={20} className="text-gray-700 md:w-6 md:h-6" />
|
<ArrowLeft2 color="black" size={20} className="text-gray-700 md:w-6 md:h-6" />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -21,6 +21,11 @@ export type MagnifierProps = {
|
|||||||
contentScale: number;
|
contentScale: number;
|
||||||
pageDefaults: PageBackgroundDefaults;
|
pageDefaults: PageBackgroundDefaults;
|
||||||
useHardPage?: boolean;
|
useHardPage?: boolean;
|
||||||
|
/** در حین انیمیشن ورقخوردن true است */
|
||||||
|
isFlippingRef: RefObject<boolean>;
|
||||||
|
/** در حالت تکصفحه (موبایل) همیشه صفحهٔ فعلی بزرگنمایی میشود */
|
||||||
|
lockedPageId?: number | null;
|
||||||
|
blockFlipTouch?: boolean;
|
||||||
/** با تغییر آن (ورق خوردن) ذرهبین ریست میشود */
|
/** با تغییر آن (ورق خوردن) ذرهبین ریست میشود */
|
||||||
pageIndex?: number;
|
pageIndex?: number;
|
||||||
};
|
};
|
||||||
@@ -36,6 +41,9 @@ const Magnifier = memo(
|
|||||||
contentScale,
|
contentScale,
|
||||||
pageDefaults,
|
pageDefaults,
|
||||||
useHardPage = false,
|
useHardPage = false,
|
||||||
|
isFlippingRef,
|
||||||
|
lockedPageId = null,
|
||||||
|
blockFlipTouch = false,
|
||||||
pageIndex,
|
pageIndex,
|
||||||
}: MagnifierProps) => {
|
}: MagnifierProps) => {
|
||||||
const overlayRef = useRef<HTMLDivElement>(null);
|
const overlayRef = useRef<HTMLDivElement>(null);
|
||||||
@@ -54,15 +62,23 @@ const Magnifier = memo(
|
|||||||
overlayRef,
|
overlayRef,
|
||||||
lensRef,
|
lensRef,
|
||||||
mirrorRef,
|
mirrorRef,
|
||||||
|
isFlippingRef,
|
||||||
|
lockedPageId,
|
||||||
|
blockFlipTouch,
|
||||||
pageIndex,
|
pageIndex,
|
||||||
onActivePageChange: handleActivePageChange,
|
onActivePageChange: handleActivePageChange,
|
||||||
});
|
});
|
||||||
|
|
||||||
const activePage = useMemo(
|
const lockedPage = useMemo(
|
||||||
() => (activePageId != null ? pages.find((p) => p.id === activePageId) : undefined),
|
() => (lockedPageId != null ? pages.find((p) => p.id === lockedPageId) : undefined),
|
||||||
[pages, activePageId],
|
[lockedPageId, pages],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const activePage = useMemo(() => {
|
||||||
|
if (lockedPage) return lockedPage;
|
||||||
|
return activePageId != null ? pages.find((p) => p.id === activePageId) : undefined;
|
||||||
|
}, [lockedPage, pages, activePageId]);
|
||||||
|
|
||||||
const background = useMemo(
|
const background = useMemo(
|
||||||
() => (activePage ? resolvePageBackground(activePage, pageDefaults) : null),
|
() => (activePage ? resolvePageBackground(activePage, pageDefaults) : null),
|
||||||
[activePage, pageDefaults],
|
[activePage, pageDefaults],
|
||||||
|
|||||||
@@ -1,3 +1,12 @@
|
|||||||
|
/** پیدا کردن المان `.page` با شناسهٔ مشخص (حالت تکصفحهای موبایل) */
|
||||||
|
export function findPageById(root: HTMLElement, pageId: number): HTMLElement | null {
|
||||||
|
const page = root.querySelector<HTMLElement>(`.page[data-page-id="${pageId}"]`);
|
||||||
|
if (!(page instanceof HTMLElement) || !root.contains(page) || !isPageVisible(page)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return page;
|
||||||
|
}
|
||||||
|
|
||||||
/** پیدا کردن المان `.page` زیر مختصات مشخص، با اولویت بالاترین z-index در حالت اسپرد */
|
/** پیدا کردن المان `.page` زیر مختصات مشخص، با اولویت بالاترین z-index در حالت اسپرد */
|
||||||
export function findPageAtPoint(
|
export function findPageAtPoint(
|
||||||
root: HTMLElement,
|
root: HTMLElement,
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { type RefObject, useCallback, useEffect, useLayoutEffect, useRef } from "react";
|
import { type RefObject, useCallback, useEffect, useLayoutEffect, useRef } from "react";
|
||||||
import { MAGNIFIER_SIZE, ZOOM_SCALE } from "./constants";
|
import { MAGNIFIER_SIZE, ZOOM_SCALE } from "./constants";
|
||||||
import { computeMagnifierGeometry, findPageAtPoint } from "./magnifierUtils";
|
import { computeMagnifierGeometry, findPageAtPoint, findPageById } from "./magnifierUtils";
|
||||||
|
|
||||||
type UseMagnifierOptions = {
|
type UseMagnifierOptions = {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
/** ناحیهٔ موقعیتدهی لنز (مرجع مختصات نسبی) */
|
/** مرجع مختصات برای موقعیتدهی لنز (معمولاً ناحیهٔ اطراف کتاب) */
|
||||||
containerRef: RefObject<HTMLElement | null>;
|
containerRef: RefObject<HTMLElement | null>;
|
||||||
/** ناحیهٔ تعامل و جستجوی صفحات (wrapper کتاب) */
|
/** ناحیهٔ تعامل و جستجوی صفحات (wrapper کتاب) */
|
||||||
interactionRef: RefObject<HTMLElement | null>;
|
interactionRef: RefObject<HTMLElement | null>;
|
||||||
@@ -12,6 +12,12 @@ type UseMagnifierOptions = {
|
|||||||
lensRef: RefObject<HTMLElement | null>;
|
lensRef: RefObject<HTMLElement | null>;
|
||||||
/** لایهٔ داخل لنز که محتوای صفحه (BookPage با مقیاس بزرگ) داخلش رندر میشود */
|
/** لایهٔ داخل لنز که محتوای صفحه (BookPage با مقیاس بزرگ) داخلش رندر میشود */
|
||||||
mirrorRef: RefObject<HTMLElement | null>;
|
mirrorRef: RefObject<HTMLElement | null>;
|
||||||
|
/** در حین انیمیشن ورقخوردن true است — hit-test و بهروزرسانی لنز متوقف میشود */
|
||||||
|
isFlippingRef: RefObject<boolean>;
|
||||||
|
/** در حالت تکصفحه (موبایل) همیشه همین صفحه بزرگنمایی میشود، نه نتیجهٔ hit-test */
|
||||||
|
lockedPageId?: number | null;
|
||||||
|
/** جلوگیری از رسیدن touch به page-flip (کتابخانه تنظیمات را بعد از mount بهروز نمیکند) */
|
||||||
|
blockFlipTouch?: boolean;
|
||||||
/** با تغییر آن (ورق خوردن) وضعیت فعلی ریست میشود */
|
/** با تغییر آن (ورق خوردن) وضعیت فعلی ریست میشود */
|
||||||
pageIndex?: number;
|
pageIndex?: number;
|
||||||
/** وقتی صفحهٔ زیر ماوس عوض شود صدا زده میشود تا محتوای مناسب رندر شود */
|
/** وقتی صفحهٔ زیر ماوس عوض شود صدا زده میشود تا محتوای مناسب رندر شود */
|
||||||
@@ -25,12 +31,19 @@ export function useMagnifier({
|
|||||||
overlayRef,
|
overlayRef,
|
||||||
lensRef,
|
lensRef,
|
||||||
mirrorRef,
|
mirrorRef,
|
||||||
|
isFlippingRef,
|
||||||
|
lockedPageId = null,
|
||||||
|
blockFlipTouch = false,
|
||||||
pageIndex,
|
pageIndex,
|
||||||
onActivePageChange,
|
onActivePageChange,
|
||||||
}: UseMagnifierOptions) {
|
}: UseMagnifierOptions) {
|
||||||
const rafIdRef = useRef<number | null>(null);
|
const rafIdRef = useRef<number | null>(null);
|
||||||
const pendingPointRef = useRef<{ x: number; y: number } | null>(null);
|
const pendingPointRef = useRef<{ x: number; y: number } | null>(null);
|
||||||
const lastPageIdRef = useRef<number | null>(null);
|
const lastPageIdRef = useRef<number | null>(null);
|
||||||
|
const touchActiveRef = useRef(false);
|
||||||
|
const lockedPageIdRef = useRef(lockedPageId);
|
||||||
|
|
||||||
|
lockedPageIdRef.current = lockedPageId;
|
||||||
|
|
||||||
const hideLens = useCallback(() => {
|
const hideLens = useCallback(() => {
|
||||||
const lens = lensRef.current;
|
const lens = lensRef.current;
|
||||||
@@ -46,21 +59,11 @@ export function useMagnifier({
|
|||||||
}
|
}
|
||||||
}, [onActivePageChange]);
|
}, [onActivePageChange]);
|
||||||
|
|
||||||
// با تغییر صفحه (ورق خوردن) باید دوباره از صفر hit-test انجام شود
|
const applyGeometryRef = useRef<(clientX: number, clientY: number) => void>(() => {});
|
||||||
useEffect(() => {
|
|
||||||
if (!enabled) return;
|
|
||||||
pendingPointRef.current = null;
|
|
||||||
hideLens();
|
|
||||||
resetActivePage();
|
|
||||||
}, [pageIndex, enabled, hideLens, resetActivePage]);
|
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
if (!enabled) {
|
applyGeometryRef.current = (clientX: number, clientY: number) => {
|
||||||
pendingPointRef.current = null;
|
if (isFlippingRef.current) {
|
||||||
if (rafIdRef.current != null) {
|
|
||||||
cancelAnimationFrame(rafIdRef.current);
|
|
||||||
rafIdRef.current = null;
|
|
||||||
}
|
|
||||||
hideLens();
|
hideLens();
|
||||||
resetActivePage();
|
resetActivePage();
|
||||||
return;
|
return;
|
||||||
@@ -68,15 +71,15 @@ export function useMagnifier({
|
|||||||
|
|
||||||
const container = containerRef.current;
|
const container = containerRef.current;
|
||||||
const interaction = interactionRef.current;
|
const interaction = interactionRef.current;
|
||||||
if (!container || !interaction) return;
|
|
||||||
|
|
||||||
const half = MAGNIFIER_SIZE / 2;
|
|
||||||
|
|
||||||
const applyGeometry = (clientX: number, clientY: number) => {
|
|
||||||
const lens = lensRef.current;
|
const lens = lensRef.current;
|
||||||
if (!lens) return;
|
if (!container || !interaction || !lens) return;
|
||||||
|
|
||||||
|
const lockedId = lockedPageIdRef.current;
|
||||||
|
const page =
|
||||||
|
lockedId != null
|
||||||
|
? findPageById(interaction, lockedId)
|
||||||
|
: findPageAtPoint(interaction, clientX, clientY, overlayRef.current);
|
||||||
|
|
||||||
const page = findPageAtPoint(interaction, clientX, clientY, overlayRef.current);
|
|
||||||
if (!page) {
|
if (!page) {
|
||||||
hideLens();
|
hideLens();
|
||||||
resetActivePage();
|
resetActivePage();
|
||||||
@@ -96,6 +99,7 @@ export function useMagnifier({
|
|||||||
onActivePageChange(pageId);
|
onActivePageChange(pageId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const half = MAGNIFIER_SIZE / 2;
|
||||||
const { pageOffsetX, pageOffsetY, localX, localY, clampedX, clampedY } = geometry;
|
const { pageOffsetX, pageOffsetY, localX, localY, clampedX, clampedY } = geometry;
|
||||||
|
|
||||||
lens.style.visibility = "visible";
|
lens.style.visibility = "visible";
|
||||||
@@ -103,66 +107,146 @@ export function useMagnifier({
|
|||||||
|
|
||||||
const mirror = mirrorRef.current;
|
const mirror = mirrorRef.current;
|
||||||
if (mirror) {
|
if (mirror) {
|
||||||
const tx = half - localX * ZOOM_SCALE;
|
mirror.style.transform = `translate3d(${half - localX * ZOOM_SCALE}px, ${half - localY * ZOOM_SCALE}px, 0)`;
|
||||||
const ty = half - localY * ZOOM_SCALE;
|
|
||||||
mirror.style.transform = `translate3d(${tx}px, ${ty}px, 0)`;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
});
|
||||||
|
|
||||||
const flushPending = () => {
|
// با ورق خوردن، وضعیت ذرهبین کاملاً ریست میشود
|
||||||
rafIdRef.current = null;
|
useEffect(() => {
|
||||||
const point = pendingPointRef.current;
|
if (!enabled) return;
|
||||||
if (!point) return;
|
|
||||||
applyGeometry(point.x, point.y);
|
|
||||||
};
|
|
||||||
|
|
||||||
const scheduleUpdate = (clientX: number, clientY: number) => {
|
|
||||||
pendingPointRef.current = { x: clientX, y: clientY };
|
|
||||||
if (rafIdRef.current == null) {
|
|
||||||
rafIdRef.current = requestAnimationFrame(flushPending);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleMouseMove = (event: MouseEvent) => {
|
|
||||||
scheduleUpdate(event.clientX, event.clientY);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleMouseLeave = () => {
|
|
||||||
pendingPointRef.current = null;
|
pendingPointRef.current = null;
|
||||||
|
touchActiveRef.current = false;
|
||||||
hideLens();
|
hideLens();
|
||||||
resetActivePage();
|
resetActivePage();
|
||||||
};
|
lastPageIdRef.current = null;
|
||||||
|
}, [pageIndex, enabled, hideLens, resetActivePage]);
|
||||||
|
|
||||||
interaction.addEventListener("mousemove", handleMouseMove);
|
useLayoutEffect(() => {
|
||||||
interaction.addEventListener("mouseleave", handleMouseLeave);
|
if (!enabled) {
|
||||||
|
pendingPointRef.current = null;
|
||||||
return () => {
|
touchActiveRef.current = false;
|
||||||
interaction.removeEventListener("mousemove", handleMouseMove);
|
|
||||||
interaction.removeEventListener("mouseleave", handleMouseLeave);
|
|
||||||
if (rafIdRef.current != null) {
|
if (rafIdRef.current != null) {
|
||||||
cancelAnimationFrame(rafIdRef.current);
|
cancelAnimationFrame(rafIdRef.current);
|
||||||
rafIdRef.current = null;
|
rafIdRef.current = null;
|
||||||
}
|
}
|
||||||
hideLens();
|
hideLens();
|
||||||
resetActivePage();
|
resetActivePage();
|
||||||
};
|
return;
|
||||||
}, [enabled, containerRef, interactionRef, overlayRef, lensRef, mirrorRef, hideLens, resetActivePage, onActivePageChange]);
|
}
|
||||||
|
|
||||||
// بلافاصله بعد از سوییچ صفحه (رندر مجدد mirror) آخرین موقعیت شناختهشده را دوباره اعمال میکند
|
|
||||||
// تا محتوای تازهرندرشده یک فریم با موقعیت اشتباه دیده نشود.
|
|
||||||
useLayoutEffect(() => {
|
|
||||||
if (!enabled) return;
|
|
||||||
const mirror = mirrorRef.current;
|
|
||||||
const point = pendingPointRef.current;
|
|
||||||
if (!mirror || !point) return;
|
|
||||||
const half = MAGNIFIER_SIZE / 2;
|
|
||||||
const container = containerRef.current;
|
const container = containerRef.current;
|
||||||
const interaction = interactionRef.current;
|
const interaction = interactionRef.current;
|
||||||
if (!container || !interaction) return;
|
if (!container || !interaction) return;
|
||||||
const page = findPageAtPoint(interaction, point.x, point.y, overlayRef.current);
|
|
||||||
if (!page) return;
|
const flushPending = () => {
|
||||||
const geometry = computeMagnifierGeometry(point.x, point.y, page, container, MAGNIFIER_SIZE);
|
rafIdRef.current = null;
|
||||||
if (!geometry) return;
|
const point = pendingPointRef.current;
|
||||||
mirror.style.transform = `translate3d(${half - geometry.localX * ZOOM_SCALE}px, ${half - geometry.localY * ZOOM_SCALE}px, 0)`;
|
if (!point) return;
|
||||||
});
|
applyGeometryRef.current(point.x, point.y);
|
||||||
|
};
|
||||||
|
|
||||||
|
const scheduleUpdate = (clientX: number, clientY: number) => {
|
||||||
|
if (isFlippingRef.current) {
|
||||||
|
hideLens();
|
||||||
|
resetActivePage();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingPointRef.current = { x: clientX, y: clientY };
|
||||||
|
if (rafIdRef.current == null) {
|
||||||
|
rafIdRef.current = requestAnimationFrame(flushPending);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const isTouchLike = (event: PointerEvent) =>
|
||||||
|
event.pointerType === "touch" || event.pointerType === "pen";
|
||||||
|
|
||||||
|
const handlePointerMove = (event: PointerEvent) => {
|
||||||
|
if (isTouchLike(event)) {
|
||||||
|
if (!touchActiveRef.current) return;
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
|
scheduleUpdate(event.clientX, event.clientY);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePointerDown = (event: PointerEvent) => {
|
||||||
|
if (!isTouchLike(event)) return;
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
touchActiveRef.current = true;
|
||||||
|
scheduleUpdate(event.clientX, event.clientY);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePointerUpOrCancel = (event: PointerEvent) => {
|
||||||
|
if (!isTouchLike(event)) return;
|
||||||
|
event.stopPropagation();
|
||||||
|
touchActiveRef.current = false;
|
||||||
|
pendingPointRef.current = null;
|
||||||
|
hideLens();
|
||||||
|
resetActivePage();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePointerLeave = (event: PointerEvent) => {
|
||||||
|
if (isTouchLike(event)) return;
|
||||||
|
pendingPointRef.current = null;
|
||||||
|
hideLens();
|
||||||
|
resetActivePage();
|
||||||
|
};
|
||||||
|
|
||||||
|
const listenerOptions = { capture: true, passive: false } as const;
|
||||||
|
|
||||||
|
interaction.addEventListener("pointermove", handlePointerMove, listenerOptions);
|
||||||
|
interaction.addEventListener("pointerdown", handlePointerDown, listenerOptions);
|
||||||
|
interaction.addEventListener("pointerup", handlePointerUpOrCancel, listenerOptions);
|
||||||
|
interaction.addEventListener("pointercancel", handlePointerUpOrCancel, listenerOptions);
|
||||||
|
interaction.addEventListener("pointerleave", handlePointerLeave);
|
||||||
|
|
||||||
|
const blockTouchEvent = (event: TouchEvent) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
};
|
||||||
|
const touchBlockOptions = { capture: true, passive: false } as const;
|
||||||
|
|
||||||
|
if (blockFlipTouch) {
|
||||||
|
interaction.addEventListener("touchstart", blockTouchEvent, touchBlockOptions);
|
||||||
|
interaction.addEventListener("touchmove", blockTouchEvent, touchBlockOptions);
|
||||||
|
interaction.addEventListener("touchend", blockTouchEvent, touchBlockOptions);
|
||||||
|
interaction.addEventListener("touchcancel", blockTouchEvent, touchBlockOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (blockFlipTouch) {
|
||||||
|
interaction.removeEventListener("touchstart", blockTouchEvent, touchBlockOptions);
|
||||||
|
interaction.removeEventListener("touchmove", blockTouchEvent, touchBlockOptions);
|
||||||
|
interaction.removeEventListener("touchend", blockTouchEvent, touchBlockOptions);
|
||||||
|
interaction.removeEventListener("touchcancel", blockTouchEvent, touchBlockOptions);
|
||||||
|
}
|
||||||
|
interaction.removeEventListener("pointermove", handlePointerMove, listenerOptions);
|
||||||
|
interaction.removeEventListener("pointerdown", handlePointerDown, listenerOptions);
|
||||||
|
interaction.removeEventListener("pointerup", handlePointerUpOrCancel, listenerOptions);
|
||||||
|
interaction.removeEventListener("pointercancel", handlePointerUpOrCancel, listenerOptions);
|
||||||
|
interaction.removeEventListener("pointerleave", handlePointerLeave);
|
||||||
|
if (rafIdRef.current != null) {
|
||||||
|
cancelAnimationFrame(rafIdRef.current);
|
||||||
|
rafIdRef.current = null;
|
||||||
|
}
|
||||||
|
touchActiveRef.current = false;
|
||||||
|
hideLens();
|
||||||
|
resetActivePage();
|
||||||
|
};
|
||||||
|
}, [
|
||||||
|
enabled,
|
||||||
|
containerRef,
|
||||||
|
interactionRef,
|
||||||
|
overlayRef,
|
||||||
|
lensRef,
|
||||||
|
mirrorRef,
|
||||||
|
isFlippingRef,
|
||||||
|
hideLens,
|
||||||
|
resetActivePage,
|
||||||
|
onActivePageChange,
|
||||||
|
blockFlipTouch,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user