fix(mermaid): bring back pan/zoom

This commit is contained in:
Elian Doran 2025-03-21 22:53:52 +02:00
parent e0a8b64b4d
commit cbc6efdad2
No known key found for this signature in database
2 changed files with 188 additions and 202 deletions

View File

@ -1,239 +1,161 @@
import { t } from "../services/i18n.js";
import libraryLoader from "../services/library_loader.js";
import NoteContextAwareWidget from "./note_context_aware_widget.js";
import server from "../services/server.js";
import utils from "../services/utils.js";
import { loadElkIfNeeded, postprocessMermaidSvg } from "../services/mermaid.js";
import type FNote from "../entities/fnote.js";
import type { EventData } from "../components/app_context.js";
// import { t } from "../services/i18n.js";
// import libraryLoader from "../services/library_loader.js";
// import NoteContextAwareWidget from "./note_context_aware_widget.js";
// import server from "../services/server.js";
// import utils from "../services/utils.js";
// import { loadElkIfNeeded, postprocessMermaidSvg } from "../services/mermaid.js";
// import type FNote from "../entities/fnote.js";
// import type { EventData } from "../components/app_context.js";
const TPL = `<div class="mermaid-widget">
<style>
.mermaid-widget {
overflow: auto;
}
// const TPL = `<div class="mermaid-widget">
// <style>
// .mermaid-widget {
// overflow: auto;
// }
body.mobile .mermaid-widget {
min-height: 200px;
flex-grow: 2;
flex-basis: 0;
border-bottom: 1px solid var(--main-border-color);
margin-bottom: 10px;
}
// body.mobile .mermaid-widget {
// min-height: 200px;
// flex-grow: 2;
// flex-basis: 0;
// border-bottom: 1px solid var(--main-border-color);
// margin-bottom: 10px;
// }
body.desktop .mermaid-widget + .gutter {
border-bottom: 1px solid var(--main-border-color);
}
// body.desktop .mermaid-widget + .gutter {
// border-bottom: 1px solid var(--main-border-color);
// }
.mermaid-render {
overflow: auto;
height: 100%;
text-align: center;
}
// .mermaid-render {
// overflow: auto;
// height: 100%;
// text-align: center;
// }
.mermaid-render svg {
max-width: 100% !important;
width: 100%;
}
</style>
// .mermaid-render svg {
// max-width: 100% !important;
// width: 100%;
// }
// </style>
<div class="mermaid-error alert alert-warning">
<p><strong>${t("mermaid.diagram_error")}</strong></p>
<p class="error-content"></p>
</div>
// <div class="mermaid-error alert alert-warning">
// <p><strong>${t("mermaid.diagram_error")}</strong></p>
// <p class="error-content"></p>
// </div>
<div class="mermaid-render"></div>
</div>`;
// <div class="mermaid-render"></div>
// </div>`;
let idCounter = 1;
// export default class MermaidWidget extends NoteContextAwareWidget {
export default class MermaidWidget extends NoteContextAwareWidget {
// private $display!: JQuery<HTMLElement>;
// private $errorContainer!: JQuery<HTMLElement>;
// private $errorMessage!: JQuery<HTMLElement>;
// private dirtyAttachment?: boolean;
// private lastNote?: FNote;
private $display!: JQuery<HTMLElement>;
private $errorContainer!: JQuery<HTMLElement>;
private $errorMessage!: JQuery<HTMLElement>;
private dirtyAttachment?: boolean;
private zoomHandler?: () => void;
private zoomInstance?: SvgPanZoom.Instance;
private lastNote?: FNote;
// isEnabled() {
// return super.isEnabled() && this.note?.type === "mermaid" && this.note.isContentAvailable() && this.noteContext?.viewScope?.viewMode === "default";
// }
isEnabled() {
return super.isEnabled() && this.note?.type === "mermaid" && this.note.isContentAvailable() && this.noteContext?.viewScope?.viewMode === "default";
}
// doRender() {
// this.$widget = $(TPL);
// this.contentSized();
// this.$display = this.$widget.find(".mermaid-render");
// this.$errorContainer = this.$widget.find(".mermaid-error");
// this.$errorMessage = this.$errorContainer.find(".error-content");
// }
doRender() {
this.$widget = $(TPL);
this.contentSized();
this.$display = this.$widget.find(".mermaid-render");
this.$errorContainer = this.$widget.find(".mermaid-error");
this.$errorMessage = this.$errorContainer.find(".error-content");
}
// async refreshWithNote(note: FNote) {
// const isSameNote = (this.lastNote === note);
async refreshWithNote(note: FNote) {
const isSameNote = (this.lastNote === note);
// this.cleanup();
// this.$errorContainer.hide();
this.cleanup();
this.$errorContainer.hide();
// if (!isSameNote) {
// this.$display.empty();
// }
await libraryLoader.requireLibrary(libraryLoader.MERMAID);
// this.$errorContainer.hide();
mermaid.mermaidAPI.initialize({
startOnLoad: false,
...(getMermaidConfig() as any)
});
// try {
// const svg = await this.renderSvg();
if (!isSameNote) {
this.$display.empty();
}
// if (this.dirtyAttachment) {
// const payload = {
// role: "image",
// title: "mermaid-export.svg",
// mime: "image/svg+xml",
// content: svg,
// position: 0
// };
this.$errorContainer.hide();
// server.post(`notes/${this.noteId}/attachments?matchBy=title`, payload).then(() => {
// this.dirtyAttachment = false;
// });
// }
try {
const svg = await this.renderSvg();
// this.$display.html(svg);
// this.$display.attr("id", `mermaid-render-${idCounter}`);
if (this.dirtyAttachment) {
const payload = {
role: "image",
title: "mermaid-export.svg",
mime: "image/svg+xml",
content: svg,
position: 0
};
// // Enable pan to zoom.
// this.#setupPanZoom($svg[0], isSameNote);
// } catch (e: any) {
// console.warn(e);
// this.#cleanUpZoom();
// this.$display.empty();
// this.$errorMessage.text(e.message);
// this.$errorContainer.show();
// }
server.post(`notes/${this.noteId}/attachments?matchBy=title`, payload).then(() => {
this.dirtyAttachment = false;
});
}
// this.lastNote = note;
// }
this.$display.html(svg);
this.$display.attr("id", `mermaid-render-${idCounter}`);
// cleanup() {
// super.cleanup();
// if (this.zoomHandler) {
// $(window).off("resize", this.zoomHandler);
// this.zoomHandler = undefined;
// }
// }
// Fit the image to bounds.
const $svg = this.$display.find("svg");
$svg.attr("width", "100%").attr("height", "100%");
// Enable pan to zoom.
this.#setupPanZoom($svg[0], isSameNote);
} catch (e: any) {
console.warn(e);
this.#cleanUpZoom();
this.$display.empty();
this.$errorMessage.text(e.message);
this.$errorContainer.show();
}
this.lastNote = note;
}
// toggleInt(show: boolean | null | undefined): void {
// super.toggleInt(show);
cleanup() {
super.cleanup();
if (this.zoomHandler) {
$(window).off("resize", this.zoomHandler);
this.zoomHandler = undefined;
}
}
// if (!show) {
// this.cleanup();
// }
// }
#cleanUpZoom() {
if (this.zoomInstance) {
this.zoomInstance.destroy();
this.zoomInstance = undefined;
}
}
// async renderSvg() {
toggleInt(show: boolean | null | undefined): void {
super.toggleInt(show);
if (!show) {
this.cleanup();
}
}
// if (!this.note) {
// return "";
// }
async renderSvg() {
idCounter++;
// await loadElkIfNeeded(content);
if (!this.note) {
return "";
}
// }
const blob = await this.note.getBlob();
const content = blob?.content || "";
await loadElkIfNeeded(content);
const { svg } = await mermaid.mermaidAPI.render(`mermaid-graph-${idCounter}`, content);
return postprocessMermaidSvg(svg);
}
async #setupPanZoom(svgEl: SVGElement, isSameNote: boolean) {
// Clean up
let pan = null;
let zoom = null;
if (this.zoomInstance) {
// Store pan and zoom for same note, when the user is editing the note.
if (isSameNote && this.zoomInstance) {
pan = this.zoomInstance.getPan();
zoom = this.zoomInstance.getZoom();
}
this.#cleanUpZoom();
}
// async entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
// if (this.noteId && loadResults.isNoteContentReloaded(this.noteId)) {
// this.dirtyAttachment = true;
const svgPanZoom = (await import("svg-pan-zoom")).default;
const zoomInstance = svgPanZoom(svgEl, {
zoomEnabled: true,
controlIconsEnabled: true
});
// await this.refresh();
// }
// }
if (pan && zoom) {
// Restore the pan and zoom.
zoomInstance.zoom(zoom);
zoomInstance.pan(pan);
} else {
// New instance, reposition properly.
zoomInstance.center();
zoomInstance.fit();
}
// async exportSvgEvent({ ntxId }: EventData<"exportSvg">) {
// if (!this.isNoteContext(ntxId) || this.note?.type !== "mermaid") {
// return;
// }
this.zoomHandler = () => {
zoomInstance.resize();
zoomInstance.fit();
zoomInstance.center();
};
this.zoomInstance = zoomInstance;
$(window).on("resize", this.zoomHandler);
}
// const svg = await this.renderSvg();
// utils.downloadSvg(this.note.title, svg);
// }
// }
async entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
if (this.noteId && loadResults.isNoteContentReloaded(this.noteId)) {
this.dirtyAttachment = true;
await this.refresh();
}
}
async exportSvgEvent({ ntxId }: EventData<"exportSvg">) {
if (!this.isNoteContext(ntxId) || this.note?.type !== "mermaid") {
return;
}
const svg = await this.renderSvg();
utils.downloadSvg(this.note.title, svg);
}
}
export function getMermaidConfig(): MermaidConfig {
const documentStyle = window.getComputedStyle(document.documentElement);
const mermaidTheme = documentStyle.getPropertyValue("--mermaid-theme");
return {
theme: mermaidTheme.trim(),
securityLevel: "antiscript",
// TODO: Are all these options correct?
flow: { useMaxWidth: false },
sequence: { useMaxWidth: false },
gantt: { useMaxWidth: false },
class: { useMaxWidth: false },
state: { useMaxWidth: false },
pie: { useMaxWidth: true },
journey: { useMaxWidth: false },
git: { useMaxWidth: false }
};
}

