@excalidraw/excalidraw 0.17.1-1ed53b1 → 0.17.1-2f9526d

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 (76) 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-Q6NFAEKN.js} +156 -74
  4. package/dist/browser/dev/excalidraw-assets-dev/chunk-Q6NFAEKN.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-Y5X7K6KW.js} +2 -2
  9. package/dist/browser/dev/index.js +159 -86
  10. package/dist/browser/dev/index.js.map +3 -3
  11. package/dist/browser/prod/excalidraw-assets/{chunk-O4AI3NNG.js → chunk-IZMZ6RPD.js} +1 -1
  12. package/dist/browser/prod/excalidraw-assets/chunk-MDMKPHYD.js +55 -0
  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-7MVXYJUE.js +1 -0
  16. package/dist/browser/prod/index.js +14 -14
  17. package/dist/dev/{en-UQDDYCH7.json → en-OIPCBIOA.json} +3 -1
  18. package/dist/dev/index.js +320 -157
  19. package/dist/dev/index.js.map +4 -4
  20. package/dist/excalidraw/actions/actionBoundText.js +3 -1
  21. package/dist/excalidraw/actions/actionHistory.js +4 -4
  22. package/dist/excalidraw/actions/actionProperties.js +1 -1
  23. package/dist/excalidraw/actions/actionTextAutoResize.d.ts +17 -0
  24. package/dist/excalidraw/actions/actionTextAutoResize.js +38 -0
  25. package/dist/excalidraw/actions/types.d.ts +1 -1
  26. package/dist/excalidraw/components/Actions.js +1 -1
  27. package/dist/excalidraw/components/App.d.ts +1 -1
  28. package/dist/excalidraw/components/App.js +47 -32
  29. package/dist/excalidraw/components/ButtonIconSelect.js +1 -1
  30. package/dist/excalidraw/components/CheckboxItem.js +1 -1
  31. package/dist/excalidraw/components/CommandPalette/CommandPalette.js +2 -2
  32. package/dist/excalidraw/components/ContextMenu.js +1 -1
  33. package/dist/excalidraw/components/Dialog.js +1 -1
  34. package/dist/excalidraw/components/FollowMode/FollowMode.js +1 -1
  35. package/dist/excalidraw/components/IconPicker.js +2 -2
  36. package/dist/excalidraw/components/LayerUI.js +2 -2
  37. package/dist/excalidraw/components/MobileMenu.js +1 -1
  38. package/dist/excalidraw/components/PasteChartDialog.js +1 -1
  39. package/dist/excalidraw/components/canvases/InteractiveCanvas.d.ts +1 -1
  40. package/dist/excalidraw/components/canvases/InteractiveCanvas.js +2 -2
  41. package/dist/excalidraw/components/canvases/StaticCanvas.d.ts +1 -1
  42. package/dist/excalidraw/components/canvases/StaticCanvas.js +2 -2
  43. package/dist/excalidraw/components/icons.js +6 -2
  44. package/dist/excalidraw/data/restore.js +1 -0
  45. package/dist/excalidraw/element/index.d.ts +1 -1
  46. package/dist/excalidraw/element/index.js +1 -1
  47. package/dist/excalidraw/element/mutateElement.d.ts +1 -1
  48. package/dist/excalidraw/element/mutateElement.js +5 -3
  49. package/dist/excalidraw/element/newElement.d.ts +0 -5
  50. package/dist/excalidraw/element/newElement.js +15 -13
  51. package/dist/excalidraw/element/resizeElements.js +71 -22
  52. package/dist/excalidraw/element/resizeTest.js +2 -4
  53. package/dist/excalidraw/element/textElement.js +8 -3
  54. package/dist/excalidraw/element/textWysiwyg.d.ts +8 -3
  55. package/dist/excalidraw/element/textWysiwyg.js +6 -8
  56. package/dist/excalidraw/element/transformHandles.js +0 -10
  57. package/dist/excalidraw/element/types.d.ts +7 -0
  58. package/dist/excalidraw/locales/en.json +3 -1
  59. package/dist/excalidraw/scene/Fonts.d.ts +1 -3
  60. package/dist/excalidraw/scene/Fonts.js +6 -12
  61. package/dist/excalidraw/scene/Renderer.d.ts +1 -1
  62. package/dist/excalidraw/scene/Renderer.js +2 -3
  63. package/dist/excalidraw/scene/Scene.d.ts +10 -4
  64. package/dist/excalidraw/scene/Scene.js +14 -8
  65. package/dist/excalidraw/scene/export.js +1 -1
  66. package/dist/prod/{en-UQDDYCH7.json → en-OIPCBIOA.json} +3 -1
  67. package/dist/prod/index.js +28 -28
  68. package/package.json +2 -2
  69. package/dist/browser/dev/excalidraw-assets-dev/chunk-JKPJV7MZ.js.map +0 -7
  70. package/dist/browser/dev/excalidraw-assets-dev/chunk-OKAZAA6U.js.map +0 -7
  71. package/dist/browser/dev/excalidraw-assets-dev/dist-ITJNUBZF.js.map +0 -7
  72. package/dist/browser/prod/excalidraw-assets/chunk-SXBDZOS3.js +0 -55
  73. package/dist/browser/prod/excalidraw-assets/dist-54276HPL.js +0 -6
  74. package/dist/browser/prod/excalidraw-assets/image-VAGBVQ3G.js +0 -1
  75. /package/dist/browser/dev/excalidraw-assets-dev/{en-BF4XUPIZ.js.map → en-Y27YPU72.js.map} +0 -0
  76. /package/dist/browser/dev/excalidraw-assets-dev/{image-LVS32KQQ.js.map → image-Y5X7K6KW.js.map} +0 -0
@@ -1,6 +1,6 @@
1
1
  import { isInvisiblySmallElement } from "./sizeHelpers";
2
2
  import { isLinearElementType } from "./typeChecks";
3
- export { newElement, newTextElement, updateTextElement, refreshTextDimensions, newLinearElement, newImageElement, duplicateElement, } from "./newElement";
3
+ export { newElement, newTextElement, refreshTextDimensions, newLinearElement, newImageElement, duplicateElement, } from "./newElement";
4
4
  export { getElementAbsoluteCoords, getElementBounds, getCommonBounds, getDiamondPoints, getArrowheadPoints, getClosestElementBounds, } from "./bounds";
5
5
  export { OMIT_SIDES_FOR_MULTIPLE_ELEMENTS, getTransformHandlesFromCoords, getTransformHandles, } from "./transformHandles";
6
6
  export { resizeTest, getCursorForResizingElement, getElementWithTransformHandleType, getTransformHandleTypeFromCoords, } from "./resizeTest";
@@ -2,7 +2,7 @@ import type { ExcalidrawElement } from "./types";
2
2
  import type { Mutable } from "../utility-types";
3
3
  export type ElementUpdate<TElement extends ExcalidrawElement> = Omit<Partial<TElement>, "id" | "version" | "versionNonce" | "updated">;
4
4
  export declare const mutateElement: <TElement extends Mutable<ExcalidrawElement>>(element: TElement, updates: ElementUpdate<TElement>, informMutation?: boolean) => TElement;
