refactor(deps): use webpack import for canvas

This commit is contained in:
Elian Doran 2025-01-18 11:09:57 +02:00
parent ab65913e52
commit 7d3f506efb
No known key found for this signature in database
10 changed files with 144 additions and 159 deletions

View File

@ -73,7 +73,6 @@ const copy = async () => {
}
const nodeModulesFolder = [
"node_modules/@excalidraw/excalidraw/dist/",
"node_modules/katex/dist/",
"node_modules/dayjs/",
"node_modules/boxicons/css/",

View File

@ -49,7 +49,6 @@ cp "$script_dir/../build/electron-main.js" "$DIR"
if [[ -d "$DIR"/node_modules ]]; then
# cleanup of useless files in dependencies
for d in 'image-q/demo' \
'@excalidraw/excalidraw/dist/excalidraw-assets-dev' '@excalidraw/excalidraw/dist/excalidraw.development.js' '@excalidraw/excalidraw/dist/excalidraw-with-preact.development.js' \
'mermaid/dist/mermaid.js' \
'boxicons/svg' 'boxicons/node_modules/react'/* \
'@jimp/plugin-print/fonts' 'jimp/browser' 'jimp/fonts'; do

33
package-lock.json generated
View File

@ -16,6 +16,7 @@
"@mermaid-js/layout-elk": "0.1.7",
"@mind-elixir/node-menu": "1.0.3",
"@triliumnext/express-partial-content": "1.0.1",
"@types/react-dom": "18.3.1",
"archiver": "7.0.1",
"async-mutex": "0.5.0",
"autocomplete.js": "0.38.1",
@ -133,6 +134,7 @@
"@types/mime-types": "2.1.4",
"@types/multer": "1.4.12",
"@types/node": "22.10.7",
"@types/react": "18.3.1",
"@types/safe-compare": "1.1.2",
"@types/sanitize-html": "2.13.0",
"@types/sax": "1.2.7",
@ -3912,6 +3914,12 @@
"undici-types": "~6.20.0"
}
},
"node_modules/@types/prop-types": {
"version": "15.7.14",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz",
"integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==",
"license": "MIT"
},
"node_modules/@types/qs": {
"version": "6.9.17",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.17.tgz",
@ -3926,6 +3934,25 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/react": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.1.tgz",
"integrity": "sha512-V0kuGBX3+prX+DQ/7r2qsv1NsdfnCLnTgnRJ1pYnxykBhGMz+qj+box5lq7XsO5mtZsBqpjwwTu/7wszPfMBcw==",
"license": "MIT",
"dependencies": {
"@types/prop-types": "*",
"csstype": "^3.0.2"
}
},
"node_modules/@types/react-dom": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.1.tgz",
"integrity": "sha512-qW1Mfv8taImTthu4KoXgDfLuk4bydU6Q/TkADnDWWHwi4NX4BR+LWfTp2sVmTqRrsHvyDDTelgelxJ+SsejKKQ==",
"license": "MIT",
"dependencies": {
"@types/react": "*"
}
},
"node_modules/@types/readdir-glob": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/@types/readdir-glob/-/readdir-glob-1.1.5.tgz",
@ -6652,6 +6679,12 @@
"node": ">=18"
}
},
"node_modules/csstype": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"license": "MIT"
},
"node_modules/cytoscape": {
"version": "3.30.4",
"resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.30.4.tgz",

View File

@ -61,6 +61,7 @@
"@mermaid-js/layout-elk": "0.1.7",
"@mind-elixir/node-menu": "1.0.3",
"@triliumnext/express-partial-content": "1.0.1",
"@types/react-dom": "18.3.1",
"archiver": "7.0.1",
"async-mutex": "0.5.0",
"autocomplete.js": "0.38.1",
@ -175,6 +176,7 @@
"@types/mime-types": "2.1.4",
"@types/multer": "1.4.12",
"@types/node": "22.10.7",
"@types/react": "18.3.1",
"@types/safe-compare": "1.1.2",
"@types/sanitize-html": "2.13.0",
"@types/sax": "1.2.7",

View File

@ -72,10 +72,6 @@ const MERMAID: Library = {
js: ["node_modules/mermaid/dist/mermaid.min.js"]
};
const EXCALIDRAW: Library = {
js: ["node_modules/react/umd/react.production.min.js", "node_modules/react-dom/umd/react-dom.production.min.js", "node_modules/@excalidraw/excalidraw/dist/excalidraw.production.min.js"]
};
const MARKJS: Library = {
js: ["node_modules/mark.js/dist/jquery.mark.es6.min.js"]
};
@ -198,7 +194,6 @@ export default {
KATEX,
WHEEL_ZOOM,
MERMAID,
EXCALIDRAW,
MARKJS,
I18NEXT,
HIGHLIGHT_JS

View File

@ -234,7 +234,7 @@ function goToLink(evt: MouseEvent | JQuery.ClickEvent) {
return goToLinkExt(evt, hrefLink, $link);
}
function goToLinkExt(evt: MouseEvent | JQuery.ClickEvent, hrefLink: string | undefined, $link: JQuery<HTMLElement> | null) {
function goToLinkExt(evt: MouseEvent | JQuery.ClickEvent | React.PointerEvent<HTMLCanvasElement>, hrefLink: string | undefined, $link: JQuery<HTMLElement> | null) {
if (hrefLink?.startsWith("data:")) {
return true;
}
@ -249,13 +249,10 @@ function goToLinkExt(evt: MouseEvent | JQuery.ClickEvent, hrefLink: string | und
const { notePath, viewScope } = parseNavigationStateFromUrl(hrefLink);
const ctrlKey = utils.isCtrlKey(evt);
const isLeftClick = evt.which === 1;
const isMiddleClick = evt.which === 2;
const isLeftClick = ("which" in evt && evt.which === 1);
const isMiddleClick = ("which" in evt && evt.which === 2);
const openInNewTab = (isLeftClick && ctrlKey) || isMiddleClick;
const leftClick = evt.which === 1;
const middleClick = evt.which === 2;
if (notePath) {
if (openInNewTab) {
appContext.tabManager.openTabWithNoteWithHoisting(notePath, { viewScope });
@ -276,7 +273,7 @@ function goToLinkExt(evt: MouseEvent | JQuery.ClickEvent, hrefLink: string | und
const withinEditLink = $link?.hasClass("ck-link-actions__preview");
const outsideOfCKEditor = !$link || $link.closest("[contenteditable]").length === 0;
if (openInNewTab || (withinEditLink && (leftClick || middleClick)) || (outsideOfCKEditor && (leftClick || middleClick))) {
if (openInNewTab || (withinEditLink && (isLeftClick || isMiddleClick)) || (outsideOfCKEditor && (isLeftClick || isMiddleClick))) {
if (hrefLink.toLowerCase().startsWith("http") || hrefLink.startsWith("api/")) {
window.open(hrefLink, "_blank");
} else if ((hrefLink.toLowerCase().startsWith("file:") || hrefLink.toLowerCase().startsWith("geo:")) && utils.isElectron()) {

View File

@ -97,7 +97,7 @@ function isMac() {
return navigator.platform.indexOf("Mac") > -1;
}
function isCtrlKey(evt: KeyboardEvent | MouseEvent | JQuery.ClickEvent | JQuery.ContextMenuEvent | JQuery.TriggeredEvent) {
function isCtrlKey(evt: KeyboardEvent | MouseEvent | JQuery.ClickEvent | JQuery.ContextMenuEvent | JQuery.TriggeredEvent | React.PointerEvent<HTMLCanvasElement>) {
return (!isMac() && evt.ctrlKey) || (isMac() && evt.metaKey);
}

View File

@ -54,72 +54,6 @@ declare global {
process?: ElectronProcess;
glob?: CustomGlobals;
React: {
createElement(any, any?, any?);
Fragment: any;
useState({
width: undefined,
height: undefined
});
useRef(ref: null);
useEffect(cb: () => void, args: unknown[]);
useCallback(cb: (el, ev) => void, args: unknown[]);
};
ReactDOM: {
unmountComponentAtNode(el: HTMLElement);
createRoot(el: HTMLElement);
}
ExcalidrawLib: {
getSceneVersion(el: unknown[]): number;
exportToSvg(opts: {
elements: ExcalidrawElement[],
appState: ExcalidrawAppState,
exportPadding: number,
metadata: string,
files: ExcalidrawElement[]
}): Promise<HTMLElement>;
updateScene,
Excalidraw: unknown
}
EXCALIDRAW_ASSET_PATH: string;
}
interface ExcalidrawApi {
getSceneElements(): ExcalidrawElement[];
getAppState(): ExcalidrawAppState;
getFiles(): ExcalidrawElement[];
updateScene(scene: ExcalidrawScene);
updateLibrary(opts: { libraryItems?: ExcalidrawLibrary[], merge: boolean }): Promise<ExcalidrawLibrary[]>;
addFiles(files: ExcalidrawElement[]);
history: {
clear();
}
}
interface ExcalidrawElement {
fileId: number;
}
interface ExcalidrawLibrary {
id: string;
name: string;
}
interface ExcalidrawScene {
elements: unknown[];
appState: ExcalidrawAppState;
collaborators: unknown[];
}
interface ExcalidrawAppState {
scrollX?: number;
scrollY?: number;
zoom?: number;
theme?: string;
width?: number;
height?: number;
offsetLeft?: number;
offsetTop?: number;
}
interface AutoCompleteConfig {

View File

@ -1,9 +1,14 @@
import libraryLoader from "../../services/library_loader.js";
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 type { default as ExcalidrawLib } from "@excalidraw/excalidraw";
import type { ExcalidrawElement, Theme } from "@excalidraw/excalidraw/types/element/types.js";
import type { AppState, BinaryFileData, ExcalidrawImperativeAPI, ExcalidrawProps, LibraryItem, SceneData } from "@excalidraw/excalidraw/types/types.js";
import type { JSX } from "react";
import type React from "react";
const TPL = `
<div class="canvas-widget note-detail-canvas note-detail-printable note-detail">
<style>
@ -54,8 +59,8 @@ const TPL = `
interface CanvasContent {
elements: ExcalidrawElement[],
files: ExcalidrawElement[],
appState: ExcalidrawAppState
files: BinaryFileData[],
appState: Partial<AppState>
}
interface AttachmentMetadata {
@ -114,13 +119,12 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
private currentNoteId: string;
private currentSceneVersion: number;
private libraryChanged: boolean;
private librarycache: ExcalidrawLibrary[];
private librarycache: LibraryItem[];
private attachmentMetadata: AttachmentMetadata[];
private themeStyle!: string;
private excalidrawApi!: ExcalidrawApi;
private excalidrawWrapperRef!: {
current: HTMLElement
};
private themeStyle!: Theme;
private excalidrawLib!: typeof ExcalidrawLib;
private excalidrawApi!: ExcalidrawImperativeAPI;
private excalidrawWrapperRef!: React.RefObject<HTMLElement | null>;
private $render!: JQuery<HTMLElement>;
private reactHandlers!: JQuery<HTMLElement>;
@ -133,7 +137,8 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
this.SCENE_VERSION_ERROR = -2; // -2 indicates error
// ensure that assets are loaded from trilium
window.EXCALIDRAW_ASSET_PATH = `${window.location.origin}/node_modules/@excalidraw/excalidraw/dist/`;
// TODO:
(window as any).EXCALIDRAW_ASSET_PATH = `${window.location.origin}/node_modules/@excalidraw/excalidraw/dist/`;
// temporary vars
this.currentNoteId = "";
@ -169,22 +174,37 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
this.$widget.toggleClass("full-height", true);
this.$render = this.$widget.find(".canvas-render");
const documentStyle = window.getComputedStyle(document.documentElement);
this.themeStyle = documentStyle.getPropertyValue("--theme-style")?.trim();
this.themeStyle = documentStyle.getPropertyValue("--theme-style")?.trim() as Theme;
libraryLoader.requireLibrary(libraryLoader.EXCALIDRAW).then(() => {
const React = window.React;
const ReactDOM = window.ReactDOM;
this.#init();
return this.$widget;
}
async #init() {
const renderElement = this.$render.get(0);
if (!renderElement) {
throw new Error("Unable to find element to render.");
}
ReactDOM.unmountComponentAtNode(renderElement);
const root = ReactDOM.createRoot(renderElement);
root.render(React.createElement(() => this.createExcalidrawReactApp()));
});
// See https://github.com/excalidraw/excalidraw/issues/7899.
if (!window.process) {
(window.process as any) = {};
}
if (!window.process.env) {
window.process.env = {};
}
(window.process.env as any).PREACT = false;
return this.$widget;
const excalidraw = (await import("@excalidraw/excalidraw"));
this.excalidrawLib = excalidraw;
const { unmountComponentAtNode } = await import("react-dom");
const { createRoot } = await import("react-dom/client");
const React = (await import("react")).default;
unmountComponentAtNode(renderElement);
const root = createRoot(renderElement);
root.render(React.createElement(() => this.createExcalidrawReactApp(React, excalidraw.Excalidraw)));
}
/**
@ -215,15 +235,15 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
* newly instantiated?
*/
if (!blob?.content?.trim()) {
const sceneData = {
const sceneData: SceneData = {
elements: [],
appState: {
theme: this.themeStyle
},
collaborators: []
}
};
this.excalidrawApi.updateScene(sceneData);
// TODO: Props mismatch.
this.excalidrawApi.updateScene(sceneData as any);
} else if (blob.content) {
let content: CanvasContent;
@ -240,26 +260,28 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
};
}
const { elements, files, appState = {} } = content;
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: ExcalidrawScene = {
const sceneData: SceneData = {
elements,
appState,
collaborators: []
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 = [];
const fileArray: BinaryFileData[] = [];
for (const fileId in files) {
const file = files[fileId];
// TODO: dataURL is replaceable with a trilium image url
@ -288,7 +310,7 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
}
// Extract libraryItems from the blobs
const libraryItems = results.map((result) => result?.blob?.getJsonContentSafely()).filter((item) => !!item) as ExcalidrawLibrary[];
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);
@ -302,7 +324,8 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
});
// Update the scene
this.excalidrawApi.updateScene(sceneData);
// TODO: Fix type of sceneData
this.excalidrawApi.updateScene(sceneData as any);
this.excalidrawApi.addFiles(fileArray);
this.excalidrawApi.history.clear();
}
@ -328,18 +351,17 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
const files = this.excalidrawApi.getFiles();
// parallel svg export to combat bitrot and enable rendering image for note inclusion, preview, and share
const svg = await window.ExcalidrawLib.exportToSvg({
const svg = await this.excalidrawLib.exportToSvg({
elements,
appState,
exportPadding: 5, // 5 px padding
metadata: "trilium-export",
files
});
const svgString = svg.outerHTML;
const activeFiles: Record<string, ExcalidrawElement> = {};
const activeFiles: Record<string, BinaryFileData> = {};
elements.forEach((element) => {
if (element.fileId) {
if ("fileId" in element && element.fileId) {
activeFiles[element.fileId] = files[element.fileId];
}
});
@ -362,7 +384,12 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
// 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({ merge: true });
const libraryItems = await this.excalidrawApi.updateLibrary({
libraryItems(currentLibraryItems) {
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.
@ -445,33 +472,35 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
}
}
createExcalidrawReactApp() {
const React = window.React;
const { Excalidraw } = window.ExcalidrawLib;
const excalidrawWrapperRef = React.useRef(null);
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({
const [dimensions, setDimensions] = react.useState<{ width?: number, height?: number}>({
width: undefined,
height: undefined
});
React.useEffect(() => {
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") {
return;
}
if (excalidrawWrapperRef.current) {
const dimensions = {
width: excalidrawWrapperRef.current.getBoundingClientRect().width,
height: excalidrawWrapperRef.current.getBoundingClientRect().height
};
setDimensions(dimensions);
}
};
window.addEventListener("resize", onResize);
@ -479,8 +508,11 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
return () => window.removeEventListener("resize", onResize);
}, [excalidrawWrapperRef]);
const onLinkOpen = React.useCallback((element, event) => {
const onLinkOpen = react.useCallback<NonNullable<ExcalidrawProps["onLinkOpen"]>>((element, event) => {
let link = element.link;
if (!link) {
return false;
}
if (link.startsWith("root/")) {
link = "#" + link;
@ -493,25 +525,24 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
return linkService.goToLinkExt(nativeEvent, link, null);
}, []);
return React.createElement(
React.Fragment,
return react.createElement(
react.Fragment,
null,
React.createElement(
react.createElement(
"div",
{
className: "excalidraw-wrapper",
ref: excalidrawWrapperRef
},
React.createElement(Excalidraw, {
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: ExcalidrawApi) => {
excalidrawAPI: (api: ExcalidrawImperativeAPI) => {
this.excalidrawApi = api;
},
width: dimensions.width,
height: dimensions.height,
onPaste: (data: unknown, event: unknown) => {
console.log("Verbose: excalidraw internal paste. No trilium action implemented.", data, event);
return false;
},
onLibraryChange: () => {
this.libraryChanged = true;
@ -528,9 +559,11 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
autoFocus: false,
onLinkOpen,
UIOptions: {
canvasActions: {
saveToActiveFile: false,
saveAsImage: false
}
}
})
)
);
@ -555,7 +588,7 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
getSceneVersion() {
if (this.excalidrawApi) {
const elements = this.excalidrawApi.getSceneElements();
return window.ExcalidrawLib.getSceneVersion(elements);
return this.excalidrawLib.getSceneVersion(elements);
} else {
return this.SCENE_VERSION_ERROR;
}

View File

@ -44,13 +44,6 @@ async function register(app: express.Application) {
app.use(`/assets/vX/stylesheets`, express.static(path.join(srcRoot, "public/stylesheets")));
app.use(`/${assetPath}/libraries`, persistentCacheStatic(path.join(srcRoot, "..", "libraries")));
app.use(`/assets/vX/libraries`, express.static(path.join(srcRoot, "..", "libraries")));
// excalidraw-view mode in shared notes
app.use(`/${assetPath}/node_modules/react/umd/react.production.min.js`, persistentCacheStatic(path.join(srcRoot, "..", "node_modules/react/umd/react.production.min.js")));
app.use(`/${assetPath}/node_modules/react/umd/react.development.js`, persistentCacheStatic(path.join(srcRoot, "..", "node_modules/react/umd/react.development.js")));
app.use(`/${assetPath}/node_modules/react-dom/umd/react-dom.production.min.js`, persistentCacheStatic(path.join(srcRoot, "..", "node_modules/react-dom/umd/react-dom.production.min.js")));
app.use(`/${assetPath}/node_modules/react-dom/umd/react-dom.development.js`, persistentCacheStatic(path.join(srcRoot, "..", "node_modules/react-dom/umd/react-dom.development.js")));
// expose the whole dist folder since complete assets are needed in edit and share
app.use(`/node_modules/@excalidraw/excalidraw/dist/`, express.static(path.join(srcRoot, "..", "node_modules/@excalidraw/excalidraw/dist/")));
app.use(`/${assetPath}/node_modules/@excalidraw/excalidraw/dist/`, persistentCacheStatic(path.join(srcRoot, "..", "node_modules/@excalidraw/excalidraw/dist/")));