@tldraw/editor 3.14.0-canary.f907ed7d9ee5 → 3.14.0-canary.faba3f64c07f

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 (54) hide show
  1. package/dist-cjs/index.d.ts +133 -50
  2. package/dist-cjs/index.js +4 -1
  3. package/dist-cjs/index.js.map +2 -2
  4. package/dist-cjs/lib/editor/Editor.js +60 -22
  5. package/dist-cjs/lib/editor/Editor.js.map +2 -2
  6. package/dist-cjs/lib/editor/managers/TextManager/TextManager.js +73 -42
  7. package/dist-cjs/lib/editor/managers/TextManager/TextManager.js.map +2 -2
  8. package/dist-cjs/lib/editor/shapes/ShapeUtil.js +0 -10
  9. package/dist-cjs/lib/editor/shapes/ShapeUtil.js.map +2 -2
  10. package/dist-cjs/lib/editor/tools/StateNode.js +3 -3
  11. package/dist-cjs/lib/editor/tools/StateNode.js.map +2 -2
  12. package/dist-cjs/lib/editor/types/emit-types.js.map +1 -1
  13. package/dist-cjs/lib/editor/types/external-content.js.map +1 -1
  14. package/dist-cjs/lib/hooks/useCanvasEvents.js +1 -2
  15. package/dist-cjs/lib/hooks/useCanvasEvents.js.map +2 -2
  16. package/dist-cjs/lib/primitives/geometry/Geometry2d.js +6 -2
  17. package/dist-cjs/lib/primitives/geometry/Geometry2d.js.map +2 -2
  18. package/dist-cjs/lib/utils/reparenting.js +232 -0
  19. package/dist-cjs/lib/utils/reparenting.js.map +7 -0
  20. package/dist-cjs/version.js +3 -3
  21. package/dist-cjs/version.js.map +1 -1
  22. package/dist-esm/index.d.mts +133 -50
  23. package/dist-esm/index.mjs +4 -1
  24. package/dist-esm/index.mjs.map +2 -2
  25. package/dist-esm/lib/editor/Editor.mjs +60 -22
  26. package/dist-esm/lib/editor/Editor.mjs.map +2 -2
  27. package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs +73 -42
  28. package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs.map +2 -2
  29. package/dist-esm/lib/editor/shapes/ShapeUtil.mjs +0 -10
  30. package/dist-esm/lib/editor/shapes/ShapeUtil.mjs.map +2 -2
  31. package/dist-esm/lib/editor/tools/StateNode.mjs +3 -3
  32. package/dist-esm/lib/editor/tools/StateNode.mjs.map +2 -2
  33. package/dist-esm/lib/hooks/useCanvasEvents.mjs +1 -2
  34. package/dist-esm/lib/hooks/useCanvasEvents.mjs.map +2 -2
  35. package/dist-esm/lib/primitives/geometry/Geometry2d.mjs +6 -2
  36. package/dist-esm/lib/primitives/geometry/Geometry2d.mjs.map +2 -2
  37. package/dist-esm/lib/utils/reparenting.mjs +216 -0
  38. package/dist-esm/lib/utils/reparenting.mjs.map +7 -0
  39. package/dist-esm/version.mjs +3 -3
  40. package/dist-esm/version.mjs.map +1 -1
  41. package/editor.css +442 -492
  42. package/package.json +7 -7
  43. package/src/index.ts +7 -0
  44. package/src/lib/editor/Editor.ts +73 -30
  45. package/src/lib/editor/managers/TextManager/TextManager.test.ts +1 -5
  46. package/src/lib/editor/managers/TextManager/TextManager.ts +118 -86
  47. package/src/lib/editor/shapes/ShapeUtil.ts +47 -15
  48. package/src/lib/editor/tools/StateNode.ts +3 -3
  49. package/src/lib/editor/types/emit-types.ts +4 -0
  50. package/src/lib/editor/types/external-content.ts +11 -2
  51. package/src/lib/hooks/useCanvasEvents.ts +0 -1
  52. package/src/lib/primitives/geometry/Geometry2d.ts +7 -2
  53. package/src/lib/utils/reparenting.ts +383 -0
  54. package/src/version.ts +3 -3
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tldraw/editor",
3
3
  "description": "A tiny little drawing app (editor).",
