import React from "react"; import { Action, ActionsManagerInterface, UpdaterFn, ActionName, ActionResult, PanelComponentProps, } from "./types"; import { ExcalidrawElement } from "../element/types"; import { AppClassProperties, AppState } from "../types"; import { MODES } from "../constants"; import { trackEvent } from "../analytics"; const trackAction = ( action: Action, source: "ui" | "keyboard" | "api", value: any, ) => { if (action.trackEvent !== false) { try { if (action.trackEvent === true) { trackEvent( action.name, source, typeof value === "number" || typeof value === "string" ? String(value) : undefined, ); } else { action.trackEvent?.(action, source, value); } } catch (error) { console.error("error while logging action:", error); } } }; export class ActionManager implements ActionsManagerInterface { actions = {} as ActionsManagerInterface["actions"]; updater: (actionResult: ActionResult | Promise) => void; getAppState: () => Readonly; getElementsIncludingDeleted: () => readonly ExcalidrawElement[]; app: AppClassProperties; constructor( updater: UpdaterFn, getAppState: () => AppState, getElementsIncludingDeleted: () => readonly ExcalidrawElement[], app: AppClassProperties, ) { this.updater = (actionResult) => { if (actionResult && "then" in 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(), ), ); if (data.length !== 1) { if (data.length > 1) { console.warn("Canceling as multiple actions match this shortcut", data); } return false; } const action = data[0]; const { viewModeEnabled } = this.getAppState(); if (viewModeEnabled) { if (!Object.values(MODES).includes(data[0].name)) { return false; } } trackAction(action, "keyboard", null); event.preventDefault(); this.updater( data[0].perform( this.getElementsIncludingDeleted(), this.getAppState(), null, this.app, ), ); return true; } executeAction(action: Action) { this.updater( action.perform( this.getElementsIncludingDeleted(), this.getAppState(), null, this.app, ), ); trackAction(action, "api", null); } /** * @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!; const updateData = (formState?: any) => { this.updater( action.perform( this.getElementsIncludingDeleted(), this.getAppState(), formState, this.app, ), ); trackAction(action, "ui", formState); }; return ( ); } return null; }; }