From f5d5d383dbfbd55edd8f805539dcd2928c878b9d Mon Sep 17 00:00:00 2001 From: Brice Dutheil Date: Fri, 12 Jan 2024 15:54:41 +0100 Subject: [PATCH 1/5] chore: Now exports binary files Refs: #199 --- excalidraw-assets/src/App.tsx | 43 ++++++++++++++++++----------------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/excalidraw-assets/src/App.tsx b/excalidraw-assets/src/App.tsx index bfb23bc..8693e8b 100644 --- a/excalidraw-assets/src/App.tsx +++ b/excalidraw-assets/src/App.tsx @@ -26,7 +26,7 @@ const defaultInitialData = { const initialData = anyWindow.initialData ?? defaultInitialData; class ExcalidrawApiBridge { - private readonly excalidrawRef: any; + private readonly excalidrawApiRef: React.Ref; private continuousSavingEnabled = true; private _setTheme: React.Dispatch | null = null; set setTheme(value: React.Dispatch) { @@ -49,49 +49,50 @@ class ExcalidrawApiBridge { } constructor(excalidrawRef: React.MutableRefObject) { - this.excalidrawRef = excalidrawRef; + this.excalidrawApiRef = excalidrawRef; window.addEventListener( "message", this.pluginMessageHandler.bind(this) ); } - private excalidraw() { - return this.excalidrawRef.current; + private excalidrawApi() : ExcalidrawImperativeAPI { + // @ts-ignore // Object is initialized in constructor + return this.excalidrawApiRef.current; } // @ts-ignore readonly updateApp = ({elements, appState}) => { - this.excalidraw().updateScene({ + this.excalidrawApi().updateScene({ elements: elements, appState: appState, }); }; readonly updateAppState = (appState: object) => { - this.excalidraw().updateScene({ - elements: this.excalidraw().getSceneElements(), + this.excalidrawApi().updateScene({ + elements: this.excalidrawApi().getSceneElements(), appState: { - ...this.excalidraw().getAppState(), + ...this.excalidrawApi().getAppState(), ...appState }, }); }; readonly saveAsJson = () => { - let binaryFiles = {}; return serializeAsJSON( - this.excalidraw().getSceneElements(), - this.excalidraw().getAppState(), - binaryFiles, + this.excalidrawApi().getSceneElements(), + this.excalidrawApi().getAppState(), + this.excalidrawApi().getFiles(), "local" ) }; readonly saveAsSvg = (exportParams: object) => { console.debug("saveAsSvg export config", exportParams); - let sceneElements = this.excalidraw().getSceneElements(); - let appState = this.excalidraw().getAppState(); + let sceneElements = this.excalidrawApi().getSceneElements(); + let appState = this.excalidrawApi().getAppState(); + let files = this.excalidrawApi().getFiles(); // Doc: https://docs.excalidraw.com/docs/@excalidraw/excalidraw/api/utils/export#exporttosvg return exportToSvg({ @@ -101,16 +102,16 @@ class ExcalidrawApiBridge { ...exportParams, exportEmbedScene: true, }, - files: {}, + files: files, }); }; readonly saveAsBlob = (exportParams: object, mimeType: string) => { console.debug("saveAsPng export config", exportParams); - let sceneElements = this.excalidraw().getSceneElements(); - let appState = this.excalidraw().getAppState(); + let sceneElements = this.excalidrawApi().getSceneElements(); + let appState = this.excalidrawApi().getAppState(); + let files = this.excalidrawApi().getFiles(); - let binaryFiles = {}; // Doc: https://docs.excalidraw.com/docs/@excalidraw/excalidraw/api/utils/export#exporttoblob return exportToBlob({ elements: sceneElements, @@ -119,7 +120,7 @@ class ExcalidrawApiBridge { ...exportParams, exportEmbedScene: true, }, - files: binaryFiles, + files: files, mimeType: mimeType, }); }; @@ -182,7 +183,7 @@ class ExcalidrawApiBridge { this.currentSceneVersion = updateSceneVersion; this.updateApp({ elements: elements || [], - appState: {} // TODO load appState ? + appState: {}, // TODO load appState ? }); } break; @@ -219,7 +220,7 @@ class ExcalidrawApiBridge { this.currentSceneVersion = updateSceneVersion; this.updateApp({ elements: restoredState.elements || [], - appState: {} // TODO load appState ? (restoredState.appState) + appState: {}, // TODO load appState ? (restoredState.appState) }); } }) From 80c2beff6e0de6eb26a59570bd35ba894f4a8184 Mon Sep 17 00:00:00 2001 From: Brice Dutheil Date: Fri, 12 Jan 2024 18:01:51 +0100 Subject: [PATCH 2/5] feature: Properly save embedded images in excalidraw file --- .../bric3/excalidraw/editor/ExcalidrawWebViewController.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugin/src/main/kotlin/com/github/bric3/excalidraw/editor/ExcalidrawWebViewController.kt b/plugin/src/main/kotlin/com/github/bric3/excalidraw/editor/ExcalidrawWebViewController.kt index d339c81..109415d 100644 --- a/plugin/src/main/kotlin/com/github/bric3/excalidraw/editor/ExcalidrawWebViewController.kt +++ b/plugin/src/main/kotlin/com/github/bric3/excalidraw/editor/ExcalidrawWebViewController.kt @@ -253,7 +253,8 @@ class ExcalidrawWebViewController( window.postMessage({ type: "update", - elements: json.elements + elements: json.elements, + files: json.files }, 'https://$pluginDomain') """ ) From fc21b8b42f20603fdc37ccdcbb25a20bc26e32cb Mon Sep 17 00:00:00 2001 From: Brice Dutheil Date: Tue, 27 Aug 2024 02:30:30 +0200 Subject: [PATCH 3/5] feature: Load embedded images from excalidraw files This was contributed by @lionelhorn. Approach is to wait for the "update" message that has the embedded files then create `initialData` with the files and only then render the Excalidraw component. Mobx is used for state management replacing react `useState`. Which is easier to get the code a bit cleaner code than passing the setters of `useState` to the bridge. Co-authored-by: Lionel --- excalidraw-assets/package.json | 2 + excalidraw-assets/src/App.tsx | 413 +++--------------- excalidraw-assets/src/DebugHelperView.tsx | 15 + excalidraw-assets/src/ExcalidrawApiBridge.tsx | 325 ++++++++++++++ excalidraw-assets/src/bridge-context.tsx | 17 + excalidraw-assets/src/index.tsx | 13 +- excalidraw-assets/src/vars.tsx | 24 + excalidraw-assets/tsconfig.json | 2 +- excalidraw-assets/yarn.lock | 58 ++- .../editor/ExcalidrawWebViewController.kt | 2 +- 10 files changed, 506 insertions(+), 365 deletions(-) create mode 100644 excalidraw-assets/src/DebugHelperView.tsx create mode 100644 excalidraw-assets/src/ExcalidrawApiBridge.tsx create mode 100644 excalidraw-assets/src/bridge-context.tsx create mode 100644 excalidraw-assets/src/vars.tsx diff --git a/excalidraw-assets/package.json b/excalidraw-assets/package.json index 994af5d..1d3efa2 100644 --- a/excalidraw-assets/package.json +++ b/excalidraw-assets/package.json @@ -12,6 +12,8 @@ "@types/react": "^18.0.0", "@types/react-dom": "^18.0.0", "awesome-debounce-promise": "^2.1.0", + "mobx": "6.13.1", + "mobx-react": "9.1.1", "react": "^18.0.0", "react-app-rewired": "^2.1.8", "react-dom": "^18.0.0", diff --git a/excalidraw-assets/src/App.tsx b/excalidraw-assets/src/App.tsx index 8693e8b..7a024de 100644 --- a/excalidraw-assets/src/App.tsx +++ b/excalidraw-assets/src/App.tsx @@ -1,366 +1,61 @@ -import React from "react"; -import { - Excalidraw, - exportToBlob, - exportToSvg, - getSceneVersion, - loadFromBlob, - MainMenu, - serializeAsJSON, -} from "@excalidraw/excalidraw"; -import AwesomeDebouncePromise from 'awesome-debounce-promise'; -import {RestoredDataState} from "@excalidraw/excalidraw/types/data/restore"; -import {Theme} from "@excalidraw/excalidraw/types/element/types"; -import {ExcalidrawImperativeAPI} from "@excalidraw/excalidraw/types/types"; - -// hack to access the non typed window object (any) to add old school javascript -let anyWindow = (window as any); - -const defaultInitialData = { - readOnly: false, - gridMode: false, - zenMode: false, - theme: "light", - debounceAutoSaveInMs: 300 -} -const initialData = anyWindow.initialData ?? defaultInitialData; - -class ExcalidrawApiBridge { - private readonly excalidrawApiRef: React.Ref; - private continuousSavingEnabled = true; - private _setTheme: React.Dispatch | null = null; - set setTheme(value: React.Dispatch) { - this._setTheme = value; - } - - private _setViewModeEnabled: React.Dispatch | null = null; - set setViewModeEnabled(value: React.Dispatch) { - this._setViewModeEnabled = value; - } - - private _setGridModeEnabled: React.Dispatch | null = null; - set setGridModeEnabled(value: React.Dispatch) { - this._setGridModeEnabled = value; - } - - private _setZenModeEnabled: React.Dispatch | null = null; - set setZenModeEnabled(value: React.Dispatch) { - this._setZenModeEnabled = value; - } - - constructor(excalidrawRef: React.MutableRefObject) { - this.excalidrawApiRef = excalidrawRef; - window.addEventListener( - "message", - this.pluginMessageHandler.bind(this) - ); - } - - private excalidrawApi() : ExcalidrawImperativeAPI { - // @ts-ignore // Object is initialized in constructor - return this.excalidrawApiRef.current; - } - - // @ts-ignore - readonly updateApp = ({elements, appState}) => { - this.excalidrawApi().updateScene({ - elements: elements, - appState: appState, - }); - }; - - readonly updateAppState = (appState: object) => { - this.excalidrawApi().updateScene({ - elements: this.excalidrawApi().getSceneElements(), - appState: { - ...this.excalidrawApi().getAppState(), - ...appState - }, - }); - }; - - readonly saveAsJson = () => { - return serializeAsJSON( - this.excalidrawApi().getSceneElements(), - this.excalidrawApi().getAppState(), - this.excalidrawApi().getFiles(), - "local" - ) - }; - - readonly saveAsSvg = (exportParams: object) => { - console.debug("saveAsSvg export config", exportParams); - let sceneElements = this.excalidrawApi().getSceneElements(); - let appState = this.excalidrawApi().getAppState(); - let files = this.excalidrawApi().getFiles(); - - // Doc: https://docs.excalidraw.com/docs/@excalidraw/excalidraw/api/utils/export#exporttosvg - return exportToSvg({ - elements: sceneElements, - appState: { - ...appState, - ...exportParams, - exportEmbedScene: true, - }, - files: files, - }); - }; - - readonly saveAsBlob = (exportParams: object, mimeType: string) => { - console.debug("saveAsPng export config", exportParams); - let sceneElements = this.excalidrawApi().getSceneElements(); - let appState = this.excalidrawApi().getAppState(); - let files = this.excalidrawApi().getFiles(); - - // Doc: https://docs.excalidraw.com/docs/@excalidraw/excalidraw/api/utils/export#exporttoblob - return exportToBlob({ - elements: sceneElements, - appState: { - ...appState, - ...exportParams, - exportEmbedScene: true, - }, - files: files, - mimeType: mimeType, - }); - }; - - currentSceneVersion = getSceneVersion([]); // scene elements are empty on load - - private _continuousSaving = (elements: object[], appState: object) => { - if (!this.continuousSavingEnabled) { - return; - } - console.debug("debounced scene changed") - // @ts-ignore - const newSceneVersion = getSceneVersion(elements); - // maybe check appState - if (this.currentSceneVersion !== newSceneVersion) { - this.currentSceneVersion = newSceneVersion; - - let jsonContent = this.saveAsJson(); - - this.dispatchToPlugin({ - type: "continuous-update", - content: jsonContent, - }); - } - } - debouncedContinuousSaving = AwesomeDebouncePromise( - this._continuousSaving, - initialData.debounceAutoSaveInMs - ) - - dispatchToPlugin(message: object): void { - console.debug("dispatchToPlugin: ", message); - - // `cefQuery` is only declared and available in the JCEF browser, which explains why it's not resolved here. - // noinspection JSUnresolvedReference - if (anyWindow.cefQuery) { - // noinspection JSUnresolvedReference - anyWindow.cefQuery({ - request: JSON.stringify(message), - persistent: false, - onSuccess: function (response: any) { - console.debug("success for message", message, ", response", response); - }, - onFailure: function (error_code: any, error_message: any) { - console.debug("failure for message", message, ", error_code", error_code, ", error_message", error_message); - } - }); - } - } - - - private pluginMessageHandler(e: MessageEvent) { - const message = e.data; - console.debug("got event: " + message.type + ", message: ", message); - switch (message.type) { - case "update": { - const {elements} = message; - const updateSceneVersion = getSceneVersion(elements); - if (this.currentSceneVersion !== updateSceneVersion) { - this.currentSceneVersion = updateSceneVersion; - this.updateApp({ - elements: elements || [], - appState: {}, // TODO load appState ? - }); - } - break; - } - - case "load-from-file": { - const {fileToFetch} = message - fetch('/vfs/' + fileToFetch) - .then(response => response.blob()) - .then(async blob => { - // as the plugin uses IntelliJ's auto-saving mechanism instead. - this.continuousSavingEnabled = false - - try { - return loadFromBlob(blob, null, null); - } catch (error: unknown) { - // Javascript/Typescript errors can be of any type really, even null. - let errorStr = error instanceof Error ? error.toString() : JSON.stringify(error); - console.error(errorStr) - // Also, maybe error can be passed to the dispatcher? - this.dispatchToPlugin({ - type: "excalidraw-error", - errorMessage: "cannot load image" - }) - } - }) - .then((restoredState: RestoredDataState | undefined) => { - if (!restoredState) { - return; - } - - const updateSceneVersion = getSceneVersion(restoredState.elements); - if (this.currentSceneVersion !== updateSceneVersion) { - this.currentSceneVersion = updateSceneVersion; - this.updateApp({ - elements: restoredState.elements || [], - appState: {}, // TODO load appState ? (restoredState.appState) - }); - } - }) - - break; - } - - case "toggle-read-only": { - this._setViewModeEnabled!(message.readOnly); - break; - } - - case "toggle-scene-modes": { - const modes = message.sceneModes ?? {}; - if ("gridMode" in modes) this._setGridModeEnabled!(modes.gridMode); - if ("zenMode" in modes) this._setZenModeEnabled!(modes.zenMode); - break; - } - - case "theme-change": { - this._setTheme!(message.theme); - break; - } - - case "save-as-json": { - this.dispatchToPlugin({ - type: "json-content", - json: this.saveAsJson(), - correlationId: message.correlationId ?? null - }); - break; - } - - case "save-as-svg": { - const exportConfig = message.exportConfig ?? {}; - this.saveAsSvg(exportConfig).then(svg => { - this.dispatchToPlugin({ - type: "svg-content", - svg: svg.outerHTML, - correlationId: message.correlationId ?? null - }); - }) - break; - } - - case "save-as-binary-image": { - const exportConfig = message.exportConfig ?? {}; - const mimeType = message.mimeType ?? "image/png"; - const thisBridge = this; - this.saveAsBlob(exportConfig, mimeType).then((blob: Blob) => { - const reader = new FileReader(); - reader.readAsDataURL(blob); - reader.onloadend = function () { - let base64data = reader.result; - thisBridge.dispatchToPlugin({ - type: "binary-image-base64-content", - base64Payload: base64data, - correlationId: message.correlationId ?? null - }); - }; - }); - break; - } - } - } -} - -let apiBridge: ExcalidrawApiBridge | null = null; - - -export const App = () => { - const excalidrawApiRef = React.useRef(null); - apiBridge = new ExcalidrawApiBridge(excalidrawApiRef) - - const excalidrawRef = React.useCallback((excalidrawApi: ExcalidrawImperativeAPI) => { - excalidrawApiRef.current = excalidrawApi; - apiBridge!.dispatchToPlugin({type: "ready"}) +import {useCallback} from "react"; +import {observer} from "mobx-react"; +import {Excalidraw, MainMenu,} from "@excalidraw/excalidraw"; +import {ExcalidrawElement} from "@excalidraw/excalidraw/types/element/types"; +import {AppState, BinaryFiles, ExcalidrawImperativeAPI} from "@excalidraw/excalidraw/types/types"; +import {useBridge} from "./bridge-context"; + +export const App = observer(() => { + const {bridge} = useBridge(); + + const excalidrawRef = useCallback((excalidrawApi: ExcalidrawImperativeAPI) => { + bridge.setApi(excalidrawApi); }, []); - // React Hook "React.useState" cannot be called in a class component. - const [theme, setTheme] = React.useState(initialData.theme); - apiBridge.setTheme = setTheme; - const [viewModeEnabled, setViewModeEnabled] = React.useState(initialData.readOnly); - apiBridge.setViewModeEnabled = setViewModeEnabled; - const [gridModeEnabled, setGridModeEnabled] = React.useState(initialData.gridMode); - apiBridge.setGridModeEnabled = setGridModeEnabled; - const [zenModeEnabled, setZenModeEnabled] = React.useState(initialData.zenMode); - apiBridge.setZenModeEnabled = setZenModeEnabled; - // see https://codesandbox.io/s/excalidraw-forked-xsw0k?file=/src/App.js - - - let onDrawingChange = async (elements: any, state: object) => { - await apiBridge!.debouncedContinuousSaving(elements, state); + let onDrawingChange = async (elements: any, state: object, files: BinaryFiles) => { + bridge!.debouncedContinuousSaving(elements, state, files); }; + if (!bridge.isDataReady) { + return
Loading
+ } return ( -
- { - console.debug("scene changed") - onDrawingChange(elements, state).then(ignored => { - }) - }} - viewModeEnabled={viewModeEnabled} - zenModeEnabled={zenModeEnabled} - gridModeEnabled={gridModeEnabled} - theme={theme} - // UIOptions={{ canvasActions: { clearCanvas: false, export: false, loadScene: false, saveScene: false } }} - UIOptions={{ - canvasActions: { - loadScene: false, - saveAsImage: false, - saveToActiveFile: false, - } - }} - > - { /* - Customize main menu. - * See list ogf available items - https://github.com/excalidraw/excalidraw/blob/v0.17.0/src/components/main-menu/DefaultItems.tsx - * Default menu - https://github.com/excalidraw/excalidraw/blob/v0.17.0/excalidraw-app/components/AppMainMenu.tsx - */} - - - - - - - -
+
+ { + console.debug("scene changed") + onDrawingChange(elements, appState, files); + }} + viewModeEnabled={bridge.excalidrawOptions.viewMode} + zenModeEnabled={bridge.excalidrawOptions.zenMode} + gridModeEnabled={bridge.excalidrawOptions.gridMode} + theme={bridge.excalidrawOptions.theme} + // UIOptions={{ canvasActions: { clearCanvas: false, export: false, loadScene: false, saveScene: false } }} + UIOptions={{ + canvasActions: { + loadScene: false, + saveAsImage: false, + saveToActiveFile: false, + } + }} + > + { /* + Customize main menu. + * See list of available items + https://github.com/excalidraw/excalidraw/blob/v0.17.0/src/components/main-menu/DefaultItems.tsx + * Default menu + https://github.com/excalidraw/excalidraw/blob/v0.17.0/excalidraw-app/components/AppMainMenu.tsx + */} + + + + + + + +
); -} +}) \ No newline at end of file diff --git a/excalidraw-assets/src/DebugHelperView.tsx b/excalidraw-assets/src/DebugHelperView.tsx new file mode 100644 index 0000000..c254a80 --- /dev/null +++ b/excalidraw-assets/src/DebugHelperView.tsx @@ -0,0 +1,15 @@ +import {observer} from "mobx-react"; +import {FC} from "react"; +import {useBridge} from "./bridge-context"; +import {toJS} from "mobx"; + +export const DebugHelperView: FC = observer(() => { + const {bridge} = useBridge() + + return (
+
{bridge.excalidrawOptions.theme}
+
isDataReady: {JSON.stringify(bridge.isDataReady)}
+
isApiReady: {JSON.stringify(toJS(bridge.isApiReady))}
+
isBridgeReady: {JSON.stringify(toJS(bridge.isBridgeReady))}
+
) +}); \ No newline at end of file diff --git a/excalidraw-assets/src/ExcalidrawApiBridge.tsx b/excalidraw-assets/src/ExcalidrawApiBridge.tsx new file mode 100644 index 0000000..1b18a19 --- /dev/null +++ b/excalidraw-assets/src/ExcalidrawApiBridge.tsx @@ -0,0 +1,325 @@ +import { + AppState, + BinaryFiles, + ExcalidrawImperativeAPI, + ExcalidrawInitialDataState, +} from "@excalidraw/excalidraw/types/types"; +import {exportToBlob, exportToSvg, getSceneVersion, loadFromBlob, serializeAsJSON} from "@excalidraw/excalidraw"; +import AwesomeDebouncePromise from "awesome-debounce-promise"; +import {RestoredDataState} from "@excalidraw/excalidraw/types/data/restore"; +import {makeAutoObservable, makeObservable, observable, runInAction} from "mobx"; +import {initialProps} from "./vars"; + +export class BridgeDefaultSettings { + continuousSavingEnabled = true + debounceAutoSaveInMs = 1000; +} + +export class ExcalidrawOptions { + theme = initialProps.theme + viewMode: boolean = initialProps.readOnly + gridMode: boolean = initialProps.gridMode + zenMode: boolean = initialProps.zenMode + + constructor() { + makeAutoObservable(this); + } +} + +export class ExcalidrawApiBridge { + public settings: BridgeDefaultSettings; + public currentSceneVersion: number; + + debouncedContinuousSaving: (elements: object[], appState: object, files: BinaryFiles) => void; + + excalidrawOptions: ExcalidrawOptions; + private _excalidrawApi: ExcalidrawImperativeAPI | null = null; + + isApiReady: boolean = false; + isDataReady: boolean = false; + isBridgeReady: boolean = false; + + initialData: ExcalidrawInitialDataState | Promise | null | undefined; + + constructor() { + window.addEventListener( + "message", + this.pluginMessageHandler.bind(this) + ); + + this.settings = new BridgeDefaultSettings(); + this.currentSceneVersion = getSceneVersion([]); // scene elements are empty on load + this.excalidrawOptions = new ExcalidrawOptions() + + this.debouncedContinuousSaving = AwesomeDebouncePromise( + this._continuousSaving, + this.settings.debounceAutoSaveInMs + ) + + makeObservable(this, { + isApiReady: observable, + isDataReady: observable + }) + + this.dispatchToPlugin({type: "ready"}) + + runInAction(() => { + this.isBridgeReady = true; + }) + } + + get api() { + if (!this._excalidrawApi) { + throw new Error("Excalidraw api not defined.") + } + + return this._excalidrawApi; + } + + set api(api: ExcalidrawImperativeAPI) { + if (api) { + this._excalidrawApi = api; + runInAction(() => { + this.isApiReady = true; + }) + } + } + + readonly updateApp = (args: Pick & { appState?: AppState }) => { + const {elements, appState} = args; + this.api.updateScene({elements, appState}); + }; + + readonly updateAppState = (appState: object) => { + this.api.updateScene({ + elements: this.api.getSceneElements(), + appState: { + ...this.api.getAppState(), + ...appState + }, + }); + }; + + readonly saveAsJson = () => { + const serialized = serializeAsJSON( + this.api.getSceneElements(), + this.api.getAppState(), + this.api.getFiles(), + "local"); + + console.log("saveAsJson", serialized); + return serialized + }; + + readonly saveAsSvg = (exportParams: object) => { + console.debug("saveAsSvg export config", exportParams); + let sceneElements = this.api.getSceneElements(); + let appState = this.api.getAppState(); + let files = this.api.getFiles(); + + // Doc: https://docs.excalidraw.com/docs/@excalidraw/excalidraw/api/utils/export#exporttosvg + return exportToSvg({ + elements: sceneElements, + appState: { + ...appState, + ...exportParams, + exportEmbedScene: true, + }, + files: files, + }); + }; + + readonly saveAsBlob = (exportParams: object, mimeType: string) => { + console.debug("saveAsPng export config", exportParams); + let sceneElements = this.api.getSceneElements(); + let appState = this.api.getAppState(); + let files = this.api.getFiles(); + + // Doc: https://docs.excalidraw.com/docs/@excalidraw/excalidraw/api/utils/export#exporttoblob + return exportToBlob({ + elements: sceneElements, + appState: { + ...appState, + ...exportParams, + exportEmbedScene: true, + }, + files: files, + mimeType: mimeType, + }); + }; + + private _continuousSaving = (elements: object[], appState: object, files: BinaryFiles) => { + if (!this.settings.continuousSavingEnabled) { + return; + } + console.debug("debounced scene changed") + + // @ts-ignore + const newSceneVersion = getSceneVersion(elements); + + // maybe check appState + if (this.currentSceneVersion !== newSceneVersion) { + this.currentSceneVersion = newSceneVersion; + + let jsonContent = this.saveAsJson(); + + this.dispatchToPlugin({ + type: "continuous-update", + content: jsonContent, + }); + } + } + + + dispatchToPlugin(message: object): void { + console.debug("dispatchToPlugin: ", message); + + window.cefQuery({ + request: JSON.stringify(message), + persistent: false, + onSuccess: function (response: any) { + console.debug("success for message", message, ", response", response); + }, + onFailure: function (error_code: any, error_message: any) { + console.debug("failure for message", message, ", error_code", error_code, ", error_message", error_message); + } + }); + } + + private pluginMessageHandler(e: MessageEvent) { + const message = e.data; + console.debug("got event: " + message.type + ", message: ", message); + switch (message.type) { + case "update": { + // Receive json from plugin + console.log("event = update", message) + + const {elements} = message; + const updateSceneVersion = getSceneVersion(elements); + + if (this.currentSceneVersion !== updateSceneVersion) { + this.currentSceneVersion = updateSceneVersion; + // this.updateApp({ + // elements: elements || [], + // appState: {}, // TODO load appState ? + // }); + } + + this.initialData = { + type: "excalidraw", + version: 2, + appState: { + gridSize: null, + viewBackgroundColor: "#ffffff" + }, + elements: message.elements, + files: message.files + } + + this.isDataReady = true; + break; + } + + case "load-from-file": { + const {fileToFetch} = message + fetch('/vfs/' + fileToFetch) + .then(response => response.blob()) + .then(async blob => { + // as the plugin uses IntelliJ's auto-saving mechanism instead. + this.settings.continuousSavingEnabled = false + + try { + return loadFromBlob(blob, null, null); + } catch (error: unknown) { + // Javascript/Typescript errors can be of any type really, even null. + let errorStr = error instanceof Error ? error.toString() : JSON.stringify(error); + console.error(errorStr) + // Also, maybe error can be passed to the dispatcher? + this.dispatchToPlugin({ + type: "excalidraw-error", + errorMessage: "cannot load image" + }) + } + }) + .then((restoredState: RestoredDataState | undefined) => { + if (!restoredState) { + return; + } + + const updateSceneVersion = getSceneVersion(restoredState.elements); + if (this.currentSceneVersion !== updateSceneVersion) { + this.currentSceneVersion = updateSceneVersion; + this.updateApp({ + elements: restoredState.elements || [], + + }); + // appState: {}, // TODO load appState ? (restoredState.appState) + } + }) + + break; + } + + case "toggle-read-only": { + this.excalidrawOptions.viewMode = message.readOnly; + break; + } + + case "toggle-scene-modes": { + const modes = message.sceneModes ?? {}; + if ("gridMode" in modes) this.excalidrawOptions.gridMode = modes.gridMode; + if ("zenMode" in modes) this.excalidrawOptions.zenMode = modes.zenMode; + break; + } + + case "theme-change": { + this.excalidrawOptions.theme = message.theme; + break; + } + + case "save-as-json": { + this.dispatchToPlugin({ + type: "json-content", + json: this.saveAsJson(), + correlationId: message.correlationId ?? null + }); + break; + } + + case "save-as-svg": { + const exportConfig = message.exportConfig ?? {}; + this.saveAsSvg(exportConfig).then(svg => { + this.dispatchToPlugin({ + type: "svg-content", + svg: svg.outerHTML, + correlationId: message.correlationId ?? null + }); + }) + break; + } + + case "save-as-binary-image": { + const exportConfig = message.exportConfig ?? {}; + const mimeType = message.mimeType ?? "image/png"; + const thisBridge = this; + this.saveAsBlob(exportConfig, mimeType).then((blob: Blob) => { + const reader = new FileReader(); + reader.readAsDataURL(blob); + reader.onloadend = function () { + let base64data = reader.result; + thisBridge.dispatchToPlugin({ + type: "binary-image-base64-content", + base64Payload: base64data, + correlationId: message.correlationId ?? null + }); + }; + }); + break; + } + } + } + + setApi(excalidrawApi: ExcalidrawImperativeAPI) { + this.api = excalidrawApi + } +} \ No newline at end of file diff --git a/excalidraw-assets/src/bridge-context.tsx b/excalidraw-assets/src/bridge-context.tsx new file mode 100644 index 0000000..35d1fd9 --- /dev/null +++ b/excalidraw-assets/src/bridge-context.tsx @@ -0,0 +1,17 @@ +import {createContext, useContext} from "react"; +import {ExcalidrawApiBridge} from "./ExcalidrawApiBridge"; + +export interface BridgeContextType { + bridge: ExcalidrawApiBridge +} + +export const BridgeContext = createContext(null); + +export function useBridge() { + const context = useContext(BridgeContext) + if (!context) { + throw new Error("useBridge should be used on a BridgeContext provider"); + + } + return context; +} \ No newline at end of file diff --git a/excalidraw-assets/src/index.tsx b/excalidraw-assets/src/index.tsx index 5111803..002b348 100644 --- a/excalidraw-assets/src/index.tsx +++ b/excalidraw-assets/src/index.tsx @@ -5,8 +5,19 @@ import { createRoot } from "react-dom/client"; // From React 18 import "./styles.css"; import {App} from "./App"; +import {BridgeContext} from "./bridge-context"; +import {ExcalidrawApiBridge} from "./ExcalidrawApiBridge"; +import {DebugHelperView} from "./DebugHelperView"; // https://reactjs.org/blog/2022/03/08/react-18-upgrade-guide.html#updates-to-client-rendering-apis const rootContainer = document.getElementById("root"); const root = createRoot(rootContainer!) -root.render(React.createElement(App)); \ No newline at end of file + +const bridge = new ExcalidrawApiBridge(); + +root.render( + + {/**/} + + +); \ No newline at end of file diff --git a/excalidraw-assets/src/vars.tsx b/excalidraw-assets/src/vars.tsx new file mode 100644 index 0000000..3a13296 --- /dev/null +++ b/excalidraw-assets/src/vars.tsx @@ -0,0 +1,24 @@ +declare global { + interface Window { + cefQuery: any + initialProps: InitialProps + } +} + +type InitialProps = { + "theme": "light" | "dark", + "readOnly": false, + "gridMode": false, + "zenMode": false, + "debounceAutoSaveInMs": number +} + +export const defaultInitialProps: InitialProps = { + readOnly: false, + gridMode: false, + zenMode: false, + theme: "light", + debounceAutoSaveInMs: 300 +} + +export const initialProps = window.initialProps ?? defaultInitialProps; \ No newline at end of file diff --git a/excalidraw-assets/tsconfig.json b/excalidraw-assets/tsconfig.json index a841e1d..d51be21 100644 --- a/excalidraw-assets/tsconfig.json +++ b/excalidraw-assets/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "target": "es6", + "target": "es2021", "lib": [ "dom", "dom.iterable", diff --git a/excalidraw-assets/yarn.lock b/excalidraw-assets/yarn.lock index 395630f..c9fbea4 100644 --- a/excalidraw-assets/yarn.lock +++ b/excalidraw-assets/yarn.lock @@ -4320,9 +4320,9 @@ __metadata: linkType: hard "caniuse-lite@npm:^1.0.0, caniuse-lite@npm:^1.0.30001400, caniuse-lite@npm:^1.0.30001426": - version: 1.0.30001576 - resolution: "caniuse-lite@npm:1.0.30001576" - checksum: 10/51632942733593f310e581bd91c9558b8d75fbf67160a39f8036d2976cd7df9183e96d4c9d9e6f18e0205950b940d9c761bcfb7810962d7899f8a1179fde6e3f + version: 1.0.30001653 + resolution: "caniuse-lite@npm:1.0.30001653" + checksum: 10/cd9b1c0fe03249e593789a11a9ef14f987b385e60441748945916b19e74e7bc5c82c40d4836496a647586651898741aed1598ae0792114a9f0d7d7fdb2b7deb0 languageName: node linkType: hard @@ -6034,6 +6034,8 @@ __metadata: cross-env: "npm:^7.0.2" eslint: "npm:^8.29.0" eslint-config-react-app: "npm:^7.0.1" + mobx: "npm:6.13.1" + mobx-react: "npm:9.1.1" react: "npm:^18.0.0" react-app-rewired: "npm:^2.1.8" react-dom: "npm:^18.0.0" @@ -8782,6 +8784,47 @@ __metadata: languageName: node linkType: hard +"mobx-react-lite@npm:^4.0.7": + version: 4.0.7 + resolution: "mobx-react-lite@npm:4.0.7" + dependencies: + use-sync-external-store: "npm:^1.2.0" + peerDependencies: + mobx: ^6.9.0 + react: ^16.8.0 || ^17 || ^18 + peerDependenciesMeta: + react-dom: + optional: true + react-native: + optional: true + checksum: 10/294754f8a3b44aa83ace02397530f059cce40dda09bdd546f0fca6b004be4dff2dfc6e180ab24686957e45d244408b08e13754cc74c3c2af3076f6ad0ecaa898 + languageName: node + linkType: hard + +"mobx-react@npm:9.1.1": + version: 9.1.1 + resolution: "mobx-react@npm:9.1.1" + dependencies: + mobx-react-lite: "npm:^4.0.7" + peerDependencies: + mobx: ^6.9.0 + react: ^16.8.0 || ^17 || ^18 + peerDependenciesMeta: + react-dom: + optional: true + react-native: + optional: true + checksum: 10/61f2a9bb36417f4d88ef737520369ace1ca75865ad5ae5d90ae25e9febbf984d7855ec40a9bdbb7bf60942bfbb9cf9396da1019e83ffa71460cf776b53b1a2fe + languageName: node + linkType: hard + +"mobx@npm:6.13.1": + version: 6.13.1 + resolution: "mobx@npm:6.13.1" + checksum: 10/832a025f830d004f4c443b3dc2eb999f584dbda38bb7da4268de8af6de9983f7934bfefccf2f87a6dd7a3ab8151fce1460692a1d586dc90ba2705447ba10e3da + languageName: node + linkType: hard + "ms@npm:2.0.0": version: 2.0.0 resolution: "ms@npm:2.0.0" @@ -12279,6 +12322,15 @@ __metadata: languageName: node linkType: hard +"use-sync-external-store@npm:^1.2.0": + version: 1.2.2 + resolution: "use-sync-external-store@npm:1.2.2" + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + checksum: 10/671e9c190aab9a8374a5d468c6ba17f52c38b6fae970110bc196fc1e2b57204149aea9619be49a1bb5207fb6e51d8afd19c3bcb94afe61813fed039821461dc0 + languageName: node + linkType: hard + "util-deprecate@npm:^1.0.1, util-deprecate@npm:^1.0.2, util-deprecate@npm:~1.0.1": version: 1.0.2 resolution: "util-deprecate@npm:1.0.2" diff --git a/plugin/src/main/kotlin/com/github/bric3/excalidraw/editor/ExcalidrawWebViewController.kt b/plugin/src/main/kotlin/com/github/bric3/excalidraw/editor/ExcalidrawWebViewController.kt index 109415d..1fe8d9c 100644 --- a/plugin/src/main/kotlin/com/github/bric3/excalidraw/editor/ExcalidrawWebViewController.kt +++ b/plugin/src/main/kotlin/com/github/bric3/excalidraw/editor/ExcalidrawWebViewController.kt @@ -175,7 +175,7 @@ class ExcalidrawWebViewController( """ window.EXCALIDRAW_ASSET_PATH = "/"; // loads excalidraw assets from plugin (instead of CDN) - window.initialData = { + window.initialProps = { "theme": "$uiTheme", "readOnly": false, "gridMode": false, From 040af1b2b46fae6e3499e9657549f0c82554b5ad Mon Sep 17 00:00:00 2001 From: Brice Dutheil Date: Tue, 27 Aug 2024 02:31:25 +0200 Subject: [PATCH 4/5] fix: Deprecation --- .../com/github/bric3/excalidraw/editor/ExcalidrawEditor.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/src/main/kotlin/com/github/bric3/excalidraw/editor/ExcalidrawEditor.kt b/plugin/src/main/kotlin/com/github/bric3/excalidraw/editor/ExcalidrawEditor.kt index e93da0f..5a7a918 100644 --- a/plugin/src/main/kotlin/com/github/bric3/excalidraw/editor/ExcalidrawEditor.kt +++ b/plugin/src/main/kotlin/com/github/bric3/excalidraw/editor/ExcalidrawEditor.kt @@ -90,7 +90,7 @@ class ExcalidrawEditor( with(busConnection) { subscribe(EditorColorsManager.TOPIC, this@ExcalidrawEditor) subscribe(EditorColorsManager.TOPIC, this@ExcalidrawEditor) - subscribe(AppTopics.FILE_DOCUMENT_SYNC, object : FileDocumentManagerListener { + subscribe(FileDocumentManagerListener.TOPIC, object : FileDocumentManagerListener { override fun beforeAllDocumentsSaving() { // This is the manual or auto save action of IntelliJ debuggingLogWithThread(logger) { "ExcalidrawEditor::beforeAllDocumentsSaving" } From 4ead3b4c14ff7f8bb9398758d37e0095ea962ccc Mon Sep 17 00:00:00 2001 From: Brice Dutheil Date: Tue, 27 Aug 2024 11:06:51 +0200 Subject: [PATCH 5/5] fix: Use the Gradle property set notation on YarnProxy --- excalidraw-assets/build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/excalidraw-assets/build.gradle.kts b/excalidraw-assets/build.gradle.kts index 38ea84f..d5373c5 100644 --- a/excalidraw-assets/build.gradle.kts +++ b/excalidraw-assets/build.gradle.kts @@ -211,10 +211,10 @@ open class YarnProxy @Inject constructor( @get:Input var yarnArgs: String = "" set(value) { - super.getScript().set(value) + super.script = value } init { - super.getScript().set(yarnArgs) + super.script = yarnArgs } } \ No newline at end of file