diff --git a/quartz/components/scripts/mermaid.inline.ts b/quartz/components/scripts/mermaid.inline.ts index 36d384c..19ef24d 100644 --- a/quartz/components/scripts/mermaid.inline.ts +++ b/quartz/components/scripts/mermaid.inline.ts @@ -12,7 +12,8 @@ class DiagramPanZoom { private scale = 1 private readonly MIN_SCALE = 0.5 private readonly MAX_SCALE = 3 - private readonly ZOOM_SENSITIVITY = 0.001 + + cleanups: (() => void)[] = [] constructor( private container: HTMLElement, @@ -20,19 +21,33 @@ class DiagramPanZoom { ) { this.setupEventListeners() this.setupNavigationControls() + this.resetTransform() } private setupEventListeners() { // Mouse drag events - this.container.addEventListener("mousedown", this.onMouseDown.bind(this)) - document.addEventListener("mousemove", this.onMouseMove.bind(this)) - document.addEventListener("mouseup", this.onMouseUp.bind(this)) + const mouseDownHandler = this.onMouseDown.bind(this) + const mouseMoveHandler = this.onMouseMove.bind(this) + const mouseUpHandler = this.onMouseUp.bind(this) + const resizeHandler = this.resetTransform.bind(this) - // Wheel zoom events - this.container.addEventListener("wheel", this.onWheel.bind(this), { passive: false }) + this.container.addEventListener("mousedown", mouseDownHandler) + document.addEventListener("mousemove", mouseMoveHandler) + document.addEventListener("mouseup", mouseUpHandler) + window.addEventListener("resize", resizeHandler) - // Reset on window resize - window.addEventListener("resize", this.resetTransform.bind(this)) + this.cleanups.push( + () => 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() { @@ -84,26 +99,6 @@ class DiagramPanZoom { 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) { const newScale = Math.min(Math.max(this.scale + delta, this.MIN_SCALE), this.MAX_SCALE) @@ -126,7 +121,11 @@ class DiagramPanZoom { private resetTransform() { 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() } } @@ -149,38 +148,59 @@ document.addEventListener("nav", async () => { const nodes = center.querySelectorAll("code.mermaid") as NodeListOf 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( // @ts-ignore "https://cdnjs.cloudflare.com/ajax/libs/mermaid/11.4.0/mermaid.esm.min.mjs" ) const mermaid = mermaidImport.default - 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 }) + const textMapping: WeakMap = new WeakMap() + for (const node of nodes) { + textMapping.set(node, node.innerText) + } + + async function renderMermaid() { + // de-init any other diagrams + for (const node of nodes) { + node.removeAttribute("data-processed") + const oldText = textMapping.get(node) + if (oldText) { + node.innerHTML = oldText + } + } + + const computedStyleMap = cssVars.reduce( + (acc, key) => { + 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++) { const codeBlock = nodes[i] as HTMLElement @@ -203,7 +223,6 @@ document.addEventListener("nav", async () => { if (!popupContainer) return let panZoom: DiagramPanZoom | null = null - function showMermaid() { const container = popupContainer.querySelector("#mermaid-space") as HTMLElement const content = popupContainer.querySelector(".mermaid-content") as HTMLElement @@ -224,24 +243,15 @@ document.addEventListener("nav", async () => { function hideMermaid() { popupContainer.classList.remove("active") + panZoom?.cleanup() 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) registerEscapeHandler(popupContainer, hideMermaid) - document.addEventListener("keydown", handleEscape) window.addCleanup(() => { - closeBtn.removeEventListener("click", hideMermaid) + panZoom?.cleanup() expandBtn.removeEventListener("click", showMermaid) }) } diff --git a/quartz/components/styles/mermaid.inline.scss b/quartz/components/styles/mermaid.inline.scss index 79a1c84..f25448d 100644 --- a/quartz/components/styles/mermaid.inline.scss +++ b/quartz/components/styles/mermaid.inline.scss @@ -53,46 +53,16 @@ pre { } & > #mermaid-space { - display: grid; - width: 90%; - height: 90vh; - margin: 5vh auto; - background: var(--light); - box-shadow: - 0 14px 50px rgba(27, 33, 48, 0.12), - 0 10px 30px rgba(27, 33, 48, 0.16); + border: 1px solid var(--lightgray); + background-color: var(--light); + border-radius: 5px; + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + height: 80vh; + width: 80vw; 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 { padding: 2rem; diff --git a/quartz/plugins/transformers/ofm.ts b/quartz/plugins/transformers/ofm.ts index 12baf2f..1f4873d 100644 --- a/quartz/plugins/transformers/ofm.ts +++ b/quartz/plugins/transformers/ofm.ts @@ -675,7 +675,6 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin> properties: { className: ["expand-button"], "aria-label": "Expand mermaid diagram", - "aria-hidden": "true", "data-view-component": true, }, children: [ @@ -706,70 +705,13 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin> { type: "element", tagName: "div", - properties: { id: "mermaid-container" }, + properties: { id: "mermaid-container", role: "dialog" }, children: [ { type: "element", tagName: "div", properties: { id: "mermaid-space" }, 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", tagName: "div",