@tldraw/editor 3.14.0-canary.cf19563e117d → 3.14.0-canary.d1b8a584b27c

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 (73) hide show
  1. package/dist-cjs/index.d.ts +59 -42
  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 +33 -6
  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 +73 -42
  12. package/dist-cjs/lib/editor/managers/TextManager/TextManager.js.map +2 -2
  13. package/dist-cjs/lib/editor/shapes/ShapeUtil.js.map +1 -1
  14. package/dist-cjs/lib/editor/shapes/group/GroupShapeUtil.js +1 -1
  15. package/dist-cjs/lib/editor/shapes/group/GroupShapeUtil.js.map +1 -1
  16. package/dist-cjs/lib/editor/tools/StateNode.js +3 -3
  17. package/dist-cjs/lib/editor/tools/StateNode.js.map +2 -2
  18. package/dist-cjs/lib/editor/types/external-content.js.map +1 -1
  19. package/dist-cjs/lib/hooks/useCanvasEvents.js +1 -2
  20. package/dist-cjs/lib/hooks/useCanvasEvents.js.map +2 -2
  21. package/dist-cjs/lib/primitives/Box.js +0 -6
  22. package/dist-cjs/lib/primitives/Box.js.map +2 -2
  23. package/dist-cjs/lib/utils/areShapesContentEqual.js +1 -1
  24. package/dist-cjs/lib/utils/areShapesContentEqual.js.map +2 -2
  25. package/dist-cjs/lib/utils/richText.js +7 -2
  26. package/dist-cjs/lib/utils/richText.js.map +2 -2
  27. package/dist-cjs/version.js +3 -3
  28. package/dist-cjs/version.js.map +1 -1
  29. package/dist-esm/index.d.mts +59 -42
  30. package/dist-esm/index.mjs +1 -3
  31. package/dist-esm/index.mjs.map +2 -2
  32. package/dist-esm/lib/config/TLSessionStateSnapshot.mjs +1 -1
  33. package/dist-esm/lib/config/TLSessionStateSnapshot.mjs.map +2 -2
  34. package/dist-esm/lib/editor/Editor.mjs +33 -6
  35. package/dist-esm/lib/editor/Editor.mjs.map +2 -2
  36. package/dist-esm/lib/editor/bindings/BindingUtil.mjs.map +2 -2
  37. package/dist-esm/lib/editor/managers/FontManager/FontManager.mjs +1 -2
  38. package/dist-esm/lib/editor/managers/FontManager/FontManager.mjs.map +2 -2
  39. package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs +73 -42
  40. package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs.map +2 -2
  41. package/dist-esm/lib/editor/shapes/ShapeUtil.mjs.map +1 -1
  42. package/dist-esm/lib/editor/shapes/group/GroupShapeUtil.mjs +1 -1
  43. package/dist-esm/lib/editor/shapes/group/GroupShapeUtil.mjs.map +1 -1
  44. package/dist-esm/lib/editor/tools/StateNode.mjs +3 -3
  45. package/dist-esm/lib/editor/tools/StateNode.mjs.map +2 -2
  46. package/dist-esm/lib/hooks/useCanvasEvents.mjs +1 -2
  47. package/dist-esm/lib/hooks/useCanvasEvents.mjs.map +2 -2
  48. package/dist-esm/lib/primitives/Box.mjs +0 -6
  49. package/dist-esm/lib/primitives/Box.mjs.map +2 -2
  50. package/dist-esm/lib/utils/areShapesContentEqual.mjs +1 -1
  51. package/dist-esm/lib/utils/areShapesContentEqual.mjs.map +2 -2
  52. package/dist-esm/lib/utils/richText.mjs +8 -3
  53. package/dist-esm/lib/utils/richText.mjs.map +2 -2
  54. package/dist-esm/version.mjs +3 -3
  55. package/dist-esm/version.mjs.map +1 -1
  56. package/editor.css +433 -482
  57. package/package.json +8 -9
  58. package/src/index.ts +2 -1
  59. package/src/lib/config/TLSessionStateSnapshot.ts +1 -1
  60. package/src/lib/editor/Editor.ts +32 -3
  61. package/src/lib/editor/bindings/BindingUtil.ts +6 -0
  62. package/src/lib/editor/managers/FontManager/FontManager.ts +1 -2
  63. package/src/lib/editor/managers/TextManager/TextManager.test.ts +1 -5
  64. package/src/lib/editor/managers/TextManager/TextManager.ts +118 -86
  65. package/src/lib/editor/shapes/ShapeUtil.ts +1 -0
  66. package/src/lib/editor/shapes/group/GroupShapeUtil.tsx +1 -1
  67. package/src/lib/editor/tools/StateNode.ts +3 -3
  68. package/src/lib/editor/types/external-content.ts +11 -2
  69. package/src/lib/hooks/useCanvasEvents.ts +0 -1
  70. package/src/lib/primitives/Box.ts +0 -8
  71. package/src/lib/utils/areShapesContentEqual.ts +1 -2
  72. package/src/lib/utils/richText.ts +9 -3
  73. 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.cf19563e117d",
