@tldraw/editor 3.14.0-canary.4c533b76dc35 → 3.14.0-canary.4deeaa9df15f

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 (66) hide show
  1. package/dist-cjs/index.d.ts +52 -44
  2. package/dist-cjs/index.js +1 -3
  3. package/dist-cjs/index.js.map +2 -2
  4. package/dist-cjs/lib/config/TLSessionStateSnapshot.js +1 -12
  5. package/dist-cjs/lib/config/TLSessionStateSnapshot.js.map +3 -3
  6. package/dist-cjs/lib/editor/Editor.js +40 -20
  7. package/dist-cjs/lib/editor/Editor.js.map +2 -2
  8. package/dist-cjs/lib/editor/bindings/BindingUtil.js.map +2 -2
  9. package/dist-cjs/lib/editor/managers/FontManager/FontManager.js +1 -2
  10. package/dist-cjs/lib/editor/managers/FontManager/FontManager.js.map +2 -2
  11. package/dist-cjs/lib/editor/managers/TextManager/TextManager.js +72 -42
  12. package/dist-cjs/lib/editor/managers/TextManager/TextManager.js.map +2 -2
  13. package/dist-cjs/lib/editor/shapes/group/GroupShapeUtil.js +1 -1
  14. package/dist-cjs/lib/editor/shapes/group/GroupShapeUtil.js.map +1 -1
  15. package/dist-cjs/lib/editor/types/external-content.js.map +1 -1
  16. package/dist-cjs/lib/hooks/useCanvasEvents.js +1 -2
  17. package/dist-cjs/lib/hooks/useCanvasEvents.js.map +2 -2
  18. package/dist-cjs/lib/primitives/Box.js +0 -6
  19. package/dist-cjs/lib/primitives/Box.js.map +2 -2
  20. package/dist-cjs/lib/utils/areShapesContentEqual.js +1 -1
  21. package/dist-cjs/lib/utils/areShapesContentEqual.js.map +2 -2
  22. package/dist-cjs/lib/utils/richText.js +7 -2
  23. package/dist-cjs/lib/utils/richText.js.map +2 -2
  24. package/dist-cjs/version.js +3 -3
  25. package/dist-cjs/version.js.map +1 -1
  26. package/dist-esm/index.d.mts +52 -44
  27. package/dist-esm/index.mjs +1 -3
  28. package/dist-esm/index.mjs.map +2 -2
  29. package/dist-esm/lib/config/TLSessionStateSnapshot.mjs +1 -1
  30. package/dist-esm/lib/config/TLSessionStateSnapshot.mjs.map +2 -2
  31. package/dist-esm/lib/editor/Editor.mjs +40 -20
  32. package/dist-esm/lib/editor/Editor.mjs.map +2 -2
  33. package/dist-esm/lib/editor/bindings/BindingUtil.mjs.map +2 -2
  34. package/dist-esm/lib/editor/managers/FontManager/FontManager.mjs +1 -2
  35. package/dist-esm/lib/editor/managers/FontManager/FontManager.mjs.map +2 -2
  36. package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs +72 -42
  37. package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs.map +2 -2
  38. package/dist-esm/lib/editor/shapes/group/GroupShapeUtil.mjs +1 -1
  39. package/dist-esm/lib/editor/shapes/group/GroupShapeUtil.mjs.map +1 -1
  40. package/dist-esm/lib/hooks/useCanvasEvents.mjs +1 -2
  41. package/dist-esm/lib/hooks/useCanvasEvents.mjs.map +2 -2
  42. package/dist-esm/lib/primitives/Box.mjs +0 -6
  43. package/dist-esm/lib/primitives/Box.mjs.map +2 -2
  44. package/dist-esm/lib/utils/areShapesContentEqual.mjs +1 -1
  45. package/dist-esm/lib/utils/areShapesContentEqual.mjs.map +2 -2
  46. package/dist-esm/lib/utils/richText.mjs +8 -3
  47. package/dist-esm/lib/utils/richText.mjs.map +2 -2
  48. package/dist-esm/version.mjs +3 -3
  49. package/dist-esm/version.mjs.map +1 -1
  50. package/editor.css +433 -482
  51. package/package.json +8 -9
  52. package/src/index.ts +1 -1
  53. package/src/lib/config/TLSessionStateSnapshot.ts +1 -1
  54. package/src/lib/editor/Editor.test.ts +11 -11
  55. package/src/lib/editor/Editor.ts +42 -17
  56. package/src/lib/editor/bindings/BindingUtil.ts +6 -0
  57. package/src/lib/editor/managers/FontManager/FontManager.ts +1 -2
  58. package/src/lib/editor/managers/TextManager/TextManager.test.ts +1 -5
  59. package/src/lib/editor/managers/TextManager/TextManager.ts +116 -86
  60. package/src/lib/editor/shapes/group/GroupShapeUtil.tsx +1 -1
  61. package/src/lib/editor/types/external-content.ts +1 -1
  62. package/src/lib/hooks/useCanvasEvents.ts +0 -1
  63. package/src/lib/primitives/Box.ts +0 -8
  64. package/src/lib/utils/areShapesContentEqual.ts +1 -2
  65. package/src/lib/utils/richText.ts +9 -3
  66. 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.4c533b76dc35",
