@tldraw/editor 3.14.0-canary.b34920584558 → 3.14.0-canary.b58d09b378a2

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 (37) hide show
  1. package/dist-cjs/index.d.ts +51 -37
  2. package/dist-cjs/index.js +1 -1
  3. package/dist-cjs/index.js.map +2 -2
  4. package/dist-cjs/lib/editor/Editor.js +25 -1
  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/tools/StateNode.js +3 -3
  9. package/dist-cjs/lib/editor/tools/StateNode.js.map +2 -2
  10. package/dist-cjs/lib/editor/types/external-content.js.map +1 -1
  11. package/dist-cjs/lib/hooks/useCanvasEvents.js +1 -2
  12. package/dist-cjs/lib/hooks/useCanvasEvents.js.map +2 -2
  13. package/dist-cjs/version.js +3 -3
  14. package/dist-cjs/version.js.map +1 -1
  15. package/dist-esm/index.d.mts +51 -37
  16. package/dist-esm/index.mjs +1 -1
  17. package/dist-esm/index.mjs.map +2 -2
  18. package/dist-esm/lib/editor/Editor.mjs +25 -1
  19. package/dist-esm/lib/editor/Editor.mjs.map +2 -2
  20. package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs +73 -42
  21. package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs.map +2 -2
  22. package/dist-esm/lib/editor/tools/StateNode.mjs +3 -3
  23. package/dist-esm/lib/editor/tools/StateNode.mjs.map +2 -2
  24. package/dist-esm/lib/hooks/useCanvasEvents.mjs +1 -2
  25. package/dist-esm/lib/hooks/useCanvasEvents.mjs.map +2 -2
  26. package/dist-esm/version.mjs +3 -3
  27. package/dist-esm/version.mjs.map +1 -1
  28. package/editor.css +39 -1
  29. package/package.json +7 -7
  30. package/src/index.ts +2 -0
  31. package/src/lib/editor/Editor.ts +28 -1
  32. package/src/lib/editor/managers/TextManager/TextManager.test.ts +1 -5
  33. package/src/lib/editor/managers/TextManager/TextManager.ts +117 -86
  34. package/src/lib/editor/tools/StateNode.ts +3 -3
  35. package/src/lib/editor/types/external-content.ts +11 -2
  36. package/src/lib/hooks/useCanvasEvents.ts +0 -1
  37. package/src/version.ts +3 -3
@@ -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,99 @@ 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
+ 'overflow-wrap': 'break-word',
79
+ 'word-break': 'auto',
80
+ width: null,
81
+ height: null,
82
+ 'max-width': null,
83
+ 'min-width': null,
84
+ }
85
+
86
+ this.elm = elm
49
87
  }
50
88
 
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
89
+ dispose() {
90
+ return this.elm.remove()
91
+ }
92
+
93
+ private resetElmStyles() {
94
+ const { elm, defaultStyles } = this
95
+ for (const key in defaultStyles) {
96
+ elm.style.setProperty(key, defaultStyles[key])
68
97
  }
69
- ): BoxModel & { scrollWidth: number } {
98
+ }
99
+
100
+ measureText(textToMeasure: string, opts: TLMeasureTextOpts): BoxModel & { scrollWidth: number } {
70
101
  const div = document.createElement('div')
71
102
  div.textContent = normalizeTextForDom(textToMeasure)
72
103
  return this.measureHtml(div.innerHTML, opts)
73
104
  }
