link to another pager
This commit is contained in:
@@ -128,6 +128,71 @@
|
|||||||
"fill": "#3b82f6",
|
"fill": "#3b82f6",
|
||||||
"opacity": 100
|
"opacity": 100
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "link-next-page",
|
||||||
|
"type": "link",
|
||||||
|
"x": 100,
|
||||||
|
"y": 500,
|
||||||
|
"width": 150,
|
||||||
|
"height": 50,
|
||||||
|
"text": "صفحه بعدی",
|
||||||
|
"fontSize": 18,
|
||||||
|
"linkUrl": "page://next",
|
||||||
|
"fill": "#3b82f6",
|
||||||
|
"opacity": 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "link-prev-page",
|
||||||
|
"type": "link",
|
||||||
|
"x": 280,
|
||||||
|
"y": 500,
|
||||||
|
"width": 150,
|
||||||
|
"height": 50,
|
||||||
|
"text": "صفحه قبلی",
|
||||||
|
"fontSize": 18,
|
||||||
|
"linkUrl": "page://prev",
|
||||||
|
"fill": "#3b82f6",
|
||||||
|
"opacity": 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "link-first-page",
|
||||||
|
"type": "link",
|
||||||
|
"x": 460,
|
||||||
|
"y": 500,
|
||||||
|
"width": 150,
|
||||||
|
"height": 50,
|
||||||
|
"text": "اولین صفحه",
|
||||||
|
"fontSize": 18,
|
||||||
|
"linkUrl": "page://first",
|
||||||
|
"fill": "#3b82f6",
|
||||||
|
"opacity": 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "link-last-page",
|
||||||
|
"type": "link",
|
||||||
|
"x": 100,
|
||||||
|
"y": 570,
|
||||||
|
"width": 150,
|
||||||
|
"height": 50,
|
||||||
|
"text": "آخرین صفحه",
|
||||||
|
"fontSize": 18,
|
||||||
|
"linkUrl": "page://last",
|
||||||
|
"fill": "#3b82f6",
|
||||||
|
"opacity": 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "link-to-page",
|
||||||
|
"type": "link",
|
||||||
|
"x": 280,
|
||||||
|
"y": 570,
|
||||||
|
"width": 150,
|
||||||
|
"height": 50,
|
||||||
|
"text": "صفحه",
|
||||||
|
"fontSize": 18,
|
||||||
|
"linkUrl": "page://3",
|
||||||
|
"fill": "#3b82f6",
|
||||||
|
"opacity": 100
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "image-2",
|
"id": "image-2",
|
||||||
"type": "image",
|
"type": "image",
|
||||||
@@ -328,6 +393,58 @@
|
|||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"shapeType": "abstract",
|
"shapeType": "abstract",
|
||||||
"opacity": 100
|
"opacity": 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "link-nav-next-3",
|
||||||
|
"type": "link",
|
||||||
|
"x": 100,
|
||||||
|
"y": 600,
|
||||||
|
"width": 140,
|
||||||
|
"height": 45,
|
||||||
|
"text": "صفحه بعدی",
|
||||||
|
"fontSize": 16,
|
||||||
|
"linkUrl": "page://next",
|
||||||
|
"fill": "#3b82f6",
|
||||||
|
"opacity": 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "link-nav-prev-3",
|
||||||
|
"type": "link",
|
||||||
|
"x": 260,
|
||||||
|
"y": 600,
|
||||||
|
"width": 140,
|
||||||
|
"height": 45,
|
||||||
|
"text": "صفحه قبلی",
|
||||||
|
"fontSize": 16,
|
||||||
|
"linkUrl": "page://prev",
|
||||||
|
"fill": "#3b82f6",
|
||||||
|
"opacity": 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "link-nav-first-3",
|
||||||
|
"type": "link",
|
||||||
|
"x": 420,
|
||||||
|
"y": 600,
|
||||||
|
"width": 140,
|
||||||
|
"height": 45,
|
||||||
|
"text": "اولین صفحه",
|
||||||
|
"fontSize": 16,
|
||||||
|
"linkUrl": "page://first",
|
||||||
|
"fill": "#3b82f6",
|
||||||
|
"opacity": 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "link-nav-last-3",
|
||||||
|
"type": "link",
|
||||||
|
"x": 580,
|
||||||
|
"y": 600,
|
||||||
|
"width": 140,
|
||||||
|
"height": 45,
|
||||||
|
"text": "آخرین صفحه",
|
||||||
|
"fontSize": 16,
|
||||||
|
"linkUrl": "page://last",
|
||||||
|
"fill": "#3b82f6",
|
||||||
|
"opacity": 100
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import type { EditorObject } from '@/pages/editor/store/editorStore';
|
|||||||
type BookPageProps = {
|
type BookPageProps = {
|
||||||
page: PageData;
|
page: PageData;
|
||||||
scale?: number;
|
scale?: number;
|
||||||
|
onLinkClick?: (linkUrl: string) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -14,7 +15,7 @@ type BookPageProps = {
|
|||||||
* این کامپوننت به عنوان child برای HTMLFlipBook استفاده میشود
|
* این کامپوننت به عنوان child برای HTMLFlipBook استفاده میشود
|
||||||
* نیاز به forwardRef دارد تا react-pageflip بتواند ref را مدیریت کند
|
* نیاز به forwardRef دارد تا react-pageflip بتواند ref را مدیریت کند
|
||||||
*/
|
*/
|
||||||
const BookPage = forwardRef<HTMLDivElement, BookPageProps>(({ page, scale = 1 }, ref) => {
|
const BookPage = forwardRef<HTMLDivElement, BookPageProps>(({ page, scale = 1, onLinkClick }, ref) => {
|
||||||
// تابع برای تبدیل opacity به رنگ (مثل editor)
|
// تابع برای تبدیل opacity به رنگ (مثل editor)
|
||||||
// در editor، getColorWithOpacity انتظار opacity 0-100 دارد
|
// در editor، getColorWithOpacity انتظار opacity 0-100 دارد
|
||||||
// در dataTransformer، opacity از 0-1 به 0-100 تبدیل شده است
|
// در dataTransformer، opacity از 0-1 به 0-100 تبدیل شده است
|
||||||
@@ -108,18 +109,24 @@ const BookPage = forwardRef<HTMLDivElement, BookPageProps>(({ page, scale = 1 },
|
|||||||
width: obj.width ? `${obj.width * scale}px` : 'auto',
|
width: obj.width ? `${obj.width * scale}px` : 'auto',
|
||||||
height: obj.height ? `${obj.height * scale}px` : 'auto',
|
height: obj.height ? `${obj.height * scale}px` : 'auto',
|
||||||
objectFit: 'contain',
|
objectFit: 'contain',
|
||||||
|
zIndex: index,
|
||||||
|
pointerEvents: 'auto',
|
||||||
|
}}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
onMouseDown={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
onTouchStart={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
case 'link':
|
case 'link': {
|
||||||
return (
|
const isInternalLink = obj.linkUrl?.startsWith('page://');
|
||||||
<a
|
const linkStyle = {
|
||||||
key={obj.id || index}
|
|
||||||
href={obj.linkUrl || '#'}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
style={{
|
|
||||||
...baseStyle,
|
...baseStyle,
|
||||||
width: obj.width ? `${obj.width * scale}px` : 'auto',
|
width: obj.width ? `${obj.width * scale}px` : 'auto',
|
||||||
height: obj.height ? `${obj.height * scale}px` : 'auto',
|
height: obj.height ? `${obj.height * scale}px` : 'auto',
|
||||||
@@ -129,12 +136,67 @@ const BookPage = forwardRef<HTMLDivElement, BookPageProps>(({ page, scale = 1 },
|
|||||||
textDecoration: 'underline',
|
textDecoration: 'underline',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
whiteSpace: 'pre-wrap',
|
whiteSpace: 'pre-wrap' as const,
|
||||||
|
cursor: 'pointer',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isInternalLink) {
|
||||||
|
// لینک داخلی - از button با onClick استفاده میکنیم
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={obj.id || index}
|
||||||
|
type="button"
|
||||||
|
style={{
|
||||||
|
...linkStyle,
|
||||||
|
zIndex: 9999,
|
||||||
|
pointerEvents: 'auto',
|
||||||
|
border: 'none',
|
||||||
|
background: 'none',
|
||||||
|
padding: 0,
|
||||||
|
cursor: 'pointer',
|
||||||
|
}}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
if (obj.linkUrl && onLinkClick) {
|
||||||
|
onLinkClick(obj.linkUrl);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onMouseDown={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
onTouchStart={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{obj.text || obj.linkUrl || ''}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// لینک خارجی - از <a> با target="_blank" استفاده میکنیم
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
key={obj.id || index}
|
||||||
|
href={obj.linkUrl || '#'}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
style={{
|
||||||
|
...linkStyle,
|
||||||
|
zIndex: 9999,
|
||||||
|
pointerEvents: 'auto',
|
||||||
|
}}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
onMouseDown={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{obj.text || obj.linkUrl || ''}
|
{obj.text || obj.linkUrl || ''}
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
case 'rectangle': {
|
case 'rectangle': {
|
||||||
if (obj.shapeType === 'circle') {
|
if (obj.shapeType === 'circle') {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ type BookViewerProps = {
|
|||||||
interface PageFlipAPI {
|
interface PageFlipAPI {
|
||||||
flipNext: () => void;
|
flipNext: () => void;
|
||||||
flipPrev: () => void;
|
flipPrev: () => void;
|
||||||
|
turnToPage: (page: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FlipBookInstance {
|
interface FlipBookInstance {
|
||||||
@@ -66,6 +67,81 @@ const BookViewer: FC<BookViewerProps> = ({ pages }) => {
|
|||||||
}
|
}
|
||||||
}, [currentPage]);
|
}, [currentPage]);
|
||||||
|
|
||||||
|
const handleLinkClick = useCallback((linkUrl: string) => {
|
||||||
|
if (linkUrl.startsWith('page://')) {
|
||||||
|
const action = linkUrl.replace('page://', '');
|
||||||
|
const pageFlipAPI = bookRef.current?.pageFlip();
|
||||||
|
|
||||||
|
if (!pageFlipAPI) return;
|
||||||
|
|
||||||
|
if (action === 'next') {
|
||||||
|
goToNextPage();
|
||||||
|
} else if (action === 'prev') {
|
||||||
|
goToPrevPage();
|
||||||
|
} else if (action === 'first') {
|
||||||
|
// رفتن به صفحه اول
|
||||||
|
const flipToFirst = () => {
|
||||||
|
if (currentPage > 0) {
|
||||||
|
pageFlipAPI.flipPrev();
|
||||||
|
setTimeout(() => {
|
||||||
|
setCurrentPage(prev => {
|
||||||
|
if (prev > 1) {
|
||||||
|
flipToFirst();
|
||||||
|
}
|
||||||
|
return prev - 1;
|
||||||
|
});
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
flipToFirst();
|
||||||
|
} else if (action === 'last') {
|
||||||
|
// رفتن به صفحه آخر
|
||||||
|
const lastPageIndex = pages.length - 1;
|
||||||
|
const flipToLast = () => {
|
||||||
|
if (currentPage < lastPageIndex) {
|
||||||
|
pageFlipAPI.flipNext();
|
||||||
|
setTimeout(() => {
|
||||||
|
setCurrentPage(prev => {
|
||||||
|
if (prev < lastPageIndex - 1) {
|
||||||
|
flipToLast();
|
||||||
|
}
|
||||||
|
return prev + 1;
|
||||||
|
});
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
flipToLast();
|
||||||
|
} else {
|
||||||
|
// شماره صفحه مشخص (مثل page://3)
|
||||||
|
const pageNum = parseInt(action, 10);
|
||||||
|
if (!isNaN(pageNum) && pageNum >= 1 && pageNum <= pages.length) {
|
||||||
|
const targetIndex = pageNum - 1;
|
||||||
|
const diff = targetIndex - currentPage;
|
||||||
|
|
||||||
|
const flipToTarget = (remaining: number) => {
|
||||||
|
if (remaining === 0) return;
|
||||||
|
|
||||||
|
if (remaining > 0) {
|
||||||
|
pageFlipAPI.flipNext();
|
||||||
|
setTimeout(() => {
|
||||||
|
setCurrentPage(prev => prev + 1);
|
||||||
|
flipToTarget(remaining - 1);
|
||||||
|
}, 300);
|
||||||
|
} else {
|
||||||
|
pageFlipAPI.flipPrev();
|
||||||
|
setTimeout(() => {
|
||||||
|
setCurrentPage(prev => prev - 1);
|
||||||
|
flipToTarget(remaining + 1);
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
flipToTarget(diff);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [currentPage, pages.length, goToNextPage, goToPrevPage]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center gap-4 md:gap-6 w-full h-full" dir="rtl">
|
<div className="flex flex-col items-center gap-4 md:gap-6 w-full h-full" dir="rtl">
|
||||||
{/* نمایش کتاب */}
|
{/* نمایش کتاب */}
|
||||||
@@ -100,7 +176,7 @@ const BookViewer: FC<BookViewerProps> = ({ pages }) => {
|
|||||||
swipeDistance={30}
|
swipeDistance={30}
|
||||||
startZIndex={1}
|
startZIndex={1}
|
||||||
showPageCorners={true}
|
showPageCorners={true}
|
||||||
disableFlipByClick={false}
|
disableFlipByClick={true}
|
||||||
className="flipbook-container mx-auto"
|
className="flipbook-container mx-auto"
|
||||||
style={{
|
style={{
|
||||||
direction: 'rtl',
|
direction: 'rtl',
|
||||||
@@ -108,7 +184,7 @@ const BookViewer: FC<BookViewerProps> = ({ pages }) => {
|
|||||||
onFlip={handlePageFlip}
|
onFlip={handlePageFlip}
|
||||||
>
|
>
|
||||||
{pages.map((page) => (
|
{pages.map((page) => (
|
||||||
<BookPage key={page.id} page={page} scale={0.5} />
|
<BookPage key={page.id} page={page} scale={0.5} onLinkClick={handleLinkClick} />
|
||||||
))}
|
))}
|
||||||
</HTMLFlipBook>
|
</HTMLFlipBook>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user