mirror of
https://github.com/hcengineering/platform.git
synced 2026-09-09 19:27:44 +02:00
fix: allow to open mermaid diagram in fullscreen (#10477)
* fix: allow to open mermaid diagram in fullscreen * fix large diagram scroll Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com> --------- Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com>
This commit is contained in:
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
<!--
|
||||
//
|
||||
// Copyright © 2026 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Html, Modal, ButtonIcon, IconClose, IconMaximize, IconMinimize, Scroller } from '@hcengineering/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
|
||||
export let svg: string
|
||||
export let fullSize = false
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
</script>
|
||||
|
||||
<Modal type={'type-component'} padding={'0.5rem'} bottomPadding={'0'} on:fullsize on:close>
|
||||
<svelte:fragment slot="beforeTitle">
|
||||
<ButtonIcon
|
||||
icon={IconClose}
|
||||
kind={'tertiary'}
|
||||
size={'small'}
|
||||
noPrint
|
||||
on:click={() => {
|
||||
dispatch('close')
|
||||
}}
|
||||
/>
|
||||
<div class="hulyHeader-divider short no-line no-print" />
|
||||
<ButtonIcon
|
||||
icon={!fullSize ? IconMaximize : IconMinimize}
|
||||
kind={'tertiary'}
|
||||
size={'small'}
|
||||
noPrint
|
||||
on:click={() => {
|
||||
fullSize = !fullSize
|
||||
dispatch('fullsize', fullSize)
|
||||
}}
|
||||
/>
|
||||
<div class="hulyHeader-divider short no-print" />
|
||||
</svelte:fragment>
|
||||
|
||||
<Scroller horizontal stickedScrollBars thinScrollBars>
|
||||
<div class="mermaid-container">
|
||||
<Html value={svg} />
|
||||
</div>
|
||||
</Scroller>
|
||||
</Modal>
|
||||
|
||||
<style>
|
||||
.mermaid-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.mermaid-container :global(svg) {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
}
|
||||
</style>
|
||||
@@ -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<MermaidOptions>({
|
||||
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<MermaidOptions>({
|
||||
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<MermaidOptions>({
|
||||
|
||||
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<MermaidOptions>({
|
||||
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')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user