mirror of
https://github.com/TriliumNext/Notes.git
synced 2025-08-11 02:42:27 +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 { t } from "../services/i18n.js";
|
||||||
import libraryLoader from "../services/library_loader.js";
|
// import libraryLoader from "../services/library_loader.js";
|
||||||
import NoteContextAwareWidget from "./note_context_aware_widget.js";
|
// import NoteContextAwareWidget from "./note_context_aware_widget.js";
|
||||||
import server from "../services/server.js";
|
// import server from "../services/server.js";
|
||||||
import utils from "../services/utils.js";
|
// import utils from "../services/utils.js";
|
||||||
import { loadElkIfNeeded, postprocessMermaidSvg } from "../services/mermaid.js";
|
// import { loadElkIfNeeded, postprocessMermaidSvg } from "../services/mermaid.js";
|
||||||
import type FNote from "../entities/fnote.js";
|
// import type FNote from "../entities/fnote.js";
|
||||||
import type { EventData } from "../components/app_context.js";
|
// import type { EventData } from "../components/app_context.js";
|
||||||
|
|
||||||
const TPL = `<div class="mermaid-widget">
|
// const TPL = `<div class="mermaid-widget">
|
||||||
<style>
|
// <style>
|
||||||
.mermaid-widget {
|
// .mermaid-widget {
|
||||||
overflow: auto;
|
// overflow: auto;
|
||||||
}
|
// }
|
||||||
|
|
||||||
body.mobile .mermaid-widget {
|
// body.mobile .mermaid-widget {
|
||||||
min-height: 200px;
|
// min-height: 200px;
|
||||||
flex-grow: 2;
|
// flex-grow: 2;
|
||||||
flex-basis: 0;
|
// flex-basis: 0;
|
||||||
border-bottom: 1px solid var(--main-border-color);
|
// border-bottom: 1px solid var(--main-border-color);
|
||||||
margin-bottom: 10px;
|
// margin-bottom: 10px;
|
||||||
}
|
// }
|
||||||
|
|
||||||
body.desktop .mermaid-widget + .gutter {
|
// body.desktop .mermaid-widget + .gutter {
|
||||||
border-bottom: 1px solid var(--main-border-color);
|
// border-bottom: 1px solid var(--main-border-color);
|
||||||
}
|
// }
|
||||||
|
|
||||||
.mermaid-render {
|
// .mermaid-render {
|
||||||
overflow: auto;
|
// overflow: auto;
|
||||||
height: 100%;
|
// height: 100%;
|
||||||
text-align: center;
|
// text-align: center;
|
||||||
}
|
// }
|
||||||
|
|
||||||
.mermaid-render svg {
|
// .mermaid-render svg {
|
||||||
max-width: 100% !important;
|
// max-width: 100% !important;
|
||||||
width: 100%;
|
// width: 100%;
|
||||||
}
|
// }
|
||||||
</style>
|
// </style>
|
||||||
|
|
||||||
<div class="mermaid-error alert alert-warning">
|
// <div class="mermaid-error alert alert-warning">
|
||||||
<p><strong>${t("mermaid.diagram_error")}</strong></p>
|
// <p><strong>${t("mermaid.diagram_error")}</strong></p>
|
||||||
<p class="error-content"></p>
|
// <p class="error-content"></p>
|
||||||
</div>
|
// </div>
|
||||||
|
|
||||||
<div class="mermaid-render"></div>
|
// <div class="mermaid-render"></div>
|
||||||
</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>;
|
// isEnabled() {
|
||||||
private $errorContainer!: JQuery<HTMLElement>;
|
// return super.isEnabled() && this.note?.type === "mermaid" && this.note.isContentAvailable() && this.noteContext?.viewScope?.viewMode === "default";
|
||||||
private $errorMessage!: JQuery<HTMLElement>;
|
// }
|
||||||
private dirtyAttachment?: boolean;
|
|
||||||
private zoomHandler?: () => void;
|
|
||||||
private zoomInstance?: SvgPanZoom.Instance;
|
|
||||||
private lastNote?: FNote;
|
|
||||||
|
|
||||||
isEnabled() {
|
// doRender() {
|
||||||
return super.isEnabled() && this.note?.type === "mermaid" && this.note.isContentAvailable() && this.noteContext?.viewScope?.viewMode === "default";
|
// 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() {
|
// async refreshWithNote(note: FNote) {
|
||||||
this.$widget = $(TPL);
|
// const isSameNote = (this.lastNote === note);
|
||||||
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) {
|
// this.cleanup();
|
||||||
const isSameNote = (this.lastNote === note);
|
// this.$errorContainer.hide();
|
||||||
|
|
||||||
this.cleanup();
|
// if (!isSameNote) {
|
||||||
this.$errorContainer.hide();
|
// this.$display.empty();
|
||||||
|
// }
|
||||||
|
|
||||||
await libraryLoader.requireLibrary(libraryLoader.MERMAID);
|
// this.$errorContainer.hide();
|
||||||
|
|
||||||
mermaid.mermaidAPI.initialize({
|
// try {
|
||||||
startOnLoad: false,
|
// const svg = await this.renderSvg();
|
||||||
...(getMermaidConfig() as any)
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!isSameNote) {
|
// if (this.dirtyAttachment) {
|
||||||
this.$display.empty();
|
// 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 {
|
// this.$display.html(svg);
|
||||||
const svg = await this.renderSvg();
|
// this.$display.attr("id", `mermaid-render-${idCounter}`);
|
||||||
|
|
||||||
if (this.dirtyAttachment) {
|
// // Enable pan to zoom.
|
||||||
const payload = {
|
// this.#setupPanZoom($svg[0], isSameNote);
|
||||||
role: "image",
|
// } catch (e: any) {
|
||||||
title: "mermaid-export.svg",
|
// console.warn(e);
|
||||||
mime: "image/svg+xml",
|
// this.#cleanUpZoom();
|
||||||
content: svg,
|
// this.$display.empty();
|
||||||
position: 0
|
// this.$errorMessage.text(e.message);
|
||||||
};
|
// this.$errorContainer.show();
|
||||||
|
// }
|
||||||
|
|
||||||
server.post(`notes/${this.noteId}/attachments?matchBy=title`, payload).then(() => {
|
// this.lastNote = note;
|
||||||
this.dirtyAttachment = false;
|
// }
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
this.$display.html(svg);
|
// cleanup() {
|
||||||
this.$display.attr("id", `mermaid-render-${idCounter}`);
|
// 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() {
|
// if (!show) {
|
||||||
super.cleanup();
|
// this.cleanup();
|
||||||
if (this.zoomHandler) {
|
// }
|
||||||
$(window).off("resize", this.zoomHandler);
|
// }
|
||||||
this.zoomHandler = undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#cleanUpZoom() {
|
// async renderSvg() {
|
||||||
if (this.zoomInstance) {
|
|
||||||
this.zoomInstance.destroy();
|
|
||||||
this.zoomInstance = undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleInt(show: boolean | null | undefined): void {
|
|
||||||
super.toggleInt(show);
|
|
||||||
|
|
||||||
if (!show) {
|
// if (!this.note) {
|
||||||
this.cleanup();
|
// return "";
|
||||||
}
|
// }
|
||||||
}
|
|
||||||
|
|
||||||
async renderSvg() {
|
// await loadElkIfNeeded(content);
|
||||||
idCounter++;
|
|
||||||
|
|
||||||
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) {
|
// async entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
|
||||||
// Clean up
|
// if (this.noteId && loadResults.isNoteContentReloaded(this.noteId)) {
|
||||||
let pan = null;
|
// this.dirtyAttachment = true;
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
const svgPanZoom = (await import("svg-pan-zoom")).default;
|
// await this.refresh();
|
||||||
const zoomInstance = svgPanZoom(svgEl, {
|
// }
|
||||||
zoomEnabled: true,
|
// }
|
||||||
controlIconsEnabled: true
|
|
||||||
});
|
|
||||||
|
|
||||||
if (pan && zoom) {
|
// async exportSvgEvent({ ntxId }: EventData<"exportSvg">) {
|
||||||
// Restore the pan and zoom.
|
// if (!this.isNoteContext(ntxId) || this.note?.type !== "mermaid") {
|
||||||
zoomInstance.zoom(zoom);
|
// return;
|
||||||
zoomInstance.pan(pan);
|
// }
|
||||||
} else {
|
|
||||||
// New instance, reposition properly.
|
|
||||||
zoomInstance.center();
|
|
||||||
zoomInstance.fit();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.zoomHandler = () => {
|
// const svg = await this.renderSvg();
|
||||||
zoomInstance.resize();
|
// utils.downloadSvg(this.note.title, svg);
|
||||||
zoomInstance.fit();
|
// }
|
||||||
zoomInstance.center();
|
// }
|
||||||
};
|
|
||||||
this.zoomInstance = zoomInstance;
|
|
||||||
$(window).on("resize", this.zoomHandler);
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
export default abstract class AbstractSvgSplitTypeWidget extends AbstractSplitTypeWidget {
|
||||||
|
|
||||||
private $renderContainer!: JQuery<HTMLElement>;
|
private $renderContainer!: JQuery<HTMLElement>;
|
||||||
|
private zoomHandler?: () => void;
|
||||||
|
private zoomInstance?: SvgPanZoom.Instance;
|
||||||
|
|
||||||
doRender(): void {
|
doRender(): void {
|
||||||
super.doRender();
|
super.doRender();
|
||||||
this.$renderContainer = $(`<div class="render-container"></div>`);
|
this.$renderContainer = $(`<div>`)
|
||||||
|
.addClass("render-container")
|
||||||
|
.css("height", "100%");
|
||||||
this.$preview.append(this.$renderContainer);
|
this.$preview.append(this.$renderContainer);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -42,9 +46,15 @@ export default abstract class AbstractSvgSplitTypeWidget extends AbstractSplitTy
|
|||||||
if (this.note) {
|
if (this.note) {
|
||||||
const svg = await this.renderSvg(content);
|
const svg = await this.renderSvg(content);
|
||||||
this.$renderContainer.html(svg);
|
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.
|
* 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>;
|
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