diff --git a/src/actions/actionExport.tsx b/src/actions/actionExport.tsx index a583bcb6..01868f68 100644 --- a/src/actions/actionExport.tsx +++ b/src/actions/actionExport.tsx @@ -113,8 +113,8 @@ export const actionSaveToActiveFile = register({ ), }); -export const actionSaveAsScene = register({ - name: "saveAsScene", +export const actionSaveFileToDisk = register({ + name: "saveFileToDisk", perform: async (elements, appState, value) => { try { const { fileHandle } = await saveAsJSON(elements, { diff --git a/src/actions/index.ts b/src/actions/index.ts index 3c699da3..f37c7842 100644 --- a/src/actions/index.ts +++ b/src/actions/index.ts @@ -35,7 +35,7 @@ export { actionChangeProjectName, actionChangeExportBackground, actionSaveToActiveFile, - actionSaveAsScene, + actionSaveFileToDisk, actionLoadScene, } from "./actionExport"; diff --git a/src/actions/types.ts b/src/actions/types.ts index eff523f9..7a98d96e 100644 --- a/src/actions/types.ts +++ b/src/actions/types.ts @@ -67,7 +67,7 @@ export type ActionName = | "changeExportBackground" | "changeExportEmbedScene" | "saveToActiveFile" - | "saveAsScene" + | "saveFileToDisk" | "loadScene" | "duplicateSelection" | "deleteSelectedElements" diff --git a/src/components/App.tsx b/src/components/App.tsx index b2bd9345..c5c0d1fc 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -452,7 +452,6 @@ class App extends React.Component { const { onCollabButtonClick, - onExportToBackend, renderTopRightUI, renderFooter, renderCustomStats, @@ -493,7 +492,6 @@ class App extends React.Component { toggleZenMode={this.toggleZenMode} langCode={getLanguage().code} isCollaborating={this.props.isCollaborating || false} - onExportToBackend={onExportToBackend} renderTopRightUI={renderTopRightUI} renderCustomFooter={renderFooter} viewModeEnabled={viewModeEnabled} diff --git a/src/components/JSONExportDialog.tsx b/src/components/JSONExportDialog.tsx index c878c8e7..7efea4be 100644 --- a/src/components/JSONExportDialog.tsx +++ b/src/components/JSONExportDialog.tsx @@ -3,11 +3,11 @@ import { ActionsManagerInterface } from "../actions/types"; import { NonDeletedExcalidrawElement } from "../element/types"; import { t } from "../i18n"; import { useIsMobile } from "./App"; -import { AppState } from "../types"; +import { AppState, ExportOpts } from "../types"; import { Dialog } from "./Dialog"; import { exportFile, exportToFileIcon, link } from "./icons"; import { ToolButton } from "./ToolButton"; -import { actionSaveAsScene } from "../actions/actionExport"; +import { actionSaveFileToDisk } from "../actions/actionExport"; import { Card } from "./Card"; import "./ExportDialog.scss"; @@ -23,35 +23,39 @@ const JSONExportModal = ({ appState, actionManager, onExportToBackend, + exportOpts, }: { appState: AppState; elements: readonly NonDeletedExcalidrawElement[]; actionManager: ActionsManagerInterface; onExportToBackend?: ExportCB; onCloseRequest: () => void; + exportOpts: ExportOpts; }) => { return (
- -
{exportToFileIcon}
-

{t("exportDialog.disk_title")}

-
- {t("exportDialog.disk_details")} - {!fsSupported && actionManager.renderAction("changeProjectName")} -
- { - actionManager.executeAction(actionSaveAsScene); - }} - /> -
- {onExportToBackend && ( + {exportOpts.saveFileToDisk && ( + +
{exportToFileIcon}
+

{t("exportDialog.disk_title")}

+
+ {t("exportDialog.disk_details")} + {!fsSupported && actionManager.renderAction("changeProjectName")} +
+ { + actionManager.executeAction(actionSaveFileToDisk); + }} + /> +
+ )} + {exportOpts.onExportToBackend && (
{link}

{t("exportDialog.link_title")}

@@ -62,7 +66,7 @@ const JSONExportModal = ({ title={t("exportDialog.link_button")} aria-label={t("exportDialog.link_button")} showAriaLabel={true} - onClick={() => onExportToBackend(elements)} + onClick={() => onExportToBackend!(elements)} />
)} @@ -76,11 +80,13 @@ export const JSONExportDialog = ({ appState, actionManager, onExportToBackend, + exportOpts, }: { appState: AppState; elements: readonly NonDeletedExcalidrawElement[]; actionManager: ActionsManagerInterface; onExportToBackend?: ExportCB; + exportOpts: ExportOpts; }) => { const [modalIsShown, setModalIsShown] = useState(false); @@ -109,6 +115,7 @@ export const JSONExportDialog = ({ actionManager={actionManager} onExportToBackend={onExportToBackend} onCloseRequest={handleClose} + exportOpts={exportOpts} /> )} diff --git a/src/components/LayerUI.tsx b/src/components/LayerUI.tsx index 7c73c368..d914779e 100644 --- a/src/components/LayerUI.tsx +++ b/src/components/LayerUI.tsx @@ -63,11 +63,6 @@ interface LayerUIProps { toggleZenMode: () => void; langCode: Language["code"]; isCollaborating: boolean; - onExportToBackend?: ( - exportedElements: readonly NonDeletedExcalidrawElement[], - appState: AppState, - canvas: HTMLCanvasElement | null, - ) => void; renderTopRightUI?: (isMobile: boolean, appState: AppState) => JSX.Element; renderCustomFooter?: (isMobile: boolean, appState: AppState) => JSX.Element; viewModeEnabled: boolean; @@ -371,7 +366,6 @@ const LayerUI = ({ showThemeBtn, toggleZenMode, isCollaborating, - onExportToBackend, renderTopRightUI, renderCustomFooter, viewModeEnabled, @@ -393,14 +387,15 @@ const LayerUI = ({ elements={elements} appState={appState} actionManager={actionManager} - onExportToBackend={ - onExportToBackend - ? (elements) => { - onExportToBackend && - onExportToBackend(elements, appState, canvas); - } - : undefined - } + onExportToBackend={(elements) => { + UIOptions.canvasActions.export.onExportToBackend && + UIOptions.canvasActions.export.onExportToBackend( + elements, + appState, + canvas, + ); + }} + exportOpts={UIOptions.canvasActions.export} /> ); }; diff --git a/src/constants.ts b/src/constants.ts index 1575a627..9ac619b1 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -131,9 +131,8 @@ export const DEFAULT_UI_OPTIONS: AppProps["UIOptions"] = { canvasActions: { changeViewBackgroundColor: true, clearCanvas: true, - export: true, + export: { saveFileToDisk: true }, loadScene: true, - saveAsScene: true, saveToActiveFile: true, theme: true, }, diff --git a/src/excalidraw-app/index.tsx b/src/excalidraw-app/index.tsx index 29933951..741b0d55 100644 --- a/src/excalidraw-app/index.tsx +++ b/src/excalidraw-app/index.tsx @@ -424,7 +424,11 @@ const ExcalidrawWrapper = () => { onCollabButtonClick={collabAPI?.onCollabButtonClick} isCollaborating={collabAPI?.isCollaborating()} onPointerUpdate={collabAPI?.onPointerUpdate} - onExportToBackend={onExportToBackend} + UIOptions={{ + canvasActions: { + export: { onExportToBackend }, + }, + }} renderTopRightUI={renderTopRightUI} renderFooter={renderFooter} langCode={langCode} diff --git a/src/packages/excalidraw/CHANGELOG.md b/src/packages/excalidraw/CHANGELOG.md index 80056e07..9e3bc463 100644 --- a/src/packages/excalidraw/CHANGELOG.md +++ b/src/packages/excalidraw/CHANGELOG.md @@ -15,6 +15,17 @@ Please add the latest change on the top under the correct section. ## Excalidraw API +### Features + +- Export dialog can be customised with [`UiOptions.canvasActions.export`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#exportOpts) [#3658](https://github.com/excalidraw/excalidraw/pull/3658). + + Also, [`UIOptions`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#UIOptions) is now memoized to avoid unnecessary rerenders. + +#### BREAKING CHANGE + +- `UIOptions.canvasActions.saveAsScene` is now renamed to `UiOptions.canvasActions.export.saveFileToDisk`. Defaults to `true` hence the **save file to disk** button is rendered inside the export dialog. +- `exportToBackend` is now renamed to `UIOptions.canvasActions.export.exportToBackend`. If this prop is not passed, the **shareable-link** button will not be rendered, same as before. + ### Refactor - #### BREAKING CHANGE diff --git a/src/packages/excalidraw/README_NEXT.md b/src/packages/excalidraw/README_NEXT.md index f1e5a612..ec7eadb2 100644 --- a/src/packages/excalidraw/README_NEXT.md +++ b/src/packages/excalidraw/README_NEXT.md @@ -363,7 +363,6 @@ To view the full example visit :point_down: | [`onCollabButtonClick`](#onCollabButtonClick) | Function | | Callback to be triggered when the collab button is clicked | | [`isCollaborating`](#isCollaborating) | `boolean` | | This implies if the app is in collaboration mode | | [`onPointerUpdate`](#onPointerUpdate) | Function | | Callback triggered when mouse pointer is updated. | -| [`onExportToBackend`](#onExportToBackend) | Function | | Callback triggered when link button is clicked on export dialog | | [`langCode`](#langCode) | string | `en` | Language code string | | [`renderTopRightUI`](#renderTopRightUI) | Function | | Function that renders custom UI in top right corner | | [`renderFooter `](#renderFooter) | Function | | Function that renders custom UI footer | @@ -488,10 +487,6 @@ This callback is triggered when mouse pointer is updated. 3.`pointersMap`: [`pointers map`](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L131) of the scene -#### `onExportToBackend` - -This callback is triggered when the shareable-link button is clicked in the export dialog. The link button will only be shown if this callback is passed. - ```js (exportedElements, appState, canvas) => void ``` @@ -571,12 +566,20 @@ This prop can be used to customise UI of Excalidraw. Currently we support custom | --- | --- | --- | --- | | `changeViewBackgroundColor` | boolean | true | Implies whether to show `Background color picker` | | `clearCanvas` | boolean | true | Implies whether to show `Clear canvas button` | -| `export` | boolean | true | Implies whether to show `Export button` | +| `export` | false | [exportOpts](#exportOpts) |
{ saveFileToDisk: true }
| This prop allows to customize the UI inside the export dialog. By default it shows the "saveFileToDisk". If this prop is `false` the export button will not be rendered. For more details visit [`exportOpts`](#exportOpts). | | `loadScene` | boolean | true | Implies whether to show `Load button` | -| `saveAsScene` | boolean | true | Implies whether to show `Save as button` | | `saveToActiveFile` | boolean | true | Implies whether to show `Save button` to save to current file | | `theme` | boolean | true | Implies whether to show `Theme toggle` | +#### `exportOpts` + +The below attributes can be set in `UIOptions.canvasActions.export` to customize the export dialog. If `UIOptions.canvasActions.export` is `false` the export button will not be rendered. + +| Attribute | Type | Default | Description | +| --- | --- | --- | --- | +| `saveFileToDisk` | boolean | true | Implies if save file to disk button should be shown | +| `exportToBackend` |
 (exportedElements: readonly NonDeletedExcalidrawElement[],appState: AppState,canvas: HTMLCanvasElement | null) => void 
| | This callback is triggered when the shareable-link button is clicked in the export dialog. The link button will only be shown if this callback is passed. | + #### `onPaste` This callback is triggered if passed when something is pasted into the scene. You can use this callback in case you want to do something additional when the paste event occurs. diff --git a/src/packages/excalidraw/index.tsx b/src/packages/excalidraw/index.tsx index 69af5695..b7bc53ad 100644 --- a/src/packages/excalidraw/index.tsx +++ b/src/packages/excalidraw/index.tsx @@ -7,7 +7,7 @@ import App from "../../components/App"; import "../../css/app.scss"; import "../../css/styles.scss"; -import { ExcalidrawAPIRefValue, ExcalidrawProps } from "../../types"; +import { AppProps, ExcalidrawAPIRefValue, ExcalidrawProps } from "../../types"; import { defaultLang } from "../../i18n"; import { DEFAULT_UI_OPTIONS } from "../../constants"; @@ -19,7 +19,6 @@ const Excalidraw = (props: ExcalidrawProps) => { onCollabButtonClick, isCollaborating, onPointerUpdate, - onExportToBackend, renderTopRightUI, renderFooter, langCode = defaultLang.code, @@ -38,13 +37,19 @@ const Excalidraw = (props: ExcalidrawProps) => { const canvasActions = props.UIOptions?.canvasActions; - const UIOptions = { + const UIOptions: AppProps["UIOptions"] = { canvasActions: { ...DEFAULT_UI_OPTIONS.canvasActions, ...canvasActions, }, }; + if (canvasActions?.export) { + UIOptions.canvasActions.export.saveFileToDisk = + canvasActions.export?.saveFileToDisk || + DEFAULT_UI_OPTIONS.canvasActions.export.saveFileToDisk; + } + useEffect(() => { // Block pinch-zooming on iOS outside of the content area const handleTouchMove = (event: TouchEvent) => { @@ -72,7 +77,6 @@ const Excalidraw = (props: ExcalidrawProps) => { onCollabButtonClick={onCollabButtonClick} isCollaborating={isCollaborating} onPointerUpdate={onPointerUpdate} - onExportToBackend={onExportToBackend} renderTopRightUI={renderTopRightUI} renderFooter={renderFooter} langCode={langCode} @@ -99,12 +103,58 @@ const areEqual = ( prevProps: PublicExcalidrawProps, nextProps: PublicExcalidrawProps, ) => { - const { initialData: prevInitialData, ...prev } = prevProps; - const { initialData: nextInitialData, ...next } = nextProps; + const { + initialData: prevInitialData, + UIOptions: prevUIOptions = {}, + ...prev + } = prevProps; + const { + initialData: nextInitialData, + UIOptions: nextUIOptions = {}, + ...next + } = nextProps; + + // comparing UIOptions + const prevUIOptionsKeys = Object.keys(prevUIOptions) as (keyof Partial< + typeof DEFAULT_UI_OPTIONS + >)[]; + const nextUIOptionsKeys = Object.keys(nextUIOptions) as (keyof Partial< + typeof DEFAULT_UI_OPTIONS + >)[]; + + if (prevUIOptionsKeys.length !== nextUIOptionsKeys.length) { + return false; + } + + const isUIOptionsSame = prevUIOptionsKeys.every((key) => { + if (key === "canvasActions") { + const canvasOptionKeys = Object.keys( + prevUIOptions.canvasActions!, + ) as (keyof Partial)[]; + canvasOptionKeys.every((key) => { + if ( + key === "export" && + prevUIOptions?.canvasActions?.export && + nextUIOptions?.canvasActions?.export + ) { + return ( + prevUIOptions.canvasActions.export.saveFileToDisk === + nextUIOptions.canvasActions.export.saveFileToDisk + ); + } + return ( + prevUIOptions?.canvasActions?.[key] === + nextUIOptions?.canvasActions?.[key] + ); + }); + } + return true; + }); const prevKeys = Object.keys(prevProps) as (keyof typeof prev)[]; const nextKeys = Object.keys(nextProps) as (keyof typeof next)[]; return ( + isUIOptionsSame && prevKeys.length === nextKeys.length && prevKeys.every((key) => prev[key] === next[key]) ); diff --git a/src/tests/excalidrawPackage.test.tsx b/src/tests/excalidrawPackage.test.tsx index f2149b12..628c27a4 100644 --- a/src/tests/excalidrawPackage.test.tsx +++ b/src/tests/excalidrawPackage.test.tsx @@ -178,9 +178,11 @@ describe("", () => { expect(queryByTestId(container, "load-button")).toBeNull(); }); - it("should hide save as button when saveAsScene is false", async () => { + it("should hide save as button when saveFileToDisk is false", async () => { const { container } = await render( - , + , ); expect(queryByTestId(container, "save-as-button")).toBeNull(); diff --git a/src/types.ts b/src/types.ts index 210d0690..10f99dce 100644 --- a/src/types.ts +++ b/src/types.ts @@ -178,11 +178,6 @@ export interface ExcalidrawProps { button: "down" | "up"; pointersMap: Gesture["pointers"]; }) => void; - onExportToBackend?: ( - exportedElements: readonly NonDeletedExcalidrawElement[], - appState: AppState, - canvas: HTMLCanvasElement | null, - ) => void; onPaste?: ( data: ClipboardData, event: ClipboardEvent | null, @@ -219,12 +214,20 @@ export enum UserIdleState { IDLE = "idle", } +export type ExportOpts = { + saveFileToDisk?: boolean; + onExportToBackend?: ( + exportedElements: readonly NonDeletedExcalidrawElement[], + appState: AppState, + canvas: HTMLCanvasElement | null, + ) => void; +}; + type CanvasActions = { changeViewBackgroundColor?: boolean; clearCanvas?: boolean; - export?: boolean; + export?: false | ExportOpts; loadScene?: boolean; - saveAsScene?: boolean; saveToActiveFile?: boolean; theme?: boolean; }; @@ -235,7 +238,7 @@ export type UIOptions = { export type AppProps = ExcalidrawProps & { UIOptions: { - canvasActions: Required; + canvasActions: Required & { export: ExportOpts }; }; detectScroll: boolean; handleKeyboardGlobally: boolean;