@excalidraw/excalidraw 0.17.1-1ed53b1 → 0.17.1-22b3927

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/dist/browser/dev/excalidraw-assets-dev/{chunk-JKPJV7MZ.js → chunk-Q6A4M3MN.js} +4 -2
  2. package/dist/browser/dev/excalidraw-assets-dev/chunk-Q6A4M3MN.js.map +7 -0
  3. package/dist/browser/dev/excalidraw-assets-dev/{chunk-OKAZAA6U.js → chunk-VC7RRIDZ.js} +230 -93
  4. package/dist/browser/dev/excalidraw-assets-dev/chunk-VC7RRIDZ.js.map +7 -0
  5. package/dist/browser/dev/excalidraw-assets-dev/{dist-ITJNUBZF.js → dist-6QVAH5JA.js} +36 -14
  6. package/dist/browser/dev/excalidraw-assets-dev/dist-6QVAH5JA.js.map +7 -0
  7. package/dist/browser/dev/excalidraw-assets-dev/{en-BF4XUPIZ.js → en-Y27YPU72.js} +2 -2
  8. package/dist/browser/dev/excalidraw-assets-dev/{image-LVS32KQQ.js → image-J7S3ALXP.js} +2 -2
  9. package/dist/browser/dev/index.js +335 -116
  10. package/dist/browser/dev/index.js.map +4 -4
  11. package/dist/browser/prod/excalidraw-assets/chunk-CWO763YJ.js +55 -0
  12. package/dist/browser/prod/excalidraw-assets/{chunk-O4AI3NNG.js → chunk-IZMZ6RPD.js} +1 -1
  13. package/dist/browser/prod/excalidraw-assets/dist-567JAXHK.js +7 -0
  14. package/dist/browser/prod/excalidraw-assets/{en-N7CLNF6C.js → en-GSUSWMSH.js} +1 -1
  15. package/dist/browser/prod/excalidraw-assets/image-SZBFRCU2.js +1 -0
  16. package/dist/browser/prod/index.js +24 -24
  17. package/dist/dev/{en-UQDDYCH7.json → en-OIPCBIOA.json} +3 -1
  18. package/dist/dev/index.js +576 -207
  19. package/dist/dev/index.js.map +4 -4
  20. package/dist/excalidraw/actions/actionAddToLibrary.d.ts +3 -3
  21. package/dist/excalidraw/actions/actionAlign.d.ts +6 -6
  22. package/dist/excalidraw/actions/actionBoundText.d.ts +3 -3
  23. package/dist/excalidraw/actions/actionBoundText.js +3 -1
  24. package/dist/excalidraw/actions/actionCanvas.d.ts +13 -13
  25. package/dist/excalidraw/actions/actionClipboard.d.ts +12 -12
  26. package/dist/excalidraw/actions/actionDeleteSelected.d.ts +3 -3
  27. package/dist/excalidraw/actions/actionDistribute.d.ts +2 -2
  28. package/dist/excalidraw/actions/actionDuplicateSelection.d.ts +1 -1
  29. package/dist/excalidraw/actions/actionElementLock.d.ts +2 -2
  30. package/dist/excalidraw/actions/actionExport.d.ts +11 -11
  31. package/dist/excalidraw/actions/actionFinalize.d.ts +2 -2
  32. package/dist/excalidraw/actions/actionFlip.d.ts +2 -2
  33. package/dist/excalidraw/actions/actionFrame.d.ts +312 -4
  34. package/dist/excalidraw/actions/actionGroup.d.ts +312 -2
  35. package/dist/excalidraw/actions/actionHistory.js +4 -4
  36. package/dist/excalidraw/actions/actionLinearEditor.d.ts +1 -1
  37. package/dist/excalidraw/actions/actionLink.d.ts +1 -1
  38. package/dist/excalidraw/actions/actionMenu.d.ts +3 -3
  39. package/dist/excalidraw/actions/actionNavigate.d.ts +2 -2
  40. package/dist/excalidraw/actions/actionProperties.d.ts +13 -13
  41. package/dist/excalidraw/actions/actionProperties.js +1 -1
  42. package/dist/excalidraw/actions/actionSelectAll.d.ts +1 -1
  43. package/dist/excalidraw/actions/actionStyles.d.ts +5 -2
  44. package/dist/excalidraw/actions/actionTextAutoResize.d.ts +17 -0
  45. package/dist/excalidraw/actions/actionTextAutoResize.js +38 -0
  46. package/dist/excalidraw/actions/actionToggleGridMode.d.ts +1 -1
  47. package/dist/excalidraw/actions/actionToggleObjectsSnapMode.d.ts +1 -1
  48. package/dist/excalidraw/actions/actionToggleStats.d.ts +1 -1
  49. package/dist/excalidraw/actions/actionToggleViewMode.d.ts +1 -1
  50. package/dist/excalidraw/actions/actionToggleZenMode.d.ts +1 -1
  51. package/dist/excalidraw/actions/actionZindex.d.ts +4 -4
  52. package/dist/excalidraw/actions/types.d.ts +1 -1
  53. package/dist/excalidraw/change.js +13 -6
  54. package/dist/excalidraw/components/Actions.js +1 -1
  55. package/dist/excalidraw/components/App.d.ts +2 -2
  56. package/dist/excalidraw/components/App.js +133 -51
  57. package/dist/excalidraw/components/ButtonIconSelect.js +1 -1
  58. package/dist/excalidraw/components/CheckboxItem.js +1 -1
  59. package/dist/excalidraw/components/CommandPalette/CommandPalette.js +2 -2
  60. package/dist/excalidraw/components/ContextMenu.js +1 -1
  61. package/dist/excalidraw/components/Dialog.js +1 -1
  62. package/dist/excalidraw/components/FollowMode/FollowMode.js +1 -1
  63. package/dist/excalidraw/components/IconPicker.js +2 -2
  64. package/dist/excalidraw/components/LayerUI.js +2 -2
  65. package/dist/excalidraw/components/MobileMenu.js +1 -1
  66. package/dist/excalidraw/components/PasteChartDialog.js +1 -1
  67. package/dist/excalidraw/components/canvases/InteractiveCanvas.d.ts +3 -2
  68. package/dist/excalidraw/components/canvases/InteractiveCanvas.js +4 -2
  69. package/dist/excalidraw/components/canvases/StaticCanvas.d.ts +1 -1
  70. package/dist/excalidraw/components/canvases/StaticCanvas.js +2 -2
  71. package/dist/excalidraw/components/icons.js +6 -2
  72. package/dist/excalidraw/constants.d.ts +1 -0
  73. package/dist/excalidraw/constants.js +5 -0
  74. package/dist/excalidraw/data/restore.js +3 -0
  75. package/dist/excalidraw/element/dragElements.d.ts +2 -2
  76. package/dist/excalidraw/element/dragElements.js +27 -3
  77. package/dist/excalidraw/element/embeddable.d.ts +1 -1
  78. package/dist/excalidraw/element/index.d.ts +1 -1
  79. package/dist/excalidraw/element/index.js +1 -1
  80. package/dist/excalidraw/element/mutateElement.d.ts +1 -1
  81. package/dist/excalidraw/element/mutateElement.js +5 -3
  82. package/dist/excalidraw/element/newElement.d.ts +2 -5
  83. package/dist/excalidraw/element/newElement.js +16 -14
  84. package/dist/excalidraw/element/resizeElements.js +73 -21
  85. package/dist/excalidraw/element/resizeTest.js +2 -4
  86. package/dist/excalidraw/element/textElement.d.ts +1 -0
  87. package/dist/excalidraw/element/textElement.js +11 -3
  88. package/dist/excalidraw/element/textWysiwyg.d.ts +10 -4
  89. package/dist/excalidraw/element/textWysiwyg.js +38 -17
  90. package/dist/excalidraw/element/transformHandles.js +0 -10
  91. package/dist/excalidraw/element/types.d.ts +7 -0
  92. package/dist/excalidraw/fractionalIndex.js +2 -4
  93. package/dist/excalidraw/locales/en.json +3 -1
  94. package/dist/excalidraw/mermaid.d.ts +2 -0
  95. package/dist/excalidraw/mermaid.js +28 -0
  96. package/dist/excalidraw/renderer/interactiveScene.d.ts +1 -1
  97. package/dist/excalidraw/renderer/interactiveScene.js +31 -5
  98. package/dist/excalidraw/renderer/renderElement.d.ts +2 -2
  99. package/dist/excalidraw/renderer/renderElement.js +2 -2
  100. package/dist/excalidraw/scene/Fonts.d.ts +1 -3
  101. package/dist/excalidraw/scene/Fonts.js +6 -12
  102. package/dist/excalidraw/scene/Renderer.d.ts +1 -1
  103. package/dist/excalidraw/scene/Renderer.js +2 -3
  104. package/dist/excalidraw/scene/Scene.d.ts +10 -4
  105. package/dist/excalidraw/scene/Scene.js +14 -8
  106. package/dist/excalidraw/scene/export.js +1 -1
  107. package/dist/excalidraw/scene/types.d.ts +2 -1
  108. package/dist/excalidraw/snapping.js +2 -1
  109. package/dist/excalidraw/store.d.ts +32 -2
  110. package/dist/excalidraw/store.js +27 -0
  111. package/dist/excalidraw/types.d.ts +1 -0
  112. package/dist/prod/{en-UQDDYCH7.json → en-OIPCBIOA.json} +3 -1
  113. package/dist/prod/index.js +42 -42
  114. package/package.json +2 -2
  115. package/dist/browser/dev/excalidraw-assets-dev/chunk-JKPJV7MZ.js.map +0 -7
  116. package/dist/browser/dev/excalidraw-assets-dev/chunk-OKAZAA6U.js.map +0 -7
  117. package/dist/browser/dev/excalidraw-assets-dev/dist-ITJNUBZF.js.map +0 -7
  118. package/dist/browser/prod/excalidraw-assets/chunk-SXBDZOS3.js +0 -55
  119. package/dist/browser/prod/excalidraw-assets/dist-54276HPL.js +0 -6
  120. package/dist/browser/prod/excalidraw-assets/image-VAGBVQ3G.js +0 -1
  121. /package/dist/browser/dev/excalidraw-assets-dev/{en-BF4XUPIZ.js.map → en-Y27YPU72.js.map} +0 -0
  122. /package/dist/browser/dev/excalidraw-assets-dev/{image-LVS32KQQ.js.map → image-J7S3ALXP.js.map} +0 -0