4
- "version": "3.14.0-canary.f907ed7d9ee5",
4
+ "version": "3.14.0-canary.faba3f64c07f",
5
5
  "author": {
6
6
  "name": "tldraw Inc.",
7
7
  "email": "hello@tldraw.com"
@@ -48,12 +48,12 @@
48
48
  "@tiptap/core": "^2.9.1",
49
49
  "@tiptap/pm": "^2.9.1",
50
50
  "@tiptap/react": "^2.9.1",
51
- "@tldraw/state": "3.14.0-canary.f907ed7d9ee5",
52
- "@tldraw/state-react": "3.14.0-canary.f907ed7d9ee5",
53
- "@tldraw/store": "3.14.0-canary.f907ed7d9ee5",
54
- "@tldraw/tlschema": "3.14.0-canary.f907ed7d9ee5",
55
- "@tldraw/utils": "3.14.0-canary.f907ed7d9ee5",
56
- "@tldraw/validate": "3.14.0-canary.f907ed7d9ee5",
51
+ "@tldraw/state": "3.14.0-canary.faba3f64c07f",
52
+ "@tldraw/state-react": "3.14.0-canary.faba3f64c07f",
53
+ "@tldraw/store": "3.14.0-canary.faba3f64c07f",
54
+ "@tldraw/tlschema": "3.14.0-canary.faba3f64c07f",
55
+ "@tldraw/utils": "3.14.0-canary.faba3f64c07f",
56
+ "@tldraw/validate": "3.14.0-canary.faba3f64c07f",
57
57
  "@types/core-js": "^2.5.8",
58
58
  "@use-gesture/react": "^10.3.1",
59
59
  "classnames": "^2.5.1",
package/src/index.ts CHANGED
@@ -174,6 +174,7 @@ export {
174
174
  } from './lib/editor/managers/SnapManager/SnapManager'
175
175
  export {
176
176
  TextManager,
177
+ type TLMeasureTextOpts,
177
178
  type TLMeasureTextSpanOpts,
178
179
  } from './lib/editor/managers/TextManager/TextManager'
179
180
  export { UserPreferencesManager } from './lib/editor/managers/UserPreferencesManager/UserPreferencesManager'
@@ -181,6 +182,10 @@ export { BaseBoxShapeUtil, type TLBaseBoxShape } from './lib/editor/shapes/BaseB
181
182
  export {
182
183
  ShapeUtil,
183
184
  type TLCropInfo,
185
+ type TLDragShapesInInfo,
186
+ type TLDragShapesOutInfo,
187
+ type TLDragShapesOverInfo,
188
+ type TLDropShapesOverInfo,
184
189
  type TLGeometryOpts,
185
190
  type TLHandleDragInfo,
186
191
  type TLResizeInfo,
@@ -252,6 +257,7 @@ export {
252
257
  type TLExternalContent,
253
258
  type TLExternalContentSource,
254
259
  type TLFileExternalAsset,
260
+ type TLFileReplaceExternalContent,
255
261
  type TLFilesExternalContent,
256
262
  type TLSvgTextExternalContent,
257
263
  type TLTextExternalContent,
@@ -444,6 +450,7 @@ export { hardResetEditor } from './lib/utils/hardResetEditor'
444
450
  export { isAccelKey } from './lib/utils/keyboard'
445
451
  export { normalizeWheel } from './lib/utils/normalizeWheel'
446
452
  export { refreshPage } from './lib/utils/refreshPage'
453
+ export { getDroppedShapesToNewParents, kickoutOccludedShapes } from './lib/utils/reparenting'
447
454
  export {
448
455
  getFontsFromRichText,
449
456
  type RichTextFontVisitor,
@@ -348,6 +348,8 @@ export class Editor extends EventEmitter<TLEventMap> {
348
348
  this.getContainer = getContainer
349
349
 
350
350
  this.textMeasure = new TextManager(this)
351
+ this.disposables.add(() => this.textMeasure.dispose())
352
+
351
353
  this.fonts = new FontManager(this, fontAssetUrls)
352
354
 
353
355
  this._tickManager = new TickManager(this)
@@ -2120,6 +2122,20 @@ export class Editor extends EventEmitter<TLEventMap> {
2120
2122
  return this.getShapesPageBounds(this.getSelectedShapeIds())
2121
2123
  }
2122
2124
 
2125
+ /**
2126
+ * The bounds of the selection bounding box in the current page space.
2127
+ *
2128
+ * @readonly
2129
+ * @public
2130
+ */
2131
+ getSelectionScreenBounds(): Box | undefined {
2132
+ const bounds = this.getSelectionPageBounds()
2133
+ if (!bounds) return undefined
2134
+ const { x, y } = this.pageToScreen(bounds.point)
2135
+ const zoom = this.getZoomLevel()
2136
+ return new Box(x, y, bounds.width * zoom, bounds.height * zoom)
2137
+ }
2138
+
2123
2139
  /**
2124
2140
  * @internal
2125
2141
  */
@@ -5512,7 +5528,7 @@ export class Editor extends EventEmitter<TLEventMap> {
5512
5528
  if (!id) return undefined
5513
5529
  const freshShape = this.getShape(id)
5514
5530
  if (freshShape === undefined || !isShapeId(freshShape.parentId)) return undefined
5515
- return this.store.get(freshShape.parentId)
5531
+ return this.getShape(freshShape.parentId)
5516
5532
  }
5517
5533
 
5518
5534
  /**
@@ -5695,6 +5711,10 @@ export class Editor extends EventEmitter<TLEventMap> {
5695
5711
  const newPoint = invertedParentTransform.applyToPoint(pagePoint)
5696
5712
  const newRotation = pageTransform.rotation() - parentPageRotation
5697
5713
 
5714
+ if (shape.id === parentId) {
5715
+ throw Error('Attempted to reparent a shape to itself!')
5716
+ }
5717
+
5698
5718
  changes.push({
5699
5719
  id: shape.id,
5700
5720
  type: shape.type,
@@ -5798,6 +5818,11 @@ export class Editor extends EventEmitter<TLEventMap> {
5798
5818
  return shapeIds
5799
5819
  }
5800
5820
 
5821
+ /** @deprecated Use {@link Editor.getDraggingOverShape} instead */
5822
+ getDroppingOverShape(point: Vec, droppingShapes: TLShape[]): TLShape | undefined {
5823
+ return this.getDraggingOverShape(point, droppingShapes)
5824
+ }
5825
+
5801
5826
  /**
5802
5827
  * Get the shape that some shapes should be dropped on at a given point.
5803
5828
  *
@@ -5808,35 +5833,33 @@ export class Editor extends EventEmitter<TLEventMap> {
5808
5833
  *
5809
5834
  * @public
5810
5835
  */
5811
- getDroppingOverShape(point: VecLike, droppingShapes: TLShape[] = []) {
5812
- // starting from the top...
5813
- const currentPageShapesSorted = this.getCurrentPageShapesSorted()
5814
- for (let i = currentPageShapesSorted.length - 1; i >= 0; i--) {
5815
- const shape = currentPageShapesSorted[i]
5816
-
5817
- if (
5818
- // ignore hidden shapes
5819
- this.isShapeHidden(shape) ||
5820
- // don't allow dropping on selected shapes
5821
- this.getSelectedShapeIds().includes(shape.id) ||
5822
- // only allow shapes that can receive children
5823
- !this.getShapeUtil(shape).canDropShapes(shape, droppingShapes) ||
5824
- // don't allow dropping a shape on itself or one of it's children
5825
- droppingShapes.find((s) => s.id === shape.id || this.hasAncestor(shape, s.id))
5826
- ) {
5827
- continue
5828
- }
5836
+ getDraggingOverShape(point: Vec, droppingShapes: TLShape[]): TLShape | undefined {
5837
+ // get fresh moving shapes
5838
+ const draggingShapes = compact(droppingShapes.map((s) => this.getShape(s))).filter(
5839
+ (s) => !s.isLocked && !this.isShapeHidden(s)
5840
+ )
5829
5841
 
5830
- // Only allow dropping into the masked page bounds of the shape, e.g. when a frame is
5831
- // partially clipped by its own parent frame
5832
- const maskedPageBounds = this.getShapeMaskedPageBounds(shape.id)
5842
+ const maybeDraggingOverShapes = this.getShapesAtPoint(point, {
5843
+ hitInside: true,
5844
+ margin: 0,
5845
+ }).filter(
5846
+ (s) =>
5847
+ !droppingShapes.includes(s) &&
5848
+ !s.isLocked &&
5849
+ !this.isShapeHidden(s) &&
5850
+ !draggingShapes.includes(s)
5851
+ )
5833
5852
 
5853
+ for (const maybeDraggingOverShape of maybeDraggingOverShapes) {
5854
+ const shapeUtil = this.getShapeUtil(maybeDraggingOverShape)
5855
+ // Any shape that can handle any dragging interactions is a valid target
5834
5856
  if (
5835
- maskedPageBounds &&
5836
- maskedPageBounds.containsPoint(point) &&
5837
- this.getShapeGeometry(shape).hitTestPoint(this.getPointInShapeSpace(shape, point), 0, true)
5857
+ shapeUtil.onDragShapesOver ||
5858
+ shapeUtil.onDragShapesIn ||
5859
+ shapeUtil.onDragShapesOut ||
5860
+ shapeUtil.onDropShapesOver
5838
5861
  ) {
5839
- return shape
5862
+ return maybeDraggingOverShape
5840
5863
  }
5841
5864
  }
5842
5865
  }
@@ -6195,11 +6218,12 @@ export class Editor extends EventEmitter<TLEventMap> {
6195
6218
  */
6196
6219
  duplicateShapes(shapes: TLShapeId[] | TLShape[], offset?: VecLike): this {
6197
6220
  this.run(() => {
6198
- const ids =
6221
+ const _ids =
6199
6222
  typeof shapes[0] === 'string'
6200
6223
  ? (shapes as TLShapeId[])
6201
6224
  : (shapes as TLShape[]).map((s) => s.id)
6202
6225
 
6226
+ const ids = this._shouldIgnoreShapeLock ? _ids : this._getUnlockedShapeIds(_ids)
6203
6227
  if (ids.length <= 0) return this
6204
6228
 
6205
6229
  const initialIds = new Set(ids)
@@ -7789,9 +7813,10 @@ export class Editor extends EventEmitter<TLEventMap> {
7789
7813
 
7790
7814
  for (let i = currentPageShapesSorted.length - 1; i >= 0; i--) {
7791
7815
  const parent = currentPageShapesSorted[i]
7816
+ const util = this.getShapeUtil(parent)
7792
7817
  if (
7818
+ util.canReceiveNewChildrenOfType(parent, partial.type) &&
7793
7819
  !this.isShapeHidden(parent) &&
7794
- this.getShapeUtil(parent).canReceiveNewChildrenOfType(parent, partial.type) &&
7795
7820
  this.isPointInShape(
7796
7821
  parent,
7797
7822
  // If no parent is provided, then we can treat the
@@ -7920,6 +7945,8 @@ export class Editor extends EventEmitter<TLEventMap> {
7920
7945
  }
7921
7946
  })
7922
7947
 
7948
+ this.emit('created-shapes', shapeRecordsToCreate)
7949
+ this.emit('edit')
7923
7950
  this.store.put(shapeRecordsToCreate)
7924
7951
  })
7925
7952
 
@@ -8314,6 +8341,8 @@ export class Editor extends EventEmitter<TLEventMap> {
8314
8341
  updates.push(updated)
8315
8342
  }
8316
8343
 
8344
+ this.emit('edited-shapes', updates)
8345
+ this.emit('edit')
8317
8346
  this.store.put(updates)
8318
8347
  })
8319
8348
  }
@@ -8363,6 +8392,8 @@ export class Editor extends EventEmitter<TLEventMap> {
8363
8392
  })
8364
8393
  }
8365
8394
 
8395
+ this.emit('deleted-shapes', [...allShapeIdsToDelete])
8396
+ this.emit('edit')
8366
8397
  return this.run(() => this.store.remove([...allShapeIdsToDelete]))
8367
8398
  }
8368
8399
 
@@ -8811,6 +8842,7 @@ export class Editor extends EventEmitter<TLEventMap> {
8811
8842
  } = {
8812
8843
  text: null,
8813
8844
  files: null,
8845
+ 'file-replace': null,
8814
8846
  embed: null,
8815
8847
  'svg-text': null,
8816
8848
  url: null,
@@ -8860,6 +8892,15 @@ export class Editor extends EventEmitter<TLEventMap> {
8860
8892
  return this.externalContentHandlers[info.type]?.(info as any)
8861
8893
  }
8862
8894
 
8895
+ /**
8896
+ * Handle replacing external content.
8897
+ *
8898
+ * @param info - Info about the external content.
8899
+ */
8900
+ async replaceExternalContent<E>(info: TLExternalContent<E>): Promise<void> {
8901
+ return this.externalContentHandlers[info.type]?.(info as any)
8902
+ }
8903
+
8863
8904
  /**
8864
8905
  * Get content that can be exported for the given shape ids.
8865
8906
  *
@@ -9479,6 +9520,8 @@ export class Editor extends EventEmitter<TLEventMap> {
9479
9520
  previousPagePoint,
9480
9521
  currentScreenPoint,
9481
9522
  currentPagePoint,
9523
+ originScreenPoint,
9524
+ originPagePoint,
9482
9525
  } = this.inputs
9483
9526
 
9484
9527
  const { screenBounds } = this.store.unsafeGetWithoutCapture(TLINSTANCE_ID)!
@@ -9507,8 +9550,8 @@ export class Editor extends EventEmitter<TLEventMap> {
9507
9550
  // Reset velocity on pointer down, or when a pinch starts or ends
9508
9551
  if (info.name === 'pointer_down' || this.inputs.isPinching) {
9509
9552
  pointerVelocity.set(0, 0)
9510
- this.inputs.originScreenPoint.setTo(currentScreenPoint)
9511
- this.inputs.originPagePoint.setTo(currentPagePoint)
9553
+ originScreenPoint.setTo(currentScreenPoint)
9554
+ originPagePoint.setTo(currentPagePoint)
9512
9555
  }
9513
9556
 
9514
9557
  // todo: We only have to do this if there are multiple users in the document
@@ -99,7 +99,7 @@ describe('TextManager', () => {
99
99
  })
100
100
 
101
101
  it('should handle empty text', () => {
102
- const result = textManager.measureText('', defaultOpts)
102
+ const result = textManager.measureText('', { ...defaultOpts, measureScrollWidth: true })
103
103
  expect(result).toHaveProperty('x', 0)
104
104
  expect(result).toHaveProperty('y', 0)
105
105
  expect(result).toHaveProperty('w')
@@ -128,7 +128,6 @@ describe('TextManager', () => {
128
128
  y: 0,
129
129
  w: expect.any(Number),
130
130
  h: expect.any(Number),
131
- scrollWidth: expect.any(Number),
132
131
  })
133
132
  })
134
133
 
@@ -141,7 +140,6 @@ describe('TextManager', () => {
141
140
  y: 0,
142
141
  w: expect.any(Number),
143
142
  h: expect.any(Number),
144
- scrollWidth: expect.any(Number),
145
143
  })
146
144
  })
147
145
 
@@ -154,7 +152,6 @@ describe('TextManager', () => {
154
152
  y: 0,
155
153
  w: expect.any(Number),
156
154
  h: expect.any(Number),
157
- scrollWidth: expect.any(Number),
158
155
  })
159
156
  })
160
157
 
@@ -173,7 +170,6 @@ describe('TextManager', () => {
173
170
  y: 0,
174
171
  w: expect.any(Number),
175
172
  h: expect.any(Number),
176
- scrollWidth: expect.any(Number),
177
173
  })
178
174
  })
179
175
  })
@@ -20,6 +20,28 @@ const textAlignmentsForLtr = {
20
20
  'end-legacy': 'right',
21
21
  }
22
22
 
23
+ /** @public */
24
+ export interface TLMeasureTextOpts {
25
+ fontStyle: string
26
+ fontWeight: string
27
+ fontFamily: string
28
+ fontSize: number
29
+ /** This must be a number, e.g. 1.35, not a pixel value. */
30
+ lineHeight: number
31
+ /**
32
+ * When maxWidth is a number, the text will be wrapped to that maxWidth. When maxWidth
33
+ * is null, the text will be measured without wrapping, but explicit line breaks and
34
+ * space are preserved.
35
+ */
36
+ maxWidth: null | number
37
+ minWidth?: null | number
38
+ // todo: make this a number so that it is consistent with other TLMeasureTextSpanOpts
39
+ padding: string
40
+ otherStyles?: Record<string, string>
41
+ disableOverflowWrapBreaking?: boolean
42
+ measureScrollWidth?: boolean
43
+ }
44
+
23
45
  /** @public */
24
46
  export interface TLMeasureTextSpanOpts {
25
47
  overflow: 'wrap' | 'truncate-ellipsis' | 'truncate-clip'
@@ -33,96 +55,99 @@ export interface TLMeasureTextSpanOpts {
33
55
  lineHeight: number
34
56
  textAlign: TLDefaultHorizontalAlignStyle
35
57
  otherStyles?: Record<string, string>
58
+ measureScrollWidth?: boolean
36
59
  }
37
60
 
38
61
  const spaceCharacterRegex = /\s/
39
62
 
40
63
  /** @public */
41
64
  export class TextManager {
42
- private baseElem: HTMLDivElement
65
+ private elm: HTMLDivElement
66
+ private defaultStyles: Record<string, string | null>
43
67
 
44
68
  constructor(public editor: Editor) {
45
- this.baseElem = document.createElement('div')
46
- this.baseElem.classList.add('tl-text')
47
- this.baseElem.classList.add('tl-text-measure')
48
- this.baseElem.tabIndex = -1
69
+ const elm = document.createElement('div')
70
+ elm.classList.add('tl-text')
71
+ elm.classList.add('tl-text-measure')
72
+ elm.setAttribute('dir', 'auto')
73
+ elm.tabIndex = -1
74
+ this.editor.getContainer().appendChild(elm)
75
+
76
+ // we need to save the default styles so that we can restore them when we're done
77
+ // these must be the css names, not the js names for the styles
78
+ this.defaultStyles = {
79
+ 'overflow-wrap': 'break-word',
80
+ 'word-break': 'auto',
81
+ width: null,
82
+ height: null,
83
+ 'max-width': null,
84
+ 'min-width': null,
85
+ }
86
+
87
+ this.elm = elm
49
88
  }
50
89
 
51
- measureText(
52
- textToMeasure: string,
53
- opts: {
54
- fontStyle: string
55
- fontWeight: string
56
- fontFamily: string
57
- fontSize: number
58
- lineHeight: number
59
- /**
60
- * When maxWidth is a number, the text will be wrapped to that maxWidth. When maxWidth
61
- * is null, the text will be measured without wrapping, but explicit line breaks and
62
- * space are preserved.
63
- */
64
- maxWidth: null | number
65
- minWidth?: null | number
66
- padding: string
67
- disableOverflowWrapBreaking?: boolean
90
+ dispose() {
91
+ return this.elm.remove()
92
+ }
93
+
94
+ private resetElmStyles() {
95
+ const { elm, defaultStyles } = this
96
+ for (const key in defaultStyles) {
97
+ elm.style.setProperty(key, defaultStyles[key])
68
98
  }
69
- ): BoxModel & { scrollWidth: number } {
99
+ }
100
+
101
+ measureText(textToMeasure: string, opts: TLMeasureTextOpts): BoxModel & { scrollWidth: number } {
70
102
  const div = document.createElement('div')
71
103
  div.textContent = normalizeTextForDom(textToMeasure)
72
104
  return this.measureHtml(div.innerHTML, opts)
73
105
  }
74
106
 
75
- measureHtml(
76
- html: string,
77
- opts: {
78
- fontStyle: string
79
- fontWeight: string
80
- fontFamily: string
81
- fontSize: number
82
- lineHeight: number
83
- /**
84
- * When maxWidth is a number, the text will be wrapped to that maxWidth. When maxWidth
85
- * is null, the text will be measured without wrapping, but explicit line breaks and
86
- * space are preserved.
87
- */
88
- maxWidth: null | number
89
- minWidth?: null | number
90
- otherStyles?: Record<string, string>
91
- padding: string
92
- disableOverflowWrapBreaking?: boolean
107
+ measureHtml(html: string, opts: TLMeasureTextOpts): BoxModel & { scrollWidth: number } {
108
+ const { elm } = this
109
+
110
+ if (opts.otherStyles) {
111
+ for (const key in opts.otherStyles) {
112
+ if (!this.defaultStyles[key]) {
113
+ // we need to save the original style so that we can restore it when we're done
114
+ this.defaultStyles[key] = elm.style.getPropertyValue(key)
115
+ }
116
+ }
93
117
  }
94
- ): BoxModel & { scrollWidth: number } {
95
- // Duplicate our base element; we don't need to clone deep
96
- const wrapperElm = this.baseElem.cloneNode() as HTMLDivElement
97
- this.editor.getContainer().appendChild(wrapperElm)
98
- wrapperElm.innerHTML = html
99
- this.baseElem.insertAdjacentElement('afterend', wrapperElm)
100
-
101
- wrapperElm.setAttribute('dir', 'auto')
102
- // N.B. This property, while discouraged ("intended for Document Type Definition (DTD) designers")
103
- // is necessary for ensuring correct mixed RTL/LTR behavior when exporting SVGs.
104
- wrapperElm.style.setProperty('unicode-bidi', 'plaintext')
105
- wrapperElm.style.setProperty('font-family', opts.fontFamily)
106
- wrapperElm.style.setProperty('font-style', opts.fontStyle)
107
- wrapperElm.style.setProperty('font-weight', opts.fontWeight)
108
- wrapperElm.style.setProperty('font-size', opts.fontSize + 'px')
109
- wrapperElm.style.setProperty('line-height', opts.lineHeight * opts.fontSize + 'px')
110
- wrapperElm.style.setProperty('max-width', opts.maxWidth === null ? null : opts.maxWidth + 'px')
111
- wrapperElm.style.setProperty('min-width', opts.minWidth === null ? null : opts.minWidth + 'px')
112
- wrapperElm.style.setProperty('padding', opts.padding)
113
- wrapperElm.style.setProperty(
114
- 'overflow-wrap',
115
- opts.disableOverflowWrapBreaking ? 'normal' : 'break-word'
116
- )
118
+
119
+ elm.innerHTML = html
120
+
121
+ // Apply the default styles to the element (for all styles here or that were ever seen in opts.otherStyles)
122
+ this.resetElmStyles()
123
+
124
+ elm.style.setProperty('font-family', opts.fontFamily)
125
+ elm.style.setProperty('font-style', opts.fontStyle)
126
+ elm.style.setProperty('font-weight', opts.fontWeight)
127
+ elm.style.setProperty('font-size', opts.fontSize + 'px')
128
+ elm.style.setProperty('line-height', opts.lineHeight.toString())
129
+ elm.style.setProperty('padding', opts.padding)
130
+
131
+ if (opts.maxWidth) {
132
+ elm.style.setProperty('max-width', opts.maxWidth + 'px')
133
+ }
134
+
135
+ if (opts.minWidth) {
136
+ elm.style.setProperty('min-width', opts.minWidth + 'px')
137
+ }
138
+
139
+ if (opts.disableOverflowWrapBreaking) {
140
+ elm.style.setProperty('overflow-wrap', 'normal')
141
+ }
142
+
117
143
  if (opts.otherStyles) {
118
144
  for (const [key, value] of Object.entries(opts.otherStyles)) {
119
- wrapperElm.style.setProperty(key, value)
145
+ elm.style.setProperty(key, value)
120
146
  }
121
147
  }
122
148
 
123
- const scrollWidth = wrapperElm.scrollWidth
124
- const rect = wrapperElm.getBoundingClientRect()
125
- wrapperElm.remove()
149
+ const scrollWidth = opts.measureScrollWidth ? elm.scrollWidth : 0
150
+ const rect = elm.getBoundingClientRect()
126
151
 
127
152
  return {
128
153
  x: 0,
@@ -247,27 +272,29 @@ export class TextManager {
247
272
  ): { text: string; box: BoxModel }[] {
248
273
  if (textToMeasure === '') return []
249
274
 
250
- const elm = this.baseElem.cloneNode() as HTMLDivElement
251
- this.editor.getContainer().appendChild(elm)
275
+ const { elm } = this
276
+
277
+ if (opts.otherStyles) {
278
+ for (const key in opts.otherStyles) {
279
+ if (!this.defaultStyles[key]) {
280
+ // we need to save the original style so that we can restore it when we're done
281
+ this.defaultStyles[key] = elm.style.getPropertyValue(key)
282
+ }
283
+ }
284
+ }
285
+
286
+ this.resetElmStyles()
287
+
288
+ elm.style.setProperty('font-family', opts.fontFamily)
289
+ elm.style.setProperty('font-style', opts.fontStyle)
290
+ elm.style.setProperty('font-weight', opts.fontWeight)
291
+ elm.style.setProperty('font-size', opts.fontSize + 'px')
292
+ elm.style.setProperty('line-height', opts.lineHeight.toString())
252
293
 
253
294
  const elementWidth = Math.ceil(opts.width - opts.padding * 2)
254
- elm.setAttribute('dir', 'auto')
255
- // N.B. This property, while discouraged ("intended for Document Type Definition (DTD) designers")
256
- // is necessary for ensuring correct mixed RTL/LTR behavior when exporting SVGs.
257
- elm.style.setProperty('unicode-bidi', 'plaintext')
258
295
  elm.style.setProperty('width', `${elementWidth}px`)
259
296
  elm.style.setProperty('height', 'min-content')
260
- elm.style.setProperty('font-size', `${opts.fontSize}px`)
261
- elm.style.setProperty('font-family', opts.fontFamily)
262
- elm.style.setProperty('font-weight', opts.fontWeight)
263
- elm.style.setProperty('line-height', `${opts.lineHeight * opts.fontSize}px`)
264
297
  elm.style.setProperty('text-align', textAlignmentsForLtr[opts.textAlign])
265
- elm.style.setProperty('font-style', opts.fontStyle)
266
- if (opts.otherStyles) {
267
- for (const [key, value] of Object.entries(opts.otherStyles)) {
268
- elm.style.setProperty(key, value)
269
- }
270
- }
271
298
 
272
299
  const shouldTruncateToFirstLine =
273
300
  opts.overflow === 'truncate-ellipsis' || opts.overflow === 'truncate-clip'
@@ -277,6 +304,12 @@ export class TextManager {
277
304
  elm.style.setProperty('word-break', 'break-all')
278
305
  }
279
306
 
307
+ if (opts.otherStyles) {
308
+ for (const [key, value] of Object.entries(opts.otherStyles)) {
309
+ elm.style.setProperty(key, value)
310
+ }
311
+ }
312
+
280
313
  const normalizedText = normalizeTextForDom(textToMeasure)
281
314
 
282
315
  // Render the text into the measurement element:
@@ -313,11 +346,10 @@ export class TextManager {
313
346
  h: lastSpan.box.h,
314
347
  },
315
348
  })
349
+
316
350
  return truncatedSpans
317
351
  }
318
352
 
319
- elm.remove()
320
-
321
353
  return spans
322
354
  }
323
355
  }