import React from "react"; import { Action, UpdaterFn, ActionName, ActionResult, PanelComponentProps, ActionSource, } from "./types"; import { ExcalidrawElement } from "../element/types"; import { AppClassProperties, AppState } from "../types"; import { trackEvent } from "../analytics"; const trackAction = ( action: Action, source: ActionSource, appState: Readonly, 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; 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(), 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( action: T, source: ActionSource = "api", value: Parameters[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 ( ); } 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) ); }; }