diff --git a/apps/client/package.json b/apps/client/package.json index 8eab329fc..6406d4d56 100644 --- a/apps/client/package.json +++ b/apps/client/package.json @@ -33,6 +33,7 @@ "@triliumnext/highlightjs": "workspace:*", "@triliumnext/share-theme": "workspace:*", "@triliumnext/split.js": "workspace:*", + "@zumer/snapdom": "2.0.1", "autocomplete.js": "0.38.1", "bootstrap": "5.3.8", "boxicons": "2.1.4", diff --git a/apps/client/src/services/utils.ts b/apps/client/src/services/utils.ts index 2045cd4d7..5722dbc6f 100644 --- a/apps/client/src/services/utils.ts +++ b/apps/client/src/services/utils.ts @@ -925,6 +925,7 @@ export default { areObjectsEqual, copyHtmlToClipboard, createImageSrcUrl, + triggerDownload, downloadSvg, downloadSvgAsPng, compareVersions, diff --git a/apps/client/src/widgets/type_widgets/MindMap.tsx b/apps/client/src/widgets/type_widgets/MindMap.tsx index f8409c75c..626af1b6a 100644 --- a/apps/client/src/widgets/type_widgets/MindMap.tsx +++ b/apps/client/src/widgets/type_widgets/MindMap.tsx @@ -11,6 +11,7 @@ import { useEditorSpacedUpdate, useNoteLabelBoolean, useSyncedRef, useTriliumEve import { refToJQuerySelector } from "../react/react_utils"; import utils from "../../services/utils"; import { DISPLAYABLE_LOCALE_IDS } from "@triliumnext/commons"; +import { snapdom, SnapdomOptions } from "@zumer/snapdom"; const NEW_TOPIC_NAME = ""; @@ -45,11 +46,26 @@ export default function MindMap({ note, ntxId, noteContext }: TypeWidgetProps) { const apiRef = useRef(null); const containerRef = useRef(null); const [ isReadOnly ] = useNoteLabelBoolean(note, "readOnly"); + + // Shared options for snapdom screenshot generation used in both attachment saving and exports + const imageOptions : SnapdomOptions = { + backgroundColor: "transparent", + scale: 2 + }; + const spacedUpdate = useEditorSpacedUpdate({ note, noteContext, getData: async () => { if (!apiRef.current) return; + + const result = await snapdom(apiRef.current.nodes, imageOptions); + + // a data URL in the format: "data:image/svg+xml;charset=utf-8," + // We need to extract the content after the comma and decode the URL encoding (%3C to <, %20 to space, etc.) + // to get raw SVG content that Trilium's backend can store as an attachment + const svgContent = decodeURIComponent(result.url.split(',')[1]); + return { content: apiRef.current.getDataString(), attachments: [ @@ -57,7 +73,7 @@ export default function MindMap({ note, ntxId, noteContext }: TypeWidgetProps) { role: "image", title: "mindmap-export.svg", mime: "image/svg+xml", - content: await apiRef.current.exportSvg().text(), + content: svgContent, position: 0 } ] @@ -88,13 +104,18 @@ export default function MindMap({ note, ntxId, noteContext }: TypeWidgetProps) { // Export as PNG or SVG. useTriliumEvents([ "exportSvg", "exportPng" ], async ({ ntxId: eventNtxId }, eventName) => { if (eventNtxId !== ntxId || !apiRef.current) return; - const title = note.title; - const svg = await apiRef.current.exportSvg().text(); + + const result = await snapdom(apiRef.current.nodes, imageOptions); + + let dataUrl; if (eventName === "exportSvg") { - utils.downloadSvg(title, svg); + dataUrl = result.url; // Native SVG Data URL } else { - utils.downloadSvgAsPng(title, svg); + const pngImg = await result.toPng(); + dataUrl = pngImg.src; // PNG Data URL } + + await utils.triggerDownload(`${note.title}.${eventName === "exportSvg" ? "svg" : "png"}`, dataUrl); }); const onKeyDown = useCallback((e: KeyboardEvent) => { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e9647fbf5..0894e014f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -211,6 +211,9 @@ importers: '@triliumnext/split.js': specifier: workspace:* version: link:../../packages/splitjs + '@zumer/snapdom': + specifier: 2.0.1 + version: 2.0.1 autocomplete.js: specifier: 0.38.1 version: 0.38.1 @@ -5754,6 +5757,9 @@ packages: resolution: {integrity: sha512-PI6UdgpSeVoGvzguKHmy2bwOqI3UYkntLZOCpyJSKIi7234c5aJmQYkJB/P4P2YUJkqhbqvu7iM2/0eJZ178nA==} engines: {bun: '>=0.7.0', deno: '>=1.0.0', node: '>=16.5.0'} + '@zumer/snapdom@2.0.1': + resolution: {integrity: sha512-78/qbYl2FTv4H6qaXcNfAujfIOSzdvs83NW63VbyC9QA3sqNPfPvhn4xYMO6Gy11hXwJUEhd0z65yKiNzDwy9w==} + abab@2.0.6: resolution: {integrity: sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==} deprecated: Use your platform's native atob() and btoa() methods instead @@ -20671,6 +20677,8 @@ snapshots: '@zip.js/zip.js@2.8.2': {} + '@zumer/snapdom@2.0.1': {} + abab@2.0.6: {} abbrev@1.1.1: {}