feat: Custom actions and shortcuts

This commit is contained in:
Daniel J. Geiger
2023-01-06 13:34:39 -06:00
parent 0f11f7da15
commit 8e5d376b49
14 changed files with 452 additions and 98 deletions
+16
View File
@@ -36,10 +36,12 @@ export const SelectedShapeActions = ({
appState,
elements,
renderAction,
getCustomActions,
}: {
appState: AppState;
elements: readonly ExcalidrawElement[];
renderAction: ActionManager["renderAction"];
getCustomActions: ActionManager["getCustomActions"];
}) => {
const targetElements = getTargetElements(
getNonDeletedElements(elements),
@@ -92,6 +94,15 @@ export const SelectedShapeActions = ({
{showChangeBackgroundIcons && (
<div>{renderAction("changeBackgroundColor")}</div>
)}
{getCustomActions().map((action) => {
if (
action.panelComponentPredicate &&
action.panelComponentPredicate(targetElements, appState)
) {
return renderAction(action.name);
}
return null;
})}
{showFillIcons && renderAction("changeFillStyle")}
{(hasStrokeWidth(appState.activeTool.type) ||
@@ -209,12 +220,14 @@ export const ShapesSwitcher = ({
setAppState,
onImageAction,
appState,
onContextMenu,
}: {
canvas: HTMLCanvasElement | null;
activeTool: AppState["activeTool"];
setAppState: React.Component<any, AppState>["setState"];
onImageAction: (data: { pointerType: PointerType | null }) => void;
appState: AppState;
onContextMenu?: (event: React.MouseEvent, source: string) => void;
}) => (
<>
{SHAPES.map(({ value, icon, key, numericKey, fillable }, index) => {
@@ -264,6 +277,9 @@ export const ShapesSwitcher = ({
onImageAction({ pointerType });
}
}}
onContextMenu={(event, source) => {
onContextMenu && onContextMenu(event, source);
}}
/>
);
})}
+64 -9
View File
@@ -38,7 +38,7 @@ import {
} from "../actions";
import { createRedoAction, createUndoAction } from "../actions/actionHistory";
import { ActionManager } from "../actions/manager";
import { actions } from "../actions/register";
import { getActions } from "../actions/register";
import { ActionResult } from "../actions/types";
import { trackEvent } from "../analytics";
import { getDefaultAppState, isEraserActive } from "../appState";
@@ -418,6 +418,12 @@ class App extends React.Component<AppProps, AppState> {
this.id = nanoid();
this.library = new Library(this);
this.actionManager = new ActionManager(
this.syncActionResult,
() => this.state,
() => this.scene.getElementsIncludingDeleted(),
this,
);
if (excalidrawRef) {
const readyPromise =
("current" in excalidrawRef && excalidrawRef.current?.readyPromise) ||
@@ -438,6 +444,7 @@ class App extends React.Component<AppProps, AppState> {
getSceneElements: this.getSceneElements,
getAppState: () => this.state,
getFiles: () => this.files,
actionManager: this.actionManager,
refresh: this.refresh,
setToast: this.setToast,
id: this.id,
@@ -465,13 +472,8 @@ class App extends React.Component<AppProps, AppState> {
onSceneUpdated: this.onSceneUpdated,
});
this.history = new History();
this.actionManager = new ActionManager(
this.syncActionResult,
() => this.state,
() => this.scene.getElementsIncludingDeleted(),
this,
);
this.actionManager.registerAll(actions);
this.actionManager.registerAll(getActions());
this.actionManager.registerActionGuards();
this.actionManager.registerAction(createUndoAction(this.history));
this.actionManager.registerAction(createRedoAction(this.history));
@@ -587,6 +589,7 @@ class App extends React.Component<AppProps, AppState> {
renderTopRightUI={renderTopRightUI}
renderCustomStats={renderCustomStats}
renderCustomSidebar={this.props.renderSidebar}
onContextMenu={this.handleCustomContextMenu}
showExitZenModeBtn={
typeof this.props?.zenModeEnabled === "undefined" &&
this.state.zenModeEnabled
@@ -5968,6 +5971,28 @@ class App extends React.Component<AppProps, AppState> {
}
};
private handleCustomContextMenu = (
event: React.MouseEvent,
source: string,
) => {
event.preventDefault();
const container = this.excalidrawContainerRef.current!;
const { top: offsetTop, left: offsetLeft } =
container.getBoundingClientRect();
const left = event.clientX - offsetLeft;
const top = event.clientY - offsetTop;
this.setState({}, () => {
this.setState({
contextMenu: {
top,
left,
items: this.getContextMenuItems("custom", source),
},
});
});
};
private handleCanvasContextMenu = (
event: React.PointerEvent<HTMLCanvasElement>,
) => {
@@ -6139,9 +6164,39 @@ class App extends React.Component<AppProps, AppState> {
};
private getContextMenuItems = (
type: "canvas" | "element",
type: "canvas" | "element" | "custom",
source?: string,
): ContextMenuItems => {
const options: ContextMenuItems = [];
const allElements = this.actionManager.getElementsIncludingDeleted();
const appState = this.actionManager.getAppState();
let addedCustom = false;
this.actionManager.getCustomActions().forEach((action) => {
const predicate =
type === "custom"
? action.customPredicate
: action.contextItemPredicate;
if (
predicate &&
predicate(
allElements,
appState,
this.actionManager.app.props,
this.actionManager.app,
type === "custom" ? { source } : undefined,
) &&
this.actionManager.isActionEnabled(allElements, appState, action.name)
) {
addedCustom = true;
options.push(action);
}
});
if (type === "custom") {
return options;
}
if (addedCustom) {
options.push(CONTEXT_MENU_SEPARATOR);
}
options.push(actionCopyAsPng, actionCopyAsSvg);
+4 -1
View File
@@ -5,6 +5,7 @@ import { t } from "../i18n";
import "./ContextMenu.scss";
import {
getShortcutFromShortcutName,
CustomShortcutName,
ShortcutName,
} from "../actions/shortcuts";
import { Action } from "../actions/types";
@@ -110,7 +111,9 @@ export const ContextMenu = React.memo(
<div className="context-menu-item__label">{label}</div>
<kbd className="context-menu-item__shortcut">
{actionName
? getShortcutFromShortcutName(actionName as ShortcutName)
? getShortcutFromShortcutName(
actionName as ShortcutName | CustomShortcutName,
)
: ""}
</kbd>
</button>
+5
View File
@@ -78,6 +78,7 @@ interface LayerUIProps {
onImageAction: (data: { insertOnCanvasDirectly: boolean }) => void;
renderWelcomeScreen: boolean;
children?: React.ReactNode;
onContextMenu?: (event: React.MouseEvent, source: string) => void;
}
const LayerUI = ({
@@ -104,6 +105,7 @@ const LayerUI = ({
onImageAction,
renderWelcomeScreen,
children,
onContextMenu,
}: LayerUIProps) => {
const device = useDevice();
@@ -240,6 +242,7 @@ const LayerUI = ({
appState={appState}
elements={elements}
renderAction={actionManager.renderAction}
getCustomActions={actionManager.getCustomActions}
/>
</Island>
</Section>
@@ -327,6 +330,7 @@ const LayerUI = ({
insertOnCanvasDirectly: pointerType !== "mouse",
});
}}
onContextMenu={onContextMenu}
/>
{/* {actionManager.renderAction("eraser", {
// size: "small",
@@ -433,6 +437,7 @@ const LayerUI = ({
renderSidebars={renderSidebars}
device={device}
renderMenu={renderMenu}
onContextMenu={onContextMenu}
/>
)}
+4
View File
@@ -42,6 +42,7 @@ type MobileMenuProps = {
device: Device;
renderWelcomeScreen?: boolean;
renderMenu: () => React.ReactNode;
onContextMenu?: (event: React.MouseEvent, source: string) => void;
};
export const MobileMenu = ({
@@ -60,6 +61,7 @@ export const MobileMenu = ({
device,
renderWelcomeScreen,
renderMenu,
onContextMenu,
}: MobileMenuProps) => {
const renderToolbar = () => {
return (
@@ -98,6 +100,7 @@ export const MobileMenu = ({
insertOnCanvasDirectly: pointerType !== "mouse",
});
}}
onContextMenu={onContextMenu}
/>
</Stack.Row>
</Island>
@@ -190,6 +193,7 @@ export const MobileMenu = ({
appState={appState}
elements={elements}
renderAction={actionManager.renderAction}
getCustomActions={actionManager.getCustomActions}
/>
</Section>
) : null}
+6
View File
@@ -26,6 +26,7 @@ type ToolButtonBaseProps = {
selected?: boolean;
className?: string;
isLoading?: boolean;
onContextMenu?(event: React.MouseEvent, source: string): void;
};
type ToolButtonProps =
@@ -157,6 +158,11 @@ export const ToolButton = React.forwardRef((props: ToolButtonProps, ref) => {
lastPointerTypeRef.current = null;
});
}}
onContextMenu={(event) => {
if (props.onContextMenu !== undefined) {
props.onContextMenu(event, props.name ?? "");
}
}}
>
<input
className={`ToolIcon_type_radio ${sizeCn}`}