chore(client/ts): port canvas

This commit is contained in:
Elian Doran 2025-01-18 00:42:19 +02:00
parent 2167948509
commit 7c7fd044c6
No known key found for this signature in database
3 changed files with 112 additions and 15 deletions

View File

@ -234,7 +234,7 @@ function goToLink(evt: MouseEvent | JQuery.ClickEvent) {
return goToLinkExt(evt, hrefLink, $link); return goToLinkExt(evt, hrefLink, $link);
} }
function goToLinkExt(evt: MouseEvent | JQuery.ClickEvent, hrefLink: string | undefined, $link: JQuery<HTMLElement>) { function goToLinkExt(evt: MouseEvent | JQuery.ClickEvent, hrefLink: string | undefined, $link: JQuery<HTMLElement> | null) {
if (hrefLink?.startsWith("data:")) { if (hrefLink?.startsWith("data:")) {
return true; return true;
} }
@ -242,7 +242,7 @@ function goToLinkExt(evt: MouseEvent | JQuery.ClickEvent, hrefLink: string | und
evt.preventDefault(); evt.preventDefault();
evt.stopPropagation(); evt.stopPropagation();
if (hrefLink?.startsWith("#fn")) { if (hrefLink?.startsWith("#fn") && $link) {
return handleFootnote(hrefLink, $link); return handleFootnote(hrefLink, $link);
} }

View File

@ -54,6 +54,72 @@ declare global {
process?: ElectronProcess; process?: ElectronProcess;
glob?: CustomGlobals; glob?: CustomGlobals;
React: {
createElement(any, any?, any?);
Fragment: any;
useState({
width: undefined,
height: undefined
});
useRef(ref: null);
useEffect(cb: () => void, args: unknown[]);
useCallback(cb: (el, ev) => void, args: unknown[]);
};
ReactDOM: {
unmountComponentAtNode(el: HTMLElement);
createRoot(el: HTMLElement);
}
ExcalidrawLib: {
getSceneVersion(el: unknown[]): number;
exportToSvg(opts: {
elements: ExcalidrawElement[],
appState: ExcalidrawAppState,
exportPadding: number,
metadata: string,
files: ExcalidrawElement[]
}): Promise<HTMLElement>;
updateScene,
Excalidraw: unknown
}
EXCALIDRAW_ASSET_PATH: string;
}
interface ExcalidrawApi {
getSceneElements(): ExcalidrawElement[];
getAppState(): ExcalidrawAppState;
getFiles(): ExcalidrawElement[];
updateScene(scene: ExcalidrawScene);
updateLibrary(opts: { libraryItems?: ExcalidrawLibrary[], merge: boolean }): Promise<ExcalidrawLibrary[]>;
addFiles(files: ExcalidrawElement[]);
history: {
clear();
}
}
interface ExcalidrawElement {
fileId: number;
}
interface ExcalidrawLibrary {
id: string;
name: string;
}
interface ExcalidrawScene {
elements: unknown[];
appState: ExcalidrawAppState;
collaborators: unknown[];
}
interface ExcalidrawAppState {
scrollX?: number;
scrollY?: number;
zoom?: number;
theme?: string;
width?: number;
height?: number;
offsetLeft?: number;
offsetTop?: number;
} }
interface AutoCompleteConfig { interface AutoCompleteConfig {

View File

@ -3,6 +3,7 @@ import TypeWidget from "./type_widget.js";
import utils from "../../services/utils.js"; import utils from "../../services/utils.js";
import linkService from "../../services/link.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";
const TPL = ` const TPL = `
<div class="canvas-widget note-detail-canvas note-detail-printable note-detail"> <div class="canvas-widget note-detail-canvas note-detail-printable note-detail">
<style> <style>
@ -51,6 +52,17 @@ const TPL = `
</div> </div>
`; `;
interface CanvasContent {
elements: ExcalidrawElement[],
files: ExcalidrawElement[],
appState: ExcalidrawAppState
}
interface AttachmentMetadata {
title: string;
attachmentId: string;
}
/** /**
* # Canvas note with excalidraw * # Canvas note with excalidraw
* @author thfrei 2022-05-11 * @author thfrei 2022-05-11
@ -95,6 +107,24 @@ const TPL = `
* - Make it easy to include a canvas note inside a text note * - Make it easy to include a canvas note inside a text note
*/ */
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 currentSceneVersion: number;
private libraryChanged: boolean;
private librarycache: ExcalidrawLibrary[];
private attachmentMetadata: AttachmentMetadata[];
private themeStyle!: string;
private excalidrawApi!: ExcalidrawApi;
private excalidrawWrapperRef!: {
current: HTMLElement
};
private $render!: JQuery<HTMLElement>;
private reactHandlers!: JQuery<HTMLElement>;
constructor() { constructor() {
super(); super();
@ -145,6 +175,9 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
const React = window.React; const React = window.React;
const ReactDOM = window.ReactDOM; const ReactDOM = window.ReactDOM;
const renderElement = this.$render.get(0); const renderElement = this.$render.get(0);
if (!renderElement) {
throw new Error("Unable to find element to render.");
}
ReactDOM.unmountComponentAtNode(renderElement); ReactDOM.unmountComponentAtNode(renderElement);
const root = ReactDOM.createRoot(renderElement); const root = ReactDOM.createRoot(renderElement);
@ -156,10 +189,8 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
/** /**
* called to populate the widget container with the note content * called to populate the widget container with the note content
*
* @param {FNote} note
*/ */
async doRefresh(note) { async doRefresh(note: FNote) {
// 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) {
@ -183,7 +214,7 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
* note into this fresh note. Probably due to that this note-instance does not get * note into this fresh note. Probably due to that this note-instance does not get
* newly instantiated? * newly instantiated?
*/ */
if (!blob.content?.trim()) { if (!blob?.content?.trim()) {
const sceneData = { const sceneData = {
elements: [], elements: [],
appState: { appState: {
@ -194,11 +225,11 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
this.excalidrawApi.updateScene(sceneData); this.excalidrawApi.updateScene(sceneData);
} else if (blob.content) { } else if (blob.content) {
// load saved content into excalidraw canvas let content: CanvasContent;
let content;
// load saved content into excalidraw canvas
try { try {
content = blob.getJsonContent(); content = blob.getJsonContent() as CanvasContent;
} catch (err) { } catch (err) {
console.error("Error parsing content. Probably note.type changed. Starting with empty canvas", note, blob, err); console.error("Error parsing content. Probably note.type changed. Starting with empty canvas", note, blob, err);
@ -219,7 +250,7 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
appState.offsetLeft = boundingClientRect.left; appState.offsetLeft = boundingClientRect.left;
appState.offsetTop = boundingClientRect.top; appState.offsetTop = boundingClientRect.top;
const sceneData = { const sceneData: ExcalidrawScene = {
elements, elements,
appState, appState,
collaborators: [] collaborators: []
@ -257,7 +288,7 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
} }
// Extract libraryItems from the blobs // Extract libraryItems from the blobs
const libraryItems = results.map((result) => result.blob.getJsonContentSafely()).filter((item) => !!item); const libraryItems = results.map((result) => result?.blob?.getJsonContentSafely()).filter((item) => !!item) as ExcalidrawLibrary[];
// Extract metadata for each attachment // Extract metadata for each attachment
const metadata = results.map((result) => result.metadata); const metadata = results.map((result) => result.metadata);
@ -297,7 +328,7 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
const files = this.excalidrawApi.getFiles(); const files = this.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 ExcalidrawLib.exportToSvg({ const svg = await window.ExcalidrawLib.exportToSvg({
elements, elements,
appState, appState,
exportPadding: 5, // 5 px padding exportPadding: 5, // 5 px padding
@ -306,7 +337,7 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
}); });
const svgString = svg.outerHTML; const svgString = svg.outerHTML;
const activeFiles = {}; const activeFiles: Record<string, ExcalidrawElement> = {};
elements.forEach((element) => { elements.forEach((element) => {
if (element.fileId) { if (element.fileId) {
activeFiles[element.fileId] = files[element.fileId]; activeFiles[element.fileId] = files[element.fileId];
@ -474,12 +505,12 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
React.createElement(Excalidraw, { React.createElement(Excalidraw, {
// this makes sure that 1) manual theme switch button is hidden 2) theme stays as it should after opening menu // this makes sure that 1) manual theme switch button is hidden 2) theme stays as it should after opening menu
theme: this.themeStyle, theme: this.themeStyle,
excalidrawAPI: (api) => { excalidrawAPI: (api: ExcalidrawApi) => {
this.excalidrawApi = api; this.excalidrawApi = api;
}, },
width: dimensions.width, width: dimensions.width,
height: dimensions.height, height: dimensions.height,
onPaste: (data, event) => { onPaste: (data: unknown, event: unknown) => {
console.log("Verbose: excalidraw internal paste. No trilium action implemented.", data, event); console.log("Verbose: excalidraw internal paste. No trilium action implemented.", data, event);
}, },
onLibraryChange: () => { onLibraryChange: () => {