mirror of
https://github.com/zadam/trilium.git
synced 2025-11-11 17:08:58 +01: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.
|
||||
* @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:")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -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 (
|
||||
<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="excalidraw-wrapper">
|
||||
<Excalidraw
|
||||
@ -49,6 +73,7 @@ export default function Canvas({ note }: TypeWidgetProps) {
|
||||
export: false
|
||||
}
|
||||
}}
|
||||
onLinkOpen={onLinkOpen}
|
||||
{...persistence}
|
||||
/>
|
||||
</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> {
|
||||
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.
|
||||
|
||||
@ -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