mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
fix(files): zoom file viewer content, not the browser page (#4741)
* fix(files): zoom file viewer content, not the browser page * fix(files): use effect lifecycle for SVG blob URL to survive strict mode
This commit is contained in:
+12
-6
@@ -1071,15 +1071,21 @@ const HtmlPreview = memo(function HtmlPreview({ content }: { content: string })
|
||||
})
|
||||
|
||||
function SvgPreview({ content }: { content: string }) {
|
||||
const wrappedContent = `<!DOCTYPE html><html><head><style>body{margin:0;display:flex;align-items:center;justify-content:center;min-height:100vh;background:transparent;}svg{max-width:100%;max-height:100vh;}</style></head><body>${content}</body></html>`
|
||||
const [blobUrl, setBlobUrl] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
const url = URL.createObjectURL(new Blob([content], { type: 'image/svg+xml' }))
|
||||
setBlobUrl(url)
|
||||
return () => URL.revokeObjectURL(url)
|
||||
}, [content])
|
||||
|
||||
return (
|
||||
<ZoomablePreview className='h-full' contentClassName='h-full w-full'>
|
||||
<iframe
|
||||
srcDoc={wrappedContent}
|
||||
sandbox=''
|
||||
title='SVG Preview'
|
||||
className='h-full w-full border-0'
|
||||
<img
|
||||
src={blobUrl}
|
||||
alt='SVG preview'
|
||||
className='max-h-full max-w-full select-none object-contain'
|
||||
draggable={false}
|
||||
/>
|
||||
</ZoomablePreview>
|
||||
)
|
||||
|
||||
+25
-3
@@ -1,17 +1,39 @@
|
||||
interface BindPreviewWheelZoomOptions {
|
||||
/**
|
||||
* Called for non-modifier wheel events (two-finger scroll). When provided,
|
||||
* the container's native scrolling is suppressed and the consumer drives
|
||||
* pan via `deltaX` / `deltaY`. Use for transform-based viewers (e.g. image)
|
||||
* where the content is not a real scroll container.
|
||||
*/
|
||||
onPan?: (event: WheelEvent) => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind browser pinch/ctrl-wheel zoom and horizontal wheel gestures for preview scroll containers.
|
||||
* Bind browser pinch/ctrl-wheel zoom and horizontal wheel gestures for preview
|
||||
* scroll containers. Trackpad pinch fires `wheel` with `ctrlKey=true`; without
|
||||
* a non-passive native listener the browser falls back to page zoom. `metaKey`
|
||||
* is also accepted so Cmd+scroll zooms on macOS, matching Figma/tldraw/Excalidraw.
|
||||
*/
|
||||
export function bindPreviewWheelZoom(
|
||||
container: HTMLElement,
|
||||
onZoom: (event: WheelEvent) => void
|
||||
onZoom: (event: WheelEvent) => void,
|
||||
options: BindPreviewWheelZoomOptions = {}
|
||||
): () => void {
|
||||
const { onPan } = options
|
||||
|
||||
const onWheel = (event: WheelEvent) => {
|
||||
if (event.ctrlKey) {
|
||||
if (event.ctrlKey || event.metaKey) {
|
||||
event.preventDefault()
|
||||
onZoom(event)
|
||||
return
|
||||
}
|
||||
|
||||
if (onPan) {
|
||||
event.preventDefault()
|
||||
onPan(event)
|
||||
return
|
||||
}
|
||||
|
||||
const horizontalDelta = event.deltaX !== 0 ? event.deltaX : event.shiftKey ? event.deltaY : 0
|
||||
if (horizontalDelta === 0 || container.scrollWidth <= container.clientWidth) return
|
||||
|
||||
|
||||
+35
-27
@@ -1,9 +1,10 @@
|
||||
'use client'
|
||||
|
||||
import type { MouseEvent, ReactNode, WheelEvent } from 'react'
|
||||
import { useCallback, useLayoutEffect, useRef, useState } from 'react'
|
||||
import type { MouseEvent, ReactNode } from 'react'
|
||||
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react'
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
import { PreviewToolbar } from './preview-toolbar'
|
||||
import { bindPreviewWheelZoom } from './preview-wheel-zoom'
|
||||
|
||||
const ZOOM_MIN = 0.25
|
||||
const ZOOM_MAX = 4
|
||||
@@ -133,31 +134,39 @@ export function ZoomablePreview({
|
||||
applyZoom(clampZoom(zoom / ZOOM_BUTTON_FACTOR))
|
||||
}
|
||||
|
||||
const handleWheel = (e: WheelEvent<HTMLDivElement>) => {
|
||||
e.preventDefault()
|
||||
if (e.ctrlKey || e.metaKey) {
|
||||
hasInteractedRef.current = true
|
||||
const rect = e.currentTarget.getBoundingClientRect()
|
||||
applyZoom(
|
||||
clampZoom(zoomRef.current * Math.exp(-e.deltaY * ZOOM_WHEEL_SENSITIVITY)),
|
||||
e.clientX - rect.left,
|
||||
e.clientY - rect.top
|
||||
)
|
||||
} else {
|
||||
hasInteractedRef.current = true
|
||||
setOffset((currentOffset) =>
|
||||
clampOffset(
|
||||
containerSizeRef.current,
|
||||
contentSizeRef.current,
|
||||
{
|
||||
x: currentOffset.x - e.deltaX,
|
||||
y: currentOffset.y - e.deltaY,
|
||||
},
|
||||
zoomRef.current
|
||||
useEffect(() => {
|
||||
const viewport = viewportRef.current
|
||||
if (!viewport) return
|
||||
|
||||
return bindPreviewWheelZoom(
|
||||
viewport,
|
||||
(event) => {
|
||||
hasInteractedRef.current = true
|
||||
const rect = viewport.getBoundingClientRect()
|
||||
applyZoom(
|
||||
clampZoom(zoomRef.current * Math.exp(-event.deltaY * ZOOM_WHEEL_SENSITIVITY)),
|
||||
event.clientX - rect.left,
|
||||
event.clientY - rect.top
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
onPan: (event) => {
|
||||
hasInteractedRef.current = true
|
||||
setOffset((currentOffset) =>
|
||||
clampOffset(
|
||||
containerSizeRef.current,
|
||||
contentSizeRef.current,
|
||||
{
|
||||
x: currentOffset.x - event.deltaX,
|
||||
y: currentOffset.y - event.deltaY,
|
||||
},
|
||||
zoomRef.current
|
||||
)
|
||||
)
|
||||
},
|
||||
}
|
||||
)
|
||||
}, [applyZoom])
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const updateSizes = () => {
|
||||
@@ -257,7 +266,6 @@ export function ZoomablePreview({
|
||||
onMouseMove={handleMouseMove}
|
||||
onMouseUp={handleMouseUp}
|
||||
onMouseLeave={handleMouseUp}
|
||||
onWheel={handleWheel}
|
||||
>
|
||||
<div className='pointer-events-none absolute inset-0 flex items-center justify-center'>
|
||||
<div
|
||||
|
||||
Reference in New Issue
Block a user