mirror of
https://github.com/TriliumNext/Notes.git
synced 2025-07-27 10:02:59 +08:00
Merge pull request #2255 from TriliumNext/feature/client_size_optimisation
Client bundle size optimization
This commit is contained in:
commit
825c2c1fe9
@ -51,8 +51,7 @@
|
||||
"mind-elixir": "4.6.0",
|
||||
"normalize.css": "8.0.1",
|
||||
"panzoom": "9.4.3",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0",
|
||||
"preact": "10.26.8",
|
||||
"split.js": "1.6.5",
|
||||
"svg-pan-zoom": "3.6.2",
|
||||
"vanilla-js-wheel-zoom": "9.0.4"
|
||||
@ -64,8 +63,6 @@
|
||||
"@types/leaflet": "1.9.18",
|
||||
"@types/leaflet-gpx": "1.3.7",
|
||||
"@types/mark.js": "8.11.12",
|
||||
"@types/react": "19.1.7",
|
||||
"@types/react-dom": "19.1.6",
|
||||
"copy-webpack-plugin": "13.0.0",
|
||||
"happy-dom": "18.0.1",
|
||||
"script-loader": "0.7.2",
|
||||
@ -75,7 +72,9 @@
|
||||
"name": "client",
|
||||
"targets": {
|
||||
"serve": {
|
||||
"dependsOn": ["^build"]
|
||||
"dependsOn": [
|
||||
"^build"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
2
apps/client/src/types-assets.d.ts
vendored
2
apps/client/src/types-assets.d.ts
vendored
@ -3,7 +3,7 @@ declare module "*.png" {
|
||||
export default path;
|
||||
}
|
||||
|
||||
declare module "@triliumnext/ckeditor5/emoji_definitions/en.json?url" {
|
||||
declare module "*?url" {
|
||||
var path: string;
|
||||
export default path;
|
||||
}
|
||||
|
2
apps/client/src/types.d.ts
vendored
2
apps/client/src/types.d.ts
vendored
@ -57,6 +57,8 @@ declare global {
|
||||
|
||||
process?: ElectronProcess;
|
||||
glob?: CustomGlobals;
|
||||
|
||||
EXCALIDRAW_ASSET_PATH?: string;
|
||||
}
|
||||
|
||||
interface AutoCompleteConfig {
|
||||
|
@ -1,16 +1,11 @@
|
||||
import TypeWidget from "./type_widget.js";
|
||||
import utils from "../../services/utils.js";
|
||||
import linkService from "../../services/link.js";
|
||||
import server from "../../services/server.js";
|
||||
import type FNote from "../../entities/fnote.js";
|
||||
import options from "../../services/options.js";
|
||||
import type { ExcalidrawElement, Theme } from "@excalidraw/excalidraw/element/types";
|
||||
import type { AppState, BinaryFileData, ExcalidrawImperativeAPI, ExcalidrawProps, LibraryItem, SceneData } from "@excalidraw/excalidraw/types";
|
||||
import type { JSX } from "react";
|
||||
import type React from "react";
|
||||
import type { Root } from "react-dom/client";
|
||||
import "@excalidraw/excalidraw/index.css";
|
||||
import asset_path from "../../asset_path.js";
|
||||
import type { LibraryItem } from "@excalidraw/excalidraw/types";
|
||||
import type { Theme } from "@excalidraw/excalidraw/element/types";
|
||||
import type Canvas from "./canvas_el.js";
|
||||
import { CanvasContent } from "./canvas_el.js";
|
||||
|
||||
const TPL = /*html*/`
|
||||
<div class="canvas-widget note-detail-canvas note-detail-printable note-detail">
|
||||
@ -28,6 +23,7 @@ const TPL = /*html*/`
|
||||
|
||||
.excalidraw-wrapper {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
:root[dir="ltr"]
|
||||
.excalidraw
|
||||
@ -51,11 +47,7 @@ const TPL = /*html*/`
|
||||
</div>
|
||||
`;
|
||||
|
||||
interface CanvasContent {
|
||||
elements: ExcalidrawElement[];
|
||||
files: BinaryFileData[];
|
||||
appState: Partial<AppState>;
|
||||
}
|
||||
|
||||
|
||||
interface AttachmentMetadata {
|
||||
title: string;
|
||||
@ -107,37 +99,22 @@ interface AttachmentMetadata {
|
||||
*/
|
||||
export default class ExcalidrawTypeWidget extends TypeWidget {
|
||||
|
||||
private readonly SCENE_VERSION_INITIAL: number;
|
||||
private readonly SCENE_VERSION_ERROR: number;
|
||||
|
||||
private currentNoteId: string;
|
||||
private currentSceneVersion: number;
|
||||
|
||||
private libraryChanged: boolean;
|
||||
private librarycache: LibraryItem[];
|
||||
private attachmentMetadata: AttachmentMetadata[];
|
||||
private themeStyle!: Theme;
|
||||
private excalidrawLib!: typeof import("@excalidraw/excalidraw");
|
||||
private excalidrawApi!: ExcalidrawImperativeAPI;
|
||||
private excalidrawWrapperRef!: React.RefObject<HTMLElement | null>;
|
||||
|
||||
private $render!: JQuery<HTMLElement>;
|
||||
private root?: Root;
|
||||
private reactHandlers!: JQuery<HTMLElement>;
|
||||
private canvasInstance!: Canvas;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
// constants
|
||||
this.SCENE_VERSION_INITIAL = -1; // -1 indicates that it is fresh. excalidraw scene version is always >0
|
||||
this.SCENE_VERSION_ERROR = -2; // -2 indicates error
|
||||
|
||||
// currently required by excalidraw, in order to allows self-hosting fonts locally.
|
||||
// this avoids making excalidraw load the fonts from an external CDN.
|
||||
(window as any).EXCALIDRAW_ASSET_PATH = `${window.location.pathname}/node_modules/@excalidraw/excalidraw/dist/prod`;
|
||||
|
||||
// temporary vars
|
||||
this.currentNoteId = "";
|
||||
this.currentSceneVersion = this.SCENE_VERSION_INITIAL;
|
||||
|
||||
// will be overwritten
|
||||
this.$render;
|
||||
@ -182,34 +159,48 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
|
||||
throw new Error("Unable to find element to render.");
|
||||
}
|
||||
|
||||
// See https://github.com/excalidraw/excalidraw/issues/7899.
|
||||
if (!window.process) {
|
||||
(window.process as any) = {};
|
||||
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"),
|
||||
zenModeEnabled: false,
|
||||
gridModeEnabled: false,
|
||||
isCollaborating: false,
|
||||
detectScroll: false,
|
||||
handleKeyboardGlobally: false,
|
||||
autoFocus: false,
|
||||
UIOptions: {
|
||||
canvasActions: {
|
||||
saveToActiveFile: false,
|
||||
export: false
|
||||
}
|
||||
if (!window.process.env) {
|
||||
window.process.env = {};
|
||||
}
|
||||
(window.process.env as any).PREACT = false;
|
||||
},
|
||||
onLibraryChange: () => {
|
||||
this.libraryChanged = true;
|
||||
|
||||
const excalidraw = await import("@excalidraw/excalidraw");
|
||||
this.excalidrawLib = excalidraw;
|
||||
this.saveData();
|
||||
},
|
||||
});
|
||||
|
||||
const { createRoot } = await import("react-dom/client");
|
||||
const React = (await import("react")).default;
|
||||
this.root?.unmount();
|
||||
this.root = createRoot(renderElement);
|
||||
this.root.render(React.createElement(() => this.createExcalidrawReactApp(React, excalidraw.Excalidraw)));
|
||||
await setupFonts();
|
||||
this.canvasInstance.renderCanvas(renderElement);
|
||||
}
|
||||
|
||||
/**
|
||||
* called to populate the widget container with the note content
|
||||
*/
|
||||
async doRefresh(note: FNote) {
|
||||
if (!this.canvasInstance) {
|
||||
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.currentSceneVersion = this.SCENE_VERSION_INITIAL;
|
||||
this.canvasInstance.resetSceneVersion();
|
||||
}
|
||||
this.currentNoteId = note.noteId;
|
||||
|
||||
@ -217,10 +208,7 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
|
||||
const blob = await note.getBlob();
|
||||
|
||||
// before we load content into excalidraw, make sure excalidraw has loaded
|
||||
while (!this.excalidrawApi) {
|
||||
console.log("excalidrawApi not yet loaded, sleep 200ms...");
|
||||
await utils.sleep(200);
|
||||
}
|
||||
await this.canvasInstance.waitForApiToBecomeAvailable();
|
||||
|
||||
/**
|
||||
* new and empty note - make sure that canvas is empty.
|
||||
@ -229,15 +217,7 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
|
||||
* newly instantiated?
|
||||
*/
|
||||
if (!blob?.content?.trim()) {
|
||||
const sceneData: SceneData = {
|
||||
elements: [],
|
||||
appState: {
|
||||
theme: this.themeStyle
|
||||
}
|
||||
};
|
||||
|
||||
// TODO: Props mismatch.
|
||||
this.excalidrawApi.updateScene(sceneData as any);
|
||||
this.canvasInstance.resetScene(this.themeStyle);
|
||||
} else if (blob.content) {
|
||||
let content: CanvasContent;
|
||||
|
||||
@ -254,36 +234,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) => {
|
||||
@ -310,23 +261,19 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
|
||||
const metadata = results.map((result) => result.metadata);
|
||||
|
||||
// Update the library and save to independent variables
|
||||
this.excalidrawApi.updateLibrary({ libraryItems, merge: false });
|
||||
this.canvasInstance.updateLibrary(libraryItems);
|
||||
|
||||
// save state of library to compare it to the new state later.
|
||||
this.librarycache = libraryItems;
|
||||
this.attachmentMetadata = metadata;
|
||||
});
|
||||
|
||||
// Update the scene
|
||||
// TODO: Fix type of sceneData
|
||||
this.excalidrawApi.updateScene(sceneData as any);
|
||||
this.excalidrawApi.addFiles(fileArray);
|
||||
this.excalidrawApi.history.clear();
|
||||
|
||||
}
|
||||
|
||||
// set initial scene version
|
||||
if (this.currentSceneVersion === this.SCENE_VERSION_INITIAL) {
|
||||
this.currentSceneVersion = this.getSceneVersion();
|
||||
if (this.canvasInstance.isInitialScene()) {
|
||||
this.canvasInstance.updateSceneVersion();
|
||||
}
|
||||
}
|
||||
|
||||
@ -335,56 +282,14 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
|
||||
* this is automatically called after this.saveData();
|
||||
*/
|
||||
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 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()
|
||||
|
||||
// there's no separate method to get library items, so have to abuse this one
|
||||
const libraryItems = await this.excalidrawApi.updateLibrary({
|
||||
libraryItems() {
|
||||
return [];
|
||||
},
|
||||
merge: true
|
||||
});
|
||||
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.
|
||||
@ -453,146 +358,39 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
|
||||
}
|
||||
// changeHandler is called upon any tiny change in excalidraw. button clicked, hover, etc.
|
||||
// make sure only when a new element is added, we actually save something.
|
||||
const isNewSceneVersion = this.isNewSceneVersion();
|
||||
const isNewSceneVersion = this.canvasInstance.isNewSceneVersion();
|
||||
/**
|
||||
* FIXME: however, we might want to make an exception, if viewport changed, since viewport
|
||||
* is desired to save? (add) and appState background, and some things
|
||||
*/
|
||||
|
||||
// upon updateScene, onchange is called, even though "nothing really changed" that is worth saving
|
||||
const isNotInitialScene = this.currentSceneVersion !== this.SCENE_VERSION_INITIAL;
|
||||
|
||||
const isNotInitialScene = !this.canvasInstance.isInitialScene();
|
||||
const shouldSave = isNewSceneVersion && isNotInitialScene;
|
||||
|
||||
if (shouldSave) {
|
||||
this.updateSceneVersion();
|
||||
this.canvasInstance.updateSceneVersion();
|
||||
this.saveData();
|
||||
}
|
||||
}
|
||||
|
||||
createExcalidrawReactApp(react: typeof React, excalidrawComponent: React.MemoExoticComponent<(props: ExcalidrawProps) => JSX.Element>) {
|
||||
const excalidrawWrapperRef = react.useRef<HTMLElement>(null);
|
||||
this.excalidrawWrapperRef = excalidrawWrapperRef;
|
||||
const [dimensions, setDimensions] = react.useState<{ width?: number; height?: number }>({
|
||||
width: undefined,
|
||||
height: undefined
|
||||
});
|
||||
|
||||
react.useEffect(() => {
|
||||
if (excalidrawWrapperRef.current) {
|
||||
const dimensions = {
|
||||
width: excalidrawWrapperRef.current.getBoundingClientRect().width,
|
||||
height: excalidrawWrapperRef.current.getBoundingClientRect().height
|
||||
};
|
||||
setDimensions(dimensions);
|
||||
}
|
||||
|
||||
const onResize = () => {
|
||||
if (this.note?.type !== "canvas") {
|
||||
async function setupFonts() {
|
||||
if (window.EXCALIDRAW_ASSET_PATH) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (excalidrawWrapperRef.current) {
|
||||
const dimensions = {
|
||||
width: excalidrawWrapperRef.current.getBoundingClientRect().width,
|
||||
height: excalidrawWrapperRef.current.getBoundingClientRect().height
|
||||
};
|
||||
setDimensions(dimensions);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("resize", onResize);
|
||||
|
||||
return () => window.removeEventListener("resize", onResize);
|
||||
}, [excalidrawWrapperRef]);
|
||||
|
||||
const onLinkOpen = react.useCallback<NonNullable<ExcalidrawProps["onLinkOpen"]>>((element, event) => {
|
||||
let link = element.link;
|
||||
if (!link) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (link.startsWith("root/")) {
|
||||
link = "#" + link;
|
||||
}
|
||||
|
||||
const { nativeEvent } = event.detail;
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
return linkService.goToLinkExt(nativeEvent, link, null);
|
||||
}, []);
|
||||
|
||||
return react.createElement(
|
||||
react.Fragment,
|
||||
null,
|
||||
react.createElement(
|
||||
"div",
|
||||
{
|
||||
className: "excalidraw-wrapper",
|
||||
ref: excalidrawWrapperRef
|
||||
},
|
||||
react.createElement(excalidrawComponent, {
|
||||
// this makes sure that 1) manual theme switch button is hidden 2) theme stays as it should after opening menu
|
||||
theme: this.themeStyle,
|
||||
excalidrawAPI: (api: ExcalidrawImperativeAPI) => {
|
||||
this.excalidrawApi = api;
|
||||
},
|
||||
onLibraryChange: () => {
|
||||
this.libraryChanged = true;
|
||||
|
||||
this.saveData();
|
||||
},
|
||||
onChange: () => this.onChangeHandler(),
|
||||
viewModeEnabled: options.is("databaseReadonly"),
|
||||
zenModeEnabled: false,
|
||||
gridModeEnabled: false,
|
||||
isCollaborating: false,
|
||||
detectScroll: false,
|
||||
handleKeyboardGlobally: false,
|
||||
autoFocus: false,
|
||||
onLinkOpen,
|
||||
UIOptions: {
|
||||
canvasActions: {
|
||||
saveToActiveFile: false,
|
||||
export: false
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* needed to ensure, that multipleOnChangeHandler calls do not trigger a save.
|
||||
* we compare the scene version as suggested in:
|
||||
* https://github.com/excalidraw/excalidraw/issues/3014#issuecomment-778115329
|
||||
*
|
||||
* info: sceneVersions are not incrementing. it seems to be a pseudo-random number
|
||||
*/
|
||||
isNewSceneVersion() {
|
||||
if (options.is("databaseReadonly")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const sceneVersion = this.getSceneVersion();
|
||||
|
||||
return (
|
||||
this.currentSceneVersion === this.SCENE_VERSION_INITIAL || // initial scene version update
|
||||
this.currentSceneVersion !== sceneVersion
|
||||
); // ensure scene changed
|
||||
}
|
||||
|
||||
getSceneVersion() {
|
||||
if (this.excalidrawApi) {
|
||||
const elements = this.excalidrawApi.getSceneElements();
|
||||
return this.excalidrawLib.getSceneVersion(elements);
|
||||
// currently required by excalidraw, in order to allows self-hosting fonts locally.
|
||||
// this avoids making excalidraw load the fonts from an external CDN.
|
||||
let path: string;
|
||||
if (!glob.isDev) {
|
||||
path = `${window.location.pathname}/node_modules/@excalidraw/excalidraw/dist/prod`;
|
||||
} else {
|
||||
return this.SCENE_VERSION_ERROR;
|
||||
}
|
||||
path = (await import("../../../node_modules/@excalidraw/excalidraw/dist/prod/fonts/Excalifont/Excalifont-Regular-a88b72a24fb54c9f94e3b5fdaa7481c9.woff2?url")).default;
|
||||
let pathComponents = path.split("/");
|
||||
path = pathComponents.slice(0, pathComponents.length - 2).join("/");
|
||||
}
|
||||
|
||||
updateSceneVersion() {
|
||||
this.currentSceneVersion = this.getSceneVersion();
|
||||
}
|
||||
window.EXCALIDRAW_ASSET_PATH = path;
|
||||
}
|
||||
|
179
apps/client/src/widgets/type_widgets/canvas_el.ts
Normal file
179
apps/client/src/widgets/type_widgets/canvas_el.ts
Normal file
@ -0,0 +1,179 @@
|
||||
import "@excalidraw/excalidraw/index.css";
|
||||
import { Excalidraw, getSceneVersion, exportToSvg } from "@excalidraw/excalidraw";
|
||||
import { createElement, render, unmountComponentAtNode } from "preact/compat";
|
||||
import { AppState, BinaryFileData, ExcalidrawImperativeAPI, ExcalidrawProps, LibraryItem } from "@excalidraw/excalidraw/types";
|
||||
import type { ComponentType } from "preact";
|
||||
import { ExcalidrawElement, NonDeletedExcalidrawElement, Theme } from "@excalidraw/excalidraw/element/types";
|
||||
|
||||
export interface CanvasContent {
|
||||
elements: ExcalidrawElement[];
|
||||
files: BinaryFileData[];
|
||||
appState: Partial<AppState>;
|
||||
}
|
||||
|
||||
/** Indicates that it is fresh. excalidraw scene version is always >0 */
|
||||
const SCENE_VERSION_INITIAL = -1;
|
||||
|
||||
export default class Canvas {
|
||||
|
||||
private currentSceneVersion: number;
|
||||
private opts: ExcalidrawProps;
|
||||
private excalidrawApi!: ExcalidrawImperativeAPI;
|
||||
private initializedPromise: JQuery.Deferred<void>;
|
||||
|
||||
constructor(opts: ExcalidrawProps) {
|
||||
this.opts = opts;
|
||||
this.currentSceneVersion = SCENE_VERSION_INITIAL;
|
||||
this.initializedPromise = $.Deferred();
|
||||
}
|
||||
|
||||
renderCanvas(targetEl: HTMLElement) {
|
||||
unmountComponentAtNode(targetEl);
|
||||
render(this.createCanvasElement({
|
||||
...this.opts,
|
||||
excalidrawAPI: (api: ExcalidrawImperativeAPI) => {
|
||||
this.excalidrawApi = api;
|
||||
this.initializedPromise.resolve();
|
||||
},
|
||||
}), targetEl);
|
||||
}
|
||||
|
||||
async waitForApiToBecomeAvailable() {
|
||||
while (!this.excalidrawApi) {
|
||||
await this.initializedPromise;
|
||||
}
|
||||
}
|
||||
|
||||
private createCanvasElement(opts: ExcalidrawProps) {
|
||||
return createElement("div", { className: "excalidraw-wrapper", },
|
||||
createElement(Excalidraw as ComponentType<ExcalidrawProps>, opts)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* needed to ensure, that multipleOnChangeHandler calls do not trigger a save.
|
||||
* we compare the scene version as suggested in:
|
||||
* https://github.com/excalidraw/excalidraw/issues/3014#issuecomment-778115329
|
||||
*
|
||||
* info: sceneVersions are not incrementing. it seems to be a pseudo-random number
|
||||
*/
|
||||
isNewSceneVersion() {
|
||||
const sceneVersion = this.getSceneVersion();
|
||||
|
||||
return (
|
||||
this.currentSceneVersion === SCENE_VERSION_INITIAL || // initial scene version update
|
||||
this.currentSceneVersion !== sceneVersion
|
||||
); // ensure scene changed
|
||||
}
|
||||
|
||||
getSceneVersion() {
|
||||
const elements = this.excalidrawApi.getSceneElements();
|
||||
return getSceneVersion(elements);
|
||||
}
|
||||
|
||||
updateSceneVersion() {
|
||||
this.currentSceneVersion = this.getSceneVersion();
|
||||
}
|
||||
|
||||
resetSceneVersion() {
|
||||
this.currentSceneVersion = SCENE_VERSION_INITIAL;
|
||||
}
|
||||
|
||||
isInitialScene() {
|
||||
return this.currentSceneVersion === SCENE_VERSION_INITIAL;
|
||||
}
|
||||
|
||||
resetScene(theme: Theme) {
|
||||
this.excalidrawApi.updateScene({
|
||||
elements: [],
|
||||
appState: {
|
||||
theme
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
content,
|
||||
svg: svgString
|
||||
}
|
||||
}
|
||||
|
||||
async getLibraryItems() {
|
||||
return this.excalidrawApi.updateLibrary({
|
||||
libraryItems() {
|
||||
return [];
|
||||
},
|
||||
merge: true
|
||||
});
|
||||
}
|
||||
|
||||
async updateLibrary(libraryItems: LibraryItem[]) {
|
||||
this.excalidrawApi.updateLibrary({ libraryItems, merge: false });
|
||||
}
|
||||
|
||||
}
|
@ -43,11 +43,22 @@ export default defineConfig(() => ({
|
||||
{
|
||||
find: "@triliumnext/highlightjs",
|
||||
replacement: resolve(__dirname, "node_modules/@triliumnext/highlightjs/dist")
|
||||
},
|
||||
{
|
||||
find: "react",
|
||||
replacement: "preact/compat"
|
||||
},
|
||||
{
|
||||
find: "react-dom",
|
||||
replacement: "preact/compat"
|
||||
}
|
||||
],
|
||||
dedupe: [
|
||||
"react",
|
||||
"react-dom"
|
||||
"react-dom",
|
||||
"preact",
|
||||
"preact/compat",
|
||||
"preact/hooks"
|
||||
]
|
||||
},
|
||||
// Uncomment this if you are using workers.
|
||||
@ -97,5 +108,8 @@ export default defineConfig(() => ({
|
||||
},
|
||||
commonjsOptions: {
|
||||
transformMixedEsModules: true,
|
||||
},
|
||||
define: {
|
||||
"process.env.IS_PREACT": JSON.stringify("true"),
|
||||
}
|
||||
}));
|
||||
|
@ -92,6 +92,9 @@
|
||||
},
|
||||
"overrides": {
|
||||
"node-abi": "4.9.0",
|
||||
"mermaid": "11.6.0",
|
||||
"preact": "10.26.8",
|
||||
"roughjs": "4.6.6",
|
||||
"@types/express-serve-static-core": "5.0.6",
|
||||
"flat@<5.0.1": ">=5.0.1",
|
||||
"debug@>=3.2.0 <3.2.7": ">=3.2.7",
|
||||
|
772
pnpm-lock.yaml
generated
772
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user