import { registerEscapeHandler, removeAllChildren } from "./util" import { argdown, IArgdownRequest, ParserPlugin, ModelPlugin, ColorPlugin, HtmlExportPlugin } from "@argdown/node" interface Position { x: number y: number } class DiagramPanZoom { private isDragging = false private startPan: Position = { x: 0, y: 0 } private currentPan: Position = { x: 0, y: 0 } private scale = 1 private readonly MIN_SCALE = 0.5 private readonly MAX_SCALE = 3 cleanups: (() => void)[] = [] constructor( private container: HTMLElement, private content: HTMLElement, ) { this.setupEventListeners() this.setupNavigationControls() this.resetTransform() } private setupEventListeners() { // Mouse drag events const mouseDownHandler = this.onMouseDown.bind(this) const mouseMoveHandler = this.onMouseMove.bind(this) const mouseUpHandler = this.onMouseUp.bind(this) const resizeHandler = this.resetTransform.bind(this) this.container.addEventListener("mousedown", mouseDownHandler) document.addEventListener("mousemove", mouseMoveHandler) document.addEventListener("mouseup", mouseUpHandler) window.addEventListener("resize", resizeHandler) 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() { const controls = document.createElement("div") controls.className = "argdown-controls" // Zoom controls const zoomIn = this.createButton("+", () => this.zoom(0.1)) const zoomOut = this.createButton("-", () => this.zoom(-0.1)) const resetBtn = this.createButton("Reset", () => this.resetTransform()) controls.appendChild(zoomOut) controls.appendChild(resetBtn) controls.appendChild(zoomIn) this.container.appendChild(controls) } private createButton(text: string, onClick: () => void): HTMLButtonElement { const button = document.createElement("button") button.textContent = text button.className = "argdown-control-button" button.addEventListener("click", onClick) window.addCleanup(() => button.removeEventListener("click", onClick)) return button } private onMouseDown(e: MouseEvent) { if (e.button !== 0) return // Only handle left click this.isDragging = true this.startPan = { x: e.clientX - this.currentPan.x, y: e.clientY - this.currentPan.y } this.container.style.cursor = "grabbing" } private onMouseMove(e: MouseEvent) { if (!this.isDragging) return e.preventDefault() this.currentPan = { x: e.clientX - this.startPan.x, y: e.clientY - this.startPan.y, } this.updateTransform() } private onMouseUp() { this.isDragging = false this.container.style.cursor = "grab" } private zoom(delta: number) { const newScale = Math.min(Math.max(this.scale + delta, this.MIN_SCALE), this.MAX_SCALE) // Zoom around center const rect = this.content.getBoundingClientRect() const centerX = rect.width / 2 const centerY = rect.height / 2 const scaleDiff = newScale - this.scale this.currentPan.x -= centerX * scaleDiff this.currentPan.y -= centerY * scaleDiff this.scale = newScale this.updateTransform() } private updateTransform() { this.content.style.transform = `translate(${this.currentPan.x}px, ${this.currentPan.y}px) scale(${this.scale})` } private resetTransform() { this.scale = 1 const svg = this.content.querySelector("svg")! this.currentPan = { x: svg.getBoundingClientRect().width / 2, y: svg.getBoundingClientRect().height / 2, } this.updateTransform() } } // const cssVars = [ // "--secondary", // "--tertiary", // "--gray", // "--light", // "--lightgray", // "--highlight", // "--dark", // "--darkgray", // "--codeFont", // ] as const const argdownParser = new ParserPlugin() argdown.addPlugin(argdownParser, "parse-input") const argdownModeler = new ModelPlugin() argdown.addPlugin(argdownModeler, "model-input") const argdownPainter = new ColorPlugin() argdown.addPlugin(argdownPainter, "paint-input") const argdownHTML = new HtmlExportPlugin() argdown.addPlugin(argdownHTML, "html") // let argdownImport = undefined document.addEventListener("nav", async () => { const center = document.querySelector(".center") as HTMLElement const nodes = center.querySelectorAll("code.argdown") as NodeListOf if (nodes.length === 0) return // argdownImport ||= await import( // // @ts-ignore // "https://cdnjs.cloudflare.com/ajax/libs/mermaid/11.4.0/argdown.esm.min.mjs" // ) // const mermaid = argdownImport.default const textMapping: WeakMap = new WeakMap() for (const node of nodes) { textMapping.set(node, node.innerText) } async function renderArgdown() { // de-init any other diagrams let requests: Array = [] for (const node of nodes) { node.removeAttribute("data-processed") const oldText = textMapping.get(node) if (oldText) { node.innerHTML = oldText requests.push( { node, process: ["parse-input", "model-input", "paint-input", "html"] } ) } } // 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" // argdown.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 argdown.run({ requests }) } await renderArgdown() document.addEventListener("themechange", renderArgdown) window.addCleanup(() => document.removeEventListener("themechange", renderArgdown)) for (let i = 0; i < nodes.length; i++) { const codeBlock = nodes[i] as HTMLElement const pre = codeBlock.parentElement as HTMLPreElement const clipboardBtn = pre.querySelector(".clipboard-button") as HTMLButtonElement const expandBtn = pre.querySelector(".expand-button") as HTMLButtonElement const clipboardStyle = window.getComputedStyle(clipboardBtn) const clipboardWidth = clipboardBtn.offsetWidth + parseFloat(clipboardStyle.marginLeft || "0") + parseFloat(clipboardStyle.marginRight || "0") // Set expand button position expandBtn.style.right = `calc(${clipboardWidth}px + 0.3rem)` pre.prepend(expandBtn) // query popup container const popupContainer = pre.querySelector("#argdown-container") as HTMLElement if (!popupContainer) return let panZoom: DiagramPanZoom | null = null function showArgdown() { const container = popupContainer.querySelector("#argdown-space") as HTMLElement const content = popupContainer.querySelector(".argdown-content") as HTMLElement if (!content) return removeAllChildren(content) // Clone the mermaid content const argdownContent = codeBlock.querySelector("svg")!.cloneNode(true) as SVGElement content.appendChild(argdownContent) // Show container popupContainer.classList.add("active") container.style.cursor = "grab" // Initialize pan-zoom after showing the popup panZoom = new DiagramPanZoom(container, content) } function hideArgdown() { popupContainer.classList.remove("active") panZoom?.cleanup() panZoom = null } expandBtn.addEventListener("click", showArgdown) registerEscapeHandler(popupContainer, hideArgdown) window.addCleanup(() => { panZoom?.cleanup() expandBtn.removeEventListener("click", showArgdown) }) } })