diff options
| author | kj_sh604 | 2026-03-15 16:19:35 -0400 |
|---|---|---|
| committer | kj_sh604 | 2026-03-15 16:19:35 -0400 |
| commit | 6ec259a0e71174651bae95d4628138bf6fd68742 (patch) | |
| tree | 5e33c6a5ec091ecabfcb257fdc7b6a88ed8754ac /packages/excalidraw/actions/manager.tsx | |
| parent | 16c8578b15c727f22921f8a80a56ee4d4e7f2272 (diff) | |
refactor: packages/
Diffstat (limited to 'packages/excalidraw/actions/manager.tsx')
| -rw-r--r-- | packages/excalidraw/actions/manager.tsx | 194 |
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) + ); + }; +} |
