Skip to content

Commit 87b8037

Browse files
committed
fix(mermaid): themechange detector + expand simplification
1 parent e59181c commit 87b8037

File tree

3 files changed

+88
-166
lines changed

3 files changed

+88
-166
lines changed

quartz/components/scripts/mermaid.inline.ts

+78-68
Original file line numberDiff line numberDiff line change
@@ -12,27 +12,42 @@ class DiagramPanZoom {
1212
private scale = 1
1313
private readonly MIN_SCALE = 0.5
1414
private readonly MAX_SCALE = 3
15-
private readonly ZOOM_SENSITIVITY = 0.001
15+
16+
cleanups: (() => void)[] = []
1617

1718
constructor(
1819
private container: HTMLElement,
1920
private content: HTMLElement,
2021
) {
2122
this.setupEventListeners()
2223
this.setupNavigationControls()
24+
this.resetTransform()
2325
}
2426

2527
private setupEventListeners() {
2628
// Mouse drag events
27-
this.container.addEventListener("mousedown", this.onMouseDown.bind(this))
28-
document.addEventListener("mousemove", this.onMouseMove.bind(this))
29-
document.addEventListener("mouseup", this.onMouseUp.bind(this))
30-
31-
// Wheel zoom events
32-
this.container.addEventListener("wheel", this.onWheel.bind(this), { passive: false })
29+
const mouseDownHandler = this.onMouseDown.bind(this)
30+
const mouseMoveHandler = this.onMouseMove.bind(this)
31+
const mouseUpHandler = this.onMouseUp.bind(this)
32+
const resizeHandler = this.resetTransform.bind(this)
33+
34+
this.container.addEventListener("mousedown", mouseDownHandler)
35+
document.addEventListener("mousemove", mouseMoveHandler)
36+
document.addEventListener("mouseup", mouseUpHandler)
37+
window.addEventListener("resize", resizeHandler)
38+
39+
this.cleanups.push(
40+
() => this.container.removeEventListener("mousedown", mouseDownHandler),
41+
() => document.removeEventListener("mousemove", mouseMoveHandler),
42+
() => document.removeEventListener("mouseup", mouseUpHandler),
43+
() => window.removeEventListener("resize", resizeHandler),
44+
)
45+
}
3346

34-
// Reset on window resize
35-
window.addEventListener("resize", this.resetTransform.bind(this))
47+
cleanup() {
48+
for (const cleanup of this.cleanups) {
49+
cleanup()
50+
}
3651
}
3752

3853
private setupNavigationControls() {
@@ -84,26 +99,6 @@ class DiagramPanZoom {
8499
this.container.style.cursor = "grab"
85100
}
86101

87-
private onWheel(e: WheelEvent) {
88-
e.preventDefault()
89-
90-
const delta = -e.deltaY * this.ZOOM_SENSITIVITY
91-
const newScale = Math.min(Math.max(this.scale + delta, this.MIN_SCALE), this.MAX_SCALE)
92-
93-
// Calculate mouse position relative to content
94-
const rect = this.content.getBoundingClientRect()
95-
const mouseX = e.clientX - rect.left
96-
const mouseY = e.clientY - rect.top
97-
98-
// Adjust pan to zoom around mouse position
99-
const scaleDiff = newScale - this.scale
100-
this.currentPan.x -= mouseX * scaleDiff
101-
this.currentPan.y -= mouseY * scaleDiff
102-
103-
this.scale = newScale
104-
this.updateTransform()
105-
}
106-
107102
private zoom(delta: number) {
108103
const newScale = Math.min(Math.max(this.scale + delta, this.MIN_SCALE), this.MAX_SCALE)
109104

@@ -126,7 +121,11 @@ class DiagramPanZoom {
126121

127122
private resetTransform() {
128123
this.scale = 1
129-
this.currentPan = { x: 0, y: 0 }
124+
const svg = this.content.querySelector("svg")!
125+
this.currentPan = {
126+
x: svg.getBoundingClientRect().width / 2,
127+
y: svg.getBoundingClientRect().height / 2,
128+
}
130129
this.updateTransform()
131130
}
132131
}
@@ -149,38 +148,59 @@ document.addEventListener("nav", async () => {
149148
const nodes = center.querySelectorAll("code.mermaid") as NodeListOf<HTMLElement>
150149
if (nodes.length === 0) return
151150

152-
const computedStyleMap = cssVars.reduce(
153-
(acc, key) => {
154-
acc[key] = getComputedStyle(document.documentElement).getPropertyValue(key)
155-
return acc
156-
},
157-
{} as Record<(typeof cssVars)[number], string>,
158-
)
159-
160151
mermaidImport ||= await import(
161152
// @ts-ignore
162153
"https://cdnjs.cloudflare.com/ajax/libs/mermaid/11.4.0/mermaid.esm.min.mjs"
163154
)
164155
const mermaid = mermaidImport.default
165156

166-
const darkMode = document.documentElement.getAttribute("saved-theme") === "dark"
167-
mermaid.initialize({
168-
startOnLoad: false,
169-
securityLevel: "loose",
170-
theme: darkMode ? "dark" : "base",
171-
themeVariables: {
172-
fontFamily: computedStyleMap["--codeFont"],
173-
primaryColor: computedStyleMap["--light"],
174-
primaryTextColor: computedStyleMap["--darkgray"],
175-
primaryBorderColor: computedStyleMap["--tertiary"],
176-
lineColor: computedStyleMap["--darkgray"],
177-
secondaryColor: computedStyleMap["--secondary"],
178-
tertiaryColor: computedStyleMap["--tertiary"],
179-
clusterBkg: computedStyleMap["--light"],
180-
edgeLabelBackground: computedStyleMap["--highlight"],
181-
},
182-
})
183-
await mermaid.run({ nodes })
157+
const textMapping: WeakMap<HTMLElement, string> = new WeakMap()
158+
for (const node of nodes) {
159+
textMapping.set(node, node.innerText)
160+
}
161+
162+
async function renderMermaid() {
163+
// de-init any other diagrams
164+
for (const node of nodes) {
165+
node.removeAttribute("data-processed")
166+
const oldText = textMapping.get(node)
167+
if (oldText) {
168+
node.innerHTML = oldText
169+
}
170+
}
171+
172+
const computedStyleMap = cssVars.reduce(
173+
(acc, key) => {
174+
acc[key] = window.getComputedStyle(document.documentElement).getPropertyValue(key)
175+
return acc
176+
},
177+
{} as Record<(typeof cssVars)[number], string>,
178+
)
179+
180+
const darkMode = document.documentElement.getAttribute("saved-theme") === "dark"
181+
mermaid.initialize({
182+
startOnLoad: false,
183+
securityLevel: "loose",
184+
theme: darkMode ? "dark" : "base",
185+
themeVariables: {
186+
fontFamily: computedStyleMap["--codeFont"],
187+
primaryColor: computedStyleMap["--light"],
188+
primaryTextColor: computedStyleMap["--darkgray"],
189+
primaryBorderColor: computedStyleMap["--tertiary"],
190+
lineColor: computedStyleMap["--darkgray"],
191+
secondaryColor: computedStyleMap["--secondary"],
192+
tertiaryColor: computedStyleMap["--tertiary"],
193+
clusterBkg: computedStyleMap["--light"],
194+
edgeLabelBackground: computedStyleMap["--highlight"],
195+
},
196+
})
197+
198+
await mermaid.run({ nodes })
199+
}
200+
201+
await renderMermaid()
202+
document.addEventListener("themechange", renderMermaid)
203+
window.addCleanup(() => document.removeEventListener("themechange", renderMermaid))
184204

185205
for (let i = 0; i < nodes.length; i++) {
186206
const codeBlock = nodes[i] as HTMLElement
@@ -203,7 +223,6 @@ document.addEventListener("nav", async () => {
203223
if (!popupContainer) return
204224

205225
let panZoom: DiagramPanZoom | null = null
206-
207226
function showMermaid() {
208227
const container = popupContainer.querySelector("#mermaid-space") as HTMLElement
209228
const content = popupContainer.querySelector(".mermaid-content") as HTMLElement
@@ -224,24 +243,15 @@ document.addEventListener("nav", async () => {
224243

225244
function hideMermaid() {
226245
popupContainer.classList.remove("active")
246+
panZoom?.cleanup()
227247
panZoom = null
228248
}
229249

230-
function handleEscape(e: any) {
231-
if (e.key === "Escape") {
232-
hideMermaid()
233-
}
234-
}
235-
236-
const closeBtn = popupContainer.querySelector(".close-button") as HTMLButtonElement
237-
238-
closeBtn.addEventListener("click", hideMermaid)
239250
expandBtn.addEventListener("click", showMermaid)
240251
registerEscapeHandler(popupContainer, hideMermaid)
241-
document.addEventListener("keydown", handleEscape)
242252

243253
window.addCleanup(() => {
244-
closeBtn.removeEventListener("click", hideMermaid)
254+
panZoom?.cleanup()
245255
expandBtn.removeEventListener("click", showMermaid)
246256
})
247257
}

quartz/components/styles/mermaid.inline.scss

+9-39
Original file line numberDiff line numberDiff line change
@@ -53,46 +53,16 @@ pre {
5353
}
5454

5555
& > #mermaid-space {
56-
display: grid;
57-
width: 90%;
58-
height: 90vh;
59-
margin: 5vh auto;
60-
background: var(--light);
61-
box-shadow:
62-
0 14px 50px rgba(27, 33, 48, 0.12),
63-
0 10px 30px rgba(27, 33, 48, 0.16);
56+
border: 1px solid var(--lightgray);
57+
background-color: var(--light);
58+
border-radius: 5px;
59+
position: fixed;
60+
top: 50%;
61+
left: 50%;
62+
transform: translate(-50%, -50%);
63+
height: 80vh;
64+
width: 80vw;
6465
overflow: hidden;
65-
position: relative;
66-
67-
& > .mermaid-header {
68-
display: flex;
69-
justify-content: flex-end;
70-
padding: 1rem;
71-
border-bottom: 1px solid var(--lightgray);
72-
background: var(--light);
73-
z-index: 2;
74-
max-height: fit-content;
75-
76-
& > .close-button {
77-
display: flex;
78-
align-items: center;
79-
justify-content: center;
80-
width: 32px;
81-
height: 32px;
82-
padding: 0;
83-
background: transparent;
84-
border: none;
85-
border-radius: 4px;
86-
color: var(--darkgray);
87-
cursor: pointer;
88-
transition: all 0.2s ease;
89-
90-
&:hover {
91-
background: var(--lightgray);
92-
color: var(--dark);
93-
}
94-
}
95-
}
9666

9767
& > .mermaid-content {
9868
padding: 2rem;

quartz/plugins/transformers/ofm.ts

+1-59
Original file line numberDiff line numberDiff line change
@@ -675,7 +675,6 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>>
675675
properties: {
676676
className: ["expand-button"],
677677
"aria-label": "Expand mermaid diagram",
678-
"aria-hidden": "true",
679678
"data-view-component": true,
680679
},
681680
children: [
@@ -706,70 +705,13 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>>
706705
{
707706
type: "element",
708707
tagName: "div",
709-
properties: { id: "mermaid-container" },
708+
properties: { id: "mermaid-container", role: "dialog" },
710709
children: [
711710
{
712711
type: "element",
713712
tagName: "div",
714713
properties: { id: "mermaid-space" },
715714
children: [
716-
{
717-
type: "element",
718-
tagName: "div",
719-
properties: { className: ["mermaid-header"] },
720-
children: [
721-
{
722-
type: "element",
723-
tagName: "button",
724-
properties: {
725-
className: ["close-button"],
726-
"aria-label": "close button",
727-
},
728-
children: [
729-
{
730-
type: "element",
731-
tagName: "svg",
732-
properties: {
733-
"aria-hidden": "true",
734-
xmlns: "http://www.w3.org/2000/svg",
735-
width: 24,
736-
height: 24,
737-
viewBox: "0 0 24 24",
738-
fill: "none",
739-
stroke: "currentColor",
740-
"stroke-width": "2",
741-
"stroke-linecap": "round",
742-
"stroke-linejoin": "round",
743-
},
744-
children: [
745-
{
746-
type: "element",
747-
tagName: "line",
748-
properties: {
749-
x1: 18,
750-
y1: 6,
751-
x2: 6,
752-
y2: 18,
753-
},
754-
children: [],
755-
},
756-
{
757-
type: "element",
758-
tagName: "line",
759-
properties: {
760-
x1: 6,
761-
y1: 6,
762-
x2: 18,
763-
y2: 18,
764-
},
765-
children: [],
766-
},
767-
],
768-
},
769-
],
770-
},
771-
],
772-
},
773715
{
774716
type: "element",
775717
tagName: "div",

0 commit comments

Comments
 (0)