Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 46926c66e7 | |||
| a448ff10de | |||
| 80a92ea4c4 | |||
| 63a774c4af |
@@ -276,3 +276,13 @@ html {
|
||||
.flipbook-container.flipbook-magnifier-active .stf__item {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -88,7 +88,7 @@ const ObjectRenderer = ({
|
||||
groupNode?.clearCache();
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [shouldApplyMask, isSelected, maskRelXDep, maskRelYDep, maskShape?.width, maskShape?.height, maskShape?.rotation, obj.width, obj.height, obj.rotation, obj.maskInvert, obj.strokeWidth, obj.stroke, obj.fill, obj.fillType, obj.gradient]);
|
||||
}, [shouldApplyMask, isSelected, maskRelXDep, maskRelYDep, maskShape?.width, maskShape?.height, maskShape?.rotation, obj.width, obj.height, obj.rotation, obj.maskInvert, obj.strokeWidth, obj.stroke, obj.fill, obj.fillType, obj.gradient, obj.blur]);
|
||||
|
||||
// Refresh cache after transformer is attached (when isSelected changes)
|
||||
useEffect(() => {
|
||||
|
||||
@@ -20,6 +20,7 @@ const ShapeSettings = ({ selectedObject, onUpdate }: ShapeSettingsProps) => {
|
||||
const baseStroke = selectedObject.stroke ?? "#1e40af";
|
||||
const baseStrokeWidth = selectedObject.strokeWidth ?? 0;
|
||||
const baseBorderRadius = selectedObject.borderRadius ?? 0;
|
||||
const baseBlur = selectedObject.blur ?? 0;
|
||||
const isSquareShape =
|
||||
selectedObject.shapeType === "square" || selectedObject.shapeType === undefined;
|
||||
|
||||
@@ -179,6 +180,35 @@ const ShapeSettings = ({ selectedObject, onUpdate }: ShapeSettingsProps) => {
|
||||
min={0}
|
||||
/>
|
||||
)}
|
||||
<div className="space-y-1">
|
||||
<label className="text-sm">بلور</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="range"
|
||||
min={0}
|
||||
max={50}
|
||||
value={baseBlur}
|
||||
onChange={(e) =>
|
||||
onUpdate(selectedObject.id, {
|
||||
blur: Math.max(0, Math.min(50, Number(e.target.value))),
|
||||
})
|
||||
}
|
||||
className="w-full"
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
min={0}
|
||||
max={50}
|
||||
value={baseBlur}
|
||||
onChange={(e) =>
|
||||
onUpdate(selectedObject.id, {
|
||||
blur: Math.max(0, Math.min(50, Number(e.target.value) || 0)),
|
||||
})
|
||||
}
|
||||
className="w-16 h-9 rounded-lg border border-border px-2 text-xs"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Star } from "react-konva";
|
||||
import Konva from "konva";
|
||||
import type { ShapeProps } from "./types";
|
||||
import { getKonvaGradientProps } from "../../utils/gradient";
|
||||
import { getKonvaBlurProps, useShapeBlurCache } from "../../utils/shapeBlur";
|
||||
|
||||
const AbstractShape = ({ obj, isSelected, onSelect, onUpdate, draggable }: ShapeProps) => {
|
||||
const shapeRef = useRef<Konva.Star>(null);
|
||||
@@ -18,6 +19,18 @@ const AbstractShape = ({ obj, isSelected, onSelect, onUpdate, draggable }: Shape
|
||||
|
||||
const actualStrokeWidth = obj.strokeWidth ?? 0;
|
||||
const hasStroke = actualStrokeWidth > 0;
|
||||
const blurProps = isMask ? {} : getKonvaBlurProps(obj.blur);
|
||||
|
||||
useShapeBlurCache(shapeRef, isMask ? 0 : obj.blur, [
|
||||
obj.width,
|
||||
obj.height,
|
||||
obj.fill,
|
||||
obj.fillType,
|
||||
obj.gradient,
|
||||
obj.stroke,
|
||||
actualStrokeWidth,
|
||||
obj.blur,
|
||||
]);
|
||||
|
||||
// برای نمایش انتخاب: اگر strokeWidth واقعی > 0 است، از آن استفاده کن، وگرنه stroke را برای انتخاب نمایش بده
|
||||
const displayStroke = isSelected
|
||||
@@ -57,6 +70,7 @@ const AbstractShape = ({ obj, isSelected, onSelect, onUpdate, draggable }: Shape
|
||||
strokeWidth={displayStrokeWidth}
|
||||
dash={isMask && showGuide ? [10, 5] : undefined}
|
||||
rotation={obj.rotation || 0}
|
||||
{...blurProps}
|
||||
draggable={draggable}
|
||||
onClick={(e) => {
|
||||
if (shapeRef.current) {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Circle } from "react-konva";
|
||||
import Konva from "konva";
|
||||
import type { ShapeProps } from "./types";
|
||||
import { getKonvaGradientProps } from "../../utils/gradient";
|
||||
import { getKonvaBlurProps, useShapeBlurCache } from "../../utils/shapeBlur";
|
||||
|
||||
const CircleShape = ({ obj, isSelected, onSelect, onUpdate, draggable }: ShapeProps) => {
|
||||
const shapeRef = useRef<Konva.Circle>(null);
|
||||
@@ -14,6 +15,17 @@ const CircleShape = ({ obj, isSelected, onSelect, onUpdate, draggable }: ShapePr
|
||||
|
||||
const actualStrokeWidth = obj.strokeWidth ?? 0;
|
||||
const hasStroke = actualStrokeWidth > 0;
|
||||
const blurProps = isMask ? {} : getKonvaBlurProps(obj.blur);
|
||||
|
||||
useShapeBlurCache(shapeRef, isMask ? 0 : obj.blur, [
|
||||
obj.width,
|
||||
obj.fill,
|
||||
obj.fillType,
|
||||
obj.gradient,
|
||||
obj.stroke,
|
||||
actualStrokeWidth,
|
||||
obj.blur,
|
||||
]);
|
||||
|
||||
// برای نمایش انتخاب: اگر strokeWidth واقعی > 0 است، از آن استفاده کن، وگرنه stroke را برای انتخاب نمایش بده
|
||||
const displayStroke = isSelected
|
||||
@@ -46,6 +58,7 @@ const CircleShape = ({ obj, isSelected, onSelect, onUpdate, draggable }: ShapePr
|
||||
strokeWidth={displayStrokeWidth}
|
||||
dash={isMask && showGuide ? [10, 5] : undefined}
|
||||
rotation={obj.rotation || 0}
|
||||
{...blurProps}
|
||||
draggable={draggable}
|
||||
onClick={(e) => {
|
||||
if (shapeRef.current) {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Rect } from "react-konva";
|
||||
import Konva from "konva";
|
||||
import type { ShapeProps } from "./types";
|
||||
import { getKonvaGradientProps } from "../../utils/gradient";
|
||||
import { getKonvaBlurProps, useShapeBlurCache } from "../../utils/shapeBlur";
|
||||
|
||||
const RectangleShape = ({ obj, isSelected, onSelect, onUpdate, draggable }: ShapeProps) => {
|
||||
const shapeRef = useRef<Konva.Rect>(null);
|
||||
@@ -15,6 +16,19 @@ const RectangleShape = ({ obj, isSelected, onSelect, onUpdate, draggable }: Shap
|
||||
const actualStrokeWidth = obj.strokeWidth ?? 0;
|
||||
const hasStroke = actualStrokeWidth > 0;
|
||||
const cornerRadius = Math.max(0, obj.borderRadius ?? 0);
|
||||
const blurProps = isMask ? {} : getKonvaBlurProps(obj.blur);
|
||||
|
||||
useShapeBlurCache(shapeRef, isMask ? 0 : obj.blur, [
|
||||
obj.width,
|
||||
obj.height,
|
||||
cornerRadius,
|
||||
obj.fill,
|
||||
obj.fillType,
|
||||
obj.gradient,
|
||||
obj.stroke,
|
||||
actualStrokeWidth,
|
||||
obj.blur,
|
||||
]);
|
||||
|
||||
// برای نمایش انتخاب: اگر strokeWidth واقعی > 0 است، از آن استفاده کن، وگرنه stroke را برای انتخاب نمایش بده
|
||||
const displayStroke = isSelected
|
||||
@@ -54,6 +68,7 @@ const RectangleShape = ({ obj, isSelected, onSelect, onUpdate, draggable }: Shap
|
||||
strokeWidth={displayStrokeWidth}
|
||||
dash={isMask && showGuide ? [10, 5] : undefined}
|
||||
rotation={obj.rotation || 0}
|
||||
{...blurProps}
|
||||
draggable={draggable}
|
||||
onClick={(e) => {
|
||||
if (shapeRef.current) {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { RegularPolygon } from "react-konva";
|
||||
import Konva from "konva";
|
||||
import type { ShapeProps } from "./types";
|
||||
import { getKonvaGradientProps } from "../../utils/gradient";
|
||||
import { getKonvaBlurProps, useShapeBlurCache } from "../../utils/shapeBlur";
|
||||
|
||||
const TriangleShape = ({ obj, isSelected, onSelect, onUpdate, draggable }: ShapeProps) => {
|
||||
const shapeRef = useRef<Konva.RegularPolygon>(null);
|
||||
@@ -15,6 +16,18 @@ const TriangleShape = ({ obj, isSelected, onSelect, onUpdate, draggable }: Shape
|
||||
|
||||
const actualStrokeWidth = obj.strokeWidth ?? 0;
|
||||
const hasStroke = actualStrokeWidth > 0;
|
||||
const blurProps = isMask ? {} : getKonvaBlurProps(obj.blur);
|
||||
|
||||
useShapeBlurCache(shapeRef, isMask ? 0 : obj.blur, [
|
||||
obj.width,
|
||||
obj.height,
|
||||
obj.fill,
|
||||
obj.fillType,
|
||||
obj.gradient,
|
||||
obj.stroke,
|
||||
actualStrokeWidth,
|
||||
obj.blur,
|
||||
]);
|
||||
|
||||
// برای نمایش انتخاب: اگر strokeWidth واقعی > 0 است، از آن استفاده کن، وگرنه stroke را برای انتخاب نمایش بده
|
||||
const displayStroke = isSelected
|
||||
@@ -53,6 +66,7 @@ const TriangleShape = ({ obj, isSelected, onSelect, onUpdate, draggable }: Shape
|
||||
strokeWidth={displayStrokeWidth}
|
||||
dash={isMask && showGuide ? [10, 5] : undefined}
|
||||
rotation={obj.rotation || 0}
|
||||
{...blurProps}
|
||||
draggable={draggable}
|
||||
onClick={(e) => {
|
||||
if (shapeRef.current) {
|
||||
|
||||
@@ -82,6 +82,8 @@ export type EditorObject = {
|
||||
scaleY?: number;
|
||||
shapeType?: ShapeType;
|
||||
borderRadius?: number;
|
||||
/** میزان بلور شکل به پیکسل (۰ = بدون بلور) */
|
||||
blur?: number;
|
||||
tableData?: TableData;
|
||||
visible?: boolean;
|
||||
isMask?: boolean;
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { useLayoutEffect, type CSSProperties } from "react";
|
||||
import type Konva from "konva";
|
||||
import KonvaLib from "konva";
|
||||
|
||||
export function getKonvaBlurProps(blur?: number) {
|
||||
const blurRadius = blur ?? 0;
|
||||
if (blurRadius <= 0) return {};
|
||||
return {
|
||||
filters: [KonvaLib.Filters.Blur],
|
||||
blurRadius,
|
||||
};
|
||||
}
|
||||
|
||||
export function getCssBlurStyle(blur?: number, scale = 1): CSSProperties {
|
||||
const blurPx = blur ?? 0;
|
||||
if (blurPx <= 0) return {};
|
||||
return { filter: `blur(${blurPx * scale}px)` };
|
||||
}
|
||||
|
||||
export function useShapeBlurCache(
|
||||
shapeRef: React.RefObject<Konva.Shape | null>,
|
||||
blur: number | undefined,
|
||||
cacheKey: unknown,
|
||||
) {
|
||||
useLayoutEffect(() => {
|
||||
const node = shapeRef.current;
|
||||
if (!node) return;
|
||||
|
||||
const blurRadius = blur ?? 0;
|
||||
if (blurRadius > 0) {
|
||||
node.clearCache();
|
||||
node.cache({
|
||||
offset: blurRadius,
|
||||
drawBorder: false,
|
||||
});
|
||||
} else {
|
||||
node.clearCache();
|
||||
}
|
||||
|
||||
node.getLayer()?.batchDraw();
|
||||
}, [blur, cacheKey, shapeRef]);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { forwardRef } from 'react';
|
||||
import { forwardRef, memo } from 'react';
|
||||
import { type PageData } from '../types';
|
||||
import type { EditorObject } from '@/pages/editor/store/editorStore';
|
||||
import { toCssLinearGradient, getSvgGradientEndpoints } from '@/pages/editor/utils/gradient';
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
resolveTextMaxWidth,
|
||||
usesWrappedLayout,
|
||||
} from '@/pages/editor/utils/textStyle';
|
||||
import { getCssBlurStyle } from '@/pages/editor/utils/shapeBlur';
|
||||
import '@/pages/viewer/styles/entranceAnimations.css';
|
||||
import { mergeEntranceAnimationStyle } from '@/pages/viewer/utils/entranceAnimationStyle';
|
||||
import { getMaskImageStyle, getMaskedLayout } from '@/pages/viewer/utils/maskStyle';
|
||||
@@ -84,7 +85,7 @@ type BookPageProps = {
|
||||
* این کامپوننت به عنوان child برای HTMLFlipBook استفاده میشود
|
||||
* نیاز به forwardRef دارد تا react-pageflip بتواند ref را مدیریت کند
|
||||
*/
|
||||
const BookPage = forwardRef<HTMLDivElement, BookPageProps>(
|
||||
const BookPage = memo(forwardRef<HTMLDivElement, BookPageProps>(
|
||||
({ page, scale = 1, pageWidth, pageHeight, onLinkClick, backgroundType, backgroundColor, backgroundGradient, backgroundImageUrl, backgroundVideoUrl, entrancePhase = 'idle', disableEntranceAnimations = false, staticMedia = false, showPaperShadow = true, useHardPage = false, idSuffix = '' }, ref) => {
|
||||
const phase: EntrancePhase = disableEntranceAnimations ? 'settled' : entrancePhase;
|
||||
// تابع برای تبدیل opacity به رنگ (مثل editor)
|
||||
@@ -174,6 +175,7 @@ const BookPage = forwardRef<HTMLDivElement, BookPageProps>(
|
||||
obj.fillType === 'gradient' && obj.gradient
|
||||
? { backgroundImage: toCssLinearGradient(obj.gradient) }
|
||||
: { backgroundColor: obj.fill || 'transparent' };
|
||||
const shapeBlurStyle = getCssBlurStyle(obj.blur, scale);
|
||||
|
||||
switch (obj.type) {
|
||||
case 'text': {
|
||||
@@ -487,6 +489,7 @@ const BookPage = forwardRef<HTMLDivElement, BookPageProps>(
|
||||
height: `${radius * 2}px`,
|
||||
borderRadius: '50%',
|
||||
...objectFillStyle,
|
||||
...shapeBlurStyle,
|
||||
border: hasStroke && obj.stroke
|
||||
? `${actualStrokeWidth * scale}px solid ${obj.stroke}`
|
||||
: 'none',
|
||||
@@ -549,6 +552,7 @@ const BookPage = forwardRef<HTMLDivElement, BookPageProps>(
|
||||
width: `${size}px`,
|
||||
height: `${size}px`,
|
||||
opacity: (obj.opacity ?? 100) / 100,
|
||||
...shapeBlurStyle,
|
||||
transform: obj.rotation ? `rotate(${obj.rotation}deg)` : undefined,
|
||||
transformOrigin: 'center center',
|
||||
zIndex: index,
|
||||
@@ -608,6 +612,7 @@ const BookPage = forwardRef<HTMLDivElement, BookPageProps>(
|
||||
height: `${radius * 2}px`,
|
||||
clipPath: 'polygon(50% 0%, 61% 35%, 98% 35%, 68% 57%, 79% 91%, 50% 70%, 21% 91%, 32% 57%, 2% 35%, 39% 35%)',
|
||||
...objectFillStyle,
|
||||
...shapeBlurStyle,
|
||||
border: obj.stroke
|
||||
? `${(obj.strokeWidth || 1) * scale}px solid ${obj.stroke}`
|
||||
: 'none',
|
||||
@@ -630,6 +635,7 @@ const BookPage = forwardRef<HTMLDivElement, BookPageProps>(
|
||||
width: `${(obj.width || 100) * scale}px`,
|
||||
height: `${(obj.height || 100) * scale}px`,
|
||||
...objectFillStyle,
|
||||
...shapeBlurStyle,
|
||||
borderRadius: `${Math.max(0, (obj.borderRadius || 0) * scale)}px`,
|
||||
border: hasStroke && obj.stroke
|
||||
? `${actualStrokeWidth * scale}px solid ${obj.stroke}`
|
||||
@@ -1037,7 +1043,8 @@ const BookPage = forwardRef<HTMLDivElement, BookPageProps>(
|
||||
{useHardPage && showPaperShadow && <div aria-hidden className="page-paper-shadow" />}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
BookPage.displayName = 'BookPage';
|
||||
|
||||
|
||||
@@ -126,13 +126,23 @@ const BookViewer: FC<BookViewerProps> = ({ pages, catalogSize, documentSettings
|
||||
const bookRef = useRef<FlipBookInstance | null>(null);
|
||||
const bookAreaRef = useRef<HTMLDivElement | null>(null);
|
||||
const flipbookWrapperRef = useRef<HTMLDivElement | null>(null);
|
||||
const spineOverlayRef = useRef<HTMLDivElement | null>(null);
|
||||
const pageFlipHandlerRef = useRef<(e: FlipEvent) => void>(() => {});
|
||||
const [currentPage, setCurrentPage] = useState(0);
|
||||
const [isAutoPlayActive, setIsAutoPlayActive] = useState(false);
|
||||
const [startPage, setStartPage] = useState(0);
|
||||
const [magnifierEnabled, setMagnifierEnabled] = useState(false);
|
||||
/** در حالت read خط وسط روی شکاف جلد دیده میشود؛ هنگام ورقزدن باید زیر ورق باشد */
|
||||
const [isBookFlipping, setIsBookFlipping] = useState(false);
|
||||
/** ref نه state — تغییر z-index هنگام hover ورق نباید BookPage را remount/re-render کند (انیمیشن ورود دوباره اجرا میشد) */
|
||||
const isBookFlippingRef = useRef(false);
|
||||
const setBookFlippingVisual = useCallback((flipping: boolean) => {
|
||||
if (isBookFlippingRef.current === flipping) return;
|
||||
isBookFlippingRef.current = flipping;
|
||||
flipbookWrapperRef.current?.classList.toggle("relative", 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-0", flipping);
|
||||
}, []);
|
||||
const isDesktop = useIsDesktop();
|
||||
const legacyIOS = useLegacyIOSSafari();
|
||||
const isBrowserFullscreen = useIsBrowserFullscreen();
|
||||
@@ -142,8 +152,8 @@ const BookViewer: FC<BookViewerProps> = ({ pages, catalogSize, documentSettings
|
||||
const leftToRightFlip = documentSettings?.leftToRightFlip ?? false;
|
||||
const displayStyle = documentSettings?.displayStyle ?? "double";
|
||||
const usePortrait = !isDesktop || displayStyle === "single";
|
||||
/** react-pageflip از RTL پشتیبانی نمیکند؛ ترتیب صفحات برعکس + شروع از آخر (فقط دسکتاپ) */
|
||||
const isRtlBook = !leftToRightFlip && isDesktop;
|
||||
/** react-pageflip از RTL پشتیبانی نمیکند؛ ترتیب صفحات برعکس + شروع از آخر */
|
||||
const isRtlBook = !leftToRightFlip;
|
||||
/**
|
||||
* page-flip فقط صفحهٔ با ایندکس 0 را بهعنوان جلد تکی در نظر میگیرد. بعد از برعکسکردن
|
||||
* آرایه برای RTL، جلد واقعی (صفحهٔ منطقی 0) در انتهای آرایه قرار میگیرد و اگر تعداد کل
|
||||
@@ -222,6 +232,8 @@ const BookViewer: FC<BookViewerProps> = ({ pages, catalogSize, documentSettings
|
||||
const pagePixelWidth = viewportFit.w;
|
||||
const pagePixelHeight = viewportFit.h;
|
||||
const contentScale = viewportFit.contentScale;
|
||||
const flipDisabledOnMobile = !isDesktop && magnifierEnabled;
|
||||
const magnifierLockedPageId = usePortrait ? (pages[currentPage]?.id ?? null) : null;
|
||||
|
||||
const flipKey = `${usePortrait ? "portrait" : "spread"}-${pagePixelWidth}-${pagePixelHeight}-${flipIndexCount}`;
|
||||
|
||||
@@ -390,39 +402,47 @@ const BookViewer: FC<BookViewerProps> = ({ pages, catalogSize, documentSettings
|
||||
[flipIndexCount, scheduleEntranceForIndices, getVisibleLogicalIndices, toLogicalIndex],
|
||||
);
|
||||
|
||||
const beginPageNavigation = useCallback(() => {
|
||||
if (flipDisabledOnMobile) return false;
|
||||
setBookFlippingVisual(true);
|
||||
return true;
|
||||
}, [flipDisabledOnMobile, setBookFlippingVisual]);
|
||||
|
||||
/** بعد از اتمام انیمیشن ورق، ایندکس واقعی کتاب را میگیریم و انیمیشن ورود را یکبار شروع میکنیم */
|
||||
const handleChangeState = useCallback(
|
||||
(e: PageFlipStateEvent) => {
|
||||
if (e.data === "read") {
|
||||
setIsBookFlipping(false);
|
||||
setBookFlippingVisual(false);
|
||||
syncPageIndexFromBook();
|
||||
commitEntranceAtCurrentSpread();
|
||||
return;
|
||||
}
|
||||
setIsBookFlipping(true);
|
||||
setBookFlippingVisual(true);
|
||||
},
|
||||
[syncPageIndexFromBook, commitEntranceAtCurrentSpread],
|
||||
[syncPageIndexFromBook, commitEntranceAtCurrentSpread, setBookFlippingVisual],
|
||||
);
|
||||
|
||||
const goToNextPage = useCallback(() => {
|
||||
if (currentPage >= pages.length - 1) return;
|
||||
if (!beginPageNavigation()) return;
|
||||
const api = bookRef.current?.pageFlip() as PageFlipWithFlipController | undefined;
|
||||
if (isRtlBook) {
|
||||
triggerFlipPrev(api);
|
||||
} else {
|
||||
api?.flipNext();
|
||||
}
|
||||
}, [currentPage, pages.length, isRtlBook]);
|
||||
}, [currentPage, pages.length, isRtlBook, beginPageNavigation]);
|
||||
|
||||
const goToPrevPage = useCallback(() => {
|
||||
if (currentPage <= 0) return;
|
||||
if (!beginPageNavigation()) return;
|
||||
const api = bookRef.current?.pageFlip() as PageFlipWithFlipController | undefined;
|
||||
if (isRtlBook) {
|
||||
api?.flipNext();
|
||||
} else {
|
||||
triggerFlipPrev(api);
|
||||
}
|
||||
}, [currentPage, isRtlBook]);
|
||||
}, [currentPage, isRtlBook, beginPageNavigation]);
|
||||
|
||||
const stopAutoPlayByUser = useCallback(() => {
|
||||
if (isAutoPlayActive) {
|
||||
@@ -450,10 +470,17 @@ const BookViewer: FC<BookViewerProps> = ({ pages, catalogSize, documentSettings
|
||||
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||
}, [magnifierEnabled]);
|
||||
|
||||
useEffect(() => {
|
||||
if (flipDisabledOnMobile && isAutoPlayActive) {
|
||||
setIsAutoPlayActive(false);
|
||||
}
|
||||
}, [flipDisabledOnMobile, isAutoPlayActive]);
|
||||
|
||||
const navigateToPageIndex = useCallback(
|
||||
(targetIndex: number) => {
|
||||
const pageFlipAPI = bookRef.current?.pageFlip();
|
||||
if (!pageFlipAPI) return;
|
||||
if (!beginPageNavigation()) return;
|
||||
|
||||
const logicalIdx = Math.max(0, Math.min(targetIndex, pages.length - 1));
|
||||
const flipIdx = toFlipIndex(logicalIdx);
|
||||
@@ -461,7 +488,7 @@ const BookViewer: FC<BookViewerProps> = ({ pages, catalogSize, documentSettings
|
||||
lastFlipPageRef.current = logicalIdx;
|
||||
scheduleEntranceForIndices(getVisibleLogicalIndices(flipIdx));
|
||||
},
|
||||
[pages.length, scheduleEntranceForIndices, getVisibleLogicalIndices, toFlipIndex],
|
||||
[pages.length, scheduleEntranceForIndices, getVisibleLogicalIndices, toFlipIndex, beginPageNavigation],
|
||||
);
|
||||
|
||||
const handleLinkClick = useCallback(
|
||||
@@ -470,6 +497,7 @@ const BookViewer: FC<BookViewerProps> = ({ pages, catalogSize, documentSettings
|
||||
stopAutoPlayByUser();
|
||||
|
||||
if (!linkUrl.startsWith("page://")) return;
|
||||
if (flipDisabledOnMobile) return;
|
||||
|
||||
const action = linkUrl.replace("page://", "");
|
||||
|
||||
@@ -488,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);
|
||||
@@ -509,7 +537,7 @@ const BookViewer: FC<BookViewerProps> = ({ pages, catalogSize, documentSettings
|
||||
|
||||
// پخش خودکار
|
||||
useEffect(() => {
|
||||
if (!isAutoPlayActive || pages.length <= 1) return;
|
||||
if (!isAutoPlayActive || pages.length <= 1 || flipDisabledOnMobile) return;
|
||||
const timer = setInterval(() => {
|
||||
if (currentPage >= pages.length - 1) {
|
||||
bookRef.current?.pageFlip()?.turnToPage?.(toFlipIndex(0));
|
||||
@@ -525,7 +553,7 @@ const BookViewer: FC<BookViewerProps> = ({ pages, catalogSize, documentSettings
|
||||
}
|
||||
}, AUTO_PLAY_INTERVAL_MS);
|
||||
return () => clearInterval(timer);
|
||||
}, [isAutoPlayActive, currentPage, pages.length, isRtlBook, toFlipIndex]);
|
||||
}, [isAutoPlayActive, currentPage, pages.length, isRtlBook, toFlipIndex, flipDisabledOnMobile]);
|
||||
|
||||
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">
|
||||
@@ -534,10 +562,8 @@ const BookViewer: FC<BookViewerProps> = ({ pages, catalogSize, documentSettings
|
||||
{/* Border و Shadow عمودی در وسط (فقط حالت دوصفحهای دسکتاپ) */}
|
||||
{!usePortrait && (
|
||||
<div
|
||||
className={clx(
|
||||
"hidden md:block absolute inset-y-0 left-1/2 -translate-x-1/2 w-px border-r border-gray-300 pointer-events-none transition-none",
|
||||
isBookFlipping ? "z-0" : "z-50",
|
||||
)}
|
||||
ref={spineOverlayRef}
|
||||
className="hidden md:block absolute inset-y-0 left-1/2 -translate-x-1/2 w-px border-r border-gray-300 pointer-events-none transition-none z-50"
|
||||
style={{
|
||||
boxShadow: "-5px 0 15px rgba(0, 0, 0, 0.15), 5px 0 15px rgba(0, 0, 0, 0.15)",
|
||||
}}
|
||||
@@ -550,12 +576,10 @@ const BookViewer: FC<BookViewerProps> = ({ pages, catalogSize, documentSettings
|
||||
<div
|
||||
ref={flipbookWrapperRef}
|
||||
dir="ltr"
|
||||
className={clx(
|
||||
"max-w-full flex justify-center shrink-0 rounded-sm",
|
||||
isBookFlipping && "relative z-10",
|
||||
)}
|
||||
className="max-w-full flex justify-center shrink-0 rounded-sm"
|
||||
style={{
|
||||
cursor: magnifierEnabled ? "zoom-in" : undefined,
|
||||
touchAction: magnifierEnabled ? "none" : undefined,
|
||||
overflow: "hidden",
|
||||
width: usePortrait ? pagePixelWidth : pagePixelWidth * 2,
|
||||
height: pagePixelHeight,
|
||||
@@ -579,12 +603,12 @@ const BookViewer: FC<BookViewerProps> = ({ pages, catalogSize, documentSettings
|
||||
autoSize={false}
|
||||
maxShadowOpacity={legacyIOS ? 0 : isDesktop ? 0.55 : 0.32}
|
||||
showCover={hasMultiplePages}
|
||||
mobileScrollSupport={!usePortrait}
|
||||
mobileScrollSupport={!usePortrait && !magnifierEnabled}
|
||||
clickEventForward={true}
|
||||
useMouseEvents={hasMultiplePages && !magnifierEnabled}
|
||||
swipeDistance={hasMultiplePages ? (usePortrait ? 48 : 30) : 9999}
|
||||
swipeDistance={flipDisabledOnMobile || !hasMultiplePages ? 9999 : usePortrait ? 48 : 30}
|
||||
startZIndex={1}
|
||||
showPageCorners={hasMultiplePages}
|
||||
showPageCorners={hasMultiplePages && !flipDisabledOnMobile}
|
||||
disableFlipByClick={true}
|
||||
className={clx(
|
||||
"flipbook-container mx-auto max-w-full",
|
||||
@@ -622,6 +646,9 @@ const BookViewer: FC<BookViewerProps> = ({ pages, catalogSize, documentSettings
|
||||
enabled={magnifierEnabled}
|
||||
containerRef={bookAreaRef}
|
||||
interactionRef={flipbookWrapperRef}
|
||||
isFlippingRef={isBookFlippingRef}
|
||||
lockedPageId={magnifierLockedPageId}
|
||||
blockFlipTouch={flipDisabledOnMobile}
|
||||
pages={displayPages}
|
||||
pageWidth={pagePixelWidth}
|
||||
pageHeight={pagePixelHeight}
|
||||
@@ -647,9 +674,13 @@ const BookViewer: FC<BookViewerProps> = ({ pages, catalogSize, documentSettings
|
||||
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"
|
||||
aria-label={leftToRightFlip ? "صفحه بعد" : "صفحه قبل"}
|
||||
title={flipDisabledOnMobile ? "برای ورق زدن ابتدا ذرهبین را خاموش کنید" : undefined}
|
||||
>
|
||||
<ArrowRight2 color="black" size={20} className="text-gray-700 md:w-6 md:h-6" />
|
||||
</button>
|
||||
@@ -676,9 +707,13 @@ const BookViewer: FC<BookViewerProps> = ({ pages, catalogSize, documentSettings
|
||||
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"
|
||||
aria-label={leftToRightFlip ? "صفحه قبل" : "صفحه بعد"}
|
||||
title={flipDisabledOnMobile ? "برای ورق زدن ابتدا ذرهبین را خاموش کنید" : undefined}
|
||||
>
|
||||
<ArrowLeft2 color="black" size={20} className="text-gray-700 md:w-6 md:h-6" />
|
||||
</button>
|
||||
|
||||
@@ -21,6 +21,11 @@ export type MagnifierProps = {
|
||||
contentScale: number;
|
||||
pageDefaults: PageBackgroundDefaults;
|
||||
useHardPage?: boolean;
|
||||
/** در حین انیمیشن ورقخوردن true است */
|
||||
isFlippingRef: RefObject<boolean>;
|
||||
/** در حالت تکصفحه (موبایل) همیشه صفحهٔ فعلی بزرگنمایی میشود */
|
||||
lockedPageId?: number | null;
|
||||
blockFlipTouch?: boolean;
|
||||
/** با تغییر آن (ورق خوردن) ذرهبین ریست میشود */
|
||||
pageIndex?: number;
|
||||
};
|
||||
@@ -36,6 +41,9 @@ const Magnifier = memo(
|
||||
contentScale,
|
||||
pageDefaults,
|
||||
useHardPage = false,
|
||||
isFlippingRef,
|
||||
lockedPageId = null,
|
||||
blockFlipTouch = false,
|
||||
pageIndex,
|
||||
}: MagnifierProps) => {
|
||||
const overlayRef = useRef<HTMLDivElement>(null);
|
||||
@@ -54,15 +62,23 @@ const Magnifier = memo(
|
||||
overlayRef,
|
||||
lensRef,
|
||||
mirrorRef,
|
||||
isFlippingRef,
|
||||
lockedPageId,
|
||||
blockFlipTouch,
|
||||
pageIndex,
|
||||
onActivePageChange: handleActivePageChange,
|
||||
});
|
||||
|
||||
const activePage = useMemo(
|
||||
() => (activePageId != null ? pages.find((p) => p.id === activePageId) : undefined),
|
||||
[pages, activePageId],
|
||||
const lockedPage = useMemo(
|
||||
() => (lockedPageId != null ? pages.find((p) => p.id === lockedPageId) : undefined),
|
||||
[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(
|
||||
() => (activePage ? resolvePageBackground(activePage, pageDefaults) : null),
|
||||
[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 در حالت اسپرد */
|
||||
export function findPageAtPoint(
|
||||
root: HTMLElement,
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { type RefObject, useCallback, useEffect, useLayoutEffect, useRef } from "react";
|
||||
import { MAGNIFIER_SIZE, ZOOM_SCALE } from "./constants";
|
||||
import { computeMagnifierGeometry, findPageAtPoint } from "./magnifierUtils";
|
||||
import { computeMagnifierGeometry, findPageAtPoint, findPageById } from "./magnifierUtils";
|
||||
|
||||
type UseMagnifierOptions = {
|
||||
enabled: boolean;
|
||||
/** ناحیهٔ موقعیتدهی لنز (مرجع مختصات نسبی) */
|
||||
/** مرجع مختصات برای موقعیتدهی لنز (معمولاً ناحیهٔ اطراف کتاب) */
|
||||
containerRef: RefObject<HTMLElement | null>;
|
||||
/** ناحیهٔ تعامل و جستجوی صفحات (wrapper کتاب) */
|
||||
interactionRef: RefObject<HTMLElement | null>;
|
||||
@@ -12,6 +12,12 @@ type UseMagnifierOptions = {
|
||||
lensRef: RefObject<HTMLElement | null>;
|
||||
/** لایهٔ داخل لنز که محتوای صفحه (BookPage با مقیاس بزرگ) داخلش رندر میشود */
|
||||
mirrorRef: RefObject<HTMLElement | null>;
|
||||
/** در حین انیمیشن ورقخوردن true است — hit-test و بهروزرسانی لنز متوقف میشود */
|
||||
isFlippingRef: RefObject<boolean>;
|
||||
/** در حالت تکصفحه (موبایل) همیشه همین صفحه بزرگنمایی میشود، نه نتیجهٔ hit-test */
|
||||
lockedPageId?: number | null;
|
||||
/** جلوگیری از رسیدن touch به page-flip (کتابخانه تنظیمات را بعد از mount بهروز نمیکند) */
|
||||
blockFlipTouch?: boolean;
|
||||
/** با تغییر آن (ورق خوردن) وضعیت فعلی ریست میشود */
|
||||
pageIndex?: number;
|
||||
/** وقتی صفحهٔ زیر ماوس عوض شود صدا زده میشود تا محتوای مناسب رندر شود */
|
||||
@@ -25,12 +31,19 @@ export function useMagnifier({
|
||||
overlayRef,
|
||||
lensRef,
|
||||
mirrorRef,
|
||||
isFlippingRef,
|
||||
lockedPageId = null,
|
||||
blockFlipTouch = false,
|
||||
pageIndex,
|
||||
onActivePageChange,
|
||||
}: UseMagnifierOptions) {
|
||||
const rafIdRef = useRef<number | null>(null);
|
||||
const pendingPointRef = useRef<{ x: number; y: number } | null>(null);
|
||||
const lastPageIdRef = useRef<number | null>(null);
|
||||
const touchActiveRef = useRef(false);
|
||||
const lockedPageIdRef = useRef(lockedPageId);
|
||||
|
||||
lockedPageIdRef.current = lockedPageId;
|
||||
|
||||
const hideLens = useCallback(() => {
|
||||
const lens = lensRef.current;
|
||||
@@ -46,21 +59,11 @@ export function useMagnifier({
|
||||
}
|
||||
}, [onActivePageChange]);
|
||||
|
||||
// با تغییر صفحه (ورق خوردن) باید دوباره از صفر hit-test انجام شود
|
||||
useEffect(() => {
|
||||
if (!enabled) return;
|
||||
pendingPointRef.current = null;
|
||||
hideLens();
|
||||
resetActivePage();
|
||||
}, [pageIndex, enabled, hideLens, resetActivePage]);
|
||||
const applyGeometryRef = useRef<(clientX: number, clientY: number) => void>(() => {});
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!enabled) {
|
||||
pendingPointRef.current = null;
|
||||
if (rafIdRef.current != null) {
|
||||
cancelAnimationFrame(rafIdRef.current);
|
||||
rafIdRef.current = null;
|
||||
}
|
||||
applyGeometryRef.current = (clientX: number, clientY: number) => {
|
||||
if (isFlippingRef.current) {
|
||||
hideLens();
|
||||
resetActivePage();
|
||||
return;
|
||||
@@ -68,15 +71,15 @@ export function useMagnifier({
|
||||
|
||||
const container = containerRef.current;
|
||||
const interaction = interactionRef.current;
|
||||
if (!container || !interaction) return;
|
||||
|
||||
const half = MAGNIFIER_SIZE / 2;
|
||||
|
||||
const applyGeometry = (clientX: number, clientY: number) => {
|
||||
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) {
|
||||
hideLens();
|
||||
resetActivePage();
|
||||
@@ -96,6 +99,7 @@ export function useMagnifier({
|
||||
onActivePageChange(pageId);
|
||||
}
|
||||
|
||||
const half = MAGNIFIER_SIZE / 2;
|
||||
const { pageOffsetX, pageOffsetY, localX, localY, clampedX, clampedY } = geometry;
|
||||
|
||||
lens.style.visibility = "visible";
|
||||
@@ -103,66 +107,146 @@ export function useMagnifier({
|
||||
|
||||
const mirror = mirrorRef.current;
|
||||
if (mirror) {
|
||||
const tx = half - localX * ZOOM_SCALE;
|
||||
const ty = half - localY * ZOOM_SCALE;
|
||||
mirror.style.transform = `translate3d(${tx}px, ${ty}px, 0)`;
|
||||
mirror.style.transform = `translate3d(${half - localX * ZOOM_SCALE}px, ${half - localY * ZOOM_SCALE}px, 0)`;
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
const flushPending = () => {
|
||||
rafIdRef.current = null;
|
||||
const point = pendingPointRef.current;
|
||||
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 = () => {
|
||||
// با ورق خوردن، وضعیت ذرهبین کاملاً ریست میشود
|
||||
useEffect(() => {
|
||||
if (!enabled) return;
|
||||
pendingPointRef.current = null;
|
||||
touchActiveRef.current = false;
|
||||
hideLens();
|
||||
resetActivePage();
|
||||
};
|
||||
lastPageIdRef.current = null;
|
||||
}, [pageIndex, enabled, hideLens, resetActivePage]);
|
||||
|
||||
interaction.addEventListener("mousemove", handleMouseMove);
|
||||
interaction.addEventListener("mouseleave", handleMouseLeave);
|
||||
|
||||
return () => {
|
||||
interaction.removeEventListener("mousemove", handleMouseMove);
|
||||
interaction.removeEventListener("mouseleave", handleMouseLeave);
|
||||
useLayoutEffect(() => {
|
||||
if (!enabled) {
|
||||
pendingPointRef.current = null;
|
||||
touchActiveRef.current = false;
|
||||
if (rafIdRef.current != null) {
|
||||
cancelAnimationFrame(rafIdRef.current);
|
||||
rafIdRef.current = null;
|
||||
}
|
||||
hideLens();
|
||||
resetActivePage();
|
||||
};
|
||||
}, [enabled, containerRef, interactionRef, overlayRef, lensRef, mirrorRef, hideLens, resetActivePage, onActivePageChange]);
|
||||
return;
|
||||
}
|
||||
|
||||
// بلافاصله بعد از سوییچ صفحه (رندر مجدد 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 interaction = interactionRef.current;
|
||||
if (!container || !interaction) return;
|
||||
const page = findPageAtPoint(interaction, point.x, point.y, overlayRef.current);
|
||||
if (!page) return;
|
||||
const geometry = computeMagnifierGeometry(point.x, point.y, page, container, MAGNIFIER_SIZE);
|
||||
if (!geometry) return;
|
||||
mirror.style.transform = `translate3d(${half - geometry.localX * ZOOM_SCALE}px, ${half - geometry.localY * ZOOM_SCALE}px, 0)`;
|
||||
});
|
||||
|
||||
const flushPending = () => {
|
||||
rafIdRef.current = null;
|
||||
const point = pendingPointRef.current;
|
||||
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,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ type ViewerDataPage = {
|
||||
strokeWidth?: number;
|
||||
shapeType?: string;
|
||||
borderRadius?: number;
|
||||
blur?: number;
|
||||
imageUrl?: string;
|
||||
videoUrl?: string;
|
||||
audioUrl?: string;
|
||||
@@ -129,6 +130,9 @@ export function transformViewerDataToPages(data: ViewerData): PageData[] {
|
||||
if (obj.type === "rectangle" && obj.borderRadius !== undefined) {
|
||||
baseObject.borderRadius = obj.borderRadius;
|
||||
}
|
||||
if (obj.type === "rectangle" && obj.blur !== undefined) {
|
||||
baseObject.blur = obj.blur;
|
||||
}
|
||||
|
||||
if (
|
||||
(obj.type === "image" || obj.type === "sticker" || obj.type === "document") &&
|
||||
|
||||
Reference in New Issue
Block a user