View File

@ -12,10 +12,14 @@ import AbstractSplitTypeWidget from "./abstract_split_type_widget.js";
export default abstract class AbstractSvgSplitTypeWidget extends AbstractSplitTypeWidget {
private $renderContainer!: JQuery<HTMLElement>;
private zoomHandler?: () => void;
private zoomInstance?: SvgPanZoom.Instance;
doRender(): void {
super.doRender();
this.$renderContainer = $(`<div class="render-container"></div>`);
this.$renderContainer = $(`<div>`)
.addClass("render-container")
.css("height", "100%");
this.$preview.append(this.$renderContainer);
}
@ -42,9 +46,15 @@ export default abstract class AbstractSvgSplitTypeWidget extends AbstractSplitTy
if (this.note) {
const svg = await this.renderSvg(content);
this.$renderContainer.html(svg);
await this.#setupPanZoom();
}
}
cleanup(): void {
this.#cleanUpZoom();
super.cleanup();
}
/**
* Called upon when the SVG preview needs refreshing, such as when the editor has switched to a new note or the content has switched.
*
@ -54,4 +64,58 @@ export default abstract class AbstractSvgSplitTypeWidget extends AbstractSplitTy
*/
abstract renderSvg(content: string): Promise<string>;
async #setupPanZoom() {
// Clean up
let pan = null;
let zoom = null;
if (this.zoomInstance) {
// Store pan and zoom for same note, when the user is editing the note.
pan = this.zoomInstance.getPan();
zoom = this.zoomInstance.getZoom();
this.#cleanUpZoom();
}
const $svgEl = this.$renderContainer.find("svg");
// Fit the image to bounds
$svgEl.attr("width", "100%")
.attr("height", "100%")
.css("max-width", "100%");
if (!$svgEl.length) {
return;
}
const svgPanZoom = (await import("svg-pan-zoom")).default;
const zoomInstance = svgPanZoom($svgEl[0], {
zoomEnabled: true,
controlIconsEnabled: true
});
if (pan && zoom) {
// Restore the pan and zoom.
zoomInstance.zoom(zoom);
zoomInstance.pan(pan);
} else {
// New instance, reposition properly.
zoomInstance.center();
zoomInstance.fit();
}
this.zoomHandler = () => {
zoomInstance.resize();
zoomInstance.fit();
zoomInstance.center();
};
this.zoomInstance = zoomInstance;
$(window).on("resize", this.zoomHandler);
}
#cleanUpZoom() {
if (this.zoomInstance) {
this.zoomInstance.destroy();
this.zoomInstance = undefined;
}
}
}