mirror of
https://github.com/siyuan-note/siyuan.git
synced 2026-01-16 05:35:28 +01:00
✨内置思维导图改为markmap
- 实现编辑内容动态更新,只重新渲染编辑节点 - 导出图片按钮实现导出svg - 添加重置视图按钮
This commit is contained in:
parent
702d5fdda3
commit
5abb991e5e
4 changed files with 213 additions and 37 deletions
|
|
@ -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
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -11,18 +11,51 @@ export const genIconHTML = (element?: false | HTMLElement, actions = ["edit", "m
|
|||
return '<div class="protyle-icons"></div>';
|
||||
}
|
||||
}
|
||||
if (actions.length === 3) {
|
||||
return `<div class="protyle-icons">
|
||||
<span aria-label="${window.siyuan.languages.refresh}" class="b3-tooltips__nw b3-tooltips protyle-icon protyle-icon--first protyle-action__reload"><svg><use xlink:href="#iconRefresh"></use></svg></span>
|
||||
<span aria-label="${window.siyuan.languages.edit}" class="b3-tooltips__nw b3-tooltips protyle-icon protyle-action__edit${enable ? "" : " fn__none"}"><svg><use xlink:href="#iconEdit"></use></svg></span>
|
||||
<span aria-label="${window.siyuan.languages.more}" class="b3-tooltips__nw b3-tooltips protyle-icon protyle-action__menu protyle-icon--last"><svg><use xlink:href="#iconMore"></use></svg></span>
|
||||
</div>`;
|
||||
} else {
|
||||
return `<div class="protyle-icons">
|
||||
<span aria-label="${window.siyuan.languages.edit}" class="b3-tooltips__nw b3-tooltips protyle-icon protyle-icon--first protyle-action__edit${enable ? "" : " fn__none"}"><svg><use xlink:href="#iconEdit"></use></svg></span>
|
||||
<span aria-label="${window.siyuan.languages.more}" class="b3-tooltips__nw b3-tooltips protyle-icon protyle-action__menu protyle-icon--last${enable ? "" : " protyle-icon--first"}"><svg><use xlink:href="#iconMore"></use></svg></span>
|
||||
</div>`;
|
||||
const mapActionToHTML = (action: string, isFirst: boolean, isLast: boolean) => {
|
||||
const classList = ["b3-tooltips__nw", "b3-tooltips", "protyle-icon"];
|
||||
if (isFirst) classList.push("protyle-icon--first");
|
||||
if (isLast) classList.push("protyle-icon--last");
|
||||
let aria = "";
|
||||
let className = "";
|
||||
let icon = "";
|
||||
switch (action) {
|
||||
case "reload":
|
||||
case "refresh":
|
||||
aria = window.siyuan.languages.refresh;
|
||||
className = "protyle-action__reload";
|
||||
icon = "iconRefresh";
|
||||
break;
|
||||
case "home":
|
||||
case "fit":
|
||||
aria = window.siyuan.languages.reset;
|
||||
className = "protyle-action__home";
|
||||
icon = "#iconHistory";
|
||||
break;
|
||||
case "edit":
|
||||
aria = window.siyuan.languages.edit;
|
||||
className = "protyle-action__edit";
|
||||
icon = "iconEdit";
|
||||
break;
|
||||
case "more":
|
||||
default:
|
||||
aria = window.siyuan.languages.more;
|
||||
className = "protyle-action__menu";
|
||||
icon = "iconMore";
|
||||
break;
|
||||
}
|
||||
// Only the edit button honors read-only enable
|
||||
const hidden = (action === "edit" && !enable) ? " fn__none" : "";
|
||||
return `<span aria-label="${aria}" class="${classList.join(" ")} ${className}${hidden}"><svg><use xlink:href="#${icon}"></use></svg></span>`;
|
||||
};
|
||||
const res: string[] = [];
|
||||
for (let i = 0; i < actions.length; i++) {
|
||||
const isFirst = i === 0;
|
||||
const isLast = i === actions.length - 1;
|
||||
res.push(mapActionToHTML(actions[i], isFirst, isLast));
|
||||
}
|
||||
return `<div class="protyle-icons">
|
||||
${res.join("\n ")}
|
||||
</div>`;
|
||||
};
|
||||
|
||||
export const genRenderFrame = (renderElement: Element) => {
|
||||
|
|
|
|||
|
|
@ -987,6 +987,17 @@ export class Toolbar {
|
|||
break;
|
||||
case "refresh":
|
||||
btnElement.classList.toggle("block__icon--active");
|
||||
// If this is a mindmap, call markmap instance fit() to reset view
|
||||
if (renderElement.getAttribute("data-subtype") === "mindmap") {
|
||||
const mmEntry: any = (renderElement as any).__markmap || (nodeElement as any).__markmap || null;
|
||||
if (mmEntry && mmEntry.markmap && typeof mmEntry.markmap.fit === "function") {
|
||||
try {
|
||||
mmEntry.markmap.fit();
|
||||
} catch (e) {
|
||||
console.error("markmap fit error", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "before":
|
||||
insertEmptyBlock(protyle, "beforebegin", id);
|
||||
|
|
@ -1017,6 +1028,55 @@ export class Toolbar {
|
|||
});
|
||||
return;
|
||||
}
|
||||
// mindmap: try to export SVG directly using the rendered SVG
|
||||
if (renderElement.getAttribute("data-subtype") === "mindmap") {
|
||||
try {
|
||||
// find rendered svg inside the renderElement
|
||||
const svgElement = renderElement.querySelector("svg.markmap") as SVGSVGElement;
|
||||
if (!svgElement) {
|
||||
throw new Error("SVG not found");
|
||||
}
|
||||
const clonedSvg = svgElement.cloneNode(true) as SVGSVGElement;
|
||||
const bbox = svgElement.getBBox();
|
||||
clonedSvg.setAttribute("viewBox", `${bbox.x-20} ${bbox.y-20} ${bbox.width+40} ${bbox.height+40}`);
|
||||
clonedSvg.setAttribute("width", String(bbox.width+40));
|
||||
clonedSvg.setAttribute("height", String(bbox.height+40));
|
||||
clonedSvg.setAttribute("xmlns", "http://www.w3.org/2000/svg");
|
||||
clonedSvg.setAttribute("xmlns:xlink", "http://www.w3.org/1999/xlink");
|
||||
|
||||
// inline styles
|
||||
const styles = Array.from(document.styleSheets)
|
||||
.filter(sheet => {
|
||||
try {
|
||||
return sheet.cssRules;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
})
|
||||
.reduce((acc, sheet) => {
|
||||
return acc + Array.from((sheet as CSSStyleSheet).cssRules).map(rule => rule.cssText).join("\n");
|
||||
}, "");
|
||||
|
||||
const styleElement = document.createElementNS("http://www.w3.org/2000/svg", "style");
|
||||
styleElement.textContent = styles;
|
||||
clonedSvg.insertBefore(styleElement, clonedSvg.firstChild);
|
||||
|
||||
const serializer = new XMLSerializer();
|
||||
const svgString = serializer.serializeToString(clonedSvg);
|
||||
const blob = new Blob([svgString], { type: "image/svg+xml;charset=utf-8" });
|
||||
const formData = new FormData();
|
||||
formData.append("file", blob);
|
||||
formData.append("type", "image/svg+xml");
|
||||
fetchPost("/api/export/exportAsFile", formData, (response) => {
|
||||
openByMobile(response.data.file);
|
||||
hideMessage(msgId);
|
||||
});
|
||||
return;
|
||||
} catch (err) {
|
||||
console.error("mindmap export error", err);
|
||||
// fall through to html-to-image fallback
|
||||
}
|
||||
}
|
||||
setTimeout(() => {
|
||||
addScript("/stage/protyle/js/html-to-image.min.js?v=1.11.13", "protyleHtml2image").then(() => {
|
||||
(renderElement as HTMLHtmlElement).style.display = "inline-block";
|
||||
|
|
@ -1067,11 +1127,32 @@ export class Toolbar {
|
|||
}
|
||||
});
|
||||
} else {
|
||||
renderElement.setAttribute("data-content", Lute.EscapeHTMLStr(textElement.value));
|
||||
renderElement.removeAttribute("data-render");
|
||||
// mindmap: try incremental update via stored markmap instance
|
||||
const isMindmap = renderElement.getAttribute("data-subtype") === "mindmap";
|
||||
const mmEntry: any = (renderElement as any).__markmap || (nodeElement as any).__markmap || null;
|
||||
if (isMindmap && mmEntry && mmEntry.transformer && mmEntry.markmap && this.subElement.querySelector('[data-type="refresh"]').classList.contains("block__icon--active")) {
|
||||
try {
|
||||
// update markmap in-place
|
||||
const txu = mmEntry.transformer.transform(textElement.value) || {};
|
||||
const rootu = txu.root || null;
|
||||
mmEntry.markmap.setData(rootu, mmEntry.options);
|
||||
renderElement.setAttribute("data-content", Lute.EscapeHTMLStr(textElement.value));
|
||||
} catch (err) {
|
||||
// fallback to re-render
|
||||
renderElement.setAttribute("data-content", Lute.EscapeHTMLStr(textElement.value));
|
||||
renderElement.removeAttribute("data-render");
|
||||
}
|
||||
} else {
|
||||
renderElement.setAttribute("data-content", Lute.EscapeHTMLStr(textElement.value));
|
||||
renderElement.removeAttribute("data-render");
|
||||
}
|
||||
}
|
||||
if (!types.includes("NodeBlockQueryEmbed") || !types.includes("NodeHTMLBlock") || !isInlineMemo) {
|
||||
processRender(renderElement);
|
||||
// If mindmap was updated via markmap.setData above, no need to run full processRender.
|
||||
const didIncrementalUpdate = renderElement.getAttribute("data-subtype") === "mindmap" && ((renderElement as any).__markmap && (renderElement as any).__markmap.markmap) && this.subElement.querySelector('[data-type="refresh"]').classList.contains("block__icon--active");
|
||||
if (!didIncrementalUpdate) {
|
||||
if (!types.includes("NodeBlockQueryEmbed") || !types.includes("NodeHTMLBlock") || !isInlineMemo) {
|
||||
processRender(renderElement);
|
||||
}
|
||||
}
|
||||
event.stopPropagation();
|
||||
});
|
||||
|
|
@ -1158,11 +1239,30 @@ export class Toolbar {
|
|||
} else {
|
||||
renderElement.setAttribute("data-content", Lute.EscapeHTMLStr(textElement.value));
|
||||
renderElement.removeAttribute("data-render");
|
||||
if (types.includes("NodeBlockQueryEmbed")) {
|
||||
blockRender(protyle, renderElement);
|
||||
(renderElement as HTMLElement).style.height = "";
|
||||
// If this is a mindmap and we have a stored markmap instance, prefer incremental update
|
||||
const isMindmap = renderElement.getAttribute("data-subtype") === "mindmap";
|
||||
const mmEntry: any = (renderElement as any).__markmap || (nodeElement as any).__markmap || null;
|
||||
if (isMindmap && mmEntry && mmEntry.transformer && mmEntry.markmap) {
|
||||
try {
|
||||
const txu = mmEntry.transformer.transform(textElement.value) || {};
|
||||
const rootu = txu.root || null;
|
||||
mmEntry.markmap.setData(rootu, mmEntry.options);
|
||||
} catch (err) {
|
||||
// fallback to full render
|
||||
if (types.includes("NodeBlockQueryEmbed")) {
|
||||
blockRender(protyle, renderElement);
|
||||
(renderElement as HTMLElement).style.height = "";
|
||||
} else {
|
||||
processRender(renderElement);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
processRender(renderElement);
|
||||
if (types.includes("NodeBlockQueryEmbed")) {
|
||||
blockRender(protyle, renderElement);
|
||||
(renderElement as HTMLElement).style.height = "";
|
||||
} else {
|
||||
processRender(renderElement);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2886,6 +2886,24 @@ export class WYSIWYG {
|
|||
return;
|
||||
}
|
||||
|
||||
const fitElement = hasClosestByClassName(event.target, "protyle-action__home");
|
||||
if (fitElement) {
|
||||
const blockElement = hasClosestBlock(fitElement);
|
||||
if (blockElement && blockElement.getAttribute("data-subtype") === "mindmap") {
|
||||
try {
|
||||
const mmEntry: any = (blockElement as any).__markmap || null;
|
||||
if (mmEntry && mmEntry.markmap && typeof mmEntry.markmap.fit === "function") {
|
||||
mmEntry.markmap.fit();
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("markmap fit error", e);
|
||||
}
|
||||
}
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
const languageElement = hasClosestByClassName(event.target, "protyle-action__language");
|
||||
if (languageElement && !protyle.disabled && !ctrlIsPressed) {
|
||||
protyle.toolbar.showCodeLanguage(protyle, [languageElement]);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue