textWysiwyg.tsx 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704
  1. import { CODES, KEYS } from "../keys";
  2. import {
  3. isWritableElement,
  4. getFontString,
  5. getFontFamilyString,
  6. isTestEnv,
  7. } from "../utils";
  8. import Scene from "../scene/Scene";
  9. import {
  10. isArrowElement,
  11. isBoundToContainer,
  12. isTextElement,
  13. } from "./typeChecks";
  14. import { CLASSES, VERTICAL_ALIGN } from "../constants";
  15. import {
  16. ExcalidrawElement,
  17. ExcalidrawLinearElement,
  18. ExcalidrawTextElementWithContainer,
  19. ExcalidrawTextElement,
  20. ExcalidrawTextContainer,
  21. } from "./types";
  22. import { AppState } from "../types";
  23. import { mutateElement } from "./mutateElement";
  24. import {
  25. getApproxLineHeight,
  26. getBoundTextElementId,
  27. getContainerCoords,
  28. getContainerDims,
  29. getContainerElement,
  30. getTextElementAngle,
  31. getTextWidth,
  32. measureText,
  33. normalizeText,
  34. redrawTextBoundingBox,
  35. wrapText,
  36. getMaxContainerHeight,
  37. getMaxContainerWidth,
  38. } from "./textElement";
  39. import {
  40. actionDecreaseFontSize,
  41. actionIncreaseFontSize,
  42. } from "../actions/actionProperties";
  43. import { actionZoomIn, actionZoomOut } from "../actions/actionCanvas";
  44. import App from "../components/App";
  45. import { LinearElementEditor } from "./linearElementEditor";
  46. import { parseClipboard } from "../clipboard";
  47. const getTransform = (
  48. width: number,
  49. height: number,
  50. angle: number,
  51. appState: AppState,
  52. maxWidth: number,
  53. maxHeight: number,
  54. ) => {
  55. const { zoom } = appState;
  56. const degree = (180 * angle) / Math.PI;
  57. let translateX = (width * (zoom.value - 1)) / 2;
  58. let translateY = (height * (zoom.value - 1)) / 2;
  59. if (width > maxWidth && zoom.value !== 1) {
  60. translateX = (maxWidth * (zoom.value - 1)) / 2;
  61. }
  62. if (height > maxHeight && zoom.value !== 1) {
  63. translateY = (maxHeight * (zoom.value - 1)) / 2;
  64. }
  65. return `translate(${translateX}px, ${translateY}px) scale(${zoom.value}) rotate(${degree}deg)`;
  66. };
  67. const originalContainerCache: {
  68. [id: ExcalidrawTextContainer["id"]]:
  69. | {
  70. height: ExcalidrawTextContainer["height"];
  71. }
  72. | undefined;
  73. } = {};
  74. export const updateOriginalContainerCache = (
  75. id: ExcalidrawTextContainer["id"],
  76. height: ExcalidrawTextContainer["height"],
  77. ) => {
  78. const data =
  79. originalContainerCache[id] || (originalContainerCache[id] = { height });
  80. data.height = height;
  81. return data;
  82. };
  83. export const resetOriginalContainerCache = (
  84. id: ExcalidrawTextContainer["id"],
  85. ) => {
  86. if (originalContainerCache[id]) {
  87. delete originalContainerCache[id];
  88. }
  89. };
  90. export const getOriginalContainerHeightFromCache = (
  91. id: ExcalidrawTextContainer["id"],
  92. ) => {
  93. return originalContainerCache[id]?.height ?? null;
  94. };
  95. export const textWysiwyg = ({
  96. id,
  97. onChange,
  98. onSubmit,
  99. getViewportCoords,
  100. element,
  101. canvas,
  102. excalidrawContainer,
  103. app,
  104. }: {
  105. id: ExcalidrawElement["id"];
  106. onChange?: (text: string) => void;
  107. onSubmit: (data: {
  108. text: string;
  109. viaKeyboard: boolean;
  110. originalText: string;
  111. }) => void;
  112. getViewportCoords: (x: number, y: number) => [number, number];
  113. element: ExcalidrawTextElement;
  114. canvas: HTMLCanvasElement | null;
  115. excalidrawContainer: HTMLDivElement | null;
  116. app: App;
  117. }) => {
  118. const textPropertiesUpdated = (
  119. updatedTextElement: ExcalidrawTextElement,
  120. editable: HTMLTextAreaElement,
  121. ) => {
  122. if (!editable.style.fontFamily || !editable.style.fontSize) {
  123. return false;
  124. }
  125. const currentFont = editable.style.fontFamily.replace(/"/g, "");
  126. if (
  127. getFontFamilyString({ fontFamily: updatedTextElement.fontFamily }) !==
  128. currentFont
  129. ) {
  130. return true;
  131. }
  132. if (`${updatedTextElement.fontSize}px` !== editable.style.fontSize) {
  133. return true;
  134. }
  135. return false;
  136. };
  137. const updateWysiwygStyle = () => {
  138. const appState = app.state;
  139. const updatedTextElement =
  140. Scene.getScene(element)?.getElement<ExcalidrawTextElement>(id);
  141. if (!updatedTextElement) {
  142. return;
  143. }
  144. const { textAlign, verticalAlign } = updatedTextElement;
  145. const approxLineHeight = getApproxLineHeight(
  146. getFontString(updatedTextElement),
  147. );
  148. if (updatedTextElement && isTextElement(updatedTextElement)) {
  149. let coordX = updatedTextElement.x;
  150. let coordY = updatedTextElement.y;
  151. const container = getContainerElement(updatedTextElement);
  152. let maxWidth = updatedTextElement.width;
  153. let maxHeight = updatedTextElement.height;
  154. let textElementWidth = updatedTextElement.width;
  155. // Set to element height by default since that's
  156. // what is going to be used for unbounded text
  157. let textElementHeight = updatedTextElement.height;
  158. if (container && updatedTextElement.containerId) {
  159. if (isArrowElement(container)) {
  160. const boundTextCoords =
  161. LinearElementEditor.getBoundTextElementPosition(
  162. container,
  163. updatedTextElement as ExcalidrawTextElementWithContainer,
  164. );
  165. coordX = boundTextCoords.x;
  166. coordY = boundTextCoords.y;
  167. }
  168. const propertiesUpdated = textPropertiesUpdated(
  169. updatedTextElement,
  170. editable,
  171. );
  172. const containerDims = getContainerDims(container);
  173. // using editor.style.height to get the accurate height of text editor
  174. const editorHeight = Number(editable.style.height.slice(0, -2));
  175. if (editorHeight > 0) {
  176. textElementHeight = editorHeight;
  177. }
  178. if (propertiesUpdated) {
  179. // update height of the editor after properties updated
  180. textElementHeight = updatedTextElement.height;
  181. }
  182. let originalContainerData;
  183. if (propertiesUpdated) {
  184. originalContainerData = updateOriginalContainerCache(
  185. container.id,
  186. containerDims.height,
  187. );
  188. } else {
  189. originalContainerData = originalContainerCache[container.id];
  190. if (!originalContainerData) {
  191. originalContainerData = updateOriginalContainerCache(
  192. container.id,
  193. containerDims.height,
  194. );
  195. }
  196. }
  197. maxWidth = getMaxContainerWidth(container);
  198. maxHeight = getMaxContainerHeight(container);
  199. // autogrow container height if text exceeds
  200. if (!isArrowElement(container) && textElementHeight > maxHeight) {
  201. const diff = Math.min(
  202. textElementHeight - maxHeight,
  203. approxLineHeight,
  204. );
  205. mutateElement(container, { height: containerDims.height + diff });
  206. return;
  207. } else if (
  208. // autoshrink container height until original container height
  209. // is reached when text is removed
  210. !isArrowElement(container) &&
  211. containerDims.height > originalContainerData.height &&
  212. textElementHeight < maxHeight
  213. ) {
  214. const diff = Math.min(
  215. maxHeight - textElementHeight,
  216. approxLineHeight,
  217. );
  218. mutateElement(container, { height: containerDims.height - diff });
  219. }
  220. // Start pushing text upward until a diff of 30px (padding)
  221. // is reached
  222. else {
  223. const containerCoords = getContainerCoords(container);
  224. // vertically center align the text
  225. if (verticalAlign === VERTICAL_ALIGN.MIDDLE) {
  226. if (!isArrowElement(container)) {
  227. coordY =
  228. containerCoords.y + maxHeight / 2 - textElementHeight / 2;
  229. }
  230. }
  231. if (verticalAlign === VERTICAL_ALIGN.BOTTOM) {
  232. coordY = containerCoords.y + (maxHeight - textElementHeight);
  233. }
  234. }
  235. }
  236. const [viewportX, viewportY] = getViewportCoords(coordX, coordY);
  237. const initialSelectionStart = editable.selectionStart;
  238. const initialSelectionEnd = editable.selectionEnd;
  239. const initialLength = editable.value.length;
  240. editable.value = updatedTextElement.originalText;
  241. // restore cursor position after value updated so it doesn't
  242. // go to the end of text when container auto expanded
  243. if (
  244. initialSelectionStart === initialSelectionEnd &&
  245. initialSelectionEnd !== initialLength
  246. ) {
  247. // get diff between length and selection end and shift
  248. // the cursor by "diff" times to position correctly
  249. const diff = initialLength - initialSelectionEnd;
  250. editable.selectionStart = editable.value.length - diff;
  251. editable.selectionEnd = editable.value.length - diff;
  252. }
  253. const lines = updatedTextElement.originalText.split("\n");
  254. const lineHeight = updatedTextElement.containerId
  255. ? approxLineHeight
  256. : updatedTextElement.height / lines.length;
  257. if (!container) {
  258. maxWidth = (appState.width - 8 - viewportX) / appState.zoom.value;
  259. textElementWidth = Math.min(textElementWidth, maxWidth);
  260. } else {
  261. textElementWidth += 0.5;
  262. }
  263. // Make sure text editor height doesn't go beyond viewport
  264. const editorMaxHeight =
  265. (appState.height - viewportY) / appState.zoom.value;
  266. Object.assign(editable.style, {
  267. font: getFontString(updatedTextElement),
  268. // must be defined *after* font ¯\_(ツ)_/¯
  269. lineHeight: `${lineHeight}px`,
  270. width: `${textElementWidth}px`,
  271. height: `${textElementHeight}px`,
  272. left: `${viewportX}px`,
  273. top: `${viewportY}px`,
  274. transform: getTransform(
  275. textElementWidth,
  276. textElementHeight,
  277. getTextElementAngle(updatedTextElement),
  278. appState,
  279. maxWidth,
  280. editorMaxHeight,
  281. ),
  282. textAlign,
  283. verticalAlign,
  284. color: updatedTextElement.strokeColor,
  285. opacity: updatedTextElement.opacity / 100,
  286. filter: "var(--theme-filter)",
  287. maxHeight: `${editorMaxHeight}px`,
  288. });
  289. // For some reason updating font attribute doesn't set font family
  290. // hence updating font family explicitly for test environment
  291. if (isTestEnv()) {
  292. editable.style.fontFamily = getFontFamilyString(updatedTextElement);
  293. }
  294. mutateElement(updatedTextElement, { x: coordX, y: coordY });
  295. }
  296. };
  297. const editable = document.createElement("textarea");
  298. editable.dir = "auto";
  299. editable.tabIndex = 0;
  300. editable.dataset.type = "wysiwyg";
  301. // prevent line wrapping on Safari
  302. editable.wrap = "off";
  303. editable.classList.add("excalidraw-wysiwyg");
  304. let whiteSpace = "pre";
  305. let wordBreak = "normal";
  306. if (isBoundToContainer(element)) {
  307. whiteSpace = "pre-wrap";
  308. wordBreak = "break-word";
  309. }
  310. Object.assign(editable.style, {
  311. position: "absolute",
  312. display: "inline-block",
  313. minHeight: "1em",
  314. backfaceVisibility: "hidden",
  315. margin: 0,
  316. padding: 0,
  317. border: 0,
  318. outline: 0,
  319. resize: "none",
  320. background: "transparent",
  321. overflow: "hidden",
  322. // must be specified because in dark mode canvas creates a stacking context
  323. zIndex: "var(--zIndex-wysiwyg)",
  324. wordBreak,
  325. // prevent line wrapping (`whitespace: nowrap` doesn't work on FF)
  326. whiteSpace,
  327. overflowWrap: "break-word",
  328. boxSizing: "content-box",
  329. });
  330. updateWysiwygStyle();
  331. if (onChange) {
  332. editable.onpaste = async (event) => {
  333. const clipboardData = await parseClipboard(event, true);
  334. if (!clipboardData.text) {
  335. return;
  336. }
  337. const data = normalizeText(clipboardData.text);
  338. if (!data) {
  339. return;
  340. }
  341. const container = getContainerElement(element);
  342. const font = getFontString({
  343. fontSize: app.state.currentItemFontSize,
  344. fontFamily: app.state.currentItemFontFamily,
  345. });
  346. if (container) {
  347. const wrappedText = wrapText(
  348. `${editable.value}${data}`,
  349. font,
  350. getMaxContainerWidth(container),
  351. );
  352. const width = getTextWidth(wrappedText, font);
  353. editable.style.width = `${width}px`;
  354. }
  355. };
  356. editable.oninput = () => {
  357. const updatedTextElement = Scene.getScene(element)?.getElement(
  358. id,
  359. ) as ExcalidrawTextElement;
  360. const font = getFontString(updatedTextElement);
  361. if (isBoundToContainer(element)) {
  362. const container = getContainerElement(element);
  363. const wrappedText = wrapText(
  364. normalizeText(editable.value),
  365. font,
  366. getMaxContainerWidth(container!),
  367. );
  368. const { width, height } = measureText(wrappedText, font);
  369. editable.style.width = `${width}px`;
  370. editable.style.height = `${height}px`;
  371. }
  372. onChange(normalizeText(editable.value));
  373. };
  374. }
  375. editable.onkeydown = (event) => {
  376. if (!event.shiftKey && actionZoomIn.keyTest(event)) {
  377. event.preventDefault();
  378. app.actionManager.executeAction(actionZoomIn);
  379. updateWysiwygStyle();
  380. } else if (!event.shiftKey && actionZoomOut.keyTest(event)) {
  381. event.preventDefault();
  382. app.actionManager.executeAction(actionZoomOut);
  383. updateWysiwygStyle();
  384. } else if (actionDecreaseFontSize.keyTest(event)) {
  385. app.actionManager.executeAction(actionDecreaseFontSize);
  386. } else if (actionIncreaseFontSize.keyTest(event)) {
  387. app.actionManager.executeAction(actionIncreaseFontSize);
  388. } else if (event.key === KEYS.ESCAPE) {
  389. event.preventDefault();
  390. submittedViaKeyboard = true;
  391. handleSubmit();
  392. } else if (event.key === KEYS.ENTER && event[KEYS.CTRL_OR_CMD]) {
  393. event.preventDefault();
  394. if (event.isComposing || event.keyCode === 229) {
  395. return;
  396. }
  397. submittedViaKeyboard = true;
  398. handleSubmit();
  399. } else if (
  400. event.key === KEYS.TAB ||
  401. (event[KEYS.CTRL_OR_CMD] &&
  402. (event.code === CODES.BRACKET_LEFT ||
  403. event.code === CODES.BRACKET_RIGHT))
  404. ) {
  405. event.preventDefault();
  406. if (event.isComposing) {
  407. return;
  408. } else if (event.shiftKey || event.code === CODES.BRACKET_LEFT) {
  409. outdent();
  410. } else {
  411. indent();
  412. }
  413. // We must send an input event to resize the element
  414. editable.dispatchEvent(new Event("input"));
  415. }
  416. };
  417. const TAB_SIZE = 4;
  418. const TAB = " ".repeat(TAB_SIZE);
  419. const RE_LEADING_TAB = new RegExp(`^ {1,${TAB_SIZE}}`);
  420. const indent = () => {
  421. const { selectionStart, selectionEnd } = editable;
  422. const linesStartIndices = getSelectedLinesStartIndices();
  423. let value = editable.value;
  424. linesStartIndices.forEach((startIndex: number) => {
  425. const startValue = value.slice(0, startIndex);
  426. const endValue = value.slice(startIndex);
  427. value = `${startValue}${TAB}${endValue}`;
  428. });
  429. editable.value = value;
  430. editable.selectionStart = selectionStart + TAB_SIZE;
  431. editable.selectionEnd = selectionEnd + TAB_SIZE * linesStartIndices.length;
  432. };
  433. const outdent = () => {
  434. const { selectionStart, selectionEnd } = editable;
  435. const linesStartIndices = getSelectedLinesStartIndices();
  436. const removedTabs: number[] = [];
  437. let value = editable.value;
  438. linesStartIndices.forEach((startIndex) => {
  439. const tabMatch = value
  440. .slice(startIndex, startIndex + TAB_SIZE)
  441. .match(RE_LEADING_TAB);
  442. if (tabMatch) {
  443. const startValue = value.slice(0, startIndex);
  444. const endValue = value.slice(startIndex + tabMatch[0].length);
  445. // Delete a tab from the line
  446. value = `${startValue}${endValue}`;
  447. removedTabs.push(startIndex);
  448. }
  449. });
  450. editable.value = value;
  451. if (removedTabs.length) {
  452. if (selectionStart > removedTabs[removedTabs.length - 1]) {
  453. editable.selectionStart = Math.max(
  454. selectionStart - TAB_SIZE,
  455. removedTabs[removedTabs.length - 1],
  456. );
  457. } else {
  458. // If the cursor is before the first tab removed, ex:
  459. // Line| #1
  460. // Line #2
  461. // Lin|e #3
  462. // we should reset the selectionStart to his initial value.
  463. editable.selectionStart = selectionStart;
  464. }
  465. editable.selectionEnd = Math.max(
  466. editable.selectionStart,
  467. selectionEnd - TAB_SIZE * removedTabs.length,
  468. );
  469. }
  470. };
  471. /**
  472. * @returns indices of start positions of selected lines, in reverse order
  473. */
  474. const getSelectedLinesStartIndices = () => {
  475. let { selectionStart, selectionEnd, value } = editable;
  476. // chars before selectionStart on the same line
  477. const startOffset = value.slice(0, selectionStart).match(/[^\n]*$/)![0]
  478. .length;
  479. // put caret at the start of the line
  480. selectionStart = selectionStart - startOffset;
  481. const selected = value.slice(selectionStart, selectionEnd);
  482. return selected
  483. .split("\n")
  484. .reduce(
  485. (startIndices, line, idx, lines) =>
  486. startIndices.concat(
  487. idx
  488. ? // curr line index is prev line's start + prev line's length + \n
  489. startIndices[idx - 1] + lines[idx - 1].length + 1
  490. : // first selected line
  491. selectionStart,
  492. ),
  493. [] as number[],
  494. )
  495. .reverse();
  496. };
  497. const stopEvent = (event: Event) => {
  498. event.preventDefault();
  499. event.stopPropagation();
  500. };
  501. // using a state variable instead of passing it to the handleSubmit callback
  502. // so that we don't need to create separate a callback for event handlers
  503. let submittedViaKeyboard = false;
  504. const handleSubmit = () => {
  505. // cleanup must be run before onSubmit otherwise when app blurs the wysiwyg
  506. // it'd get stuck in an infinite loop of blur→onSubmit after we re-focus the
  507. // wysiwyg on update
  508. cleanup();
  509. const updateElement = Scene.getScene(element)?.getElement(
  510. element.id,
  511. ) as ExcalidrawTextElement;
  512. if (!updateElement) {
  513. return;
  514. }
  515. let text = editable.value;
  516. const container = getContainerElement(updateElement);
  517. if (container) {
  518. text = updateElement.text;
  519. if (editable.value.trim()) {
  520. const boundTextElementId = getBoundTextElementId(container);
  521. if (!boundTextElementId || boundTextElementId !== element.id) {
  522. mutateElement(container, {
  523. boundElements: (container.boundElements || []).concat({
  524. type: "text",
  525. id: element.id,
  526. }),
  527. });
  528. }
  529. } else {
  530. mutateElement(container, {
  531. boundElements: container.boundElements?.filter(
  532. (ele) =>
  533. !isTextElement(
  534. ele as ExcalidrawTextElement | ExcalidrawLinearElement,
  535. ),
  536. ),
  537. });
  538. }
  539. redrawTextBoundingBox(updateElement, container);
  540. }
  541. onSubmit({
  542. text,
  543. viaKeyboard: submittedViaKeyboard,
  544. originalText: editable.value,
  545. });
  546. };
  547. const cleanup = () => {
  548. if (isDestroyed) {
  549. return;
  550. }
  551. isDestroyed = true;
  552. // remove events to ensure they don't late-fire
  553. editable.onblur = null;
  554. editable.oninput = null;
  555. editable.onkeydown = null;
  556. if (observer) {
  557. observer.disconnect();
  558. }
  559. window.removeEventListener("resize", updateWysiwygStyle);
  560. window.removeEventListener("wheel", stopEvent, true);
  561. window.removeEventListener("pointerdown", onPointerDown);
  562. window.removeEventListener("pointerup", bindBlurEvent);
  563. window.removeEventListener("blur", handleSubmit);
  564. unbindUpdate();
  565. editable.remove();
  566. };
  567. const bindBlurEvent = (event?: MouseEvent) => {
  568. window.removeEventListener("pointerup", bindBlurEvent);
  569. // Deferred so that the pointerdown that initiates the wysiwyg doesn't
  570. // trigger the blur on ensuing pointerup.
  571. // Also to handle cases such as picking a color which would trigger a blur
  572. // in that same tick.
  573. const target = event?.target;
  574. const isTargetColorPicker =
  575. target instanceof HTMLInputElement &&
  576. target.closest(".color-picker-input") &&
  577. isWritableElement(target);
  578. setTimeout(() => {
  579. editable.onblur = handleSubmit;
  580. if (target && isTargetColorPicker) {
  581. target.onblur = () => {
  582. editable.focus();
  583. };
  584. }
  585. // case: clicking on the same property → no change → no update → no focus
  586. if (!isTargetColorPicker) {
  587. editable.focus();
  588. }
  589. });
  590. };
  591. // prevent blur when changing properties from the menu
  592. const onPointerDown = (event: MouseEvent) => {
  593. const isTargetColorPicker =
  594. event.target instanceof HTMLInputElement &&
  595. event.target.closest(".color-picker-input") &&
  596. isWritableElement(event.target);
  597. if (
  598. ((event.target instanceof HTMLElement ||
  599. event.target instanceof SVGElement) &&
  600. event.target.closest(`.${CLASSES.SHAPE_ACTIONS_MENU}`) &&
  601. !isWritableElement(event.target)) ||
  602. isTargetColorPicker
  603. ) {
  604. editable.onblur = null;
  605. window.addEventListener("pointerup", bindBlurEvent);
  606. // handle edge-case where pointerup doesn't fire e.g. due to user
  607. // alt-tabbing away
  608. window.addEventListener("blur", handleSubmit);
  609. }
  610. };
  611. // handle updates of textElement properties of editing element
  612. const unbindUpdate = Scene.getScene(element)!.addCallback(() => {
  613. updateWysiwygStyle();
  614. const isColorPickerActive = !!document.activeElement?.closest(
  615. ".color-picker-input",
  616. );
  617. if (!isColorPickerActive) {
  618. editable.focus();
  619. }
  620. });
  621. // ---------------------------------------------------------------------------
  622. let isDestroyed = false;
  623. // select on init (focusing is done separately inside the bindBlurEvent()
  624. // because we need it to happen *after* the blur event from `pointerdown`)
  625. editable.select();
  626. bindBlurEvent();
  627. // reposition wysiwyg in case of canvas is resized. Using ResizeObserver
  628. // is preferred so we catch changes from host, where window may not resize.
  629. let observer: ResizeObserver | null = null;
  630. if (canvas && "ResizeObserver" in window) {
  631. observer = new window.ResizeObserver(() => {
  632. updateWysiwygStyle();
  633. });
  634. observer.observe(canvas);
  635. } else {
  636. window.addEventListener("resize", updateWysiwygStyle);
  637. }
  638. window.addEventListener("pointerdown", onPointerDown);
  639. window.addEventListener("wheel", stopEvent, {
  640. passive: false,
  641. capture: true,
  642. });
  643. excalidrawContainer
  644. ?.querySelector(".excalidraw-textEditorContainer")!
  645. .appendChild(editable);
  646. };