mirror of
https://github.com/TriliumNext/Trilium.git
synced 2025-12-10 18:45:52 -06:00
chore(react/type_widget): finalize porting canvas
This commit is contained in:
parent
44b92a024c
commit
58a6d70cbb
@ -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.
|
* @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.
|
* @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<HTMLCanvasElement> | null, hrefLink: string | undefined, $link?: JQuery<HTMLElement> | null) {
|
export function goToLinkExt(evt: MouseEvent | JQuery.ClickEvent | JQuery.MouseDownEvent | React.PointerEvent<HTMLCanvasElement> | null, hrefLink: string | undefined, $link?: JQuery<HTMLElement> | null) {
|
||||||
if (hrefLink?.startsWith("data:")) {
|
if (hrefLink?.startsWith("data:")) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { Excalidraw, exportToSvg, getSceneVersion } from "@excalidraw/excalidraw
|
|||||||
import { TypeWidgetProps } from "./type_widget";
|
import { TypeWidgetProps } from "./type_widget";
|
||||||
import "@excalidraw/excalidraw/index.css";
|
import "@excalidraw/excalidraw/index.css";
|
||||||
import { useEditorSpacedUpdate } from "../react/hooks";
|
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 { type ExcalidrawImperativeAPI, type AppState, type BinaryFileData, LibraryItem, ExcalidrawProps } from "@excalidraw/excalidraw/types";
|
||||||
import options from "../../services/options";
|
import options from "../../services/options";
|
||||||
import "./Canvas.css";
|
import "./Canvas.css";
|
||||||
@ -11,6 +11,7 @@ import { RefObject } from "preact";
|
|||||||
import server from "../../services/server";
|
import server from "../../services/server";
|
||||||
import { NonDeletedExcalidrawElement } from "@excalidraw/excalidraw/element/types";
|
import { NonDeletedExcalidrawElement } from "@excalidraw/excalidraw/element/types";
|
||||||
import { CanvasContent } from "../type_widgets_old/canvas_el";
|
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.
|
// currently required by excalidraw, in order to allows self-hosting fonts locally.
|
||||||
// this avoids making excalidraw load the fonts from an external CDN.
|
// 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);
|
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 (
|
return (
|
||||||
<div className="canvas-widget note-detail-canvas note-detail-printable note-detail full-height">
|
<div className="canvas-widget note-detail-canvas note-detail-printable note-detail full-height" onWheel={onWheel}>
|
||||||
<div className="canvas-render">
|
<div className="canvas-render">
|
||||||
<div className="excalidraw-wrapper">
|
<div className="excalidraw-wrapper">
|
||||||
<Excalidraw
|
<Excalidraw
|
||||||
@ -49,6 +73,7 @@ export default function Canvas({ note }: TypeWidgetProps) {
|
|||||||
export: false
|
export: false
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
onLinkOpen={onLinkOpen}
|
||||||
{...persistence}
|
{...persistence}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -59,6 +84,14 @@ export default function Canvas({ note }: TypeWidgetProps) {
|
|||||||
|
|
||||||
function usePersistence(note: FNote, apiRef: RefObject<ExcalidrawImperativeAPI>, theme: AppState["theme"], isReadOnly: boolean): Partial<ExcalidrawProps> {
|
function usePersistence(note: FNote, apiRef: RefObject<ExcalidrawImperativeAPI>, theme: AppState["theme"], isReadOnly: boolean): Partial<ExcalidrawProps> {
|
||||||
const libraryChanged = useRef(false);
|
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);
|
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.
|
// 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.
|
||||||
|
|||||||
@ -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<HTMLElement>;
|
|
||||||
private reactHandlers!: JQuery<HTMLElement>;
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -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<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;
|
|
||||||
|
|
||||||
constructor(opts: ExcalidrawProps) {
|
|
||||||
this.opts = opts;
|
|
||||||
this.currentSceneVersion = SCENE_VERSION_INITIAL;
|
|
||||||
this.initializedPromise = $.Deferred();
|
|
||||||
}
|
|
||||||
|
|
||||||
createCanvasElement() {
|
|
||||||
return <CanvasElement
|
|
||||||
{...this.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;
|
|
||||||
}
|
|
||||||
|
|
||||||
isInitialized() {
|
|
||||||
return !!this.excalidrawApi;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
function CanvasElement(opts: ExcalidrawProps) {
|
|
||||||
return (
|
|
||||||
<Excalidraw
|
|
||||||
{...opts}
|
|
||||||
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 linkService.goToLinkExt(nativeEvent, link, null);
|
|
||||||
}, [])}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Loading…
x
Reference in New Issue
Block a user