mirror of
https://github.com/TriliumNext/Notes.git
synced 2025-08-10 02:02:29 +08:00
fix(mermaid): bring back pan/zoom
This commit is contained in:
parent
e0a8b64b4d
commit
cbc6efdad2
@ -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 }
|
||||
};
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user