feat: Custom actions and shortcuts
This commit is contained in:
@@ -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
@@ -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);
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}`}
|
||||
|
||||
Reference in New Issue
Block a user