diff --git a/src/transcription/ui/components/document_panzoom.py b/src/transcription/ui/components/document_panzoom.py index 3d170c2..00772c3 100644 --- a/src/transcription/ui/components/document_panzoom.py +++ b/src/transcription/ui/components/document_panzoom.py @@ -60,16 +60,16 @@ def _register_panzoom_assets() -> None: width: 100%; height: 100%; display: flex; - align-items: center; - justify-content: center; + align-items: flex-start; + justify-content: flex-start; } .document-panzoom-media { width: auto; height: auto; display: block; - max-width: none; - max-height: none; + max-width: 100%; + max-height: 100%; user-select: none; -webkit-user-drag: none; } @@ -99,79 +99,76 @@ def _attach_panzoom(host_id: str) -> None: const media = host.querySelector('[data-panzoom-media]'); if (!target) return; + const cleanup = () => {{ + const existing = window.__transcriptionPanzoom[{host_id!r}]; + if (existing?.resizeObserver) existing.resizeObserver.disconnect(); + if (existing?.wheelHandler) host.removeEventListener('wheel', existing.wheelHandler); + if (existing?.instance) existing.instance.destroy(); + }}; + const computeFitScale = () => {{ const hostRect = host.getBoundingClientRect(); - if (hostRect.width <= 0 || hostRect.height <= 0) return 1; - - if (media && media.tagName === 'IMG') {{ - if (media.naturalWidth <= 0 || media.naturalHeight <= 0) return 1; - return Math.min( - hostRect.width / media.naturalWidth, - hostRect.height / media.naturalHeight - ); - }} - - if (target.tagName === 'IFRAME') {{ - const targetRect = target.getBoundingClientRect(); - if (targetRect.width <= 0 || targetRect.height <= 0) return 1; - return hostRect.height / targetRect.height; - }} - + if (hostRect.width <= 0 || hostRect.height <= 0) return null; return 1; }}; - const initPanzoom = () => {{ - if (window.__transcriptionPanzoom[{host_id!r}]) {{ - window.__transcriptionPanzoom[{host_id!r}].destroy(); - }} + const buildInstance = () => {{ + cleanup(); - if (host.__transcriptionWheelHandler) {{ - host.removeEventListener('wheel', host.__transcriptionWheelHandler); - host.__transcriptionWheelHandler = null; - }} + const fitScale = computeFitScale(); + if (fitScale === null) return false; - let fitScale = computeFitScale(); - - if (!Number.isFinite(fitScale) || fitScale <= 0) {{ - fitScale = 1; - }} - - const startScale = fitScale; - const minScale = 0.01; + const minScale = Math.min(fitScale, 0.01); const instance = Panzoom(target, {{ - maxScale: 32, + startX: 0, + startY: 0, + startScale: fitScale, minScale: minScale, - startScale: startScale, - step: 0.18, - contain: 'inside', - roundPixels: true, + maxScale: 256, + step: 0.2, + roundPixels: false, + panOnlyWhenZoomed: true, overflow: 'hidden', }}); - window.__transcriptionPanzoom[{host_id!r}] = instance; + const wheelHandler = (event) => instance.zoomWithWheel(event); + host.addEventListener('wheel', wheelHandler, {{ passive: false }}); requestAnimationFrame(() => {{ - instance.reset({{ animate: false, force: true }}); - instance.setOptions({{ contain: undefined }}); + instance.reset({{ animate: false }}); }}); - const wheelHandler = (event) => instance.zoomWithWheel(event); - host.__transcriptionWheelHandler = wheelHandler; - host.addEventListener('wheel', wheelHandler, {{ passive: false }}); + const resizeObserver = new ResizeObserver(() => {{ + const nextFitScale = computeFitScale(); + if (nextFitScale === null) return; + instance.setOptions({{ + startScale: nextFitScale, + minScale: Math.min(nextFitScale, 0.01), + }}); + instance.reset({{ animate: false }}); + }}); + resizeObserver.observe(host); + + window.__transcriptionPanzoom[{host_id!r}] = {{ + instance, + wheelHandler, + resizeObserver, + }}; + return true; + }}; + + const initWhenReady = (retries = 15) => {{ + if (buildInstance()) return; + if (retries <= 0) return; + requestAnimationFrame(() => initWhenReady(retries - 1)); }}; if (media && media.tagName === 'IMG' && !media.complete) {{ - media.addEventListener('load', initPanzoom, {{ once: true }}); + media.addEventListener('load', () => initWhenReady(), {{ once: true }}); return; }} - if ((media && media.tagName === 'IMG' && (host.clientWidth <= 0 || host.clientHeight <= 0)) || - (target.tagName === 'IFRAME' && (host.clientWidth <= 0 || host.clientHeight <= 0))) {{ - requestAnimationFrame(initPanzoom); - return; - }} - - initPanzoom(); + initWhenReady(); }})(); """ ) @@ -186,10 +183,11 @@ def render_document_panzoom(*, document: Document, height: str = "640px") -> Non document_kind = _document_kind(document) with ui.card().classes("w-full q-pa-md"): - with ui.column().classes("q-gutter-none"): - with ui.row().classes("w-full items-center justify-between q-gutter-sm"): - ui.label("Document preview").classes("text-subtitle1 text-weight-medium") - ui.label(document.filename).classes("text-caption text-grey-4") + with ui.row().classes("w-full items-center justify-between no-wrap"): + ui.label("Document preview").classes("text-subtitle1 text-weight-medium") + ui.label(document.filename).classes("text-caption text-grey-4 ellipsis").style( + "max-width: 60%; text-align: right;" + ) with ( ui.element("div")