link to another pager
This commit is contained in:
@@ -128,6 +128,71 @@
|
||||
"fill": "#3b82f6",
|
||||
"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",
|
||||
"type": "image",
|
||||
@@ -328,6 +393,58 @@
|
||||
"strokeWidth": 2,
|
||||
"shapeType": "abstract",
|
||||
"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 = {
|
||||
page: PageData;
|
||||
scale?: number;
|
||||
onLinkClick?: (linkUrl: string) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -14,7 +15,7 @@ type BookPageProps = {
|
||||
* این کامپوننت به عنوان child برای HTMLFlipBook استفاده میشود
|
||||
* نیاز به 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)
|
||||
// در editor، getColorWithOpacity انتظار opacity 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',
|
||||
height: obj.height ? `${obj.height * scale}px` : 'auto',
|
||||
objectFit: 'contain',
|
||||
zIndex: index,
|
||||
pointerEvents: 'auto',
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
onMouseDown={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
onTouchStart={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
case 'link':
|
||||
return (
|
||||
<a
|
||||
key={obj.id || index}
|
||||
href={obj.linkUrl || '#'}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={{
|
||||
case 'link': {
|
||||
const isInternalLink = obj.linkUrl?.startsWith('page://');
|
||||
const linkStyle = {
|
||||
...baseStyle,
|
||||
width: obj.width ? `${obj.width * 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',
|
||||
display: 'flex',
|
||||
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 || ''}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
case 'rectangle': {
|
||||
if (obj.shapeType === 'circle') {
|
||||
|
||||
@@ -11,6 +11,7 @@ type BookViewerProps = {
|
||||
interface PageFlipAPI {
|
||||
flipNext: () => void;
|
||||
flipPrev: () => void;
|
||||
turnToPage: (page: number) => void;
|
||||
}
|
||||
|
||||
interface FlipBookInstance {
|
||||
@@ -66,6 +67,81 @@ const BookViewer: FC<BookViewerProps> = ({ pages }) => {
|
||||
}
|
||||
}, [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 (
|
||||
<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}
|
||||
startZIndex={1}
|
||||
showPageCorners={true}
|
||||
disableFlipByClick={false}
|
||||
disableFlipByClick={true}
|
||||
className="flipbook-container mx-auto"
|
||||
style={{
|
||||
direction: 'rtl',
|
||||
@@ -108,7 +184,7 @@ const BookViewer: FC<BookViewerProps> = ({ pages }) => {
|
||||
onFlip={handlePageFlip}
|
||||
>
|
||||
{pages.map((page) => (
|
||||
<BookPage key={page.id} page={page} scale={0.5} />
|
||||
<BookPage key={page.id} page={page} scale={0.5} onLinkClick={handleLinkClick} />
|
||||
))}
|
||||
</HTMLFlipBook>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user