fix(mermaid): themechange detector + expand simplification

This commit is contained in:
Jacky Zhao 2025-03-11 11:45:45 -07:00
parent e59181c3aa
commit 87b803790c
3 changed files with 87 additions and 165 deletions

View file

@ -12,7 +12,8 @@ class DiagramPanZoom {
private scale = 1 private scale = 1
private readonly MIN_SCALE = 0.5 private readonly MIN_SCALE = 0.5
private readonly MAX_SCALE = 3 private readonly MAX_SCALE = 3
private readonly ZOOM_SENSITIVITY = 0.001
cleanups: (() => void)[] = []
constructor( constructor(
private container: HTMLElement, private container: HTMLElement,
@ -20,19 +21,33 @@ class DiagramPanZoom {
) { ) {
this.setupEventListeners() this.setupEventListeners()
this.setupNavigationControls() this.setupNavigationControls()
this.resetTransform()
} }
private setupEventListeners() { private setupEventListeners() {
// Mouse drag events // Mouse drag events
this.container.addEventListener("mousedown", this.onMouseDown.bind(this)) const mouseDownHandler = this.onMouseDown.bind(this)
document.addEventListener("mousemove", this.onMouseMove.bind(this)) const mouseMoveHandler = this.onMouseMove.bind(this)
document.addEventListener("mouseup", this.onMouseUp.bind(this)) const mouseUpHandler = this.onMouseUp.bind(this)
const resizeHandler = this.resetTransform.bind(this)
// Wheel zoom events this.container.addEventListener("mousedown", mouseDownHandler)
this.container.addEventListener("wheel", this.onWheel.bind(this), { passive: false }) document.addEventListener("mousemove", mouseMoveHandler)
document.addEventListener("mouseup", mouseUpHandler)
window.addEventListener("resize", resizeHandler)
// Reset on window resize this.cleanups.push(
window.addEventListener("resize", this.resetTransform.bind(this)) () => this.container.removeEventListener("mousedown", mouseDownHandler),
() => document.removeEventListener("mousemove", mouseMoveHandler),
() => document.removeEventListener("mouseup", mouseUpHandler),
() => window.removeEventListener("resize", resizeHandler),
)
}
cleanup() {
for (const cleanup of this.cleanups) {
cleanup()
}
} }
private setupNavigationControls() { private setupNavigationControls() {
@ -84,26 +99,6 @@ class DiagramPanZoom {
this.container.style.cursor = "grab" this.container.style.cursor = "grab"
} }
private onWheel(e: WheelEvent) {
e.preventDefault()
const delta = -e.deltaY * this.ZOOM_SENSITIVITY
const newScale = Math.min(Math.max(this.scale + delta, this.MIN_SCALE), this.MAX_SCALE)
// Calculate mouse position relative to content
const rect = this.content.getBoundingClientRect()
const mouseX = e.clientX - rect.left
const mouseY = e.clientY - rect.top
// Adjust pan to zoom around mouse position
const scaleDiff = newScale - this.scale
this.currentPan.x -= mouseX * scaleDiff
this.currentPan.y -= mouseY * scaleDiff
this.scale = newScale
this.updateTransform()
}
private zoom(delta: number) { private zoom(delta: number) {
const newScale = Math.min(Math.max(this.scale + delta, this.MIN_SCALE), this.MAX_SCALE) const newScale = Math.min(Math.max(this.scale + delta, this.MIN_SCALE), this.MAX_SCALE)
@ -126,7 +121,11 @@ class DiagramPanZoom {
private resetTransform() { private resetTransform() {
this.scale = 1 this.scale = 1
this.currentPan = { x: 0, y: 0 } const svg = this.content.querySelector("svg")!
this.currentPan = {
x: svg.getBoundingClientRect().width / 2,
y: svg.getBoundingClientRect().height / 2,
}
this.updateTransform() this.updateTransform()
} }
} }
@ -149,38 +148,59 @@ document.addEventListener("nav", async () => {
const nodes = center.querySelectorAll("code.mermaid") as NodeListOf<HTMLElement> const nodes = center.querySelectorAll("code.mermaid") as NodeListOf<HTMLElement>
if (nodes.length === 0) return if (nodes.length === 0) return
const computedStyleMap = cssVars.reduce(
(acc, key) => {
acc[key] = getComputedStyle(document.documentElement).getPropertyValue(key)
return acc
},
{} as Record<(typeof cssVars)[number], string>,
)
mermaidImport ||= await import( mermaidImport ||= await import(
// @ts-ignore // @ts-ignore
"https://cdnjs.cloudflare.com/ajax/libs/mermaid/11.4.0/mermaid.esm.min.mjs" "https://cdnjs.cloudflare.com/ajax/libs/mermaid/11.4.0/mermaid.esm.min.mjs"
) )
const mermaid = mermaidImport.default const mermaid = mermaidImport.default
const darkMode = document.documentElement.getAttribute("saved-theme") === "dark" const textMapping: WeakMap<HTMLElement, string> = new WeakMap()
mermaid.initialize({ for (const node of nodes) {
startOnLoad: false, textMapping.set(node, node.innerText)
securityLevel: "loose", }
theme: darkMode ? "dark" : "base",
themeVariables: { async function renderMermaid() {
fontFamily: computedStyleMap["--codeFont"], // de-init any other diagrams
primaryColor: computedStyleMap["--light"], for (const node of nodes) {
primaryTextColor: computedStyleMap["--darkgray"], node.removeAttribute("data-processed")
primaryBorderColor: computedStyleMap["--tertiary"], const oldText = textMapping.get(node)
lineColor: computedStyleMap["--darkgray"], if (oldText) {
secondaryColor: computedStyleMap["--secondary"], node.innerHTML = oldText
tertiaryColor: computedStyleMap["--tertiary"], }
clusterBkg: computedStyleMap["--light"], }
edgeLabelBackground: computedStyleMap["--highlight"],
}, const computedStyleMap = cssVars.reduce(
}) (acc, key) => {
await mermaid.run({ nodes }) acc[key] = window.getComputedStyle(document.documentElement).getPropertyValue(key)
return acc
},
{} as Record<(typeof cssVars)[number], string>,
)
const darkMode = document.documentElement.getAttribute("saved-theme") === "dark"
mermaid.initialize({
startOnLoad: false,
securityLevel: "loose",
theme: darkMode ? "dark" : "base",
themeVariables: {
fontFamily: computedStyleMap["--codeFont"],
primaryColor: computedStyleMap["--light"],
primaryTextColor: computedStyleMap["--darkgray"],
primaryBorderColor: computedStyleMap["--tertiary"],
lineColor: computedStyleMap["--darkgray"],
secondaryColor: computedStyleMap["--secondary"],
tertiaryColor: computedStyleMap["--tertiary"],
clusterBkg: computedStyleMap["--light"],
edgeLabelBackground: computedStyleMap["--highlight"],
},
})
await mermaid.run({ nodes })
}
await renderMermaid()
document.addEventListener("themechange", renderMermaid)
window.addCleanup(() => document.removeEventListener("themechange", renderMermaid))
for (let i = 0; i < nodes.length; i++) { for (let i = 0; i < nodes.length; i++) {
const codeBlock = nodes[i] as HTMLElement const codeBlock = nodes[i] as HTMLElement
@ -203,7 +223,6 @@ document.addEventListener("nav", async () => {
if (!popupContainer) return if (!popupContainer) return
let panZoom: DiagramPanZoom | null = null let panZoom: DiagramPanZoom | null = null
function showMermaid() { function showMermaid() {
const container = popupContainer.querySelector("#mermaid-space") as HTMLElement const container = popupContainer.querySelector("#mermaid-space") as HTMLElement
const content = popupContainer.querySelector(".mermaid-content") as HTMLElement const content = popupContainer.querySelector(".mermaid-content") as HTMLElement
@ -224,24 +243,15 @@ document.addEventListener("nav", async () => {
function hideMermaid() { function hideMermaid() {
popupContainer.classList.remove("active") popupContainer.classList.remove("active")
panZoom?.cleanup()
panZoom = null panZoom = null
} }
function handleEscape(e: any) {
if (e.key === "Escape") {
hideMermaid()
}
}
const closeBtn = popupContainer.querySelector(".close-button") as HTMLButtonElement
closeBtn.addEventListener("click", hideMermaid)
expandBtn.addEventListener("click", showMermaid) expandBtn.addEventListener("click", showMermaid)
registerEscapeHandler(popupContainer, hideMermaid) registerEscapeHandler(popupContainer, hideMermaid)
document.addEventListener("keydown", handleEscape)
window.addCleanup(() => { window.addCleanup(() => {
closeBtn.removeEventListener("click", hideMermaid) panZoom?.cleanup()
expandBtn.removeEventListener("click", showMermaid) expandBtn.removeEventListener("click", showMermaid)
}) })
} }

View file

@ -53,46 +53,16 @@ pre {
} }
& > #mermaid-space { & > #mermaid-space {
display: grid; border: 1px solid var(--lightgray);
width: 90%; background-color: var(--light);
height: 90vh; border-radius: 5px;
margin: 5vh auto; position: fixed;
background: var(--light); top: 50%;
box-shadow: left: 50%;
0 14px 50px rgba(27, 33, 48, 0.12), transform: translate(-50%, -50%);
0 10px 30px rgba(27, 33, 48, 0.16); height: 80vh;
width: 80vw;
overflow: hidden; overflow: hidden;
position: relative;
& > .mermaid-header {
display: flex;
justify-content: flex-end;
padding: 1rem;
border-bottom: 1px solid var(--lightgray);
background: var(--light);
z-index: 2;
max-height: fit-content;
& > .close-button {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
padding: 0;
background: transparent;
border: none;
border-radius: 4px;
color: var(--darkgray);
cursor: pointer;
transition: all 0.2s ease;
&:hover {
background: var(--lightgray);
color: var(--dark);
}
}
}
& > .mermaid-content { & > .mermaid-content {
padding: 2rem; padding: 2rem;

View file

@ -675,7 +675,6 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>>
properties: { properties: {
className: ["expand-button"], className: ["expand-button"],
"aria-label": "Expand mermaid diagram", "aria-label": "Expand mermaid diagram",
"aria-hidden": "true",
"data-view-component": true, "data-view-component": true,
}, },
children: [ children: [
@ -706,70 +705,13 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>>
{ {
type: "element", type: "element",
tagName: "div", tagName: "div",
properties: { id: "mermaid-container" }, properties: { id: "mermaid-container", role: "dialog" },
children: [ children: [
{ {
type: "element", type: "element",
tagName: "div", tagName: "div",
properties: { id: "mermaid-space" }, properties: { id: "mermaid-space" },
children: [ children: [
{
type: "element",
tagName: "div",
properties: { className: ["mermaid-header"] },
children: [
{
type: "element",
tagName: "button",
properties: {
className: ["close-button"],
"aria-label": "close button",
},
children: [
{
type: "element",
tagName: "svg",
properties: {
"aria-hidden": "true",
xmlns: "http://www.w3.org/2000/svg",
width: 24,
height: 24,
viewBox: "0 0 24 24",
fill: "none",
stroke: "currentColor",
"stroke-width": "2",
"stroke-linecap": "round",
"stroke-linejoin": "round",
},
children: [
{
type: "element",
tagName: "line",
properties: {
x1: 18,
y1: 6,
x2: 6,
y2: 18,
},
children: [],
},
{
type: "element",
tagName: "line",
properties: {
x1: 6,
y1: 6,
x2: 18,
y2: 18,
},
children: [],
},
],
},
],
},
],
},
{ {
type: "element", type: "element",
tagName: "div", tagName: "div",