From 58a6d70cbb8a3e762294cdcfc8b8953a1fb6d854 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 22 Sep 2025 10:40:57 +0300 Subject: [PATCH] chore(react/type_widget): finalize porting canvas --- apps/client/src/services/link.ts | 2 +- .../src/widgets/type_widgets/Canvas.tsx | 37 +++++- .../src/widgets/type_widgets_old/canvas.ts | 116 ------------------ .../widgets/type_widgets_old/canvas_el.tsx | 93 -------------- 4 files changed, 36 insertions(+), 212 deletions(-) delete mode 100644 apps/client/src/widgets/type_widgets_old/canvas.ts delete mode 100644 apps/client/src/widgets/type_widgets_old/canvas_el.tsx diff --git a/apps/client/src/services/link.ts b/apps/client/src/services/link.ts index 809eb58ef..3628cb70f 100644 --- a/apps/client/src/services/link.ts +++ b/apps/client/src/services/link.ts @@ -289,7 +289,7 @@ function goToLink(evt: MouseEvent | JQuery.ClickEvent | JQuery.MouseDownEvent) { * @param $link the jQuery element of the link that was clicked, used to determine if the link is an anchor link (e.g., `#fn1` or `#fnref1`) and to handle it accordingly. * @returns `true` if the link was handled (i.e., the element was found and scrolled to), or a falsy value otherwise. */ -function goToLinkExt(evt: MouseEvent | JQuery.ClickEvent | JQuery.MouseDownEvent | React.PointerEvent | null, hrefLink: string | undefined, $link?: JQuery | null) { +export function goToLinkExt(evt: MouseEvent | JQuery.ClickEvent | JQuery.MouseDownEvent | React.PointerEvent | null, hrefLink: string | undefined, $link?: JQuery | null) { if (hrefLink?.startsWith("data:")) { return true; } diff --git a/apps/client/src/widgets/type_widgets/Canvas.tsx b/apps/client/src/widgets/type_widgets/Canvas.tsx index 349b965be..c24516455 100644 --- a/apps/client/src/widgets/type_widgets/Canvas.tsx +++ b/apps/client/src/widgets/type_widgets/Canvas.tsx @@ -2,7 +2,7 @@ import { Excalidraw, exportToSvg, getSceneVersion } from "@excalidraw/excalidraw import { TypeWidgetProps } from "./type_widget"; import "@excalidraw/excalidraw/index.css"; import { useEditorSpacedUpdate } from "../react/hooks"; -import { useMemo, useRef } from "preact/hooks"; +import { useCallback, useMemo, useRef } from "preact/hooks"; import { type ExcalidrawImperativeAPI, type AppState, type BinaryFileData, LibraryItem, ExcalidrawProps } from "@excalidraw/excalidraw/types"; import options from "../../services/options"; import "./Canvas.css"; @@ -11,6 +11,7 @@ import { RefObject } from "preact"; import server from "../../services/server"; import { NonDeletedExcalidrawElement } from "@excalidraw/excalidraw/element/types"; import { CanvasContent } from "../type_widgets_old/canvas_el"; +import { goToLinkExt } from "../../services/link"; // currently required by excalidraw, in order to allows self-hosting fonts locally. // this avoids making excalidraw load the fonts from an external CDN. @@ -30,8 +31,31 @@ export default function Canvas({ note }: TypeWidgetProps) { }, []); const persistence = usePersistence(note, apiRef, themeStyle, isReadOnly); + /** Use excalidraw's native zoom instead of the global zoom. */ + const onWheel = useCallback((e: MouseEvent) => { + if (e.ctrlKey) { + e.preventDefault(); + e.stopPropagation(); + } + }, []); + + const onLinkOpen = useCallback((element: NonDeletedExcalidrawElement, event: CustomEvent) => { + let link = element.link; + if (!link) { + return false; + } + + if (link.startsWith("root/")) { + link = "#" + link; + } + + const { nativeEvent } = event.detail; + event.preventDefault(); + return goToLinkExt(nativeEvent, link, null); + }, []); + return ( -
+
@@ -59,6 +84,14 @@ export default function Canvas({ note }: TypeWidgetProps) { function usePersistence(note: FNote, apiRef: RefObject, theme: AppState["theme"], isReadOnly: boolean): Partial { const libraryChanged = useRef(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 + */ 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. diff --git a/apps/client/src/widgets/type_widgets_old/canvas.ts b/apps/client/src/widgets/type_widgets_old/canvas.ts deleted file mode 100644 index 442d72693..000000000 --- a/apps/client/src/widgets/type_widgets_old/canvas.ts +++ /dev/null @@ -1,116 +0,0 @@ -import TypeWidget from "./type_widget.js"; -import server from "../../services/server.js"; -import type FNote from "../../entities/fnote.js"; -import options from "../../services/options.js"; -import type { LibraryItem } from "@excalidraw/excalidraw/types"; -import type Canvas from "./canvas_el.js"; -import { CanvasContent } from "./canvas_el.js"; -import { renderReactWidget } from "../react/react_utils.jsx"; -import SpacedUpdate from "../../services/spaced_update.js"; -import protected_session_holder from "../../services/protected_session_holder.js"; - -/** - * # Canvas note with excalidraw - * @author thfrei 2022-05-11 - * - * Background: - * excalidraw gives great support for hand-drawn notes. It also allows including images and support - * for sketching. Excalidraw has a vibrant and active community. - * - * Functionality: - * We store the excalidraw assets (elements and files) in the note. In addition to that, we - * export the SVG from the canvas on every update and store it in the note's attachment. It is used when - * calling api/images and makes referencing very easy. - * - * Paths not taken. - * - excalidraw-to-svg (node.js) could be used to avoid storing the svg in the backend. - * We could render the SVG on the fly. However, as of now, it does not render any hand drawn - * (freedraw) paths. There is an issue with Path2D object not present in the node-canvas library - * used by jsdom. (See Trilium PR for samples and other issues in the respective library. - * Link will be added later). Related links: - * - https://github.com/Automattic/node-canvas/pull/2013 - * - https://github.com/google/canvas-5-polyfill - * - https://github.com/Automattic/node-canvas/issues/1116 - * - https://www.npmjs.com/package/path2d-polyfill - * - excalidraw-to-svg (node.js) takes quite some time to load an image (1-2s) - * - excalidraw-utils (browser) does render freedraw, however NOT freedraw with a background. It is not - * used, since it is a big dependency, and has the same functionality as react + excalidraw. - * - infinite-drawing-canvas with fabric.js. This library lacked a lot of features, excalidraw already - * has. - * - * Known issues: - * - the 3 excalidraw fonts should be included in the share and everywhere, so that it is shown - * when requiring svg. - * - * Discussion of storing svg in the note attachment: - * - Pro: we will combat bit-rot. Showing the SVG will be very fast and easy, since it is already there. - * - Con: The note will get bigger (~40-50%?), we will generate more bandwidth. However, using trilium - * desktop instance mitigates that issue. - * - * Roadmap: - * - Support image-notes as reference in excalidraw - * - Support canvas note as reference (svg) in other canvas notes. - * - Make it easy to include a canvas note inside a text note - */ -export default class ExcalidrawTypeWidget extends TypeWidget { - - private currentNoteId: string; - - private libraryChanged: boolean; - private librarycache: LibraryItem[]; - private attachmentMetadata: AttachmentMetadata[]; - private themeStyle!: Theme; - - private $render!: JQuery; - private reactHandlers!: JQuery; - private canvasInstance!: Canvas; - - constructor() { - super(); - - // temporary vars - this.currentNoteId = ""; - - // will be overwritten - this.$render; - this.$widget; - this.reactHandlers; // used to control react state - - // 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.spacedUpdate = new SpacedUpdate(async () => { - if (!this.noteContext) return; - - const { note } = this.noteContext; - if (!note) return; - - const { noteId } = note; - const data = await this.getData(); - - // for read only notes - if (data === undefined) return; - - protected_session_holder.touchProtectedSessionIfNecessary(note); - await server.put(`notes/${noteId}/data`, data, this.componentId); - this.dataSaved(); - }); - } - - doRender() { - this.$widget = $(TPL); - this.$widget.bind("mousewheel DOMMouseScroll", (event) => { - if (event.ctrlKey) { - event.preventDefault(); - event.stopPropagation(); - return false; - } - }); - - this.$render = this.$widget.find(".canvas-render"); - - this.#init(); - - return this.$widget; - } - -} diff --git a/apps/client/src/widgets/type_widgets_old/canvas_el.tsx b/apps/client/src/widgets/type_widgets_old/canvas_el.tsx deleted file mode 100644 index 410269261..000000000 --- a/apps/client/src/widgets/type_widgets_old/canvas_el.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import { Excalidraw, getSceneVersion, exportToSvg } from "@excalidraw/excalidraw"; -import { AppState, BinaryFileData, ExcalidrawImperativeAPI, ExcalidrawProps, LibraryItem } from "@excalidraw/excalidraw/types"; -import { ExcalidrawElement, NonDeletedExcalidrawElement, Theme } from "@excalidraw/excalidraw/element/types"; -import { useCallback } from "preact/hooks"; -import linkService from "../../services/link.js"; - -export interface CanvasContent { - elements: ExcalidrawElement[]; - files: BinaryFileData[]; - appState: Partial; -} - -/** 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; - - constructor(opts: ExcalidrawProps) { - this.opts = opts; - this.currentSceneVersion = SCENE_VERSION_INITIAL; - this.initializedPromise = $.Deferred(); - } - - createCanvasElement() { - return - } - - /** - * 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; - } - - isInitialized() { - return !!this.excalidrawApi; - } - -} - -function CanvasElement(opts: ExcalidrawProps) { - return ( - { - 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); - }, [])} - /> - ); -}