diff --git a/services/web/frontend/js/features/pdf-preview/components/pdf-viewer.tsx b/services/web/frontend/js/features/pdf-preview/components/pdf-viewer.tsx index 657ad95cbb..bc264c6202 100644 --- a/services/web/frontend/js/features/pdf-preview/components/pdf-viewer.tsx +++ b/services/web/frontend/js/features/pdf-preview/components/pdf-viewer.tsx @@ -1,4 +1,4 @@ -import { lazy, memo } from 'react' +import { lazy, memo, useCallback, useRef } from 'react' import { useDetachCompileContext as useCompileContext } from '../../../shared/context/detach-compile-context' const PdfJsViewer = lazy( @@ -8,6 +8,36 @@ const PdfJsViewer = lazy( function PdfViewer() { const { pdfUrl, pdfFile, pdfViewer } = useCompileContext() + // Remember the current RevealJS slide (kept in the deck's URL hash, e.g. + // "#/3/2") so that recompiling — which swaps the iframe src for a fresh + // build — reopens the deck on the same slide instead of jumping to the + // start. Only works when the output is same-origin (the usual self-hosted + // case); cross-origin reads fail silently and we just lose the position. + const lastHashRef = useRef('') + + const handlePresentationLoad = useCallback( + (event: React.SyntheticEvent) => { + const win = event.currentTarget.contentWindow + if (!win) { + return + } + try { + const capture = () => { + try { + lastHashRef.current = win.location.hash + } catch { + // cross-origin after navigation — ignore + } + } + capture() + win.addEventListener('hashchange', capture) + } catch { + // cross-origin: can't track the slide position + } + }, + [] + ) + if (!pdfUrl) { return null } @@ -15,10 +45,14 @@ function PdfViewer() { // HTML outputs (RevealJS, etc.) must always use the native iframe; // PDF.js cannot render HTML. if (pdfUrl.includes('output.html')) { + // Re-append the remembered slide hash so the new build opens where the + // user left off. RevealJS reads the hash on load and navigates there. + const src = `${pdfUrl}${lastHashRef.current}` return (