@@ -23,7 +23,7 @@ const getTransform = (width, height, angle, appState, maxWidth, maxHeight) => {
23
23
  }
24
24
  return `translate(${translateX}px, ${translateY}px) scale(${zoom.value}) rotate(${degree}deg)`;
25
25
  };
26
- export const textWysiwyg = ({ id, onChange, onSubmit, getViewportCoords, element, canvas, excalidrawContainer, app, }) => {
26
+ export const textWysiwyg = ({ id, onChange, onSubmit, getViewportCoords, element, canvas, excalidrawContainer, app, autoSelect = true, }) => {
27
27
  const textPropertiesUpdated = (updatedTextElement, editable) => {
28
28
  if (!editable.style.fontFamily || !editable.style.fontSize) {
29
29
  return false;
@@ -53,8 +53,6 @@ export const textWysiwyg = ({ id, onChange, onSubmit, getViewportCoords, element
53
53
  let maxWidth = updatedTextElement.width;
54
54
  let maxHeight = updatedTextElement.height;
55
55
  let textElementWidth = updatedTextElement.width;
56
- // Set to element height by default since that's
57
- // what is going to be used for unbounded text
58
56
  const textElementHeight = updatedTextElement.height;
59
57
  if (container && updatedTextElement.containerId) {
60
58
  if (isArrowElement(container)) {
@@ -113,6 +111,9 @@ export const textWysiwyg = ({ id, onChange, onSubmit, getViewportCoords, element
113
111
  maxWidth = (appState.width - 8 - viewportX) / appState.zoom.value;
114
112
  textElementWidth = Math.min(textElementWidth, maxWidth);
115
113
  }
114
+ else {
115
+ textElementWidth += 0.5;
116
+ }
116
117
  // Make sure text editor height doesn't go beyond viewport
117
118
  const editorMaxHeight = (appState.height - viewportY) / appState.zoom.value;
118
119
  Object.assign(editable.style, {
@@ -149,7 +150,7 @@ export const textWysiwyg = ({ id, onChange, onSubmit, getViewportCoords, element
149
150
  editable.classList.add("excalidraw-wysiwyg");
150
151
  let whiteSpace = "pre";
151
152
  let wordBreak = "normal";
152
- if (isBoundToContainer(element)) {
153
+ if (isBoundToContainer(element) || !element.autoResize) {
153
154
  whiteSpace = "pre-wrap";
154
155
  wordBreak = "break-word";
155
156
  }
@@ -326,6 +327,11 @@ export const textWysiwyg = ({ id, onChange, onSubmit, getViewportCoords, element
326
327
  // so that we don't need to create separate a callback for event handlers
327
328
  let submittedViaKeyboard = false;
328
329
  const handleSubmit = () => {
330
+ // prevent double submit
331
+ if (isDestroyed) {
332
+ return;
333
+ }
334
+ isDestroyed = true;
329
335
  // cleanup must be run before onSubmit otherwise when app blurs the wysiwyg
330
336
  // it'd get stuck in an infinite loop of blur→onSubmit after we re-focus the
331
337
  // wysiwyg on update
@@ -334,10 +340,8 @@ export const textWysiwyg = ({ id, onChange, onSubmit, getViewportCoords, element
334
340
  if (!updateElement) {
335
341
  return;
336
342
  }
337
- let text = editable.value;
338
343
  const container = getContainerElement(updateElement, app.scene.getNonDeletedElementsMap());
339
344
  if (container) {
340
- text = updateElement.text;
341
345
  if (editable.value.trim()) {
342
346
  const boundTextElementId = getBoundTextElementId(container);
343
347
  if (!boundTextElementId || boundTextElementId !== element.id) {
@@ -361,16 +365,11 @@ export const textWysiwyg = ({ id, onChange, onSubmit, getViewportCoords, element
361
365
  redrawTextBoundingBox(updateElement, container, app.scene.getNonDeletedElementsMap());
362
366
  }
363
367
  onSubmit({
364
- text,
365
368
  viaKeyboard: submittedViaKeyboard,
366
- originalText: editable.value,
369
+ nextOriginalText: editable.value,
367
370
  });
368
371
  };
369
372
  const cleanup = () => {
370
- if (isDestroyed) {
371
- return;
372
- }
373
- isDestroyed = true;
374
373
  // remove events to ensure they don't late-fire
375
374
  editable.onblur = null;
376
375
  editable.oninput = null;
@@ -439,9 +438,24 @@ export const textWysiwyg = ({ id, onChange, onSubmit, getViewportCoords, element
439
438
  // alt-tabbing away
440
439
  window.addEventListener("blur", handleSubmit);
441
440
  }
441
+ else if (event.target instanceof HTMLElement &&
442
+ !event.target.contains(editable) &&
443
+ // Vitest simply ignores stopPropagation, capture-mode, or rAF
444
+ // so without introducing crazier hacks, nothing we can do
445
+ !isTestEnv()) {
446
+ // On mobile, blur event doesn't seem to always fire correctly,
447
+ // so we want to also submit on pointerdown outside the wysiwyg.
448
+ // Done in the next frame to prevent pointerdown from creating a new text
449
+ // immediately (if tools locked) so that users on mobile have chance
450
+ // to submit first (to hide virtual keyboard).
451
+ // Note: revisit if we want to differ this behavior on Desktop
452
+ requestAnimationFrame(() => {
453
+ handleSubmit();
454
+ });
455
+ }
442
456
  };
443
457
  // handle updates of textElement properties of editing element
444
- const unbindUpdate = Scene.getScene(element).addCallback(() => {
458
+ const unbindUpdate = Scene.getScene(element).onUpdate(() => {
445
459
  updateWysiwygStyle();
446
460
  const isColorPickerActive = !!document.activeElement?.closest(".color-picker-content");
447
461
  if (!isColorPickerActive) {
@@ -450,9 +464,11 @@ export const textWysiwyg = ({ id, onChange, onSubmit, getViewportCoords, element
450
464
  });
451
465
  // ---------------------------------------------------------------------------
452
466
  let isDestroyed = false;
453
- // select on init (focusing is done separately inside the bindBlurEvent()
454
- // because we need it to happen *after* the blur event from `pointerdown`)
455
- editable.select();
467
+ if (autoSelect) {
468
+ // select on init (focusing is done separately inside the bindBlurEvent()
469
+ // because we need it to happen *after* the blur event from `pointerdown`)
470
+ editable.select();
471
+ }
456
472
  bindBlurEvent();
457
473
  // reposition wysiwyg in case of canvas is resized. Using ResizeObserver
458
474
  // is preferred so we catch changes from host, where window may not resize.
@@ -466,7 +482,12 @@ export const textWysiwyg = ({ id, onChange, onSubmit, getViewportCoords, element
466
482
  else {
467
483
  window.addEventListener("resize", updateWysiwygStyle);
468
484
  }
469
- window.addEventListener("pointerdown", onPointerDown);
485
+ editable.onpointerdown = (event) => event.stopPropagation();
486
+ // rAF (+ capture to by doubly sure) so we don't catch te pointerdown that
487
+ // triggered the wysiwyg
488
+ requestAnimationFrame(() => {
489
+ window.addEventListener("pointerdown", onPointerDown, { capture: true });
490
+ });
470
491
  window.addEventListener("wheel", stopEvent, {
471
492
  passive: false,
472
493
  capture: true,
@@ -1,6 +1,5 @@
1
1
  import { getElementAbsoluteCoords } from "./bounds";
2
2
  import { rotate } from "../math";
3
- import { isTextElement } from ".";
4
3
  import { isFrameLikeElement, isLinearElement } from "./typeChecks";
5
4
  import { DEFAULT_TRANSFORM_HANDLE_SPACING, isAndroid, isIOS, } from "../constants";
6
5
  const transformHandleSizes = {
@@ -28,12 +27,6 @@ export const OMIT_SIDES_FOR_FRAME = {
28
27
  w: true,
29
28
  rotation: true,
30
29
  };
31
- const OMIT_SIDES_FOR_TEXT_ELEMENT = {
32
- e: true,
33
- s: true,
34
- n: true,
35
- w: true,
36
- };
37
30
  const OMIT_SIDES_FOR_LINE_SLASH = {
38
31
  e: true,
39
32
  s: true,
@@ -147,9 +140,6 @@ export const getTransformHandles = (element, zoom, elementsMap, pointerType = "m
147
140
  }
148
141
  }
149
142
  }
150
- else if (isTextElement(element)) {
151
- omitSides = OMIT_SIDES_FOR_TEXT_ELEMENT;
152
- }
153
143
  else if (isFrameLikeElement(element)) {
154
144
  omitSides = {
155
145
  ...omitSides,
@@ -154,6 +154,13 @@ export type ExcalidrawTextElement = _ExcalidrawElementBase & Readonly<{
154
154
  verticalAlign: VerticalAlign;
155
155
  containerId: ExcalidrawGenericElement["id"] | null;
156
156
  originalText: string;
157
+ /**
158
+ * If `true` the width will fit the text. If `false`, the text will
159
+ * wrap to fit the width.
160
+ *
161
+ * @default true
162
+ */
163
+ autoResize: boolean;
157
164
  /**
158
165
  * Unitless line height (aligned to W3C). To get line height in px, multiply
159
166
  * with font size (using `getLineHeightInPx` helper).
@@ -97,12 +97,10 @@ const getMovedIndicesGroups = (elements, movedElements) => {
97
97
  const indicesGroups = [];
98
98
  let i = 0;
99
99
  while (i < elements.length) {
100
- if (movedElements.has(elements[i].id) &&
101
- !isValidFractionalIndex(elements[i]?.index, elements[i - 1]?.index, elements[i + 1]?.index)) {
100
+ if (movedElements.has(elements[i].id)) {
102
101
  const indicesGroup = [i - 1, i]; // push the lower bound index as the first item
103
102
  while (++i < elements.length) {
104
- if (!(movedElements.has(elements[i].id) &&
105
- !isValidFractionalIndex(elements[i]?.index, elements[i - 1]?.index, elements[i + 1]?.index))) {
103
+ if (!movedElements.has(elements[i].id)) {
106
104
  break;
107
105
  }
108
106
  indicesGroup.push(i);
@@ -148,7 +148,9 @@
148
148
  "discordChat": "Discord chat",
149
149
  "zoomToFitViewport": "Zoom to fit in viewport",
150
150
  "zoomToFitSelection": "Zoom to fit selection",
151
- "zoomToFit": "Zoom to fit all elements"
151
+ "zoomToFit": "Zoom to fit all elements",
152
+ "installPWA": "Install Excalidraw locally (PWA)",
153
+ "autoResize": "Enable text auto-resizing"
152
154
  },
153
155
  "library": {
154
156
  "noItems": "No items added yet...",
@@ -0,0 +1,2 @@
1
+ /** heuristically checks whether the text may be a mermaid diagram definition */
2
+ export declare const isMaybeMermaidDefinition: (text: string) => boolean;
@@ -0,0 +1,28 @@
1
+ /** heuristically checks whether the text may be a mermaid diagram definition */
2
+ export const isMaybeMermaidDefinition = (text) => {
3
+ const chartTypes = [
4
+ "flowchart",
5
+ "sequenceDiagram",
6
+ "classDiagram",
7
+ "stateDiagram",
8
+ "stateDiagram-v2",
9
+ "erDiagram",
10
+ "journey",
11
+ "gantt",
12
+ "pie",
13
+ "quadrantChart",
14
+ "requirementDiagram",
15
+ "gitGraph",
16
+ "C4Context",
17
+ "mindmap",
18
+ "timeline",
19
+ "zenuml",
20
+ "sankey",
21
+ "xychart",
22
+ "block",
23
+ ];
24
+ const re = new RegExp(`^(?:%%{.*?}%%[\\s\\n]*)?\\b${chartTypes
25
+ .map((x) => `${x}(-beta)?`)
26
+ .join("|")}\\b`);
27
+ return re.test(text.trim());
28
+ };
@@ -9,7 +9,7 @@ export declare const renderInteractiveSceneThrottled: {
9
9
  * Interactive scene is the ui-canvas where we render bounding boxes, selections
10
10
  * and other ui stuff.
11
11
  */
12
- export declare const renderInteractiveScene: <U extends ({ canvas, elementsMap, visibleElements, selectedElements, scale, appState, renderConfig, device, }: InteractiveSceneRenderConfig) => {
12
+ export declare const renderInteractiveScene: <U extends ({ canvas, elementsMap, visibleElements, selectedElements, allElementsMap, scale, appState, renderConfig, device, }: InteractiveSceneRenderConfig) => {
13
13
  atLeastOneVisibleElement: boolean;
14
14
  elementsMap: RenderableElementsMap;
15
15
  scrollBars?: undefined;
@@ -12,7 +12,7 @@ import { maxBindingGap } from "../element/binding";
12
12
  import { LinearElementEditor } from "../element/linearElementEditor";
13
13
  import { bootstrapCanvas, fillCircle, getNormalizedCanvasDimensions, } from "./helpers";
14
14
  import oc from "open-color";
15
- import { isFrameLikeElement, isLinearElement } from "../element/typeChecks";
15
+ import { isFrameLikeElement, isLinearElement, isTextElement, } from "../element/typeChecks";
16
16
  const renderLinearElementPointHighlight = (context, appState, elementsMap) => {
17
17
  const { elementId, hoverPointIndex } = appState.selectedLinearElement;
18
18
  if (appState.editingLinearElement?.selectedPointsIndices?.includes(hoverPointIndex)) {
@@ -124,10 +124,11 @@ const renderBindingHighlightForSuggestedPointBinding = (context, suggestedBindin
124
124
  fillCircle(context, x, y, threshold);
125
125
  });
126
126
  };
127
- const renderSelectionBorder = (context, appState, elementProperties, padding = DEFAULT_TRANSFORM_HANDLE_SPACING * 2) => {
127
+ const renderSelectionBorder = (context, appState, elementProperties) => {
128
128
  const { angle, elementX1, elementY1, elementX2, elementY2, selectionColors, cx, cy, dashed, activeEmbeddable, } = elementProperties;
129
129
  const elementWidth = elementX2 - elementX1;
130
130
  const elementHeight = elementY2 - elementY1;
131
+ const padding = DEFAULT_TRANSFORM_HANDLE_SPACING * 2;
131
132
  const linePadding = padding / appState.zoom.value;
132
133
  const lineWidth = 8 / appState.zoom.value;
133
134
  const spaceWidth = 4 / appState.zoom.value;
@@ -264,7 +265,23 @@ const renderTransformHandles = (context, renderConfig, appState, transformHandle
264
265
  }
265
266
  });
266
267
  };
267
- const _renderInteractiveScene = ({ canvas, elementsMap, visibleElements, selectedElements, scale, appState, renderConfig, device, }) => {
268
+ const renderTextBox = (text, context, appState, selectionColor) => {
269
+ context.save();
270
+ const padding = (DEFAULT_TRANSFORM_HANDLE_SPACING * 2) / appState.zoom.value;
271
+ const width = text.width + padding * 2;
272
+ const height = text.height + padding * 2;
273
+ const cx = text.x + width / 2;
274
+ const cy = text.y + height / 2;
275
+ const shiftX = -(width / 2 + padding);
276
+ const shiftY = -(height / 2 + padding);
277
+ context.translate(cx + appState.scrollX, cy + appState.scrollY);
278
+ context.rotate(text.angle);
279
+ context.lineWidth = 1 / appState.zoom.value;
280
+ context.strokeStyle = selectionColor;
281
+ context.strokeRect(shiftX, shiftY, width, height);
282
+ context.restore();
283
+ };
284
+ const _renderInteractiveScene = ({ canvas, elementsMap, visibleElements, selectedElements, allElementsMap, scale, appState, renderConfig, device, }) => {
268
285
  if (canvas === null) {
269
286
  return { atLeastOneVisibleElement: false, elementsMap };
270
287
  }
@@ -295,12 +312,18 @@ const _renderInteractiveScene = ({ canvas, elementsMap, visibleElements, selecte
295
312
  // Paint selection element
296
313
  if (appState.selectionElement) {
297
314
  try {
298
- renderSelectionElement(appState.selectionElement, context, appState);
315
+ renderSelectionElement(appState.selectionElement, context, appState, renderConfig.selectionColor);
299
316
  }
300
317
  catch (error) {
301
318
  console.error(error);
302
319
  }
303
320
  }
321
+ if (appState.editingElement && isTextElement(appState.editingElement)) {
322
+ const textElement = allElementsMap.get(appState.editingElement.id);
323
+ if (textElement && !textElement.autoResize) {
324
+ renderTextBox(textElement, context, appState, renderConfig.selectionColor);
325
+ }
326
+ }
304
327
  if (appState.isBindingEnabled) {
305
328
  appState.suggestedBindings
306
329
  .filter((binding) => binding != null)
@@ -405,7 +428,10 @@ const _renderInteractiveScene = ({ canvas, elementsMap, visibleElements, selecte
405
428
  context.fillStyle = oc.white;
406
429
  const transformHandles = getTransformHandles(selectedElements[0], appState.zoom, elementsMap, "mouse", // when we render we don't know which pointer type so use mouse,
407
430
  getOmitSidesForDevice(device));
408
- if (!appState.viewModeEnabled && showBoundingBox) {
431
+ if (!appState.viewModeEnabled &&
432
+ showBoundingBox &&
433
+ // do not show transform handles when text is being edited
434
+ !isTextElement(appState.editingElement)) {
409
435
  renderTransformHandles(context, renderConfig, appState, transformHandles, selectedElements[0].angle);
410
436
  }
411
437
  }
@@ -1,6 +1,6 @@
1
1
  import type { ExcalidrawElement, ExcalidrawTextElement, NonDeletedExcalidrawElement, ExcalidrawFreeDrawElement, ExcalidrawFrameLikeElement, NonDeletedSceneElementsMap } from "../element/types";
2
2
  import type { RoughCanvas } from "roughjs/bin/canvas";
3
- import type { StaticCanvasRenderConfig, RenderableElementsMap } from "../scene/types";
3
+ import type { StaticCanvasRenderConfig, RenderableElementsMap, InteractiveCanvasRenderConfig } from "../scene/types";
4
4
  import type { AppState, StaticCanvasAppState, InteractiveCanvasAppState, ElementsPendingErasure } from "../types";
5
5
  export declare const IMAGE_INVERT_FILTER = "invert(100%) hue-rotate(180deg) saturate(1.25)";
6
6
  export declare const getRenderOpacity: (element: ExcalidrawElement, containingFrame: ExcalidrawFrameLikeElement | null, elementsPendingErasure: ElementsPendingErasure) => number;
@@ -17,7 +17,7 @@ export interface ExcalidrawElementWithCanvas {
17
17
  }
18
18
  export declare const DEFAULT_LINK_SIZE = 14;
19
19
  export declare const elementWithCanvasCache: WeakMap<ExcalidrawElement, ExcalidrawElementWithCanvas>;
20
- export declare const renderSelectionElement: (element: NonDeletedExcalidrawElement, context: CanvasRenderingContext2D, appState: InteractiveCanvasAppState) => void;
20
+ export declare const renderSelectionElement: (element: NonDeletedExcalidrawElement, context: CanvasRenderingContext2D, appState: InteractiveCanvasAppState, selectionColor: InteractiveCanvasRenderConfig["selectionColor"]) => void;
21
21
  export declare const renderElement: (element: NonDeletedExcalidrawElement, elementsMap: RenderableElementsMap, allElementsMap: NonDeletedSceneElementsMap, rc: RoughCanvas, context: CanvasRenderingContext2D, renderConfig: StaticCanvasRenderConfig, appState: StaticCanvasAppState) => void;
22
22
  export declare const pathsCache: WeakMap<ExcalidrawFreeDrawElement, Path2D>;
23
23
  export declare function generateFreeDrawShape(element: ExcalidrawFreeDrawElement): Path2D;
@@ -323,7 +323,7 @@ const drawElementFromCanvas = (elementWithCanvas, context, renderConfig, appStat
323
323
  context.restore();
324
324
  // Clear the nested element we appended to the DOM
325
325
  };
326
- export const renderSelectionElement = (element, context, appState) => {
326
+ export const renderSelectionElement = (element, context, appState, selectionColor) => {
327
327
  context.save();
328
328
  context.translate(element.x + appState.scrollX, element.y + appState.scrollY);
329
329
  context.fillStyle = "rgba(0, 0, 200, 0.04)";
@@ -334,7 +334,7 @@ export const renderSelectionElement = (element, context, appState) => {
334
334
  const offset = 0.5 / appState.zoom.value;
335
335
  context.fillRect(offset, offset, element.width, element.height);
336
336
  context.lineWidth = 1 / appState.zoom.value;
337
- context.strokeStyle = " rgb(105, 101, 219)";
337
+ context.strokeStyle = selectionColor;
338
338
  context.strokeRect(offset, offset, element.width, element.height);
339
339
  context.restore();
340
340
  };
@@ -2,10 +2,8 @@ import type { ExcalidrawElement } from "../element/types";
2
2
  import type Scene from "./Scene";
3
3
  export declare class Fonts {
4
4
  private scene;
5
- private onSceneUpdated;
6
- constructor({ scene, onSceneUpdated, }: {
5
+ constructor({ scene }: {
7
6
  scene: Scene;
8
- onSceneUpdated: () => void;
9
7
  });
10
8
  private static loadedFontFaces;
11
9
  /**
@@ -1,15 +1,11 @@
1
- import { isTextElement, refreshTextDimensions } from "../element";
1
+ import { isTextElement } from "../element";
2
2
  import { newElementWith } from "../element/mutateElement";
3
- import { getContainerElement } from "../element/textElement";
4
- import { isBoundToContainer } from "../element/typeChecks";
5
3
  import { getFontString } from "../utils";
6
4
  import { ShapeCache } from "./ShapeCache";
7
5
  export class Fonts {
8
6
  scene;
9
- onSceneUpdated;
10
- constructor({ scene, onSceneUpdated, }) {
7
+ constructor({ scene }) {
11
8
  this.scene = scene;
12
- this.onSceneUpdated = onSceneUpdated;
13
9
  }
14
10
  // it's ok to track fonts across multiple instances only once, so let's use
15
11
  // a static member to reduce memory footprint
@@ -39,17 +35,15 @@ export class Fonts {
39
35
  }
40
36
  let didUpdate = false;
41
37
  this.scene.mapElements((element) => {
42
- if (isTextElement(element) && !isBoundToContainer(element)) {
43
- ShapeCache.delete(element);
38
+ if (isTextElement(element)) {
44
39
  didUpdate = true;
45
- return newElementWith(element, {
46
- ...refreshTextDimensions(element, getContainerElement(element, this.scene.getNonDeletedElementsMap()), this.scene.getNonDeletedElementsMap()),
47
- });
40
+ ShapeCache.delete(element);
41
+ return newElementWith(element, {}, true);
48
42
  }
49
43
  return element;
50
44
  });
51
45
  if (didUpdate) {
52
- this.onSceneUpdated();
46
+ this.scene.triggerUpdate();
53
47
  }
54
48
  };
55
49
  loadFontsForElements = async (elements) => {
@@ -14,7 +14,7 @@ export declare class Renderer {
14
14
  width: AppState["width"];
15
15
  editingElement: AppState["editingElement"];
16
16
  pendingImageElementId: AppState["pendingImageElementId"];
17
- versionNonce: ReturnType<InstanceType<typeof Scene>["getVersionNonce"]>;
17
+ sceneNonce: ReturnType<InstanceType<typeof Scene>["getSceneNonce"]>;
18
18
  }) => {
19
19
  elementsMap: Map<string, NonDeletedExcalidrawElement> & import("../utility-types").MakeBrand<"NonDeletedElementsMap"> & import("../utility-types").MakeBrand<"RenderableElementsMap">;
20
20
  visibleElements: readonly NonDeletedExcalidrawElement[];
@@ -45,9 +45,8 @@ export class Renderer {
45
45
  return elementsMap;
46
46
  };
47
47
  return memoize(({ zoom, offsetLeft, offsetTop, scrollX, scrollY, height, width, editingElement, pendingImageElementId,
48
- // unused but serves we cache on it to invalidate elements if they
49
- // get mutated
50
- versionNonce: _versionNonce, }) => {
48
+ // cache-invalidation nonce
49
+ sceneNonce: _sceneNonce, }) => {
51
50
  const elements = this.scene.getNonDeletedElements();
52
51
  const elementsMap = getRenderableElements({
53
52
  elements,
@@ -19,7 +19,14 @@ declare class Scene {
19
19
  private frames;
20
20
  private elementsMap;
21
21
  private selectedElementsCache;
22
- private versionNonce;
22
+ /**
23
+ * Random integer regenerated each scene update.
24
+ *
25
+ * Does not relate to elements versions, it's only a renderer
26
+ * cache-invalidation nonce at the moment.
27
+ */
28
+ private sceneNonce;
29
+ getSceneNonce(): number | undefined;
23
30
  getNonDeletedElementsMap(): Map<string, Ordered<NonDeletedExcalidrawElement>> & import("../utility-types").MakeBrand<"NonDeletedSceneElementsMap">;
24
31
  getElementsIncludingDeleted(): readonly OrderedExcalidrawElement[];
25
32
  getElementsMapIncludingDeleted(): Map<string, Ordered<ExcalidrawElement>> & import("../utility-types").MakeBrand<"SceneElementsMap">;
@@ -38,7 +45,6 @@ declare class Scene {
38
45
  }): NonDeleted<ExcalidrawElement>[];
39
46
  getNonDeletedFramesLikes(): readonly NonDeleted<ExcalidrawFrameLikeElement>[];
40
47
  getElement<T extends ExcalidrawElement>(id: T["id"]): T | null;
41
- getVersionNonce(): number | undefined;
42
48
  getNonDeletedElement(id: ExcalidrawElement["id"]): NonDeleted<ExcalidrawElement> | null;
43
49
  /**
44
50
  * A utility method to help with updating all scene elements, with the added
@@ -54,8 +60,8 @@ declare class Scene {
54
60
  */
55
61
  mapElements(iteratee: (element: ExcalidrawElement) => ExcalidrawElement): boolean;
56
62
  replaceAllElements(nextElements: ElementsMapOrArray): void;
57
- informMutation(): void;
58
- addCallback(cb: SceneStateCallback): SceneStateCallbackRemover;
63
+ triggerUpdate(): void;
64
+ onUpdate(cb: SceneStateCallback): SceneStateCallbackRemover;
59
65
  destroy(): void;
60
66
  insertElementAtIndex(element: ExcalidrawElement, index: number): void;
61
67
  insertElementsAtIndex(elements: ExcalidrawElement[], index: number): void;
@@ -72,7 +72,16 @@ class Scene {
72
72
  elements: null,
73
73
  cache: new Map(),
74
74
  };
75
- versionNonce;
75
+ /**
76
+ * Random integer regenerated each scene update.
77
+ *
78
+ * Does not relate to elements versions, it's only a renderer
79
+ * cache-invalidation nonce at the moment.
80
+ */
81
+ sceneNonce;
82
+ getSceneNonce() {
83
+ return this.sceneNonce;
84
+ }
76
85
  getNonDeletedElementsMap() {
77
86
  return this.nonDeletedElementsMap;
78
87
  }
@@ -118,9 +127,6 @@ class Scene {
118
127
  getElement(id) {
119
128
  return this.elementsMap.get(id) || null;
120
129
  }
121
- getVersionNonce() {
122
- return this.versionNonce;
123
- }
124
130
  getNonDeletedElement(id) {
125
131
  const element = this.getElement(id);
126
132
  if (element && isNonDeletedElement(element)) {
@@ -179,15 +185,15 @@ class Scene {
179
185
  this.nonDeletedElementsMap = nonDeletedElements.elementsMap;
180
186
  this.frames = nextFrameLikes;
181
187
  this.nonDeletedFramesLikes = getNonDeletedElements(this.frames).elements;
182
- this.informMutation();
188
+ this.triggerUpdate();
183
189
  }
184
- informMutation() {
185
- this.versionNonce = randomInteger();
190
+ triggerUpdate() {
191
+ this.sceneNonce = randomInteger();
186
192
  for (const callback of Array.from(this.callbacks)) {
187
193
  callback();
188
194
  }
189
195
  }
190
- addCallback(cb) {
196
+ onUpdate(cb) {
191
197
  if (this.callbacks.has(cb)) {
192
198
  throw new Error();
193
199
  }
@@ -200,7 +200,7 @@ export const exportToSvg = async (elements, appState, files, opts) => {
200
200
  if (import.meta.env.VITE_IS_EXCALIDRAW_NPM_PACKAGE) {
201
201
  assetPath =
202
202
  window.EXCALIDRAW_ASSET_PATH ||
203
- `https://unpkg.com/${import.meta.env.VITE_PKG_NAME}@${import.meta.env.PKG_VERSION}`;
203
+ `https://unpkg.com/${import.meta.env.VITE_PKG_NAME}@${import.meta.env.VITE_PKG_VERSION}`;
204
204
  if (assetPath?.startsWith("/")) {
205
205
  assetPath = assetPath.replace("/", `${window.location.origin}/`);
206
206
  }
@@ -33,7 +33,7 @@ export type InteractiveCanvasRenderConfig = {
33
33
  remotePointerUserStates: Map<SocketId, UserIdleState>;
34
34
  remotePointerUsernames: Map<SocketId, string>;
35
35
  remotePointerButton: Map<SocketId, string | undefined>;
36
- selectionColor?: string;
36
+ selectionColor: string;
37
37
  renderScrollbars?: boolean;
38
38
  };
39
39
  export type RenderInteractiveSceneCallback = {
@@ -56,6 +56,7 @@ export type InteractiveSceneRenderConfig = {
56
56
  elementsMap: RenderableElementsMap;
57
57
  visibleElements: readonly NonDeletedExcalidrawElement[];
58
58
  selectedElements: readonly NonDeletedExcalidrawElement[];
59
+ allElementsMap: NonDeletedSceneElementsMap;
59
60
  scale: number;
60
61
  appState: InteractiveCanvasAppState;
61
62
  renderConfig: InteractiveCanvasRenderConfig;
@@ -835,5 +835,6 @@ export const isActiveToolNonLinearSnappable = (activeToolType) => {
835
835
  activeToolType === TOOL_TYPE.diamond ||
836
836
  activeToolType === TOOL_TYPE.frame ||
837
837
  activeToolType === TOOL_TYPE.magicframe ||
838
- activeToolType === TOOL_TYPE.image);
838
+ activeToolType === TOOL_TYPE.image ||
839
+ activeToolType === TOOL_TYPE.text);
839
840
  };
@@ -2,11 +2,41 @@ import { AppStateChange, ElementsChange } from "./change";
2
2
  import type { OrderedExcalidrawElement } from "./element/types";
3
3
  import { Emitter } from "./emitter";
4
4
  import type { AppState, ObservedAppState } from "./types";
5
+ import type { ValueOf } from "./utility-types";
5
6
  export declare const getObservedAppState: (appState: AppState) => ObservedAppState;
6
- export type StoreActionType = "capture" | "update" | "none";
7
7
  export declare const StoreAction: {
8
- [K in Uppercase<StoreActionType>]: StoreActionType;
8
+ /**
9
+ * Immediately undoable.
10
+ *
11
+ * Use for updates which should be captured.
12
+ * Should be used for most of the local updates.
13
+ *
14
+ * These updates will _immediately_ make it to the local undo / redo stacks.
15
+ */
16
+ readonly CAPTURE: "capture";
17
+ /**
18
+ * Never undoable.
19
+ *
20
+ * Use for updates which should never be recorded, such as remote updates
21
+ * or scene initialization.
22
+ *
23
+ * These updates will _never_ make it to the local undo / redo stacks.
24
+ */
25
+ readonly UPDATE: "update";
26
+ /**
27
+ * Eventually undoable.
28
+ *
29
+ * Use for updates which should not be captured immediately - likely
30
+ * exceptions which are part of some async multi-step process. Otherwise, all
31
+ * such updates would end up being captured with the next
32
+ * `StoreAction.CAPTURE` - triggered either by the next `updateScene`
33
+ * or internally by the editor.
34
+ *
35
+ * These updates will _eventually_ make it to the local undo / redo stacks.
36
+ */
37
+ readonly NONE: "none";
9
38
  };
39
+ export type StoreActionType = ValueOf<typeof StoreAction>;
10
40
  /**
11
41
  * Represent an increment to the Store.
12
42
  */
@@ -25,8 +25,35 @@ export const getObservedAppState = (appState) => {
25
25
  };
26
26
  const isObservedAppState = (appState) => !!Reflect.get(appState, hiddenObservedAppStateProp);
27
27
  export const StoreAction = {
28
+ /**
29
+ * Immediately undoable.
30
+ *
31
+ * Use for updates which should be captured.
32
+ * Should be used for most of the local updates.
33
+ *
34
+ * These updates will _immediately_ make it to the local undo / redo stacks.
35
+ */
28
36
  CAPTURE: "capture",
37
+ /**
38
+ * Never undoable.
39
+ *
40
+ * Use for updates which should never be recorded, such as remote updates
41
+ * or scene initialization.
42
+ *
43
+ * These updates will _never_ make it to the local undo / redo stacks.
44
+ */
29
45
  UPDATE: "update",
46
+ /**
47
+ * Eventually undoable.
48
+ *
49
+ * Use for updates which should not be captured immediately - likely
50
+ * exceptions which are part of some async multi-step process. Otherwise, all
51
+ * such updates would end up being captured with the next
52
+ * `StoreAction.CAPTURE` - triggered either by the next `updateScene`
53
+ * or internally by the editor.
54
+ *
55
+ * These updates will _eventually_ make it to the local undo / redo stacks.
56
+ */
30
57
  NONE: "none",
31
58
  };
32
59
  /**
@@ -134,6 +134,7 @@ export type InteractiveCanvasAppState = Readonly<_CommonCanvasAppState & {
134
134
  collaborators: AppState["collaborators"];
135
135
  snapLines: AppState["snapLines"];
136
136
  zenModeEnabled: AppState["zenModeEnabled"];
137
+ editingElement: AppState["editingElement"];
137
138
  }>;
138
139
  export type ObservedAppState = ObservedStandaloneAppState & ObservedElementsAppState;
139
140
  export type ObservedStandaloneAppState = {