ui.ts 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304
  1. import {
  2. ExcalidrawElement,
  3. ExcalidrawLinearElement,
  4. ExcalidrawTextElement,
  5. } from "../../element/types";
  6. import { CODES } from "../../keys";
  7. import { ToolName } from "../queries/toolQueries";
  8. import { fireEvent, GlobalTestState } from "../test-utils";
  9. import { mutateElement } from "../../element/mutateElement";
  10. import { API } from "./api";
  11. const { h } = window;
  12. let altKey = false;
  13. let shiftKey = false;
  14. let ctrlKey = false;
  15. export type KeyboardModifiers = {
  16. alt?: boolean;
  17. shift?: boolean;
  18. ctrl?: boolean;
  19. };
  20. export class Keyboard {
  21. static withModifierKeys = (modifiers: KeyboardModifiers, cb: () => void) => {
  22. const prevAltKey = altKey;
  23. const prevShiftKey = shiftKey;
  24. const prevCtrlKey = ctrlKey;
  25. altKey = !!modifiers.alt;
  26. shiftKey = !!modifiers.shift;
  27. ctrlKey = !!modifiers.ctrl;
  28. try {
  29. cb();
  30. } finally {
  31. altKey = prevAltKey;
  32. shiftKey = prevShiftKey;
  33. ctrlKey = prevCtrlKey;
  34. }
  35. };
  36. static keyDown = (key: string) => {
  37. fireEvent.keyDown(document, {
  38. key,
  39. ctrlKey,
  40. shiftKey,
  41. altKey,
  42. });
  43. };
  44. static keyUp = (key: string) => {
  45. fireEvent.keyUp(document, {
  46. key,
  47. ctrlKey,
  48. shiftKey,
  49. altKey,
  50. });
  51. };
  52. static keyPress = (key: string) => {
  53. Keyboard.keyDown(key);
  54. Keyboard.keyUp(key);
  55. };
  56. static codeDown = (code: string) => {
  57. fireEvent.keyDown(document, {
  58. code,
  59. ctrlKey,
  60. shiftKey,
  61. altKey,
  62. });
  63. };
  64. static codeUp = (code: string) => {
  65. fireEvent.keyUp(document, {
  66. code,
  67. ctrlKey,
  68. shiftKey,
  69. altKey,
  70. });
  71. };
  72. static codePress = (code: string) => {
  73. Keyboard.codeDown(code);
  74. Keyboard.codeUp(code);
  75. };
  76. }
  77. export class Pointer {
  78. public clientX = 0;
  79. public clientY = 0;
  80. constructor(
  81. private readonly pointerType: "mouse" | "touch" | "pen",
  82. private readonly pointerId = 1,
  83. ) {}
  84. reset() {
  85. this.clientX = 0;
  86. this.clientY = 0;
  87. }
  88. getPosition() {
  89. return [this.clientX, this.clientY];
  90. }
  91. restorePosition(x = 0, y = 0) {
  92. this.clientX = x;
  93. this.clientY = y;
  94. fireEvent.pointerMove(GlobalTestState.canvas, this.getEvent());
  95. }
  96. private getEvent() {
  97. return {
  98. clientX: this.clientX,
  99. clientY: this.clientY,
  100. pointerType: this.pointerType,
  101. pointerId: this.pointerId,
  102. altKey,
  103. shiftKey,
  104. ctrlKey,
  105. };
  106. }
  107. // incremental (moving by deltas)
  108. // ---------------------------------------------------------------------------
  109. move(dx: number, dy: number) {
  110. if (dx !== 0 || dy !== 0) {
  111. this.clientX += dx;
  112. this.clientY += dy;
  113. fireEvent.pointerMove(GlobalTestState.canvas, this.getEvent());
  114. }
  115. }
  116. down(dx = 0, dy = 0) {
  117. this.move(dx, dy);
  118. fireEvent.pointerDown(GlobalTestState.canvas, this.getEvent());
  119. }
  120. up(dx = 0, dy = 0) {
  121. this.move(dx, dy);
  122. fireEvent.pointerUp(GlobalTestState.canvas, this.getEvent());
  123. }
  124. click(dx = 0, dy = 0) {
  125. this.down(dx, dy);
  126. this.up();
  127. }
  128. doubleClick(dx = 0, dy = 0) {
  129. this.move(dx, dy);
  130. fireEvent.doubleClick(GlobalTestState.canvas, this.getEvent());
  131. }
  132. // absolute coords
  133. // ---------------------------------------------------------------------------
  134. moveTo(x: number = this.clientX, y: number = this.clientY) {
  135. this.clientX = x;
  136. this.clientY = y;
  137. fireEvent.pointerMove(GlobalTestState.canvas, this.getEvent());
  138. }
  139. downAt(x = this.clientX, y = this.clientY) {
  140. this.clientX = x;
  141. this.clientY = y;
  142. fireEvent.pointerDown(GlobalTestState.canvas, this.getEvent());
  143. }
  144. upAt(x = this.clientX, y = this.clientY) {
  145. this.clientX = x;
  146. this.clientY = y;
  147. fireEvent.pointerUp(GlobalTestState.canvas, this.getEvent());
  148. }
  149. clickAt(x: number, y: number) {
  150. this.downAt(x, y);
  151. this.upAt();
  152. }
  153. doubleClickAt(x: number, y: number) {
  154. this.moveTo(x, y);
  155. fireEvent.doubleClick(GlobalTestState.canvas, this.getEvent());
  156. }
  157. // ---------------------------------------------------------------------------
  158. select(
  159. /** if multiple elements supplied, they're shift-selected */
  160. elements: ExcalidrawElement | ExcalidrawElement[],
  161. ) {
  162. API.clearSelection();
  163. Keyboard.withModifierKeys({ shift: true }, () => {
  164. elements = Array.isArray(elements) ? elements : [elements];
  165. elements.forEach((element) => {
  166. this.reset();
  167. this.click(element.x, element.y);
  168. });
  169. });
  170. this.reset();
  171. }
  172. clickOn(element: ExcalidrawElement) {
  173. this.reset();
  174. this.click(element.x, element.y);
  175. this.reset();
  176. }
  177. doubleClickOn(element: ExcalidrawElement) {
  178. this.reset();
  179. this.doubleClick(element.x, element.y);
  180. this.reset();
  181. }
  182. }
  183. const mouse = new Pointer("mouse");
  184. export class UI {
  185. static clickTool = (toolName: ToolName) => {
  186. fireEvent.click(GlobalTestState.renderResult.getByToolName(toolName));
  187. };
  188. /**
  189. * Creates an Excalidraw element, and returns a proxy that wraps it so that
  190. * accessing props will return the latest ones from the object existing in
  191. * the app's elements array. This is because across the app lifecycle we tend
  192. * to recreate element objects and the returned reference will become stale.
  193. *
  194. * If you need to get the actual element, not the proxy, call `get()` method
  195. * on the proxy object.
  196. */
  197. static createElement<T extends ToolName>(
  198. type: T,
  199. {
  200. position = 0,
  201. x = position,
  202. y = position,
  203. size = 10,
  204. width = size,
  205. height = width,
  206. angle = 0,
  207. }: {
  208. position?: number;
  209. x?: number;
  210. y?: number;
  211. size?: number;
  212. width?: number;
  213. height?: number;
  214. angle?: number;
  215. } = {},
  216. ): (T extends "arrow" | "line" | "freedraw"
  217. ? ExcalidrawLinearElement
  218. : T extends "text"
  219. ? ExcalidrawTextElement
  220. : ExcalidrawElement) & {
  221. /** Returns the actual, current element from the elements array, instead
  222. of the proxy */
  223. get(): T extends "arrow" | "line" | "freedraw"
  224. ? ExcalidrawLinearElement
  225. : T extends "text"
  226. ? ExcalidrawTextElement
  227. : ExcalidrawElement;
  228. } {
  229. UI.clickTool(type);
  230. mouse.reset();
  231. mouse.down(x, y);
  232. mouse.reset();
  233. mouse.up(x + (width ?? height ?? size), y + (height ?? size));
  234. const origElement = h.elements[h.elements.length - 1] as any;
  235. if (angle !== 0) {
  236. mutateElement(origElement, { angle });
  237. }
  238. return new Proxy(
  239. {},
  240. {
  241. get(target, prop) {
  242. const currentElement = h.elements.find(
  243. (element) => element.id === origElement.id,
  244. ) as any;
  245. if (prop === "get") {
  246. if (currentElement.hasOwnProperty("get")) {
  247. throw new Error(
  248. "trying to get `get` test property, but ExcalidrawElement seems to define its own",
  249. );
  250. }
  251. return () => currentElement;
  252. }
  253. return currentElement[prop];
  254. },
  255. },
  256. ) as any;
  257. }
  258. static group(elements: ExcalidrawElement[]) {
  259. mouse.select(elements);
  260. Keyboard.withModifierKeys({ ctrl: true }, () => {
  261. Keyboard.codePress(CODES.G);
  262. });
  263. }
  264. }