chore(canvas): bring back scene API

This commit is contained in:
Elian Doran 2025-06-10 23:09:36 +03:00
parent 5ad3d7d077
commit dd58685455
No known key found for this signature in database
2 changed files with 83 additions and 67 deletions

View File

@ -4,6 +4,7 @@ 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 { ExcalidrawImperativeAPI } from "@excalidraw/excalidraw/types"; import { ExcalidrawImperativeAPI } from "@excalidraw/excalidraw/types";
import type Canvas from "./canvas_el.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">
@ -101,11 +102,8 @@ 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[];
@ -116,21 +114,17 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
private $render!: JQuery<HTMLElement>; private $render!: JQuery<HTMLElement>;
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. // 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.
(window as any).EXCALIDRAW_ASSET_PATH = `${window.location.pathname}/node_modules/@excalidraw/excalidraw/dist/prod`; (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;
@ -184,12 +178,11 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
} }
(window.process.env as any).PREACT = false; (window.process.env as any).PREACT = false;
const renderCanvas = (await import("./canvas_el.js")).default; const Canvas = (await import("./canvas_el.js")).default;
renderCanvas(renderElement, { this.canvasInstance = new Canvas({
excalidrawAPI: (api: ExcalidrawImperativeAPI) => {
this.excalidrawApi = api;
},
}); });
this.canvasInstance.renderCanvas(renderElement);
} }
/** /**
@ -200,7 +193,7 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
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;
@ -208,7 +201,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) { while (!this.canvasInstance?.excalidrawApi) {
console.log("excalidrawApi not yet loaded, sleep 200ms..."); console.log("excalidrawApi not yet loaded, sleep 200ms...");
await utils.sleep(200); await utils.sleep(200);
} }
@ -228,7 +221,7 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
}; };
// TODO: Props mismatch. // TODO: Props mismatch.
this.excalidrawApi.updateScene(sceneData as any); this.canvasInstance.excalidrawApi.updateScene(sceneData as any);
} else if (blob.content) { } else if (blob.content) {
let content: CanvasContent; let content: CanvasContent;
@ -301,7 +294,7 @@ 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.excalidrawApi.updateLibrary({ libraryItems, merge: false });
// 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;
@ -310,14 +303,14 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
// Update the scene // Update the scene
// TODO: Fix type of sceneData // TODO: Fix type of sceneData
this.excalidrawApi.updateScene(sceneData as any); this.canvasInstance.excalidrawApi.updateScene(sceneData as any);
this.excalidrawApi.addFiles(fileArray); this.canvasInstance.excalidrawApi.addFiles(fileArray);
this.excalidrawApi.history.clear(); this.canvasInstance.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();
} }
} }
@ -326,14 +319,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 elements = this.canvasInstance.excalidrawApi.getSceneElements();
const appState = this.excalidrawApi.getAppState(); const appState = this.canvasInstance.excalidrawApi.getAppState();
/** /**
* A file is not deleted, even though removed from canvas. Therefore, we only keep * 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? * files that are referenced by an element. Maybe this will change with a new excalidraw version?
*/ */
const files = this.excalidrawApi.getFiles(); const files = this.canvasInstance.excalidrawApi.getFiles();
// parallel svg export to combat bitrot and enable rendering image for note inclusion, preview, and share // parallel svg export to combat bitrot and enable rendering image for note inclusion, preview, and share
const svg = await this.excalidrawLib.exportToSvg({ const svg = await this.excalidrawLib.exportToSvg({
@ -370,7 +363,7 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
// 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.excalidrawApi.updateLibrary({
libraryItems() { libraryItems() {
return []; return [];
}, },
@ -444,53 +437,20 @@ 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();
} }
} }
/**
* 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 {
return this.SCENE_VERSION_ERROR;
}
}
updateSceneVersion() {
this.currentSceneVersion = this.getSceneVersion();
}
} }

View File

@ -1,10 +1,66 @@
import "@excalidraw/excalidraw/index.css"; import "@excalidraw/excalidraw/index.css";
import { Excalidraw } from "@excalidraw/excalidraw"; import { Excalidraw, getSceneVersion, exportToSvg } from "@excalidraw/excalidraw";
import { createElement, createRef, Fragment, render } from "preact/compat"; import { createElement, createRef, Fragment, render } from "preact/compat";
import { ExcalidrawProps } from "@excalidraw/excalidraw/types"; import { ExcalidrawImperativeAPI, ExcalidrawProps } from "@excalidraw/excalidraw/types";
/** -1 indicates that it is fresh. excalidraw scene version is always >0 */
const SCENE_VERSION_INITIAL = -1;
/** -2 indicates error */
const SCENE_VERSION_ERROR = -2;
export default class Canvas {
private currentSceneVersion: number;
private opts: ExcalidrawProps;
excalidrawApi!: ExcalidrawImperativeAPI;
constructor(opts: ExcalidrawProps) {
this.opts = opts;
this.currentSceneVersion = SCENE_VERSION_INITIAL;
}
renderCanvas(targetEl: HTMLElement) {
render(createCanvasElement({
...this.opts,
excalidrawAPI: (api: ExcalidrawImperativeAPI) => {
this.excalidrawApi = api;
},
}), targetEl);
}
/**
* 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;
}
export default function renderCanvas(targetEl: HTMLElement, opts: ExcalidrawProps) {
render(createCanvasElement(opts), targetEl);
} }
function createCanvasElement(opts: ExcalidrawProps) { function createCanvasElement(opts: ExcalidrawProps) {