chore(canvas): bring back load/save

This commit is contained in:
Elian Doran 2025-06-10 23:25:43 +03:00
parent dd58685455
commit ddbd268a66
No known key found for this signature in database
2 changed files with 109 additions and 91 deletions

View File

@ -108,7 +108,6 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
private librarycache: LibraryItem[];
private attachmentMetadata: AttachmentMetadata[];
private themeStyle!: Theme;
private excalidrawLib!: typeof import("@excalidraw/excalidraw");
private excalidrawApi!: ExcalidrawImperativeAPI;
private excalidrawWrapperRef!: React.RefObject<HTMLElement | null>;
@ -180,7 +179,10 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
const Canvas = (await import("./canvas_el.js")).default;
this.canvasInstance = new Canvas({
// this makes sure that 1) manual theme switch button is hidden 2) theme stays as it should after opening menu
theme: this.themeStyle,
onChange: () => this.onChangeHandler(),
viewModeEnabled: options.is("databaseReadonly"),
});
this.canvasInstance.renderCanvas(renderElement);
}
@ -238,36 +240,7 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
};
}
const { elements, files } = content;
const appState: Partial<AppState> = content.appState ?? {};
appState.theme = this.themeStyle;
if (this.excalidrawWrapperRef.current) {
const boundingClientRect = this.excalidrawWrapperRef.current.getBoundingClientRect();
appState.width = boundingClientRect.width;
appState.height = boundingClientRect.height;
appState.offsetLeft = boundingClientRect.left;
appState.offsetTop = boundingClientRect.top;
}
const sceneData: SceneData = {
elements,
appState
};
// files are expected in an array when loading. they are stored as a key-index object
// see example for loading here:
// https://github.com/excalidraw/excalidraw/blob/c5a7723185f6ca05e0ceb0b0d45c4e3fbcb81b2a/src/packages/excalidraw/example/App.js#L68
const fileArray: BinaryFileData[] = [];
for (const fileId in files) {
const file = files[fileId];
// TODO: dataURL is replaceable with a trilium image url
// maybe we can save normal images (pasted) with base64 data url, and trilium images
// with their respective url! nice
// file.dataURL = "http://localhost:8080/api/images/ltjOiU8nwoZx/start.png";
fileArray.push(file);
}
this.canvasInstance.loadData(content, this.themeStyle);
Promise.all(
(await note.getAttachmentsByRole("canvasLibraryItem")).map(async (attachment) => {
@ -301,11 +274,7 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
this.attachmentMetadata = metadata;
});
// Update the scene
// TODO: Fix type of sceneData
this.canvasInstance.excalidrawApi.updateScene(sceneData as any);
this.canvasInstance.excalidrawApi.addFiles(fileArray);
this.canvasInstance.excalidrawApi.history.clear();
}
// set initial scene version
@ -319,45 +288,8 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
* this is automatically called after this.saveData();
*/
async getData() {
const elements = this.canvasInstance.excalidrawApi.getSceneElements();
const appState = this.canvasInstance.excalidrawApi.getAppState();
/**
* A file is not deleted, even though removed from canvas. Therefore, we only keep
* files that are referenced by an element. Maybe this will change with a new excalidraw version?
*/
const files = this.canvasInstance.excalidrawApi.getFiles();
// parallel svg export to combat bitrot and enable rendering image for note inclusion, preview, and share
const svg = await this.excalidrawLib.exportToSvg({
elements,
appState,
exportPadding: 5, // 5 px padding
files
});
const svgString = svg.outerHTML;
const activeFiles: Record<string, BinaryFileData> = {};
// TODO: Used any where upstream typings appear to be broken.
elements.forEach((element: any) => {
if ("fileId" in element && element.fileId) {
activeFiles[element.fileId] = files[element.fileId];
}
});
const content = {
type: "excalidraw",
version: 2,
elements,
files: activeFiles,
appState: {
scrollX: appState.scrollX,
scrollY: appState.scrollY,
zoom: appState.zoom
}
};
const attachments = [{ role: "image", title: "canvas-export.svg", mime: "image/svg+xml", content: svgString, position: 0 }];
const { content, svg } = await this.canvasInstance.getData();
const attachments = [{ role: "image", title: "canvas-export.svg", mime: "image/svg+xml", content: svg, position: 0 }];
if (this.libraryChanged) {
// this.libraryChanged is unset in dataSaved()

View File

@ -1,7 +1,7 @@
import "@excalidraw/excalidraw/index.css";
import { Excalidraw, getSceneVersion, exportToSvg } from "@excalidraw/excalidraw";
import { createElement, createRef, Fragment, render } from "preact/compat";
import { ExcalidrawImperativeAPI, ExcalidrawProps } from "@excalidraw/excalidraw/types";
import { createElement, createRef, Fragment, RefObject, render } from "preact/compat";
import { AppState, BinaryFileData, ExcalidrawImperativeAPI, ExcalidrawProps, SceneData } from "@excalidraw/excalidraw/types";
/** -1 indicates that it is fresh. excalidraw scene version is always >0 */
const SCENE_VERSION_INITIAL = -1;
@ -12,6 +12,7 @@ export default class Canvas {
private currentSceneVersion: number;
private opts: ExcalidrawProps;
private excalidrawWrapperRef: RefObject<HTMLElement>;
excalidrawApi!: ExcalidrawImperativeAPI;
constructor(opts: ExcalidrawProps) {
@ -20,7 +21,7 @@ export default class Canvas {
}
renderCanvas(targetEl: HTMLElement) {
render(createCanvasElement({
render(this.createCanvasElement({
...this.opts,
excalidrawAPI: (api: ExcalidrawImperativeAPI) => {
this.excalidrawApi = api;
@ -28,6 +29,20 @@ export default class Canvas {
}), targetEl);
}
private createCanvasElement(opts: ExcalidrawProps) {
const excalidrawWrapperRef = createRef<HTMLElement>();
return createElement(Fragment, null,
createElement(
"div",
{
className: "excalidraw-wrapper",
ref: excalidrawWrapperRef
},
createElement(Excalidraw, opts)
));
}
/**
* needed to ensure, that multipleOnChangeHandler calls do not trigger a save.
* we compare the scene version as suggested in:
@ -61,18 +76,89 @@ export default class Canvas {
return this.currentSceneVersion === SCENE_VERSION_INITIAL;
}
loadData(content: any, theme: any) {
const { elements, files } = content;
const appState: Partial<AppState> = content.appState ?? {};
appState.theme = theme;
if (this.excalidrawWrapperRef?.current) {
const boundingClientRect = this.excalidrawWrapperRef.current.getBoundingClientRect();
appState.width = boundingClientRect.width;
appState.height = boundingClientRect.height;
appState.offsetLeft = boundingClientRect.left;
appState.offsetTop = boundingClientRect.top;
}
const sceneData: SceneData = {
elements,
appState
};
// files are expected in an array when loading. they are stored as a key-index object
// see example for loading here:
// https://github.com/excalidraw/excalidraw/blob/c5a7723185f6ca05e0ceb0b0d45c4e3fbcb81b2a/src/packages/excalidraw/example/App.js#L68
const fileArray: BinaryFileData[] = [];
for (const fileId in files) {
const file = files[fileId];
// TODO: dataURL is replaceable with a trilium image url
// maybe we can save normal images (pasted) with base64 data url, and trilium images
// with their respective url! nice
// file.dataURL = "http://localhost:8080/api/images/ltjOiU8nwoZx/start.png";
fileArray.push(file);
}
// Update the scene
// TODO: Fix type of sceneData
this.excalidrawApi.updateScene(sceneData as any);
this.excalidrawApi.addFiles(fileArray);
this.excalidrawApi.history.clear();
}
async getData() {
const elements = this.excalidrawApi.getSceneElements();
const appState = this.excalidrawApi.getAppState();
/**
* A file is not deleted, even though removed from canvas. Therefore, we only keep
* files that are referenced by an element. Maybe this will change with a new excalidraw version?
*/
const files = this.excalidrawApi.getFiles();
// parallel svg export to combat bitrot and enable rendering image for note inclusion, preview, and share
const svg = await exportToSvg({
elements,
appState,
exportPadding: 5, // 5 px padding
files
});
const svgString = svg.outerHTML;
const activeFiles: Record<string, BinaryFileData> = {};
// TODO: Used any where upstream typings appear to be broken.
elements.forEach((element: any) => {
if ("fileId" in element && element.fileId) {
activeFiles[element.fileId] = files[element.fileId];
}
});
const content = {
type: "excalidraw",
version: 2,
elements,
files: activeFiles,
appState: {
scrollX: appState.scrollX,
scrollY: appState.scrollY,
zoom: appState.zoom
}
};
return {
content,
svg: svgString
}
}
}
function createCanvasElement(opts: ExcalidrawProps) {
const excalidrawWrapperRef = createRef<HTMLElement>();
return createElement(Fragment, null,
createElement(
"div",
{
className: "excalidraw-wrapper",
ref: excalidrawWrapperRef
},
createElement(Excalidraw, opts)
));
}