1
0
mirror of https://github.com/excalidraw/excalidraw.git synced 2024-11-02 03:25:53 +01:00

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
This commit is contained in:
Aakansha Doshi 2021-03-29 20:06:34 +05:30 committed by GitHub
parent 58a7568c9f
commit 0d818f3810
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 126 additions and 75 deletions

@ -281,6 +281,7 @@ export type ExcalidrawImperativeAPI = {
getAppState: () => InstanceType<typeof App>["state"];
setCanvasOffsets: InstanceType<typeof App>["setCanvasOffsets"];
importLibrary: InstanceType<typeof App>["importLibraryFromUrl"];
setToastMessage: InstanceType<typeof App>["setToastMessage"];
readyPromise: ResolvablePromise<ExcalidrawImperativeAPI>;
ready: true;
};
@ -342,6 +343,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
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<ExcalidrawProps, AppState> {
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<ExcalidrawProps, AppState> {
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<ExcalidrawProps, AppState> {
this.setState({ toastMessage: null });
};
setToastMessage = (toastMessage: string) => {
this.setState({ toastMessage });
};
restoreFileFromShare = async () => {
try {
const webShareTargetCache = await caches.open("web-share-target");

@ -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<any, AppState>["setState"];
elements: readonly NonDeletedExcalidrawElement[];
onClose: () => void;
renderCustomStats: ExcalidrawProps["renderCustomStats"];
}) => {
const isMobile = useIsMobile();
const [storageSizes, setStorageSizes] = useState<StorageSizes>({
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 (
<div className="Stats">
<Island padding={2}>
@ -88,17 +50,7 @@ export const Stats = (props: {
<td>{t("stats.height")}</td>
<td>{Math.round(boundingBox[3]) - Math.round(boundingBox[1])}</td>
</tr>
<tr>
<th colSpan={2}>{t("stats.storage")}</th>
</tr>
<tr>
<td>{t("stats.scene")}</td>
<td>{nFormatter(storageSizes.scene, 1)}</td>
</tr>
<tr>
<td>{t("stats.total")}</td>
<td>{nFormatter(storageSizes.total, 1)}</td>
</tr>
{selectedElements.length === 1 && (
<tr>
<th colSpan={2}>{t("stats.element")}</th>
@ -154,28 +106,7 @@ export const Stats = (props: {
</td>
</tr>
)}
<tr>
<th colSpan={2}>{t("stats.version")}</th>
</tr>
<tr>
<td
colSpan={2}
style={{ textAlign: "center", cursor: "pointer" }}
onClick={async () => {
try {
await copyTextToSystemClipboard(getVersion());
props.setAppState({
toastMessage: t("toast.copyToClipboard"),
});
} catch {}
}}
title={t("stats.versionCopy")}
>
{timestamp}
<br />
{hash}
</td>
</tr>
{props.renderCustomStats?.(props.elements, props.appState)}
</tbody>
</table>
</Island>

@ -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<StorageSizes>({
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 (
<>
<tr>
<th colSpan={2}>{t("stats.storage")}</th>
</tr>
<tr>
<td>{t("stats.scene")}</td>
<td>{nFormatter(storageSizes.scene, 1)}</td>
</tr>
<tr>
<td>{t("stats.total")}</td>
<td>{nFormatter(storageSizes.total, 1)}</td>
</tr>
<tr>
<th colSpan={2}>{t("stats.version")}</th>
</tr>
<tr>
<td
colSpan={2}
style={{ textAlign: "center", cursor: "pointer" }}
onClick={async () => {
try {
await copyTextToSystemClipboard(getVersion());
props.setToastMessage(t("toast.copyToClipboard"));
} catch {}
}}
title={t("stats.versionCopy")}
>
{timestamp}
<br />
{hash}
</td>
</tr>
</>
);
};
export default CustomStats;

@ -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 (
<CustomStats
setToastMessage={(message) => excalidrawAPI!.setToastMessage(message)}
/>
);
};
return (
<>
<Excalidraw
@ -337,6 +346,7 @@ const ExcalidrawWrapper = () => {
onExportToBackend={onExportToBackend}
renderFooter={renderFooter}
langCode={langCode}
renderCustomStats={renderCustomStats}
/>
{excalidrawAPI && <CollabWrapper excalidrawAPI={excalidrawAPI} />}
{errorMessage && (

@ -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).

@ -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 | <pre> (<a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L78">ExcalidrawElement[]</a>) => void </pre> | 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.

@ -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}
/>
</IsMobileProvider>
</InitializeApp>

@ -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 = {