client/text editor/canvas: patch Excalidraw to disable responsive mode on the desktop layout

This commit is contained in:
Adorian Doran
2026-10-03 08:58:49 +03:00
parent 60d7faeff0
commit 409121e58d
7 changed files with 108 additions and 20 deletions
@@ -3,6 +3,8 @@ import { type ComponentChildren, type RefObject, render } from "preact";
import { act } from "preact/test-utils";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { CanvasEditorProps } from "./Canvas";
const excalidrawProps = vi.fn((_props: ExcalidrawProps) => undefined);
vi.mock("@excalidraw/excalidraw", () => ({
Excalidraw: (props: ExcalidrawProps) => {
@@ -46,7 +48,7 @@ describe("CanvasEditor", () => {
container.remove();
});
async function mount(isEmbedded?: boolean, children?: ComponentChildren) {
async function mount(props: Partial<CanvasEditorProps> = {}) {
await act(async () => {
render(
<CanvasEditor
@@ -54,10 +56,8 @@ describe("CanvasEditor", () => {
isReadOnly={false}
colorScheme="light"
persistence={{}}
isEmbedded={isEmbedded}
>
{children}
</CanvasEditor>,
{...props}
/>,
container
);
});
@@ -73,7 +73,7 @@ describe("CanvasEditor", () => {
}
it("reads its position again before pointer input once the drawing moved", async () => {
const excalidraw = await mount(true);
const excalidraw = await mount({ isEmbedded: true });
point(excalidraw);
expect(refresh).not.toHaveBeenCalled();
@@ -94,8 +94,26 @@ describe("CanvasEditor", () => {
});
it("renders its children inside Excalidraw", async () => {
const excalidraw = await mount(true, <span className="excalidraw-child" />);
const excalidraw = await mount({
isEmbedded: true,
children: <span className="excalidraw-child" />
});
expect(excalidraw?.querySelector(".excalidraw-child")).not.toBeNull();
});
it("keeps Excalidraw's desktop layout at any size only with `isDesktopLayout`", async () => {
const getFormFactor = () => excalidrawProps.mock.lastCall?.[0].UIOptions?.getFormFactor;
await mount({ isDesktopLayout: true });
const desktopFormFactor = getFormFactor();
expect(desktopFormFactor?.(320, 200)).toBe("desktop");
// Excalidraw renders again when a function in `UIOptions` changes.
await mount({ isDesktopLayout: true, isReadOnly: true });
expect(getFormFactor()).toBe(desktopFormFactor);
await mount();
expect(getFormFactor()).toBeUndefined();
});
});
@@ -47,13 +47,16 @@ export interface CanvasEditorProps {
persistence: Partial<ExcalidrawProps>;
/** Whether the editor moves on the page, as inside a text note that scrolls. */
isEmbedded?: boolean;
/** Whether Excalidraw keeps its desktop layout however small the editor is. */
isDesktopLayout?: boolean;
/** Rendered inside Excalidraw, where its hooks such as `useI18n()` work. */
children?: ComponentChildren;
}
/** The Excalidraw editor of a canvas note or drawing, loaded and saved by `persistence`. */
export function CanvasEditor({
apiRef, isReadOnly, colorScheme, persistence, isEmbedded = false, children
apiRef, isReadOnly, colorScheme, persistence, isEmbedded = false, isDesktopLayout = false,
children
}: CanvasEditorProps) {
const [ locale ] = useTriliumOption("locale");
const noteDrop = useCanvasNoteDrop(apiRef, isReadOnly);
@@ -129,7 +132,8 @@ export function CanvasEditor({
canvasActions: {
saveToActiveFile: false,
export: false
}
},
getFormFactor: isDesktopLayout ? getDesktopFormFactor : undefined
}}
onLinkOpen={onLinkOpen}
validateEmbeddable={validateEmbeddable}
@@ -143,6 +147,11 @@ export function CanvasEditor({
)
}
/** The form factor of Excalidraw's desktop layout, for an editor of any size. */
function getDesktopFormFactor() {
return "desktop" as const;
}
/**
* Extracts a Trilium note ID from an embeddable's link, accepting the canonical note-path form
* (`root/<noteId>` or `#root/<noteId>`). Returns `null` for anything else so non-note embeds are
@@ -7,8 +7,11 @@ vi.mock("@excalidraw/excalidraw", () => ({ exportToSvg: vi.fn() }));
vi.mock("./Canvas", () => ({ CanvasEditor: MockCanvasEditor }));
vi.mock("./persistence", () => ({ useCanvasDrawingPersistence: () => ({}) }));
const canvasEditorProps = vi.fn();
/** Renders Excalidraw's focusable container after a render, as once its language loads. */
function MockCanvasEditor() {
function MockCanvasEditor(props: { isDesktopLayout?: boolean }) {
canvasEditorProps(props);
const [ isLoaded, setIsLoaded ] = useState(false);
useEffect(() => setIsLoaded(true), []);
return isLoaded ? <div className="excalidraw" tabIndex={0} /> : <span className="loading" />;
@@ -323,4 +326,20 @@ describe("CanvasDrawing", () => {
expect(document.activeElement).toBe(editable);
editable.remove();
});
it("keeps the desktop layout of Excalidraw in the desktop layout of Trilium only", async () => {
const device = window.glob.device;
await mount();
expect(canvasEditorProps).toHaveBeenLastCalledWith(
expect.objectContaining({ isDesktopLayout: true })
);
render(null, box);
window.glob.device = "mobile";
await mount();
window.glob.device = device;
expect(canvasEditorProps).toHaveBeenLastCalledWith(
expect.objectContaining({ isDesktopLayout: false })
);
});
});
@@ -10,6 +10,7 @@ import type FAttachment from "../../../entities/fattachment";
import type FNote from "../../../entities/fnote";
import type { AttachmentEditor } from "../../../services/content_renderer";
import options from "../../../services/options";
import { isDesktop } from "../../../services/utils";
import { useColorScheme } from "../../react/hooks";
import { CanvasEditor } from "./Canvas";
import CanvasDrawingMenu from "./CanvasDrawingMenu";
@@ -49,6 +50,7 @@ export default function CanvasDrawing({ attachment, editor }: CanvasDrawingProps
colorScheme={colorScheme}
persistence={persistence}
isEmbedded
isDesktopLayout={isDesktop()}
>
<CanvasDrawingMenu apiRef={apiRef} isEditable={isEditable} />
{isEditable && <CanvasEmbedTools rootRef={rootRef} apiRef={apiRef} />}
@@ -29,9 +29,12 @@ const CODE_CLASSES = [
* Excalidraw names the parts to review.
*/
describe("Excalidraw contract of the canvas drawing", () => {
const productionDir = dirname(createRequire(import.meta.url).resolve("@excalidraw/excalidraw"));
// Vitest resolves the `development` export, so both folders come from `dist`.
const entry = createRequire(import.meta.url).resolve("@excalidraw/excalidraw");
const distDir = join(dirname(entry), "..");
const productionDir = join(distDir, "prod");
// The minified bundle renames the objects of the translations and the zoom constants.
const developmentDir = join(productionDir, "../dev");
const developmentDir = join(distDir, "dev");
it("renders every class and test id that the drawing selects", () => {
const bundle = readBundle(productionDir, [ ".js", ".css" ]);
@@ -86,6 +89,14 @@ describe("Excalidraw contract of the canvas drawing", () => {
expect(source).toContain(`varDEFAULT_SIDEBAR={name:"${LIBRARY_SIDEBAR}"`);
});
it("takes the layout from `UIOptions.getFormFactor`, which the Trilium patch adds", () => {
const unpatched = [ productionDir, developmentDir ].filter((dir) => {
const source = readBundle(dir, [ ".js" ]).replace(/\s+/g, "");
return !source.includes("this.props.UIOptions.getFormFactor?.(");
});
expect(unpatched).toEqual([]);
});
});
/** The text of the scripts or stylesheets in `dir`, without its locales and fonts. */
File diff suppressed because one or more lines are too long
+4 -4
View File
@@ -85,7 +85,7 @@ overrides:
extract-zip>yauzl: 3.4.0
patchedDependencies:
'@excalidraw/[email protected]': 859531a8ff622291b895d36bcd33e07ced6fcb3bcc90631ccaf776789b03f0c2
'@excalidraw/[email protected]': dab54354a2d9ff87cb224deac1f930f25e7767a1bbf8a76bd8d55dcf2481715e
[email protected]: 8acc288f8e9cde3c8a0c6e89f0b3796aeeb8251b88db9d1ba313e2352a591d68
[email protected]: 09dfeb6f635c52ef565ca21aec3f70eb129577661f85eb67262092e8cb9f8548
@@ -207,7 +207,7 @@ importers:
dependencies:
'@excalidraw/excalidraw':
specifier: 0.18.1
version: 0.18.1(patch_hash=859531a8ff622291b895d36bcd33e07ced6fcb3bcc90631ccaf776789b03f0c2)([email protected]([email protected]))([email protected])
version: 0.18.1(patch_hash=dab54354a2d9ff87cb224deac1f930f25e7767a1bbf8a76bd8d55dcf2481715e)([email protected]([email protected]))([email protected])
'@floating-ui/dom':
specifier: 1.8.0
version: 1.8.0
@@ -857,7 +857,7 @@ importers:
dependencies:
'@excalidraw/excalidraw':
specifier: 0.18.1
version: 0.18.1(patch_hash=859531a8ff622291b895d36bcd33e07ced6fcb3bcc90631ccaf776789b03f0c2)([email protected]([email protected]))([email protected])
version: 0.18.1(patch_hash=dab54354a2d9ff87cb224deac1f930f25e7767a1bbf8a76bd8d55dcf2481715e)([email protected]([email protected]))([email protected])
'@floating-ui/dom':
specifier: 1.8.0
version: 1.8.0
@@ -13735,7 +13735,7 @@ snapshots:
'@eslint/core': 1.2.1
levn: 0.4.1
'@excalidraw/[email protected](patch_hash=859531a8ff622291b895d36bcd33e07ced6fcb3bcc90631ccaf776789b03f0c2)([email protected]([email protected]))([email protected])':
'@excalidraw/[email protected](patch_hash=dab54354a2d9ff87cb224deac1f930f25e7767a1bbf8a76bd8d55dcf2481715e)([email protected]([email protected]))([email protected])':
dependencies:
'@braintree/sanitize-url': 6.0.2
'@excalidraw/laser-pointer': 1.3.1