d05d1309ca
* refactor(CodeBlockView): generalize tool and preview Generalize code tool to action tool - CodeTool -> ActionTool - usePreviewTools -> useImagePreview - rename code tool classname from icon to tool-icon Generalize preview - move image preview components to Preview dir - simplify file names * refactor(useImageTools): simplify implementation, add pan * refactor(Preview): move image tools to floating toolbar * refactor: add enableDrag, enable zooming for SvgPreview * test: add tests for preview components * feat(Preview): add download buttons to dropdown * refactor(Preview): remove setTools from preview, improve SvgPreview * refactor: add useTemporaryValue * test: add tests for hooks * test: add tests for CodeToolButton and CodeToolbar * refactor(PreviewTool): add a setting item to enable preview tools * test: update snapshot * refactor: extract more code tools to hooks, add tests * refactor: extract tools from CodeEditor and CodeViewer * test: add tests for new tool hooks * refactor(CodeEditor): change collapsible to expanded, change wrappable to unwrapped * refactor: migrate codePreview to codeViewer * docs: CodeBlockView * refactor: add custom file icons, center the reset button * refactor: improve code quality * refactor: improve migration by deprecating codePreview * refactor: improve PlantUml and svgToCanvas * fix: plantuml style * test: fix tests * fix: button icon * refactor(SvgPreview): debounce rendering * feat(PreviewTool): add a dialog tool * fix: remove isValidPlantUML, improve plantuml rendering * refactor: extract shadow dom renderer * refactor: improve plantuml error messages * test: add tests for ImageToolbar and ImageToolButton * refactor: add ImagePreviewLayout to simplify layout and tests * refactor: add useDebouncedRender, update docs * chore: clean up unused props * refactor: clean transformation before copy/download/preview * refactor: update migrate version * refactor: style refactoring and fixes - show header background in split view - fix status bar radius - reset special view transformation on theme change - fix wrap tool icon - add a divider to split view - improve split view toggling (switch back to previous view) - revert copy tool to separate tools - fix top border radius for special views * refactor: move GraphvizPreview to shadow DOM - use renderString - keep renderSvgInShadowHost api consistent with others * fix: tests, icons, deleted files * refactor: use ResetIcon in ImageToolbar * test: remove unnecessary tests * fix: min height for special preview * fix: update migrate
121 lines
3.5 KiB
TypeScript
121 lines
3.5 KiB
TypeScript
import { nanoid } from '@reduxjs/toolkit'
|
|
import { useMermaid } from '@renderer/hooks/useMermaid'
|
|
import React, { memo, useCallback, useEffect, useRef, useState } from 'react'
|
|
import styled from 'styled-components'
|
|
|
|
import { useDebouncedRender } from './hooks/useDebouncedRender'
|
|
import ImagePreviewLayout from './ImagePreviewLayout'
|
|
import { BasicPreviewHandles, BasicPreviewProps } from './types'
|
|
|
|
/** 预览 Mermaid 图表
|
|
* 使用 usePreviewRenderer hook 重构,同时保留必要的可见性检测逻辑
|
|
* FIXME: 等将来 mermaid-js 修复可见性问题后可以进一步简化
|
|
*/
|
|
const MermaidPreview = ({
|
|
children,
|
|
enableToolbar = false,
|
|
ref
|
|
}: BasicPreviewProps & { ref?: React.RefObject<BasicPreviewHandles | null> }) => {
|
|
const { mermaid, isLoading: isLoadingMermaid, error: mermaidError } = useMermaid()
|
|
const diagramId = useRef<string>(`mermaid-${nanoid(6)}`).current
|
|
const [isVisible, setIsVisible] = useState(true)
|
|
|
|
// 定义渲染函数
|
|
const renderMermaid = useCallback(
|
|
async (content: string, container: HTMLDivElement) => {
|
|
// 验证语法,提前抛出异常
|
|
await mermaid.parse(content)
|
|
|
|
const { svg } = await mermaid.render(diagramId, content, container)
|
|
|
|
// 避免不可见时产生 undefined 和 NaN
|
|
const fixedSvg = svg.replace(/translate\(undefined,\s*NaN\)/g, 'translate(0, 0)')
|
|
container.innerHTML = fixedSvg
|
|
},
|
|
[diagramId, mermaid]
|
|
)
|
|
|
|
// 可见性检测函数
|
|
const shouldRender = useCallback(() => {
|
|
return !isLoadingMermaid && isVisible
|
|
}, [isLoadingMermaid, isVisible])
|
|
|
|
// 使用预览渲染器 hook
|
|
const {
|
|
containerRef,
|
|
error: renderError,
|
|
isLoading: isRendering
|
|
} = useDebouncedRender(children, renderMermaid, {
|
|
debounceDelay: 300,
|
|
shouldRender
|
|
})
|
|
|
|
/**
|
|
* 监听可见性变化,用于触发重新渲染。
|
|
* 这是为了解决 `MessageGroup` 组件的 `fold` 布局中被 `display: none` 隐藏的图标无法正确渲染的问题。
|
|
* 监听时向上遍历到第一个有 `fold` className 的父节点为止(也就是目前的 `MessageWrapper`)。
|
|
* FIXME: 将来 mermaid-js 修复此问题后可以移除这里的相关逻辑。
|
|
*/
|
|
useEffect(() => {
|
|
if (!containerRef.current) return
|
|
|
|
const checkVisibility = () => {
|
|
const element = containerRef.current
|
|
if (!element) return
|
|
|
|
const currentlyVisible = element.offsetParent !== null
|
|
setIsVisible(currentlyVisible)
|
|
}
|
|
|
|
// 初始检查
|
|
checkVisibility()
|
|
|
|
const observer = new MutationObserver(() => {
|
|
checkVisibility()
|
|
})
|
|
|
|
let targetElement = containerRef.current.parentElement
|
|
while (targetElement) {
|
|
observer.observe(targetElement, {
|
|
attributes: true,
|
|
attributeFilter: ['class', 'style']
|
|
})
|
|
|
|
if (targetElement.className?.includes('fold')) {
|
|
break
|
|
}
|
|
|
|
targetElement = targetElement.parentElement
|
|
}
|
|
|
|
return () => {
|
|
observer.disconnect()
|
|
}
|
|
}, [containerRef])
|
|
|
|
// 合并加载状态和错误状态
|
|
const isLoading = isLoadingMermaid || isRendering
|
|
const error = mermaidError || renderError
|
|
|
|
return (
|
|
<ImagePreviewLayout
|
|
loading={isLoading}
|
|
error={error}
|
|
enableToolbar={enableToolbar}
|
|
ref={ref}
|
|
imageRef={containerRef}
|
|
source="mermaid">
|
|
<StyledMermaid ref={containerRef} className="mermaid special-preview" />
|
|
</ImagePreviewLayout>
|
|
)
|
|
}
|
|
|
|
const StyledMermaid = styled.div`
|
|
overflow: auto;
|
|
position: relative;
|
|
width: 100%;
|
|
height: 100%;
|
|
`
|
|
|
|
export default memo(MermaidPreview)
|