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

View File

@ -54,6 +54,72 @@ declare global {
process?: ElectronProcess;
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 {

View File

@ -3,6 +3,7 @@ import TypeWidget from "./type_widget.js";
import utils from "../../services/utils.js";
import linkService from "../../services/link.js";
import server from "../../services/server.js";
import type FNote from "../../entities/fnote.js";
const TPL = `
<div class="canvas-widget note-detail-canvas note-detail-printable note-detail">
<style>
@ -51,6 +52,17 @@ const TPL = `
</div>
`;
interface CanvasContent {
elements: ExcalidrawElement[],
files: ExcalidrawElement[],
appState: ExcalidrawAppState
}
interface AttachmentMetadata {
title: string;
attachmentId: string;
}
/**
* # Canvas note with excalidraw
* @author thfrei 2022-05-11
@ -95,6 +107,24 @@ const TPL = `
* - Make it easy to include a canvas note inside a text note
*/
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() {
super();
@ -145,6 +175,9 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
const React = window.React;
const ReactDOM = window.ReactDOM;
const renderElement = this.$render.get(0);
if (!renderElement) {
throw new Error("Unable to find element to render.");
}
ReactDOM.unmountComponentAtNode(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
*
* @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
const noteChanged = this.currentNoteId !== note.noteId;
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
* newly instantiated?
*/
if (!blob.content?.trim()) {
if (!blob?.content?.trim()) {
const sceneData = {
elements: [],
appState: {
@ -194,11 +225,11 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
this.excalidrawApi.updateScene(sceneData);
} else if (blob.content) {
// load saved content into excalidraw canvas
let content;
let content: CanvasContent;
// load saved content into excalidraw canvas
try {
content = blob.getJsonContent();
content = blob.getJsonContent() as CanvasContent;
} catch (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.offsetTop = boundingClientRect.top;
const sceneData = {
const sceneData: ExcalidrawScene = {
elements,
appState,
collaborators: []
@ -257,7 +288,7 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
}
// 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
const metadata = results.map((result) => result.metadata);
@ -297,7 +328,7 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
const files = this.excalidrawApi.getFiles();
// 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,
appState,
exportPadding: 5, // 5 px padding
@ -306,7 +337,7 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
});
const svgString = svg.outerHTML;
const activeFiles = {};
const activeFiles: Record<string, ExcalidrawElement> = {};
elements.forEach((element) => {
if (element.fileId) {
activeFiles[element.fileId] = files[element.fileId];
@ -474,12 +505,12 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
React.createElement(Excalidraw, {
// this makes sure that 1) manual theme switch button is hidden 2) theme stays as it should after opening menu
theme: this.themeStyle,
excalidrawAPI: (api) => {
excalidrawAPI: (api: ExcalidrawApi) => {
this.excalidrawApi = api;
},
width: dimensions.width,
height: dimensions.height,
onPaste: (data, event) => {
onPaste: (data: unknown, event: unknown) => {
console.log("Verbose: excalidraw internal paste. No trilium action implemented.", data, event);
},
onLibraryChange: () => {