manager.tsx 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  1. import React from "react";
  2. import {
  3. Action,
  4. UpdaterFn,
  5. ActionName,
  6. ActionResult,
  7. PanelComponentProps,
  8. ActionSource,
  9. } from "./types";
  10. import { ExcalidrawElement } from "../element/types";
  11. import { AppClassProperties, AppState } from "../types";
  12. import { MODES } from "../constants";
  13. import { trackEvent } from "../analytics";
  14. const trackAction = (
  15. action: Action,
  16. source: ActionSource,
  17. appState: Readonly<AppState>,
  18. elements: readonly ExcalidrawElement[],
  19. app: AppClassProperties,
  20. value: any,
  21. ) => {
  22. if (action.trackEvent) {
  23. try {
  24. if (typeof action.trackEvent === "object") {
  25. const shouldTrack = action.trackEvent.predicate
  26. ? action.trackEvent.predicate(appState, elements, value)
  27. : true;
  28. if (shouldTrack) {
  29. trackEvent(
  30. action.trackEvent.category,
  31. action.trackEvent.action || action.name,
  32. `${source} (${app.device.isMobile ? "mobile" : "desktop"})`,
  33. );
  34. }
  35. }
  36. } catch (error) {
  37. console.error("error while logging action:", error);
  38. }
  39. }
  40. };
  41. export class ActionManager {
  42. actions = {} as Record<ActionName, Action>;
  43. updater: (actionResult: ActionResult | Promise<ActionResult>) => void;
  44. getAppState: () => Readonly<AppState>;
  45. getElementsIncludingDeleted: () => readonly ExcalidrawElement[];
  46. app: AppClassProperties;
  47. constructor(
  48. updater: UpdaterFn,
  49. getAppState: () => AppState,
  50. getElementsIncludingDeleted: () => readonly ExcalidrawElement[],
  51. app: AppClassProperties,
  52. ) {
  53. this.updater = (actionResult) => {
  54. if (actionResult && "then" in actionResult) {
  55. actionResult.then((actionResult) => {
  56. return updater(actionResult);
  57. });
  58. } else {
  59. return updater(actionResult);
  60. }
  61. };
  62. this.getAppState = getAppState;
  63. this.getElementsIncludingDeleted = getElementsIncludingDeleted;
  64. this.app = app;
  65. }
  66. registerAction(action: Action) {
  67. this.actions[action.name] = action;
  68. }
  69. registerAll(actions: readonly Action[]) {
  70. actions.forEach((action) => this.registerAction(action));
  71. }
  72. handleKeyDown(event: React.KeyboardEvent | KeyboardEvent) {
  73. const canvasActions = this.app.props.UIOptions.canvasActions;
  74. const data = Object.values(this.actions)
  75. .sort((a, b) => (b.keyPriority || 0) - (a.keyPriority || 0))
  76. .filter(
  77. (action) =>
  78. (action.name in canvasActions
  79. ? canvasActions[action.name as keyof typeof canvasActions]
  80. : true) &&
  81. action.keyTest &&
  82. action.keyTest(
  83. event,
  84. this.getAppState(),
  85. this.getElementsIncludingDeleted(),
  86. ),
  87. );
  88. if (data.length !== 1) {
  89. if (data.length > 1) {
  90. console.warn("Canceling as multiple actions match this shortcut", data);
  91. }
  92. return false;
  93. }
  94. const action = data[0];
  95. const { viewModeEnabled } = this.getAppState();
  96. if (viewModeEnabled) {
  97. if (!Object.values(MODES).includes(data[0].name)) {
  98. return false;
  99. }
  100. }
  101. const elements = this.getElementsIncludingDeleted();
  102. const appState = this.getAppState();
  103. const value = null;
  104. trackAction(action, "keyboard", appState, elements, this.app, null);
  105. event.preventDefault();
  106. event.stopPropagation();
  107. this.updater(data[0].perform(elements, appState, value, this.app));
  108. return true;
  109. }
  110. executeAction(action: Action, source: ActionSource = "api") {
  111. const elements = this.getElementsIncludingDeleted();
  112. const appState = this.getAppState();
  113. const value = null;
  114. trackAction(action, source, appState, elements, this.app, value);
  115. this.updater(action.perform(elements, appState, value, this.app));
  116. }
  117. /**
  118. * @param data additional data sent to the PanelComponent
  119. */
  120. renderAction = (name: ActionName, data?: PanelComponentProps["data"]) => {
  121. const canvasActions = this.app.props.UIOptions.canvasActions;
  122. if (
  123. this.actions[name] &&
  124. "PanelComponent" in this.actions[name] &&
  125. (name in canvasActions
  126. ? canvasActions[name as keyof typeof canvasActions]
  127. : true)
  128. ) {
  129. const action = this.actions[name];
  130. const PanelComponent = action.PanelComponent!;
  131. const elements = this.getElementsIncludingDeleted();
  132. const appState = this.getAppState();
  133. const updateData = (formState?: any) => {
  134. trackAction(action, "ui", appState, elements, this.app, formState);
  135. this.updater(
  136. action.perform(
  137. this.getElementsIncludingDeleted(),
  138. this.getAppState(),
  139. formState,
  140. this.app,
  141. ),
  142. );
  143. };
  144. return (
  145. <PanelComponent
  146. elements={this.getElementsIncludingDeleted()}
  147. appState={this.getAppState()}
  148. updateData={updateData}
  149. appProps={this.app.props}
  150. data={data}
  151. />
  152. );
  153. }
  154. return null;
  155. };
  156. }