next and prev page

This commit is contained in:
hamid zarghami
2026-05-03 09:53:51 +03:30
parent 3cffa1b107
commit 1bad82e680
2 changed files with 83 additions and 14 deletions
+1
View File
@@ -480,6 +480,7 @@ const BookPage = forwardRef<HTMLDivElement, BookPageProps>(
return (
<div
ref={ref}
dir="rtl"
className="page"
data-density="soft"
style={{
+82 -14
View File
@@ -34,6 +34,7 @@ interface PageFlipAPI {
flipNext: () => void;
flipPrev: () => void;
turnToPage: (page: number) => void;
getCurrentPageIndex?: () => number;
}
interface FlipBookInstance {
@@ -44,6 +45,33 @@ interface FlipEvent {
data: number;
}
type PageFlipInitEvent = { data: { page: number; mode: string } };
type PageFlipStateEvent = { data: string };
/** مختصات داخلی رندر کتاب (همان فضایی که getBoundsRect و flipNext استفاده می‌کنند) */
type FlipBookRect = { left: number; top: number; width: number; height: number; pageWidth: number };
type PageFlipWithFlipController = PageFlipAPI & {
getBoundsRect: () => FlipBookRect;
getFlipController: () => { flip: (p: { x: number; y: number }) => void };
};
/**
* page-flip داخل flipPrev با x=10 ثابت می‌زند؛ با disableFlipByClick آزمایش گوشه رد می‌شود
* (چون bookPos.x = 10 - rect.left معمولاً ≤ 0). flipNext همان rect.left را جمع می‌کند.
*/
function triggerFlipPrev(api: PageFlipWithFlipController | undefined) {
if (!api?.getBoundsRect || !api.getFlipController) {
api?.flipPrev();
return;
}
const rect = api.getBoundsRect();
api.getFlipController().flip({
x: rect.left + 12,
y: rect.top + 2,
});
}
/**
* کامپوننت اصلی کتابخوان با استفاده از react-pageflip
* این کامپوننت از HTMLFlipBook استفاده می‌کند که یک wrapper برای PageFlip است
@@ -51,6 +79,7 @@ interface FlipEvent {
*/
const BookViewer: FC<BookViewerProps> = ({ pages, catalogSize }) => {
const bookRef = useRef<FlipBookInstance | null>(null);
const pageFlipHandlerRef = useRef<(e: FlipEvent) => void>(() => {});
const [currentPage, setCurrentPage] = useState(0);
const isDesktop = useIsDesktop();
@@ -189,6 +218,40 @@ const BookViewer: FC<BookViewerProps> = ({ pages, catalogSize }) => {
setCurrentPage(pageNum);
}, [pages.length, playPageFlipSound]);
pageFlipHandlerRef.current = handlePageFlip;
const onFlipForwarded = useCallback((e: FlipEvent) => {
pageFlipHandlerRef.current(e);
}, []);
const syncPageIndexFromBook = useCallback(() => {
const api = bookRef.current?.pageFlip();
const idx = api?.getCurrentPageIndex?.();
if (typeof idx !== 'number' || pages.length <= 0) return;
const clamped = Math.max(0, Math.min(idx, pages.length - 1));
setCurrentPage(clamped);
}, [pages.length]);
const handleBookInit = useCallback(
(e: PageFlipInitEvent) => {
const p = e.data?.page;
if (typeof p !== 'number' || pages.length <= 0) return;
const clamped = Math.max(0, Math.min(p, pages.length - 1));
setCurrentPage(clamped);
lastFlipPageRef.current = clamped;
},
[pages.length]
);
/** بعد از اتمام انیمیشن، ایندکس واقعی کتاب را می‌گیریم (RTL روی روت کتاب مختصات page-flip را به‌هم می‌زند) */
const handleChangeState = useCallback(
(e: PageFlipStateEvent) => {
if (e.data !== 'read') return;
syncPageIndexFromBook();
},
[syncPageIndexFromBook]
);
const goToNextPage = useCallback(() => {
if (currentPage < pages.length - 1) {
bookRef.current?.pageFlip()?.flipNext();
@@ -196,15 +259,15 @@ const BookViewer: FC<BookViewerProps> = ({ pages, catalogSize }) => {
}, [currentPage, pages.length]);
const goToPrevPage = useCallback(() => {
if (currentPage > 0) {
bookRef.current?.pageFlip()?.flipPrev();
}
if (currentPage <= 0) return;
const api = bookRef.current?.pageFlip() as PageFlipWithFlipController | undefined;
triggerFlipPrev(api);
}, [currentPage]);
const handleLinkClick = useCallback((linkUrl: string) => {
if (linkUrl.startsWith('page://')) {
const action = linkUrl.replace('page://', '');
const pageFlipAPI = bookRef.current?.pageFlip();
const pageFlipAPI = bookRef.current?.pageFlip() as PageFlipWithFlipController | undefined;
if (!pageFlipAPI) return;
@@ -216,7 +279,7 @@ const BookViewer: FC<BookViewerProps> = ({ pages, catalogSize }) => {
// رفتن به صفحه اول
const flipToFirst = () => {
if (currentPage > 0) {
pageFlipAPI.flipPrev();
triggerFlipPrev(pageFlipAPI);
setTimeout(() => {
setCurrentPage(prev => {
if (prev > 1) {
@@ -262,7 +325,7 @@ const BookViewer: FC<BookViewerProps> = ({ pages, catalogSize }) => {
flipToTarget(remaining - 1);
}, 300);
} else {
pageFlipAPI.flipPrev();
triggerFlipPrev(pageFlipAPI);
setTimeout(() => {
setCurrentPage(prev => prev - 1);
flipToTarget(remaining + 1);
@@ -302,7 +365,12 @@ const BookViewer: FC<BookViewerProps> = ({ pages, catalogSize }) => {
boxShadow: '-5px 0 15px rgba(0, 0, 0, 0.15), 5px 0 15px rgba(0, 0, 0, 0.15)',
}}
/>
{/*
page-flip با مختصات LTR کار می‌کند؛ direction:rtl روی روت کتاب باعث ناهماهنگی
ایندکس/onFlip با دکمه‌ها می‌شود. محتوای فارسی داخل BookPage با dir=rtl است.
*/}
<div
dir="ltr"
className="max-w-full flex justify-center"
style={{ overflow: isDesktop ? 'hidden' : 'visible', isolation: 'isolate' }}
>
@@ -331,10 +399,10 @@ const BookViewer: FC<BookViewerProps> = ({ pages, catalogSize }) => {
showPageCorners={true}
disableFlipByClick={true}
className="flipbook-container mx-auto max-w-full"
style={{
direction: 'rtl',
}}
onFlip={handlePageFlip}
style={{}}
onFlip={onFlipForwarded}
onInit={handleBookInit}
onChangeState={handleChangeState}
>
{pages.map((page) => (
<BookPage
@@ -355,11 +423,11 @@ const BookViewer: FC<BookViewerProps> = ({ pages, catalogSize }) => {
<button
type="button"
onClick={goToNextPage}
disabled={currentPage <= 0}
disabled={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"
aria-label="صفحه بعد"
>
<ArrowLeft2 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>
{/* شماره صفحه */}
@@ -373,11 +441,11 @@ const BookViewer: FC<BookViewerProps> = ({ pages, catalogSize }) => {
<button
type="button"
onClick={goToPrevPage}
disabled={currentPage >= pages.length - 1}
disabled={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"
aria-label="صفحه قبل"
>
<ArrowRight2 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>
</div>
</div>