Compare commits

...
Author SHA1 Message Date
Zsolt Viczian 3b37ffbf6e remove toast debug message 2022-03-21 18:43:00 +01:00
Zsolt Viczian 38b58ea1da testing iOS 2022-03-21 18:39:00 +01:00
Zsolt Viczian 89a0dbafde testing 2022-03-21 18:25:11 +01:00
Zsolt Viczian 01789c3375 testing 2022-03-21 18:19:42 +01:00
Zsolt Viczian 563caa3f07 iOS debug 2022-03-21 18:14:02 +01:00
zsviczianandGitHub bb04943564 debugging iOS 2022-03-21 15:35:14 +01:00
zsviczianandGitHub 7bcc1f2a41 debugging iOS 2022-03-21 15:31:55 +01:00
zsviczianandGitHub fb449b6758 debugging iOS 2022-03-21 15:24:01 +01:00
zsviczianandGitHub 8d60f22ff7 Update Actions.tsx 2022-03-21 15:20:13 +01:00
zsviczianandGitHub 93bd035d03 debugging iOS 2022-03-21 15:18:55 +01:00
Zsolt Viczian 4dec449516 cleanup 2022-03-20 19:12:50 +01:00
Zsolt Viczian c45433c8db testing to understand why it does not work on iOS 2022-03-20 19:03:16 +01:00
Zsolt Viczian 22cd6f5115 setDeviceType 2022-03-20 18:21:19 +01:00
Zsolt Viczian 53ba9dffd9 setDeviceType 2022-03-20 18:14:47 +01:00
Zsolt Viczian 7e7864ca3d testing 2022-03-20 17:51:53 +01:00
Zsolt Viczian 15d88d0fe0 passing penDetected to ShapesSwitcher 2022-03-20 17:40:25 +01:00
Zsolt Viczian 24d7380333 penMode 2022-03-20 17:07:25 +01:00
Zsolt Viczian 0ecb53e2f2 updated test, set penMode on ToolButton change 2022-03-20 12:13:35 +01:00
Zsolt Viczian cf8024bdc0 moved penMode from state to context 2022-03-20 11:36:02 +01:00
Aakansha DoshiandGitHub ceb43ed8fb fix: erase all elements which are hit with single point click (#4934) 2022-03-17 21:03:59 +05:30
Ives van HoorneandGitHub 8c0a0415de update README.md to show commands in code blocks (#4932) 2022-03-16 18:49:28 +01:00
zsviczianandGitHub 192debd829 fix: Add multiElement-edit finalize action to Desktop (currently only visible in Mobile view) (#4764)
* add finalize action to Desktop UI

* Update LayerUI.tsx

* add size to panel component

* finzalize button style

* add finalize button

* changed isMobile to DeviceInfo, added isTouchScreen

* cleanup

* rename deviceInfo to deviceType

* rename deviceInfo to deviceType

* added updateObject

* Update App.tsx
2022-03-16 15:59:30 +01:00
Aakansha DoshiandGitHub 1cfb4dfd8b feat: Map shortcut O to ellipse and Add eraser shortcut E (#4930)
* feat: Add erase shortcut Shift+E

* map o to ellipse and E to Eraser

* fix tests

* use key

* move eraser to tools and rename shape to tools
2022-03-16 18:31:20 +05:30
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
fb32886355 chore(deps-dev): bump ts-loader in /src/packages/excalidraw (#4912)
Bumps [ts-loader](https://github.com/TypeStrong/ts-loader) from 9.2.6 to 9.2.8.
- [Release notes](https://github.com/TypeStrong/ts-loader/releases)
- [Changelog](https://github.com/TypeStrong/ts-loader/blob/main/CHANGELOG.md)
- [Commits](https://github.com/TypeStrong/ts-loader/compare/v9.2.6...v9.2.8)

---
updated-dependencies:
- dependency-name: ts-loader
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-03-16 13:25:36 +05:30
Aakansha DoshiandGitHub 065df495ba fix: Hide eraser in view mode in desktop (#4929) 2022-03-16 13:09:34 +05:30
Aakansha DoshiandGitHub 558227f744 feat: update eraser cursor (#4922)
* feat: update eraser cursor

* fix dark theme

* check before adding active class

* use custom cursor instead of DOM manipulation

* cache canvas and redraw only when theme changes

* use oc colors

* remove

* cache preview data url

* increase linwidth

* update coords for cursor

* add white 2px outline

* improvements

* use 1px line width 6px radius for outer

* improve
2022-03-15 20:56:39 +05:30
Aakansha DoshiandGitHub 6d45430344 fix: undo when erasing elements by clicking (#4921)
* fix: undo when erasing elements by clicking

* newline remove
2022-03-14 14:59:55 +05:30
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
3aa0c5ebc0 chore(deps-dev): bump css-loader in /src/packages/excalidraw (#4911)
Bumps [css-loader](https://github.com/webpack-contrib/css-loader) from 6.6.0 to 6.7.1.
- [Release notes](https://github.com/webpack-contrib/css-loader/releases)
- [Changelog](https://github.com/webpack-contrib/css-loader/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack-contrib/css-loader/compare/v6.6.0...v6.7.1)

---
updated-dependencies:
- dependency-name: css-loader
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-03-14 14:47:01 +05:30
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
e940993e0e chore(deps-dev): bump css-loader in /src/packages/utils (#4914)
Bumps [css-loader](https://github.com/webpack-contrib/css-loader) from 6.5.1 to 6.7.1.
- [Release notes](https://github.com/webpack-contrib/css-loader/releases)
- [Changelog](https://github.com/webpack-contrib/css-loader/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack-contrib/css-loader/compare/v6.5.1...v6.7.1)

---
updated-dependencies:
- dependency-name: css-loader
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-03-14 14:45:33 +05:30
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
8f90aeb8d5 chore(deps-dev): bump ts-loader in /src/packages/utils (#4913)
Bumps [ts-loader](https://github.com/TypeStrong/ts-loader) from 9.2.6 to 9.2.8.
- [Release notes](https://github.com/TypeStrong/ts-loader/releases)
- [Changelog](https://github.com/TypeStrong/ts-loader/blob/main/CHANGELOG.md)
- [Commits](https://github.com/TypeStrong/ts-loader/compare/v9.2.6...v9.2.8)

---
updated-dependencies:
- dependency-name: ts-loader
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-03-14 14:45:02 +05:30
Aakansha DoshiandGitHub e92d133973 fix: undo when erasing (#4900) 2022-03-11 20:44:17 +05:30
David LuzarandGitHub b682d88167 fix: incorrectly erasing on mobile (#4899)
* fix: incorrectly erasing on mobile

* reintroduce fix for erasing on single-point click

* fix snaps
2022-03-11 15:45:59 +01:00
Aakansha DoshiandGitHub 7daf1a7944 feat: Add Eraser 🎉 (#4887)
* feat: Add Eraser 🎉

* Eraser working

* remove unused state

* fix

* toggle eraser

* Support deselect with Alt/Option

* rename actionDelete -> actionErase

* Add util isEraserActive

* show eraser in mobile

* render eraser conditionally in mobile

* use selection if eraser in local storage state

* Add sampling to erase accurately

* use pointerDownState

* set eraser to false in AllowedExcalidrawElementTypes

* rename/reword fixes

* don't use updateScene

* handle bound text when erasing

* fix hover state in mobile

* consider all hitElements instead of a single

* code improvements

* revert to select if eraser active and elements selected

* show eraser in zenmode

* erase element when clicked on element while eraser active

* set groupIds to empty when eraser active

* fix test

* remove dragged distance
2022-03-11 19:53:42 +05:30
40 changed files with 803 additions and 482 deletions
+35 -8
View File
@@ -128,14 +128,41 @@ For collaboration, you will need to set up [collab server](https://github.com/ex
#### Commands
| Command | Description |
| ------------------ | --------------------------------- |
| `yarn` | Install the dependencies |
| `yarn start` | Run the project |
| `yarn fix` | Reformat all files with Prettier |
| `yarn test` | Run tests |
| `yarn test:update` | Update test snapshots |
| `yarn test:code` | Test for formatting with Prettier |
##### Install the dependencies
```
yarn
```
##### Run the project
```
yarn start
```
##### Reformat all files with Prettier
```
yarn fix
```
##### Run tests
```
yarn test
```
##### Update test snapshots
```
yarn test:update
```
##### Test for formatting with Prettier
```
yarn test:code
```
#### Docker Compose
+32 -3
View File
@@ -1,5 +1,5 @@
import { ColorPicker } from "../components/ColorPicker";
import { zoomIn, zoomOut } from "../components/icons";
import { eraser, zoomIn, zoomOut } from "../components/icons";
import { ToolButton } from "../components/ToolButton";
import { DarkModeToggle } from "../components/DarkModeToggle";
import { THEME, ZOOM_STEP } from "../constants";
@@ -15,8 +15,9 @@ import { getShortcutKey } from "../utils";
import { register } from "./register";
import { Tooltip } from "../components/Tooltip";
import { newElementWith } from "../element/mutateElement";
import { getDefaultAppState } from "../appState";
import { getDefaultAppState, isEraserActive } from "../appState";
import ClearCanvas from "../components/ClearCanvas";
import clsx from "clsx";
export const actionChangeViewBackgroundColor = register({
name: "changeViewBackgroundColor",
@@ -61,7 +62,6 @@ export const actionClearCanvas = register({
theme: appState.theme,
elementLocked: appState.elementLocked,
penMode: appState.penMode,
penDetected: appState.penDetected,
exportBackground: appState.exportBackground,
exportEmbedScene: appState.exportEmbedScene,
gridSize: appState.gridSize,
@@ -289,3 +289,32 @@ export const actionToggleTheme = register({
),
keyTest: (event) => event.altKey && event.shiftKey && event.code === CODES.D,
});
export const actionErase = register({
name: "eraser",
perform: (elements, appState) => {
return {
appState: {
...appState,
selectedElementIds: {},
selectedGroupIds: {},
elementType: isEraserActive(appState) ? "selection" : "eraser",
},
commitToHistory: true,
};
},
keyTest: (event) => event.key === KEYS.E,
PanelComponent: ({ elements, appState, updateData, data }) => (
<ToolButton
type="button"
icon={eraser}
className={clsx("eraser", { active: isEraserActive(appState) })}
title={`${t("toolBar.eraser")}-${getShortcutKey("E")}`}
aria-label={t("toolBar.eraser")}
onClick={() => {
updateData(null);
}}
size={data?.size || "medium"}
></ToolButton>
),
});
+3 -3
View File
@@ -8,7 +8,7 @@ import { DarkModeToggle } from "../components/DarkModeToggle";
import { loadFromJSON, saveAsJSON } from "../data";
import { resaveAsImageWithScene } from "../data/resave";
import { t } from "../i18n";
import { useIsMobile } from "../components/App";
import { useDeviceType } from "../components/App";
import { KEYS } from "../keys";
import { register } from "./register";
import { CheckboxItem } from "../components/CheckboxItem";
@@ -200,7 +200,7 @@ export const actionSaveFileToDisk = register({
icon={saveAs}
title={t("buttons.saveAs")}
aria-label={t("buttons.saveAs")}
showAriaLabel={useIsMobile()}
showAriaLabel={useDeviceType().isMobile}
hidden={!nativeFileSystemSupported}
onClick={() => updateData(null)}
data-testid="save-as-button"
@@ -243,7 +243,7 @@ export const actionLoadScene = register({
icon={load}
title={t("buttons.load")}
aria-label={t("buttons.load")}
showAriaLabel={useIsMobile()}
showAriaLabel={useDeviceType().isMobile}
onClick={updateData}
data-testid="load-button"
/>
+2 -1
View File
@@ -165,7 +165,7 @@ export const actionFinalize = register({
(!appState.draggingElement && appState.multiElement === null))) ||
((event.key === KEYS.ESCAPE || event.key === KEYS.ENTER) &&
appState.multiElement !== null),
PanelComponent: ({ appState, updateData }) => (
PanelComponent: ({ appState, updateData, data }) => (
<ToolButton
type="button"
icon={done}
@@ -173,6 +173,7 @@ export const actionFinalize = register({
aria-label={t("buttons.done")}
onClick={updateData}
visible={appState.multiElement != null}
size={data?.size || "medium"}
/>
),
});
+2 -1
View File
@@ -106,7 +106,8 @@ export type ActionName =
| "increaseFontSize"
| "decreaseFontSize"
| "unbindText"
| "hyperlink";
| "hyperlink"
| "eraser";
export type PanelComponentProps = {
elements: readonly ExcalidrawElement[];
+6 -2
View File
@@ -44,7 +44,6 @@ export const getDefaultAppState = (): Omit<
elementLocked: false,
elementType: "selection",
penMode: false,
penDetected: false,
errorMessage: null,
exportBackground: true,
exportScale: defaultExportScale,
@@ -133,7 +132,6 @@ const APP_STATE_STORAGE_CONF = (<
elementLocked: { browser: true, export: false, server: false },
elementType: { browser: true, export: false, server: false },
penMode: { browser: false, export: false, server: false },
penDetected: { browser: false, export: false, server: false },
errorMessage: { browser: false, export: false, server: false },
exportBackground: { browser: true, export: false, server: false },
exportEmbedScene: { browser: true, export: false, server: false },
@@ -213,3 +211,9 @@ export const cleanAppStateForExport = (appState: Partial<AppState>) => {
export const clearAppStateForDatabase = (appState: Partial<AppState>) => {
return _clearAppStateForStorage(appState, "server");
};
export const isEraserActive = ({
elementType,
}: {
elementType: AppState["elementType"];
}) => elementType === "eraser";
+57 -44
View File
@@ -3,7 +3,7 @@ import { ActionManager } from "../actions/manager";
import { getNonDeletedElements } from "../element";
import { ExcalidrawElement, PointerType } from "../element/types";
import { t } from "../i18n";
import { useIsMobile } from "../components/App";
import { useDeviceType } from "../components/App";
import {
canChangeSharpness,
canHaveArrowheads,
@@ -14,7 +14,7 @@ import {
hasText,
} from "../scene";
import { SHAPES } from "../shapes";
import { AppState, Zoom } from "../types";
import { AppState, DeviceType, Zoom } from "../types";
import { capitalizeString, isTransparent, setCursorForShape } from "../utils";
import Stack from "./Stack";
import { ToolButton } from "./ToolButton";
@@ -30,7 +30,7 @@ export const SelectedShapeActions = ({
appState: AppState;
elements: readonly ExcalidrawElement[];
renderAction: ActionManager["renderAction"];
elementType: ExcalidrawElement["type"];
elementType: AppState["elementType"];
}) => {
const targetElements = getTargetElements(
getNonDeletedElements(elements),
@@ -46,7 +46,7 @@ export const SelectedShapeActions = ({
isSingleElementBoundContainer = true;
}
const isEditing = Boolean(appState.editingElement);
const isMobile = useIsMobile();
const deviceType = useDeviceType();
const isRTL = document.documentElement.getAttribute("dir") === "rtl";
const showFillIcons =
@@ -168,8 +168,8 @@ export const SelectedShapeActions = ({
<fieldset>
<legend>{t("labels.actions")}</legend>
<div className="buttonList">
{!isMobile && renderAction("duplicateSelection")}
{!isMobile && renderAction("deleteSelectedElements")}
{!deviceType.isMobile && renderAction("duplicateSelection")}
{!deviceType.isMobile && renderAction("deleteSelectedElements")}
{renderAction("group")}
{renderAction("ungroup")}
{targetElements.length === 1 && renderAction("hyperlink")}
@@ -185,48 +185,61 @@ export const ShapesSwitcher = ({
elementType,
setAppState,
onImageAction,
appState,
setDeviceType,
}: {
canvas: HTMLCanvasElement | null;
elementType: ExcalidrawElement["type"];
elementType: AppState["elementType"];
setAppState: React.Component<any, AppState>["setState"];
onImageAction: (data: { pointerType: PointerType | null }) => void;
}) => (
<>
{SHAPES.map(({ value, icon, key }, index) => {
const label = t(`toolBar.${value}`);
const letter = key && (typeof key === "string" ? key : key[0]);
const shortcut = letter
? `${capitalizeString(letter)} ${t("helpDialog.or")} ${index + 1}`
: `${index + 1}`;
return (
<ToolButton
className="Shape"
key={value}
type="radio"
icon={icon}
checked={elementType === value}
name="editor-current-shape"
title={`${capitalizeString(label)}${shortcut}`}
keyBindingLabel={`${index + 1}`}
aria-label={capitalizeString(label)}
aria-keyshortcuts={shortcut}
data-testid={value}
onChange={({ pointerType }) => {
setAppState({
elementType: value,
multiElement: null,
selectedElementIds: {},
});
setCursorForShape(canvas, value);
if (value === "image") {
onImageAction({ pointerType });
}
}}
/>
);
})}
</>
);
appState: AppState;
setDeviceType: (obj: Partial<DeviceType>) => void;
}) => {
const penDetected = useDeviceType().penDetected;
return (
<>
{SHAPES.map(({ value, icon, key }, index) => {
const label = t(`toolBar.${value}`);
const letter = key && (typeof key === "string" ? key : key[0]);
const shortcut = letter
? `${capitalizeString(letter)} ${t("helpDialog.or")} ${index + 1}`
: `${index + 1}`;
return (
<ToolButton
className="Shape"
key={value}
type="radio"
icon={icon}
checked={elementType === value}
name="editor-current-shape"
title={`${capitalizeString(label)}${shortcut}`}
keyBindingLabel={`${index + 1}`}
aria-label={capitalizeString(label)}
aria-keyshortcuts={shortcut}
data-testid={value}
onPointerDown={({ pointerType }) => {
if (!penDetected && pointerType === "pen") {
setAppState({ penMode: true });
setDeviceType({ penDetected: true });
}
}}
onChange={({ pointerType }) => {
setAppState({
elementType: value,
multiElement: null,
selectedElementIds: {},
});
setCursorForShape(canvas, { ...appState, elementType: value });
if (value === "image") {
onImageAction({ pointerType });
}
}}
/>
);
})}
</>
);
};
export const ZoomActions = ({
renderAction,
+209 -47
View File
@@ -35,7 +35,7 @@ import { ActionManager } from "../actions/manager";
import { actions } from "../actions/register";
import { ActionResult } from "../actions/types";
import { trackEvent } from "../analytics";
import { getDefaultAppState } from "../appState";
import { getDefaultAppState, isEraserActive } from "../appState";
import {
copyToClipboard,
parseClipboard,
@@ -195,6 +195,7 @@ import {
LibraryItems,
PointerDownState,
SceneData,
DeviceType,
} from "../types";
import {
debounce,
@@ -214,6 +215,8 @@ import {
withBatchedUpdates,
wrapEvent,
withBatchedUpdatesThrottled,
updateObject,
setEraserCursor,
} from "../utils";
import ContextMenu, { ContextMenuOption } from "./ContextMenu";
import LayerUI from "./LayerUI";
@@ -252,8 +255,13 @@ import {
isLocalLink,
} from "../element/Hyperlink";
const IsMobileContext = React.createContext(false);
export const useIsMobile = () => useContext(IsMobileContext);
const defaultDeviceTypeContext: DeviceType = {
isMobile: false,
isTouchScreen: false,
penDetected: false,
};
const DeviceTypeContext = React.createContext(defaultDeviceTypeContext);
export const useDeviceType = () => useContext(DeviceTypeContext);
const ExcalidrawContainerContext = React.createContext<{
container: HTMLDivElement | null;
id: string | null;
@@ -285,7 +293,11 @@ class App extends React.Component<AppProps, AppState> {
rc: RoughCanvas | null = null;
unmounted: boolean = false;
actionManager: ActionManager;
isMobile = false;
deviceType: DeviceType = {
isMobile: false,
isTouchScreen: false,
penDetected: false,
};
detachIsMobileMqHandler?: () => void;
private excalidrawContainerRef = React.createRef<HTMLDivElement>();
@@ -314,6 +326,7 @@ class App extends React.Component<AppProps, AppState> {
lastPointerDown: React.PointerEvent<HTMLCanvasElement> | null = null;
lastPointerUp: React.PointerEvent<HTMLElement> | PointerEvent | null = null;
contextMenuOpen: boolean = false;
lastScenePointer: { x: number; y: number } | null = null;
constructor(props: AppProps) {
super(props);
@@ -466,7 +479,7 @@ class App extends React.Component<AppProps, AppState> {
<div
className={clsx("excalidraw excalidraw-container", {
"excalidraw--view-mode": viewModeEnabled,
"excalidraw--mobile": this.isMobile,
"excalidraw--mobile": this.deviceType.isMobile,
})}
ref={this.excalidrawContainerRef}
onDrop={this.handleAppOnDrop}
@@ -478,12 +491,13 @@ class App extends React.Component<AppProps, AppState> {
<ExcalidrawContainerContext.Provider
value={this.excalidrawContainerValue}
>
<IsMobileContext.Provider value={this.isMobile}>
<DeviceTypeContext.Provider value={this.deviceType}>
<LayerUI
canvas={this.canvas}
appState={this.state}
files={this.files}
setAppState={this.setAppState}
setDeviceType={this.setDeviceType}
actionManager={this.actionManager}
elements={this.scene.getElements()}
onCollabButtonClick={onCollabButtonClick}
@@ -545,7 +559,7 @@ class App extends React.Component<AppProps, AppState> {
/>
)}
<main>{this.renderCanvas()}</main>
</IsMobileContext.Provider>
</DeviceTypeContext.Provider>
</ExcalidrawContainerContext.Provider>
</div>
);
@@ -889,9 +903,12 @@ class App extends React.Component<AppProps, AppState> {
// ---------------------------------------------------------------------
const { width, height } =
this.excalidrawContainerRef.current!.getBoundingClientRect();
this.isMobile =
width < MQ_MAX_WIDTH_PORTRAIT ||
(height < MQ_MAX_HEIGHT_LANDSCAPE && width < MQ_MAX_WIDTH_LANDSCAPE);
this.setDeviceType({
isMobile:
width < MQ_MAX_WIDTH_PORTRAIT ||
(height < MQ_MAX_HEIGHT_LANDSCAPE &&
width < MQ_MAX_WIDTH_LANDSCAPE),
});
// refresh offsets
// ---------------------------------------------------------------------
this.updateDOMRect();
@@ -901,7 +918,9 @@ class App extends React.Component<AppProps, AppState> {
const mediaQuery = window.matchMedia(
`(max-width: ${MQ_MAX_WIDTH_PORTRAIT}px), (max-height: ${MQ_MAX_HEIGHT_LANDSCAPE}px) and (max-width: ${MQ_MAX_WIDTH_LANDSCAPE}px)`,
);
const handler = () => (this.isMobile = mediaQuery.matches);
const handler = () => {
this.setDeviceType({ isMobile: mediaQuery.matches });
};
mediaQuery.addListener(handler);
this.detachIsMobileMqHandler = () => mediaQuery.removeListener(handler);
}
@@ -1044,6 +1063,15 @@ class App extends React.Component<AppProps, AppState> {
}
componentDidUpdate(prevProps: AppProps, prevState: AppState) {
if (
Object.keys(this.state.selectedElementIds).length &&
isEraserActive(this.state)
) {
this.setState({ elementType: "selection" });
}
if (prevState.theme !== this.state.theme) {
setEraserCursor(this.canvas, this.state.theme);
}
// Hide hyperlink popup if shown when element type is not selection
if (
prevState.elementType === "selection" &&
@@ -1194,7 +1222,7 @@ class App extends React.Component<AppProps, AppState> {
theme: this.state.theme,
imageCache: this.imageCache,
isExporting: false,
renderScrollbars: !this.isMobile,
renderScrollbars: !this.deviceType.isMobile,
},
);
@@ -1533,6 +1561,10 @@ class App extends React.Component<AppProps, AppState> {
this.setState(obj);
};
setDeviceType = (obj: Partial<DeviceType>): void => {
this.deviceType = updateObject(this.deviceType, obj);
};
removePointer = (event: React.PointerEvent<HTMLElement> | PointerEvent) => {
if (touchTimeout) {
this.resetContextMenuTimer();
@@ -1866,7 +1898,7 @@ class App extends React.Component<AppProps, AppState> {
} else if (this.state.elementType === "selection") {
resetCursor(this.canvas);
} else {
setCursorForShape(this.canvas, this.state.elementType);
setCursorForShape(this.canvas, this.state);
this.setState({
selectedElementIds: {},
selectedGroupIds: {},
@@ -1892,7 +1924,7 @@ class App extends React.Component<AppProps, AppState> {
private selectShapeTool(elementType: AppState["elementType"]) {
if (!isHoldingSpace) {
setCursorForShape(this.canvas, elementType);
setCursorForShape(this.canvas, this.state);
}
if (isToolIcon(document.activeElement)) {
this.focusContainer();
@@ -2036,7 +2068,7 @@ class App extends React.Component<AppProps, AppState> {
editingElement: null,
});
if (this.state.elementLocked) {
setCursorForShape(this.canvas, this.state.elementType);
setCursorForShape(this.canvas, this.state);
}
this.focusContainer();
@@ -2380,7 +2412,7 @@ class App extends React.Component<AppProps, AppState> {
element,
this.state,
[scenePointer.x, scenePointer.y],
this.isMobile,
this.deviceType.isMobile,
) &&
index <= hitElementIndex
);
@@ -2413,7 +2445,7 @@ class App extends React.Component<AppProps, AppState> {
this.hitLinkElement!,
this.state,
[lastPointerDownCoords.x, lastPointerDownCoords.y],
this.isMobile,
this.deviceType.isMobile,
);
const lastPointerUpCoords = viewportCoordsToSceneCoords(
this.lastPointerUp!,
@@ -2423,7 +2455,7 @@ class App extends React.Component<AppProps, AppState> {
this.hitLinkElement!,
this.state,
[lastPointerUpCoords.x, lastPointerUpCoords.y],
this.isMobile,
this.deviceType.isMobile,
);
if (lastPointerDownHittingLinkIcon && lastPointerUpHittingLinkIcon) {
const url = this.hitLinkElement.link;
@@ -2450,7 +2482,6 @@ class App extends React.Component<AppProps, AppState> {
event: React.PointerEvent<HTMLCanvasElement>,
) => {
this.savePointer(event.clientX, event.clientY, this.state.cursorButton);
if (gesture.pointers.has(event.pointerId)) {
gesture.pointers.set(event.pointerId, {
x: event.clientX,
@@ -2519,7 +2550,7 @@ class App extends React.Component<AppProps, AppState> {
if (isOverScrollBar) {
resetCursor(this.canvas);
} else {
setCursorForShape(this.canvas, this.state.elementType);
setCursorForShape(this.canvas, this.state);
}
}
@@ -2569,7 +2600,7 @@ class App extends React.Component<AppProps, AppState> {
const { points, lastCommittedPoint } = multiElement;
const lastPoint = points[points.length - 1];
setCursorForShape(this.canvas, this.state.elementType);
setCursorForShape(this.canvas, this.state);
if (lastPoint === lastCommittedPoint) {
// if we haven't yet created a temp point and we're beyond commit-zone
@@ -2624,7 +2655,8 @@ class App extends React.Component<AppProps, AppState> {
if (
hasDeselectedButton ||
(this.state.elementType !== "selection" &&
this.state.elementType !== "text")
this.state.elementType !== "text" &&
this.state.elementType !== "eraser")
) {
return;
}
@@ -2682,7 +2714,9 @@ class App extends React.Component<AppProps, AppState> {
scenePointer,
hitElement,
);
if (isEraserActive(this.state)) {
return;
}
if (
this.hitLinkElement &&
!this.state.selectedElementIds[this.hitLinkElement.id]
@@ -2699,8 +2733,7 @@ class App extends React.Component<AppProps, AppState> {
!this.state.showHyperlinkPopup
) {
this.setState({ showHyperlinkPopup: "info" });
}
if (this.state.elementType === "text") {
} else if (this.state.elementType === "text") {
setCursor(
this.canvas,
isTextElement(hitElement) ? CURSOR_TYPE.TEXT : CURSOR_TYPE.CROSSHAIR,
@@ -2741,6 +2774,80 @@ class App extends React.Component<AppProps, AppState> {
}
};
private handleEraser = (
event: PointerEvent,
pointerDownState: PointerDownState,
scenePointer: { x: number; y: number },
) => {
const updateElementIds = (elements: ExcalidrawElement[]) => {
elements.forEach((element) => {
idsToUpdate.push(element.id);
if (event.altKey) {
if (pointerDownState.elementIdsToErase[element.id]) {
pointerDownState.elementIdsToErase[element.id] = false;
}
} else {
pointerDownState.elementIdsToErase[element.id] = true;
}
});
};
const idsToUpdate: Array<string> = [];
const distance = distance2d(
pointerDownState.lastCoords.x,
pointerDownState.lastCoords.y,
scenePointer.x,
scenePointer.y,
);
const threshold = 10 / this.state.zoom.value;
const point = { ...pointerDownState.lastCoords };
let samplingInterval = 0;
while (samplingInterval <= distance) {
const hitElements = this.getElementsAtPosition(point.x, point.y);
updateElementIds(hitElements);
// Exit since we reached current point
if (samplingInterval === distance) {
break;
}
// Calculate next point in the line at a distance of sampling interval
samplingInterval = Math.min(samplingInterval + threshold, distance);
const distanceRatio = samplingInterval / distance;
const nextX =
(1 - distanceRatio) * point.x + distanceRatio * scenePointer.x;
const nextY =
(1 - distanceRatio) * point.y + distanceRatio * scenePointer.y;
point.x = nextX;
point.y = nextY;
}
const elements = this.scene.getElements().map((ele) => {
const id =
isBoundToContainer(ele) && idsToUpdate.includes(ele.containerId)
? ele.containerId
: ele.id;
if (idsToUpdate.includes(id)) {
if (event.altKey) {
if (pointerDownState.elementIdsToErase[id] === false) {
return newElementWith(ele, {
opacity: this.state.currentItemOpacity,
});
}
} else {
return newElementWith(ele, { opacity: 20 });
}
}
return ele;
});
this.scene.replaceAllElements(elements);
pointerDownState.lastCoords.x = scenePointer.x;
pointerDownState.lastCoords.y = scenePointer.y;
};
// set touch moving for mobile context menu
private handleTouchMove = (event: React.TouchEvent<HTMLCanvasElement>) => {
invalidateContextMenu = true;
@@ -2761,18 +2868,22 @@ class App extends React.Component<AppProps, AppState> {
//fires only once, if pen is detected, penMode is enabled
//the user can disable this by toggling the penMode button
if (!this.state.penDetected && event.pointerType === "pen") {
this.setState((prevState) => {
return {
penMode: true,
penDetected: true,
};
});
if (!this.deviceType.penDetected && event.pointerType === "pen") {
this.setDeviceType({ penDetected: true });
this.setState({ penMode: true });
}
if (
!this.deviceType.isTouchScreen &&
["pen", "touch"].includes(event.pointerType)
) {
this.setDeviceType({ isTouchScreen: true });
}
if (isPanning) {
return;
}
this.lastPointerDown = event;
this.setState({
lastPointerDownWith: event.pointerType,
@@ -2865,7 +2976,7 @@ class App extends React.Component<AppProps, AppState> {
this.state.elementType,
pointerDownState,
);
} else {
} else if (this.state.elementType !== "eraser") {
this.createGenericElementOnPointerDown(
this.state.elementType,
pointerDownState,
@@ -2899,8 +3010,7 @@ class App extends React.Component<AppProps, AppState> {
event: React.PointerEvent<HTMLCanvasElement>,
) => {
this.lastPointerUp = event;
const isTouchScreen = ["pen", "touch"].includes(event.pointerType);
if (isTouchScreen) {
if (this.deviceType.isTouchScreen) {
const scenePointer = viewportCoordsToSceneCoords(
{ clientX: event.clientX, clientY: event.clientY },
this.state,
@@ -2918,7 +3028,7 @@ class App extends React.Component<AppProps, AppState> {
this.hitLinkElement &&
!this.state.selectedElementIds[this.hitLinkElement.id]
) {
this.redirectToLink(event, isTouchScreen);
this.redirectToLink(event, this.deviceType.isTouchScreen);
}
this.removePointer(event);
@@ -3042,7 +3152,7 @@ class App extends React.Component<AppProps, AppState> {
if (this.state.viewModeEnabled) {
setCursor(this.canvas, CURSOR_TYPE.GRAB);
} else {
setCursorForShape(this.canvas, this.state.elementType);
setCursorForShape(this.canvas, this.state);
}
}
this.setState({
@@ -3139,6 +3249,7 @@ class App extends React.Component<AppProps, AppState> {
boxSelection: {
hasOccurred: false,
},
elementIdsToErase: {},
};
}
@@ -3166,7 +3277,7 @@ class App extends React.Component<AppProps, AppState> {
const onPointerUp = withBatchedUpdates(() => {
isDraggingScrollBar = false;
setCursorForShape(this.canvas, this.state.elementType);
setCursorForShape(this.canvas, this.state);
lastPointerUp = null;
this.setState({
cursorButton: "up",
@@ -3287,7 +3398,7 @@ class App extends React.Component<AppProps, AppState> {
pointerDownState.hit.element,
this.state,
[pointerDownState.origin.x, pointerDownState.origin.y],
this.isMobile,
this.deviceType.isMobile,
)
) {
return false;
@@ -3727,7 +3838,6 @@ class App extends React.Component<AppProps, AppState> {
),
);
}
const target = event.target;
if (!(target instanceof HTMLElement)) {
return;
@@ -3738,6 +3848,12 @@ class App extends React.Component<AppProps, AppState> {
}
const pointerCoords = viewportCoordsToSceneCoords(event, this.state);
if (isEraserActive(this.state)) {
this.handleEraser(event, pointerDownState, pointerCoords);
return;
}
const [gridX, gridY] = getGridPoint(
pointerCoords.x,
pointerCoords.y,
@@ -4090,7 +4206,6 @@ class App extends React.Component<AppProps, AppState> {
isResizing,
isRotating,
} = this.state;
this.setState({
isResizing: false,
isRotating: false,
@@ -4311,6 +4426,36 @@ class App extends React.Component<AppProps, AppState> {
// Code below handles selection when element(s) weren't
// drag or added to selection on pointer down phase.
const hitElement = pointerDownState.hit.element;
if (isEraserActive(this.state)) {
const draggedDistance = distance2d(
this.lastPointerDown!.clientX,
this.lastPointerDown!.clientY,
this.lastPointerUp!.clientX,
this.lastPointerUp!.clientY,
);
if (draggedDistance === 0) {
const scenePointer = viewportCoordsToSceneCoords(
{
clientX: this.lastPointerUp!.clientX,
clientY: this.lastPointerUp!.clientY,
},
this.state,
);
const hitElements = this.getElementsAtPosition(
scenePointer.x,
scenePointer.y,
);
hitElements.forEach(
(hitElement) =>
(pointerDownState.elementIdsToErase[hitElement.id] = true),
);
}
this.eraseElements(pointerDownState);
return;
}
if (
hitElement &&
!pointerDownState.drag.hasOccurred &&
@@ -4450,6 +4595,23 @@ class App extends React.Component<AppProps, AppState> {
});
}
private eraseElements = (pointerDownState: PointerDownState) => {
const elements = this.scene.getElements().map((ele) => {
if (pointerDownState.elementIdsToErase[ele.id]) {
return newElementWith(ele, { isDeleted: true });
} else if (
isBoundToContainer(ele) &&
pointerDownState.elementIdsToErase[ele.containerId]
) {
return newElementWith(ele, { isDeleted: true });
}
return ele;
});
this.history.resumeRecording();
this.scene.replaceAllElements(elements);
};
private initializeImage = async ({
imageFile,
imageElement: _imageElement,
@@ -5266,7 +5428,7 @@ class App extends React.Component<AppProps, AppState> {
} else {
ContextMenu.push({
options: [
this.isMobile &&
this.deviceType.isMobile &&
navigator.clipboard && {
name: "paste",
perform: (elements, appStates) => {
@@ -5277,7 +5439,7 @@ class App extends React.Component<AppProps, AppState> {
},
contextItemLabel: "labels.paste",
},
this.isMobile && navigator.clipboard && separator,
this.deviceType.isMobile && navigator.clipboard && separator,
probablySupportsClipboardBlob &&
elements.length > 0 &&
actionCopyAsPng,
@@ -5323,9 +5485,9 @@ class App extends React.Component<AppProps, AppState> {
} else {
ContextMenu.push({
options: [
this.isMobile && actionCut,
this.isMobile && navigator.clipboard && actionCopy,
this.isMobile &&
this.deviceType.isMobile && actionCut,
this.deviceType.isMobile && navigator.clipboard && actionCopy,
this.deviceType.isMobile &&
navigator.clipboard && {
name: "paste",
perform: (elements, appStates) => {
@@ -5336,7 +5498,7 @@ class App extends React.Component<AppProps, AppState> {
},
contextItemLabel: "labels.paste",
},
this.isMobile && separator,
this.deviceType.isMobile && separator,
...options,
separator,
actionCopyStyles,
+2 -2
View File
@@ -1,6 +1,6 @@
import { useState } from "react";
import { t } from "../i18n";
import { useIsMobile } from "./App";
import { useDeviceType } from "./App";
import { trash } from "./icons";
import { ToolButton } from "./ToolButton";
@@ -19,7 +19,7 @@ const ClearCanvas = ({ onConfirm }: { onConfirm: () => void }) => {
icon={trash}
title={t("buttons.clearReset")}
aria-label={t("buttons.clearReset")}
showAriaLabel={useIsMobile()}
showAriaLabel={useDeviceType().isMobile}
onClick={toggleDialog}
data-testid="clear-canvas-button"
/>
+2 -2
View File
@@ -1,7 +1,7 @@
import clsx from "clsx";
import { ToolButton } from "./ToolButton";
import { t } from "../i18n";
import { useIsMobile } from "../components/App";
import { useDeviceType } from "../components/App";
import { users } from "./icons";
import "./CollabButton.scss";
@@ -26,7 +26,7 @@ const CollabButton = ({
type="button"
title={t("labels.liveCollaboration")}
aria-label={t("labels.liveCollaboration")}
showAriaLabel={useIsMobile()}
showAriaLabel={useDeviceType().isMobile}
>
{collaboratorCount > 0 && (
<div className="CollabButton-collaborators">{collaboratorCount}</div>
+2 -2
View File
@@ -2,7 +2,7 @@ import clsx from "clsx";
import React, { useEffect, useState } from "react";
import { useCallbackRefState } from "../hooks/useCallbackRefState";
import { t } from "../i18n";
import { useExcalidrawContainer, useIsMobile } from "../components/App";
import { useExcalidrawContainer, useDeviceType } from "../components/App";
import { KEYS } from "../keys";
import "./Dialog.scss";
import { back, close } from "./icons";
@@ -94,7 +94,7 @@ export const Dialog = (props: DialogProps) => {
onClick={onClose}
aria-label={t("buttons.close")}
>
{useIsMobile() ? back : close}
{useDeviceType().isMobile ? back : close}
</button>
</h2>
<div className="Dialog__content">{props.children}</div>
+6 -2
View File
@@ -139,7 +139,7 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
<Section title={t("helpDialog.shortcuts")}>
<Columns>
<Column>
<ShortcutIsland caption={t("helpDialog.shapes")}>
<ShortcutIsland caption={t("helpDialog.tools")}>
<Shortcut
label={t("toolBar.selection")}
shortcuts={["V", "1"]}
@@ -149,7 +149,7 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
shortcuts={["R", "2"]}
/>
<Shortcut label={t("toolBar.diamond")} shortcuts={["D", "3"]} />
<Shortcut label={t("toolBar.ellipse")} shortcuts={["E", "4"]} />
<Shortcut label={t("toolBar.ellipse")} shortcuts={["O", "4"]} />
<Shortcut label={t("toolBar.arrow")} shortcuts={["A", "5"]} />
<Shortcut label={t("toolBar.line")} shortcuts={["P", "6"]} />
<Shortcut
@@ -159,6 +159,10 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
<Shortcut label={t("toolBar.text")} shortcuts={["T", "8"]} />
<Shortcut label={t("toolBar.image")} shortcuts={["9"]} />
<Shortcut label={t("toolBar.library")} shortcuts={["0"]} />
<Shortcut
label={t("toolBar.eraser")}
shortcuts={[getShortcutKey("E")]}
/>
<Shortcut
label={t("helpDialog.editSelectedShape")}
shortcuts={[
+4
View File
@@ -11,6 +11,7 @@ import {
isTextElement,
} from "../element/typeChecks";
import { getShortcutKey } from "../utils";
import { isEraserActive } from "../appState";
interface HintViewerProps {
appState: AppState;
@@ -22,6 +23,9 @@ const getHints = ({ appState, elements, isMobile }: HintViewerProps) => {
const { elementType, isResizing, isRotating, lastPointerDownWith } = appState;
const multiMode = appState.multiElement !== null;
if (isEraserActive(appState)) {
return t("hints.eraserRevert");
}
if (elementType === "arrow" || elementType === "line") {
if (!multiMode) {
return t("hints.linearElement");
+2 -2
View File
@@ -6,7 +6,7 @@ import { canvasToBlob } from "../data/blob";
import { NonDeletedExcalidrawElement } from "../element/types";
import { CanvasError } from "../errors";
import { t } from "../i18n";
import { useIsMobile } from "./App";
import { useDeviceType } from "./App";
import { getSelectedElements, isSomeElementSelected } from "../scene";
import { exportToCanvas } from "../scene/export";
import { AppState, BinaryFiles } from "../types";
@@ -250,7 +250,7 @@ export const ImageExportDialog = ({
icon={exportImage}
type="button"
aria-label={t("buttons.exportImage")}
showAriaLabel={useIsMobile()}
showAriaLabel={useDeviceType().isMobile}
title={t("buttons.exportImage")}
/>
{modalIsShown && (
+2 -2
View File
@@ -2,7 +2,7 @@ import React, { useState } from "react";
import { ActionsManagerInterface } from "../actions/types";
import { NonDeletedExcalidrawElement } from "../element/types";
import { t } from "../i18n";
import { useIsMobile } from "./App";
import { useDeviceType } from "./App";
import { AppState, ExportOpts, BinaryFiles } from "../types";
import { Dialog } from "./Dialog";
import { exportFile, exportToFileIcon, link } from "./icons";
@@ -114,7 +114,7 @@ export const JSONExportDialog = ({
icon={exportFile}
type="button"
aria-label={t("buttons.export")}
showAriaLabel={useIsMobile()}
showAriaLabel={useDeviceType().isMobile}
title={t("buttons.export")}
/>
{modalIsShown && (
+50 -16
View File
@@ -6,10 +6,15 @@ import { exportCanvas } from "../data";
import { isTextElement, showSelectedShapeActions } from "../element";
import { NonDeletedExcalidrawElement } from "../element/types";
import { Language, t } from "../i18n";
import { useIsMobile } from "../components/App";
import { calculateScrollCenter, getSelectedElements } from "../scene";
import { ExportType } from "../scene/types";
import { AppProps, AppState, ExcalidrawProps, BinaryFiles } from "../types";
import {
AppProps,
AppState,
ExcalidrawProps,
BinaryFiles,
DeviceType,
} from "../types";
import { muteFSAbortError } from "../utils";
import { SelectedShapeActions, ShapesSwitcher, ZoomActions } from "./Actions";
import { BackgroundPickerAndDarkModeToggle } from "./BackgroundPickerAndDarkModeToggle";
@@ -37,6 +42,7 @@ import { LibraryMenu } from "./LibraryMenu";
import "./LayerUI.scss";
import "./Toolbar.scss";
import { PenModeButton } from "./PenModeButton";
import { useDeviceType } from "../components/App";
interface LayerUIProps {
actionManager: ActionManager;
@@ -44,6 +50,7 @@ interface LayerUIProps {
files: BinaryFiles;
canvas: HTMLCanvasElement | null;
setAppState: React.Component<any, AppState>["setState"];
setDeviceType: (obj: Partial<DeviceType>) => void;
elements: readonly NonDeletedExcalidrawElement[];
onCollabButtonClick?: () => void;
onLockToggle: () => void;
@@ -74,6 +81,7 @@ const LayerUI = ({
appState,
files,
setAppState,
setDeviceType,
canvas,
elements,
onCollabButtonClick,
@@ -95,7 +103,7 @@ const LayerUI = ({
id,
onImageAction,
}: LayerUIProps) => {
const isMobile = useIsMobile();
const deviceType = useDeviceType();
const renderJSONExportDialog = () => {
if (!UIOptions.canvasActions.export) {
@@ -321,7 +329,7 @@ const LayerUI = ({
checked={appState.penMode}
onChange={onPenModeToggle}
title={t("toolBar.penMode")}
penDetected={appState.penDetected}
penDetected={deviceType.penDetected}
/>
<LockButton
zenModeEnabled={zenModeEnabled}
@@ -338,11 +346,12 @@ const LayerUI = ({
<HintViewer
appState={appState}
elements={elements}
isMobile={isMobile}
isMobile={deviceType.isMobile}
/>
{heading}
<Stack.Row gap={1}>
<ShapesSwitcher
appState={appState}
canvas={canvas}
elementType={appState.elementType}
setAppState={setAppState}
@@ -351,6 +360,7 @@ const LayerUI = ({
insertOnCanvasDirectly: pointerType !== "mouse",
});
}}
setDeviceType={setDeviceType}
/>
</Stack.Row>
</Island>
@@ -388,7 +398,7 @@ const LayerUI = ({
</Tooltip>
))}
</UserList>
{renderTopRightUI?.(isMobile, appState)}
{renderTopRightUI?.(deviceType.isMobile, appState)}
</div>
</div>
</FixedSideContainer>
@@ -418,16 +428,39 @@ const LayerUI = ({
/>
</Island>
{!viewModeEnabled && (
<div
className={clsx("undo-redo-buttons zen-mode-transition", {
"layer-ui__wrapper__footer-left--transition-bottom":
zenModeEnabled,
})}
>
{actionManager.renderAction("undo", { size: "small" })}
{actionManager.renderAction("redo", { size: "small" })}
</div>
<>
<div
className={clsx("undo-redo-buttons zen-mode-transition", {
"layer-ui__wrapper__footer-left--transition-bottom":
zenModeEnabled,
})}
>
{actionManager.renderAction("undo", { size: "small" })}
{actionManager.renderAction("redo", { size: "small" })}
</div>
<div
className={clsx("eraser-buttons zen-mode-transition", {
"layer-ui__wrapper__footer-left--transition-left":
zenModeEnabled,
})}
>
{actionManager.renderAction("eraser", { size: "small" })}
</div>
</>
)}
{!viewModeEnabled &&
appState.multiElement &&
deviceType.isTouchScreen && (
<div
className={clsx("finalize-button zen-mode-transition", {
"layer-ui__wrapper__footer-left--transition-left":
zenModeEnabled,
})}
>
{actionManager.renderAction("finalize", { size: "small" })}
</div>
)}
</Section>
</Stack.Col>
</div>
@@ -495,7 +528,7 @@ const LayerUI = ({
</>
);
return isMobile ? (
return deviceType.isMobile ? (
<>
{dialogs}
<MobileMenu
@@ -506,6 +539,7 @@ const LayerUI = ({
renderJSONExportDialog={renderJSONExportDialog}
renderImageExportDialog={renderImageExportDialog}
setAppState={setAppState}
setDeviceType={setDeviceType}
onCollabButtonClick={onCollabButtonClick}
onLockToggle={onLockToggle}
onPenModeToggle={onPenModeToggle}
+2 -2
View File
@@ -12,7 +12,7 @@ import {
LibraryItems,
} from "../types";
import { muteFSAbortError } from "../utils";
import { useIsMobile } from "./App";
import { useDeviceType } from "./App";
import ConfirmDialog from "./ConfirmDialog";
import { exportToFileIcon, load, publishIcon, trash } from "./icons";
import { LibraryUnit } from "./LibraryUnit";
@@ -85,7 +85,7 @@ const LibraryMenuItems = ({
const [showRemoveLibAlert, setShowRemoveLibAlert] = useState(false);
const isMobile = useIsMobile();
const isMobile = useDeviceType().isMobile;
const renderLibraryActions = () => {
const itemsSelected = !!selectedItems.length;
+2 -2
View File
@@ -2,7 +2,7 @@ import clsx from "clsx";
import oc from "open-color";
import { useEffect, useRef, useState } from "react";
import { MIME_TYPES } from "../constants";
import { useIsMobile } from "../components/App";
import { useDeviceType } from "../components/App";
import { exportToSvg } from "../scene/export";
import { BinaryFiles, LibraryItem } from "../types";
import "./LibraryUnit.scss";
@@ -66,7 +66,7 @@ export const LibraryUnit = ({
}, [elements, files]);
const [isHovered, setIsHovered] = useState(false);
const isMobile = useIsMobile();
const isMobile = useDeviceType().isMobile;
const adder = isPending && (
<div className="library-unit__adder">{PLUS_ICON}</div>
);
+19 -3
View File
@@ -1,5 +1,5 @@
import React from "react";
import { AppState } from "../types";
import { AppState, DeviceType } from "../types";
import { ActionManager } from "../actions/manager";
import { t } from "../i18n";
import Stack from "./Stack";
@@ -8,7 +8,7 @@ import { NonDeletedExcalidrawElement } from "../element/types";
import { FixedSideContainer } from "./FixedSideContainer";
import { Island } from "./Island";
import { HintViewer } from "./HintViewer";
import { calculateScrollCenter } from "../scene";
import { calculateScrollCenter, getSelectedElements } from "../scene";
import { SelectedShapeActions, ShapesSwitcher } from "./Actions";
import { Section } from "./Section";
import CollabButton from "./CollabButton";
@@ -18,6 +18,7 @@ import { UserList } from "./UserList";
import { BackgroundPickerAndDarkModeToggle } from "./BackgroundPickerAndDarkModeToggle";
import { LibraryButton } from "./LibraryButton";
import { PenModeButton } from "./PenModeButton";
import { useDeviceType } from "./App";
type MobileMenuProps = {
appState: AppState;
@@ -25,6 +26,7 @@ type MobileMenuProps = {
renderJSONExportDialog: () => React.ReactNode;
renderImageExportDialog: () => React.ReactNode;
setAppState: React.Component<any, AppState>["setState"];
setDeviceType: (obj: Partial<DeviceType>) => void;
elements: readonly NonDeletedExcalidrawElement[];
libraryMenu: JSX.Element | null;
onCollabButtonClick?: () => void;
@@ -50,6 +52,7 @@ export const MobileMenu = ({
renderJSONExportDialog,
renderImageExportDialog,
setAppState,
setDeviceType,
onCollabButtonClick,
onLockToggle,
onPenModeToggle,
@@ -61,6 +64,7 @@ export const MobileMenu = ({
onImageAction,
renderTopRightUI,
}: MobileMenuProps) => {
const deviceType = useDeviceType();
const renderToolbar = () => {
return (
<FixedSideContainer side="top" className="App-top-bar">
@@ -72,6 +76,7 @@ export const MobileMenu = ({
{heading}
<Stack.Row gap={1}>
<ShapesSwitcher
appState={appState}
canvas={canvas}
elementType={appState.elementType}
setAppState={setAppState}
@@ -80,6 +85,7 @@ export const MobileMenu = ({
insertOnCanvasDirectly: pointerType !== "mouse",
});
}}
setDeviceType={setDeviceType}
/>
</Stack.Row>
</Island>
@@ -100,7 +106,7 @@ export const MobileMenu = ({
onChange={onPenModeToggle}
title={t("toolBar.penMode")}
isMobile
penDetected={appState.penDetected}
penDetected={deviceType.penDetected}
/>
</Stack.Row>
{libraryMenu}
@@ -113,6 +119,12 @@ export const MobileMenu = ({
};
const renderAppToolbar = () => {
// Render eraser conditionally in mobile
const showEraser =
!appState.viewModeEnabled &&
!appState.editingElement &&
getSelectedElements(elements, appState).length === 0;
if (viewModeEnabled) {
return (
<div className="App-toolbar-content">
@@ -120,12 +132,16 @@ export const MobileMenu = ({
</div>
);
}
return (
<div className="App-toolbar-content">
{actionManager.renderAction("toggleCanvasMenu")}
{actionManager.renderAction("toggleEditMenu")}
{actionManager.renderAction("undo")}
{actionManager.renderAction("redo")}
{showEraser && actionManager.renderAction("eraser")}
{actionManager.renderAction(
appState.multiElement ? "finalize" : "duplicateSelection",
)}
+6 -6
View File
@@ -4,7 +4,7 @@ import React, { useState, useLayoutEffect, useRef } from "react";
import { createPortal } from "react-dom";
import clsx from "clsx";
import { KEYS } from "../keys";
import { useExcalidrawContainer, useIsMobile } from "./App";
import { useExcalidrawContainer, useDeviceType } from "./App";
import { AppState } from "../types";
import { THEME } from "../constants";
@@ -59,17 +59,17 @@ export const Modal = (props: {
const useBodyRoot = (theme: AppState["theme"]) => {
const [div, setDiv] = useState<HTMLDivElement | null>(null);
const isMobile = useIsMobile();
const isMobileRef = useRef(isMobile);
isMobileRef.current = isMobile;
const deviceType = useDeviceType();
const isMobileRef = useRef(deviceType.isMobile);
isMobileRef.current = deviceType.isMobile;
const { container: excalidrawContainer } = useExcalidrawContainer();
useLayoutEffect(() => {
if (div) {
div.classList.toggle("excalidraw--mobile", isMobile);
div.classList.toggle("excalidraw--mobile", deviceType.isMobile);
}
}, [div, isMobile]);
}, [div, deviceType.isMobile]);
useLayoutEffect(() => {
const isDarkTheme =
+3 -3
View File
@@ -2,7 +2,7 @@ import React from "react";
import { getCommonBounds } from "../element/bounds";
import { NonDeletedExcalidrawElement } from "../element/types";
import { t } from "../i18n";
import { useIsMobile } from "../components/App";
import { useDeviceType } from "../components/App";
import { getTargetElements } from "../scene";
import { AppState, ExcalidrawProps } from "../types";
import { close } from "./icons";
@@ -16,13 +16,13 @@ export const Stats = (props: {
onClose: () => void;
renderCustomStats: ExcalidrawProps["renderCustomStats"];
}) => {
const isMobile = useIsMobile();
const deviceType = useDeviceType();
const boundingBox = getCommonBounds(props.elements);
const selectedElements = getTargetElements(props.elements, props.appState);
const selectedBoundingBox = getCommonBounds(selectedElements);
if (isMobile && props.appState.openMenu) {
if (deviceType.isMobile && props.appState.openMenu) {
return null;
}
+2
View File
@@ -48,6 +48,7 @@ type ToolButtonProps =
type: "radio";
checked: boolean;
onChange?(data: { pointerType: PointerType | null }): void;
onPointerDown?(data: { pointerType: PointerType }): void;
});
export const ToolButton = React.forwardRef((props: ToolButtonProps, ref) => {
@@ -149,6 +150,7 @@ export const ToolButton = React.forwardRef((props: ToolButtonProps, ref) => {
title={props.title}
onPointerDown={(event) => {
lastPointerTypeRef.current = event.pointerType || null;
props.onPointerDown?.({ pointerType: event.pointerType || null });
}}
onPointerUp={() => {
requestAnimationFrame(() => {
+4
View File
@@ -934,3 +934,7 @@ export const editIcon = createIcon(
></path>,
{ width: 640, height: 512 },
);
export const eraser = createIcon(
<path d="M480 416C497.7 416 512 430.3 512 448C512 465.7 497.7 480 480 480H150.6C133.7 480 117.4 473.3 105.4 461.3L25.37 381.3C.3786 356.3 .3786 315.7 25.37 290.7L258.7 57.37C283.7 32.38 324.3 32.38 349.3 57.37L486.6 194.7C511.6 219.7 511.6 260.3 486.6 285.3L355.9 416H480zM265.4 416L332.7 348.7L195.3 211.3L70.63 336L150.6 416L265.4 416z" />,
);
+21 -1
View File
@@ -290,6 +290,16 @@
width: 100%;
box-sizing: border-box;
.eraser {
&.ToolIcon:hover {
--icon-fill-color: #fff;
--keybinding-color: #fff;
}
&.active {
background-color: var(--color-primary);
}
}
}
.App-toolbar-content {
@@ -467,7 +477,17 @@
font-family: var(--ui-font);
}
.undo-redo-buttons {
.finalize-button {
display: grid;
grid-auto-flow: column;
gap: 0.4em;
margin-top: auto;
margin-bottom: auto;
margin-inline-start: 0.6em;
}
.undo-redo-buttons,
.eraser-buttons {
display: grid;
grid-auto-flow: column;
gap: 0.4em;
+3 -2
View File
@@ -31,8 +31,8 @@ type RestoredAppState = Omit<
>;
export const AllowedExcalidrawElementTypes: Record<
ExcalidrawElement["type"],
true
AppState["elementType"],
boolean
> = {
selection: true,
text: true,
@@ -43,6 +43,7 @@ export const AllowedExcalidrawElementTypes: Record<
image: true,
arrow: true,
freedraw: true,
eraser: false,
};
export type RestoredDataState = {
+1 -2
View File
@@ -1,4 +1,3 @@
import { SHAPES } from "../shapes";
import { updateBoundElements } from "./binding";
import { getCommonBounds } from "./bounds";
import { mutateElement } from "./mutateElement";
@@ -93,7 +92,7 @@ export const getDragOffsetXY = (
export const dragNewElement = (
draggingElement: NonDeletedExcalidrawElement,
elementType: typeof SHAPES[number]["value"],
elementType: AppState["elementType"],
originX: number,
originY: number,
x: number,
+2 -1
View File
@@ -10,5 +10,6 @@ export const showSelectedShapeActions = (
!appState.viewModeEnabled &&
(appState.editingElement ||
getSelectedElements(elements, appState).length ||
appState.elementType !== "selection"),
(appState.elementType !== "selection" &&
appState.elementType !== "eraser")),
);
+3 -2
View File
@@ -1,3 +1,4 @@
import { AppState } from "../types";
import {
ExcalidrawElement,
ExcalidrawTextElement,
@@ -60,7 +61,7 @@ export const isLinearElement = (
};
export const isLinearElementType = (
elementType: ExcalidrawElement["type"],
elementType: AppState["elementType"],
): boolean => {
return (
elementType === "arrow" || elementType === "line" // || elementType === "freedraw"
@@ -74,7 +75,7 @@ export const isBindingElement = (
};
export const isBindingElementType = (
elementType: ExcalidrawElement["type"],
elementType: AppState["elementType"],
): boolean => {
return elementType === "arrow";
};
+5 -3
View File
@@ -196,7 +196,8 @@
"library": "Library",
"lock": "Keep selected tool active after drawing",
"penMode": "Prevent pinch-zoom and accept freedraw input only from pen",
"link": "Add/ Update link for a selected shape"
"link": "Add/ Update link for a selected shape",
"eraser": "Eraser"
},
"headings": {
"canvasActions": "Canvas actions",
@@ -221,7 +222,8 @@
"placeImage": "Click to place the image, or click and drag to set its size manually",
"publishLibrary": "Publish your own library",
"bindTextToElement": "Press enter to add text",
"deepBoxSelect": "Hold CtrlOrCmd to deep select, and to prevent dragging"
"deepBoxSelect": "Hold CtrlOrCmd to deep select, and to prevent dragging",
"eraserRevert": "Hold Alt to revert the elements marked for deletion"
},
"canvasError": {
"cannotShowPreview": "Cannot show preview",
@@ -281,7 +283,7 @@
"howto": "Follow our guides",
"or": "or",
"preventBinding": "Prevent arrow binding",
"shapes": "Shapes",
"tools": "Tools",
"shortcuts": "Keyboard shortcuts",
"textFinish": "Finish editing (text editor)",
"textNewLine": "Add new line (text editor)",
+2 -2
View File
@@ -56,12 +56,12 @@
"babel-loader": "8.2.3",
"babel-plugin-transform-class-properties": "6.24.1",
"cross-env": "7.0.3",
"css-loader": "6.6.0",
"css-loader": "6.7.1",
"mini-css-extract-plugin": "2.4.6",
"postcss-loader": "6.2.1",
"sass-loader": "12.4.0",
"terser-webpack-plugin": "5.3.1",
"ts-loader": "9.2.6",
"ts-loader": "9.2.8",
"typescript": "4.5.4",
"webpack": "5.65.0",
"webpack-bundle-analyzer": "4.5.0",
+19 -32
View File
@@ -2069,13 +2069,13 @@ cross-spawn@^7.0.1, cross-spawn@^7.0.3:
shebang-command "^2.0.0"
which "^2.0.1"
css-loader@6.6.0:
version "6.6.0"
resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-6.6.0.tgz#c792ad5510bd1712618b49381bd0310574fafbd3"
integrity sha512-FK7H2lisOixPT406s5gZM1S3l8GrfhEBT3ZiL2UX1Ng1XWs0y2GPllz/OTyvbaHe12VgQrIXIzuEGVlbUhodqg==
css-loader@6.7.1:
version "6.7.1"
resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-6.7.1.tgz#e98106f154f6e1baf3fc3bc455cb9981c1d5fd2e"
integrity sha512-yB5CNFa14MbPJcomwNh3wLThtkZgcNyI2bNMRt8iE5Z8Vwl7f8vQXFAzn2HDOJvtDq2NTZBUGMSUNNyrv3/+cw==
dependencies:
icss-utils "^5.1.0"
postcss "^8.4.5"
postcss "^8.4.7"
postcss-modules-extract-imports "^3.0.0"
postcss-modules-local-by-default "^4.0.0"
postcss-modules-scope "^3.0.0"
@@ -3036,15 +3036,7 @@ methods@~1.1.2:
resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=
micromatch@^4.0.0:
version "4.0.2"
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.2.tgz#4fcb0999bf9fbc2fcbdd212f6d629b9a56c39259"
integrity sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==
dependencies:
braces "^3.0.1"
picomatch "^2.0.5"
micromatch@^4.0.2, micromatch@^4.0.4:
micromatch@^4.0.0, micromatch@^4.0.2, micromatch@^4.0.4:
version "4.0.4"
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.4.tgz#896d519dfe9db25fce94ceb7a500919bf881ebf9"
integrity sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==
@@ -3150,10 +3142,10 @@ multicast-dns@^6.0.1:
dns-packet "^1.3.1"
thunky "^1.0.2"
nanoid@^3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.2.0.tgz#62667522da6673971cca916a6d3eff3f415ff80c"
integrity sha512-fmsZYa9lpn69Ad5eDn7FMcnnSR+8R34W9qJEijxYhTbfOWzr22n1QxCMzXLK+ODyW2973V3Fux959iQoUxzUIA==
nanoid@^3.3.1:
version "3.3.1"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.1.tgz#6347a18cac88af88f58af0b3594b723d5e99bb35"
integrity sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw==
negotiator@0.6.2:
version "0.6.2"
@@ -3356,11 +3348,6 @@ picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3:
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.0.tgz#f1f061de8f6a4bf022892e2d128234fb98302972"
integrity sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==
picomatch@^2.0.5:
version "2.2.2"
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad"
integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==
pkg-dir@^4.1.0, pkg-dir@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3"
@@ -3429,12 +3416,12 @@ postcss-value-parser@^4.1.0, postcss-value-parser@^4.2.0:
resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514"
integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==
postcss@^8.4.5:
version "8.4.6"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.6.tgz#c5ff3c3c457a23864f32cb45ac9b741498a09ae1"
integrity sha512-OovjwIzs9Te46vlEx7+uXB0PLijpwjXGKXjVGGPIGubGpq7uh5Xgf6D6FiJ/SzJMBosHDp6a2hiXOS97iBXcaA==
postcss@^8.4.7:
version "8.4.8"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.8.tgz#dad963a76e82c081a0657d3a2f3602ce10c2e032"
integrity sha512-2tXEqGxrjvAO6U+CJzDL2Fk2kPHTv1jQsYkSoMeOis2SsYaXRO2COxTdQp99cYvif9JTXaAk9lYGc3VhJt7JPQ==
dependencies:
nanoid "^3.2.0"
nanoid "^3.3.1"
picocolors "^1.0.0"
source-map-js "^1.0.2"
@@ -4002,10 +3989,10 @@ totalist@^1.0.0:
resolved "https://registry.yarnpkg.com/totalist/-/totalist-1.1.0.tgz#a4d65a3e546517701e3e5c37a47a70ac97fe56df"
integrity sha512-gduQwd1rOdDMGxFG1gEvhV88Oirdo2p+KjoYFU7k2g+i7n6AFFbDQ5kMPUsW0pNbfQsB/cwXvT1i4Bue0s9g5g==
ts-loader@9.2.6:
version "9.2.6"
resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-9.2.6.tgz#9937c4dd0a1e3dbbb5e433f8102a6601c6615d74"
integrity sha512-QMTC4UFzHmu9wU2VHZEmWWE9cUajjfcdcws+Gh7FhiO+Dy0RnR1bNz0YCHqhI0yRowCE9arVnNxYHqELOy9Hjw==
ts-loader@9.2.8:
version "9.2.8"
resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-9.2.8.tgz#e89aa32fa829c5cad0a1d023d6b3adecd51d5a48"
integrity sha512-gxSak7IHUuRtwKf3FIPSW1VpZcqF9+MBrHOvBp9cjHh+525SjtCIJKVGjRKIAfxBwDGDGCFF00rTfzB1quxdSw==
dependencies:
chalk "^4.1.0"
enhanced-resolve "^5.0.0"
+2 -2
View File
@@ -44,10 +44,10 @@
"babel-loader": "8.2.3",
"babel-plugin-transform-class-properties": "6.24.1",
"cross-env": "7.0.3",
"css-loader": "6.5.1",
"css-loader": "6.7.1",
"file-loader": "6.2.0",
"sass-loader": "12.4.0",
"ts-loader": "9.2.6",
"ts-loader": "9.2.8",
"webpack": "5.66.0",
"webpack-bundle-analyzer": "4.5.0",
"webpack-cli": "4.9.2"
+29 -34
View File
@@ -1565,11 +1565,6 @@ color-name@~1.1.4:
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
colorette@^1.2.2:
version "1.2.2"
resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.2.tgz#cbcc79d5e99caea2dbf10eb3a26fd8b3e6acfa94"
integrity sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w==
colorette@^2.0.14:
version "2.0.16"
resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.16.tgz#713b9af84fdb000139f04546bd4a93f62a5085da"
@@ -1634,18 +1629,18 @@ cross-spawn@^7.0.1, cross-spawn@^7.0.3:
shebang-command "^2.0.0"
which "^2.0.1"
css-loader@6.5.1:
version "6.5.1"
resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-6.5.1.tgz#0c43d4fbe0d97f699c91e9818cb585759091d1b1"
integrity sha512-gEy2w9AnJNnD9Kuo4XAP9VflW/ujKoS9c/syO+uWMlm5igc7LysKzPXaDoR2vroROkSwsTS2tGr1yGGEbZOYZQ==
css-loader@6.7.1:
version "6.7.1"
resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-6.7.1.tgz#e98106f154f6e1baf3fc3bc455cb9981c1d5fd2e"
integrity sha512-yB5CNFa14MbPJcomwNh3wLThtkZgcNyI2bNMRt8iE5Z8Vwl7f8vQXFAzn2HDOJvtDq2NTZBUGMSUNNyrv3/+cw==
dependencies:
icss-utils "^5.1.0"
postcss "^8.2.15"
postcss "^8.4.7"
postcss-modules-extract-imports "^3.0.0"
postcss-modules-local-by-default "^4.0.0"
postcss-modules-scope "^3.0.0"
postcss-modules-values "^4.0.0"
postcss-value-parser "^4.1.0"
postcss-value-parser "^4.2.0"
semver "^7.3.5"
cssesc@^3.0.0:
@@ -2142,10 +2137,10 @@ ms@2.1.2:
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
nanoid@^3.1.23:
version "3.2.0"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.2.0.tgz#62667522da6673971cca916a6d3eff3f415ff80c"
integrity sha512-fmsZYa9lpn69Ad5eDn7FMcnnSR+8R34W9qJEijxYhTbfOWzr22n1QxCMzXLK+ODyW2973V3Fux959iQoUxzUIA==
nanoid@^3.3.1:
version "3.3.1"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.1.tgz#6347a18cac88af88f58af0b3594b723d5e99bb35"
integrity sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw==
neo-async@^2.6.2:
version "2.6.2"
@@ -2292,19 +2287,19 @@ postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4:
uniq "^1.0.1"
util-deprecate "^1.0.2"
postcss-value-parser@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz#443f6a20ced6481a2bda4fa8532a6e55d789a2cb"
integrity sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==
postcss-value-parser@^4.1.0, postcss-value-parser@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514"
integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==
postcss@^8.2.15:
version "8.3.0"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.3.0.tgz#b1a713f6172ca427e3f05ef1303de8b65683325f"
integrity sha512-+ogXpdAjWGa+fdYY5BQ96V/6tAo+TdSSIMP5huJBIygdWwKtVoB5JWZ7yUd4xZ8r+8Kvvx4nyg/PQ071H4UtcQ==
postcss@^8.4.7:
version "8.4.8"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.8.tgz#dad963a76e82c081a0657d3a2f3602ce10c2e032"
integrity sha512-2tXEqGxrjvAO6U+CJzDL2Fk2kPHTv1jQsYkSoMeOis2SsYaXRO2COxTdQp99cYvif9JTXaAk9lYGc3VhJt7JPQ==
dependencies:
colorette "^1.2.2"
nanoid "^3.1.23"
source-map-js "^0.6.2"
nanoid "^3.3.1"
picocolors "^1.0.0"
source-map-js "^1.0.2"
punycode@^2.1.0:
version "2.1.1"
@@ -2491,10 +2486,10 @@ sirv@^1.0.7:
mime "^2.3.1"
totalist "^1.0.0"
source-map-js@^0.6.2:
version "0.6.2"
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-0.6.2.tgz#0bb5de631b41cfbda6cfba8bd05a80efdfd2385e"
integrity sha512-/3GptzWzu0+0MBQFrDKzw/DvvMTUORvgY6k6jd/VS6iCR4RDTKWH6v6WPwQoUO8667uQEf9Oe38DxAYWY5F/Ug==
source-map-js@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c"
integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==
source-map-support@~0.5.19:
version "0.5.19"
@@ -2605,10 +2600,10 @@ totalist@^1.0.0:
resolved "https://registry.yarnpkg.com/totalist/-/totalist-1.1.0.tgz#a4d65a3e546517701e3e5c37a47a70ac97fe56df"
integrity sha512-gduQwd1rOdDMGxFG1gEvhV88Oirdo2p+KjoYFU7k2g+i7n6AFFbDQ5kMPUsW0pNbfQsB/cwXvT1i4Bue0s9g5g==
ts-loader@9.2.6:
version "9.2.6"
resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-9.2.6.tgz#9937c4dd0a1e3dbbb5e433f8102a6601c6615d74"
integrity sha512-QMTC4UFzHmu9wU2VHZEmWWE9cUajjfcdcws+Gh7FhiO+Dy0RnR1bNz0YCHqhI0yRowCE9arVnNxYHqELOy9Hjw==
ts-loader@9.2.8:
version "9.2.8"
resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-9.2.8.tgz#e89aa32fa829c5cad0a1d023d6b3adecd51d5a48"
integrity sha512-gxSak7IHUuRtwKf3FIPSW1VpZcqF9+MBrHOvBp9cjHh+525SjtCIJKVGjRKIAfxBwDGDGCFF00rTfzB1quxdSw==
dependencies:
chalk "^4.1.0"
enhanced-resolve "^5.0.0"
+1 -1
View File
@@ -40,7 +40,7 @@ export const SHAPES = [
</svg>
),
value: "ellipse",
key: KEYS.E,
key: KEYS.O,
},
{
icon: (
@@ -49,7 +49,6 @@ Object {
"data": null,
"shown": false,
},
"penDetected": false,
"penMode": false,
"pendingImageElement": null,
"previousSelectedElementIds": Object {},
@@ -220,7 +219,6 @@ Object {
"data": null,
"shown": false,
},
"penDetected": false,
"penMode": false,
"pendingImageElement": null,
"previousSelectedElementIds": Object {},
@@ -547,7 +545,6 @@ Object {
"data": null,
"shown": false,
},
"penDetected": false,
"penMode": false,
"pendingImageElement": null,
"previousSelectedElementIds": Object {},
@@ -874,7 +871,6 @@ Object {
"data": null,
"shown": false,
},
"penDetected": false,
"penMode": false,
"pendingImageElement": null,
"previousSelectedElementIds": Object {},
@@ -1045,7 +1041,6 @@ Object {
"data": null,
"shown": false,
},
"penDetected": false,
"penMode": false,
"pendingImageElement": null,
"previousSelectedElementIds": Object {},
@@ -1251,7 +1246,6 @@ Object {
"data": null,
"shown": false,
},
"penDetected": false,
"penMode": false,
"pendingImageElement": null,
"previousSelectedElementIds": Object {},
@@ -1514,7 +1508,6 @@ Object {
"data": null,
"shown": false,
},
"penDetected": false,
"penMode": false,
"pendingImageElement": null,
"previousSelectedElementIds": Object {
@@ -1859,7 +1852,6 @@ Object {
"data": null,
"shown": false,
},
"penDetected": false,
"penMode": false,
"pendingImageElement": null,
"previousSelectedElementIds": Object {},
@@ -2634,7 +2626,6 @@ Object {
"data": null,
"shown": false,
},
"penDetected": false,
"penMode": false,
"pendingImageElement": null,
"previousSelectedElementIds": Object {},
@@ -2961,7 +2952,6 @@ Object {
"data": null,
"shown": false,
},
"penDetected": false,
"penMode": false,
"pendingImageElement": null,
"previousSelectedElementIds": Object {},
@@ -3288,7 +3278,6 @@ Object {
"data": null,
"shown": false,
},
"penDetected": false,
"penMode": false,
"pendingImageElement": null,
"previousSelectedElementIds": Object {
@@ -3693,7 +3682,6 @@ Object {
"data": null,
"shown": false,
},
"penDetected": false,
"penMode": false,
"pendingImageElement": null,
"previousSelectedElementIds": Object {
@@ -3962,7 +3950,6 @@ Object {
"data": null,
"shown": false,
},
"penDetected": false,
"penMode": false,
"pendingImageElement": null,
"previousSelectedElementIds": Object {
@@ -4310,7 +4297,6 @@ Object {
"data": null,
"shown": false,
},
"penDetected": false,
"penMode": false,
"pendingImageElement": null,
"previousSelectedElementIds": Object {},
@@ -4412,7 +4398,6 @@ Object {
"data": null,
"shown": false,
},
"penDetected": false,
"penMode": false,
"pendingImageElement": null,
"previousSelectedElementIds": Object {},
@@ -4492,7 +4477,6 @@ Object {
"data": null,
"shown": false,
},
"penDetected": false,
"penMode": false,
"pendingImageElement": null,
"previousSelectedElementIds": Object {},
@@ -49,7 +49,6 @@ Object {
"data": null,
"shown": false,
},
"penDetected": false,
"penMode": false,
"pendingImageElement": null,
"previousSelectedElementIds": Object {
@@ -542,7 +541,6 @@ Object {
"data": null,
"shown": false,
},
"penDetected": false,
"penMode": false,
"pendingImageElement": null,
"previousSelectedElementIds": Object {
@@ -1041,7 +1039,6 @@ Object {
"data": null,
"shown": false,
},
"penDetected": false,
"penMode": false,
"pendingImageElement": null,
"previousSelectedElementIds": Object {},
@@ -1875,7 +1872,6 @@ Object {
"data": null,
"shown": false,
},
"penDetected": false,
"penMode": false,
"pendingImageElement": null,
"previousSelectedElementIds": Object {
@@ -2089,7 +2085,6 @@ Object {
"data": null,
"shown": false,
},
"penDetected": false,
"penMode": false,
"pendingImageElement": null,
"previousSelectedElementIds": Object {
@@ -2579,7 +2574,6 @@ Object {
"data": null,
"shown": false,
},
"penDetected": false,
"penMode": false,
"pendingImageElement": null,
"previousSelectedElementIds": Object {
@@ -2846,7 +2840,6 @@ Object {
"data": null,
"shown": false,
},
"penDetected": false,
"penMode": false,
"pendingImageElement": null,
"previousSelectedElementIds": Object {},
@@ -3017,7 +3010,6 @@ Object {
"data": null,
"shown": false,
},
"penDetected": false,
"penMode": false,
"pendingImageElement": null,
"previousSelectedElementIds": Object {
@@ -3490,7 +3482,6 @@ Object {
"data": null,
"shown": false,
},
"penDetected": false,
"penMode": false,
"pendingImageElement": null,
"previousSelectedElementIds": Object {},
@@ -3739,7 +3730,6 @@ Object {
"data": null,
"shown": false,
},
"penDetected": false,
"penMode": false,
"pendingImageElement": null,
"previousSelectedElementIds": Object {
@@ -3953,7 +3943,6 @@ Object {
"data": null,
"shown": false,
},
"penDetected": false,
"penMode": false,
"pendingImageElement": null,
"previousSelectedElementIds": Object {
@@ -4210,7 +4199,6 @@ Object {
"data": null,
"shown": false,
},
"penDetected": false,
"penMode": false,
"pendingImageElement": null,
"previousSelectedElementIds": Object {
@@ -4476,7 +4464,6 @@ Object {
"data": null,
"shown": false,
},
"penDetected": false,
"penMode": false,
"pendingImageElement": null,
"previousSelectedElementIds": Object {
@@ -4884,7 +4871,6 @@ Object {
"data": null,
"shown": false,
},
"penDetected": false,
"penMode": false,
"pendingImageElement": null,
"previousSelectedElementIds": Object {
@@ -5197,7 +5183,6 @@ Object {
"data": null,
"shown": false,
},
"penDetected": false,
"penMode": false,
"pendingImageElement": null,
"previousSelectedElementIds": Object {
@@ -5486,7 +5471,6 @@ Object {
"data": null,
"shown": false,
},
"penDetected": false,
"penMode": false,
"pendingImageElement": null,
"previousSelectedElementIds": Object {
@@ -5705,7 +5689,6 @@ Object {
"data": null,
"shown": false,
},
"penDetected": false,
"penMode": false,
"pendingImageElement": null,
"previousSelectedElementIds": Object {
@@ -5876,7 +5859,6 @@ Object {
"data": null,
"shown": false,
},
"penDetected": false,
"penMode": false,
"pendingImageElement": null,
"previousSelectedElementIds": Object {},
@@ -6361,7 +6343,6 @@ Object {
"data": null,
"shown": false,
},
"penDetected": false,
"penMode": false,
"pendingImageElement": null,
"previousSelectedElementIds": Object {
@@ -6698,7 +6679,6 @@ Object {
"data": null,
"shown": false,
},
"penDetected": false,
"penMode": false,
"pendingImageElement": null,
"previousSelectedElementIds": Object {},
@@ -8876,7 +8856,6 @@ Object {
"data": null,
"shown": false,
},
"penDetected": false,
"penMode": false,
"pendingImageElement": null,
"previousSelectedElementIds": Object {
@@ -9261,7 +9240,6 @@ Object {
"data": null,
"shown": false,
},
"penDetected": false,
"penMode": false,
"pendingImageElement": null,
"previousSelectedElementIds": Object {
@@ -9528,7 +9506,6 @@ Object {
"data": null,
"shown": false,
},
"penDetected": false,
"penMode": false,
"pendingImageElement": null,
"previousSelectedElementIds": Object {
@@ -9757,7 +9734,6 @@ Object {
"data": null,
"shown": false,
},
"penDetected": false,
"penMode": false,
"pendingImageElement": null,
"previousSelectedElementIds": Object {
@@ -10053,7 +10029,6 @@ Object {
"data": null,
"shown": false,
},
"penDetected": false,
"penMode": false,
"pendingImageElement": null,
"previousSelectedElementIds": Object {},
@@ -10224,7 +10199,6 @@ Object {
"data": null,
"shown": false,
},
"penDetected": false,
"penMode": false,
"pendingImageElement": null,
"previousSelectedElementIds": Object {},
@@ -10395,7 +10369,6 @@ Object {
"data": null,
"shown": false,
},
"penDetected": false,
"penMode": false,
"pendingImageElement": null,
"previousSelectedElementIds": Object {},
@@ -10566,7 +10539,6 @@ Object {
"data": null,
"shown": false,
},
"penDetected": false,
"penMode": false,
"pendingImageElement": null,
"previousSelectedElementIds": Object {},
@@ -10767,7 +10739,6 @@ Object {
"data": null,
"shown": false,
},
"penDetected": false,
"penMode": false,
"pendingImageElement": null,
"previousSelectedElementIds": Object {},
@@ -10968,7 +10939,6 @@ Object {
"data": null,
"shown": false,
},
"penDetected": false,
"penMode": false,
"pendingImageElement": null,
"previousSelectedElementIds": Object {},
@@ -11187,7 +11157,6 @@ Object {
"data": null,
"shown": false,
},
"penDetected": false,
"penMode": false,
"pendingImageElement": null,
"previousSelectedElementIds": Object {},
@@ -11388,7 +11357,6 @@ Object {
"data": null,
"shown": false,
},
"penDetected": false,
"penMode": false,
"pendingImageElement": null,
"previousSelectedElementIds": Object {},
@@ -11510,177 +11478,6 @@ exports[`regression tests key d selects diamond tool: [end of test] number of el
exports[`regression tests key d selects diamond tool: [end of test] number of renders 1`] = `8`;
exports[`regression tests key e selects ellipse tool: [end of test] appState 1`] = `
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,
"exportScale": 1,
"exportWithDarkMode": false,
"fileHandle": null,
"gridSize": null,
"height": 768,
"isBindingEnabled": true,
"isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
"isRotating": false,
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"offsetLeft": 0,
"offsetTop": 0,
"openMenu": null,
"openPopup": null,
"pasteDialog": Object {
"data": null,
"shown": false,
},
"penDetected": false,
"penMode": false,
"pendingImageElement": null,
"previousSelectedElementIds": Object {},
"resizingElement": null,
"scrollX": 0,
"scrollY": 0,
"scrolledOutside": false,
"selectedElementIds": Object {
"id0": true,
},
"selectedGroupIds": Object {},
"selectionElement": null,
"shouldCacheIgnoreZoom": false,
"showHelpDialog": false,
"showHyperlinkPopup": false,
"showStats": false,
"startBoundElement": null,
"suggestedBindings": Array [],
"theme": "light",
"toastMessage": null,
"viewBackgroundColor": "#ffffff",
"viewModeEnabled": false,
"width": 1024,
"zenModeEnabled": false,
"zoom": Object {
"value": 1,
},
}
`;
exports[`regression tests key e selects ellipse tool: [end of test] element 0 1`] = `
Object {
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"fillStyle": "hachure",
"groupIds": Array [],
"height": 10,
"id": "id0",
"isDeleted": false,
"link": null,
"opacity": 100,
"roughness": 1,
"seed": 337897,
"strokeColor": "#000000",
"strokeSharpness": "sharp",
"strokeStyle": "solid",
"strokeWidth": 1,
"type": "ellipse",
"updated": 1,
"version": 2,
"versionNonce": 1278240551,
"width": 10,
"x": 10,
"y": 10,
}
`;
exports[`regression tests key e selects ellipse tool: [end of test] history 1`] = `
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 [],
},
Object {
"appState": Object {
"editingGroupId": null,
"editingLinearElement": null,
"name": "Untitled-201933152653",
"selectedElementIds": Object {
"id0": true,
},
"selectedGroupIds": Object {},
"viewBackgroundColor": "#ffffff",
},
"elements": Array [
Object {
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"fillStyle": "hachure",
"groupIds": Array [],
"height": 10,
"id": "id0",
"isDeleted": false,
"link": null,
"opacity": 100,
"roughness": 1,
"seed": 337897,
"strokeColor": "#000000",
"strokeSharpness": "sharp",
"strokeStyle": "solid",
"strokeWidth": 1,
"type": "ellipse",
"updated": 1,
"version": 2,
"versionNonce": 1278240551,
"width": 10,
"x": 10,
"y": 10,
},
],
},
],
}
`;
exports[`regression tests key e selects ellipse tool: [end of test] number of elements 1`] = `1`;
exports[`regression tests key e selects ellipse tool: [end of test] number of renders 1`] = `8`;
exports[`regression tests key l selects line tool: [end of test] appState 1`] = `
Object {
"collaborators": Map {},
@@ -11730,7 +11527,6 @@ Object {
"data": null,
"shown": false,
},
"penDetected": false,
"penMode": false,
"pendingImageElement": null,
"previousSelectedElementIds": Object {},
@@ -11882,6 +11678,176 @@ exports[`regression tests key l selects line tool: [end of test] number of eleme
exports[`regression tests key l selects line tool: [end of test] number of renders 1`] = `8`;
exports[`regression tests key o selects ellipse tool: [end of test] appState 1`] = `
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,
"exportScale": 1,
"exportWithDarkMode": false,
"fileHandle": null,
"gridSize": null,
"height": 768,
"isBindingEnabled": true,
"isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
"isRotating": false,
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"offsetLeft": 0,
"offsetTop": 0,
"openMenu": null,
"openPopup": null,
"pasteDialog": Object {
"data": null,
"shown": false,
},
"penMode": false,
"pendingImageElement": null,
"previousSelectedElementIds": Object {},
"resizingElement": null,
"scrollX": 0,
"scrollY": 0,
"scrolledOutside": false,
"selectedElementIds": Object {
"id0": true,
},
"selectedGroupIds": Object {},
"selectionElement": null,
"shouldCacheIgnoreZoom": false,
"showHelpDialog": false,
"showHyperlinkPopup": false,
"showStats": false,
"startBoundElement": null,
"suggestedBindings": Array [],
"theme": "light",
"toastMessage": null,
"viewBackgroundColor": "#ffffff",
"viewModeEnabled": false,
"width": 1024,
"zenModeEnabled": false,
"zoom": Object {
"value": 1,
},
}
`;
exports[`regression tests key o selects ellipse tool: [end of test] element 0 1`] = `
Object {
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"fillStyle": "hachure",
"groupIds": Array [],
"height": 10,
"id": "id0",
"isDeleted": false,
"link": null,
"opacity": 100,
"roughness": 1,
"seed": 337897,
"strokeColor": "#000000",
"strokeSharpness": "sharp",
"strokeStyle": "solid",
"strokeWidth": 1,
"type": "ellipse",
"updated": 1,
"version": 2,
"versionNonce": 1278240551,
"width": 10,
"x": 10,
"y": 10,
}
`;
exports[`regression tests key o selects ellipse tool: [end of test] history 1`] = `
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 [],
},
Object {
"appState": Object {
"editingGroupId": null,
"editingLinearElement": null,
"name": "Untitled-201933152653",
"selectedElementIds": Object {
"id0": true,
},
"selectedGroupIds": Object {},
"viewBackgroundColor": "#ffffff",
},
"elements": Array [
Object {
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"fillStyle": "hachure",
"groupIds": Array [],
"height": 10,
"id": "id0",
"isDeleted": false,
"link": null,
"opacity": 100,
"roughness": 1,
"seed": 337897,
"strokeColor": "#000000",
"strokeSharpness": "sharp",
"strokeStyle": "solid",
"strokeWidth": 1,
"type": "ellipse",
"updated": 1,
"version": 2,
"versionNonce": 1278240551,
"width": 10,
"x": 10,
"y": 10,
},
],
},
],
}
`;
exports[`regression tests key o selects ellipse tool: [end of test] number of elements 1`] = `1`;
exports[`regression tests key o selects ellipse tool: [end of test] number of renders 1`] = `8`;
exports[`regression tests key r selects rectangle tool: [end of test] appState 1`] = `
Object {
"collaborators": Map {},
@@ -11931,7 +11897,6 @@ Object {
"data": null,
"shown": false,
},
"penDetected": false,
"penMode": false,
"pendingImageElement": null,
"previousSelectedElementIds": Object {},
@@ -12102,7 +12067,6 @@ Object {
"data": null,
"shown": false,
},
"penDetected": false,
"penMode": false,
"pendingImageElement": null,
"previousSelectedElementIds": Object {},
@@ -12321,7 +12285,6 @@ Object {
"data": null,
"shown": false,
},
"penDetected": false,
"penMode": false,
"pendingImageElement": null,
"previousSelectedElementIds": Object {
@@ -13090,7 +13053,6 @@ Object {
"data": null,
"shown": false,
},
"penDetected": false,
"penMode": false,
"pendingImageElement": null,
"previousSelectedElementIds": Object {
@@ -13357,7 +13319,6 @@ Object {
"data": null,
"shown": false,
},
"penDetected": false,
"penMode": false,
"pendingImageElement": null,
"previousSelectedElementIds": Object {},
@@ -13461,7 +13422,6 @@ Object {
"data": null,
"shown": false,
},
"penDetected": false,
"penMode": false,
"pendingImageElement": null,
"previousSelectedElementIds": Object {},
@@ -13563,7 +13523,6 @@ Object {
"data": null,
"shown": false,
},
"penDetected": false,
"penMode": false,
"pendingImageElement": null,
"previousSelectedElementIds": Object {
@@ -13737,7 +13696,6 @@ Object {
"data": null,
"shown": false,
},
"penDetected": false,
"penMode": false,
"pendingImageElement": null,
"previousSelectedElementIds": Object {
@@ -14077,7 +14035,6 @@ Object {
"data": null,
"shown": false,
},
"penDetected": false,
"penMode": false,
"pendingImageElement": null,
"previousSelectedElementIds": Object {},
@@ -14287,7 +14244,6 @@ Object {
"data": null,
"shown": false,
},
"penDetected": false,
"penMode": false,
"pendingImageElement": null,
"previousSelectedElementIds": Object {
@@ -15171,7 +15127,6 @@ Object {
"data": null,
"shown": false,
},
"penDetected": false,
"penMode": false,
"pendingImageElement": null,
"previousSelectedElementIds": Object {},
@@ -15273,7 +15228,6 @@ Object {
"data": null,
"shown": false,
},
"penDetected": false,
"penMode": false,
"pendingImageElement": null,
"previousSelectedElementIds": Object {
@@ -16086,7 +16040,6 @@ Object {
"data": null,
"shown": false,
},
"penDetected": false,
"penMode": false,
"pendingImageElement": null,
"previousSelectedElementIds": Object {
@@ -16518,7 +16471,6 @@ Object {
"data": null,
"shown": false,
},
"penDetected": false,
"penMode": false,
"pendingImageElement": null,
"previousSelectedElementIds": Object {
@@ -16807,7 +16759,6 @@ Object {
"data": null,
"shown": false,
},
"penDetected": false,
"penMode": false,
"pendingImageElement": null,
"previousSelectedElementIds": Object {},
@@ -16911,7 +16862,6 @@ Object {
"data": null,
"shown": false,
},
"penDetected": false,
"penMode": false,
"pendingImageElement": null,
"previousSelectedElementIds": Object {},
@@ -17439,7 +17389,6 @@ Object {
"data": null,
"shown": false,
},
"penDetected": false,
"penMode": false,
"pendingImageElement": null,
"previousSelectedElementIds": Object {},
@@ -17541,7 +17490,6 @@ Object {
"data": null,
"shown": false,
},
"penDetected": false,
"penMode": false,
"pendingImageElement": null,
"previousSelectedElementIds": Object {},
@@ -17592,4 +17540,4 @@ Object {
exports[`regression tests zoom hotkeys: [end of test] number of elements 1`] = `0`;
exports[`regression tests zoom hotkeys: [end of test] number of renders 1`] = `6`;
exports[`regression tests zoom hotkeys: [end of test] number of renders 1`] = `6`;
@@ -47,7 +47,6 @@ Object {
"data": null,
"shown": false,
},
"penDetected": false,
"penMode": false,
"pendingImageElement": null,
"previousSelectedElementIds": Object {},
+1 -1
View File
@@ -135,7 +135,7 @@ describe("regression tests", () => {
for (const [keys, shape, shouldSelect] of [
[`2${KEYS.R}`, "rectangle", true],
[`3${KEYS.D}`, "diamond", true],
[`4${KEYS.E}`, "ellipse", true],
[`4${KEYS.O}`, "ellipse", true],
[`5${KEYS.A}`, "arrow", true],
[`6${KEYS.L}`, "line", true],
[`7${KEYS.X}`, "freedraw", false],
+8 -2
View File
@@ -77,10 +77,9 @@ export type AppState = {
// (e.g. text element when typing into the input)
editingElement: NonDeletedExcalidrawElement | null;
editingLinearElement: LinearElementEditor | null;
elementType: typeof SHAPES[number]["value"];
elementType: typeof SHAPES[number]["value"] | "eraser";
elementLocked: boolean;
penMode: boolean;
penDetected: boolean;
exportBackground: boolean;
exportEmbedScene: boolean;
exportWithDarkMode: boolean;
@@ -384,6 +383,7 @@ export type PointerDownState = Readonly<{
boxSelection: {
hasOccurred: boolean;
};
elementIdsToErase: { [key: ExcalidrawElement["id"]]: boolean };
}>;
export type ExcalidrawImperativeAPI = {
@@ -407,3 +407,9 @@ export type ExcalidrawImperativeAPI = {
ready: true;
id: string;
};
export type DeviceType = {
isMobile: boolean;
isTouchScreen: boolean;
penDetected: boolean;
};
+81 -4
View File
@@ -1,13 +1,17 @@
import oc from "open-color";
import colors from "./colors";
import {
CURSOR_TYPE,
DEFAULT_VERSION,
EVENT,
FONT_FAMILY,
MIME_TYPES,
THEME,
WINDOWS_EMOJI_FALLBACK_FONT,
} from "./constants";
import { FontFamilyValues, FontString } from "./element/types";
import { Zoom } from "./types";
import { AppState, DataURL, Zoom } from "./types";
import { unstable_batchedUpdates } from "react-dom";
import { isDarwin } from "./keys";
@@ -215,18 +219,62 @@ export const setCursor = (canvas: HTMLCanvasElement | null, cursor: string) => {
}
};
let eraserCanvasCache: any;
let previewDataURL: string;
export const setEraserCursor = (
canvas: HTMLCanvasElement | null,
theme: AppState["theme"],
) => {
const cursorImageSizePx = 20;
const drawCanvas = () => {
const isDarkTheme = theme === THEME.DARK;
eraserCanvasCache = document.createElement("canvas");
eraserCanvasCache.theme = theme;
eraserCanvasCache.height = cursorImageSizePx;
eraserCanvasCache.width = cursorImageSizePx;
const context = eraserCanvasCache.getContext("2d")!;
context.lineWidth = 1;
context.beginPath();
context.arc(
eraserCanvasCache.width / 2,
eraserCanvasCache.height / 2,
5,
0,
2 * Math.PI,
);
context.fillStyle = isDarkTheme ? oc.black : oc.white;
context.fill();
context.strokeStyle = isDarkTheme ? oc.white : oc.black;
context.stroke();
previewDataURL = eraserCanvasCache.toDataURL(MIME_TYPES.svg) as DataURL;
};
if (!eraserCanvasCache || eraserCanvasCache.theme !== theme) {
drawCanvas();
}
setCursor(
canvas,
`url(${previewDataURL}) ${cursorImageSizePx / 2} ${
cursorImageSizePx / 2
}, auto`,
);
};
export const setCursorForShape = (
canvas: HTMLCanvasElement | null,
shape: string,
appState: AppState,
) => {
if (!canvas) {
return;
}
if (shape === "selection") {
if (appState.elementType === "selection") {
resetCursor(canvas);
} else if (appState.elementType === "eraser") {
setEraserCursor(canvas, appState.theme);
// do nothing if image tool is selected which suggests there's
// a image-preview set as the cursor
} else if (shape !== "image") {
} else if (appState.elementType !== "image") {
canvas.style.cursor = CURSOR_TYPE.CROSSHAIR;
}
};
@@ -535,3 +583,32 @@ export const wrapEvent = <T extends Event>(name: EVENT, nativeEvent: T) => {
cancelable: true,
});
};
export const updateObject = <T extends Record<string, any>>(
obj: T,
updates: Partial<T>,
): T => {
let didChange = false;
for (const key in updates) {
const value = (updates as any)[key];
if (typeof value !== "undefined") {
if (
(obj as any)[key] === value &&
// if object, always update because its attrs could have changed
(typeof value !== "object" || value === null)
) {
continue;
}
didChange = true;
}
}
if (!didChange) {
return obj;
}
return {
...obj,
...updates,
};
};