4
+ "version": "3.14.0-canary.d1b8a584b27c",
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.cf19563e117d",
52
- "@tldraw/state-react": "3.14.0-canary.cf19563e117d",
53
- "@tldraw/store": "3.14.0-canary.cf19563e117d",
54
- "@tldraw/tlschema": "3.14.0-canary.cf19563e117d",
55
- "@tldraw/utils": "3.14.0-canary.cf19563e117d",
56
- "@tldraw/validate": "3.14.0-canary.cf19563e117d",
51
+ "@tldraw/state": "3.14.0-canary.d1b8a584b27c",
52
+ "@tldraw/state-react": "3.14.0-canary.d1b8a584b27c",
53
+ "@tldraw/store": "3.14.0-canary.d1b8a584b27c",
54
+ "@tldraw/tlschema": "3.14.0-canary.d1b8a584b27c",
55
+ "@tldraw/utils": "3.14.0-canary.d1b8a584b27c",
56
+ "@tldraw/validate": "3.14.0-canary.d1b8a584b27c",
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'
@@ -253,6 +253,7 @@ export {
253
253
  type TLExternalContent,
254
254
  type TLExternalContentSource,
255
255
  type TLFileExternalAsset,
256
+ type TLFileReplaceExternalContent,
256
257
  type TLFilesExternalContent,
257
258
  type TLSvgTextExternalContent,
258
259
  type TLTextExternalContent,
@@ -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
@@ -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
+ 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
  */
@@ -6193,11 +6211,12 @@ export class Editor extends EventEmitter<TLEventMap> {
6193
6211
  */
6194
6212
  duplicateShapes(shapes: TLShapeId[] | TLShape[], offset?: VecLike): this {
6195
6213
  this.run(() => {
6196
- const ids =
6214
+ const _ids =
6197
6215
  typeof shapes[0] === 'string'
6198
6216
  ? (shapes as TLShapeId[])
6199
6217
  : (shapes as TLShape[]).map((s) => s.id)
6200
6218
 
6219
+ const ids = this._shouldIgnoreShapeLock ? _ids : this._getUnlockedShapeIds(_ids)
6201
6220
  if (ids.length <= 0) return this
6202
6221
 
6203
6222
  const initialIds = new Set(ids)
@@ -8809,6 +8828,7 @@ export class Editor extends EventEmitter<TLEventMap> {
8809
8828
  } = {
8810
8829
  text: null,
8811
8830
  files: null,
8831
+ 'file-replace': null,
8812
8832
  embed: null,
8813
8833
  'svg-text': null,
8814
8834
  url: null,
@@ -8858,6 +8878,15 @@ export class Editor extends EventEmitter<TLEventMap> {
8858
8878
  return this.externalContentHandlers[info.type]?.(info as any)
8859
8879
  }
8860
8880
 
8881
+ /**
8882
+ * Handle replacing external content.
8883
+ *
8884
+ * @param info - Info about the external content.
8885
+ */
8886
+ async replaceExternalContent<E>(info: TLExternalContent<E>): Promise<void> {
8887
+ return this.externalContentHandlers[info.type]?.(info as any)
8888
+ }
8889
+
8861
8890
  /**
8862
8891
  * Get content that can be exported for the given shape ids.
8863
8892
  *
@@ -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,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
  }
@@ -745,6 +745,7 @@ export interface TLCropInfo<T extends TLShape> {
745
745
  crop: TLShapeCrop
746
746
  uncroppedSize: { w: number; h: number }
747
747
  initialShape: T
748
+ aspectRatioLocked?: boolean
748
749
  }
749
750
 
750
751
  /**
@@ -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() {
@@ -206,15 +206,15 @@ export abstract class StateNode implements Partial<TLEventHandlers> {
206
206
  }
207
207
 
208
208
  // todo: move this logic into transition
209
- exit(info: any, from: string) {
209
+ exit(info: any, to: string) {
210
210
  if (debugFlags.measurePerformance.get() && this.performanceTracker.isStarted()) {
211
211
  this.performanceTracker.stop()
212
212
  }
213
213
  this._isActive.set(false)
214
- this.onExit?.(info, from)
214
+ this.onExit?.(info, to)
215
215
 
216
216
  if (!this.getIsActive()) {
217
- this.getCurrent()?.exit(info, from)
217
+ this.getCurrent()?.exit(info, to)
218
218
  }
219
219
  }
220
220
 
@@ -1,4 +1,4 @@
1
- import { TLAssetId } from '@tldraw/tlschema'
1
+ import { TLAssetId, TLShapeId } from '@tldraw/tlschema'
2
2
  import { VecLike } from '../../primitives/Vec'
3
3
  import { TLContent } from './clipboard-types'
4
4
 
@@ -52,7 +52,15 @@ 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
+ }
57
+
58
+ /** @public */
59
+ export interface TLFileReplaceExternalContent extends TLBaseExternalContent {
60
+ type: 'file-replace'
61
+ file: File
62
+ shapeId: TLShapeId
63
+ isImage: boolean
56
64
  }
57
65
 
58
66
  /** @public */
@@ -90,6 +98,7 @@ export interface TLExcalidrawExternalContent extends TLBaseExternalContent {
90
98
  export type TLExternalContent<EmbedDefinition> =
91
99
  | TLTextExternalContent
92
100
  | TLFilesExternalContent
101
+ | TLFileReplaceExternalContent
93
102
  | TLUrlExternalContent
94
103
  | TLSvgTextExternalContent
95
104
  | TLEmbedExternalContent<EmbedDefinition>
@@ -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
@@ -1,8 +1,8 @@
1
1
  import { getSchema, JSONContent, Editor as TTEditor } from '@tiptap/core'
2
- import { Node } from '@tiptap/pm/model'
2
+ import { Node, Schema } from '@tiptap/pm/model'
3
3
  import { EditorProviderProps } from '@tiptap/react'
4
4
  import { TLRichText } from '@tldraw/tlschema'
5
- import { assert } from '@tldraw/utils'
5
+ import { assert, WeakCache } from '@tldraw/utils'
6
6
  import { Editor } from '../editor/Editor'
7
7
  import { TLFontFace } from '../editor/managers/FontManager/FontManager'
8
8
 
@@ -39,6 +39,11 @@ export type RichTextFontVisitor = (
39
39
  addFont: (font: TLFontFace) => void
40
40
  ) => RichTextFontVisitorState
41
41
 
42
+ const schemaCache = new WeakCache<EditorProviderProps, Schema>()
43
+ export function getTipTapSchema(tipTapConfig: EditorProviderProps) {
44
+ return schemaCache.get(tipTapConfig, () => getSchema(tipTapConfig.extensions ?? []))
45
+ }
46
+
42
47
  /** @public */
43
48
  export function getFontsFromRichText(
44
49
  editor: Editor,
@@ -49,7 +54,8 @@ export function getFontsFromRichText(
49
54
  assert(tipTapConfig, 'textOptions.tipTapConfig must be set to use rich text')
50
55
  assert(addFontsFromNode, 'textOptions.addFontsFromNode must be set to use rich text')
51
56
 
52
- const schema = getSchema(tipTapConfig.extensions ?? [])
57
+ const schema = getTipTapSchema(tipTapConfig)
58
+
53
59
  const rootNode = Node.fromJSON(schema, richText as JSONContent)
54
60
 
55
61
  const fonts = new Set<TLFontFace>()
package/src/version.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  // This file is automatically generated by internal/scripts/refresh-assets.ts.
2
2
  // Do not edit manually. Or do, I'm a comment, not a cop.
3
3
 
4
- export const version = '3.14.0-canary.cf19563e117d'
4
+ export const version = '3.14.0-canary.d1b8a584b27c'
5
5
  export const publishDates = {
6
6
  major: '2024-09-13T14:36:29.063Z',
7
- minor: '2025-06-10T07:40:22.660Z',
8
- patch: '2025-06-10T07:40:22.660Z',
7
+ minor: '2025-06-23T14:58:35.827Z',
8
+ patch: '2025-06-23T14:58:35.827Z',
9
9
  }