5
- export declare const newElementWith: <TElement extends ExcalidrawElement>(element: TElement, updates: ElementUpdate<TElement>) => TElement;
5
+ export declare const newElementWith: <TElement extends ExcalidrawElement>(element: TElement, updates: ElementUpdate<TElement>, force?: boolean) => TElement;
6
6
  /**
7
7
  * Mutates element, bumping `version`, `versionNonce`, and `updated`.
8
8
  *
@@ -71,11 +71,13 @@ export const mutateElement = (element, updates, informMutation = true) => {
71
71
  element.versionNonce = randomInteger();
72
72
  element.updated = getUpdatedTimestamp();
73
73
  if (informMutation) {
74
- Scene.getScene(element)?.informMutation();
74
+ Scene.getScene(element)?.triggerUpdate();
75
75
  }
76
76
  return element;
77
77
  };
78
- export const newElementWith = (element, updates) => {
78
+ export const newElementWith = (element, updates,
79
+ /** pass `true` to always regenerate */
80
+ force = false) => {
79
81
  let didChange = false;
80
82
  for (const key in updates) {
81
83
  const value = updates[key];
@@ -88,7 +90,7 @@ export const newElementWith = (element, updates) => {
88
90
  didChange = true;
89
91
  }
90
92
  }
91
- if (!didChange) {
93
+ if (!didChange && !force) {
92
94
  return element;
93
95
  }
94
96
  return {
@@ -34,11 +34,6 @@ export declare const refreshTextDimensions: (textElement: ExcalidrawTextElement,
34
34
  height: number;
35
35
  text: string;
36
36
  } | undefined;
37
- export declare const updateTextElement: (textElement: ExcalidrawTextElement, container: ExcalidrawTextContainer | null, elementsMap: ElementsMap, { text, isDeleted, originalText, }: {
38
- text: string;
39
- isDeleted?: boolean | undefined;
40
- originalText: string;
41
- }) => ExcalidrawTextElement;
42
37
  export declare const newFreeDrawElement: (opts: {
43
38
  type: "freedraw";
44
39
  points?: ExcalidrawFreeDrawElement["points"];
@@ -85,7 +85,7 @@ export const newTextElement = (opts) => {
85
85
  const textAlign = opts.textAlign || DEFAULT_TEXT_ALIGN;
86
86
  const verticalAlign = opts.verticalAlign || DEFAULT_VERTICAL_ALIGN;
87
87
  const offsets = getTextElementPositionOffsets({ textAlign, verticalAlign }, metrics);
88
- const textElement = newElementWith({
88
+ const textElementProps = {
89
89
  ..._newElementBase("text", opts),
90
90
  text,
91
91
  fontSize,
@@ -98,18 +98,25 @@ export const newTextElement = (opts) => {
98
98
  height: metrics.height,
99
99
  containerId: opts.containerId || null,
100
100
  originalText: text,
101
+ autoResize: true,
101
102
  lineHeight,
102
- }, {});
103
+ };
104
+ const textElement = newElementWith(textElementProps, {});
103
105
  return textElement;
104
106
  };
105
107
  const getAdjustedDimensions = (element, elementsMap, nextText) => {
106
- const { width: nextWidth, height: nextHeight } = measureText(nextText, getFontString(element), element.lineHeight);
108
+ let { width: nextWidth, height: nextHeight } = measureText(nextText, getFontString(element), element.lineHeight);
109
+ // wrapped text
110
+ if (!element.autoResize) {
111
+ nextWidth = element.width;
112
+ }
107
113
  const { textAlign, verticalAlign } = element;
108
114
  let x;
109
115
  let y;
110
116
  if (textAlign === "center" &&
111
117
  verticalAlign === VERTICAL_ALIGN.MIDDLE &&
112
- !element.containerId) {
118
+ !element.containerId &&
119
+ element.autoResize) {
113
120
  const prevMetrics = measureText(element.text, getFontString(element), element.lineHeight);
114
121
  const offsets = getTextElementPositionOffsets(element, {
115
122
  width: nextWidth - prevMetrics.width,
@@ -142,19 +149,14 @@ export const refreshTextDimensions = (textElement, container, elementsMap, text
142
149
  if (textElement.isDeleted) {
143
150
  return;
144
151
  }
145
- if (container) {
146
- text = wrapText(text, getFontString(textElement), getBoundTextMaxWidth(container, textElement));
152
+ if (container || !textElement.autoResize) {
153
+ text = wrapText(text, getFontString(textElement), container
154
+ ? getBoundTextMaxWidth(container, textElement)
155
+ : textElement.width);
147
156
  }
148
157
  const dimensions = getAdjustedDimensions(textElement, elementsMap, text);
149
158
  return { text, ...dimensions };
150
159
  };
151
- export const updateTextElement = (textElement, container, elementsMap, { text, isDeleted, originalText, }) => {
152
- return newElementWith(textElement, {
153
- originalText,
154
- isDeleted: isDeleted ?? textElement.isDeleted,
155
- ...refreshTextDimensions(textElement, container, elementsMap, originalText),
156
- });
157
- };
158
160
  export const newFreeDrawElement = (opts) => {
159
161
  return {
160
162
  ..._newElementBase(opts.type, opts),
@@ -1,4 +1,4 @@
1
- import { MIN_FONT_SIZE, SHIFT_LOCKING_ANGLE } from "../constants";
1
+ import { BOUND_TEXT_PADDING, MIN_FONT_SIZE, SHIFT_LOCKING_ANGLE, } from "../constants";
2
2
  import { rescalePoints } from "../points";
3
3
  import { rotate, centerPoint, rotatePoint } from "../math";
4
4
  import { getElementAbsoluteCoords, getCommonBounds, getResizedElementAbsoluteCoords, getCommonBoundingBox, } from "./bounds";
@@ -7,7 +7,7 @@ import { mutateElement } from "./mutateElement";
7
7
  import { getFontString } from "../utils";
8
8
  import { updateBoundElements } from "./binding";
9
9
  import Scene from "../scene/Scene";
10
- import { getApproxMinLineWidth, getBoundTextElement, getBoundTextElementId, getContainerElement, handleBindTextResize, getBoundTextMaxWidth, getApproxMinLineHeight, } from "./textElement";
10
+ import { getApproxMinLineWidth, getBoundTextElement, getBoundTextElementId, getContainerElement, handleBindTextResize, getBoundTextMaxWidth, getApproxMinLineHeight, wrapText, measureText, getMinCharWidth, } from "./textElement";
11
11
  import { LinearElementEditor } from "./linearElementEditor";
12
12
  import { isInGroup } from "../groups";
13
13
  export const normalizeAngle = (angle) => {
@@ -27,12 +27,8 @@ export const transformElements = (originalElements, transformHandleType, selecte
27
27
  rotateSingleElement(element, elementsMap, pointerX, pointerY, shouldRotateWithDiscreteAngle);
28
28
  updateBoundElements(element, elementsMap);
29
29
  }
30
- else if (isTextElement(element) &&
31
- (transformHandleType === "nw" ||
32
- transformHandleType === "ne" ||
33
- transformHandleType === "sw" ||
34
- transformHandleType === "se")) {
35
- resizeSingleTextElement(element, elementsMap, transformHandleType, shouldResizeFromCenter, pointerX, pointerY);
30
+ else if (isTextElement(element) && transformHandleType) {
31
+ resizeSingleTextElement(originalElements, element, elementsMap, transformHandleType, shouldResizeFromCenter, pointerX, pointerY);
36
32
  updateBoundElements(element, elementsMap);
37
33
  }
38
34
  else if (transformHandleType) {
@@ -100,23 +96,25 @@ const measureFontSizeFromWidth = (element, elementsMap, nextWidth) => {
100
96
  size: nextFontSize,
101
97
  };
102
98
  };
103
- const resizeSingleTextElement = (element, elementsMap, transformHandleType, shouldResizeFromCenter, pointerX, pointerY) => {
99
+ const resizeSingleTextElement = (originalElements, element, elementsMap, transformHandleType, shouldResizeFromCenter, pointerX, pointerY) => {
104
100
  const [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords(element, elementsMap);
105
101
  // rotation pointer with reverse angle
106
102
  const [rotatedX, rotatedY] = rotate(pointerX, pointerY, cx, cy, -element.angle);
107
103
  let scaleX = 0;
108
104
  let scaleY = 0;
109
- if (transformHandleType.includes("e")) {
110
- scaleX = (rotatedX - x1) / (x2 - x1);
111
- }
112
- if (transformHandleType.includes("w")) {
113
- scaleX = (x2 - rotatedX) / (x2 - x1);
114
- }
115
- if (transformHandleType.includes("n")) {
116
- scaleY = (y2 - rotatedY) / (y2 - y1);
117
- }
118
- if (transformHandleType.includes("s")) {
119
- scaleY = (rotatedY - y1) / (y2 - y1);
105
+ if (transformHandleType !== "e" && transformHandleType !== "w") {
106
+ if (transformHandleType.includes("e")) {
107
+ scaleX = (rotatedX - x1) / (x2 - x1);
108
+ }
109
+ if (transformHandleType.includes("w")) {
110
+ scaleX = (x2 - rotatedX) / (x2 - x1);
111
+ }
112
+ if (transformHandleType.includes("n")) {
113
+ scaleY = (y2 - rotatedY) / (y2 - y1);
114
+ }
115
+ if (transformHandleType.includes("s")) {
116
+ scaleY = (rotatedY - y1) / (y2 - y1);
117
+ }
120
118
  }
121
119
  const scale = Math.max(scaleX, scaleY);
122
120
  if (scale > 0) {
@@ -171,6 +169,57 @@ const resizeSingleTextElement = (element, elementsMap, transformHandleType, shou
171
169
  y: nextY,
172
170
  });
173
171
  }
172
+ if (transformHandleType === "e" || transformHandleType === "w") {
173
+ const stateAtResizeStart = originalElements.get(element.id);
174
+ const [x1, y1, x2, y2] = getResizedElementAbsoluteCoords(stateAtResizeStart, stateAtResizeStart.width, stateAtResizeStart.height, true);
175
+ const startTopLeft = [x1, y1];
176
+ const startBottomRight = [x2, y2];
177
+ const startCenter = centerPoint(startTopLeft, startBottomRight);
178
+ const rotatedPointer = rotatePoint([pointerX, pointerY], startCenter, -stateAtResizeStart.angle);
179
+ const [esx1, , esx2] = getResizedElementAbsoluteCoords(element, element.width, element.height, true);
180
+ const boundsCurrentWidth = esx2 - esx1;
181
+ const atStartBoundsWidth = startBottomRight[0] - startTopLeft[0];
182
+ const minWidth = getMinCharWidth(getFontString(element)) + BOUND_TEXT_PADDING * 2;
183
+ let scaleX = atStartBoundsWidth / boundsCurrentWidth;
184
+ if (transformHandleType.includes("e")) {
185
+ scaleX = (rotatedPointer[0] - startTopLeft[0]) / boundsCurrentWidth;
186
+ }
187
+ if (transformHandleType.includes("w")) {
188
+ scaleX = (startBottomRight[0] - rotatedPointer[0]) / boundsCurrentWidth;
189
+ }
190
+ const newWidth = element.width * scaleX < minWidth ? minWidth : element.width * scaleX;
191
+ const text = wrapText(element.originalText, getFontString(element), Math.abs(newWidth));
192
+ const metrics = measureText(text, getFontString(element), element.lineHeight);
193
+ const eleNewHeight = metrics.height;
194
+ const [newBoundsX1, newBoundsY1, newBoundsX2, newBoundsY2] = getResizedElementAbsoluteCoords(stateAtResizeStart, newWidth, eleNewHeight, true);
195
+ const newBoundsWidth = newBoundsX2 - newBoundsX1;
196
+ const newBoundsHeight = newBoundsY2 - newBoundsY1;
197
+ let newTopLeft = [...startTopLeft];
198
+ if (["n", "w", "nw"].includes(transformHandleType)) {
199
+ newTopLeft = [
200
+ startBottomRight[0] - Math.abs(newBoundsWidth),
201
+ startTopLeft[1],
202
+ ];
203
+ }
204
+ // adjust topLeft to new rotation point
205
+ const angle = stateAtResizeStart.angle;
206
+ const rotatedTopLeft = rotatePoint(newTopLeft, startCenter, angle);
207
+ const newCenter = [
208
+ newTopLeft[0] + Math.abs(newBoundsWidth) / 2,
209
+ newTopLeft[1] + Math.abs(newBoundsHeight) / 2,
210
+ ];
211
+ const rotatedNewCenter = rotatePoint(newCenter, startCenter, angle);
212
+ newTopLeft = rotatePoint(rotatedTopLeft, rotatedNewCenter, -angle);
213
+ const resizedElement = {
214
+ width: Math.abs(newWidth),
215
+ height: Math.abs(metrics.height),
216
+ x: newTopLeft[0],
217
+ y: newTopLeft[1],
218
+ text,
219
+ autoResize: false,
220
+ };
221
+ mutateElement(element, resizedElement);
222
+ }
174
223
  };
175
224
  export const resizeSingleElement = (originalElements, shouldMaintainAspectRatio, element, elementsMap, transformHandleDirection, shouldResizeFromCenter, pointerX, pointerY) => {
176
225
  const stateAtResizeStart = originalElements.get(element.id);
@@ -541,7 +590,7 @@ export const resizeMultipleElements = (originalElements, selectedElements, eleme
541
590
  handleBindTextResize(element, elementsMap, transformHandleType, true);
542
591
  }
543
592
  }
544
- Scene.getScene(elementsAndUpdates[0].element)?.informMutation();
593
+ Scene.getScene(elementsAndUpdates[0].element)?.triggerUpdate();
545
594
  };
546
595
  const rotateMultipleElements = (originalElements, elements, elementsMap, pointerX, pointerY, shouldRotateWithDiscreteAngle, centerX, centerY) => {
547
596
  let centerAngle = (5 * Math.PI) / 2 + Math.atan2(pointerY - centerY, pointerX - centerX);
@@ -574,7 +623,7 @@ const rotateMultipleElements = (originalElements, elements, elementsMap, pointer
574
623
  }, false);
575
624
  }
576
625
  });
577
- Scene.getScene(elements[0])?.informMutation();
626
+ Scene.getScene(elements[0])?.triggerUpdate();
578
627
  };
579
628
  export const getResizeOffsetXY = (transformHandleType, selectedElements, elementsMap, x, y) => {
580
629
  const [x1, y1, x2, y2] = selectedElements.length === 1
@@ -28,10 +28,8 @@ export const resizeTest = (element, elementsMap, appState, x, y, zoom, pointerTy
28
28
  }
29
29
  if (canResizeFromSides(device)) {
30
30
  const [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords(element, elementsMap);
31
- // Note that for a text element, when "resized" from the side
32
- // we should make it wrap/unwrap
33
- if (element.type !== "text" &&
34
- !(isLinearElement(element) && element.points.length <= 2)) {
31
+ // do not resize from the sides for linear elements with only two points
32
+ if (!(isLinearElement(element) && element.points.length <= 2)) {
35
33
  const SPACING = SIDE_RESIZING_THRESHOLD / zoom.value;
36
34
  const sides = getSelectionBorders([x1 - SPACING, y1 - SPACING], [x2 + SPACING, y2 + SPACING], [cx, cy], angleToDegrees(element.angle));
37
35
  for (const [dir, side] of Object.entries(sides)) {
@@ -24,12 +24,17 @@ export const redrawTextBoundingBox = (textElement, container, elementsMap, infor
24
24
  angle: container?.angle ?? textElement.angle,
25
25
  };
26
26
  boundTextUpdates.text = textElement.text;
27
- if (container) {
28
- maxWidth = getBoundTextMaxWidth(container, textElement);
27
+ if (container || !textElement.autoResize) {
28
+ maxWidth = container
29
+ ? getBoundTextMaxWidth(container, textElement)
30
+ : textElement.width;
29
31
  boundTextUpdates.text = wrapText(textElement.originalText, getFontString(textElement), maxWidth);
30
32
  }
31
33
  const metrics = measureText(boundTextUpdates.text, getFontString(textElement), textElement.lineHeight);
32
- boundTextUpdates.width = metrics.width;
34
+ // Note: only update width for unwrapped text and bound texts (which always have autoResize set to true)
35
+ if (textElement.autoResize) {
36
+ boundTextUpdates.width = metrics.width;
37
+ }
33
38
  boundTextUpdates.height = metrics.height;
34
39
  if (container) {
35
40
  const maxContainerHeight = getBoundTextMaxHeight(container, textElement);
@@ -2,11 +2,16 @@ import type { ExcalidrawElement, ExcalidrawTextElement } from "./types";
2
2
  import type App from "../components/App";
3
3
  export declare const textWysiwyg: ({ id, onChange, onSubmit, getViewportCoords, element, canvas, excalidrawContainer, app, }: {
4
4
  id: ExcalidrawElement["id"];
5
- onChange?: ((text: string) => void) | undefined;
5
+ /**
6
+ * textWysiwyg only deals with `originalText`
7
+ *
8
+ * Note: `text`, which can be wrapped and therefore different from `originalText`,
9
+ * is derived from `originalText`
10
+ */
11
+ onChange?: ((nextOriginalText: string) => void) | undefined;
6
12
  onSubmit: (data: {
7
- text: string;
8
13
  viaKeyboard: boolean;
9
- originalText: string;
14
+ nextOriginalText: string;
10
15
  }) => void;
11
16
  getViewportCoords: (x: number, y: number) => [number, number];
12
17
  element: ExcalidrawTextElement;
@@ -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
  }
@@ -334,10 +335,8 @@ export const textWysiwyg = ({ id, onChange, onSubmit, getViewportCoords, element
334
335
  if (!updateElement) {
335
336
  return;
336
337
  }
337
- let text = editable.value;
338
338
  const container = getContainerElement(updateElement, app.scene.getNonDeletedElementsMap());
339
339
  if (container) {
340
- text = updateElement.text;
341
340
  if (editable.value.trim()) {
342
341
  const boundTextElementId = getBoundTextElementId(container);
343
342
  if (!boundTextElementId || boundTextElementId !== element.id) {
@@ -361,9 +360,8 @@ export const textWysiwyg = ({ id, onChange, onSubmit, getViewportCoords, element
361
360
  redrawTextBoundingBox(updateElement, container, app.scene.getNonDeletedElementsMap());
362
361
  }
363
362
  onSubmit({
364
- text,
365
363
  viaKeyboard: submittedViaKeyboard,
366
- originalText: editable.value,
364
+ nextOriginalText: editable.value,
367
365
  });
368
366
  };
369
367
  const cleanup = () => {
@@ -441,7 +439,7 @@ export const textWysiwyg = ({ id, onChange, onSubmit, getViewportCoords, element
441
439
  }
442
440
  };
443
441
  // handle updates of textElement properties of editing element
444
- const unbindUpdate = Scene.getScene(element).addCallback(() => {
442
+ const unbindUpdate = Scene.getScene(element).onUpdate(() => {
445
443
  updateWysiwygStyle();
446
444
  const isColorPickerActive = !!document.activeElement?.closest(".color-picker-content");
447
445
  if (!isColorPickerActive) {
@@ -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).
@@ -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...",
@@ -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
  }
@@ -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...",