aboutsummaryrefslogtreecommitdiffstats
path: root/packages/excalidraw/actions/manager.tsx
diff options
context:
space:
mode:
authorkj_sh6042026-03-15 16:19:35 -0400
committerkj_sh6042026-03-15 16:19:35 -0400
commit6ec259a0e71174651bae95d4628138bf6fd68742 (patch)
tree5e33c6a5ec091ecabfcb257fdc7b6a88ed8754ac /packages/excalidraw/actions/manager.tsx
parent16c8578b15c727f22921f8a80a56ee4d4e7f2272 (diff)
refactor: packages/
Diffstat (limited to 'packages/excalidraw/actions/manager.tsx')
-rw-r--r--packages/excalidraw/actions/manager.tsx194
1 files changed, 194 insertions, 0 deletions
diff --git a/packages/excalidraw/actions/manager.tsx b/packages/excalidraw/actions/manager.tsx
new file mode 100644
index 0000000..f378438
--- /dev/null
+++ b/packages/excalidraw/actions/manager.tsx
@@ -0,0 +1,194 @@
+import React from "react";
+import type {
+ Action,
+ UpdaterFn,
+ ActionName,
+ ActionResult,
+ PanelComponentProps,
+ ActionSource,
+} from "./types";
+import type {
+ ExcalidrawElement,
+ OrderedExcalidrawElement,
+} from "../element/types";
+import type { AppClassProperties, AppState } from "../types";
+import { trackEvent } from "../analytics";
+import { isPromiseLike } from "../utils";
+
+const trackAction = (
+ action: Action,
+ source: ActionSource,
+ appState: Readonly<AppState>,
+ elements: readonly ExcalidrawElement[],
+ app: AppClassProperties,
+ value: any,
+) => {
+ if (action.trackEvent) {
+ try {
+ if (typeof action.trackEvent === "object") {
+ const shouldTrack = action.trackEvent.predicate
+ ? action.trackEvent.predicate(appState, elements, value)
+ : true;
+ if (shouldTrack) {
+ trackEvent(
+ action.trackEvent.category,
+ action.trackEvent.action || action.name,
+ `${source} (${app.device.editor.isMobile ? "mobile" : "desktop"})`,
+ );
+ }
+ }
+ } catch (error) {
+ console.error("error while logging action:", error);
+ }
+ }
+};
+
+export class ActionManager {
+ actions = {} as Record<ActionName, Action>;
+
+ updater: (actionResult: ActionResult | Promise<ActionResult>) => void;
+
+ getAppState: () => Readonly<AppState>;
+ getElementsIncludingDeleted: () => readonly OrderedExcalidrawElement[];
+ app: AppClassProperties;
+
+ constructor(
+ updater: UpdaterFn,
+ getAppState: () => AppState,
+ getElementsIncludingDeleted: () => readonly OrderedExcalidrawElement[],
+ app: AppClassProperties,
+ ) {
+ this.updater = (actionResult) => {
+ if (isPromiseLike(actionResult)) {
+ actionResult.then((actionResult) => {
+ return updater(actionResult);
+ });
+ } else {
+ return updater(actionResult);
+ }
+ };
+ this.getAppState = getAppState;
+ this.getElementsIncludingDeleted = getElementsIncludingDeleted;
+ this.app = app;
+ }
+
+ registerAction(action: Action) {
+ this.actions[action.name] = action;
+ }
+
+ registerAll(actions: readonly Action[]) {
+ actions.forEach((action) => this.registerAction(action));
+ }
+
+ handleKeyDown(event: React.KeyboardEvent | KeyboardEvent) {
+ const canvasActions = this.app.props.UIOptions.canvasActions;
+ const data = Object.values(this.actions)
+ .sort((a, b) => (b.keyPriority || 0) - (a.keyPriority || 0))
+ .filter(
+ (action) =>
+ (action.name in canvasActions
+ ? canvasActions[action.name as keyof typeof canvasActions]
+ : true) &&
+ action.keyTest &&
+ action.keyTest(
+ event,
+ this.getAppState(),
+ this.getElementsIncludingDeleted(),
+ this.app,
+ ),
+ );
+
+ if (data.length !== 1) {
+ if (data.length > 1) {
+ console.warn("Canceling as multiple actions match this shortcut", data);
+ }
+ return false;
+ }
+
+ const action = data[0];
+
+ if (this.getAppState().viewModeEnabled && action.viewMode !== true) {
+ return false;
+ }
+
+ const elements = this.getElementsIncludingDeleted();
+ const appState = this.getAppState();
+ const value = null;
+
+ trackAction(action, "keyboard", appState, elements, this.app, null);
+
+ event.preventDefault();
+ event.stopPropagation();
+ this.updater(data[0].perform(elements, appState, value, this.app));
+ return true;
+ }
+
+ executeAction<T extends Action>(
+ action: T,
+ source: ActionSource = "api",
+ value: Parameters<T["perform"]>[2] = null,
+ ) {
+ const elements = this.getElementsIncludingDeleted();
+ const appState = this.getAppState();
+
+ trackAction(action, source, appState, elements, this.app, value);
+
+ this.updater(action.perform(elements, appState, value, this.app));
+ }
+
+ /**
+ * @param data additional data sent to the PanelComponent
+ */
+ renderAction = (name: ActionName, data?: PanelComponentProps["data"]) => {
+ const canvasActions = this.app.props.UIOptions.canvasActions;
+
+ if (
+ this.actions[name] &&
+ "PanelComponent" in this.actions[name] &&
+ (name in canvasActions
+ ? canvasActions[name as keyof typeof canvasActions]
+ : true)
+ ) {
+ const action = this.actions[name];
+ const PanelComponent = action.PanelComponent!;
+ PanelComponent.displayName = "PanelComponent";
+ const elements = this.getElementsIncludingDeleted();
+ const appState = this.getAppState();
+ const updateData = (formState?: any) => {
+ trackAction(action, "ui", appState, elements, this.app, formState);
+
+ this.updater(
+ action.perform(
+ this.getElementsIncludingDeleted(),
+ this.getAppState(),
+ formState,
+ this.app,
+ ),
+ );
+ };
+
+ return (
+ <PanelComponent
+ elements={this.getElementsIncludingDeleted()}
+ appState={this.getAppState()}
+ updateData={updateData}
+ appProps={this.app.props}
+ app={this.app}
+ data={data}
+ />
+ );
+ }
+
+ return null;
+ };
+
+ isActionEnabled = (action: Action) => {
+ const elements = this.getElementsIncludingDeleted();
+ const appState = this.getAppState();
+
+ return (
+ !action.predicate ||
+ action.predicate(elements, appState, this.app.props, this.app)
+ );
+ };
+}