chore(react/type_widget): set up self-hosted fonts

This commit is contained in:
Elian Doran 2025-09-22 10:14:24 +03:00
parent 68bf5b7e68
commit 44b92a024c
No known key found for this signature in database
3 changed files with 14 additions and 97 deletions

View File

@ -1,8 +1,8 @@
import { Excalidraw, exportToSvg, getSceneVersion } from "@excalidraw/excalidraw";
import { TypeWidgetProps } from "./type_widget";
import "@excalidraw/excalidraw/index.css";
import { useEditorSpacedUpdate, useNoteBlob } from "../react/hooks";
import { useEffect, useMemo, useRef } from "preact/hooks";
import { useEditorSpacedUpdate } from "../react/hooks";
import { 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";
@ -12,6 +12,10 @@ import server from "../../services/server";
import { NonDeletedExcalidrawElement } from "@excalidraw/excalidraw/element/types";
import { CanvasContent } from "../type_widgets_old/canvas_el";
// currently required by excalidraw, in order to allows self-hosting fonts locally.
// this avoids making excalidraw load the fonts from an external CDN.
window.EXCALIDRAW_ASSET_PATH = `${window.location.pathname}/node_modules/@excalidraw/excalidraw/dist/prod`;
interface AttachmentMetadata {
title: string;
attachmentId: string;
@ -19,12 +23,12 @@ interface AttachmentMetadata {
export default function Canvas({ note }: TypeWidgetProps) {
const apiRef = useRef<ExcalidrawImperativeAPI>(null);
const viewModeEnabled = options.is("databaseReadonly");
const isReadOnly = options.is("databaseReadonly");
const themeStyle = useMemo(() => {
const documentStyle = window.getComputedStyle(document.documentElement);
return documentStyle.getPropertyValue("--theme-style")?.trim() as AppState["theme"];
}, []);
const persistence = usePersistence(note, apiRef, themeStyle);
const persistence = usePersistence(note, apiRef, themeStyle, isReadOnly);
return (
<div className="canvas-widget note-detail-canvas note-detail-printable note-detail full-height">
@ -33,7 +37,7 @@ export default function Canvas({ note }: TypeWidgetProps) {
<Excalidraw
excalidrawAPI={api => apiRef.current = api}
theme={themeStyle}
viewModeEnabled={viewModeEnabled}
viewModeEnabled={isReadOnly}
zenModeEnabled={false}
isCollaborating={false}
detectScroll={false}
@ -53,7 +57,7 @@ export default function Canvas({ note }: TypeWidgetProps) {
)
}
function usePersistence(note: FNote, apiRef: RefObject<ExcalidrawImperativeAPI>, theme: AppState["theme"]): Partial<ExcalidrawProps> {
function usePersistence(note: FNote, apiRef: RefObject<ExcalidrawImperativeAPI>, theme: AppState["theme"], isReadOnly: boolean): Partial<ExcalidrawProps> {
const libraryChanged = useRef(false);
const currentSceneVersion = useRef(0);
@ -169,7 +173,7 @@ function usePersistence(note: FNote, apiRef: RefObject<ExcalidrawImperativeAPI>,
return {
onChange: () => {
if (!apiRef.current) return;
if (!apiRef.current || isReadOnly) return;
const oldSceneVersion = currentSceneVersion.current;
const newSceneVersion = getSceneVersion(apiRef.current.getSceneElements());

View File

@ -113,93 +113,4 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
return this.$widget;
}
async #init() {
const renderElement = this.$render.get(0);
if (!renderElement) {
throw new Error("Unable to find element to render.");
}
const Canvas = (await import("./canvas_el.js")).default;
this.canvasInstance = new Canvas({
// this makes sure that 1) manual theme switch button is hidden 2) theme stays as it should after opening menu
onChange: () => this.onChangeHandler(),
onLibraryChange: () => {
this.libraryChanged = true;
this.saveData();
},
});
await setupFonts();
const canvasEl = renderReactWidget(this, this.canvasInstance.createCanvasElement())[0];
renderElement.replaceChildren(canvasEl);
}
/**
* called to populate the widget container with the note content
*/
async doRefresh(note: FNote) {
if (!this.canvasInstance) {
await this.#init();
}
this.currentNoteId = note.noteId;
// get note from backend and put into canvas
const blob = await note.getBlob();
}
/**
* save content to backend
*/
saveData() {
// Since Excalidraw sends an enormous amount of events, wait for them to stop before actually saving.
this.spacedUpdate.resetUpdateTimer();
this.spacedUpdate.scheduleUpdate();
}
onChangeHandler() {
if (options.is("databaseReadonly")) {
return;
}
if (!this.canvasInstance.isInitialized()) return;
// 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.
const isNewSceneVersion = this.canvasInstance.isNewSceneVersion();
/**
* 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
*/
// upon updateScene, onchange is called, even though "nothing really changed" that is worth saving
const isNotInitialScene = !this.canvasInstance.isInitialScene();
const shouldSave = isNewSceneVersion && isNotInitialScene;
if (shouldSave) {
this.canvasInstance.updateSceneVersion();
this.saveData();
}
}
}
async function setupFonts() {
if (window.EXCALIDRAW_ASSET_PATH) {
return;
}
// currently required by excalidraw, in order to allows self-hosting fonts locally.
// this avoids making excalidraw load the fonts from an external CDN.
let path: string;
if (!glob.isDev) {
path = `${window.location.pathname}/node_modules/@excalidraw/excalidraw/dist/prod`;
} else {
path = (await import("../../../../../node_modules/@excalidraw/excalidraw/dist/prod/fonts/Excalifont/Excalifont-Regular-a88b72a24fb54c9f94e3b5fdaa7481c9.woff2?url")).default;
let pathComponents = path.split("/");
path = pathComponents.slice(0, pathComponents.length - 2).join("/");
}
window.EXCALIDRAW_ASSET_PATH = path;
}

View File

@ -21,17 +21,19 @@ async function register(app: express.Application) {
if (process.env.NODE_ENV === "development") {
const { createServer: createViteServer } = await import("vite");
const clientDir = path.join(srcRoot, "../client");
const vite = await createViteServer({
server: { middlewareMode: true },
appType: "custom",
cacheDir: path.join(srcRoot, "../../.cache/vite"),
base: `/${assetUrlFragment}/`,
root: path.join(srcRoot, "../client")
root: clientDir
});
app.use(`/${assetUrlFragment}/`, (req, res, next) => {
req.url = `/${assetUrlFragment}` + req.url;
vite.middlewares(req, res, next);
});
app.use(`/node_modules/@excalidraw/excalidraw/dist/prod`, persistentCacheStatic(path.join(srcRoot, "../../node_modules/@excalidraw/excalidraw/dist/prod")));
} else {
const publicDir = path.join(resourceDir, "public");
if (!existsSync(publicDir)) {