Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0e3a5b2042 | |||
| c819b653bf | |||
| 60cea7a0c2 | |||
| d63b6a3469 | |||
| 0912fe1c93 | |||
| 360310de31 | |||
| 716c78e930 | |||
| ba48974351 |
@@ -117,6 +117,7 @@
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.visually-hidden {
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -35,7 +35,7 @@ export {
|
||||
actionChangeProjectName,
|
||||
actionChangeExportBackground,
|
||||
actionSaveToActiveFile,
|
||||
actionSaveAsScene,
|
||||
actionSaveFileToDisk,
|
||||
actionLoadScene,
|
||||
} from "./actionExport";
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ export type ActionName =
|
||||
| "changeExportBackground"
|
||||
| "changeExportEmbedScene"
|
||||
| "saveToActiveFile"
|
||||
| "saveAsScene"
|
||||
| "saveFileToDisk"
|
||||
| "loadScene"
|
||||
| "duplicateSelection"
|
||||
| "deleteSelectedElements"
|
||||
|
||||
+25
-4
@@ -452,7 +452,6 @@ class App extends React.Component<AppProps, AppState> {
|
||||
|
||||
const {
|
||||
onCollabButtonClick,
|
||||
onExportToBackend,
|
||||
renderTopRightUI,
|
||||
renderFooter,
|
||||
renderCustomStats,
|
||||
@@ -493,7 +492,6 @@ class App extends React.Component<AppProps, AppState> {
|
||||
toggleZenMode={this.toggleZenMode}
|
||||
langCode={getLanguage().code}
|
||||
isCollaborating={this.props.isCollaborating || false}
|
||||
onExportToBackend={onExportToBackend}
|
||||
renderTopRightUI={renderTopRightUI}
|
||||
renderCustomFooter={renderFooter}
|
||||
viewModeEnabled={viewModeEnabled}
|
||||
@@ -1860,9 +1858,29 @@ class App extends React.Component<AppProps, AppState> {
|
||||
private getElementAtPosition(
|
||||
x: number,
|
||||
y: number,
|
||||
opts?: {
|
||||
/** if true, returns the first selected element (with highest z-index)
|
||||
of all hit elements */
|
||||
preferSelected?: boolean;
|
||||
cycleElementsUnderCursor?: boolean;
|
||||
},
|
||||
): NonDeleted<ExcalidrawElement> | null {
|
||||
const allHitElements = this.getElementsAtPosition(x, y);
|
||||
if (allHitElements.length > 1) {
|
||||
if (opts?.preferSelected) {
|
||||
for (let index = allHitElements.length - 1; index > -1; index--) {
|
||||
if (this.state.selectedElementIds[allHitElements[index].id]) {
|
||||
return allHitElements[index];
|
||||
}
|
||||
}
|
||||
} else if (opts?.cycleElementsUnderCursor) {
|
||||
const selectedIdx = allHitElements.findIndex(
|
||||
(element) => this.state.selectedElementIds[element.id],
|
||||
);
|
||||
return selectedIdx > 0
|
||||
? allHitElements[selectedIdx - 1]
|
||||
: allHitElements[allHitElements.length - 1];
|
||||
}
|
||||
const elementWithHighestZIndex =
|
||||
allHitElements[allHitElements.length - 1];
|
||||
// If we're hitting element with highest z-index only on its bounding box
|
||||
@@ -2736,10 +2754,13 @@ class App extends React.Component<AppProps, AppState> {
|
||||
|
||||
// hitElement may already be set above, so check first
|
||||
pointerDownState.hit.element =
|
||||
pointerDownState.hit.element ??
|
||||
(!event[KEYS.CTRL_OR_CMD] ? pointerDownState.hit.element : null) ??
|
||||
this.getElementAtPosition(
|
||||
pointerDownState.origin.x,
|
||||
pointerDownState.origin.y,
|
||||
{
|
||||
cycleElementsUnderCursor: event[KEYS.CTRL_OR_CMD],
|
||||
},
|
||||
);
|
||||
|
||||
// For overlapped elements one position may hit
|
||||
@@ -3937,7 +3958,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
event.preventDefault();
|
||||
|
||||
const { x, y } = viewportCoordsToSceneCoords(event, this.state);
|
||||
const element = this.getElementAtPosition(x, y);
|
||||
const element = this.getElementAtPosition(x, y, { preferSelected: true });
|
||||
|
||||
const type = element ? "element" : "canvas";
|
||||
|
||||
|
||||
@@ -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";
|
||||
@@ -22,35 +22,40 @@ const JSONExportModal = ({
|
||||
elements,
|
||||
appState,
|
||||
actionManager,
|
||||
onExportToBackend,
|
||||
exportOpts,
|
||||
canvas,
|
||||
}: {
|
||||
appState: AppState;
|
||||
elements: readonly NonDeletedExcalidrawElement[];
|
||||
actionManager: ActionsManagerInterface;
|
||||
onExportToBackend?: ExportCB;
|
||||
onCloseRequest: () => void;
|
||||
exportOpts: ExportOpts;
|
||||
canvas: HTMLCanvasElement | null;
|
||||
}) => {
|
||||
const { onExportToBackend } = exportOpts;
|
||||
return (
|
||||
<div className="ExportDialog ExportDialog--json">
|
||||
<div className="ExportDialog-cards">
|
||||
<Card color="lime">
|
||||
<div className="Card-icon">{exportToFileIcon}</div>
|
||||
<h2>{t("exportDialog.disk_title")}</h2>
|
||||
<div className="Card-details">
|
||||
{t("exportDialog.disk_details")}
|
||||
{!fsSupported && actionManager.renderAction("changeProjectName")}
|
||||
</div>
|
||||
<ToolButton
|
||||
className="Card-button"
|
||||
type="button"
|
||||
title={t("exportDialog.disk_button")}
|
||||
aria-label={t("exportDialog.disk_button")}
|
||||
showAriaLabel={true}
|
||||
onClick={() => {
|
||||
actionManager.executeAction(actionSaveAsScene);
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
{exportOpts.saveFileToDisk && (
|
||||
<Card color="lime">
|
||||
<div className="Card-icon">{exportToFileIcon}</div>
|
||||
<h2>{t("exportDialog.disk_title")}</h2>
|
||||
<div className="Card-details">
|
||||
{t("exportDialog.disk_details")}
|
||||
{!fsSupported && actionManager.renderAction("changeProjectName")}
|
||||
</div>
|
||||
<ToolButton
|
||||
className="Card-button"
|
||||
type="button"
|
||||
title={t("exportDialog.disk_button")}
|
||||
aria-label={t("exportDialog.disk_button")}
|
||||
showAriaLabel={true}
|
||||
onClick={() => {
|
||||
actionManager.executeAction(actionSaveFileToDisk);
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
)}
|
||||
{onExportToBackend && (
|
||||
<Card color="pink">
|
||||
<div className="Card-icon">{link}</div>
|
||||
@@ -62,10 +67,12 @@ const JSONExportModal = ({
|
||||
title={t("exportDialog.link_button")}
|
||||
aria-label={t("exportDialog.link_button")}
|
||||
showAriaLabel={true}
|
||||
onClick={() => onExportToBackend(elements)}
|
||||
onClick={() => onExportToBackend(elements, appState, canvas)}
|
||||
/>
|
||||
</Card>
|
||||
)}
|
||||
{exportOpts.renderCustomUI &&
|
||||
exportOpts.renderCustomUI(elements, appState, canvas)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -75,12 +82,14 @@ export const JSONExportDialog = ({
|
||||
elements,
|
||||
appState,
|
||||
actionManager,
|
||||
onExportToBackend,
|
||||
exportOpts,
|
||||
canvas,
|
||||
}: {
|
||||
appState: AppState;
|
||||
elements: readonly NonDeletedExcalidrawElement[];
|
||||
actionManager: ActionsManagerInterface;
|
||||
onExportToBackend?: ExportCB;
|
||||
exportOpts: ExportOpts;
|
||||
canvas: HTMLCanvasElement | null;
|
||||
}) => {
|
||||
const [modalIsShown, setModalIsShown] = useState(false);
|
||||
|
||||
@@ -107,8 +116,9 @@ export const JSONExportDialog = ({
|
||||
elements={elements}
|
||||
appState={appState}
|
||||
actionManager={actionManager}
|
||||
onExportToBackend={onExportToBackend}
|
||||
onCloseRequest={handleClose}
|
||||
exportOpts={exportOpts}
|
||||
canvas={canvas}
|
||||
/>
|
||||
</Dialog>
|
||||
)}
|
||||
|
||||
@@ -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,20 +387,14 @@ const LayerUI = ({
|
||||
elements={elements}
|
||||
appState={appState}
|
||||
actionManager={actionManager}
|
||||
onExportToBackend={
|
||||
onExportToBackend
|
||||
? (elements) => {
|
||||
onExportToBackend &&
|
||||
onExportToBackend(elements, appState, canvas);
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
exportOpts={UIOptions.canvasActions.export}
|
||||
canvas={canvas}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const renderImageExportDialog = () => {
|
||||
if (!UIOptions.canvasActions.export) {
|
||||
if (!UIOptions.canvasActions.saveAsImage) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -131,11 +131,11 @@ 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,
|
||||
saveAsImage: true,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -307,7 +307,19 @@ export const duplicateElement = <TElement extends Mutable<ExcalidrawElement>>(
|
||||
overrides?: Partial<TElement>,
|
||||
): TElement => {
|
||||
let copy: TElement = deepCopyElement(element);
|
||||
copy.id = process.env.NODE_ENV === "test" ? `${copy.id}_copy` : randomId();
|
||||
if (process.env.NODE_ENV === "test") {
|
||||
copy.id = `${copy.id}_copy`;
|
||||
// `window.h` may not be defined in some unit tests
|
||||
if (
|
||||
window.h?.app
|
||||
?.getSceneElementsIncludingDeleted()
|
||||
.find((el) => el.id === copy.id)
|
||||
) {
|
||||
copy.id += "_copy";
|
||||
}
|
||||
} else {
|
||||
copy.id = randomId();
|
||||
}
|
||||
copy.seed = randomInteger();
|
||||
copy.groupIds = getNewGroupIdsForDuplication(
|
||||
copy.groupIds,
|
||||
|
||||
@@ -424,7 +424,13 @@ const ExcalidrawWrapper = () => {
|
||||
onCollabButtonClick={collabAPI?.onCollabButtonClick}
|
||||
isCollaborating={collabAPI?.isCollaborating()}
|
||||
onPointerUpdate={collabAPI?.onPointerUpdate}
|
||||
onExportToBackend={onExportToBackend}
|
||||
UIOptions={{
|
||||
canvasActions: {
|
||||
export: {
|
||||
onExportToBackend,
|
||||
},
|
||||
},
|
||||
}}
|
||||
renderTopRightUI={renderTopRightUI}
|
||||
renderFooter={renderFooter}
|
||||
langCode={langCode}
|
||||
|
||||
+2
-1
@@ -21,6 +21,7 @@ interface DehydratedHistoryEntry {
|
||||
const clearAppStatePropertiesForHistory = (appState: AppState) => {
|
||||
return {
|
||||
selectedElementIds: appState.selectedElementIds,
|
||||
selectedGroupIds: appState.selectedGroupIds,
|
||||
viewBackgroundColor: appState.viewBackgroundColor,
|
||||
editingLinearElement: appState.editingLinearElement,
|
||||
editingGroupId: appState.editingGroupId,
|
||||
@@ -169,7 +170,7 @@ class History {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (key === "selectedElementIds") {
|
||||
if (key === "selectedElementIds" || key === "selectedGroupIds") {
|
||||
continue;
|
||||
}
|
||||
if (nextEntry.appState[key] !== lastEntry.appState[key]) {
|
||||
|
||||
@@ -13,8 +13,24 @@ Please add the latest change on the top under the correct section.
|
||||
|
||||
## Unreleased
|
||||
|
||||
**This changes are not yet released but you can still try it out in [@excalidraw/excalidraw-next](https://www.npmjs.com/package/@excalidraw/excalidraw-next).**
|
||||
|
||||
## Excalidraw API
|
||||
|
||||
### Features
|
||||
|
||||
- Added prop `UIOptions.canvasActions.export.renderCustomUI` to support Custom UI rendering inside export dialog [#3666](https://github.com/excalidraw/excalidraw/pull/3666).
|
||||
- Added prop `UIOptions.canvasActions.saveAsImage` to show/hide the **Save as image** button in the canvas actions. Defauls to `true` hence the **Save as Image** button is rendered [#3662](https://github.com/excalidraw/excalidraw/pull/3662).
|
||||
|
||||
- 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
|
||||
@@ -22,6 +38,8 @@ Please add the latest change on the top under the correct section.
|
||||
- Removed `shouldAddWatermark: boolean` attribute from options for [export](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#export-utilities) APIs [#3639](https://github.com/excalidraw/excalidraw/pull/3639).
|
||||
- Removed `appState.shouldAddWatermark` so in case you were passing `shouldAddWatermark` in [initialData.AppState](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L42) it will not work anymore.
|
||||
|
||||
---
|
||||
|
||||
## 0.8.0 (2021-05-15)
|
||||
|
||||
## Excalidraw API
|
||||
|
||||
@@ -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,11 +566,21 @@ 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) | <pre>{ saveFileToDisk: true }</pre> | 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` |
|
||||
| `saveAsImage` | boolean | true | Implies whether to show `Save as image button` |
|
||||
|
||||
#### `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` | <pre> (exportedElements: readonly NonDeletedExcalidrawElement[],appState: AppState,canvas: HTMLCanvasElement | null) => void </pre> | | 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. |
|
||||
| `renderCustomUI` | <pre> (exportedElements: readonly NonDeletedExcalidrawElement[],appState: AppState,canvas: HTMLCanvasElement | null) => void </pre> | | This callback should be supplied if you want to render custom UI in the export dialog. |
|
||||
|
||||
#### `onPaste`
|
||||
|
||||
|
||||
@@ -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<typeof DEFAULT_UI_OPTIONS.canvasActions>)[];
|
||||
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])
|
||||
);
|
||||
|
||||
@@ -115,6 +115,7 @@ Object {
|
||||
"editingLinearElement": null,
|
||||
"name": "Untitled-201933152653",
|
||||
"selectedElementIds": Object {},
|
||||
"selectedGroupIds": Object {},
|
||||
"viewBackgroundColor": "#ffffff",
|
||||
},
|
||||
"elements": Array [],
|
||||
@@ -127,6 +128,7 @@ Object {
|
||||
"selectedElementIds": Object {
|
||||
"id0": true,
|
||||
},
|
||||
"selectedGroupIds": Object {},
|
||||
"viewBackgroundColor": "#ffffff",
|
||||
},
|
||||
"elements": Array [
|
||||
@@ -304,6 +306,7 @@ Object {
|
||||
"editingLinearElement": null,
|
||||
"name": "Untitled-201933152653",
|
||||
"selectedElementIds": Object {},
|
||||
"selectedGroupIds": Object {},
|
||||
"viewBackgroundColor": "#ffffff",
|
||||
},
|
||||
"elements": Array [],
|
||||
@@ -316,6 +319,7 @@ Object {
|
||||
"selectedElementIds": Object {
|
||||
"id0": true,
|
||||
},
|
||||
"selectedGroupIds": Object {},
|
||||
"viewBackgroundColor": "#ffffff",
|
||||
},
|
||||
"elements": Array [
|
||||
@@ -352,6 +356,7 @@ Object {
|
||||
"selectedElementIds": Object {
|
||||
"id1": true,
|
||||
},
|
||||
"selectedGroupIds": Object {},
|
||||
"viewBackgroundColor": "#ffffff",
|
||||
},
|
||||
"elements": Array [
|
||||
@@ -411,6 +416,7 @@ Object {
|
||||
"selectedElementIds": Object {
|
||||
"id0": true,
|
||||
},
|
||||
"selectedGroupIds": Object {},
|
||||
"viewBackgroundColor": "#ffffff",
|
||||
},
|
||||
"elements": Array [
|
||||
@@ -611,6 +617,7 @@ Object {
|
||||
"editingLinearElement": null,
|
||||
"name": "Untitled-201933152653",
|
||||
"selectedElementIds": Object {},
|
||||
"selectedGroupIds": Object {},
|
||||
"viewBackgroundColor": "#ffffff",
|
||||
},
|
||||
"elements": Array [],
|
||||
@@ -623,6 +630,7 @@ Object {
|
||||
"selectedElementIds": Object {
|
||||
"id0": true,
|
||||
},
|
||||
"selectedGroupIds": Object {},
|
||||
"viewBackgroundColor": "#ffffff",
|
||||
},
|
||||
"elements": Array [
|
||||
@@ -659,6 +667,7 @@ Object {
|
||||
"selectedElementIds": Object {
|
||||
"id1": true,
|
||||
},
|
||||
"selectedGroupIds": Object {},
|
||||
"viewBackgroundColor": "#ffffff",
|
||||
},
|
||||
"elements": Array [
|
||||
@@ -718,6 +727,7 @@ Object {
|
||||
"selectedElementIds": Object {
|
||||
"id0": true,
|
||||
},
|
||||
"selectedGroupIds": Object {},
|
||||
"viewBackgroundColor": "#ffffff",
|
||||
},
|
||||
"elements": Array [
|
||||
@@ -892,6 +902,7 @@ Object {
|
||||
"editingLinearElement": null,
|
||||
"name": "Untitled-201933152653",
|
||||
"selectedElementIds": Object {},
|
||||
"selectedGroupIds": Object {},
|
||||
"viewBackgroundColor": "#ffffff",
|
||||
},
|
||||
"elements": Array [],
|
||||
@@ -904,6 +915,7 @@ Object {
|
||||
"selectedElementIds": Object {
|
||||
"id0": true,
|
||||
},
|
||||
"selectedGroupIds": Object {},
|
||||
"viewBackgroundColor": "#ffffff",
|
||||
},
|
||||
"elements": Array [
|
||||
@@ -1053,6 +1065,7 @@ Object {
|
||||
"editingLinearElement": null,
|
||||
"name": "Untitled-201933152653",
|
||||
"selectedElementIds": Object {},
|
||||
"selectedGroupIds": Object {},
|
||||
"viewBackgroundColor": "#ffffff",
|
||||
},
|
||||
"elements": Array [],
|
||||
@@ -1065,6 +1078,7 @@ Object {
|
||||
"selectedElementIds": Object {
|
||||
"id0": true,
|
||||
},
|
||||
"selectedGroupIds": Object {},
|
||||
"viewBackgroundColor": "#ffffff",
|
||||
},
|
||||
"elements": Array [
|
||||
@@ -1099,6 +1113,7 @@ Object {
|
||||
"editingLinearElement": null,
|
||||
"name": "Untitled-201933152653",
|
||||
"selectedElementIds": Object {},
|
||||
"selectedGroupIds": Object {},
|
||||
"viewBackgroundColor": "#ffffff",
|
||||
},
|
||||
"elements": Array [
|
||||
@@ -1276,6 +1291,7 @@ Object {
|
||||
"editingLinearElement": null,
|
||||
"name": "Untitled-201933152653",
|
||||
"selectedElementIds": Object {},
|
||||
"selectedGroupIds": Object {},
|
||||
"viewBackgroundColor": "#ffffff",
|
||||
},
|
||||
"elements": Array [],
|
||||
@@ -1288,6 +1304,7 @@ Object {
|
||||
"selectedElementIds": Object {
|
||||
"id0": true,
|
||||
},
|
||||
"selectedGroupIds": Object {},
|
||||
"viewBackgroundColor": "#ffffff",
|
||||
},
|
||||
"elements": Array [
|
||||
@@ -1324,6 +1341,7 @@ Object {
|
||||
"selectedElementIds": Object {
|
||||
"id0_copy": true,
|
||||
},
|
||||
"selectedGroupIds": Object {},
|
||||
"viewBackgroundColor": "#ffffff",
|
||||
},
|
||||
"elements": Array [
|
||||
@@ -1534,6 +1552,7 @@ Object {
|
||||
"editingLinearElement": null,
|
||||
"name": "Untitled-201933152653",
|
||||
"selectedElementIds": Object {},
|
||||
"selectedGroupIds": Object {},
|
||||
"viewBackgroundColor": "#ffffff",
|
||||
},
|
||||
"elements": Array [],
|
||||
@@ -1546,6 +1565,7 @@ Object {
|
||||
"selectedElementIds": Object {
|
||||
"id0": true,
|
||||
},
|
||||
"selectedGroupIds": Object {},
|
||||
"viewBackgroundColor": "#ffffff",
|
||||
},
|
||||
"elements": Array [
|
||||
@@ -1582,6 +1602,7 @@ Object {
|
||||
"selectedElementIds": Object {
|
||||
"id1": true,
|
||||
},
|
||||
"selectedGroupIds": Object {},
|
||||
"viewBackgroundColor": "#ffffff",
|
||||
},
|
||||
"elements": Array [
|
||||
@@ -1643,6 +1664,9 @@ Object {
|
||||
"id1": true,
|
||||
"id2": true,
|
||||
},
|
||||
"selectedGroupIds": Object {
|
||||
"id3": true,
|
||||
},
|
||||
"viewBackgroundColor": "#ffffff",
|
||||
},
|
||||
"elements": Array [
|
||||
@@ -1847,6 +1871,7 @@ Object {
|
||||
"editingLinearElement": null,
|
||||
"name": "Untitled-201933152653",
|
||||
"selectedElementIds": Object {},
|
||||
"selectedGroupIds": Object {},
|
||||
"viewBackgroundColor": "#ffffff",
|
||||
},
|
||||
"elements": Array [],
|
||||
@@ -1859,6 +1884,7 @@ Object {
|
||||
"selectedElementIds": Object {
|
||||
"id0": true,
|
||||
},
|
||||
"selectedGroupIds": Object {},
|
||||
"viewBackgroundColor": "#ffffff",
|
||||
},
|
||||
"elements": Array [
|
||||
@@ -1895,6 +1921,7 @@ Object {
|
||||
"selectedElementIds": Object {
|
||||
"id1": true,
|
||||
},
|
||||
"selectedGroupIds": Object {},
|
||||
"viewBackgroundColor": "#ffffff",
|
||||
},
|
||||
"elements": Array [
|
||||
@@ -1954,6 +1981,7 @@ Object {
|
||||
"selectedElementIds": Object {
|
||||
"id1": true,
|
||||
},
|
||||
"selectedGroupIds": Object {},
|
||||
"viewBackgroundColor": "#ffffff",
|
||||
},
|
||||
"elements": Array [
|
||||
@@ -2013,6 +2041,7 @@ Object {
|
||||
"selectedElementIds": Object {
|
||||
"id1": true,
|
||||
},
|
||||
"selectedGroupIds": Object {},
|
||||
"viewBackgroundColor": "#ffffff",
|
||||
},
|
||||
"elements": Array [
|
||||
@@ -2072,6 +2101,7 @@ Object {
|
||||
"selectedElementIds": Object {
|
||||
"id1": true,
|
||||
},
|
||||
"selectedGroupIds": Object {},
|
||||
"viewBackgroundColor": "#ffffff",
|
||||
},
|
||||
"elements": Array [
|
||||
@@ -2131,6 +2161,7 @@ Object {
|
||||
"selectedElementIds": Object {
|
||||
"id1": true,
|
||||
},
|
||||
"selectedGroupIds": Object {},
|
||||
"viewBackgroundColor": "#ffffff",
|
||||
},
|
||||
"elements": Array [
|
||||
@@ -2190,6 +2221,7 @@ Object {
|
||||
"selectedElementIds": Object {
|
||||
"id1": true,
|
||||
},
|
||||
"selectedGroupIds": Object {},
|
||||
"viewBackgroundColor": "#ffffff",
|
||||
},
|
||||
"elements": Array [
|
||||
@@ -2249,6 +2281,7 @@ Object {
|
||||
"selectedElementIds": Object {
|
||||
"id1": true,
|
||||
},
|
||||
"selectedGroupIds": Object {},
|
||||
"viewBackgroundColor": "#ffffff",
|
||||
},
|
||||
"elements": Array [
|
||||
@@ -2308,6 +2341,7 @@ Object {
|
||||
"selectedElementIds": Object {
|
||||
"id1": true,
|
||||
},
|
||||
"selectedGroupIds": Object {},
|
||||
"viewBackgroundColor": "#ffffff",
|
||||
},
|
||||
"elements": Array [
|
||||
@@ -2367,6 +2401,7 @@ Object {
|
||||
"selectedElementIds": Object {
|
||||
"id0": true,
|
||||
},
|
||||
"selectedGroupIds": Object {},
|
||||
"viewBackgroundColor": "#ffffff",
|
||||
},
|
||||
"elements": Array [
|
||||
@@ -2567,6 +2602,7 @@ Object {
|
||||
"editingLinearElement": null,
|
||||
"name": "Untitled-201933152653",
|
||||
"selectedElementIds": Object {},
|
||||
"selectedGroupIds": Object {},
|
||||
"viewBackgroundColor": "#ffffff",
|
||||
},
|
||||
"elements": Array [],
|
||||
@@ -2579,6 +2615,7 @@ Object {
|
||||
"selectedElementIds": Object {
|
||||
"id0": true,
|
||||
},
|
||||
"selectedGroupIds": Object {},
|
||||
"viewBackgroundColor": "#ffffff",
|
||||
},
|
||||
"elements": Array [
|
||||
@@ -2615,6 +2652,7 @@ Object {
|
||||
"selectedElementIds": Object {
|
||||
"id1": true,
|
||||
},
|
||||
"selectedGroupIds": Object {},
|
||||
"viewBackgroundColor": "#ffffff",
|
||||
},
|
||||
"elements": Array [
|
||||
@@ -2674,6 +2712,7 @@ Object {
|
||||
"selectedElementIds": Object {
|
||||
"id1": true,
|
||||
},
|
||||
"selectedGroupIds": Object {},
|
||||
"viewBackgroundColor": "#ffffff",
|
||||
},
|
||||
"elements": Array [
|
||||
@@ -2874,6 +2913,7 @@ Object {
|
||||
"editingLinearElement": null,
|
||||
"name": "Untitled-201933152653",
|
||||
"selectedElementIds": Object {},
|
||||
"selectedGroupIds": Object {},
|
||||
"viewBackgroundColor": "#ffffff",
|
||||
},
|
||||
"elements": Array [],
|
||||
@@ -2886,6 +2926,7 @@ Object {
|
||||
"selectedElementIds": Object {
|
||||
"id0": true,
|
||||
},
|
||||
"selectedGroupIds": Object {},
|
||||
"viewBackgroundColor": "#ffffff",
|
||||
},
|
||||
"elements": Array [
|
||||
@@ -2922,6 +2963,7 @@ Object {
|
||||
"selectedElementIds": Object {
|
||||
"id1": true,
|
||||
},
|
||||
"selectedGroupIds": Object {},
|
||||
"viewBackgroundColor": "#ffffff",
|
||||
},
|
||||
"elements": Array [
|
||||
@@ -2981,6 +3023,7 @@ Object {
|
||||
"selectedElementIds": Object {
|
||||
"id1": true,
|
||||
},
|
||||
"selectedGroupIds": Object {},
|
||||
"viewBackgroundColor": "#ffffff",
|
||||
},
|
||||
"elements": Array [
|
||||
@@ -3185,6 +3228,7 @@ Object {
|
||||
"editingLinearElement": null,
|
||||
"name": "Untitled-201933152653",
|
||||
"selectedElementIds": Object {},
|
||||
"selectedGroupIds": Object {},
|
||||
"viewBackgroundColor": "#ffffff",
|
||||
},
|
||||
"elements": Array [],
|
||||
@@ -3197,6 +3241,7 @@ Object {
|
||||
"selectedElementIds": Object {
|
||||
"id0": true,
|
||||
},
|
||||
"selectedGroupIds": Object {},
|
||||
"viewBackgroundColor": "#ffffff",
|
||||
},
|
||||
"elements": Array [
|
||||
@@ -3233,6 +3278,7 @@ Object {
|
||||
"selectedElementIds": Object {
|
||||
"id1": true,
|
||||
},
|
||||
"selectedGroupIds": Object {},
|
||||
"viewBackgroundColor": "#ffffff",
|
||||
},
|
||||
"elements": Array [
|
||||
@@ -3294,6 +3340,9 @@ Object {
|
||||
"id1": true,
|
||||
"id2": true,
|
||||
},
|
||||
"selectedGroupIds": Object {
|
||||
"id3": true,
|
||||
},
|
||||
"viewBackgroundColor": "#ffffff",
|
||||
},
|
||||
"elements": Array [
|
||||
@@ -3359,6 +3408,7 @@ Object {
|
||||
"id1": true,
|
||||
"id2": true,
|
||||
},
|
||||
"selectedGroupIds": Object {},
|
||||
"viewBackgroundColor": "#ffffff",
|
||||
},
|
||||
"elements": Array [
|
||||
@@ -3565,6 +3615,7 @@ Object {
|
||||
"editingLinearElement": null,
|
||||
"name": "Untitled-201933152653",
|
||||
"selectedElementIds": Object {},
|
||||
"selectedGroupIds": Object {},
|
||||
"viewBackgroundColor": "#ffffff",
|
||||
},
|
||||
"elements": Array [],
|
||||
@@ -3577,6 +3628,7 @@ Object {
|
||||
"selectedElementIds": Object {
|
||||
"id0": true,
|
||||
},
|
||||
"selectedGroupIds": Object {},
|
||||
"viewBackgroundColor": "#ffffff",
|
||||
},
|
||||
"elements": Array [
|
||||
@@ -3613,6 +3665,7 @@ Object {
|
||||
"selectedElementIds": Object {
|
||||
"id1": true,
|
||||
},
|
||||
"selectedGroupIds": Object {},
|
||||
"viewBackgroundColor": "#ffffff",
|
||||
},
|
||||
"elements": Array [
|
||||
@@ -3825,6 +3878,7 @@ Object {
|
||||
"editingLinearElement": null,
|
||||
"name": "Untitled-201933152653",
|
||||
"selectedElementIds": Object {},
|
||||
"selectedGroupIds": Object {},
|
||||
"viewBackgroundColor": "#ffffff",
|
||||
},
|
||||
"elements": Array [],
|
||||
@@ -3837,6 +3891,7 @@ Object {
|
||||
"selectedElementIds": Object {
|
||||
"id0": true,
|
||||
},
|
||||
"selectedGroupIds": Object {},
|
||||
"viewBackgroundColor": "#ffffff",
|
||||
},
|
||||
"elements": Array [
|
||||
@@ -3873,6 +3928,7 @@ Object {
|
||||
"selectedElementIds": Object {
|
||||
"id1": true,
|
||||
},
|
||||
"selectedGroupIds": Object {},
|
||||
"viewBackgroundColor": "#ffffff",
|
||||
},
|
||||
"elements": Array [
|
||||
@@ -3935,6 +3991,9 @@ Object {
|
||||
"id2": true,
|
||||
"id3": true,
|
||||
},
|
||||
"selectedGroupIds": Object {
|
||||
"id4": true,
|
||||
},
|
||||
"viewBackgroundColor": "#ffffff",
|
||||
},
|
||||
"elements": Array [
|
||||
@@ -4085,6 +4144,7 @@ Object {
|
||||
"editingLinearElement": null,
|
||||
"name": "Untitled-201933152653",
|
||||
"selectedElementIds": Object {},
|
||||
"selectedGroupIds": Object {},
|
||||
"viewBackgroundColor": "#ffffff",
|
||||
},
|
||||
"elements": Array [],
|
||||
@@ -4175,6 +4235,84 @@ Object {
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`contextMenu element shows context menu for element: [end of test] appState 2`] = `
|
||||
Object {
|
||||
"collaborators": Map {},
|
||||
"currentChartType": "bar",
|
||||
"currentItemBackgroundColor": "transparent",
|
||||
"currentItemEndArrowhead": "arrow",
|
||||
"currentItemFillStyle": "hachure",
|
||||
"currentItemFontFamily": 1,
|
||||
"currentItemFontSize": 20,
|
||||
"currentItemLinearStrokeSharpness": "round",
|
||||
"currentItemOpacity": 100,
|
||||
"currentItemRoughness": 1,
|
||||
"currentItemStartArrowhead": null,
|
||||
"currentItemStrokeColor": "#000000",
|
||||
"currentItemStrokeSharpness": "sharp",
|
||||
"currentItemStrokeStyle": "solid",
|
||||
"currentItemStrokeWidth": 1,
|
||||
"currentItemTextAlign": "left",
|
||||
"cursorButton": "up",
|
||||
"draggingElement": null,
|
||||
"editingElement": null,
|
||||
"editingGroupId": null,
|
||||
"editingLinearElement": null,
|
||||
"elementLocked": false,
|
||||
"elementType": "selection",
|
||||
"errorMessage": null,
|
||||
"exportBackground": true,
|
||||
"exportEmbedScene": false,
|
||||
"exportWithDarkMode": false,
|
||||
"fileHandle": null,
|
||||
"gridSize": null,
|
||||
"height": 100,
|
||||
"isBindingEnabled": true,
|
||||
"isLibraryOpen": false,
|
||||
"isLoading": false,
|
||||
"isResizing": false,
|
||||
"isRotating": false,
|
||||
"lastPointerDownWith": "mouse",
|
||||
"multiElement": null,
|
||||
"name": "Untitled-201933152653",
|
||||
"offsetLeft": 20,
|
||||
"offsetTop": 10,
|
||||
"openMenu": null,
|
||||
"pasteDialog": Object {
|
||||
"data": null,
|
||||
"shown": false,
|
||||
},
|
||||
"previousSelectedElementIds": Object {},
|
||||
"resizingElement": null,
|
||||
"scrollX": 0,
|
||||
"scrollY": 0,
|
||||
"scrolledOutside": false,
|
||||
"selectedElementIds": Object {
|
||||
"id1": true,
|
||||
},
|
||||
"selectedGroupIds": Object {},
|
||||
"selectionElement": null,
|
||||
"shouldCacheIgnoreZoom": false,
|
||||
"showHelpDialog": false,
|
||||
"showStats": false,
|
||||
"startBoundElement": null,
|
||||
"suggestedBindings": Array [],
|
||||
"theme": "light",
|
||||
"toastMessage": null,
|
||||
"viewBackgroundColor": "#ffffff",
|
||||
"viewModeEnabled": false,
|
||||
"width": 200,
|
||||
"zenModeEnabled": false,
|
||||
"zoom": Object {
|
||||
"translation": Object {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
},
|
||||
"value": 1,
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`contextMenu element shows context menu for element: [end of test] element 0 1`] = `
|
||||
Object {
|
||||
"angle": 0,
|
||||
@@ -4201,6 +4339,58 @@ Object {
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`contextMenu element shows context menu for element: [end of test] element 0 2`] = `
|
||||
Object {
|
||||
"angle": 0,
|
||||
"backgroundColor": "red",
|
||||
"boundElementIds": null,
|
||||
"fillStyle": "hachure",
|
||||
"groupIds": Array [],
|
||||
"height": 200,
|
||||
"id": "id0",
|
||||
"isDeleted": false,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"seed": 337897,
|
||||
"strokeColor": "#000000",
|
||||
"strokeSharpness": "sharp",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"type": "rectangle",
|
||||
"version": 1,
|
||||
"versionNonce": 0,
|
||||
"width": 200,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`contextMenu element shows context menu for element: [end of test] element 1 1`] = `
|
||||
Object {
|
||||
"angle": 0,
|
||||
"backgroundColor": "red",
|
||||
"boundElementIds": null,
|
||||
"fillStyle": "hachure",
|
||||
"groupIds": Array [],
|
||||
"height": 200,
|
||||
"id": "id1",
|
||||
"isDeleted": false,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"seed": 1278240551,
|
||||
"strokeColor": "#000000",
|
||||
"strokeSharpness": "sharp",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"type": "rectangle",
|
||||
"version": 1,
|
||||
"versionNonce": 0,
|
||||
"width": 200,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`contextMenu element shows context menu for element: [end of test] history 1`] = `
|
||||
Object {
|
||||
"recording": false,
|
||||
@@ -4212,6 +4402,7 @@ Object {
|
||||
"editingLinearElement": null,
|
||||
"name": "Untitled-201933152653",
|
||||
"selectedElementIds": Object {},
|
||||
"selectedGroupIds": Object {},
|
||||
"viewBackgroundColor": "#ffffff",
|
||||
},
|
||||
"elements": Array [],
|
||||
@@ -4224,6 +4415,7 @@ Object {
|
||||
"selectedElementIds": Object {
|
||||
"id0": true,
|
||||
},
|
||||
"selectedGroupIds": Object {},
|
||||
"viewBackgroundColor": "#ffffff",
|
||||
},
|
||||
"elements": Array [
|
||||
@@ -4256,6 +4448,30 @@ Object {
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`contextMenu element shows context menu for element: [end of test] history 2`] = `
|
||||
Object {
|
||||
"recording": false,
|
||||
"redoStack": Array [],
|
||||
"stateHistory": Array [
|
||||
Object {
|
||||
"appState": Object {
|
||||
"editingGroupId": null,
|
||||
"editingLinearElement": null,
|
||||
"name": "Untitled-201933152653",
|
||||
"selectedElementIds": Object {},
|
||||
"selectedGroupIds": Object {},
|
||||
"viewBackgroundColor": "#ffffff",
|
||||
},
|
||||
"elements": Array [],
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`contextMenu element shows context menu for element: [end of test] number of elements 1`] = `1`;
|
||||
|
||||
exports[`contextMenu element shows context menu for element: [end of test] number of elements 2`] = `2`;
|
||||
|
||||
exports[`contextMenu element shows context menu for element: [end of test] number of renders 1`] = `9`;
|
||||
|
||||
exports[`contextMenu element shows context menu for element: [end of test] number of renders 2`] = `6`;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -147,6 +147,46 @@ describe("contextMenu element", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("shows context menu for element", () => {
|
||||
const rect1 = API.createElement({
|
||||
type: "rectangle",
|
||||
x: 0,
|
||||
y: 0,
|
||||
height: 200,
|
||||
width: 200,
|
||||
backgroundColor: "red",
|
||||
});
|
||||
const rect2 = API.createElement({
|
||||
type: "rectangle",
|
||||
x: 0,
|
||||
y: 0,
|
||||
height: 200,
|
||||
width: 200,
|
||||
backgroundColor: "red",
|
||||
});
|
||||
h.elements = [rect1, rect2];
|
||||
API.setSelectedElements([rect1]);
|
||||
|
||||
// lower z-index
|
||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
||||
button: 2,
|
||||
clientX: 100,
|
||||
clientY: 100,
|
||||
});
|
||||
expect(queryContextMenu()).not.toBeNull();
|
||||
expect(API.getSelectedElement().id).toBe(rect1.id);
|
||||
|
||||
// higher z-index
|
||||
API.setSelectedElements([rect2]);
|
||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
||||
button: 2,
|
||||
clientX: 100,
|
||||
clientY: 100,
|
||||
});
|
||||
expect(queryContextMenu()).not.toBeNull();
|
||||
expect(API.getSelectedElement().id).toBe(rect2.id);
|
||||
});
|
||||
|
||||
it("shows 'Group selection' in context menu for multiple selected elements", () => {
|
||||
UI.clickTool("rectangle");
|
||||
mouse.down(10, 10);
|
||||
|
||||
@@ -167,6 +167,13 @@ describe("<Excalidraw/>", () => {
|
||||
);
|
||||
|
||||
expect(queryByTestId(container, "json-export-button")).toBeNull();
|
||||
});
|
||||
|
||||
it("should hide 'Save as image' button when 'saveAsImage' is false", async () => {
|
||||
const { container } = await render(
|
||||
<Excalidraw UIOptions={{ canvasActions: { saveAsImage: false } }} />,
|
||||
);
|
||||
|
||||
expect(queryByTestId(container, "image-export-button")).toBeNull();
|
||||
});
|
||||
|
||||
@@ -178,9 +185,11 @@ describe("<Excalidraw/>", () => {
|
||||
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(
|
||||
<Excalidraw UIOptions={{ canvasActions: { saveAsScene: false } }} />,
|
||||
<Excalidraw
|
||||
UIOptions={{ canvasActions: { export: { saveFileToDisk: false } } }}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(queryByTestId(container, "save-as-button")).toBeNull();
|
||||
|
||||
@@ -20,6 +20,15 @@ const readFile = util.promisify(fs.readFile);
|
||||
const { h } = window;
|
||||
|
||||
export class API {
|
||||
static setSelectedElements = (elements: ExcalidrawElement[]) => {
|
||||
h.setState({
|
||||
selectedElementIds: elements.reduce((acc, element) => {
|
||||
acc[element.id] = true;
|
||||
return acc;
|
||||
}, {} as Record<ExcalidrawElement["id"], true>),
|
||||
});
|
||||
};
|
||||
|
||||
static getSelectedElements = (): ExcalidrawElement[] => {
|
||||
return h.elements.filter(
|
||||
(element) => h.state.selectedElementIds[element.id],
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from "react";
|
||||
import { render } from "./test-utils";
|
||||
import { assertSelectedElements, render } from "./test-utils";
|
||||
import ExcalidrawApp from "../excalidraw-app";
|
||||
import { UI } from "./helpers/ui";
|
||||
import { Keyboard, Pointer, UI } from "./helpers/ui";
|
||||
import { API } from "./helpers/api";
|
||||
import { getDefaultAppState } from "../appState";
|
||||
import { waitFor } from "@testing-library/react";
|
||||
@@ -10,6 +10,8 @@ import { EXPORT_DATA_TYPES } from "../constants";
|
||||
|
||||
const { h } = window;
|
||||
|
||||
const mouse = new Pointer("mouse");
|
||||
|
||||
describe("history", () => {
|
||||
it("initializing scene should end up with single history entry", async () => {
|
||||
await render(<ExcalidrawApp />, {
|
||||
@@ -110,4 +112,78 @@ describe("history", () => {
|
||||
expect.objectContaining({ id: "A", isDeleted: true }),
|
||||
]);
|
||||
});
|
||||
|
||||
it("undo/redo works properly with groups", async () => {
|
||||
await render(<ExcalidrawApp />);
|
||||
const rect1 = API.createElement({ type: "rectangle", groupIds: ["A"] });
|
||||
const rect2 = API.createElement({ type: "rectangle", groupIds: ["A"] });
|
||||
|
||||
h.elements = [rect1, rect2];
|
||||
mouse.select(rect1);
|
||||
assertSelectedElements([rect1, rect2]);
|
||||
expect(h.state.selectedGroupIds).toEqual({ A: true });
|
||||
|
||||
Keyboard.withModifierKeys({ ctrl: true }, () => {
|
||||
Keyboard.keyPress("d");
|
||||
});
|
||||
expect(h.elements.length).toBe(4);
|
||||
assertSelectedElements([h.elements[2], h.elements[3]]);
|
||||
expect(h.state.selectedGroupIds).not.toEqual(
|
||||
expect.objectContaining({ A: true }),
|
||||
);
|
||||
|
||||
Keyboard.withModifierKeys({ ctrl: true }, () => {
|
||||
Keyboard.keyPress("z");
|
||||
});
|
||||
expect(h.elements.length).toBe(4);
|
||||
expect(h.elements).toEqual([
|
||||
expect.objectContaining({ id: rect1.id, isDeleted: false }),
|
||||
expect.objectContaining({ id: rect2.id, isDeleted: false }),
|
||||
expect.objectContaining({ id: `${rect1.id}_copy`, isDeleted: true }),
|
||||
expect.objectContaining({ id: `${rect2.id}_copy`, isDeleted: true }),
|
||||
]);
|
||||
expect(h.state.selectedGroupIds).toEqual({ A: true });
|
||||
|
||||
Keyboard.withModifierKeys({ ctrl: true, shift: true }, () => {
|
||||
Keyboard.keyPress("z");
|
||||
});
|
||||
expect(h.elements.length).toBe(4);
|
||||
expect(h.elements).toEqual([
|
||||
expect.objectContaining({ id: rect1.id, isDeleted: false }),
|
||||
expect.objectContaining({ id: rect2.id, isDeleted: false }),
|
||||
expect.objectContaining({ id: `${rect1.id}_copy`, isDeleted: false }),
|
||||
expect.objectContaining({ id: `${rect2.id}_copy`, isDeleted: false }),
|
||||
]);
|
||||
expect(h.state.selectedGroupIds).not.toEqual(
|
||||
expect.objectContaining({ A: true }),
|
||||
);
|
||||
|
||||
// undo again, and duplicate once more
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
Keyboard.withModifierKeys({ ctrl: true }, () => {
|
||||
Keyboard.keyPress("z");
|
||||
Keyboard.keyPress("d");
|
||||
});
|
||||
expect(h.elements.length).toBe(6);
|
||||
expect(h.elements).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ id: rect1.id, isDeleted: false }),
|
||||
expect.objectContaining({ id: rect2.id, isDeleted: false }),
|
||||
expect.objectContaining({ id: `${rect1.id}_copy`, isDeleted: true }),
|
||||
expect.objectContaining({ id: `${rect2.id}_copy`, isDeleted: true }),
|
||||
expect.objectContaining({
|
||||
id: `${rect1.id}_copy_copy`,
|
||||
isDeleted: false,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: `${rect2.id}_copy_copy`,
|
||||
isDeleted: false,
|
||||
}),
|
||||
]),
|
||||
);
|
||||
expect(h.state.selectedGroupIds).not.toEqual(
|
||||
expect.objectContaining({ A: true }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,21 +7,19 @@ import * as Renderer from "../renderer/renderScene";
|
||||
import { setDateTimeForTests } from "../utils";
|
||||
import { API } from "./helpers/api";
|
||||
import { Keyboard, Pointer, UI } from "./helpers/ui";
|
||||
import { fireEvent, render, screen, waitFor } from "./test-utils";
|
||||
import {
|
||||
assertSelectedElements,
|
||||
fireEvent,
|
||||
render,
|
||||
screen,
|
||||
waitFor,
|
||||
} from "./test-utils";
|
||||
import { defaultLang } from "../i18n";
|
||||
|
||||
const { h } = window;
|
||||
|
||||
const renderScene = jest.spyOn(Renderer, "renderScene");
|
||||
|
||||
const assertSelectedElements = (...elements: ExcalidrawElement[]) => {
|
||||
expect(
|
||||
API.getSelectedElements().map((element) => {
|
||||
return element.id;
|
||||
}),
|
||||
).toEqual(expect.arrayContaining(elements.map((element) => element.id)));
|
||||
};
|
||||
|
||||
const mouse = new Pointer("mouse");
|
||||
const finger1 = new Pointer("touch", 1);
|
||||
const finger2 = new Pointer("touch", 2);
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
fireEvent,
|
||||
mockBoundingClientRect,
|
||||
restoreOriginalGetBoundingClientRect,
|
||||
assertSelectedElements,
|
||||
} from "./test-utils";
|
||||
import ExcalidrawApp from "../excalidraw-app";
|
||||
import * as Renderer from "../renderer/renderScene";
|
||||
@@ -12,7 +13,6 @@ import { KEYS } from "../keys";
|
||||
import { reseed } from "../random";
|
||||
import { API } from "./helpers/api";
|
||||
import { Keyboard, Pointer } from "./helpers/ui";
|
||||
import { getSelectedElements } from "../scene";
|
||||
|
||||
// Unmount ReactDOM from root
|
||||
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
||||
@@ -28,12 +28,6 @@ const { h } = window;
|
||||
|
||||
const mouse = new Pointer("mouse");
|
||||
|
||||
const assertSelectedIds = (ids: string[]) => {
|
||||
expect(
|
||||
getSelectedElements(h.app.getSceneElements(), h.state).map((el) => el.id),
|
||||
).toEqual(ids);
|
||||
};
|
||||
|
||||
describe("inner box-selection", () => {
|
||||
beforeEach(async () => {
|
||||
await render(<ExcalidrawApp />);
|
||||
@@ -68,7 +62,7 @@ describe("inner box-selection", () => {
|
||||
mouse.moveTo(290, 290);
|
||||
mouse.up();
|
||||
|
||||
assertSelectedIds([rect2.id, rect3.id]);
|
||||
assertSelectedElements([rect2.id, rect3.id]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -105,7 +99,7 @@ describe("inner box-selection", () => {
|
||||
mouse.moveTo(rect2.x + rect2.width + 10, rect2.y + rect2.height + 10);
|
||||
mouse.up();
|
||||
|
||||
assertSelectedIds([rect2.id, rect3.id]);
|
||||
assertSelectedElements([rect2.id, rect3.id]);
|
||||
expect(h.state.selectedGroupIds).toEqual({ A: true });
|
||||
});
|
||||
});
|
||||
@@ -140,16 +134,62 @@ describe("inner box-selection", () => {
|
||||
Keyboard.withModifierKeys({ ctrl: true }, () => {
|
||||
mouse.downAt(rect2.x - 20, rect2.x - 20);
|
||||
mouse.moveTo(rect2.x + rect2.width + 10, rect2.y + rect2.height + 10);
|
||||
assertSelectedIds([rect2.id, rect3.id]);
|
||||
assertSelectedElements([rect2.id, rect3.id]);
|
||||
expect(h.state.selectedGroupIds).toEqual({ A: true });
|
||||
mouse.moveTo(rect2.x - 10, rect2.y - 10);
|
||||
assertSelectedIds([rect1.id]);
|
||||
assertSelectedElements([rect1.id]);
|
||||
expect(h.state.selectedGroupIds).toEqual({});
|
||||
mouse.up();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("selecting with cmd/ctrl modifier", () => {
|
||||
beforeEach(async () => {
|
||||
await render(<ExcalidrawApp />);
|
||||
});
|
||||
it("cycling through elements under cursor", async () => {
|
||||
const rect1 = API.createElement({
|
||||
type: "rectangle",
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 200,
|
||||
height: 200,
|
||||
backgroundColor: "red",
|
||||
fillStyle: "solid",
|
||||
});
|
||||
const rect2 = API.createElement({
|
||||
type: "rectangle",
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 200,
|
||||
height: 200,
|
||||
backgroundColor: "red",
|
||||
fillStyle: "solid",
|
||||
});
|
||||
const rect3 = API.createElement({
|
||||
type: "rectangle",
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 200,
|
||||
height: 200,
|
||||
backgroundColor: "red",
|
||||
fillStyle: "solid",
|
||||
});
|
||||
h.elements = [rect1, rect2, rect3];
|
||||
Keyboard.withModifierKeys({ ctrl: true }, () => {
|
||||
mouse.clickAt(100, 100);
|
||||
assertSelectedElements(rect3);
|
||||
mouse.clickAt(100, 100);
|
||||
assertSelectedElements(rect2);
|
||||
mouse.clickAt(100, 100);
|
||||
assertSelectedElements(rect1);
|
||||
mouse.clickAt(100, 100);
|
||||
assertSelectedElements(rect3);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("selection element", () => {
|
||||
it("create selection element on pointer down", async () => {
|
||||
const { getByToolName, container } = await render(<ExcalidrawApp />);
|
||||
|
||||
@@ -13,6 +13,8 @@ import { ImportedDataState } from "../data/types";
|
||||
import { STORAGE_KEYS } from "../excalidraw-app/data/localStorage";
|
||||
|
||||
import { SceneData } from "../types";
|
||||
import { getSelectedElements } from "../scene/selection";
|
||||
import { ExcalidrawElement } from "../element/types";
|
||||
|
||||
const customQueries = {
|
||||
...queries,
|
||||
@@ -124,3 +126,22 @@ export const mockBoundingClientRect = () => {
|
||||
export const restoreOriginalGetBoundingClientRect = () => {
|
||||
global.window.HTMLDivElement.prototype.getBoundingClientRect = originalGetBoundingClientRect;
|
||||
};
|
||||
|
||||
export const assertSelectedElements = (
|
||||
...elements: (
|
||||
| (ExcalidrawElement["id"] | ExcalidrawElement)[]
|
||||
| ExcalidrawElement["id"]
|
||||
| ExcalidrawElement
|
||||
)[]
|
||||
) => {
|
||||
const { h } = window;
|
||||
const selectedElementIds = getSelectedElements(
|
||||
h.app.getSceneElements(),
|
||||
h.state,
|
||||
).map((el) => el.id);
|
||||
const ids = elements
|
||||
.flat()
|
||||
.map((item) => (typeof item === "string" ? item : item.id));
|
||||
expect(selectedElementIds.length).toBe(ids.length);
|
||||
expect(selectedElementIds).toEqual(expect.arrayContaining(ids));
|
||||
};
|
||||
|
||||
+17
-8
@@ -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,14 +214,28 @@ export enum UserIdleState {
|
||||
IDLE = "idle",
|
||||
}
|
||||
|
||||
export type ExportOpts = {
|
||||
saveFileToDisk?: boolean;
|
||||
onExportToBackend?: (
|
||||
exportedElements: readonly NonDeletedExcalidrawElement[],
|
||||
appState: AppState,
|
||||
canvas: HTMLCanvasElement | null,
|
||||
) => void;
|
||||
renderCustomUI?: (
|
||||
exportedElements: readonly NonDeletedExcalidrawElement[],
|
||||
appState: AppState,
|
||||
canvas: HTMLCanvasElement | null,
|
||||
) => JSX.Element;
|
||||
};
|
||||
|
||||
type CanvasActions = {
|
||||
changeViewBackgroundColor?: boolean;
|
||||
clearCanvas?: boolean;
|
||||
export?: boolean;
|
||||
export?: false | ExportOpts;
|
||||
loadScene?: boolean;
|
||||
saveAsScene?: boolean;
|
||||
saveToActiveFile?: boolean;
|
||||
theme?: boolean;
|
||||
saveAsImage?: boolean;
|
||||
};
|
||||
|
||||
export type UIOptions = {
|
||||
@@ -235,7 +244,7 @@ export type UIOptions = {
|
||||
|
||||
export type AppProps = ExcalidrawProps & {
|
||||
UIOptions: {
|
||||
canvasActions: Required<CanvasActions>;
|
||||
canvasActions: Required<CanvasActions> & { export: ExportOpts };
|
||||
};
|
||||
detectScroll: boolean;
|
||||
handleKeyboardGlobally: boolean;
|
||||
|
||||
@@ -4595,8 +4595,9 @@ dns-equal@^1.0.0:
|
||||
resolved "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz"
|
||||
|
||||
dns-packet@^1.3.1:
|
||||
version "1.3.1"
|
||||
resolved "https://registry.npmjs.org/dns-packet/-/dns-packet-1.3.1.tgz"
|
||||
version "1.3.4"
|
||||
resolved "https://registry.yarnpkg.com/dns-packet/-/dns-packet-1.3.4.tgz#e3455065824a2507ba886c55a89963bb107dec6f"
|
||||
integrity sha512-BQ6F4vycLXBvdrJZ6S3gZewt6rcrks9KBgM9vrhW+knGRqc8uEdT7fuCwloc7nny5xNoMJ17HGH0R/6fpo8ECA==
|
||||
dependencies:
|
||||
ip "^1.1.0"
|
||||
safe-buffer "^5.0.1"
|
||||
@@ -10899,13 +10900,14 @@ rxjs@^6.4.0, rxjs@^6.6.0, rxjs@^6.6.6:
|
||||
dependencies:
|
||||
tslib "^1.9.0"
|
||||
|
||||
safe-buffer@5.1.2, safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
|
||||
safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
|
||||
version "5.1.2"
|
||||
resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz"
|
||||
|
||||
safe-buffer@^5.2.0, safe-buffer@~5.2.0:
|
||||
safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@~5.2.0:
|
||||
version "5.2.1"
|
||||
resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz"
|
||||
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
|
||||
integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
|
||||
|
||||
safe-regex@^1.1.0:
|
||||
version "1.1.0"
|
||||
|
||||
Reference in New Issue
Block a user