74
105
 
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
106
+ measureHtml(html: string, opts: TLMeasureTextOpts): BoxModel & { scrollWidth: number } {
107
+ const { elm } = this
108
+
109
+ if (opts.otherStyles) {
110
+ for (const key in opts.otherStyles) {
111
+ if (!this.defaultStyles[key]) {
112
+ // we need to save the original style so that we can restore it when we're done
113
+ this.defaultStyles[key] = elm.style.getPropertyValue(key)
114
+ }
115
+ }
93
116
  }
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
- )
117
+
118
+ elm.innerHTML = html
119
+
120
+ // Apply the default styles to the element (for all styles here or that were ever seen in opts.otherStyles)
121
+ this.resetElmStyles()
122
+
123
+ elm.style.setProperty('font-family', opts.fontFamily)
124
+ elm.style.setProperty('font-style', opts.fontStyle)
125
+ elm.style.setProperty('font-weight', opts.fontWeight)
126
+ elm.style.setProperty('font-size', opts.fontSize + 'px')
127
+ elm.style.setProperty('line-height', opts.lineHeight * opts.fontSize + 'px')
128
+ elm.style.setProperty('padding', opts.padding)
129
+
130
+ if (opts.maxWidth) {
131
+ elm.style.setProperty('max-width', opts.maxWidth + 'px')
132
+ }
133
+
134
+ if (opts.minWidth) {
135
+ elm.style.setProperty('min-width', opts.minWidth + 'px')
136
+ }
137
+
138
+ if (opts.disableOverflowWrapBreaking) {
139
+ elm.style.setProperty('overflow-wrap', 'normal')
140
+ }
141
+
117
142
  if (opts.otherStyles) {
118
143
  for (const [key, value] of Object.entries(opts.otherStyles)) {
119
- wrapperElm.style.setProperty(key, value)
144
+ elm.style.setProperty(key, value)
120
145
  }
121
146
  }
122
147
 
123
- const scrollWidth = wrapperElm.scrollWidth
124
- const rect = wrapperElm.getBoundingClientRect()
125
- wrapperElm.remove()
148
+ const scrollWidth = opts.measureScrollWidth ? elm.scrollWidth : 0
149
+ const rect = elm.getBoundingClientRect()
126
150
 
127
151
  return {
128
152
  x: 0,
@@ -247,27 +271,29 @@ export class TextManager {
247
271
  ): { text: string; box: BoxModel }[] {
248
272
  if (textToMeasure === '') return []
249
273
 
250
- const elm = this.baseElem.cloneNode() as HTMLDivElement
251
- this.editor.getContainer().appendChild(elm)
274
+ const { elm } = this
275
+
276
+ if (opts.otherStyles) {
277
+ for (const key in opts.otherStyles) {
278
+ if (!this.defaultStyles[key]) {
279
+ // we need to save the original style so that we can restore it when we're done
280
+ this.defaultStyles[key] = elm.style.getPropertyValue(key)
281
+ }
282
+ }
283
+ }
284
+
285
+ this.resetElmStyles()
286
+
287
+ elm.style.setProperty('font-family', opts.fontFamily)
288
+ elm.style.setProperty('font-style', opts.fontStyle)
289
+ elm.style.setProperty('font-weight', opts.fontWeight)
290
+ elm.style.setProperty('font-size', opts.fontSize + 'px')
291
+ elm.style.setProperty('line-height', opts.lineHeight * opts.fontSize + 'px')
252
292
 
253
293
  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
294
  elm.style.setProperty('width', `${elementWidth}px`)
259
295
  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
296
  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
297
 
272
298
  const shouldTruncateToFirstLine =
273
299
  opts.overflow === 'truncate-ellipsis' || opts.overflow === 'truncate-clip'
@@ -277,6 +303,12 @@ export class TextManager {
277
303
  elm.style.setProperty('word-break', 'break-all')
278
304
  }
279
305
 
306
+ if (opts.otherStyles) {
307
+ for (const [key, value] of Object.entries(opts.otherStyles)) {
308
+ elm.style.setProperty(key, value)
309
+ }
310
+ }
311
+
280
312
  const normalizedText = normalizeTextForDom(textToMeasure)
281
313
 
282
314
  // Render the text into the measurement element:
@@ -313,11 +345,10 @@ export class TextManager {
313
345
  h: lastSpan.box.h,
314
346
  },
315
347
  })
348
+
316
349
  return truncatedSpans
317
350
  }
318
351
 
319
- elm.remove()
320
-
321
352
  return spans
322
353
  }
323
354
  }
@@ -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
  }
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.b34920584558'
4
+ export const version = '3.14.0-canary.b58d09b378a2'
5
5
  export const publishDates = {
6
6
  major: '2024-09-13T14:36:29.063Z',
7
- minor: '2025-06-13T12:47:22.619Z',
8
- patch: '2025-06-13T12:47:22.619Z',
7
+ minor: '2025-06-23T08:34:38.965Z',
8
+ patch: '2025-06-23T08:34:38.965Z',
9
9
  }