Skip to content
Open
8 changes: 7 additions & 1 deletion src/Mermaid/Mermaid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const Mermaid = memo<MermaidProps>(
shadow,
enablePanZoom = true,
defaultExpand = true,
enableNonPreviewWheelZoom = true,
className,
bodyRender,
fileName,
Expand Down Expand Up @@ -78,7 +79,12 @@ const Mermaid = memo<MermaidProps>(
: originalActions;

const defaultBody = (
<SyntaxMermaid enablePanZoom={enablePanZoom} theme={theme} variant={variant}>
<SyntaxMermaid
enableNonPreviewWheelZoom={enableNonPreviewWheelZoom}
enablePanZoom={enablePanZoom}
theme={theme}
variant={variant}
>
{tirmedChildren}
</SyntaxMermaid>
);
Expand Down
104 changes: 82 additions & 22 deletions src/Mermaid/SyntaxMermaid/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

import { useTheme } from 'antd-style';
import { kebabCase } from 'lodash-es';
import { memo, useEffect, useId, useMemo, useState } from 'react';
import { memo, useEffect, useId, useMemo, useRef, useState } from 'react';
import { TransformComponent, TransformWrapper } from 'react-zoom-pan-pinch';

import Image from '@/Image';
import { useMermaid } from '@/hooks/useMermaid';
Expand All @@ -11,7 +12,7 @@ import { mermaidThemes } from '../const';
import type { SyntaxMermaidProps } from '../type';

const SyntaxMermaid = memo<SyntaxMermaidProps>(
({ ref, children, theme: customTheme, variant, enablePanZoom }) => {
({ ref, children, theme: customTheme, variant, enablePanZoom, enableNonPreviewWheelZoom }) => {
const isDefaultTheme = customTheme === 'lobe-theme' || !customTheme;

const background = useMemo(() => {
Expand All @@ -27,6 +28,9 @@ const SyntaxMermaid = memo<SyntaxMermaidProps>(
theme: isDefaultTheme ? undefined : customTheme,
});
const [blobUrl, setBlobUrl] = useState<string>();
const [isDragging, setIsDragging] = useState(false);
const [shouldPreventPreview, setShouldPreventPreview] = useState(false);
const containerRef = useRef(null);

// 组件卸载时清理 Blob URL,避免内存泄漏
useEffect(() => {
Expand All @@ -44,36 +48,92 @@ const SyntaxMermaid = memo<SyntaxMermaidProps>(
setBlobUrl(url);
}, [isLoading, data]);

const handlePanningStart = () => {
setIsDragging(true);
setShouldPreventPreview(false);
};

const handlePanning = () => {
if (isDragging) {
setShouldPreventPreview(true);
}
};

const handlePanningStop = () => {
setIsDragging(false);
setTimeout(() => {
setShouldPreventPreview(false);
}, 100);
};

if (!blobUrl) return null;

return (
<Image
alt={'mermaid'}
maxHeight={480}
objectFit={'contain'}
preview={
enablePanZoom
? {
mask: false,
styles: {
mask: {
background: background || theme.colorBgContainerSecondary,
},
},
}
: false
}
ref={ref}
src={blobUrl}
<div
ref={containerRef}
style={{
background: variant === 'filled' ? background : undefined,
borderRadius: 0,
cursor: 'grab',
margin: 0,
maxHeight: 480,
overflow: 'hidden',
padding: variant === 'borderless' ? 0 : 16,
position: 'relative',
}}
variant={'borderless'}
/>
>
<TransformWrapper
centerOnInit={true}
initialScale={1}
maxScale={8}
minScale={0.1}
onPanning={handlePanning}
onPanningStart={handlePanningStart}
onPanningStop={handlePanningStop}
panning={{
disabled: false,
velocityDisabled: false,
}}
wheel={{
step: 0.1,
touchPadDisabled: !enableNonPreviewWheelZoom,
wheelDisabled: !enableNonPreviewWheelZoom,
}}
>
<TransformComponent
contentStyle={{ display: 'block', height: '100%', width: '100%' }}
wrapperStyle={{ display: 'block', height: '100%', width: '100%' }}
>
<Image
alt={'mermaid'}
maxHeight={480}
objectFit={'contain'}
preview={
enablePanZoom && !shouldPreventPreview
? {
mask: false,
styles: {
mask: {
background: background || theme.colorBgContainerSecondary,
},
},
}
: false
}
ref={ref}
src={blobUrl}
style={{
background: variant === 'filled' ? background : undefined,
borderRadius: 0,
margin: 0,
padding: variant === 'borderless' ? 0 : 16,
position: 'relative',
}}
variant={'borderless'}
/>
</TransformComponent>
</TransformWrapper>
</div>
);
},
);
Expand Down
1 change: 1 addition & 0 deletions src/Mermaid/demos/FullFeatured.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export default () => {
value: code,
},
copyable: true,
enableNonPreviewWheelZoom: true,
enablePanZoom: true,
shadow: false,
showLanguage: true,
Expand Down
1 change: 1 addition & 0 deletions src/Mermaid/demos/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export default () => {
value: code,
},
copyable: true,
enableNonPreviewWheelZoom: true,
enablePanZoom: false,
shadow: false,
showLanguage: true,
Expand Down
31 changes: 16 additions & 15 deletions src/Mermaid/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -388,18 +388,19 @@ export default () => {

## APIs

| Property | Description | Type | Default |
| -------------- | -------------------------------------------------------- | ---------------------------------------- | ----------- |
| children | Mermaid diagram content as a string | `string` | - |
| fullFeatured | Whether to use the full-featured mode with more controls | `boolean` | - |
| variant | Style variant of the container | `'filled' \| 'outlined' \| 'borderless'` | `'filled'` |
| shadow | Whether to show shadow effect | `boolean` | - |
| enablePanZoom | Enable pan and zoom functionality | `boolean` | `true` |
| copyable | Whether to show copy button | `boolean` | `true` |
| showLanguage | Whether to show language label | `boolean` | `true` |
| language | The language label to display | `string` | `'mermaid'` |
| theme | Theme for the diagram | `'lobe-theme' \| MermaidConfig['theme']` | - |
| defaultExpand | Whether to expand by default (for fullFeatured mode) | `boolean` | `true` |
| actionIconSize | Size of action icons | `ActionIconProps['size']` | - |
| actionsRender | Custom renderer for action buttons | `(props) => ReactNode` | - |
| bodyRender | Custom renderer for diagram body | `(props) => ReactNode` | - |
| Property | Description | Type | Default |
| ------------------------- | -------------------------------------------------------- | ---------------------------------------- | ----------- |
| children | Mermaid diagram content as a string | `string` | - |
| fullFeatured | Whether to use the full-featured mode with more controls | `boolean` | - |
| variant | Style variant of the container | `'filled' \| 'outlined' \| 'borderless'` | `'filled'` |
| shadow | Whether to show shadow effect | `boolean` | - |
| enablePanZoom | Enable pan and zoom functionality | `boolean` | `true` |
| copyable | Whether to show copy button | `boolean` | `true` |
| showLanguage | Whether to show language label | `boolean` | `true` |
| language | The language label to display | `string` | `'mermaid'` |
| theme | Theme for the diagram | `'lobe-theme' \| MermaidConfig['theme']` | - |
| defaultExpand | Whether to expand by default (for fullFeatured mode) | `boolean` | `true` |
| actionIconSize | Size of action icons | `ActionIconProps['size']` | - |
| actionsRender | Custom renderer for action buttons | `(props) => ReactNode` | - |
| bodyRender | Custom renderer for diagram body | `(props) => ReactNode` | - |
| enableNonPreviewWheelZoom | Enable zoom outof preview mode | `boolean` | `true` |
2 changes: 2 additions & 0 deletions src/Mermaid/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type { DivProps } from '@/types';

export interface SyntaxMermaidProps {
children: string;
enableNonPreviewWheelZoom?: MermaidProps['enableNonPreviewWheelZoom'];
enablePanZoom?: MermaidProps['enablePanZoom'];
ref?: Ref<HTMLDivElement>;
theme?: MermaidProps['theme'];
Expand All @@ -23,6 +24,7 @@ export interface MermaidProps extends DivProps {
children: string;
copyable?: boolean;
defaultExpand?: boolean;
enableNonPreviewWheelZoom?: boolean;
enablePanZoom?: boolean;
fileName?: string;
fullFeatured?: boolean;
Expand Down