From 0d818f3810baeb7693a78517d404df4933e72d65 Mon Sep 17 00:00:00 2001 From: Aakansha Doshi Date: Mon, 29 Mar 2021 20:06:34 +0530 Subject: [PATCH] feat: Add renderCustomStats prop and expose setToastMessage API via refs to update toast (#3360) * feat: Add renderCustomStats prop to render extra stats & move storage and version to renderCustomStats * expose Api to update toast message so single instance of toast is used * rename to setToastMessage * update docs --- src/components/App.tsx | 14 ++++- src/components/Stats.tsx | 79 ++---------------------- src/excalidraw-app/CustomStats.tsx | 85 ++++++++++++++++++++++++++ src/excalidraw-app/index.tsx | 10 +++ src/packages/excalidraw/CHANGELOG.md | 1 + src/packages/excalidraw/README_NEXT.md | 6 ++ src/packages/excalidraw/index.tsx | 2 + src/types.ts | 4 ++ 8 files changed, 126 insertions(+), 75 deletions(-) create mode 100644 src/excalidraw-app/CustomStats.tsx diff --git a/src/components/App.tsx b/src/components/App.tsx index bc47f5701..16b5c001f 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -281,6 +281,7 @@ export type ExcalidrawImperativeAPI = { getAppState: () => InstanceType["state"]; setCanvasOffsets: InstanceType["setCanvasOffsets"]; importLibrary: InstanceType["importLibraryFromUrl"]; + setToastMessage: InstanceType["setToastMessage"]; readyPromise: ResolvablePromise; ready: true; }; @@ -342,6 +343,7 @@ class App extends React.Component { getAppState: () => this.state, setCanvasOffsets: this.setCanvasOffsets, importLibrary: this.importLibraryFromUrl, + setToastMessage: this.setToastMessage, } as const; if (typeof excalidrawRef === "function") { excalidrawRef(api); @@ -428,7 +430,12 @@ class App extends React.Component { viewModeEnabled, } = this.state; - const { onCollabButtonClick, onExportToBackend, renderFooter } = this.props; + const { + onCollabButtonClick, + onExportToBackend, + renderFooter, + renderCustomStats, + } = this.props; const DEFAULT_PASTE_X = canvasDOMWidth / 2; const DEFAULT_PASTE_Y = canvasDOMHeight / 2; @@ -479,6 +486,7 @@ class App extends React.Component { setAppState={this.setAppState} elements={this.scene.getElements()} onClose={this.toggleStats} + renderCustomStats={renderCustomStats} /> )} {this.state.toastMessage !== null && ( @@ -1332,6 +1340,10 @@ class App extends React.Component { this.setState({ toastMessage: null }); }; + setToastMessage = (toastMessage: string) => { + this.setState({ toastMessage }); + }; + restoreFileFromShare = async () => { try { const webShareTargetCache = await caches.open("web-share-target"); diff --git a/src/components/Stats.tsx b/src/components/Stats.tsx index 494ab3dff..3113aa109 100644 --- a/src/components/Stats.tsx +++ b/src/components/Stats.tsx @@ -1,49 +1,22 @@ -import React, { useEffect, useState } from "react"; -import { copyTextToSystemClipboard } from "../clipboard"; -import { DEFAULT_VERSION } from "../constants"; +import React from "react"; import { getCommonBounds } from "../element/bounds"; import { NonDeletedExcalidrawElement } from "../element/types"; -import { - getElementsStorageSize, - getTotalStorageSize, -} from "../excalidraw-app/data/localStorage"; import { t } from "../i18n"; import { useIsMobile } from "../is-mobile"; import { getTargetElements } from "../scene"; -import { AppState } from "../types"; -import { debounce, getVersion, nFormatter } from "../utils"; +import { AppState, ExcalidrawProps } from "../types"; import { close } from "./icons"; import { Island } from "./Island"; import "./Stats.scss"; -type StorageSizes = { scene: number; total: number }; - -const getStorageSizes = debounce((cb: (sizes: StorageSizes) => void) => { - cb({ - scene: getElementsStorageSize(), - total: getTotalStorageSize(), - }); -}, 500); - export const Stats = (props: { appState: AppState; setAppState: React.Component["setState"]; elements: readonly NonDeletedExcalidrawElement[]; onClose: () => void; + renderCustomStats: ExcalidrawProps["renderCustomStats"]; }) => { const isMobile = useIsMobile(); - const [storageSizes, setStorageSizes] = useState({ - scene: 0, - total: 0, - }); - - useEffect(() => { - getStorageSizes((sizes) => { - setStorageSizes(sizes); - }); - }); - - useEffect(() => () => getStorageSizes.cancel(), []); const boundingBox = getCommonBounds(props.elements); const selectedElements = getTargetElements(props.elements, props.appState); @@ -53,17 +26,6 @@ export const Stats = (props: { return null; } - const version = getVersion(); - let hash; - let timestamp; - - if (version !== DEFAULT_VERSION) { - timestamp = version.slice(0, 16).replace("T", " "); - hash = version.slice(21); - } else { - timestamp = t("stats.versionNotAvailable"); - } - return (
@@ -88,17 +50,7 @@ export const Stats = (props: { {t("stats.height")} {Math.round(boundingBox[3]) - Math.round(boundingBox[1])} - - {t("stats.storage")} - - - {t("stats.scene")} - {nFormatter(storageSizes.scene, 1)} - - - {t("stats.total")} - {nFormatter(storageSizes.total, 1)} - + {selectedElements.length === 1 && ( {t("stats.element")} @@ -154,28 +106,7 @@ export const Stats = (props: { )} - - {t("stats.version")} - - - { - try { - await copyTextToSystemClipboard(getVersion()); - props.setAppState({ - toastMessage: t("toast.copyToClipboard"), - }); - } catch {} - }} - title={t("stats.versionCopy")} - > - {timestamp} -
- {hash} - - + {props.renderCustomStats?.(props.elements, props.appState)}
diff --git a/src/excalidraw-app/CustomStats.tsx b/src/excalidraw-app/CustomStats.tsx new file mode 100644 index 000000000..2b79ffc1e --- /dev/null +++ b/src/excalidraw-app/CustomStats.tsx @@ -0,0 +1,85 @@ +import React, { useEffect, useState } from "react"; +import { debounce, getVersion, nFormatter } from "../utils"; +import { + getElementsStorageSize, + getTotalStorageSize, +} from "./data/localStorage"; +import { DEFAULT_VERSION } from "../constants"; +import { t } from "../i18n"; +import { copyTextToSystemClipboard } from "../clipboard"; +type StorageSizes = { scene: number; total: number }; + +const STORAGE_SIZE_TIMEOUT = 500; + +const getStorageSizes = debounce((cb: (sizes: StorageSizes) => void) => { + cb({ + scene: getElementsStorageSize(), + total: getTotalStorageSize(), + }); +}, STORAGE_SIZE_TIMEOUT); + +type Props = { + setToastMessage: (message: string) => void; +}; +const CustomStats = (props: Props) => { + const [storageSizes, setStorageSizes] = useState({ + scene: 0, + total: 0, + }); + + useEffect(() => { + getStorageSizes((sizes) => { + setStorageSizes(sizes); + }); + }); + useEffect(() => () => getStorageSizes.cancel(), []); + + const version = getVersion(); + let hash; + let timestamp; + + if (version !== DEFAULT_VERSION) { + timestamp = version.slice(0, 16).replace("T", " "); + hash = version.slice(21); + } else { + timestamp = t("stats.versionNotAvailable"); + } + + return ( + <> + + {t("stats.storage")} + + + {t("stats.scene")} + {nFormatter(storageSizes.scene, 1)} + + + {t("stats.total")} + {nFormatter(storageSizes.total, 1)} + + + {t("stats.version")} + + + { + try { + await copyTextToSystemClipboard(getVersion()); + props.setToastMessage(t("toast.copyToClipboard")); + } catch {} + }} + title={t("stats.versionCopy")} + > + {timestamp} +
+ {hash} + + + + ); +}; + +export default CustomStats; diff --git a/src/excalidraw-app/index.tsx b/src/excalidraw-app/index.tsx index b311bd50e..79233f5db 100644 --- a/src/excalidraw-app/index.tsx +++ b/src/excalidraw-app/index.tsx @@ -50,6 +50,7 @@ import { importFromLocalStorage, saveToLocalStorage, } from "./data/localStorage"; +import CustomStats from "./CustomStats"; const languageDetector = new LanguageDetector(); languageDetector.init({ @@ -323,6 +324,14 @@ const ExcalidrawWrapper = () => { [langCode], ); + const renderCustomStats = () => { + return ( + excalidrawAPI!.setToastMessage(message)} + /> + ); + }; + return ( <> { onExportToBackend={onExportToBackend} renderFooter={renderFooter} langCode={langCode} + renderCustomStats={renderCustomStats} /> {excalidrawAPI && } {errorMessage && ( diff --git a/src/packages/excalidraw/CHANGELOG.md b/src/packages/excalidraw/CHANGELOG.md index a3b1f87a3..b8f55c500 100644 --- a/src/packages/excalidraw/CHANGELOG.md +++ b/src/packages/excalidraw/CHANGELOG.md @@ -18,6 +18,7 @@ Please add the latest change on the top under the correct section. ### Features +- Add `renderCustomStats` prop to render extra stats on host, and expose `setToastMessage` API via refs which can be used to show toast with custom message [#3360](https://github.com/excalidraw/excalidraw/pull/3360). - Support passing a CSRF token when importing libraries to prevent prompting before installation. The token is passed from [https://libraries.excalidraw.com](https://libraries.excalidraw.com/) using the `token` URL key [#3329](https://github.com/excalidraw/excalidraw/pull/3329). - #### BREAKING CHANGE Use `location.hash` when importing libraries to fix installation issues. This will require host apps to add a `hashchange` listener and call the newly exposed `excalidrawAPI.importLibrary(url)` API when applicable [#3320](https://github.com/excalidraw/excalidraw/pull/3320). diff --git a/src/packages/excalidraw/README_NEXT.md b/src/packages/excalidraw/README_NEXT.md index 790c20841..c56f6388a 100644 --- a/src/packages/excalidraw/README_NEXT.md +++ b/src/packages/excalidraw/README_NEXT.md @@ -405,6 +405,7 @@ To view the full example visit :point_down: | [`onExportToBackend`](#onExportToBackend) | Function | | Callback triggered when link button is clicked on export dialog | | [`langCode`](#langCode) | string | `en` | Language code string | | [`renderFooter `](#renderFooter) | Function | | Function that renders custom UI footer | +| [`renderCustomStats`](#renderCustomStats) | Function | | Function that can be used to render custom stats on the stats dialog. | | [`viewModeEnabled`](#viewModeEnabled) | boolean | | This implies if the app is in view mode. | | [`zenModeEnabled`](#zenModeEnabled) | boolean | | This implies if the zen mode is enabled | | [`gridModeEnabled`](#gridModeEnabled) | boolean | | This implies if the grid mode is enabled | @@ -492,6 +493,7 @@ You can pass a `ref` when you want to access some excalidraw APIs. We expose the | setScrollToContent |
 (ExcalidrawElement[]) => void 
| Scroll to the nearest element to center | | setCanvasOffsets | `() => void` | Updates the offsets for the Excalidraw component so that the coordinates are computed correctly (for example the cursor position). You should call this API when your app changes the dimensions/position of the Excalidraw container, such as when toggling a sidebar. You don't have to call this when the position is changed on page scroll (we handled that ourselves). | | importLibrary | `(url: string, token?: string) => void` | Imports library from given URL. You should call this on `hashchange`, passing the `addLibrary` value if you detect it. Optionally pass a CSRF `token` to skip prompting during installation (retrievable via `token` key from the url coming from [https://libraries.excalidraw.com](https://libraries.excalidraw.com/)). | +| setToastMessage | `(message: string) => void` | This API can be used to show the toast with custom message. | #### `readyPromise` @@ -550,6 +552,10 @@ import { defaultLang, languages } from "@excalidraw/excalidraw"; A function that renders (returns JSX) custom UI footer. For example, you can use this to render a language picker that was previously being rendered by Excalidraw itself (for now, you'll need to implement your own language picker). +#### `renderCustomStats` + +A function that can be used to render custom stats (returns JSX) in the nerd stats dialog. For example you can use this prop to render the size of the elements in the storage. + #### `viewModeEnabled` This prop indicates whether the app is in `view mode`. When supplied, the value takes precedence over `intialData.appState.viewModeEnabled`, the `view mode` will be fully controlled by the host app, and users won't be able to toggle it from within the app. diff --git a/src/packages/excalidraw/index.tsx b/src/packages/excalidraw/index.tsx index 93cf7404d..533856571 100644 --- a/src/packages/excalidraw/index.tsx +++ b/src/packages/excalidraw/index.tsx @@ -30,6 +30,7 @@ const Excalidraw = (props: ExcalidrawProps) => { libraryReturnUrl, theme, name, + renderCustomStats, } = props; useEffect(() => { @@ -71,6 +72,7 @@ const Excalidraw = (props: ExcalidrawProps) => { libraryReturnUrl={libraryReturnUrl} theme={theme} name={name} + renderCustomStats={renderCustomStats} /> diff --git a/src/types.ts b/src/types.ts index 2defeed49..b2aa6d8a4 100644 --- a/src/types.ts +++ b/src/types.ts @@ -187,6 +187,10 @@ export interface ExcalidrawProps { libraryReturnUrl?: string; theme?: "dark" | "light"; name?: string; + renderCustomStats?: ( + elements: readonly NonDeletedExcalidrawElement[], + appState: AppState, + ) => JSX.Element; } export type SceneData = {