Address remaining comments.
This commit is contained in:
parent
e192538267
commit
27e2888347
@ -6,7 +6,6 @@ import {
|
||||
PanelComponentProps,
|
||||
ActionSource,
|
||||
ActionPredicateFn,
|
||||
isActionName,
|
||||
} from "./types";
|
||||
import { ExcalidrawElement } from "../element/types";
|
||||
import { AppClassProperties, AppState } from "../types";
|
||||
@ -76,31 +75,6 @@ export class ActionManager {
|
||||
}
|
||||
}
|
||||
|
||||
getCustomActions(opts?: {
|
||||
elements?: readonly ExcalidrawElement[];
|
||||
data?: Record<string, any>;
|
||||
guardsOnly?: boolean;
|
||||
}): Action[] {
|
||||
// For testing
|
||||
if (this === undefined) {
|
||||
return [];
|
||||
}
|
||||
const filter =
|
||||
opts !== undefined &&
|
||||
("elements" in opts || "data" in opts || "guardsOnly" in opts);
|
||||
const customActions: Action[] = [];
|
||||
for (const key in this.actions) {
|
||||
const action = this.actions[key];
|
||||
if (
|
||||
!isActionName(action.name) &&
|
||||
(!filter || this.isActionEnabled(action, opts))
|
||||
) {
|
||||
customActions.push(action);
|
||||
}
|
||||
}
|
||||
return customActions;
|
||||
}
|
||||
|
||||
registerAction(action: Action) {
|
||||
this.actions[action.name] = action;
|
||||
}
|
||||
@ -117,7 +91,7 @@ export class ActionManager {
|
||||
(action) =>
|
||||
(action.name in canvasActions
|
||||
? canvasActions[action.name as keyof typeof canvasActions]
|
||||
: this.isActionEnabled(action, { guardsOnly: true })) &&
|
||||
: this.isActionEnabled(action, { noPredicates: true })) &&
|
||||
action.keyTest &&
|
||||
action.keyTest(
|
||||
event,
|
||||
@ -172,7 +146,7 @@ export class ActionManager {
|
||||
"PanelComponent" in this.actions[name] &&
|
||||
(name in canvasActions
|
||||
? canvasActions[name as keyof typeof canvasActions]
|
||||
: this.isActionEnabled(this.actions[name], { guardsOnly: true }))
|
||||
: this.isActionEnabled(this.actions[name], { noPredicates: true }))
|
||||
) {
|
||||
const action = this.actions[name];
|
||||
const PanelComponent = action.PanelComponent!;
|
||||
@ -194,7 +168,6 @@ export class ActionManager {
|
||||
|
||||
return (
|
||||
<PanelComponent
|
||||
key={name}
|
||||
elements={this.getElementsIncludingDeleted()}
|
||||
appState={this.getAppState()}
|
||||
updateData={updateData}
|
||||
@ -211,8 +184,8 @@ export class ActionManager {
|
||||
action: Action,
|
||||
opts?: {
|
||||
elements?: readonly ExcalidrawElement[];
|
||||
noPredicates?: boolean;
|
||||
data?: Record<string, any>;
|
||||
guardsOnly?: boolean;
|
||||
},
|
||||
): boolean => {
|
||||
const elements = opts?.elements ?? this.getElementsIncludingDeleted();
|
||||
@ -220,7 +193,7 @@ export class ActionManager {
|
||||
const data = opts?.data;
|
||||
|
||||
if (
|
||||
!opts?.guardsOnly &&
|
||||
!opts?.noPredicates &&
|
||||
action.predicate &&
|
||||
!action.predicate(elements, appState, this.app.props, this.app, data)
|
||||
) {
|
||||
|
@ -80,23 +80,8 @@ const shortcutMap: Record<ShortcutName, string[]> = {
|
||||
toggleLock: [getShortcutKey("CtrlOrCmd+Shift+L")],
|
||||
};
|
||||
|
||||
export type CustomShortcutName = string;
|
||||
|
||||
let customShortcutMap: Record<CustomShortcutName, string[]> = {};
|
||||
|
||||
export const registerCustomShortcuts = (
|
||||
shortcuts: Record<CustomShortcutName, string[]>,
|
||||
) => {
|
||||
customShortcutMap = { ...customShortcutMap, ...shortcuts };
|
||||
};
|
||||
|
||||
export const getShortcutFromShortcutName = (
|
||||
name: ShortcutName | CustomShortcutName,
|
||||
) => {
|
||||
const shortcuts =
|
||||
name in customShortcutMap
|
||||
? customShortcutMap[name as CustomShortcutName]
|
||||
: shortcutMap[name as ShortcutName];
|
||||
export const getShortcutFromShortcutName = (name: ShortcutName) => {
|
||||
const shortcuts = shortcutMap[name];
|
||||
// if multiple shortcuts available, take the first one
|
||||
return shortcuts && shortcuts.length > 0 ? shortcuts[0] : "";
|
||||
};
|
||||
|
@ -43,92 +43,86 @@ export type ActionPredicateFn = (
|
||||
export type UpdaterFn = (res: ActionResult) => void;
|
||||
export type ActionFilterFn = (action: Action) => void;
|
||||
|
||||
const actionNames = [
|
||||
"copy",
|
||||
"cut",
|
||||
"paste",
|
||||
"copyAsPng",
|
||||
"copyAsSvg",
|
||||
"copyText",
|
||||
"sendBackward",
|
||||
"bringForward",
|
||||
"sendToBack",
|
||||
"bringToFront",
|
||||
"copyStyles",
|
||||
"selectAll",
|
||||
"pasteStyles",
|
||||
"gridMode",
|
||||
"zenMode",
|
||||
"stats",
|
||||
"changeStrokeColor",
|
||||
"changeBackgroundColor",
|
||||
"changeFillStyle",
|
||||
"changeStrokeWidth",
|
||||
"changeStrokeShape",
|
||||
"changeSloppiness",
|
||||
"changeStrokeStyle",
|
||||
"changeArrowhead",
|
||||
"changeOpacity",
|
||||
"changeFontSize",
|
||||
"toggleCanvasMenu",
|
||||
"toggleEditMenu",
|
||||
"undo",
|
||||
"redo",
|
||||
"finalize",
|
||||
"changeProjectName",
|
||||
"changeExportBackground",
|
||||
"changeExportEmbedScene",
|
||||
"changeExportScale",
|
||||
"saveToActiveFile",
|
||||
"saveFileToDisk",
|
||||
"loadScene",
|
||||
"duplicateSelection",
|
||||
"deleteSelectedElements",
|
||||
"changeViewBackgroundColor",
|
||||
"clearCanvas",
|
||||
"zoomIn",
|
||||
"zoomOut",
|
||||
"resetZoom",
|
||||
"zoomToFit",
|
||||
"zoomToSelection",
|
||||
"changeFontFamily",
|
||||
"changeTextAlign",
|
||||
"changeVerticalAlign",
|
||||
"toggleFullScreen",
|
||||
"toggleShortcuts",
|
||||
"group",
|
||||
"ungroup",
|
||||
"goToCollaborator",
|
||||
"addToLibrary",
|
||||
"changeRoundness",
|
||||
"alignTop",
|
||||
"alignBottom",
|
||||
"alignLeft",
|
||||
"alignRight",
|
||||
"alignVerticallyCentered",
|
||||
"alignHorizontallyCentered",
|
||||
"distributeHorizontally",
|
||||
"distributeVertically",
|
||||
"flipHorizontal",
|
||||
"flipVertical",
|
||||
"viewMode",
|
||||
"exportWithDarkMode",
|
||||
"toggleTheme",
|
||||
"increaseFontSize",
|
||||
"decreaseFontSize",
|
||||
"unbindText",
|
||||
"hyperlink",
|
||||
"bindText",
|
||||
"toggleLock",
|
||||
"toggleLinearEditor",
|
||||
"toggleEraserTool",
|
||||
"toggleHandTool",
|
||||
] as const;
|
||||
|
||||
// So we can have the `isActionName` type guard
|
||||
export type ActionName = typeof actionNames[number];
|
||||
export const isActionName = (n: any): n is ActionName =>
|
||||
actionNames.includes(n);
|
||||
export type ActionName =
|
||||
| "copy"
|
||||
| "cut"
|
||||
| "paste"
|
||||
| "copyAsPng"
|
||||
| "copyAsSvg"
|
||||
| "copyText"
|
||||
| "sendBackward"
|
||||
| "bringForward"
|
||||
| "sendToBack"
|
||||
| "bringToFront"
|
||||
| "copyStyles"
|
||||
| "selectAll"
|
||||
| "pasteStyles"
|
||||
| "gridMode"
|
||||
| "zenMode"
|
||||
| "stats"
|
||||
| "changeStrokeColor"
|
||||
| "changeBackgroundColor"
|
||||
| "changeFillStyle"
|
||||
| "changeStrokeWidth"
|
||||
| "changeStrokeShape"
|
||||
| "changeSloppiness"
|
||||
| "changeStrokeStyle"
|
||||
| "changeArrowhead"
|
||||
| "changeOpacity"
|
||||
| "changeFontSize"
|
||||
| "toggleCanvasMenu"
|
||||
| "toggleEditMenu"
|
||||
| "undo"
|
||||
| "redo"
|
||||
| "finalize"
|
||||
| "changeProjectName"
|
||||
| "changeExportBackground"
|
||||
| "changeExportEmbedScene"
|
||||
| "changeExportScale"
|
||||
| "saveToActiveFile"
|
||||
| "saveFileToDisk"
|
||||
| "loadScene"
|
||||
| "duplicateSelection"
|
||||
| "deleteSelectedElements"
|
||||
| "changeViewBackgroundColor"
|
||||
| "clearCanvas"
|
||||
| "zoomIn"
|
||||
| "zoomOut"
|
||||
| "resetZoom"
|
||||
| "zoomToFit"
|
||||
| "zoomToSelection"
|
||||
| "changeFontFamily"
|
||||
| "changeTextAlign"
|
||||
| "changeVerticalAlign"
|
||||
| "toggleFullScreen"
|
||||
| "toggleShortcuts"
|
||||
| "group"
|
||||
| "ungroup"
|
||||
| "goToCollaborator"
|
||||
| "addToLibrary"
|
||||
| "changeRoundness"
|
||||
| "alignTop"
|
||||
| "alignBottom"
|
||||
| "alignLeft"
|
||||
| "alignRight"
|
||||
| "alignVerticallyCentered"
|
||||
| "alignHorizontallyCentered"
|
||||
| "distributeHorizontally"
|
||||
| "distributeVertically"
|
||||
| "flipHorizontal"
|
||||
| "flipVertical"
|
||||
| "viewMode"
|
||||
| "exportWithDarkMode"
|
||||
| "toggleTheme"
|
||||
| "increaseFontSize"
|
||||
| "decreaseFontSize"
|
||||
| "unbindText"
|
||||
| "hyperlink"
|
||||
| "bindText"
|
||||
| "toggleLock"
|
||||
| "toggleLinearEditor"
|
||||
| "toggleEraserTool"
|
||||
| "toggleHandTool";
|
||||
|
||||
export type PanelComponentProps = {
|
||||
elements: readonly ExcalidrawElement[];
|
||||
|
@ -3,7 +3,7 @@ import { ActionManager } from "../actions/manager";
|
||||
import { getNonDeletedElements } from "../element";
|
||||
import { ExcalidrawElement, PointerType } from "../element/types";
|
||||
import { t } from "../i18n";
|
||||
import { useDevice, useExcalidrawActionManager } from "../components/App";
|
||||
import { useDevice } from "../components/App";
|
||||
import {
|
||||
canChangeRoundness,
|
||||
canHaveArrowheads,
|
||||
@ -92,9 +92,6 @@ export const SelectedShapeActions = ({
|
||||
{showChangeBackgroundIcons && (
|
||||
<div>{renderAction("changeBackgroundColor")}</div>
|
||||
)}
|
||||
{useExcalidrawActionManager()
|
||||
.getCustomActions({ elements: targetElements })
|
||||
.map((action) => renderAction(action.name))}
|
||||
{showFillIcons && renderAction("changeFillStyle")}
|
||||
|
||||
{(hasStrokeWidth(appState.activeTool.type) ||
|
||||
|
@ -6163,29 +6163,6 @@ class App extends React.Component<AppProps, AppState> {
|
||||
};
|
||||
|
||||
private getContextMenuItems = (
|
||||
type: "canvas" | "element" | "custom",
|
||||
source?: string,
|
||||
): ContextMenuItems => {
|
||||
const custom: ContextMenuItems = [];
|
||||
this.actionManager
|
||||
.getCustomActions({ data: { source: source ?? "" } })
|
||||
.forEach((action) => custom.push(action));
|
||||
if (type === "custom") {
|
||||
return custom;
|
||||
}
|
||||
if (custom.length > 0) {
|
||||
custom.push(CONTEXT_MENU_SEPARATOR);
|
||||
}
|
||||
const standard: ContextMenuItems = this._getContextMenuItems(type).filter(
|
||||
(item) =>
|
||||
!item ||
|
||||
item === CONTEXT_MENU_SEPARATOR ||
|
||||
this.actionManager.isActionEnabled(item, { guardsOnly: true }),
|
||||
);
|
||||
return [...custom, ...standard];
|
||||
};
|
||||
|
||||
private _getContextMenuItems = (
|
||||
type: "canvas" | "element",
|
||||
): ContextMenuItems => {
|
||||
const options: ContextMenuItems = [];
|
||||
|
@ -5,7 +5,6 @@ import { t } from "../i18n";
|
||||
import "./ContextMenu.scss";
|
||||
import {
|
||||
getShortcutFromShortcutName,
|
||||
CustomShortcutName,
|
||||
ShortcutName,
|
||||
} from "../actions/shortcuts";
|
||||
import { Action } from "../actions/types";
|
||||
@ -111,9 +110,7 @@ export const ContextMenu = React.memo(
|
||||
<div className="context-menu-item__label">{label}</div>
|
||||
<kbd className="context-menu-item__shortcut">
|
||||
{actionName
|
||||
? getShortcutFromShortcutName(
|
||||
actionName as ShortcutName | CustomShortcutName,
|
||||
)
|
||||
? getShortcutFromShortcutName(actionName as ShortcutName)
|
||||
: ""}
|
||||
</kbd>
|
||||
</button>
|
||||
|
@ -30,22 +30,6 @@ import { NonDeletedExcalidrawElement } from "../../../element/types";
|
||||
import { ImportedLibraryData } from "../../../data/types";
|
||||
import CustomFooter from "./CustomFooter";
|
||||
import MobileFooter from "./MobileFooter";
|
||||
import { Action, ActionPredicateFn } from "../../../actions/types";
|
||||
import { ContextMenuItems } from "../../../components/ContextMenu";
|
||||
|
||||
const exampleAction: Action = {
|
||||
name: "example",
|
||||
trackEvent: false,
|
||||
perform: (elements, appState) => {
|
||||
return { elements, appState, commitToHistory: false };
|
||||
},
|
||||
predicate: (elements, appState, appProps, app, data) =>
|
||||
data === undefined || data.source === "custom",
|
||||
contextItemLabel: "labels.untitled",
|
||||
};
|
||||
const examplePredicateFn: ActionPredicateFn = (action, elements) =>
|
||||
action.name !== "example" ||
|
||||
!elements.some((el) => el.type === "text" && !el.isDeleted);
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
@ -128,8 +112,6 @@ export default function App() {
|
||||
if (!excalidrawAPI) {
|
||||
return;
|
||||
}
|
||||
excalidrawAPI.actionManager.registerAction(exampleAction);
|
||||
excalidrawAPI.actionManager.registerActionPredicate(examplePredicateFn);
|
||||
const fetchData = async () => {
|
||||
const res = await fetch("/rocket.jpeg");
|
||||
const imageData = await res.blob();
|
||||
@ -166,29 +148,6 @@ export default function App() {
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<button
|
||||
onContextMenu={(event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
event.preventDefault();
|
||||
const offsetLeft = excalidrawAPI?.getAppState().offsetLeft ?? 0;
|
||||
const offsetTop = excalidrawAPI?.getAppState().offsetTop ?? 0;
|
||||
const left = event.clientX - offsetLeft;
|
||||
const top = event.clientY - offsetTop;
|
||||
|
||||
const items: ContextMenuItems = [];
|
||||
excalidrawAPI?.actionManager
|
||||
.getCustomActions({ data: { source: "custom" } })
|
||||
.forEach((action) => items.push(action));
|
||||
items.length > 0 &&
|
||||
excalidrawAPI?.updateScene({
|
||||
appState: {
|
||||
contextMenu: { top, left, items },
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
{" "}
|
||||
Context Menu{" "}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => alert("This is dummy top right UI")}
|
||||
style={{ height: "2.5rem" }}
|
||||
|
@ -1,13 +1,7 @@
|
||||
import { ExcalidrawElement } from "../element/types";
|
||||
import { getShortcutKey } from "../utils";
|
||||
import { API } from "./helpers/api";
|
||||
import { render } from "./test-utils";
|
||||
import ExcalidrawApp from "../excalidraw-app";
|
||||
import {
|
||||
CustomShortcutName,
|
||||
getShortcutFromShortcutName,
|
||||
registerCustomShortcuts,
|
||||
} from "../actions/shortcuts";
|
||||
import { Action, ActionPredicateFn, ActionResult } from "../actions/types";
|
||||
import {
|
||||
actionChangeFontFamily,
|
||||
@ -18,14 +12,6 @@ import { isTextElement } from "../element";
|
||||
const { h } = window;
|
||||
|
||||
describe("regression tests", () => {
|
||||
it("should retrieve custom shortcuts", () => {
|
||||
const shortcuts: Record<CustomShortcutName, string[]> = {
|
||||
test: [getShortcutKey("CtrlOrCmd+1"), getShortcutKey("CtrlOrCmd+2")],
|
||||
};
|
||||
registerCustomShortcuts(shortcuts);
|
||||
expect(getShortcutFromShortcutName("test")).toBe("Ctrl+1");
|
||||
});
|
||||
|
||||
it("should apply universal action predicates", async () => {
|
||||
await render(<ExcalidrawApp />);
|
||||
// Create the test elements
|
||||
|
Loading…
x
Reference in New Issue
Block a user