From e766ef0259f363d0332ab6bc94f8c05ba389cd85 Mon Sep 17 00:00:00 2001 From: Alexander Onnikov Date: Tue, 3 Feb 2026 12:00:40 +0700 Subject: [PATCH 1/2] fix: allow to open mermaid diagram in fullscreen --- packages/theme/styles/prose.scss | 15 +++- .../codeSnippets/MermaidPopup.svelte | 80 +++++++++++++++++ .../extension/codeSnippets/mermaid.ts | 87 ++++++++++++++++--- 3 files changed, 170 insertions(+), 12 deletions(-) create mode 100644 plugins/text-editor-resources/src/components/extension/codeSnippets/MermaidPopup.svelte 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..c92005940dc --- /dev/null +++ b/plugins/text-editor-resources/src/components/extension/codeSnippets/MermaidPopup.svelte @@ -0,0 +1,80 @@ + + + + { + dispatch('close') + }} +> + + { + 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') } } From 67896b38b67e446adc60dd994cc9eed3e6ffc8c4 Mon Sep 17 00:00:00 2001 From: Alexander Onnikov Date: Tue, 3 Feb 2026 14:55:43 +0700 Subject: [PATCH 2/2] fix large diagram scroll Signed-off-by: Alexander Onnikov --- .../codeSnippets/MermaidPopup.svelte | 25 +++++++------------ 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/plugins/text-editor-resources/src/components/extension/codeSnippets/MermaidPopup.svelte b/plugins/text-editor-resources/src/components/extension/codeSnippets/MermaidPopup.svelte index c92005940dc..df6e3692e64 100644 --- a/plugins/text-editor-resources/src/components/extension/codeSnippets/MermaidPopup.svelte +++ b/plugins/text-editor-resources/src/components/extension/codeSnippets/MermaidPopup.svelte @@ -15,7 +15,7 @@ // --> - { - dispatch('close') - }} -> + -
- -
+ +
+ +
+