4
+ "version": "3.14.0-canary.4deeaa9df15f",
5
5
  "author": {
6
6
  "name": "tldraw Inc.",
7
7
  "email": "hello@tldraw.com"
@@ -48,20 +48,19 @@
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.4c533b76dc35",
52
- "@tldraw/state-react": "3.14.0-canary.4c533b76dc35",
53
- "@tldraw/store": "3.14.0-canary.4c533b76dc35",
54
- "@tldraw/tlschema": "3.14.0-canary.4c533b76dc35",
55
- "@tldraw/utils": "3.14.0-canary.4c533b76dc35",
56
- "@tldraw/validate": "3.14.0-canary.4c533b76dc35",
51
+ "@tldraw/state": "3.14.0-canary.4deeaa9df15f",
52
+ "@tldraw/state-react": "3.14.0-canary.4deeaa9df15f",
53
+ "@tldraw/store": "3.14.0-canary.4deeaa9df15f",
54
+ "@tldraw/tlschema": "3.14.0-canary.4deeaa9df15f",
55
+ "@tldraw/utils": "3.14.0-canary.4deeaa9df15f",
56
+ "@tldraw/validate": "3.14.0-canary.4deeaa9df15f",
57
57
  "@types/core-js": "^2.5.8",
58
58
  "@use-gesture/react": "^10.3.1",
59
59
  "classnames": "^2.5.1",
60
60
  "core-js": "^3.40.0",
61
61
  "eventemitter3": "^4.0.7",
62
62
  "idb": "^7.1.1",
63
- "is-plain-object": "^5.0.0",
64
- "lodash.isequal": "^4.5.0"
63
+ "is-plain-object": "^5.0.0"
65
64
  },
66
65
  "peerDependencies": {
67
66
  "react": "^18.2.0 || ^19.0.0",
package/src/index.ts CHANGED
@@ -4,7 +4,6 @@ import 'core-js/stable/array/flat-map.js'
4
4
  import 'core-js/stable/array/flat.js'
5
5
  import 'core-js/stable/string/at.js'
6
6
  import 'core-js/stable/string/replace-all.js'
7
- export { areShapesContentEqual } from './lib/utils/areShapesContentEqual'
8
7
 
9
8
  // eslint-disable-next-line local/no-export-star
10
9
  export * from '@tldraw/state'
@@ -175,6 +174,7 @@ export {
175
174
  } from './lib/editor/managers/SnapManager/SnapManager'
176
175
  export {
177
176
  TextManager,
177
+ type TLMeasureTextOpts,
178
178
  type TLMeasureTextSpanOpts,
179
179
  } from './lib/editor/managers/TextManager/TextManager'
180
180
  export { UserPreferencesManager } from './lib/editor/managers/UserPreferencesManager/UserPreferencesManager'
@@ -14,12 +14,12 @@ import {
14
14
  import {
15
15
  deleteFromSessionStorage,
16
16
  getFromSessionStorage,
17
+ isEqual,
17
18
  setInSessionStorage,
18
19
  structuredClone,
19
20
  uniqueId,
20
21
  } from '@tldraw/utils'
21
22
  import { T } from '@tldraw/validate'
22
- import isEqual from 'lodash.isequal'
23
23
  import { tlenv } from '../globals/environment'
24
24
 
25
25
  const tabIdKey = 'TLDRAW_TAB_ID_v2' as const
@@ -233,7 +233,7 @@ describe('getShapesAtPoint', () => {
233
233
  })
234
234
  })
