From 58f258a5fcc59ba38e63669684610a13e8a745c8 Mon Sep 17 00:00:00 2001 From: neon <18559658+neonsolstice@users.noreply.github.com> Date: Tue, 20 Oct 2026 04:19:56 +0000 Subject: [PATCH] fix(reader): reopen a PDF at its saved page instead of the first The PDF reader restored the saved page with a smooth scroll, which Chrome starts about a second after it is asked. With Fit page, Fit width or Automatic zoom, the first viewport resize refits the page from the current offset with an instant scroll, and a classic scrollbar appearing as the document loads is such a resize. The refit cancelled the restore before it moved, so the reader stayed on page 1 while the toolbar showed the saved page, and the next scroll saved page 1 or 2 over the position. Restore the opening page with an instant scroll, so the refit starts from the saved page and keeps it. Page turns still scroll smoothly. Fixes #1857 --- .../pdf-v4/__tests__/PdfReaderContent.spec.ts | 13 +++++++++++-- .../reader/pdf-v4/components/PdfReaderContent.vue | 5 ++++- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/client/src/features/reader/pdf-v4/__tests__/PdfReaderContent.spec.ts b/client/src/features/reader/pdf-v4/__tests__/PdfReaderContent.spec.ts index c662bbd96..7909ea801 100644 --- a/client/src/features/reader/pdf-v4/__tests__/PdfReaderContent.spec.ts +++ b/client/src/features/reader/pdf-v4/__tests__/PdfReaderContent.spec.ts @@ -10,7 +10,7 @@ const mocks = vi.hoisted(() => ({ layoutReadyListener: null as ((event: LayoutReadyEvent) => void) | null, pageChangeListener: null as ((event: PageChangeEvent) => void) | null, totalPages: 10, - scrollToPage: vi.fn<(options: { pageNumber: number }) => void>(), + scrollToPage: vi.fn<(options: { pageNumber: number; behavior?: ScrollBehavior }) => void>(), })) vi.mock('@embedpdf/plugin-scroll/vue', async () => { @@ -21,7 +21,7 @@ vi.mock('@embedpdf/plugin-scroll/vue', async () => { state: ref({ currentPage: 1, totalPages: mocks.totalPages }), provides: ref({ // EmbedPDF reports the target page synchronously when a programmatic page change starts. - scrollToPage: (options: { pageNumber: number }) => { + scrollToPage: (options: { pageNumber: number; behavior?: ScrollBehavior }) => { mocks.scrollToPage(options) mocks.pageChangeListener?.({ documentId: 'document-1', pageNumber: options.pageNumber, totalPages: mocks.totalPages }) }, @@ -125,6 +125,15 @@ describe('PdfReaderContent', () => { expect(mocks.scrollToPage.mock.calls.length).toBeLessThanOrEqual(1) }) + it('restores the opening page with an instant scroll', () => { + mountContent(7) + layoutReady() + + // A smooth scroll that has not started is cancelled by the fit zoom's refit on the first + // viewport resize, which leaves the reader on page 1. + expect(mocks.scrollToPage).toHaveBeenCalledExactlyOnceWith({ pageNumber: 7, behavior: 'instant' }) + }) + it('marks the opening page as restored and later turns as moves', () => { const wrapper = mountContent(null, 37.5) layoutReady() diff --git a/client/src/features/reader/pdf-v4/components/PdfReaderContent.vue b/client/src/features/reader/pdf-v4/components/PdfReaderContent.vue index 254b35476..fb5867c39 100644 --- a/client/src/features/reader/pdf-v4/components/PdfReaderContent.vue +++ b/client/src/features/reader/pdf-v4/components/PdfReaderContent.vue @@ -343,7 +343,10 @@ watch( restoredInitialPage.value = true if (page > 1) { restoringPage = page - scroll.value?.scrollToPage({ pageNumber: page }) + // Instant, because a fit zoom refits on the first viewport resize (a classic scrollbar + // appearing) from the current offset, which cancels a smooth scroll that has not started + // and strands the reader on page 1, so the next scroll saves page 1 over the position. + scroll.value?.scrollToPage({ pageNumber: page, behavior: 'instant' }) } }) const unsubscribePage = capability.onPageChange((event) => {