mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
Feature: Action System (#298)
* Add Action System - Add keyboard test - Add context menu label - Add PanelComponent * Show context menu items based on actions * Add render action feature - Replace bringForward etc buttons with action manager render functions * Move all property changes and canvas into actions * Remove unnecessary functions and add forgotten force update when elements array change * Extract export operations into actions * Add elements and app state as arguments to `keyTest` function * Add key priorities - Sort actions by key priority when handling key presses * Extract copy/paste styles * Add Context Menu Item order - Sort context menu items based on menu item order parameter * Remove unnecessary functions from App component
This commit is contained in:
parent
c253c0b635
commit
f465121f9b
15 changed files with 967 additions and 430 deletions
244
src/index.tsx
244
src/index.tsx
|
@ -4,19 +4,16 @@ import ReactDOM from "react-dom";
|
|||
import rough from "roughjs/bin/wrappers/rough";
|
||||
import { RoughCanvas } from "roughjs/bin/canvas";
|
||||
|
||||
import { moveOneLeft, moveAllLeft, moveOneRight, moveAllRight } from "./zindex";
|
||||
import {
|
||||
newElement,
|
||||
duplicateElement,
|
||||
resizeTest,
|
||||
isTextElement,
|
||||
textWysiwyg,
|
||||
getElementAbsoluteCoords,
|
||||
redrawTextBoundingBox
|
||||
getElementAbsoluteCoords
|
||||
} from "./element";
|
||||
import {
|
||||
clearSelection,
|
||||
getSelectedIndices,
|
||||
deleteSelectedElements,
|
||||
setSelection,
|
||||
isOverScrollBars,
|
||||
|
@ -41,7 +38,33 @@ import ContextMenu from "./components/ContextMenu";
|
|||
|
||||
import "./styles.scss";
|
||||
import { getElementWithResizeHandler } from "./element/resizeTest";
|
||||
import {
|
||||
ActionManager,
|
||||
actionDeleteSelected,
|
||||
actionSendBackward,
|
||||
actionBringForward,
|
||||
actionSendToBack,
|
||||
actionBringToFront,
|
||||
actionSelectAll,
|
||||
actionChangeStrokeColor,
|
||||
actionChangeBackgroundColor,
|
||||
actionChangeOpacity,
|
||||
actionChangeStrokeWidth,
|
||||
actionChangeFillStyle,
|
||||
actionChangeSloppiness,
|
||||
actionChangeFontSize,
|
||||
actionChangeFontFamily,
|
||||
actionChangeViewBackgroundColor,
|
||||
actionClearCanvas,
|
||||
actionChangeProjectName,
|
||||
actionChangeExportBackground,
|
||||
actionLoadScene,
|
||||
actionSaveScene,
|
||||
actionCopyStyles,
|
||||
actionPasteStyles
|
||||
} from "./actions";
|
||||
import { SidePanel } from "./components/SidePanel";
|
||||
import { ActionResult } from "./actions/types";
|
||||
|
||||
let { elements } = createScene();
|
||||
const { history } = createHistory();
|
||||
|
@ -50,8 +73,6 @@ const DEFAULT_PROJECT_NAME = `excalidraw-${getDateTime()}`;
|
|||
const CANVAS_WINDOW_OFFSET_LEFT = 250;
|
||||
const CANVAS_WINDOW_OFFSET_TOP = 0;
|
||||
|
||||
let copiedStyles: string = "{}";
|
||||
|
||||
function resetCursor() {
|
||||
document.documentElement.style.cursor = "";
|
||||
}
|
||||
|
@ -101,6 +122,48 @@ export class App extends React.Component<{}, AppState> {
|
|||
canvas: HTMLCanvasElement | null = null;
|
||||
rc: RoughCanvas | null = null;
|
||||
|
||||
actionManager: ActionManager = new ActionManager();
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
this.actionManager.registerAction(actionDeleteSelected);
|
||||
this.actionManager.registerAction(actionSendToBack);
|
||||
this.actionManager.registerAction(actionBringToFront);
|
||||
this.actionManager.registerAction(actionSendBackward);
|
||||
this.actionManager.registerAction(actionBringForward);
|
||||
this.actionManager.registerAction(actionSelectAll);
|
||||
|
||||
this.actionManager.registerAction(actionChangeStrokeColor);
|
||||
this.actionManager.registerAction(actionChangeBackgroundColor);
|
||||
this.actionManager.registerAction(actionChangeFillStyle);
|
||||
this.actionManager.registerAction(actionChangeStrokeWidth);
|
||||
this.actionManager.registerAction(actionChangeOpacity);
|
||||
this.actionManager.registerAction(actionChangeSloppiness);
|
||||
this.actionManager.registerAction(actionChangeFontSize);
|
||||
this.actionManager.registerAction(actionChangeFontFamily);
|
||||
|
||||
this.actionManager.registerAction(actionChangeViewBackgroundColor);
|
||||
this.actionManager.registerAction(actionClearCanvas);
|
||||
|
||||
this.actionManager.registerAction(actionChangeProjectName);
|
||||
this.actionManager.registerAction(actionChangeExportBackground);
|
||||
this.actionManager.registerAction(actionSaveScene);
|
||||
this.actionManager.registerAction(actionLoadScene);
|
||||
|
||||
this.actionManager.registerAction(actionCopyStyles);
|
||||
this.actionManager.registerAction(actionPasteStyles);
|
||||
}
|
||||
|
||||
private syncActionResult = (res: ActionResult) => {
|
||||
if (res.elements !== undefined) {
|
||||
elements = res.elements;
|
||||
this.forceUpdate();
|
||||
}
|
||||
|
||||
if (res.appState !== undefined) {
|
||||
this.setState({ ...res.appState });
|
||||
}
|
||||
};
|
||||
|
||||
public componentDidMount() {
|
||||
document.addEventListener("keydown", this.onKeyDown, false);
|
||||
document.addEventListener("mousemove", this.getCurrentCursorPosition);
|
||||
|
@ -166,10 +229,14 @@ export class App extends React.Component<{}, AppState> {
|
|||
}
|
||||
if (isInputLike(event.target)) return;
|
||||
|
||||
if (event.key === KEYS.BACKSPACE || event.key === KEYS.DELETE) {
|
||||
this.deleteSelectedElements();
|
||||
event.preventDefault();
|
||||
} else if (isArrowKey(event.key)) {
|
||||
const data = this.actionManager.handleKeyDown(event, elements, this.state);
|
||||
this.syncActionResult(data);
|
||||
|
||||
if (data.elements !== undefined && data.appState !== undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isArrowKey(event.key)) {
|
||||
const step = event.shiftKey
|
||||
? ELEMENT_SHIFT_TRANSLATE_AMOUNT
|
||||
: ELEMENT_TRANSLATE_AMOUNT;
|
||||
|
@ -186,46 +253,6 @@ export class App extends React.Component<{}, AppState> {
|
|||
});
|
||||
this.forceUpdate();
|
||||
event.preventDefault();
|
||||
|
||||
// Send backward: Cmd-Shift-Alt-B
|
||||
} else if (
|
||||
event[META_KEY] &&
|
||||
event.shiftKey &&
|
||||
event.altKey &&
|
||||
event.code === "KeyB"
|
||||
) {
|
||||
this.moveOneLeft();
|
||||
event.preventDefault();
|
||||
|
||||
// Send to back: Cmd-Shift-B
|
||||
} else if (event[META_KEY] && event.shiftKey && event.code === "KeyB") {
|
||||
this.moveAllLeft();
|
||||
event.preventDefault();
|
||||
|
||||
// Bring forward: Cmd-Shift-Alt-F
|
||||
} else if (
|
||||
event[META_KEY] &&
|
||||
event.shiftKey &&
|
||||
event.altKey &&
|
||||
event.code === "KeyF"
|
||||
) {
|
||||
this.moveOneRight();
|
||||
event.preventDefault();
|
||||
|
||||
// Bring to front: Cmd-Shift-F
|
||||
} else if (event[META_KEY] && event.shiftKey && event.code === "KeyF") {
|
||||
this.moveAllRight();
|
||||
event.preventDefault();
|
||||
// Select all: Cmd-A
|
||||
} else if (event[META_KEY] && event.code === "KeyA") {
|
||||
let newElements = [...elements];
|
||||
newElements.forEach(element => {
|
||||
element.isSelected = true;
|
||||
});
|
||||
|
||||
elements = newElements;
|
||||
this.forceUpdate();
|
||||
event.preventDefault();
|
||||
} else if (shapesShortcutKeys.includes(event.key.toLowerCase())) {
|
||||
this.setState({ elementType: findShapeByKey(event.key) });
|
||||
} else if (event[META_KEY] && event.code === "KeyZ") {
|
||||
|
@ -244,99 +271,11 @@ export class App extends React.Component<{}, AppState> {
|
|||
}
|
||||
this.forceUpdate();
|
||||
event.preventDefault();
|
||||
// Copy Styles: Cmd-Shift-C
|
||||
} else if (event.metaKey && event.shiftKey && event.code === "KeyC") {
|
||||
this.copyStyles();
|
||||
// Paste Styles: Cmd-Shift-V
|
||||
} else if (event.metaKey && event.shiftKey && event.code === "KeyV") {
|
||||
this.pasteStyles();
|
||||
event.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
private deleteSelectedElements = () => {
|
||||
elements = deleteSelectedElements(elements);
|
||||
this.forceUpdate();
|
||||
};
|
||||
|
||||
private clearCanvas = () => {
|
||||
if (window.confirm("This will clear the whole canvas. Are you sure?")) {
|
||||
elements = [];
|
||||
this.setState({
|
||||
viewBackgroundColor: "#ffffff",
|
||||
scrollX: 0,
|
||||
scrollY: 0
|
||||
});
|
||||
this.forceUpdate();
|
||||
}
|
||||
};
|
||||
|
||||
private copyStyles = () => {
|
||||
const element = elements.find(el => el.isSelected);
|
||||
if (element) {
|
||||
copiedStyles = JSON.stringify(element);
|
||||
}
|
||||
};
|
||||
|
||||
private pasteStyles = () => {
|
||||
const pastedElement = JSON.parse(copiedStyles);
|
||||
elements = elements.map(element => {
|
||||
if (element.isSelected) {
|
||||
const newElement = {
|
||||
...element,
|
||||
backgroundColor: pastedElement?.backgroundColor,
|
||||
strokeWidth: pastedElement?.strokeWidth,
|
||||
strokeColor: pastedElement?.strokeColor,
|
||||
fillStyle: pastedElement?.fillStyle,
|
||||
opacity: pastedElement?.opacity,
|
||||
roughness: pastedElement?.roughness
|
||||
};
|
||||
if (isTextElement(newElement)) {
|
||||
newElement.font = pastedElement?.font;
|
||||
redrawTextBoundingBox(newElement);
|
||||
}
|
||||
return newElement;
|
||||
}
|
||||
return element;
|
||||
});
|
||||
this.forceUpdate();
|
||||
};
|
||||
|
||||
private moveAllLeft = () => {
|
||||
elements = moveAllLeft([...elements], getSelectedIndices(elements));
|
||||
this.forceUpdate();
|
||||
};
|
||||
|
||||
private moveOneLeft = () => {
|
||||
elements = moveOneLeft([...elements], getSelectedIndices(elements));
|
||||
this.forceUpdate();
|
||||
};
|
||||
|
||||
private moveAllRight = () => {
|
||||
elements = moveAllRight([...elements], getSelectedIndices(elements));
|
||||
this.forceUpdate();
|
||||
};
|
||||
|
||||
private moveOneRight = () => {
|
||||
elements = moveOneRight([...elements], getSelectedIndices(elements));
|
||||
this.forceUpdate();
|
||||
};
|
||||
|
||||
private removeWheelEventListener: (() => void) | undefined;
|
||||
|
||||
private changeProperty = (
|
||||
callback: (element: ExcalidrawElement) => ExcalidrawElement
|
||||
) => {
|
||||
elements = elements.map(element => {
|
||||
if (element.isSelected) {
|
||||
return callback(element);
|
||||
}
|
||||
return element;
|
||||
});
|
||||
|
||||
this.forceUpdate();
|
||||
};
|
||||
|
||||
private copyToClipboard = () => {
|
||||
if (navigator.clipboard) {
|
||||
const text = JSON.stringify(
|
||||
|
@ -384,6 +323,9 @@ export class App extends React.Component<{}, AppState> {
|
|||
}}
|
||||
>
|
||||
<SidePanel
|
||||
actionManager={this.actionManager}
|
||||
syncActionResult={this.syncActionResult}
|
||||
appState={{ ...this.state }}
|
||||
elements={elements}
|
||||
onToolChange={value => {
|
||||
this.setState({ elementType: value });
|
||||
|
@ -392,20 +334,6 @@ export class App extends React.Component<{}, AppState> {
|
|||
value === "text" ? "text" : "crosshair";
|
||||
this.forceUpdate();
|
||||
}}
|
||||
moveAllLeft={this.moveAllLeft}
|
||||
moveAllRight={this.moveAllRight}
|
||||
moveOneLeft={this.moveOneLeft}
|
||||
moveOneRight={this.moveOneRight}
|
||||
onClearCanvas={this.clearCanvas}
|
||||
changeProperty={this.changeProperty}
|
||||
onUpdateAppState={(name, value) => {
|
||||
this.setState({ [name]: value } as any);
|
||||
}}
|
||||
onUpdateElements={newElements => {
|
||||
elements = newElements;
|
||||
this.forceUpdate();
|
||||
}}
|
||||
appState={{ ...this.state }}
|
||||
canvas={this.canvas!}
|
||||
/>
|
||||
<canvas
|
||||
|
@ -482,13 +410,11 @@ export class App extends React.Component<{}, AppState> {
|
|||
label: "Paste",
|
||||
action: () => this.pasteFromClipboard()
|
||||
},
|
||||
{ label: "Copy Styles", action: this.copyStyles },
|
||||
{ label: "Paste Styles", action: this.pasteStyles },
|
||||
{ label: "Delete", action: this.deleteSelectedElements },
|
||||
{ label: "Move Forward", action: this.moveOneRight },
|
||||
{ label: "Send to Front", action: this.moveAllRight },
|
||||
{ label: "Move Backwards", action: this.moveOneLeft },
|
||||
{ label: "Send to Back", action: this.moveAllLeft }
|
||||
...this.actionManager.getContextMenuItems(
|
||||
elements,
|
||||
this.state,
|
||||
this.syncActionResult
|
||||
)
|
||||
],
|
||||
top: e.clientY,
|
||||
left: e.clientX
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue