mirror of
				https://github.com/TriliumNext/Notes.git
				synced 2025-10-31 13:01:31 +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", |     "mind-elixir": "4.6.0", | ||||||
|     "normalize.css": "8.0.1", |     "normalize.css": "8.0.1", | ||||||
|     "panzoom": "9.4.3", |     "panzoom": "9.4.3", | ||||||
|     "react": "19.1.0", |     "preact": "10.26.8", | ||||||
|     "react-dom": "19.1.0", |  | ||||||
|     "split.js": "1.6.5", |     "split.js": "1.6.5", | ||||||
|     "svg-pan-zoom": "3.6.2", |     "svg-pan-zoom": "3.6.2", | ||||||
|     "vanilla-js-wheel-zoom": "9.0.4" |     "vanilla-js-wheel-zoom": "9.0.4" | ||||||
| @ -64,8 +63,6 @@ | |||||||
|     "@types/leaflet": "1.9.18", |     "@types/leaflet": "1.9.18", | ||||||
|     "@types/leaflet-gpx": "1.3.7", |     "@types/leaflet-gpx": "1.3.7", | ||||||
|     "@types/mark.js": "8.11.12", |     "@types/mark.js": "8.11.12", | ||||||
|     "@types/react": "19.1.7", |  | ||||||
|     "@types/react-dom": "19.1.6", |  | ||||||
|     "copy-webpack-plugin": "13.0.0", |     "copy-webpack-plugin": "13.0.0", | ||||||
|     "happy-dom": "18.0.1", |     "happy-dom": "18.0.1", | ||||||
|     "script-loader": "0.7.2", |     "script-loader": "0.7.2", | ||||||
| @ -75,7 +72,9 @@ | |||||||
|     "name": "client", |     "name": "client", | ||||||
|     "targets": { |     "targets": { | ||||||
|       "serve": { |       "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; |     export default path; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| declare module "@triliumnext/ckeditor5/emoji_definitions/en.json?url" { | declare module "*?url" { | ||||||
|     var path: string; |     var path: string; | ||||||
|     export default path; |     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; |         process?: ElectronProcess; | ||||||
|         glob?: CustomGlobals; |         glob?: CustomGlobals; | ||||||
|  | 
 | ||||||
|  |         EXCALIDRAW_ASSET_PATH?: string; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     interface AutoCompleteConfig { |     interface AutoCompleteConfig { | ||||||
|  | |||||||
| @ -1,16 +1,11 @@ | |||||||
| import TypeWidget from "./type_widget.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 server from "../../services/server.js"; | ||||||
| import type FNote from "../../entities/fnote.js"; | import type FNote from "../../entities/fnote.js"; | ||||||
| import options from "../../services/options.js"; | import options from "../../services/options.js"; | ||||||
| import type { ExcalidrawElement, Theme } from "@excalidraw/excalidraw/element/types"; | import type { LibraryItem } from "@excalidraw/excalidraw/types"; | ||||||
| import type { AppState, BinaryFileData, ExcalidrawImperativeAPI, ExcalidrawProps, LibraryItem, SceneData } from "@excalidraw/excalidraw/types"; | import type { Theme } from "@excalidraw/excalidraw/element/types"; | ||||||
| import type { JSX } from "react"; | import type Canvas from "./canvas_el.js"; | ||||||
| import type React from "react"; | import { CanvasContent } from "./canvas_el.js"; | ||||||
| import type { Root } from "react-dom/client"; |  | ||||||
| import "@excalidraw/excalidraw/index.css"; |  | ||||||
| import asset_path from "../../asset_path.js"; |  | ||||||
| 
 | 
 | ||||||
| const TPL = /*html*/` | const TPL = /*html*/` | ||||||
|     <div class="canvas-widget note-detail-canvas note-detail-printable note-detail"> |     <div class="canvas-widget note-detail-canvas note-detail-printable note-detail"> | ||||||
| @ -28,6 +23,7 @@ const TPL = /*html*/` | |||||||
| 
 | 
 | ||||||
|         .excalidraw-wrapper { |         .excalidraw-wrapper { | ||||||
|             height: 100%; |             height: 100%; | ||||||
|  |         } | ||||||
| 
 | 
 | ||||||
|         :root[dir="ltr"] |         :root[dir="ltr"] | ||||||
|         .excalidraw |         .excalidraw | ||||||
| @ -51,11 +47,7 @@ const TPL = /*html*/` | |||||||
|     </div> |     </div> | ||||||
| `;
 | `;
 | ||||||
| 
 | 
 | ||||||
| interface CanvasContent { | 
 | ||||||
|     elements: ExcalidrawElement[]; |  | ||||||
|     files: BinaryFileData[]; |  | ||||||
|     appState: Partial<AppState>; |  | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
| interface AttachmentMetadata { | interface AttachmentMetadata { | ||||||
|     title: string; |     title: string; | ||||||
| @ -107,37 +99,22 @@ interface AttachmentMetadata { | |||||||
|  */ |  */ | ||||||
| export default class ExcalidrawTypeWidget extends TypeWidget { | export default class ExcalidrawTypeWidget extends TypeWidget { | ||||||
| 
 | 
 | ||||||
|     private readonly SCENE_VERSION_INITIAL: number; |  | ||||||
|     private readonly SCENE_VERSION_ERROR: number; |  | ||||||
| 
 |  | ||||||
|     private currentNoteId: string; |     private currentNoteId: string; | ||||||
|     private currentSceneVersion: number; | 
 | ||||||
|     private libraryChanged: boolean; |     private libraryChanged: boolean; | ||||||
|     private librarycache: LibraryItem[]; |     private librarycache: LibraryItem[]; | ||||||
|     private attachmentMetadata: AttachmentMetadata[]; |     private attachmentMetadata: AttachmentMetadata[]; | ||||||
|     private themeStyle!: Theme; |     private themeStyle!: Theme; | ||||||
|     private excalidrawLib!: typeof import("@excalidraw/excalidraw"); |  | ||||||
|     private excalidrawApi!: ExcalidrawImperativeAPI; |  | ||||||
|     private excalidrawWrapperRef!: React.RefObject<HTMLElement | null>; |  | ||||||
| 
 | 
 | ||||||
|     private $render!: JQuery<HTMLElement>; |     private $render!: JQuery<HTMLElement>; | ||||||
|     private root?: Root; |  | ||||||
|     private reactHandlers!: JQuery<HTMLElement>; |     private reactHandlers!: JQuery<HTMLElement>; | ||||||
|  |     private canvasInstance!: Canvas; | ||||||
| 
 | 
 | ||||||
|     constructor() { |     constructor() { | ||||||
|         super(); |         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
 |         // temporary vars
 | ||||||
|         this.currentNoteId = ""; |         this.currentNoteId = ""; | ||||||
|         this.currentSceneVersion = this.SCENE_VERSION_INITIAL; |  | ||||||
| 
 | 
 | ||||||
|         // will be overwritten
 |         // will be overwritten
 | ||||||
|         this.$render; |         this.$render; | ||||||
| @ -182,34 +159,48 @@ export default class ExcalidrawTypeWidget extends TypeWidget { | |||||||
|             throw new Error("Unable to find element to render."); |             throw new Error("Unable to find element to render."); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         // See https://github.com/excalidraw/excalidraw/issues/7899.
 |         const Canvas = (await import("./canvas_el.js")).default; | ||||||
|         if (!window.process) { |         this.canvasInstance = new Canvas({ | ||||||
|             (window.process as any) = {}; |             // 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 = {}; |             onLibraryChange: () => { | ||||||
|         } |                 this.libraryChanged = true; | ||||||
|         (window.process.env as any).PREACT = false; |  | ||||||
| 
 | 
 | ||||||
|         const excalidraw = await import("@excalidraw/excalidraw"); |                 this.saveData(); | ||||||
|         this.excalidrawLib = excalidraw; |             }, | ||||||
|  |         }); | ||||||
| 
 | 
 | ||||||
|         const { createRoot } = await import("react-dom/client"); |         await setupFonts(); | ||||||
|         const React = (await import("react")).default; |         this.canvasInstance.renderCanvas(renderElement); | ||||||
|         this.root?.unmount(); |  | ||||||
|         this.root = createRoot(renderElement); |  | ||||||
|         this.root.render(React.createElement(() => this.createExcalidrawReactApp(React, excalidraw.Excalidraw))); |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * called to populate the widget container with the note content |      * called to populate the widget container with the note content | ||||||
|      */ |      */ | ||||||
|     async doRefresh(note: FNote) { |     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
 |         // see if the note changed, since we do not get a new class for a new note
 | ||||||
|         const noteChanged = this.currentNoteId !== note.noteId; |         const noteChanged = this.currentNoteId !== note.noteId; | ||||||
|         if (noteChanged) { |         if (noteChanged) { | ||||||
|             // reset the scene to omit unnecessary onchange handler
 |             // reset the scene to omit unnecessary onchange handler
 | ||||||
|             this.currentSceneVersion = this.SCENE_VERSION_INITIAL; |             this.canvasInstance.resetSceneVersion(); | ||||||
|         } |         } | ||||||
|         this.currentNoteId = note.noteId; |         this.currentNoteId = note.noteId; | ||||||
| 
 | 
 | ||||||
| @ -217,10 +208,7 @@ export default class ExcalidrawTypeWidget extends TypeWidget { | |||||||
|         const blob = await note.getBlob(); |         const blob = await note.getBlob(); | ||||||
| 
 | 
 | ||||||
|         // before we load content into excalidraw, make sure excalidraw has loaded
 |         // before we load content into excalidraw, make sure excalidraw has loaded
 | ||||||
|         while (!this.excalidrawApi) { |         await this.canvasInstance.waitForApiToBecomeAvailable(); | ||||||
|             console.log("excalidrawApi not yet loaded, sleep 200ms..."); |  | ||||||
|             await utils.sleep(200); |  | ||||||
|         } |  | ||||||
| 
 | 
 | ||||||
|         /** |         /** | ||||||
|          * new and empty note - make sure that canvas is empty. |          * new and empty note - make sure that canvas is empty. | ||||||
| @ -229,15 +217,7 @@ export default class ExcalidrawTypeWidget extends TypeWidget { | |||||||
|          * newly instantiated? |          * newly instantiated? | ||||||
|          */ |          */ | ||||||
|         if (!blob?.content?.trim()) { |         if (!blob?.content?.trim()) { | ||||||
|             const sceneData: SceneData = { |             this.canvasInstance.resetScene(this.themeStyle); | ||||||
|                 elements: [], |  | ||||||
|                 appState: { |  | ||||||
|                     theme: this.themeStyle |  | ||||||
|                 } |  | ||||||
|             }; |  | ||||||
| 
 |  | ||||||
|             // TODO: Props mismatch.
 |  | ||||||
|             this.excalidrawApi.updateScene(sceneData as any); |  | ||||||
|         } else if (blob.content) { |         } else if (blob.content) { | ||||||
|             let content: CanvasContent; |             let content: CanvasContent; | ||||||
| 
 | 
 | ||||||
| @ -254,36 +234,7 @@ export default class ExcalidrawTypeWidget extends TypeWidget { | |||||||
|                 }; |                 }; | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             const { elements, files } = content; |             this.canvasInstance.loadData(content, this.themeStyle); | ||||||
|             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); |  | ||||||
|             } |  | ||||||
| 
 | 
 | ||||||
|             Promise.all( |             Promise.all( | ||||||
|                 (await note.getAttachmentsByRole("canvasLibraryItem")).map(async (attachment) => { |                 (await note.getAttachmentsByRole("canvasLibraryItem")).map(async (attachment) => { | ||||||
| @ -310,23 +261,19 @@ export default class ExcalidrawTypeWidget extends TypeWidget { | |||||||
|                 const metadata = results.map((result) => result.metadata); |                 const metadata = results.map((result) => result.metadata); | ||||||
| 
 | 
 | ||||||
|                 // Update the library and save to independent variables
 |                 // 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.
 |                 // save state of library to compare it to the new state later.
 | ||||||
|                 this.librarycache = libraryItems; |                 this.librarycache = libraryItems; | ||||||
|                 this.attachmentMetadata = metadata; |                 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
 |         // set initial scene version
 | ||||||
|         if (this.currentSceneVersion === this.SCENE_VERSION_INITIAL) { |         if (this.canvasInstance.isInitialScene()) { | ||||||
|             this.currentSceneVersion = this.getSceneVersion(); |             this.canvasInstance.updateSceneVersion(); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| @ -335,56 +282,14 @@ export default class ExcalidrawTypeWidget extends TypeWidget { | |||||||
|      * this is automatically called after this.saveData(); |      * this is automatically called after this.saveData(); | ||||||
|      */ |      */ | ||||||
|     async getData() { |     async getData() { | ||||||
|         const elements = this.excalidrawApi.getSceneElements(); |         const { content, svg } = await this.canvasInstance.getData(); | ||||||
|         const appState = this.excalidrawApi.getAppState(); |         const attachments = [{ role: "image", title: "canvas-export.svg", mime: "image/svg+xml", content: svg, position: 0 }]; | ||||||
| 
 |  | ||||||
|         /** |  | ||||||
|          * 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 }]; |  | ||||||
| 
 | 
 | ||||||
|         if (this.libraryChanged) { |         if (this.libraryChanged) { | ||||||
|             // this.libraryChanged is unset in dataSaved()
 |             // this.libraryChanged is unset in dataSaved()
 | ||||||
| 
 | 
 | ||||||
|             // there's no separate method to get library items, so have to abuse this one
 |             // there's no separate method to get library items, so have to abuse this one
 | ||||||
|             const libraryItems = await this.excalidrawApi.updateLibrary({ |             const libraryItems = await this.canvasInstance.getLibraryItems(); | ||||||
|                 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.
 |             // 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.
 |             //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.
 |         // 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.
 |         // 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 |          * 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 |          *        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
 |         // 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; |         const shouldSave = isNewSceneVersion && isNotInitialScene; | ||||||
| 
 | 
 | ||||||
|         if (shouldSave) { |         if (shouldSave) { | ||||||
|             this.updateSceneVersion(); |             this.canvasInstance.updateSceneVersion(); | ||||||
|             this.saveData(); |             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(() => { | async function setupFonts() { | ||||||
|             if (excalidrawWrapperRef.current) { |     if (window.EXCALIDRAW_ASSET_PATH) { | ||||||
|                 const dimensions = { |  | ||||||
|                     width: excalidrawWrapperRef.current.getBoundingClientRect().width, |  | ||||||
|                     height: excalidrawWrapperRef.current.getBoundingClientRect().height |  | ||||||
|                 }; |  | ||||||
|                 setDimensions(dimensions); |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             const onResize = () => { |  | ||||||
|                 if (this.note?.type !== "canvas") { |  | ||||||
|         return; |         return; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|                 if (excalidrawWrapperRef.current) { |     // currently required by excalidraw, in order to allows self-hosting fonts locally.
 | ||||||
|                     const dimensions = { |     // this avoids making excalidraw load the fonts from an external CDN.
 | ||||||
|                         width: excalidrawWrapperRef.current.getBoundingClientRect().width, |     let path: string; | ||||||
|                         height: excalidrawWrapperRef.current.getBoundingClientRect().height |     if (!glob.isDev) { | ||||||
|                     }; |         path = `${window.location.pathname}/node_modules/@excalidraw/excalidraw/dist/prod`; | ||||||
|                     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); |  | ||||||
|     } else { |     } 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() { |     window.EXCALIDRAW_ASSET_PATH = path; | ||||||
|         this.currentSceneVersion = this.getSceneVersion(); |  | ||||||
|     } |  | ||||||
| } | } | ||||||
|  | |||||||
							
								
								
									
										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", |                 find: "@triliumnext/highlightjs", | ||||||
|                 replacement: resolve(__dirname, "node_modules/@triliumnext/highlightjs/dist") |                 replacement: resolve(__dirname, "node_modules/@triliumnext/highlightjs/dist") | ||||||
|  |             }, | ||||||
|  |             { | ||||||
|  |                 find: "react", | ||||||
|  |                 replacement: "preact/compat" | ||||||
|  |             }, | ||||||
|  |             { | ||||||
|  |                 find: "react-dom", | ||||||
|  |                 replacement: "preact/compat" | ||||||
|             } |             } | ||||||
|         ], |         ], | ||||||
|         dedupe: [ |         dedupe: [ | ||||||
|             "react", |             "react", | ||||||
|             "react-dom" |             "react-dom", | ||||||
|  |             "preact", | ||||||
|  |             "preact/compat", | ||||||
|  |             "preact/hooks" | ||||||
|         ] |         ] | ||||||
|     }, |     }, | ||||||
|     // Uncomment this if you are using workers.
 |     // Uncomment this if you are using workers.
 | ||||||
| @ -97,5 +108,8 @@ export default defineConfig(() => ({ | |||||||
|     }, |     }, | ||||||
|     commonjsOptions: { |     commonjsOptions: { | ||||||
|         transformMixedEsModules: true, |         transformMixedEsModules: true, | ||||||
|  |     }, | ||||||
|  |     define: { | ||||||
|  |         "process.env.IS_PREACT": JSON.stringify("true"), | ||||||
|     } |     } | ||||||
| })); | })); | ||||||
|  | |||||||
| @ -92,6 +92,9 @@ | |||||||
|     }, |     }, | ||||||
|     "overrides": { |     "overrides": { | ||||||
|       "node-abi": "4.9.0", |       "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", |       "@types/express-serve-static-core": "5.0.6", | ||||||
|       "flat@<5.0.1": ">=5.0.1", |       "flat@<5.0.1": ">=5.0.1", | ||||||
|       "debug@>=3.2.0 <3.2.7": ">=3.2.7", |       "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
	 Elian Doran
						Elian Doran