内置思维导图改为markmap

- 实现编辑内容动态更新,只重新渲染编辑节点
- 导出图片按钮实现导出svg
- 添加重置视图按钮
This commit is contained in:
Tron 2025-11-25 09:37:06 +08:00
parent 702d5fdda3
commit 5abb991e5e
4 changed files with 213 additions and 37 deletions

View file

@ -29,13 +29,15 @@ export const mindmapRender = (element: Element, cdn = Constants.PROTYLE_CDN) =>
return;
}
if (!e.firstElementChild.classList.contains("protyle-icons")) {
e.insertAdjacentHTML("afterbegin", genIconHTML(wysiswgElement));
// Add home icon for mindmap (reset view), edit and more
e.insertAdjacentHTML("afterbegin", genIconHTML(wysiswgElement, ["home", "edit", "more"]));
}
const renderElement = e.firstElementChild.nextElementSibling as HTMLElement;
if (!e.getAttribute("data-content")) {
renderElement.innerHTML = `<span style="position: absolute;left:0;top:0;width: 1px;">${Constants.ZWSP}</span>`;
return;
}
let transformer: any = null;
try {
// create or reuse container for markmap
if (!renderElement.lastElementChild || renderElement.childElementCount === 1) {
@ -67,27 +69,24 @@ export const mindmapRender = (element: Element, cdn = Constants.PROTYLE_CDN) =>
// Try to obtain markmap entry from loaded bundles (single unified reference)
const mm: any = (window as any).markmap || (window as any).Markmap || null;
// Prefer the Transformer API when available (example usage: transform -> getUsedAssets -> load assets -> create)
let data: any = null;
// Prefer the Transformer API when available (transform -> getUsedAssets -> load assets -> create)
let rootData: any = null;
try {
if (mm && typeof mm.Transformer === "function") {
const transformer = new mm.Transformer();
if (mm) {
if (typeof mm.Transformer === "function") {
transformer = new mm.Transformer();
// transform markdown -> { root, features }
const tx = transformer.transform(md) || {};
rootData = tx.root || null;
// select asset getter: prefer getUsedAssets (only assets used), fallback to getAssets
// select asset getter: prefer getUsedAssets then getAssets
const assetsGetter = typeof transformer.getUsedAssets === "function" ? "getUsedAssets" : (typeof transformer.getAssets === "function" ? "getAssets" : null);
if (assetsGetter) {
const assets = (transformer as any)[assetsGetter](tx.features || {});
const styles = assets && assets.styles;
const scripts = assets && assets.scripts;
// Use markmap provided loaders when available
const loadCSS = typeof mm.loadCSS === "function" ? mm.loadCSS : null;
const loadJS = typeof mm.loadJS === "function" ? mm.loadJS : null;
if (styles && loadCSS) {
try { loadCSS(styles); } catch (err) { /* ignore */ }
}
@ -96,16 +95,17 @@ export const mindmapRender = (element: Element, cdn = Constants.PROTYLE_CDN) =>
}
}
} else {
// fallback: try older transform functions
const transformFn = mm && (mm.transform || (window as any).markmap && (window as any).markmap.transform);
// fallback: try older transform functions (may return root-like object)
const transformFn = mm.transform || (window as any).markmap && (window as any).markmap.transform;
if (typeof transformFn === "function") {
data = transformFn(md);
try {
const tx = transformFn(md) || {};
rootData = tx.root || tx || null;
} catch (err) {
rootData = null;
}
}
}
} catch (e) {
// leave data/rootData null and let downstream handle it
data = null;
rootData = null;
}
// container for svg
@ -127,10 +127,15 @@ export const mindmapRender = (element: Element, cdn = Constants.PROTYLE_CDN) =>
const options = {
duration: 0, // 🔥 禁用动画设为0
};
// create and store markmap + transformer on the element so callers can update instead of re-creating
if (MarkmapCtor && typeof MarkmapCtor.create === "function") {
if (rootData) {
// When Transformer was used we have a `root` structure
MarkmapCtor.create(svg, options, rootData);
const markmapInstance = MarkmapCtor.create(svg, options, rootData);
const mmEntry: any = (e as any).__markmap || {};
mmEntry.transformer = transformer || mmEntry.transformer || null;
mmEntry.markmap = markmapInstance;
mmEntry.options = options;
(e as any).__markmap = mmEntry;
}
} else {
throw new Error("Markmap not available");
@ -140,6 +145,26 @@ export const mindmapRender = (element: Element, cdn = Constants.PROTYLE_CDN) =>
renderElement.innerHTML = `<span style="position: absolute;left:0;top:0;width: 1px;">${Constants.ZWSP}</span><div class="ft__error" style="height:${e.style.height || "420px"}" contenteditable="false">Mindmap render error: <br>${error}</div>`;
}
e.setAttribute("data-render", "true");
// expose a small helper to update content (callable by toolbar)
try {
const mmEntry: any = (e as any).__markmap;
if (mmEntry && mmEntry.transformer && mmEntry.markmap) {
(e as any).__markmap.updateContent = (newMarkdown: string) => {
try {
const tx2 = mmEntry.transformer.transform(newMarkdown) || {};
const root2 = tx2.root || null;
if (mmEntry.markmap && typeof mmEntry.markmap.setData === "function") {
mmEntry.markmap.setData(root2, mmEntry.options);
}
} catch (e) {
// ignore update errors
console.error("markmap updateContent error", e);
}
};
}
} catch (e) {
// ignore
}
});
});
};