merge: fix/39-reader-vertical-scroll-rendering dans develop (ancrage vertical stabilisé et suivi de page conditionné au scroll utilisateur — 3e correctif #39)
This commit is contained in:
@ -44,8 +44,8 @@ vi.mock("../api/client", () => ({
|
|||||||
api: {
|
api: {
|
||||||
cbzPages: vi.fn().mockResolvedValue({
|
cbzPages: vi.fn().mockResolvedValue({
|
||||||
bookId: 39,
|
bookId: 39,
|
||||||
pageCount: 80,
|
pageCount: 144,
|
||||||
pages: Array.from({ length: 80 }, (_, index) => ({ page: index + 1, name: `page-${index + 1}.jpg` }))
|
pages: Array.from({ length: 144 }, (_, index) => ({ page: index + 1, name: `page-${index + 1}.jpg` }))
|
||||||
}),
|
}),
|
||||||
cbzPageUrl: (bookId: number, page: number) => `/books/${bookId}/pages/${page}`
|
cbzPageUrl: (bookId: number, page: number) => `/books/${bookId}/pages/${page}`
|
||||||
}
|
}
|
||||||
@ -53,6 +53,23 @@ vi.mock("../api/client", () => ({
|
|||||||
|
|
||||||
import { CbzReader } from "../reader/CbzReader";
|
import { CbzReader } from "../reader/CbzReader";
|
||||||
|
|
||||||
|
type ElementLike = {
|
||||||
|
type: unknown;
|
||||||
|
props?: Record<string, unknown> & { children?: unknown };
|
||||||
|
};
|
||||||
|
|
||||||
|
function isElementLike(value: unknown): value is ElementLike {
|
||||||
|
return Boolean(value && typeof value === "object" && "type" in value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function findElementsByType(node: unknown, type: string): ElementLike[] {
|
||||||
|
if (Array.isArray(node)) return node.flatMap((child) => findElementsByType(child, type));
|
||||||
|
if (!isElementLike(node)) return [];
|
||||||
|
|
||||||
|
const matches = node.type === type ? [node] : [];
|
||||||
|
return [...matches, ...findElementsByType(node.props?.children, type)];
|
||||||
|
}
|
||||||
|
|
||||||
function pageElement(page: number, top: number, height = 1000): ReaderPageElement {
|
function pageElement(page: number, top: number, height = 1000): ReaderPageElement {
|
||||||
return {
|
return {
|
||||||
dataset: { readerPage: String(page) },
|
dataset: { readerPage: String(page) },
|
||||||
@ -70,7 +87,7 @@ describe("ReaderPage vertical restore", () => {
|
|||||||
runtime.stateIndex = 0;
|
runtime.stateIndex = 0;
|
||||||
runtime.refIndex = 0;
|
runtime.refIndex = 0;
|
||||||
runtime.effects = [];
|
runtime.effects = [];
|
||||||
runtime.pageElements = Array.from({ length: 80 }, (_, index) => pageElement(index + 1, index * 1000));
|
runtime.pageElements = Array.from({ length: 144 }, (_, index) => pageElement(index + 1, index * 1000));
|
||||||
runtime.frame = {
|
runtime.frame = {
|
||||||
closest: vi.fn(() => ({
|
closest: vi.fn(() => ({
|
||||||
getBoundingClientRect: () => ({ top: 0, bottom: 900 }),
|
getBoundingClientRect: () => ({ top: 0, bottom: 900 }),
|
||||||
@ -86,8 +103,8 @@ describe("ReaderPage vertical restore", () => {
|
|||||||
runtime.states = [
|
runtime.states = [
|
||||||
{
|
{
|
||||||
bookId: 39,
|
bookId: 39,
|
||||||
pageCount: 80,
|
pageCount: 144,
|
||||||
pages: Array.from({ length: 80 }, (_, index) => ({ page: index + 1, name: `page-${index + 1}.jpg` }))
|
pages: Array.from({ length: 144 }, (_, index) => ({ page: index + 1, name: `page-${index + 1}.jpg` }))
|
||||||
},
|
},
|
||||||
undefined,
|
undefined,
|
||||||
undefined,
|
undefined,
|
||||||
@ -111,18 +128,50 @@ describe("ReaderPage vertical restore", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("anchors the current CBZ/CBR page when the reader opens directly in vertical mode", () => {
|
it("anchors page 73 of a CBZ/CBR and keeps previous pages reachable in vertical mode", () => {
|
||||||
|
let controls: { onPrevious: () => void } | undefined;
|
||||||
|
const onPageCommit = vi.fn();
|
||||||
CbzReader({
|
CbzReader({
|
||||||
bookId: 39,
|
bookId: 39,
|
||||||
page: 40,
|
page: 73,
|
||||||
zoom: 100,
|
zoom: 100,
|
||||||
mode: "vertical",
|
mode: "vertical",
|
||||||
onPageCommit: vi.fn(),
|
onPageCommit,
|
||||||
onControlsChange: vi.fn()
|
onControlsChange: (nextControls) => {
|
||||||
|
controls = nextControls;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
runEffects();
|
runEffects();
|
||||||
|
|
||||||
expect(runtime.pageElements[39].scrollIntoView).toHaveBeenCalledWith({ block: "start" });
|
expect(runtime.pageElements[72].scrollIntoView).toHaveBeenCalledWith({ block: "start" });
|
||||||
|
|
||||||
|
runtime.stateIndex = 0;
|
||||||
|
runtime.refIndex = 0;
|
||||||
|
runtime.effects = [];
|
||||||
|
const tree = CbzReader({
|
||||||
|
bookId: 39,
|
||||||
|
page: 73,
|
||||||
|
zoom: 100,
|
||||||
|
mode: "vertical",
|
||||||
|
onPageCommit,
|
||||||
|
onControlsChange: vi.fn()
|
||||||
|
});
|
||||||
|
const figures = findElementsByType(tree, "figure");
|
||||||
|
const images = findElementsByType(tree, "img");
|
||||||
|
|
||||||
|
expect(figures).toHaveLength(144);
|
||||||
|
expect(figures[0].props?.["data-reader-page"]).toBe(1);
|
||||||
|
expect(figures[71].props?.["data-reader-page"]).toBe(72);
|
||||||
|
expect(figures[72].props?.["data-reader-page"]).toBe(73);
|
||||||
|
expect(images[0].props?.src).toBe("/books/39/pages/1");
|
||||||
|
expect(images[71].props?.src).toBe("/books/39/pages/72");
|
||||||
|
expect(images[71].props?.loading).toBe("lazy");
|
||||||
|
expect(images[71].props).not.toHaveProperty("hidden");
|
||||||
|
|
||||||
|
controls?.onPrevious();
|
||||||
|
|
||||||
|
expect(runtime.pageElements[71].scrollIntoView).toHaveBeenCalledWith({ block: "start" });
|
||||||
|
expect(onPageCommit).toHaveBeenCalledWith(72, 144, 1, "immediate");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -7,6 +7,10 @@ import type { ReaderControls } from "./ReaderShell";
|
|||||||
|
|
||||||
type PageCommitStrategy = "immediate" | "queued";
|
type PageCommitStrategy = "immediate" | "queued";
|
||||||
|
|
||||||
|
const VERTICAL_ANCHOR_TOLERANCE_PX = 24;
|
||||||
|
const VERTICAL_ANCHOR_STABLE_FRAMES = 18;
|
||||||
|
const VERTICAL_ANCHOR_MAX_ATTEMPTS = 180;
|
||||||
|
|
||||||
export function CbzReader({
|
export function CbzReader({
|
||||||
bookId,
|
bookId,
|
||||||
page,
|
page,
|
||||||
@ -25,6 +29,11 @@ export function CbzReader({
|
|||||||
const frameRef = useRef<HTMLDivElement>(null);
|
const frameRef = useRef<HTMLDivElement>(null);
|
||||||
const previousModeRef = useRef<ReaderMode | null>(null);
|
const previousModeRef = useRef<ReaderMode | null>(null);
|
||||||
const pendingVerticalAnchorRef = useRef(false);
|
const pendingVerticalAnchorRef = useRef(false);
|
||||||
|
const verticalTrackingReadyRef = useRef(false);
|
||||||
|
const verticalUserScrollRef = useRef(false);
|
||||||
|
const verticalAnchorFrameRef = useRef<number | null>(null);
|
||||||
|
const verticalAnchorAttemptRef = useRef(0);
|
||||||
|
const verticalAnchorStableFramesRef = useRef(0);
|
||||||
const [pages, setPages] = useState<CbzPagesDto | null>(null);
|
const [pages, setPages] = useState<CbzPagesDto | null>(null);
|
||||||
const [documentError, setDocumentError] = useState<string>();
|
const [documentError, setDocumentError] = useState<string>();
|
||||||
const [pageError, setPageError] = useState<string>();
|
const [pageError, setPageError] = useState<string>();
|
||||||
@ -69,6 +78,46 @@ export function CbzReader({
|
|||||||
},
|
},
|
||||||
[imageSizes, viewportSize, zoom]
|
[imageSizes, viewportSize, zoom]
|
||||||
);
|
);
|
||||||
|
const clearVerticalAnchorFrame = useCallback(() => {
|
||||||
|
if (verticalAnchorFrameRef.current !== null) cancelAnimationFrame(verticalAnchorFrameRef.current);
|
||||||
|
verticalAnchorFrameRef.current = null;
|
||||||
|
}, []);
|
||||||
|
const stabilizeVerticalAnchor = useCallback(
|
||||||
|
(targetPage: number) => {
|
||||||
|
clearVerticalAnchorFrame();
|
||||||
|
verticalAnchorAttemptRef.current = 0;
|
||||||
|
verticalAnchorStableFramesRef.current = 0;
|
||||||
|
verticalTrackingReadyRef.current = false;
|
||||||
|
|
||||||
|
const measure = () => {
|
||||||
|
const frame = frameRef.current;
|
||||||
|
const stage = frame?.closest(".reader-stage") as HTMLElement | null;
|
||||||
|
const target = frame?.querySelector<HTMLElement>(`[data-reader-page="${targetPage}"]`);
|
||||||
|
if (!stage || !target) return;
|
||||||
|
|
||||||
|
target.scrollIntoView({ block: "start" });
|
||||||
|
verticalAnchorFrameRef.current = requestAnimationFrame(() => {
|
||||||
|
verticalAnchorFrameRef.current = null;
|
||||||
|
const stageRect = stage.getBoundingClientRect();
|
||||||
|
const targetRect = target.getBoundingClientRect();
|
||||||
|
const aligned = Math.abs(targetRect.top - stageRect.top) <= VERTICAL_ANCHOR_TOLERANCE_PX;
|
||||||
|
const cannotScrollFurther = stage.scrollTop + stage.clientHeight >= stage.scrollHeight - 2;
|
||||||
|
verticalAnchorStableFramesRef.current = aligned || cannotScrollFurther ? verticalAnchorStableFramesRef.current + 1 : 0;
|
||||||
|
|
||||||
|
if (verticalAnchorStableFramesRef.current >= VERTICAL_ANCHOR_STABLE_FRAMES) {
|
||||||
|
verticalTrackingReadyRef.current = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
verticalAnchorAttemptRef.current += 1;
|
||||||
|
if (verticalAnchorAttemptRef.current >= VERTICAL_ANCHOR_MAX_ATTEMPTS) return;
|
||||||
|
verticalAnchorFrameRef.current = requestAnimationFrame(measure);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
verticalAnchorFrameRef.current = requestAnimationFrame(measure);
|
||||||
|
},
|
||||||
|
[clearVerticalAnchorFrame]
|
||||||
|
);
|
||||||
|
|
||||||
const go = useCallback(
|
const go = useCallback(
|
||||||
(nextPage: number) => {
|
(nextPage: number) => {
|
||||||
@ -76,6 +125,7 @@ export function CbzReader({
|
|||||||
const target = clampReaderPage(nextPage, pages.pageCount);
|
const target = clampReaderPage(nextPage, pages.pageCount);
|
||||||
setPageError(undefined);
|
setPageError(undefined);
|
||||||
if (mode === "vertical") {
|
if (mode === "vertical") {
|
||||||
|
verticalUserScrollRef.current = false;
|
||||||
frameRef.current?.querySelector<HTMLElement>(`[data-reader-page="${target}"]`)?.scrollIntoView({ block: "start" });
|
frameRef.current?.querySelector<HTMLElement>(`[data-reader-page="${target}"]`)?.scrollIntoView({ block: "start" });
|
||||||
}
|
}
|
||||||
onPageCommit(target, pages.pageCount, 1, "immediate");
|
onPageCommit(target, pages.pageCount, 1, "immediate");
|
||||||
@ -126,9 +176,17 @@ export function CbzReader({
|
|||||||
if (mode !== "vertical") {
|
if (mode !== "vertical") {
|
||||||
previousModeRef.current = mode;
|
previousModeRef.current = mode;
|
||||||
pendingVerticalAnchorRef.current = false;
|
pendingVerticalAnchorRef.current = false;
|
||||||
|
verticalTrackingReadyRef.current = false;
|
||||||
|
verticalUserScrollRef.current = false;
|
||||||
|
clearVerticalAnchorFrame();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (previousModeRef.current !== "vertical") pendingVerticalAnchorRef.current = true;
|
if (previousModeRef.current !== "vertical") {
|
||||||
|
pendingVerticalAnchorRef.current = true;
|
||||||
|
verticalTrackingReadyRef.current = false;
|
||||||
|
verticalUserScrollRef.current = false;
|
||||||
|
clearVerticalAnchorFrame();
|
||||||
|
}
|
||||||
if (pendingVerticalAnchorRef.current) {
|
if (pendingVerticalAnchorRef.current) {
|
||||||
const target = frameRef.current?.querySelector<HTMLElement>(`[data-reader-page="${currentPage}"]`);
|
const target = frameRef.current?.querySelector<HTMLElement>(`[data-reader-page="${currentPage}"]`);
|
||||||
if (!target) {
|
if (!target) {
|
||||||
@ -136,20 +194,26 @@ export function CbzReader({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
pendingVerticalAnchorRef.current = false;
|
pendingVerticalAnchorRef.current = false;
|
||||||
requestAnimationFrame(() => {
|
stabilizeVerticalAnchor(currentPage);
|
||||||
target.scrollIntoView({ block: "start" });
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
previousModeRef.current = mode;
|
previousModeRef.current = mode;
|
||||||
}, [currentPage, mode, pages]);
|
}, [clearVerticalAnchorFrame, currentPage, mode, pages, stabilizeVerticalAnchor]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (mode !== "vertical" || !pages) return;
|
if (mode !== "vertical" || !pages) return;
|
||||||
const stage = frameRef.current?.closest(".reader-stage") as HTMLElement | null;
|
const stage = frameRef.current?.closest(".reader-stage") as HTMLElement | null;
|
||||||
if (!stage) return;
|
if (!stage) return;
|
||||||
let frameId = 0;
|
let frameId = 0;
|
||||||
|
const markUserScroll = () => {
|
||||||
|
verticalUserScrollRef.current = true;
|
||||||
|
};
|
||||||
|
const markUserScrollKey = (event: KeyboardEvent) => {
|
||||||
|
if (["ArrowUp", "ArrowDown", "PageUp", "PageDown", "Home", "End", " ", "Spacebar"].includes(event.key)) markUserScroll();
|
||||||
|
};
|
||||||
|
const keyTarget = typeof window === "undefined" ? null : window;
|
||||||
const updateVisiblePage = () => {
|
const updateVisiblePage = () => {
|
||||||
frameId = 0;
|
frameId = 0;
|
||||||
|
if (!verticalTrackingReadyRef.current || !verticalUserScrollRef.current) return;
|
||||||
const stageRect = stage.getBoundingClientRect();
|
const stageRect = stage.getBoundingClientRect();
|
||||||
const visiblePage = majorityVisiblePage(
|
const visiblePage = majorityVisiblePage(
|
||||||
Array.from(stage.querySelectorAll<HTMLElement>("[data-reader-page]")).map((element) => {
|
Array.from(stage.querySelectorAll<HTMLElement>("[data-reader-page]")).map((element) => {
|
||||||
@ -165,14 +229,24 @@ export function CbzReader({
|
|||||||
if (frameId) return;
|
if (frameId) return;
|
||||||
frameId = requestAnimationFrame(updateVisiblePage);
|
frameId = requestAnimationFrame(updateVisiblePage);
|
||||||
};
|
};
|
||||||
|
stage.addEventListener("wheel", markUserScroll, { passive: true });
|
||||||
|
stage.addEventListener("touchmove", markUserScroll, { passive: true });
|
||||||
|
stage.addEventListener("pointerdown", markUserScroll, { passive: true });
|
||||||
|
keyTarget?.addEventListener("keydown", markUserScrollKey);
|
||||||
stage.addEventListener("scroll", onScroll, { passive: true });
|
stage.addEventListener("scroll", onScroll, { passive: true });
|
||||||
updateVisiblePage();
|
updateVisiblePage();
|
||||||
return () => {
|
return () => {
|
||||||
if (frameId) cancelAnimationFrame(frameId);
|
if (frameId) cancelAnimationFrame(frameId);
|
||||||
|
stage.removeEventListener("wheel", markUserScroll);
|
||||||
|
stage.removeEventListener("touchmove", markUserScroll);
|
||||||
|
stage.removeEventListener("pointerdown", markUserScroll);
|
||||||
|
keyTarget?.removeEventListener("keydown", markUserScrollKey);
|
||||||
stage.removeEventListener("scroll", onScroll);
|
stage.removeEventListener("scroll", onScroll);
|
||||||
};
|
};
|
||||||
}, [currentPage, mode, onPageCommit, pages]);
|
}, [currentPage, mode, onPageCommit, pages]);
|
||||||
|
|
||||||
|
useEffect(() => () => clearVerticalAnchorFrame(), [clearVerticalAnchorFrame]);
|
||||||
|
|
||||||
if (documentError) {
|
if (documentError) {
|
||||||
return (
|
return (
|
||||||
<div className="cbz-reader" ref={frameRef}>
|
<div className="cbz-reader" ref={frameRef}>
|
||||||
|
|||||||
@ -29,6 +29,10 @@ type PdfFailure = {
|
|||||||
|
|
||||||
type PdfPageStatus = "idle" | "loading" | PdfRenderResult | "render-failed";
|
type PdfPageStatus = "idle" | "loading" | PdfRenderResult | "render-failed";
|
||||||
|
|
||||||
|
const VERTICAL_ANCHOR_TOLERANCE_PX = 24;
|
||||||
|
const VERTICAL_ANCHOR_STABLE_FRAMES = 18;
|
||||||
|
const VERTICAL_ANCHOR_MAX_ATTEMPTS = 180;
|
||||||
|
|
||||||
function pdfTechnicalMessage(error: unknown) {
|
function pdfTechnicalMessage(error: unknown) {
|
||||||
if (error instanceof Error && error.message.trim()) return `${error.name}: ${error.message}`;
|
if (error instanceof Error && error.message.trim()) return `${error.name}: ${error.message}`;
|
||||||
if (typeof error === "string" && error.trim()) return error;
|
if (typeof error === "string" && error.trim()) return error;
|
||||||
@ -151,6 +155,11 @@ export function PdfReader({ url, page, backHref, zoom, mode, onPageCommit, onCon
|
|||||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
const previousModeRef = useRef<ReaderMode | null>(null);
|
const previousModeRef = useRef<ReaderMode | null>(null);
|
||||||
const pendingVerticalAnchorRef = useRef(false);
|
const pendingVerticalAnchorRef = useRef(false);
|
||||||
|
const verticalTrackingReadyRef = useRef(false);
|
||||||
|
const verticalUserScrollRef = useRef(false);
|
||||||
|
const verticalAnchorFrameRef = useRef<number | null>(null);
|
||||||
|
const verticalAnchorAttemptRef = useRef(0);
|
||||||
|
const verticalAnchorStableFramesRef = useRef(0);
|
||||||
const [documentProxy, setDocumentProxy] = useState<pdfjs.PDFDocumentProxy | null>(null);
|
const [documentProxy, setDocumentProxy] = useState<pdfjs.PDFDocumentProxy | null>(null);
|
||||||
const [pages, setPages] = useState(1);
|
const [pages, setPages] = useState(1);
|
||||||
const [documentError, setDocumentError] = useState<PdfFailure>();
|
const [documentError, setDocumentError] = useState<PdfFailure>();
|
||||||
@ -165,12 +174,53 @@ export function PdfReader({ url, page, backHref, zoom, mode, onPageCommit, onCon
|
|||||||
|
|
||||||
const currentPage = clampReaderPage(page, pages);
|
const currentPage = clampReaderPage(page, pages);
|
||||||
const showCanvas = pdfCanvasVisible(pageRendered, Boolean(pageError));
|
const showCanvas = pdfCanvasVisible(pageRendered, Boolean(pageError));
|
||||||
|
const clearVerticalAnchorFrame = useCallback(() => {
|
||||||
|
if (verticalAnchorFrameRef.current !== null) cancelAnimationFrame(verticalAnchorFrameRef.current);
|
||||||
|
verticalAnchorFrameRef.current = null;
|
||||||
|
}, []);
|
||||||
|
const stabilizeVerticalAnchor = useCallback(
|
||||||
|
(targetPage: number) => {
|
||||||
|
clearVerticalAnchorFrame();
|
||||||
|
verticalAnchorAttemptRef.current = 0;
|
||||||
|
verticalAnchorStableFramesRef.current = 0;
|
||||||
|
verticalTrackingReadyRef.current = false;
|
||||||
|
|
||||||
|
const measure = () => {
|
||||||
|
const frame = frameRef.current;
|
||||||
|
const stage = scrollContainerFor(frame);
|
||||||
|
const target = frame?.querySelector<HTMLElement>(`[data-reader-page="${targetPage}"]`);
|
||||||
|
if (!stage || !target) return;
|
||||||
|
|
||||||
|
target.scrollIntoView({ block: "start" });
|
||||||
|
verticalAnchorFrameRef.current = requestAnimationFrame(() => {
|
||||||
|
verticalAnchorFrameRef.current = null;
|
||||||
|
const stageRect = stage.getBoundingClientRect();
|
||||||
|
const targetRect = target.getBoundingClientRect();
|
||||||
|
const aligned = Math.abs(targetRect.top - stageRect.top) <= VERTICAL_ANCHOR_TOLERANCE_PX;
|
||||||
|
const cannotScrollFurther = stage.scrollTop + stage.clientHeight >= stage.scrollHeight - 2;
|
||||||
|
verticalAnchorStableFramesRef.current = aligned || cannotScrollFurther ? verticalAnchorStableFramesRef.current + 1 : 0;
|
||||||
|
|
||||||
|
if (verticalAnchorStableFramesRef.current >= VERTICAL_ANCHOR_STABLE_FRAMES) {
|
||||||
|
verticalTrackingReadyRef.current = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
verticalAnchorAttemptRef.current += 1;
|
||||||
|
if (verticalAnchorAttemptRef.current >= VERTICAL_ANCHOR_MAX_ATTEMPTS) return;
|
||||||
|
verticalAnchorFrameRef.current = requestAnimationFrame(measure);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
verticalAnchorFrameRef.current = requestAnimationFrame(measure);
|
||||||
|
},
|
||||||
|
[clearVerticalAnchorFrame]
|
||||||
|
);
|
||||||
|
|
||||||
const go = useCallback(
|
const go = useCallback(
|
||||||
(nextPage: number) => {
|
(nextPage: number) => {
|
||||||
const target = clampReaderPage(nextPage, pages);
|
const target = clampReaderPage(nextPage, pages);
|
||||||
setPageError(undefined);
|
setPageError(undefined);
|
||||||
if (mode === "vertical") {
|
if (mode === "vertical") {
|
||||||
|
verticalUserScrollRef.current = false;
|
||||||
frameRef.current?.querySelector<HTMLElement>(`[data-reader-page="${target}"]`)?.scrollIntoView({ block: "start" });
|
frameRef.current?.querySelector<HTMLElement>(`[data-reader-page="${target}"]`)?.scrollIntoView({ block: "start" });
|
||||||
}
|
}
|
||||||
onPageCommit(target, pages, 1, "immediate");
|
onPageCommit(target, pages, 1, "immediate");
|
||||||
@ -192,9 +242,17 @@ export function PdfReader({ url, page, backHref, zoom, mode, onPageCommit, onCon
|
|||||||
if (mode !== "vertical") {
|
if (mode !== "vertical") {
|
||||||
previousModeRef.current = mode;
|
previousModeRef.current = mode;
|
||||||
pendingVerticalAnchorRef.current = false;
|
pendingVerticalAnchorRef.current = false;
|
||||||
|
verticalTrackingReadyRef.current = false;
|
||||||
|
verticalUserScrollRef.current = false;
|
||||||
|
clearVerticalAnchorFrame();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (previousModeRef.current !== "vertical") pendingVerticalAnchorRef.current = true;
|
if (previousModeRef.current !== "vertical") {
|
||||||
|
pendingVerticalAnchorRef.current = true;
|
||||||
|
verticalTrackingReadyRef.current = false;
|
||||||
|
verticalUserScrollRef.current = false;
|
||||||
|
clearVerticalAnchorFrame();
|
||||||
|
}
|
||||||
if (pendingVerticalAnchorRef.current) {
|
if (pendingVerticalAnchorRef.current) {
|
||||||
const target = frameRef.current?.querySelector<HTMLElement>(`[data-reader-page="${currentPage}"]`);
|
const target = frameRef.current?.querySelector<HTMLElement>(`[data-reader-page="${currentPage}"]`);
|
||||||
if (!target) {
|
if (!target) {
|
||||||
@ -202,20 +260,26 @@ export function PdfReader({ url, page, backHref, zoom, mode, onPageCommit, onCon
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
pendingVerticalAnchorRef.current = false;
|
pendingVerticalAnchorRef.current = false;
|
||||||
requestAnimationFrame(() => {
|
stabilizeVerticalAnchor(currentPage);
|
||||||
target.scrollIntoView({ block: "start" });
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
previousModeRef.current = mode;
|
previousModeRef.current = mode;
|
||||||
}, [currentPage, documentProxy, mode]);
|
}, [clearVerticalAnchorFrame, currentPage, documentProxy, mode, stabilizeVerticalAnchor]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (mode !== "vertical") return;
|
if (mode !== "vertical") return;
|
||||||
const stage = scrollContainerFor(frameRef.current);
|
const stage = scrollContainerFor(frameRef.current);
|
||||||
if (!stage) return;
|
if (!stage) return;
|
||||||
let frameId = 0;
|
let frameId = 0;
|
||||||
|
const markUserScroll = () => {
|
||||||
|
verticalUserScrollRef.current = true;
|
||||||
|
};
|
||||||
|
const markUserScrollKey = (event: KeyboardEvent) => {
|
||||||
|
if (["ArrowUp", "ArrowDown", "PageUp", "PageDown", "Home", "End", " ", "Spacebar"].includes(event.key)) markUserScroll();
|
||||||
|
};
|
||||||
|
const keyTarget = typeof window === "undefined" ? null : window;
|
||||||
const updateVisiblePage = () => {
|
const updateVisiblePage = () => {
|
||||||
frameId = 0;
|
frameId = 0;
|
||||||
|
if (!verticalTrackingReadyRef.current || !verticalUserScrollRef.current) return;
|
||||||
const stageRect = stage.getBoundingClientRect();
|
const stageRect = stage.getBoundingClientRect();
|
||||||
const visiblePage = majorityVisiblePage(
|
const visiblePage = majorityVisiblePage(
|
||||||
Array.from(stage.querySelectorAll<HTMLElement>("[data-reader-page]")).map((element) => {
|
Array.from(stage.querySelectorAll<HTMLElement>("[data-reader-page]")).map((element) => {
|
||||||
@ -231,14 +295,24 @@ export function PdfReader({ url, page, backHref, zoom, mode, onPageCommit, onCon
|
|||||||
if (frameId) return;
|
if (frameId) return;
|
||||||
frameId = requestAnimationFrame(updateVisiblePage);
|
frameId = requestAnimationFrame(updateVisiblePage);
|
||||||
};
|
};
|
||||||
|
stage.addEventListener("wheel", markUserScroll, { passive: true });
|
||||||
|
stage.addEventListener("touchmove", markUserScroll, { passive: true });
|
||||||
|
stage.addEventListener("pointerdown", markUserScroll, { passive: true });
|
||||||
|
keyTarget?.addEventListener("keydown", markUserScrollKey);
|
||||||
stage.addEventListener("scroll", onScroll, { passive: true });
|
stage.addEventListener("scroll", onScroll, { passive: true });
|
||||||
updateVisiblePage();
|
updateVisiblePage();
|
||||||
return () => {
|
return () => {
|
||||||
if (frameId) cancelAnimationFrame(frameId);
|
if (frameId) cancelAnimationFrame(frameId);
|
||||||
|
stage.removeEventListener("wheel", markUserScroll);
|
||||||
|
stage.removeEventListener("touchmove", markUserScroll);
|
||||||
|
stage.removeEventListener("pointerdown", markUserScroll);
|
||||||
|
keyTarget?.removeEventListener("keydown", markUserScrollKey);
|
||||||
stage.removeEventListener("scroll", onScroll);
|
stage.removeEventListener("scroll", onScroll);
|
||||||
};
|
};
|
||||||
}, [currentPage, mode, onPageCommit, pages]);
|
}, [currentPage, mode, onPageCommit, pages]);
|
||||||
|
|
||||||
|
useEffect(() => () => clearVerticalAnchorFrame(), [clearVerticalAnchorFrame]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const frame = frameRef.current;
|
const frame = frameRef.current;
|
||||||
const stage = scrollContainerFor(frame);
|
const stage = scrollContainerFor(frame);
|
||||||
|
|||||||
Reference in New Issue
Block a user