Skip to content

Commit

Permalink
feat(ImageViewer): support native svg preview (#2958)
Browse files Browse the repository at this point in the history
* feat(image-viewer): support native svg display

* fix: optimiza element selector

* chore: fix spell check

---------

Co-authored-by: Heising <[email protected]>
  • Loading branch information
HaixingOoO and Heising authored Jun 20, 2024
1 parent 53404f6 commit 5118760
Show file tree
Hide file tree
Showing 9 changed files with 396 additions and 35 deletions.
1 change: 1 addition & 0 deletions src/image-viewer/ImageViewerMini.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export const ImageModalMiniContent: React.FC<ImageModalMiniProps> = (props) => {
preSrc={props.currentImage.thumbnail}
errorText={props.errorText}
imageReferrerpolicy={props.imageReferrerpolicy}
isSvg={props.currentImage.isSvg}
/>
</div>
);
Expand Down
78 changes: 74 additions & 4 deletions src/image-viewer/ImageViewerModal.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState, useEffect, useCallback, MouseEvent, KeyboardEvent } from 'react';
import React, { useState, useEffect, useCallback, MouseEvent, KeyboardEvent, useRef } from 'react';
import isArray from 'lodash/isArray';
import isFunction from 'lodash/isFunction';
import {
Expand Down Expand Up @@ -51,6 +51,7 @@ interface ImageModalItemProps {
preSrc?: string | File;
errorText: string;
imageReferrerpolicy?: TdImageViewerProps['imageReferrerpolicy'];
isSvg: boolean;
}

// 单个弹窗实例
Expand All @@ -62,12 +63,14 @@ export const ImageModalItem: React.FC<ImageModalItemProps> = ({
mirror,
errorText,
imageReferrerpolicy,
isSvg,
}) => {
const { classPrefix } = useConfig();

const [position, onMouseDown] = usePosition({ initPosition: [0, 0] });
const [loaded, setLoaded] = useState(false);
const [error, setError] = useState(false);
const attachSvgElRef = useRef<HTMLDivElement>(null);

const imgStyle = {
transform: `rotateZ(${rotateZ}deg) scale(${scale})`,
Expand All @@ -79,15 +82,67 @@ export const ImageModalItem: React.FC<ImageModalItemProps> = ({
const { previewUrl: preSrcImagePreviewUrl } = useImagePreviewUrl(preSrc);
const { previewUrl: mainImagePreviewUrl } = useImagePreviewUrl(src);

const createSvgShadow = async (url: string) => {
const response = await fetch(url);
if (!response.ok) {
setError(true);
throw new Error(`Failed to fetch SVG: ${response.statusText}`);
}

const svgText = await response.text();

const element = attachSvgElRef.current;
element.innerHTML = '';
element.classList?.add(`${classPrefix}-image-viewer__modal-image-svg`);
const shadowRoot = element.attachShadow({ mode: 'closed' });

const container = document.createElement('div');
container.style.background = 'transparent';
container.innerHTML = svgText;
shadowRoot.appendChild(container);

const svgElement = container.querySelector('svg');
if (svgElement) {
const svgViewBox = svgElement.getAttribute('viewBox');
if (svgViewBox) {
const viewBoxValues = svgViewBox
.split(/[\s,]/)
.filter((v) => v)
.map(parseFloat);

// svg viewbox x(0) and y(1) offset, width(2) and height(3),eg
const svgViewBoxWidth = viewBoxValues[2];
const svgViewBoxHeight = viewBoxValues[3];
container.style.width = `${svgViewBoxWidth}px`;
container.style.height = `${svgViewBoxHeight}px`;
} else {
const bbox = svgElement.getBBox();
const calculatedViewBox = `${bbox.x} ${bbox.y} ${bbox.width} ${bbox.height}`;
svgElement.setAttribute('viewBox', calculatedViewBox);

container.style.width = `${bbox.width}px`;
container.style.height = `${bbox.height}px`;
}
}
setLoaded(true);
};

useEffect(() => {
setError(false);
}, [preSrcImagePreviewUrl, mainImagePreviewUrl]);

useEffect(() => {
if (isSvg && mainImagePreviewUrl) {
createSvgShadow(mainImagePreviewUrl);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [mainImagePreviewUrl]);

return (
<div className={`${classPrefix}-image-viewer__modal-pic`}>
<div className={`${classPrefix}-image-viewer__modal-box`} style={boxStyle}>
{error && <ImageError errorText={errorText} />}
{!error && !!preSrc && (
{!error && !!preSrc && preSrcImagePreviewUrl && (
<img
className={`${classPrefix}-image-viewer__modal-image`}
onMouseDown={(event) => {
Expand All @@ -101,21 +156,35 @@ export const ImageModalItem: React.FC<ImageModalItemProps> = ({
draggable="false"
/>
)}
{!error && (
{!error && mainImagePreviewUrl && !isSvg && (
<img
className={`${classPrefix}-image-viewer__modal-image`}
onMouseDown={(event) => {
event.stopPropagation();
onMouseDown(event);
}}
src={mainImagePreviewUrl}
style={imgStyle}
onLoad={() => setLoaded(true)}
onError={() => setError(true)}
style={imgStyle}
referrerPolicy={imageReferrerpolicy}
alt="image"
draggable="false"
/>
)}
{!error && !!mainImagePreviewUrl && isSvg && (
<div
ref={attachSvgElRef}
className={`${classPrefix}-image-viewer__modal-image`}
onMouseDown={(event) => {
event.stopPropagation();
onMouseDown(event);
}}
style={imgStyle}
data-alt="svg"
draggable="false"
/>
)}
</div>
</div>
);
Expand Down Expand Up @@ -518,6 +587,7 @@ export const ImageModal: React.FC<ImageModalProps> = (props) => {
src={currentImage.mainImage}
errorText={errorText}
imageReferrerpolicy={imageReferrerpolicy}
isSvg={currentImage.isSvg}
/>
</div>
);
Expand Down
69 changes: 69 additions & 0 deletions src/image-viewer/_example/svg.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import React from 'react';
import { ImageViewer, Image } from 'tdesign-react';
import { BrowseIcon } from 'tdesign-icons-react';

const img = [
{
mainImage: 'https://tdesign.gtimg.com/demo/tdesign-logo.svg',
isSvg: true,
},
{
mainImage: 'https://tdesign.gtimg.com/demo/demo-image-1.png',
},
];

const Svg = () => {
const trigger = ({ open }) => {
const mask = (
<div
style={{
background: 'rgba(0,0,0,.6)',
color: '#fff',
height: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
onClick={open}
>
<span>
<BrowseIcon size="16px" name={'browse'} /> 预览
</span>
</div>
);

return (
<Image
alt={'test'}
src={img[0].mainImage}
overlayContent={mask}
overlayTrigger="hover"
fit="contain"
style={{
width: 160,
height: 160,
border: '4px solid var(--td-bg-color-secondarycontainer)',
borderRadius: 'var(--td-radius-medium)',
backgroundColor: '#fff',
}}
/>
);
};
return (
<div
style={{
position: 'fixed',
inset: 0,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<div>
<ImageViewer trigger={trigger} images={img} />
</div>
</div>
);
};

export default Svg;
33 changes: 22 additions & 11 deletions src/image-viewer/hooks/useList.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,30 @@
import { useEffect, useState } from 'react';
import { ImageInfo } from '../type';
import isString from 'lodash/isString';
import isArray from 'lodash/isArray';
import type { ImageInfo, TdImageViewerProps } from '../type';

const checkImages = (images) =>
images.map((image) => {
let result: ImageInfo = { mainImage: '' };
if (typeof image === 'object' && !(image instanceof File)) {
result = image;
} else {
result.mainImage = image;
result.thumbnail = image;
const isImageInfo = (image: string | File | ImageInfo): image is ImageInfo =>
!!image && !isString(image) && !(image instanceof File);

const checkImages = (images: TdImageViewerProps['images']) => {
if (!isArray(images)) return [];
return images.map((item) => {
if (isImageInfo(item)) {
return {
download: true,
thumbnail: item.mainImage,
...item,
};
}
return result;
return {
mainImage: item,
thumbnail: item,
download: true,
};
});
};

const useList = (images) => {
const useList = (images: TdImageViewerProps['images']) => {
const [list, setList] = useState(() => checkImages(images));

useEffect(() => {
Expand Down
2 changes: 1 addition & 1 deletion src/image-viewer/image-viewer.en-US.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ closeOnOverlay | Boolean | - | \- | N
draggable | Boolean | undefined | \- | N
imageReferrerpolicy | String | - | attribute of `<img>`, [MDN Definition](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy)。options: no-referrer/no-referrer-when-downgrade/origin/origin-when-cross-origin/same-origin/strict-origin/strict-origin-when-cross-origin/unsafe-url | N
imageScale | Object | - | Typescript:`ImageScale` `interface ImageScale { max: number; min: number; step: number; defaultScale?: number; }`[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/src/image-viewer/type.ts) | N
images | Array | [] | Typescript:`Array<string \| File \| ImageInfo>` `interface ImageInfo { mainImage: string \| File; thumbnail?: string \| File; download?: boolean }`[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/src/image-viewer/type.ts) | N
images | Array | [] | Typescript:`Array<string \| File \| ImageInfo>` `interface ImageInfo { mainImage: string \| File; thumbnail?: string \| File; download?: boolean; isSvg?: boolean }`[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/src/image-viewer/type.ts) | N
index | Number | 0 | \- | N
defaultIndex | Number | 0 | uncontrolled property | N
mode | String | modal | options: modal/modeless | N
Expand Down
2 changes: 1 addition & 1 deletion src/image-viewer/image-viewer.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ closeOnOverlay | Boolean | - | 是否在点击遮罩层时,触发预览关闭
draggable | Boolean | undefined | 是否允许拖拽调整位置。`mode=modal` 时,默认不允许拖拽;`mode=modeless` 时,默认允许拖拽 | N
imageReferrerpolicy | String | - | 图片预览中的 `<img>` 标签的原生属性,[MDN 定义](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy)。可选项:no-referrer/no-referrer-when-downgrade/origin/origin-when-cross-origin/same-origin/strict-origin/strict-origin-when-cross-origin/unsafe-url | N
imageScale | Object | - | 图片缩放相关配置。`imageScale.max` 缩放的最大比例;`imageScale.min` 缩放的最小比例;`imageScale.step` 缩放的步长速度; `imageScale.defaultScale` 默认的缩放比例。TS 类型:`ImageScale` `interface ImageScale { max: number; min: number; step: number; defaultScale?: number; }`[详细类型定义](https://github.com/Tencent/tdesign-react/blob/develop/src/image-viewer/type.ts) | N
images | Array | [] | 图片数组。`mainImage` 表示主图,必传;`thumbnail` 表示缩略图,如果不存在,则使用主图显示;`download` 是否允许下载图片,默认允许下载。示例: `['img_url_1', 'img_url_2']``[{ thumbnail: 'small_image_url', mainImage: 'big_image_url', download: false }]`。TS 类型:`Array<string \| File \| ImageInfo>` `interface ImageInfo { mainImage: string \| File; thumbnail?: string \| File; download?: boolean }`[详细类型定义](https://github.com/Tencent/tdesign-react/blob/develop/src/image-viewer/type.ts) | N
images | Array | [] | 图片数组。`mainImage` 表示主图,必传;`thumbnail` 表示缩略图,如果不存在,则使用主图显示;`download` 是否允许下载图片,默认允许下载。示例: `['img_url_1', 'img_url_2']``[{ thumbnail: 'small_image_url', mainImage: 'big_image_url', download: false }]`。TS 类型:`Array<string \| File \| ImageInfo>` `interface ImageInfo { mainImage: string \| File; thumbnail?: string \| File; download?: boolean; isSvg?: boolean }`[详细类型定义](https://github.com/Tencent/tdesign-react/blob/develop/src/image-viewer/type.ts) | N
index | Number | 0 | 当前预览图片所在的下标 | N
defaultIndex | Number | 0 | 当前预览图片所在的下标。非受控属性 | N
mode | String | modal | 模态预览(modal)和非模态预览(modeless)。可选项:modal/modeless | N
Expand Down
19 changes: 1 addition & 18 deletions src/image-viewer/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,28 +114,11 @@ export interface ImageScale {
defaultScale?: number;
}

export interface ImageScale {
max: number;
min: number;
step: number;
defaultScale?: number;
}

export interface ImageInfo {
mainImage: string | File;
thumbnail?: string | File;
download?: boolean;
}

export interface ImageInfo {
mainImage: string | File;
thumbnail?: string | File;
download?: boolean;
}

export interface ImageViewerScale {
minWidth: number;
minHeight: number;
isSvg?: boolean;
}

export interface ImageViewerScale {
Expand Down
Loading

0 comments on commit 5118760

Please sign in to comment.