mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-27 21:01:32 +08:00
refactor(Svg): make svg preview scalable
This commit is contained in:
parent
04326eba21
commit
66b72e5149
@ -1,3 +1,5 @@
|
||||
import { makeSvgScalable } from '@renderer/utils'
|
||||
|
||||
/**
|
||||
* Renders an SVG string inside a host element's Shadow DOM to ensure style encapsulation.
|
||||
* This function handles creating the shadow root, injecting base styles for the host,
|
||||
@ -14,13 +16,13 @@ export function renderSvgInShadowHost(svgContent: string, hostElement: HTMLEleme
|
||||
|
||||
const shadowRoot = hostElement.shadowRoot || hostElement.attachShadow({ mode: 'open' })
|
||||
|
||||
// Base styles for the host element
|
||||
// Base styles for the host element and the inner SVG
|
||||
const style = document.createElement('style')
|
||||
style.textContent = `
|
||||
:host {
|
||||
padding: 1em;
|
||||
background-color: white;
|
||||
overflow: auto;
|
||||
overflow: hidden; /* Prevent scrollbars, as scaling is now handled */
|
||||
border: 0.5px solid var(--color-code-background);
|
||||
border-radius: 8px;
|
||||
display: block;
|
||||
@ -28,20 +30,24 @@ export function renderSvgInShadowHost(svgContent: string, hostElement: HTMLEleme
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
svg {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
width: auto;
|
||||
height: auto;
|
||||
}
|
||||
`
|
||||
|
||||
// Clear previous content and append new style and SVG
|
||||
// Clear previous content and append new style
|
||||
shadowRoot.innerHTML = ''
|
||||
shadowRoot.appendChild(style)
|
||||
|
||||
// Parse and append the SVG using DOMParser to prevent script execution and check for errors
|
||||
if (svgContent.trim() === '') {
|
||||
return
|
||||
}
|
||||
|
||||
// Parse and append the SVG using DOMParser to prevent script execution and check for errors
|
||||
const parser = new DOMParser()
|
||||
const doc = parser.parseFromString(svgContent, 'image/svg+xml')
|
||||
|
||||
@ -53,6 +59,10 @@ export function renderSvgInShadowHost(svgContent: string, hostElement: HTMLEleme
|
||||
|
||||
const svgElement = doc.documentElement
|
||||
if (svgElement && svgElement.nodeName.toLowerCase() === 'svg') {
|
||||
// Standardize the SVG element for proper scaling
|
||||
makeSvgScalable(svgElement)
|
||||
|
||||
// Append the SVG element to the shadow root
|
||||
shadowRoot.appendChild(svgElement.cloneNode(true))
|
||||
} else if (svgContent.trim() !== '') {
|
||||
// Do not throw error for empty content
|
||||
|
||||
@ -6,7 +6,8 @@ import {
|
||||
captureScrollableDivAsBlob,
|
||||
captureScrollableDivAsDataURL,
|
||||
compressImage,
|
||||
convertToBase64
|
||||
convertToBase64,
|
||||
makeSvgScalable
|
||||
} from '../image'
|
||||
|
||||
// mock 依赖
|
||||
@ -125,4 +126,76 @@ describe('utils/image', () => {
|
||||
expect(func).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('makeSvgScalable', () => {
|
||||
const createSvgElement = (svgString: string): SVGElement => {
|
||||
const div = document.createElement('div')
|
||||
div.innerHTML = svgString
|
||||
return div.querySelector('svg') as SVGElement
|
||||
}
|
||||
|
||||
it('should add viewBox and remove width/height when viewBox is missing', () => {
|
||||
const svgElement = createSvgElement('<svg width="800" height="600"></svg>')
|
||||
const result = makeSvgScalable(svgElement)
|
||||
|
||||
expect(result.getAttribute('viewBox')).toBe('0 0 800 600')
|
||||
expect(result.hasAttribute('width')).toBe(false)
|
||||
expect(result.hasAttribute('height')).toBe(false)
|
||||
})
|
||||
|
||||
it('should not overwrite existing viewBox but still remove width/height', () => {
|
||||
const svgElement = createSvgElement('<svg viewBox="0 0 50 50" width="800" height="600"></svg>')
|
||||
const result = makeSvgScalable(svgElement)
|
||||
|
||||
expect(result.getAttribute('viewBox')).toBe('0 0 50 50')
|
||||
expect(result.hasAttribute('width')).toBe(false)
|
||||
expect(result.hasAttribute('height')).toBe(false)
|
||||
})
|
||||
|
||||
it('should not add viewBox for non-numeric width/height but still remove them', () => {
|
||||
const svgElement = createSvgElement('<svg width="100%" height="auto"></svg>')
|
||||
const result = makeSvgScalable(svgElement)
|
||||
|
||||
expect(result.hasAttribute('viewBox')).toBe(false)
|
||||
expect(result.hasAttribute('width')).toBe(false)
|
||||
expect(result.hasAttribute('height')).toBe(false)
|
||||
})
|
||||
|
||||
it('should do nothing if width, height, and viewBox are missing', () => {
|
||||
const svgElement = createSvgElement('<svg><circle cx="50" cy="50" r="40" /></svg>')
|
||||
const originalOuterHTML = svgElement.outerHTML
|
||||
const result = makeSvgScalable(svgElement)
|
||||
|
||||
// Check that no attributes were added
|
||||
expect(result.hasAttribute('viewBox')).toBe(false)
|
||||
expect(result.hasAttribute('width')).toBe(false)
|
||||
expect(result.hasAttribute('height')).toBe(false)
|
||||
// Check that the content is unchanged
|
||||
expect(result.outerHTML).toBe(originalOuterHTML)
|
||||
})
|
||||
|
||||
it('should not add viewBox if only one dimension is present', () => {
|
||||
const svgElement = createSvgElement('<svg height="600"></svg>')
|
||||
const result = makeSvgScalable(svgElement)
|
||||
|
||||
expect(result.hasAttribute('viewBox')).toBe(false)
|
||||
expect(result.hasAttribute('height')).toBe(false)
|
||||
})
|
||||
|
||||
it('should return the element unchanged if it is not an SVGElement', () => {
|
||||
const divElement = document.createElement('div')
|
||||
divElement.setAttribute('width', '100')
|
||||
divElement.setAttribute('height', '100')
|
||||
|
||||
const originalOuterHTML = divElement.outerHTML
|
||||
const result = makeSvgScalable(divElement)
|
||||
|
||||
// Check that the element is the same object
|
||||
expect(result).toBe(divElement)
|
||||
// Check that the content is unchanged
|
||||
expect(result.outerHTML).toBe(originalOuterHTML)
|
||||
// Verify no viewBox was added
|
||||
expect(result.hasAttribute('viewBox')).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -270,3 +270,33 @@ export const svgToSvgBlob = (svgElement: SVGElement): Blob => {
|
||||
const svgData = new XMLSerializer().serializeToString(svgElement)
|
||||
return new Blob([svgData], { type: 'image/svg+xml' })
|
||||
}
|
||||
|
||||
/**
|
||||
* 确保 SVG 元素有 viewBox 并且移除固定的 width/height 属性。
|
||||
* 用于“预览”功能,让 SVG 在容器内可缩放。
|
||||
*/
|
||||
export const makeSvgScalable = (element: Element): Element => {
|
||||
// Type Guard: Only proceed if the element is actually an SVGElement.
|
||||
if (!(element instanceof SVGElement)) {
|
||||
return element
|
||||
}
|
||||
|
||||
const hasViewBox = element.hasAttribute('viewBox')
|
||||
const width = element.getAttribute('width')
|
||||
const height = element.getAttribute('height')
|
||||
|
||||
// 缺少 viewBox 但存在 width 和 height 属性时创建 viewBox
|
||||
if (!hasViewBox && width && height) {
|
||||
const numericWidth = parseFloat(width)
|
||||
const numericHeight = parseFloat(height)
|
||||
if (!isNaN(numericWidth) && !isNaN(numericHeight)) {
|
||||
element.setAttribute('viewBox', `0 0 ${numericWidth} ${numericHeight}`)
|
||||
}
|
||||
}
|
||||
|
||||
// 移除固定的 width 和 height 属性,让 CSS 控制元素尺寸
|
||||
element.removeAttribute('width')
|
||||
element.removeAttribute('height')
|
||||
|
||||
return element
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user