mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-31 08:29:07 +08:00
refactor: add measureElementSize
This commit is contained in:
parent
ede226f74d
commit
1a83b2c16a
@ -30,11 +30,6 @@ export function renderSvgInShadowHost(svgContent: string, hostElement: HTMLEleme
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: auto;
|
||||
height: auto;
|
||||
}
|
||||
`
|
||||
|
||||
// Clear previous content and append new style
|
||||
|
||||
@ -131,71 +131,84 @@ describe('utils/image', () => {
|
||||
const createSvgElement = (svgString: string): SVGElement => {
|
||||
const div = document.createElement('div')
|
||||
div.innerHTML = svgString
|
||||
return div.querySelector('svg') as SVGElement
|
||||
const svgElement = div.querySelector<SVGElement>('svg')
|
||||
if (!svgElement) {
|
||||
throw new Error(`Test setup error: No <svg> element found in string: "${svgString}"`)
|
||||
}
|
||||
return 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)
|
||||
// Mock document.body.appendChild to avoid errors in jsdom
|
||||
beforeEach(() => {
|
||||
vi.spyOn(document.body, 'appendChild').mockImplementation(() => ({}) as Node)
|
||||
vi.spyOn(document.body, 'removeChild').mockImplementation(() => ({}) as Node)
|
||||
})
|
||||
|
||||
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)
|
||||
it('should measure and add viewBox/max-width when viewBox is missing', () => {
|
||||
const svgElement = createSvgElement('<svg width="100pt" height="80pt"></svg>')
|
||||
// Mock the measurement result on the prototype
|
||||
const spy = vi
|
||||
.spyOn(SVGElement.prototype, 'getBoundingClientRect')
|
||||
.mockReturnValue({ width: 133, height: 106 } as DOMRect)
|
||||
|
||||
const result = makeSvgScalable(svgElement) as SVGElement
|
||||
|
||||
expect(spy).toHaveBeenCalled()
|
||||
expect(result.getAttribute('viewBox')).toBe('0 0 133 106')
|
||||
expect(result.style.maxWidth).toBe('133px')
|
||||
expect(result.getAttribute('width')).toBe('100%')
|
||||
expect(result.hasAttribute('height')).toBe(false)
|
||||
|
||||
spy.mockRestore() // Clean up the prototype spy
|
||||
})
|
||||
|
||||
it('should use width attribute for max-width when viewBox is present', () => {
|
||||
const svgElement = createSvgElement('<svg viewBox="0 0 50 50" width="100pt" height="80pt"></svg>')
|
||||
const spy = vi.spyOn(SVGElement.prototype, 'getBoundingClientRect') // Spy to ensure it's NOT called
|
||||
|
||||
const result = makeSvgScalable(svgElement) as SVGElement
|
||||
|
||||
expect(spy).not.toHaveBeenCalled()
|
||||
expect(result.getAttribute('viewBox')).toBe('0 0 50 50')
|
||||
expect(result.hasAttribute('width')).toBe(false)
|
||||
expect(result.style.maxWidth).toBe('100pt')
|
||||
expect(result.getAttribute('width')).toBe('100%')
|
||||
expect(result.hasAttribute('height')).toBe(false)
|
||||
|
||||
spy.mockRestore()
|
||||
})
|
||||
|
||||
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)
|
||||
it('should handle measurement failure gracefully', () => {
|
||||
const svgElement = createSvgElement('<svg width="100pt" height="80pt"></svg>')
|
||||
// Mock a failed measurement
|
||||
const spy = vi
|
||||
.spyOn(SVGElement.prototype, 'getBoundingClientRect')
|
||||
.mockReturnValue({ width: 0, height: 0 } as DOMRect)
|
||||
|
||||
const result = makeSvgScalable(svgElement) as SVGElement
|
||||
|
||||
expect(result.hasAttribute('viewBox')).toBe(false)
|
||||
expect(result.hasAttribute('width')).toBe(false)
|
||||
expect(result.hasAttribute('height')).toBe(false)
|
||||
expect(result.style.maxWidth).toBe('100pt') // Falls back to width attribute
|
||||
expect(result.getAttribute('width')).toBe('100%')
|
||||
|
||||
spy.mockRestore()
|
||||
})
|
||||
|
||||
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)
|
||||
it('should only set width="100%" if width/height attributes are missing', () => {
|
||||
const svgElement = createSvgElement('<svg></svg>')
|
||||
const result = makeSvgScalable(svgElement) as SVGElement
|
||||
|
||||
expect(result.hasAttribute('viewBox')).toBe(false)
|
||||
expect(result.style.maxWidth).toBe('')
|
||||
expect(result.getAttribute('width')).toBe('100%')
|
||||
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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -272,31 +272,79 @@ export const svgToSvgBlob = (svgElement: SVGElement): Blob => {
|
||||
}
|
||||
|
||||
/**
|
||||
* 确保 SVG 元素有 viewBox 并且移除固定的 width/height 属性。
|
||||
* 用于“预览”功能,让 SVG 在容器内可缩放。
|
||||
* 使用离屏容器测量 DOM 元素的渲染尺寸
|
||||
* @param element 要测量的元素
|
||||
* @returns 渲染元素的宽度和高度(以像素为单位)
|
||||
*/
|
||||
function measureElementSize(element: Element): { width: number; height: number } {
|
||||
const clone = element.cloneNode(true) as Element
|
||||
|
||||
// 检查元素类型并重置样式
|
||||
if (clone instanceof HTMLElement || clone instanceof SVGElement) {
|
||||
clone.style.width = ''
|
||||
clone.style.height = ''
|
||||
clone.style.position = ''
|
||||
clone.style.visibility = ''
|
||||
}
|
||||
|
||||
// 创建一个离屏容器
|
||||
const container = document.createElement('div')
|
||||
container.style.position = 'absolute'
|
||||
container.style.top = '-9999px'
|
||||
container.style.left = '-9999px'
|
||||
container.style.visibility = 'hidden'
|
||||
|
||||
container.appendChild(clone)
|
||||
document.body.appendChild(container)
|
||||
|
||||
// 测量并清理
|
||||
const rect = clone.getBoundingClientRect()
|
||||
document.body.removeChild(container)
|
||||
|
||||
return { width: rect.width, height: rect.height }
|
||||
}
|
||||
|
||||
/**
|
||||
* 让 SVG 元素在容器内可缩放,用于“预览”功能。
|
||||
* - 补充缺失的 viewBox
|
||||
* - 补充缺失的 max-width style
|
||||
* - 把 width 改为 100%
|
||||
* - 移除 height
|
||||
*/
|
||||
export const makeSvgScalable = (element: Element): Element => {
|
||||
// Type Guard: Only proceed if the element is actually an SVGElement.
|
||||
// type guard
|
||||
if (!(element instanceof SVGElement)) {
|
||||
return element
|
||||
}
|
||||
|
||||
const hasViewBox = element.hasAttribute('viewBox')
|
||||
const width = element.getAttribute('width')
|
||||
const height = element.getAttribute('height')
|
||||
const widthStr = element.getAttribute('width')
|
||||
const heightStr = 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}`)
|
||||
element.setAttribute('max-width', `${numericWidth}px`)
|
||||
let measuredWidth: number | undefined
|
||||
|
||||
// 如果缺少 viewBox 属性,测量元素尺寸来创建
|
||||
if (!hasViewBox) {
|
||||
// 只在 width 和 height 都有值时测量
|
||||
if (widthStr && heightStr) {
|
||||
const renderedSize = measureElementSize(element)
|
||||
if (renderedSize.width > 0 && renderedSize.height > 0) {
|
||||
measuredWidth = renderedSize.width
|
||||
element.setAttribute('viewBox', `0 0 ${renderedSize.width} ${renderedSize.height}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 移除固定的 width 和 height 属性,让 CSS 控制元素尺寸
|
||||
element.removeAttribute('width')
|
||||
// 设置 max-width
|
||||
// 优先使用测量得到的宽度值,否则回退到 width 属性值
|
||||
if (measuredWidth !== undefined) {
|
||||
element.style.setProperty('max-width', `${measuredWidth}px`)
|
||||
} else if (widthStr) {
|
||||
element.style.setProperty('max-width', widthStr)
|
||||
}
|
||||
|
||||
// 调整 width 和 height
|
||||
element.setAttribute('width', '100%')
|
||||
element.removeAttribute('height')
|
||||
|
||||
return element
|
||||
|
||||
Loading…
Reference in New Issue
Block a user