mirror of
				https://github.com/excalidraw/excalidraw.git
				synced 2025-10-31 10:54:33 +01:00 
			
		
		
		
	 1ed53b153c
			
		
	
	1ed53b153c
	
	
	
		
			
			* build: enable consistent type imports eslint rule * change to warn * fix the warning in example and excalidraw-app * fix packages * enable type annotations and throw error for the rule
		
			
				
	
	
		
			195 lines
		
	
	
		
			5.3 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			195 lines
		
	
	
		
			5.3 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| 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)
 | |
|     );
 | |
|   };
 | |
| }
 |