235
235
 
236
- it('returns shapes at a point in order of their index', () => {
236
+ it('returns shapes at a point in reverse z-index order', () => {
237
237
  // Point at (50, 50) should hit shape3's edge (since it's at 50,50 with size 100x100)
238
238
  // This point is exactly at the top-left corner of shape3
239
239
  const shapes = editor.getShapesAtPoint({ x: 50, y: 50 })
@@ -313,12 +313,12 @@ describe('getShapesAtPoint', () => {
313
313
  const shapes = editor.getShapesAtPoint({ x: 100, y: 0 })
314
314
  const shapeIds = shapes.map((s) => s.id)
315
315
 
316
- // Both shapes should be detected at this overlapping point
317
- expect(shapeIds).toEqual([ids.shape1, ids.shape2])
316
+ // Both shapes should be detected at this overlapping point (reversed order - top-most first)
317
+ expect(shapeIds).toEqual([ids.shape2, ids.shape1])
318
318
  expect(shapes).toHaveLength(2)
319
319
  })
320
320
 
321
- it('maintains shape order from getCurrentPageShapesSorted', () => {
321
+ it('maintains reverse shape order and responds to z-index changes', () => {
322
322
  // Create filled shape that overlaps with shape2
323
323
  editor.createShape({
324
324
  id: ids.shape5,
@@ -333,14 +333,14 @@ describe('getShapesAtPoint', () => {
333
333
  const shapes = editor.getShapesAtPoint({ x: 120, y: 120 }, { hitInside: true })
334
334
  const shapeIds = shapes.map((s) => s.id)
335
335
 
336
- // All shapes that contain this point should be returned in z-index order
337
- expect(shapeIds).toEqual([ids.shape1, ids.shape2, ids.shape3, ids.shape4, ids.shape5])
336
+ // All shapes that contain this point should be returned in reverse z-index order (top-most first)
337
+ expect(shapeIds).toEqual([ids.shape5, ids.shape4, ids.shape3, ids.shape2, ids.shape1])
338
338
 
339
- // After bringing shape2 to front, order should change
339
+ // After bringing shape2 to front, order should change (shape2 becomes top-most)
340
340
  editor.bringToFront([ids.shape2])
341
341
  const shapes2 = editor.getShapesAtPoint({ x: 120, y: 120 }, { hitInside: true })
342
342
  const shapeIds2 = shapes2.map((s) => s.id)
343
- expect(shapeIds2).toEqual([ids.shape1, ids.shape3, ids.shape4, ids.shape5, ids.shape2])
343
+ expect(shapeIds2).toEqual([ids.shape2, ids.shape5, ids.shape4, ids.shape3, ids.shape1])
344
344
  })
345
345
 
346
346
  it('combines hitInside and margin options', () => {
@@ -361,7 +361,7 @@ describe('getShapesAtPoint', () => {
361
361
  isShapeHiddenSpy.mockRestore()
362
362
  })
363
363
 
364
- it('returns multiple shapes at same point in z-index order', () => {
364
+ it('returns multiple shapes at same point in reverse z-index order', () => {
365
365
  // Create two shapes at exactly the same position (away from existing shapes)
366
366
  editor.createShape({
367
367
  id: ids.overlap1,
@@ -383,8 +383,8 @@ describe('getShapesAtPoint', () => {
383
383
  const shapes = editor.getShapesAtPoint({ x: 600, y: 600 })
384
384
  const shapeIds = shapes.map((s) => s.id)
385
385
 
386
- // Should return both shapes in z-index order
387
- expect(shapeIds).toEqual([ids.overlap1, ids.overlap2])
386
+ // Should return both shapes in reverse z-index order (top-most first)
387
+ expect(shapeIds).toEqual([ids.overlap2, ids.overlap1])
388
388
  expect(shapes).toHaveLength(2)
389
389
  })
390
390
 
@@ -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)
@@ -506,14 +508,13 @@ export class Editor extends EventEmitter<TLEventMap> {
506
508
  shape: {
507
509
  afterChange: (shapeBefore, shapeAfter) => {
508
510
  for (const binding of this.getBindingsInvolvingShape(shapeAfter)) {
509
- if (areShapesContentEqual(shapeBefore, shapeAfter)) continue
510
-
511
511
  invalidBindingTypes.add(binding.type)
512
512
  if (binding.fromId === shapeAfter.id) {
513
513
  this.getBindingUtil(binding).onAfterChangeFromShape?.({
514
514
  binding,
515
515
  shapeBefore,
516
516
  shapeAfter,
517
+ reason: 'self',
517
518
  })
518
519
  }
519
520
  if (binding.toId === shapeAfter.id) {
@@ -521,6 +522,7 @@ export class Editor extends EventEmitter<TLEventMap> {
521
522
  binding,
522
523
  shapeBefore,
523
524
  shapeAfter,
525
+ reason: 'self',
524
526
  })
525
527
  }
526
528
  }
@@ -539,6 +541,7 @@ export class Editor extends EventEmitter<TLEventMap> {
539
541
  binding,
540
542
  shapeBefore: descendantShape,
541
543
  shapeAfter: descendantShape,
544
+ reason: 'ancestry',
542
545
  })
543
546
  }
544
547
  if (binding.toId === descendantShape.id) {
@@ -546,6 +549,7 @@ export class Editor extends EventEmitter<TLEventMap> {
546
549
  binding,
547
550
  shapeBefore: descendantShape,
548
551
  shapeAfter: descendantShape,
552
+ reason: 'ancestry',
549
553
  })
550
554
  }
551
555
  }
@@ -2118,6 +2122,20 @@ export class Editor extends EventEmitter<TLEventMap> {
2118
2122
  return this.getShapesPageBounds(this.getSelectedShapeIds())
2119
2123
  }
2120
2124
 
2125
+ /**
2126
+ * The bounds of the selection bounding box in the current page space.
2127
+ *
2128
+ * @readonly
2129
+ * @public
2130
+ */
2131
+ @computed 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
+
2121
2139
  /**
2122
2140
  * @internal
2123
2141
  */
@@ -5035,28 +5053,33 @@ export class Editor extends EventEmitter<TLEventMap> {
5035
5053
  *
5036
5054
  * @public
5037
5055
  */
5038
- isShapeOrAncestorLocked(shape?: TLShape): boolean
5039
- isShapeOrAncestorLocked(id?: TLShapeId): boolean
5040
- isShapeOrAncestorLocked(arg?: TLShape | TLShapeId): boolean {
5041
- const shape = typeof arg === 'string' ? this.getShape(arg) : arg
5042
- if (shape === undefined) return false
5043
- if (shape.isLocked) return true
5044
- return this.isShapeOrAncestorLocked(this.getShapeParent(shape))
5056
+ isShapeOrAncestorLocked(shape?: TLShape | TLShapeId): boolean {
5057
+ const _shape = shape && this.getShape(shape)
5058
+ if (_shape === undefined) return false
5059
+ if (_shape.isLocked) return true
5060
+ return this.isShapeOrAncestorLocked(this.getShapeParent(_shape))
5045
5061
  }
5046
5062
 
5063
+ /**
5064
+ * Get shapes that are outside of the viewport.
5065
+ *
5066
+ * @public
5067
+ */
5047
5068
  @computed
5048
- private _notVisibleShapes() {
5049
- return notVisibleShapes(this)
5069
+ getNotVisibleShapes() {
5070
+ return this._notVisibleShapes.get()
5050
5071
  }
5051
5072
 
5073
+ private _notVisibleShapes = notVisibleShapes(this)
5074
+
5052
5075
  /**
5053
- * Get culled shapes.
5076
+ * Get culled shapes (those that should not render), taking into account which shapes are selected or editing.
5054
5077
  *
5055
5078
  * @public
5056
5079
  */
5057
5080
  @computed
5058
5081
  getCulledShapes() {
5059
- const notVisibleShapes = this._notVisibleShapes().get()
5082
+ const notVisibleShapes = this.getNotVisibleShapes()
5060
5083
  const selectedShapeIds = this.getSelectedShapeIds()
5061
5084
  const editingId = this.getEditingShapeId()
5062
5085
  const culledShapes = new Set<TLShapeId>(notVisibleShapes)
@@ -5305,21 +5328,23 @@ export class Editor extends EventEmitter<TLEventMap> {
5305
5328
  * @example
5306
5329
  * ```ts
5307
5330
  * editor.getShapesAtPoint({ x: 100, y: 100 })
5308
- * editor.getShapesAtPoint({ x: 100, y: 100 }, { hitInside: true, exact: true })
5331
+ * editor.getShapesAtPoint({ x: 100, y: 100 }, { hitInside: true, margin: 8 })
5309
5332
  * ```
5310
5333
  *
5311
5334
  * @param point - The page point to test.
5312
5335
  * @param opts - The options for the hit point testing.
5313
5336
  *
5337
+ * @returns An array of shapes at the given point, sorted in reverse order of their absolute z-index (top-most shape first).
5338
+ *
5314
5339
  * @public
5315
5340
  */
5316
5341
  getShapesAtPoint(
5317
5342
  point: VecLike,
5318
5343
  opts = {} as { margin?: number; hitInside?: boolean }
5319
5344
  ): TLShape[] {
5320
- return this.getCurrentPageShapesSorted().filter(
5321
- (shape) => !this.isShapeHidden(shape) && this.isPointInShape(shape, point, opts)
5322
- )
5345
+ return this.getCurrentPageShapesSorted()
5346
+ .filter((shape) => !this.isShapeHidden(shape) && this.isPointInShape(shape, point, opts))
5347
+ .reverse()
5323
5348
  }
5324
5349
 
5325
5350
  /**
@@ -62,6 +62,12 @@ export interface BindingOnShapeChangeOptions<Binding extends TLUnknownBinding> {
62
62
  shapeBefore: TLShape
63
63
  /** The shape record after the change is made. */
64
64
  shapeAfter: TLShape
65
+ /**
66
+ * Why did this shape change?
67
+ * - 'self': the shape itself changed
68
+ * - 'ancestry': the ancestry of the shape changed, but the shape itself may not have done
69
+ */
70
+ reason: 'self' | 'ancestry'
65
71
  }
66
72
 
67
73
  /**
@@ -96,8 +96,7 @@ export class FontManager {
96
96
  },
97
97
  {
98
98
  areResultsEqual: areArraysShallowEqual,
99
- // @ts-expect-error
100
- areRecordsEqual: (a, b) => a.props.richText === b.props.richText,
99
+ areRecordsEqual: (a, b) => a.props === b.props && a.meta === b.meta,
101
100
  }
102
101
  )
103
102
 
@@ -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,27 @@ 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
+ lineHeight: number
30
+ /**
31
+ * When maxWidth is a number, the text will be wrapped to that maxWidth. When maxWidth
32
+ * is null, the text will be measured without wrapping, but explicit line breaks and
33
+ * space are preserved.
34
+ */
35
+ maxWidth: null | number
36
+ minWidth?: null | number
37
+ // todo: make this a number so that it is consistent with other TLMeasureTextSpanOpts
38
+ padding: string
39
+ otherStyles?: Record<string, string>
40
+ disableOverflowWrapBreaking?: boolean
41
+ measureScrollWidth?: boolean
42
+ }
43
+
23
44
  /** @public */
24
45
  export interface TLMeasureTextSpanOpts {
25
46
  overflow: 'wrap' | 'truncate-ellipsis' | 'truncate-clip'
@@ -33,96 +54,98 @@ export interface TLMeasureTextSpanOpts {
33
54
  lineHeight: number
34
55
  textAlign: TLDefaultHorizontalAlignStyle
35
56
  otherStyles?: Record<string, string>
57
+ measureScrollWidth?: boolean
36
58
  }
37
59
 
38
60
  const spaceCharacterRegex = /\s/
39
61
 
40
62
  /** @public */
41
63
  export class TextManager {
42
- private baseElem: HTMLDivElement
64
+ private elm: HTMLDivElement
65
+ private defaultStyles: Record<string, string | null>
43
66
 
44
67
  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
68
+ const elm = document.createElement('div')
69
+ elm.classList.add('tl-text')
70
+ elm.classList.add('tl-text-measure')
71
+ elm.setAttribute('dir', 'auto')
72
+ elm.tabIndex = -1
73
+ this.editor.getContainer().appendChild(elm)
74
+
75
+ // we need to save the default styles so that we can restore them when we're done
76
+ // these must be the css names, not the js names for the styles
77
+ this.defaultStyles = {
78
+ 'word-break': 'auto',
79
+ width: null,
80
+ height: null,
81
+ 'max-width': null,
82
+ 'min-width': null,
83
+ }
84
+
85
+ this.elm = elm
49
86
  }
50
87
 
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
88
+ dispose() {
89
+ return this.elm.remove()
90
+ }
91
+
92
+ private resetElmStyles() {
93
+ const { elm, defaultStyles } = this
94
+ for (const key in defaultStyles) {
95
+ elm.style.setProperty(key, defaultStyles[key])
68
96
  }
69
- ): BoxModel & { scrollWidth: number } {
97
+ }
98
+
99
+ measureText(textToMeasure: string, opts: TLMeasureTextOpts): BoxModel & { scrollWidth: number } {
70
100
  const div = document.createElement('div')
71
101
  div.textContent = normalizeTextForDom(textToMeasure)
72
102
  return this.measureHtml(div.innerHTML, opts)
73
103
  }
74
104
 
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
105
+ measureHtml(html: string, opts: TLMeasureTextOpts): BoxModel & { scrollWidth: number } {
106
+ const { elm } = this
107
+
108
+ if (opts.otherStyles) {
109
+ for (const key in opts.otherStyles) {
110
+ if (!this.defaultStyles[key]) {
111
+ // we need to save the original style so that we can restore it when we're done
112
+ this.defaultStyles[key] = elm.style.getPropertyValue(key)
113
+ }
114
+ }
93
115
  }
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
- )
116
+
117
+ elm.innerHTML = html
118
+
119
+ // Apply the default styles to the element (for all styles here or that were ever seen in opts.otherStyles)
120
+ this.resetElmStyles()
121
+
122
+ elm.style.setProperty('font-family', opts.fontFamily)
123
+ elm.style.setProperty('font-style', opts.fontStyle)
124
+ elm.style.setProperty('font-weight', opts.fontWeight)
125
+ elm.style.setProperty('font-size', opts.fontSize + 'px')
126
+ elm.style.setProperty('line-height', opts.lineHeight * opts.fontSize + 'px')
127
+ elm.style.setProperty('padding', opts.padding)
128
+
129
+ if (opts.maxWidth) {
130
+ elm.style.setProperty('max-width', opts.maxWidth + 'px')
131
+ }
132
+
133
+ if (opts.minWidth) {
134
+ elm.style.setProperty('min-width', opts.minWidth + 'px')
135
+ }
136
+
137
+ if (opts.disableOverflowWrapBreaking) {
138
+ elm.style.setProperty('overflow-wrap', 'normal')
139
+ }
140
+
117
141
  if (opts.otherStyles) {
118
142
  for (const [key, value] of Object.entries(opts.otherStyles)) {
119
- wrapperElm.style.setProperty(key, value)
143
+ elm.style.setProperty(key, value)
120
144
  }
121
145
  }
122
146
 
123
- const scrollWidth = wrapperElm.scrollWidth
124
- const rect = wrapperElm.getBoundingClientRect()
125
- wrapperElm.remove()
147
+ const scrollWidth = opts.measureScrollWidth ? elm.scrollWidth : 0
148
+ const rect = elm.getBoundingClientRect()
126
149
 
127
150
  return {
128
151
  x: 0,
@@ -247,27 +270,29 @@ export class TextManager {
247
270
  ): { text: string; box: BoxModel }[] {
248
271
  if (textToMeasure === '') return []
249
272
 
250
- const elm = this.baseElem.cloneNode() as HTMLDivElement
251
- this.editor.getContainer().appendChild(elm)
273
+ const { elm } = this
274
+
275
+ if (opts.otherStyles) {
276
+ for (const key in opts.otherStyles) {
277
+ if (!this.defaultStyles[key]) {
278
+ // we need to save the original style so that we can restore it when we're done
279
+ this.defaultStyles[key] = elm.style.getPropertyValue(key)
280
+ }
281
+ }
282
+ }
283
+
284
+ this.resetElmStyles()
285
+
286
+ elm.style.setProperty('font-family', opts.fontFamily)
287
+ elm.style.setProperty('font-style', opts.fontStyle)
288
+ elm.style.setProperty('font-weight', opts.fontWeight)
289
+ elm.style.setProperty('font-size', opts.fontSize + 'px')
290
+ elm.style.setProperty('line-height', opts.lineHeight * opts.fontSize + 'px')
252
291
 
253
292
  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
293
  elm.style.setProperty('width', `${elementWidth}px`)
259
294
  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
295
  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
296
 
272
297
  const shouldTruncateToFirstLine =
273
298
  opts.overflow === 'truncate-ellipsis' || opts.overflow === 'truncate-clip'
@@ -277,6 +302,12 @@ export class TextManager {
277
302
  elm.style.setProperty('word-break', 'break-all')
278
303
  }
279
304
 
305
+ if (opts.otherStyles) {
306
+ for (const [key, value] of Object.entries(opts.otherStyles)) {
307
+ elm.style.setProperty(key, value)
308
+ }
309
+ }
310
+
280
311
  const normalizedText = normalizeTextForDom(textToMeasure)
281
312
 
282
313
  // Render the text into the measurement element:
@@ -313,11 +344,10 @@ export class TextManager {
313
344
  h: lastSpan.box.h,
314
345
  },
315
346
  })
347
+
316
348
  return truncatedSpans
317
349
  }
318
350
 
319
- elm.remove()
320
-
321
351
  return spans
322
352
  }
323
353
  }
@@ -21,7 +21,7 @@ export class GroupShapeUtil extends ShapeUtil<TLGroupShape> {
21
21
  }
22
22
 
23
23
  canResize() {
24
- return false
24
+ return true
25
25
  }
26
26
 
27
27
  canResizeChildren() {
@@ -52,7 +52,7 @@ export interface TLTextExternalContent extends TLBaseExternalContent {
52
52
  export interface TLFilesExternalContent extends TLBaseExternalContent {
53
53
  type: 'files'
54
54
  files: File[]
55
- ignoreParent: boolean
55
+ ignoreParent?: boolean
56
56
  }
57
57
 
58
58
  /** @public */
@@ -137,7 +137,6 @@ export function useCanvasEvents() {
137
137
  type: 'files',
138
138
  files,
139
139
  point: editor.screenToPage({ x: e.clientX, y: e.clientY }),
140
- ignoreParent: false,
141
140
  })
142
141
  return
143
142
  }
@@ -591,14 +591,6 @@ export class Box {
591
591
  return b.x === a.x && b.y === a.y && b.w === a.w && b.h === a.h
592
592
  }
593
593
 
594
- prettyMuchEquals(other: Box | BoxModel) {
595
- return this.clone().toFixed().equals(Box.From(other).toFixed())
596
- }
597
-
598
- static PrettyMuchEquals(a: Box | BoxModel, b: Box | BoxModel) {
599
- return b.x === a.x && b.y === a.y && b.w === a.w && b.h === a.h
600
- }
601
-
602
594
  zeroFix() {
603
595
  this.w = Math.max(1, this.w)
604
596
  this.h = Math.max(1, this.h)
@@ -1,5 +1,4 @@
1
1
  import { TLShape } from '@tldraw/tlschema'
2
2
 
3
- /** @public */
4
3
  export const areShapesContentEqual = (a: TLShape, b: TLShape) =>
5
- a.parentId === b.parentId && a.props === b.props && a.meta === b.meta
4
+ a.props === b.props && a.meta === b.meta