mirror of
https://github.com/zadam/trilium.git
synced 2025-11-11 17:08:58 +01:00
chore(react/type_widget): set up canvas persistence
This commit is contained in:
parent
8c85aa343c
commit
68bf5b7e68
@ -1,34 +1,30 @@
|
|||||||
import { Excalidraw } from "@excalidraw/excalidraw";
|
import { Excalidraw, exportToSvg, getSceneVersion } from "@excalidraw/excalidraw";
|
||||||
import { TypeWidgetProps } from "./type_widget";
|
import { TypeWidgetProps } from "./type_widget";
|
||||||
import "@excalidraw/excalidraw/index.css";
|
import "@excalidraw/excalidraw/index.css";
|
||||||
import { useNoteBlob } from "../react/hooks";
|
import { useEditorSpacedUpdate, useNoteBlob } from "../react/hooks";
|
||||||
import { useEffect, useMemo, useRef } from "preact/hooks";
|
import { useEffect, useMemo, useRef } from "preact/hooks";
|
||||||
import type { ExcalidrawImperativeAPI, AppState } from "@excalidraw/excalidraw/types";
|
import { type ExcalidrawImperativeAPI, type AppState, type BinaryFileData, LibraryItem, ExcalidrawProps } from "@excalidraw/excalidraw/types";
|
||||||
import options from "../../services/options";
|
import options from "../../services/options";
|
||||||
import "./Canvas.css";
|
import "./Canvas.css";
|
||||||
|
import FNote from "../../entities/fnote";
|
||||||
|
import { RefObject } from "preact";
|
||||||
|
import server from "../../services/server";
|
||||||
|
import { NonDeletedExcalidrawElement } from "@excalidraw/excalidraw/element/types";
|
||||||
|
import { CanvasContent } from "../type_widgets_old/canvas_el";
|
||||||
|
|
||||||
|
interface AttachmentMetadata {
|
||||||
|
title: string;
|
||||||
|
attachmentId: string;
|
||||||
|
}
|
||||||
|
|
||||||
export default function Canvas({ note }: TypeWidgetProps) {
|
export default function Canvas({ note }: TypeWidgetProps) {
|
||||||
const apiRef = useRef<ExcalidrawImperativeAPI>(null);
|
const apiRef = useRef<ExcalidrawImperativeAPI>(null);
|
||||||
const blob = useNoteBlob(note);
|
|
||||||
const viewModeEnabled = options.is("databaseReadonly");
|
const viewModeEnabled = options.is("databaseReadonly");
|
||||||
const themeStyle = useMemo(() => {
|
const themeStyle = useMemo(() => {
|
||||||
const documentStyle = window.getComputedStyle(document.documentElement);
|
const documentStyle = window.getComputedStyle(document.documentElement);
|
||||||
return documentStyle.getPropertyValue("--theme-style")?.trim() as AppState["theme"];
|
return documentStyle.getPropertyValue("--theme-style")?.trim() as AppState["theme"];
|
||||||
}, []);
|
}, []);
|
||||||
|
const persistence = usePersistence(note, apiRef, themeStyle);
|
||||||
useEffect(() => {
|
|
||||||
const api = apiRef.current;
|
|
||||||
const content = blob?.content;
|
|
||||||
if (!api) return;
|
|
||||||
if (!content?.trim()) {
|
|
||||||
api.updateScene({
|
|
||||||
elements: [],
|
|
||||||
appState: {
|
|
||||||
theme: themeStyle
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [ blob ]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="canvas-widget note-detail-canvas note-detail-printable note-detail full-height">
|
<div className="canvas-widget note-detail-canvas note-detail-printable note-detail full-height">
|
||||||
@ -49,9 +45,240 @@ export default function Canvas({ note }: TypeWidgetProps) {
|
|||||||
export: false
|
export: false
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
{...persistence}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function usePersistence(note: FNote, apiRef: RefObject<ExcalidrawImperativeAPI>, theme: AppState["theme"]): Partial<ExcalidrawProps> {
|
||||||
|
const libraryChanged = useRef(false);
|
||||||
|
const currentSceneVersion = useRef(0);
|
||||||
|
|
||||||
|
// these 2 variables are needed to compare the library state (all library items) after loading to the state when the library changed. So we can find attachments to be deleted.
|
||||||
|
//every libraryitem is saved on its own json file in the attachments of the note.
|
||||||
|
const libraryCache = useRef<LibraryItem[]>([]);
|
||||||
|
const attachmentMetadata = useRef<AttachmentMetadata[]>([]);
|
||||||
|
|
||||||
|
const spacedUpdate = useEditorSpacedUpdate({
|
||||||
|
note,
|
||||||
|
onContentChange(newContent) {
|
||||||
|
const api = apiRef.current;
|
||||||
|
if (!api) return;
|
||||||
|
|
||||||
|
libraryCache.current = [];
|
||||||
|
attachmentMetadata.current = [];
|
||||||
|
currentSceneVersion.current = -1;
|
||||||
|
|
||||||
|
// load saved content into excalidraw canvas
|
||||||
|
let content: CanvasContent = {
|
||||||
|
elements: [],
|
||||||
|
files: [],
|
||||||
|
appState: {}
|
||||||
|
};
|
||||||
|
if (newContent) {
|
||||||
|
try {
|
||||||
|
content = JSON.parse(newContent) as CanvasContent;
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error parsing content. Probably note.type changed. Starting with empty canvas", note, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadData(api, content, theme);
|
||||||
|
|
||||||
|
// load the library state
|
||||||
|
loadLibrary(note).then(({ libraryItems, metadata }) => {
|
||||||
|
// Update the library and save to independent variables
|
||||||
|
api.updateLibrary({ libraryItems: libraryItems, merge: false });
|
||||||
|
|
||||||
|
// save state of library to compare it to the new state later.
|
||||||
|
libraryCache.current = libraryItems;
|
||||||
|
attachmentMetadata.current = metadata;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
async getData() {
|
||||||
|
const api = apiRef.current;
|
||||||
|
if (!api) return;
|
||||||
|
const { content, svg } = await getData(api);
|
||||||
|
const attachments = [{ role: "image", title: "canvas-export.svg", mime: "image/svg+xml", content: svg, position: 0 }];
|
||||||
|
|
||||||
|
if (libraryChanged.current) {
|
||||||
|
// this.libraryChanged is unset in dataSaved()
|
||||||
|
|
||||||
|
// there's no separate method to get library items, so have to abuse this one
|
||||||
|
const libraryItems = await api.updateLibrary({
|
||||||
|
libraryItems() {
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
merge: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// excalidraw saves the library as a own state. the items are saved to libraryItems. then we compare the library right now with a libraryitemcache. The cache is filled when we first load the Library into the note.
|
||||||
|
//We need the cache to delete old attachments later in the server.
|
||||||
|
|
||||||
|
const libraryItemsMissmatch = libraryCache.current.filter((obj1) => !libraryItems.some((obj2: LibraryItem) => obj1.id === obj2.id));
|
||||||
|
|
||||||
|
// before we saved the metadata of the attachments in a cache. the title of the attachment is a combination of libraryitem ´s ID und it´s name.
|
||||||
|
// we compare the library items in the libraryitemmissmatch variable (this one saves all libraryitems that are different to the state right now. E.g. you delete 1 item, this item is saved as mismatch)
|
||||||
|
// then we combine its id and title and search the according attachmentID.
|
||||||
|
|
||||||
|
const matchingItems = attachmentMetadata.current.filter((meta) => {
|
||||||
|
// Loop through the second array and check for a match
|
||||||
|
return libraryItemsMissmatch.some((item) => {
|
||||||
|
// Combine the `name` and `id` from the second array
|
||||||
|
const combinedTitle = `${item.id}${item.name}`;
|
||||||
|
return meta.title === combinedTitle;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// we save the attachment ID`s in a variable and delete every attachmentID. Now the items that the user deleted will be deleted.
|
||||||
|
const attachmentIds = matchingItems.map((item) => item.attachmentId);
|
||||||
|
|
||||||
|
//delete old attachments that are no longer used
|
||||||
|
for (const item of attachmentIds) {
|
||||||
|
await server.remove(`attachments/${item}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let position = 10;
|
||||||
|
|
||||||
|
// prepare data to save to server e.g. new library items.
|
||||||
|
for (const libraryItem of libraryItems) {
|
||||||
|
attachments.push({
|
||||||
|
role: "canvasLibraryItem",
|
||||||
|
title: libraryItem.id + libraryItem.name,
|
||||||
|
mime: "application/json",
|
||||||
|
content: JSON.stringify(libraryItem),
|
||||||
|
position: position
|
||||||
|
});
|
||||||
|
|
||||||
|
position += 10;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: JSON.stringify(content),
|
||||||
|
attachments
|
||||||
|
};
|
||||||
|
},
|
||||||
|
dataSaved() {
|
||||||
|
libraryChanged.current = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
onChange: () => {
|
||||||
|
if (!apiRef.current) return;
|
||||||
|
const oldSceneVersion = currentSceneVersion.current;
|
||||||
|
const newSceneVersion = getSceneVersion(apiRef.current.getSceneElements());
|
||||||
|
|
||||||
|
if (newSceneVersion !== oldSceneVersion) {
|
||||||
|
spacedUpdate.resetUpdateTimer();
|
||||||
|
spacedUpdate.scheduleUpdate();
|
||||||
|
currentSceneVersion.current = newSceneVersion;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onLibraryChange: () => {
|
||||||
|
libraryChanged.current = true;
|
||||||
|
spacedUpdate.resetUpdateTimer();
|
||||||
|
spacedUpdate.scheduleUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getData(api: ExcalidrawImperativeAPI) {
|
||||||
|
const elements = api.getSceneElements();
|
||||||
|
const appState = api.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 = api.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> = {};
|
||||||
|
elements.forEach((element: NonDeletedExcalidrawElement) => {
|
||||||
|
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,
|
||||||
|
gridModeEnabled: appState.gridModeEnabled
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
content,
|
||||||
|
svg: svgString
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadData(api: ExcalidrawImperativeAPI, content: CanvasContent, theme: AppState["theme"]) {
|
||||||
|
const { elements, files } = content;
|
||||||
|
const appState: Partial<AppState> = content.appState ?? {};
|
||||||
|
appState.theme = theme;
|
||||||
|
|
||||||
|
// 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
|
||||||
|
api.updateScene({
|
||||||
|
elements,
|
||||||
|
appState: appState as AppState
|
||||||
|
});
|
||||||
|
api.addFiles(fileArray);
|
||||||
|
api.history.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadLibrary(note: FNote) {
|
||||||
|
return Promise.all(
|
||||||
|
(await note.getAttachmentsByRole("canvasLibraryItem")).map(async (attachment) => {
|
||||||
|
const blob = await attachment.getBlob();
|
||||||
|
return {
|
||||||
|
blob, // Save the blob for libraryItems
|
||||||
|
metadata: {
|
||||||
|
// metadata to use in the cache variables for comparing old library state and new one. We delete unnecessary items later, calling the server directly
|
||||||
|
attachmentId: attachment.attachmentId,
|
||||||
|
title: attachment.title
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})
|
||||||
|
).then((results) => {
|
||||||
|
// Extract libraryItems from the blobs
|
||||||
|
const libraryItems = results.map((result) => result?.blob?.getJsonContentSafely()).filter((item) => !!item) as LibraryItem[];
|
||||||
|
|
||||||
|
// Extract metadata for each attachment
|
||||||
|
const metadata = results.map((result) => result.metadata);
|
||||||
|
|
||||||
|
return { libraryItems, metadata };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@ -9,11 +9,6 @@ import { renderReactWidget } from "../react/react_utils.jsx";
|
|||||||
import SpacedUpdate from "../../services/spaced_update.js";
|
import SpacedUpdate from "../../services/spaced_update.js";
|
||||||
import protected_session_holder from "../../services/protected_session_holder.js";
|
import protected_session_holder from "../../services/protected_session_holder.js";
|
||||||
|
|
||||||
interface AttachmentMetadata {
|
|
||||||
title: string;
|
|
||||||
attachmentId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* # Canvas note with excalidraw
|
* # Canvas note with excalidraw
|
||||||
* @author thfrei 2022-05-11
|
* @author thfrei 2022-05-11
|
||||||
@ -81,13 +76,6 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
|
|||||||
this.$widget;
|
this.$widget;
|
||||||
this.reactHandlers; // used to control react state
|
this.reactHandlers; // used to control react state
|
||||||
|
|
||||||
this.libraryChanged = false;
|
|
||||||
|
|
||||||
// these 2 variables are needed to compare the library state (all library items) after loading to the state when the library changed. So we can find attachments to be deleted.
|
|
||||||
//every libraryitem is saved on its own json file in the attachments of the note.
|
|
||||||
this.librarycache = [];
|
|
||||||
this.attachmentMetadata = [];
|
|
||||||
|
|
||||||
// TODO: We are duplicating the logic of note_detail.ts because it switches note ID mid-save, causing overwrites.
|
// TODO: We are duplicating the logic of note_detail.ts because it switches note ID mid-save, causing overwrites.
|
||||||
// This problem will get solved by itself once type widgets will be rewritten in React without the use of dangerous singletons.
|
// This problem will get solved by itself once type widgets will be rewritten in React without the use of dangerous singletons.
|
||||||
this.spacedUpdate = new SpacedUpdate(async () => {
|
this.spacedUpdate = new SpacedUpdate(async () => {
|
||||||
@ -155,144 +143,10 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
|
|||||||
await this.#init();
|
await this.#init();
|
||||||
}
|
}
|
||||||
|
|
||||||
// see if the note changed, since we do not get a new class for a new note
|
|
||||||
const noteChanged = this.currentNoteId !== note.noteId;
|
|
||||||
if (noteChanged) {
|
|
||||||
// reset the scene to omit unnecessary onchange handler
|
|
||||||
this.canvasInstance.resetSceneVersion();
|
|
||||||
}
|
|
||||||
this.currentNoteId = note.noteId;
|
this.currentNoteId = note.noteId;
|
||||||
|
|
||||||
// get note from backend and put into canvas
|
// get note from backend and put into canvas
|
||||||
const blob = await note.getBlob();
|
const blob = await note.getBlob();
|
||||||
|
|
||||||
/**
|
|
||||||
* new and empty note - make sure that canvas is empty.
|
|
||||||
* If we do not set it manually, we occasionally get some "bleeding" from another
|
|
||||||
* note into this fresh note. Probably due to that this note-instance does not get
|
|
||||||
* newly instantiated?
|
|
||||||
*/
|
|
||||||
if (!blob?.content?.trim()) {
|
|
||||||
|
|
||||||
} else if (blob.content) {
|
|
||||||
let content: CanvasContent;
|
|
||||||
|
|
||||||
// load saved content into excalidraw canvas
|
|
||||||
try {
|
|
||||||
content = blob.getJsonContent() as CanvasContent;
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Error parsing content. Probably note.type changed. Starting with empty canvas", note, blob, err);
|
|
||||||
|
|
||||||
content = {
|
|
||||||
elements: [],
|
|
||||||
files: [],
|
|
||||||
appState: {}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
this.canvasInstance.loadData(content, this.themeStyle);
|
|
||||||
|
|
||||||
Promise.all(
|
|
||||||
(await note.getAttachmentsByRole("canvasLibraryItem")).map(async (attachment) => {
|
|
||||||
const blob = await attachment.getBlob();
|
|
||||||
return {
|
|
||||||
blob, // Save the blob for libraryItems
|
|
||||||
metadata: {
|
|
||||||
// metadata to use in the cache variables for comparing old library state and new one. We delete unnecessary items later, calling the server directly
|
|
||||||
attachmentId: attachment.attachmentId,
|
|
||||||
title: attachment.title
|
|
||||||
}
|
|
||||||
};
|
|
||||||
})
|
|
||||||
).then((results) => {
|
|
||||||
if (note.noteId !== this.currentNoteId) {
|
|
||||||
// current note changed in the course of the async operation
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract libraryItems from the blobs
|
|
||||||
const libraryItems = results.map((result) => result?.blob?.getJsonContentSafely()).filter((item) => !!item) as LibraryItem[];
|
|
||||||
|
|
||||||
// Extract metadata for each attachment
|
|
||||||
const metadata = results.map((result) => result.metadata);
|
|
||||||
|
|
||||||
// Update the library and save to independent variables
|
|
||||||
this.canvasInstance.updateLibrary(libraryItems);
|
|
||||||
|
|
||||||
// save state of library to compare it to the new state later.
|
|
||||||
this.librarycache = libraryItems;
|
|
||||||
this.attachmentMetadata = metadata;
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// set initial scene version
|
|
||||||
if (this.canvasInstance.isInitialScene()) {
|
|
||||||
this.canvasInstance.updateSceneVersion();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* gets data from widget container that will be sent via spacedUpdate.scheduleUpdate();
|
|
||||||
* this is automatically called after this.saveData();
|
|
||||||
*/
|
|
||||||
async getData() {
|
|
||||||
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()
|
|
||||||
|
|
||||||
// there's no separate method to get library items, so have to abuse this one
|
|
||||||
const libraryItems = await this.canvasInstance.getLibraryItems();
|
|
||||||
|
|
||||||
// excalidraw saves the library as a own state. the items are saved to libraryItems. then we compare the library right now with a libraryitemcache. The cache is filled when we first load the Library into the note.
|
|
||||||
//We need the cache to delete old attachments later in the server.
|
|
||||||
|
|
||||||
const libraryItemsMissmatch = this.librarycache.filter((obj1) => !libraryItems.some((obj2: LibraryItem) => obj1.id === obj2.id));
|
|
||||||
|
|
||||||
// before we saved the metadata of the attachments in a cache. the title of the attachment is a combination of libraryitem ´s ID und it´s name.
|
|
||||||
// we compare the library items in the libraryitemmissmatch variable (this one saves all libraryitems that are different to the state right now. E.g. you delete 1 item, this item is saved as mismatch)
|
|
||||||
// then we combine its id and title and search the according attachmentID.
|
|
||||||
|
|
||||||
const matchingItems = this.attachmentMetadata.filter((meta) => {
|
|
||||||
// Loop through the second array and check for a match
|
|
||||||
return libraryItemsMissmatch.some((item) => {
|
|
||||||
// Combine the `name` and `id` from the second array
|
|
||||||
const combinedTitle = `${item.id}${item.name}`;
|
|
||||||
return meta.title === combinedTitle;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// we save the attachment ID`s in a variable and delete every attachmentID. Now the items that the user deleted will be deleted.
|
|
||||||
const attachmentIds = matchingItems.map((item) => item.attachmentId);
|
|
||||||
|
|
||||||
//delete old attachments that are no longer used
|
|
||||||
for (const item of attachmentIds) {
|
|
||||||
await server.remove(`attachments/${item}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
let position = 10;
|
|
||||||
|
|
||||||
// prepare data to save to server e.g. new library items.
|
|
||||||
for (const libraryItem of libraryItems) {
|
|
||||||
attachments.push({
|
|
||||||
role: "canvasLibraryItem",
|
|
||||||
title: libraryItem.id + libraryItem.name,
|
|
||||||
mime: "application/json",
|
|
||||||
content: JSON.stringify(libraryItem),
|
|
||||||
position: position
|
|
||||||
});
|
|
||||||
|
|
||||||
position += 10;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
content: JSON.stringify(content),
|
|
||||||
attachments
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -304,10 +158,6 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
|
|||||||
this.spacedUpdate.scheduleUpdate();
|
this.spacedUpdate.scheduleUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
dataSaved() {
|
|
||||||
this.libraryChanged = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
onChangeHandler() {
|
onChangeHandler() {
|
||||||
if (options.is("databaseReadonly")) {
|
if (options.is("databaseReadonly")) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@ -68,91 +68,6 @@ export default class Canvas {
|
|||||||
return !!this.excalidrawApi;
|
return !!this.excalidrawApi;
|
||||||
}
|
}
|
||||||
|
|
||||||
loadData(content: CanvasContent, theme: Theme) {
|
|
||||||
const { elements, files } = content;
|
|
||||||
const appState: Partial<AppState> = content.appState ?? {};
|
|
||||||
appState.theme = theme;
|
|
||||||
|
|
||||||
// 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({
|
|
||||||
elements,
|
|
||||||
appState: appState as AppState
|
|
||||||
});
|
|
||||||
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> = {};
|
|
||||||
elements.forEach((element: NonDeletedExcalidrawElement) => {
|
|
||||||
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,
|
|
||||||
gridModeEnabled: appState.gridModeEnabled
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
content,
|
|
||||||
svg: svgString
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async getLibraryItems() {
|
|
||||||
return this.excalidrawApi.updateLibrary({
|
|
||||||
libraryItems() {
|
|
||||||
return [];
|
|
||||||
},
|
|
||||||
merge: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateLibrary(libraryItems: LibraryItem[]) {
|
|
||||||
this.excalidrawApi.updateLibrary({ libraryItems, merge: false });
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function CanvasElement(opts: ExcalidrawProps) {
|
function CanvasElement(opts: ExcalidrawProps) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user