mirror of
https://github.com/open-webui/open-webui.git
synced 2026-07-29 23:32:01 -05:00
refactor(SVGPanZoom): Fix memory leaking - consolidate zoom lifecycle into PanzoomContainer (#23236)
* refactor(SVGPanZoom): attach panzoom via use: action and remove unused parent bind:this * refactor: centralize panzoom in createPanzoomAction and align ImagePreview cleanup * refactor(panzoom): consolidate zoom lifecycle into PanzoomContainer and remove action-based wiring
This commit is contained in:
@@ -293,7 +293,6 @@
|
||||
// ── File preview management ──────────────────────────────────────────
|
||||
const clearFilePreview = () => {
|
||||
fileContent = null;
|
||||
filePreviewRef?.disposePanzoom();
|
||||
if (fileImageUrl) {
|
||||
URL.revokeObjectURL(fileImageUrl);
|
||||
fileImageUrl = null;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { getContext, onDestroy, tick } from 'svelte';
|
||||
import panzoom, { type PanZoom } from 'panzoom';
|
||||
import { getContext, tick } from 'svelte';
|
||||
import { marked } from 'marked';
|
||||
import DOMPurify from 'dompurify';
|
||||
import { settings } from '$lib/stores';
|
||||
@@ -8,6 +7,7 @@
|
||||
import { initMermaid, renderMermaidDiagram } from '$lib/utils';
|
||||
import Spinner from '../../common/Spinner.svelte';
|
||||
import PDFViewer from '../../common/PDFViewer.svelte';
|
||||
import PanzoomContainer from '../../common/PanzoomContainer.svelte';
|
||||
import JsonTreeView from './JsonTreeView.svelte';
|
||||
import NotebookView from './NotebookView.svelte';
|
||||
import SqliteView from './SqliteView.svelte';
|
||||
@@ -250,38 +250,14 @@
|
||||
showRaw = true;
|
||||
}
|
||||
|
||||
let pzInstance: PanZoom | null = null;
|
||||
|
||||
const initImagePanzoom = (node: HTMLElement) => {
|
||||
pzInstance = panzoom(node, {
|
||||
bounds: true,
|
||||
boundsPadding: 0.1,
|
||||
zoomSpeed: 0.065,
|
||||
zoomDoubleClickSpeed: 1
|
||||
});
|
||||
};
|
||||
|
||||
let panzoomRef: PanzoomContainer;
|
||||
export const resetImageView = () => {
|
||||
if (pzInstance) {
|
||||
pzInstance.moveTo(0, 0);
|
||||
pzInstance.zoomAbs(0, 0, 1);
|
||||
}
|
||||
};
|
||||
|
||||
export const disposePanzoom = () => {
|
||||
if (pzInstance) {
|
||||
pzInstance.dispose();
|
||||
pzInstance = null;
|
||||
}
|
||||
panzoomRef?.reset();
|
||||
};
|
||||
|
||||
export const resetPdfView = () => {
|
||||
pdfViewerRef?.resetView();
|
||||
};
|
||||
|
||||
onDestroy(() => {
|
||||
disposePanzoom();
|
||||
});
|
||||
</script>
|
||||
|
||||
<div
|
||||
@@ -293,14 +269,18 @@
|
||||
{#if fileLoading}
|
||||
<div class="flex items-center justify-center h-full"><Spinner className="size-4" /></div>
|
||||
{:else if fileImageUrl !== null}
|
||||
<div class="w-full h-full flex items-center justify-center" use:initImagePanzoom>
|
||||
<PanzoomContainer
|
||||
bind:this={panzoomRef}
|
||||
className="w-full h-full flex items-center justify-center"
|
||||
options={{ zoomDoubleClickSpeed: 1 }}
|
||||
>
|
||||
<img
|
||||
src={fileImageUrl}
|
||||
alt={selectedFile?.split('/').pop()}
|
||||
class="max-w-full max-h-full object-contain p-3"
|
||||
draggable="false"
|
||||
/>
|
||||
</div>
|
||||
</PanzoomContainer>
|
||||
{:else if fileVideoUrl !== null}
|
||||
<div class="w-full h-full flex items-center justify-center bg-black">
|
||||
<!-- svelte-ignore a11y-media-has-caption -->
|
||||
@@ -343,9 +323,10 @@
|
||||
</div>
|
||||
{:else if fileOfficeSlides !== null && fileOfficeSlides.length > 0}
|
||||
<div class="flex flex-col h-full">
|
||||
<div
|
||||
class="w-full flex-1 min-h-0 flex items-center justify-center overflow-hidden"
|
||||
use:initImagePanzoom
|
||||
<PanzoomContainer
|
||||
bind:this={panzoomRef}
|
||||
className="w-full flex-1 min-h-0 flex items-center justify-center overflow-hidden"
|
||||
options={{ zoomDoubleClickSpeed: 1 }}
|
||||
>
|
||||
<img
|
||||
src={fileOfficeSlides[currentSlide]}
|
||||
@@ -353,7 +334,7 @@
|
||||
class="max-w-full max-h-full object-contain p-3"
|
||||
draggable="false"
|
||||
/>
|
||||
</div>
|
||||
</PanzoomContainer>
|
||||
{#if fileOfficeSlides.length > 1}
|
||||
<div
|
||||
class="flex items-center justify-center gap-3 py-2 px-3 border-t border-gray-100 dark:border-gray-800 text-xs text-gray-500"
|
||||
|
||||
@@ -25,9 +25,9 @@
|
||||
import dayjs from 'dayjs';
|
||||
import Spinner from './Spinner.svelte';
|
||||
import PDFViewer from './PDFViewer.svelte';
|
||||
import PanzoomContainer from './PanzoomContainer.svelte';
|
||||
import Reset from '../icons/Reset.svelte';
|
||||
|
||||
import panzoom, { type PanZoom } from 'panzoom';
|
||||
|
||||
export let item;
|
||||
export let show = false;
|
||||
@@ -60,21 +60,9 @@
|
||||
let pptxCurrentSlide = 0;
|
||||
let pptxError = '';
|
||||
|
||||
let pzInstance: PanZoom | null = null;
|
||||
|
||||
const initImagePanzoom = (node: HTMLElement) => {
|
||||
pzInstance = panzoom(node, {
|
||||
bounds: true,
|
||||
boundsPadding: 0.1,
|
||||
zoomSpeed: 0.065
|
||||
});
|
||||
};
|
||||
|
||||
let panzoomRef: PanzoomContainer;
|
||||
const resetImageView = () => {
|
||||
if (pzInstance) {
|
||||
pzInstance.moveTo(0, 0);
|
||||
pzInstance.zoomAbs(0, 0, 1);
|
||||
}
|
||||
panzoomRef?.reset();
|
||||
};
|
||||
|
||||
$: isPDF =
|
||||
@@ -266,10 +254,6 @@
|
||||
if (item?.context === 'full') {
|
||||
enableFullContent = true;
|
||||
}
|
||||
|
||||
return () => {
|
||||
pzInstance?.dispose();
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -445,7 +429,7 @@
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div use:initImagePanzoom>
|
||||
<PanzoomContainer bind:this={panzoomRef}>
|
||||
<img
|
||||
src={`${WEBUI_API_BASE_URL}/files/${item.id}/content`}
|
||||
alt={item?.name ?? 'Image'}
|
||||
@@ -453,7 +437,7 @@
|
||||
loading="lazy"
|
||||
draggable="false"
|
||||
/>
|
||||
</div>
|
||||
</PanzoomContainer>
|
||||
</div>
|
||||
{:else if selectedTab === ''}
|
||||
{#if item?.file?.data}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { onDestroy, onMount, getContext } from 'svelte';
|
||||
import panzoom, { type PanZoom } from 'panzoom';
|
||||
import { onDestroy, getContext } from 'svelte';
|
||||
|
||||
import fileSaver from 'file-saver';
|
||||
const { saveAs } = fileSaver;
|
||||
|
||||
import PanzoomContainer from '$lib/components/common/PanzoomContainer.svelte';
|
||||
import XMark from '$lib/components/icons/XMark.svelte';
|
||||
|
||||
export let show = false;
|
||||
@@ -13,29 +13,8 @@
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
let mounted = false;
|
||||
|
||||
let previewElement = null;
|
||||
|
||||
let instance: PanZoom;
|
||||
|
||||
let sceneParentElement: HTMLElement;
|
||||
let sceneElement: HTMLElement;
|
||||
|
||||
$: if (sceneElement) {
|
||||
instance = panzoom(sceneElement, {
|
||||
bounds: true,
|
||||
boundsPadding: 0.1,
|
||||
|
||||
zoomSpeed: 0.065
|
||||
});
|
||||
}
|
||||
const resetPanZoomViewport = () => {
|
||||
instance.moveTo(0, 0);
|
||||
instance.zoomAbs(0, 0, 1);
|
||||
console.log(instance.getTransform());
|
||||
};
|
||||
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape') {
|
||||
console.log('Escape');
|
||||
@@ -43,10 +22,6 @@
|
||||
}
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
mounted = true;
|
||||
});
|
||||
|
||||
$: if (show && previewElement) {
|
||||
document.body.appendChild(previewElement);
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
@@ -58,11 +33,15 @@
|
||||
}
|
||||
|
||||
onDestroy(() => {
|
||||
window.removeEventListener('keydown', handleKeyDown);
|
||||
show = false;
|
||||
|
||||
if (previewElement) {
|
||||
if (previewElement && previewElement.parentNode === document.body) {
|
||||
document.body.removeChild(previewElement);
|
||||
}
|
||||
// NOTE: If multiple modals can stack in the future, direct "unset" may
|
||||
// re-enable page scroll too early. Consider a shared body-scroll lock manager.
|
||||
document.body.style.overflow = 'unset';
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -181,14 +160,13 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex h-full max-h-full justify-center items-center z-0">
|
||||
<PanzoomContainer className="flex h-full max-h-full justify-center items-center z-0">
|
||||
<img
|
||||
bind:this={sceneElement}
|
||||
{src}
|
||||
{alt}
|
||||
class=" mx-auto h-full object-scale-down select-none"
|
||||
draggable="false"
|
||||
/>
|
||||
</div>
|
||||
</PanzoomContainer>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import panzoom, { type PanZoom, type PanZoomOptions } from 'panzoom';
|
||||
|
||||
const defaultOpts: PanZoomOptions = {
|
||||
bounds: true,
|
||||
boundsPadding: 0.1,
|
||||
zoomSpeed: 0.065
|
||||
};
|
||||
|
||||
export let className = '';
|
||||
export let options: Partial<PanZoomOptions> = {};
|
||||
|
||||
let containerElement: HTMLElement;
|
||||
let instance: PanZoom | undefined;
|
||||
|
||||
export const reset = () => {
|
||||
instance?.moveTo(0, 0);
|
||||
instance?.zoomAbs(0, 0, 1);
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
const localInstance = panzoom(containerElement, { ...defaultOpts, ...options });
|
||||
instance = localInstance;
|
||||
return () => {
|
||||
localInstance.dispose();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div bind:this={containerElement} class={className}>
|
||||
<slot />
|
||||
</div>
|
||||
@@ -4,15 +4,14 @@
|
||||
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
import panzoom, { type PanZoom } from 'panzoom';
|
||||
import DOMPurify from 'dompurify';
|
||||
|
||||
import { onMount, getContext } from 'svelte';
|
||||
import { getContext } from 'svelte';
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
import { copyToClipboard } from '$lib/utils';
|
||||
|
||||
import DocumentDuplicate from '../icons/DocumentDuplicate.svelte';
|
||||
import PanzoomContainer from './PanzoomContainer.svelte';
|
||||
import Tooltip from './Tooltip.svelte';
|
||||
import Clipboard from '../icons/Clipboard.svelte';
|
||||
import Reset from '../icons/Reset.svelte';
|
||||
@@ -22,23 +21,9 @@
|
||||
export let svg = '';
|
||||
export let content = '';
|
||||
|
||||
let instance: PanZoom;
|
||||
|
||||
let sceneParentElement: HTMLElement;
|
||||
let sceneElement: HTMLElement;
|
||||
|
||||
$: if (sceneElement) {
|
||||
instance = panzoom(sceneElement, {
|
||||
bounds: true,
|
||||
boundsPadding: 0.1,
|
||||
|
||||
zoomSpeed: 0.065
|
||||
});
|
||||
}
|
||||
let panzoomRef: PanzoomContainer;
|
||||
const resetPanZoomViewport = () => {
|
||||
instance.moveTo(0, 0);
|
||||
instance.zoomAbs(0, 0, 1);
|
||||
console.log(instance.getTransform());
|
||||
panzoomRef?.reset();
|
||||
};
|
||||
|
||||
const downloadAsSVG = () => {
|
||||
@@ -47,8 +32,11 @@
|
||||
};
|
||||
</script>
|
||||
|
||||
<div bind:this={sceneParentElement} class="relative {className}">
|
||||
<div bind:this={sceneElement} class="flex h-full max-h-full justify-center items-center">
|
||||
<div class="relative {className}">
|
||||
<PanzoomContainer
|
||||
bind:this={panzoomRef}
|
||||
className="flex h-full max-h-full justify-center items-center"
|
||||
>
|
||||
{@html DOMPurify.sanitize(svg, {
|
||||
USE_PROFILES: { svg: true, svgFilters: true }, // allow <svg>, <defs>, <filter>, etc.
|
||||
WHOLE_DOCUMENT: false,
|
||||
@@ -88,7 +76,7 @@
|
||||
],
|
||||
SANITIZE_DOM: true
|
||||
})}
|
||||
</div>
|
||||
</PanzoomContainer>
|
||||
|
||||
{#if content}
|
||||
<div class=" absolute top-2.5 right-2.5">
|
||||
|
||||
Reference in New Issue
Block a user