1515
1616import { codeBlockOptions } from '@hcengineering/text'
1717import { getCurrentTheme , isThemeDark , themeStore } from '@hcengineering/theme'
18+ import { showPopup } from '@hcengineering/ui'
19+ import { mergeAttributes } from '@tiptap/core'
1820import { CodeBlockLowlight , type CodeBlockLowlightOptions } from '@tiptap/extension-code-block-lowlight'
1921import { type Node as ProseMirrorNode } from '@tiptap/pm/model'
2022import { NodeSelection , Plugin , PluginKey , TextSelection , type Transaction } from '@tiptap/pm/state'
2123import { Decoration , DecorationSet , type EditorView } from '@tiptap/pm/view'
2224import { createLowlight } from 'lowlight'
2325import type { MermaidConfig } from 'mermaid'
26+ import { createRelativePositionFromTypeIndex , type RelativePosition , type Doc as YDoc } from 'yjs'
27+
2428import { isChangeEditable } from '../hooks/editable'
2529
26- import { mergeAttributes } from '@tiptap/core'
27- import { createRelativePositionFromTypeIndex , type RelativePosition , type Doc as YDoc } from 'yjs'
30+ import MermaidPopup from './MermaidPopup.svelte'
2831
2932export interface MermaidOptions extends CodeBlockLowlightOptions {
3033 ydoc ?: YDoc
@@ -156,6 +159,9 @@ export const MermaidExtension = CodeBlockLowlight.extend<MermaidOptions>({
156159 diagramBuilder : ( ) => null ,
157160 textContent : node . textContent
158161 }
162+ let pendingSelection = false
163+ let pendingSelectionPos : number | null = null
164+ let pendingSelectionTimer : number | null = null
159165
160166 const toggleFoldState = ( newState : boolean , event ?: MouseEvent ) : void => {
161167 event ?. preventDefault ( )
@@ -187,16 +193,56 @@ export const MermaidExtension = CodeBlockLowlight.extend<MermaidOptions>({
187193 toggleButtonNode . onmousedown = ( e ) => {
188194 toggleFoldState ( ! nodeState . folded , e )
189195 }
190- previewNode . ondblclick = ( e ) => {
191- toggleFoldState ( ! nodeState . folded , e )
192- }
193196
194197 previewNode . onclick = ( e ) => {
195198 if ( typeof getPos !== 'function' ) return
196- const pos = getPos ( )
197- const selection = NodeSelection . create ( editor . view . state . doc , pos )
198- editor . view . dispatch ( editor . view . state . tr . setSelection ( selection ) )
199+ if ( node ?. type . name !== MermaidExtension . name ) return
200+
199201 e . preventDefault ( )
202+ e . stopPropagation ( )
203+
204+ const pos = getPos ( )
205+
206+ const { selection } = editor . view . state
207+ const mermaid = editor . view . state . doc . nodeAt ( pos )
208+ const isSelected =
209+ containerNode . classList . contains ( 'selected' ) ||
210+ nodeState . selected ||
211+ ( pendingSelection && pendingSelectionPos === pos ) ||
212+ ( selection instanceof NodeSelection &&
213+ mermaid !== null &&
214+ selection . from === pos &&
215+ selection . to === pos + mermaid . nodeSize )
216+
217+ if ( ! isSelected ) {
218+ const nodePatch : NodePatchSpec = {
219+ pos,
220+ folded : nodeState . folded ,
221+ selected : true
222+ }
223+
224+ const selection = NodeSelection . create ( editor . view . state . doc , pos )
225+ const tr = setTxMeta ( editor . view . state . tr , { nodePatch } )
226+ tr . setSelection ( selection )
227+
228+ editor . view . dispatch ( tr )
229+ pendingSelection = true
230+ pendingSelectionPos = pos
231+ if ( pendingSelectionTimer !== null ) {
232+ clearTimeout ( pendingSelectionTimer )
233+ }
234+ pendingSelectionTimer = window . setTimeout ( ( ) => {
235+ pendingSelection = false
236+ pendingSelectionPos = null
237+ pendingSelectionTimer = null
238+ } , 1000 )
239+ return
240+ }
241+
242+ const diagram = nodeState . diagramBuilder ?.( editor . view )
243+ if ( diagram ?. svg != null ) {
244+ showPopup ( MermaidPopup , { svg : diagram . svg , fullSize : true } , 'centered' )
245+ }
200246 }
201247
202248 const syncState = ( decorations : readonly Decoration [ ] ) : void => {
@@ -228,8 +274,10 @@ export const MermaidExtension = CodeBlockLowlight.extend<MermaidOptions>({
228274
229275 if ( nodeState . selected ) {
230276 containerNode . classList . add ( 'selected' )
277+ previewNode . style . cursor = 'zoom-in'
231278 } else {
232279 containerNode . classList . remove ( 'selected' )
280+ previewNode . style . cursor = 'default'
233281 }
234282
235283 if ( ! isEmpty && error !== null ) {
@@ -255,6 +303,15 @@ export const MermaidExtension = CodeBlockLowlight.extend<MermaidOptions>({
255303 if ( ! allowFold && nodeState . folded ) {
256304 toggleFoldState ( false )
257305 }
306+
307+ if ( nodeState . selected && pendingSelection ) {
308+ pendingSelection = false
309+ pendingSelectionPos = null
310+ if ( pendingSelectionTimer !== null ) {
311+ clearTimeout ( pendingSelectionTimer )
312+ pendingSelectionTimer = null
313+ }
314+ }
258315 }
259316
260317 const toggleSelection = ( newState : boolean ) : void => {
@@ -416,14 +473,24 @@ function buildState (
416473 // Ensure SVG maintains its natural size
417474 const svg = container . querySelector ( 'svg' )
418475 if ( svg !== null ) {
476+ svg . style . width = '100%'
477+ svg . style . maxWidth = '100%'
419478 svg . style . height = 'auto'
479+ svg . style . display = 'block'
420480 // Remove any width/height attributes that might cause stretching
421481 if ( ! svg . hasAttribute ( 'viewBox' ) ) {
422482 const width = svg . getAttribute ( 'width' )
423483 const height = svg . getAttribute ( 'height' )
424- if ( width !== null && height !== null ) {
425- svg . setAttribute ( 'viewBox' , `0 0 ${ width } ${ height } ` )
484+ const widthValue = width !== null ? Number . parseFloat ( width ) : NaN
485+ const heightValue = height !== null ? Number . parseFloat ( height ) : NaN
486+ if ( Number . isFinite ( widthValue ) && Number . isFinite ( heightValue ) ) {
487+ svg . setAttribute ( 'viewBox' , `0 0 ${ widthValue } ${ heightValue } ` )
488+ svg . removeAttribute ( 'width' )
489+ svg . removeAttribute ( 'height' )
426490 }
491+ } else {
492+ svg . removeAttribute ( 'width' )
493+ svg . removeAttribute ( 'height' )
427494 }
428495 }
429496
0 commit comments