diff --git a/packages/theme/styles/prose.scss b/packages/theme/styles/prose.scss
index 6c6c1b541b4..1797af3e72e 100644
--- a/packages/theme/styles/prose.scss
+++ b/packages/theme/styles/prose.scss
@@ -574,13 +574,24 @@ pre.proseCodeBlock>pre.proseCode {
.mermaidPreviewContainer {
padding: 0.5rem;
cursor: default;
- overflow-x: auto;
+ overflow: auto;
}
&:not(.folded) .mermaidPreviewContainer {
border-top: 1px solid var(--border-color);
min-height: 6rem;
}
+
+ .mermaidPreview {
+ width: 100%;
+
+ svg {
+ width: 100%;
+ max-width: 100%;
+ height: auto;
+ display: block;
+ }
+ }
}
.proseInlineCommentHighlight {
@@ -602,4 +613,4 @@ pre.proseCodeBlock>pre.proseCode {
.theme-light {
@include meta.load-css('./github-light.scss');
-}
\ No newline at end of file
+}
diff --git a/plugins/text-editor-resources/src/components/extension/codeSnippets/MermaidPopup.svelte b/plugins/text-editor-resources/src/components/extension/codeSnippets/MermaidPopup.svelte
new file mode 100644
index 00000000000..df6e3692e64
--- /dev/null
+++ b/plugins/text-editor-resources/src/components/extension/codeSnippets/MermaidPopup.svelte
@@ -0,0 +1,73 @@
+
+
+
+
+
+ {
+ dispatch('close')
+ }}
+ />
+
+ {
+ fullSize = !fullSize
+ dispatch('fullsize', fullSize)
+ }}
+ />
+
+
+
+
+
+
+
+
+
+
+
diff --git a/plugins/text-editor-resources/src/components/extension/codeSnippets/mermaid.ts b/plugins/text-editor-resources/src/components/extension/codeSnippets/mermaid.ts
index 610cbfa03df..b12e6e40e55 100644
--- a/plugins/text-editor-resources/src/components/extension/codeSnippets/mermaid.ts
+++ b/plugins/text-editor-resources/src/components/extension/codeSnippets/mermaid.ts
@@ -15,16 +15,19 @@
import { codeBlockOptions } from '@hcengineering/text'
import { getCurrentTheme, isThemeDark, themeStore } from '@hcengineering/theme'
+import { showPopup } from '@hcengineering/ui'
+import { mergeAttributes } from '@tiptap/core'
import { CodeBlockLowlight, type CodeBlockLowlightOptions } from '@tiptap/extension-code-block-lowlight'
import { type Node as ProseMirrorNode } from '@tiptap/pm/model'
import { NodeSelection, Plugin, PluginKey, TextSelection, type Transaction } from '@tiptap/pm/state'
import { Decoration, DecorationSet, type EditorView } from '@tiptap/pm/view'
import { createLowlight } from 'lowlight'
import type { MermaidConfig } from 'mermaid'
+import { createRelativePositionFromTypeIndex, type RelativePosition, type Doc as YDoc } from 'yjs'
+
import { isChangeEditable } from '../hooks/editable'
-import { mergeAttributes } from '@tiptap/core'
-import { createRelativePositionFromTypeIndex, type RelativePosition, type Doc as YDoc } from 'yjs'
+import MermaidPopup from './MermaidPopup.svelte'
export interface MermaidOptions extends CodeBlockLowlightOptions {
ydoc?: YDoc
@@ -156,6 +159,9 @@ export const MermaidExtension = CodeBlockLowlight.extend({
diagramBuilder: () => null,
textContent: node.textContent
}
+ let pendingSelection = false
+ let pendingSelectionPos: number | null = null
+ let pendingSelectionTimer: number | null = null
const toggleFoldState = (newState: boolean, event?: MouseEvent): void => {
event?.preventDefault()
@@ -187,16 +193,56 @@ export const MermaidExtension = CodeBlockLowlight.extend({
toggleButtonNode.onmousedown = (e) => {
toggleFoldState(!nodeState.folded, e)
}
- previewNode.ondblclick = (e) => {
- toggleFoldState(!nodeState.folded, e)
- }
previewNode.onclick = (e) => {
if (typeof getPos !== 'function') return
- const pos = getPos()
- const selection = NodeSelection.create(editor.view.state.doc, pos)
- editor.view.dispatch(editor.view.state.tr.setSelection(selection))
+ if (node?.type.name !== MermaidExtension.name) return
+
e.preventDefault()
+ e.stopPropagation()
+
+ const pos = getPos()
+
+ const { selection } = editor.view.state
+ const mermaid = editor.view.state.doc.nodeAt(pos)
+ const isSelected =
+ containerNode.classList.contains('selected') ||
+ nodeState.selected ||
+ (pendingSelection && pendingSelectionPos === pos) ||
+ (selection instanceof NodeSelection &&
+ mermaid !== null &&
+ selection.from === pos &&
+ selection.to === pos + mermaid.nodeSize)
+
+ if (!isSelected) {
+ const nodePatch: NodePatchSpec = {
+ pos,
+ folded: nodeState.folded,
+ selected: true
+ }
+
+ const selection = NodeSelection.create(editor.view.state.doc, pos)
+ const tr = setTxMeta(editor.view.state.tr, { nodePatch })
+ tr.setSelection(selection)
+
+ editor.view.dispatch(tr)
+ pendingSelection = true
+ pendingSelectionPos = pos
+ if (pendingSelectionTimer !== null) {
+ clearTimeout(pendingSelectionTimer)
+ }
+ pendingSelectionTimer = window.setTimeout(() => {
+ pendingSelection = false
+ pendingSelectionPos = null
+ pendingSelectionTimer = null
+ }, 1000)
+ return
+ }
+
+ const diagram = nodeState.diagramBuilder?.(editor.view)
+ if (diagram?.svg != null) {
+ showPopup(MermaidPopup, { svg: diagram.svg, fullSize: true }, 'centered')
+ }
}
const syncState = (decorations: readonly Decoration[]): void => {
@@ -228,8 +274,10 @@ export const MermaidExtension = CodeBlockLowlight.extend({
if (nodeState.selected) {
containerNode.classList.add('selected')
+ previewNode.style.cursor = 'zoom-in'
} else {
containerNode.classList.remove('selected')
+ previewNode.style.cursor = 'default'
}
if (!isEmpty && error !== null) {
@@ -255,6 +303,15 @@ export const MermaidExtension = CodeBlockLowlight.extend({
if (!allowFold && nodeState.folded) {
toggleFoldState(false)
}
+
+ if (nodeState.selected && pendingSelection) {
+ pendingSelection = false
+ pendingSelectionPos = null
+ if (pendingSelectionTimer !== null) {
+ clearTimeout(pendingSelectionTimer)
+ pendingSelectionTimer = null
+ }
+ }
}
const toggleSelection = (newState: boolean): void => {
@@ -416,14 +473,24 @@ function buildState (
// Ensure SVG maintains its natural size
const svg = container.querySelector('svg')
if (svg !== null) {
+ svg.style.width = '100%'
+ svg.style.maxWidth = '100%'
svg.style.height = 'auto'
+ svg.style.display = 'block'
// Remove any width/height attributes that might cause stretching
if (!svg.hasAttribute('viewBox')) {
const width = svg.getAttribute('width')
const height = svg.getAttribute('height')
- if (width !== null && height !== null) {
- svg.setAttribute('viewBox', `0 0 ${width} ${height}`)
+ const widthValue = width !== null ? Number.parseFloat(width) : NaN
+ const heightValue = height !== null ? Number.parseFloat(height) : NaN
+ if (Number.isFinite(widthValue) && Number.isFinite(heightValue)) {
+ svg.setAttribute('viewBox', `0 0 ${widthValue} ${heightValue}`)
+ svg.removeAttribute('width')
+ svg.removeAttribute('height')
}
+ } else {
+ svg.removeAttribute('width')
+ svg.removeAttribute('height')
}
}