@tldraw/editor 3.14.0-canary.f2737654a470 → 3.14.0-canary.f27d19561239

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 +37 -51
  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 +1 -25
  5. package/dist-cjs/lib/editor/Editor.js.map +2 -2
  6. package/dist-cjs/lib/editor/managers/TextManager/TextManager.js +42 -73
  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 +2 -1
  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 +37 -51
  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 +1 -25
  19. package/dist-esm/lib/editor/Editor.mjs.map +2 -2
  20. package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs +42 -73
  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 +2 -1
  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 +482 -433
  29. package/package.json +7 -7
  30. package/src/index.ts +0 -2
  31. package/src/lib/editor/Editor.ts +1 -28
  32. package/src/lib/editor/managers/TextManager/TextManager.test.ts +5 -1
  33. package/src/lib/editor/managers/TextManager/TextManager.ts +86 -117
  34. package/src/lib/editor/tools/StateNode.ts +3 -3
  35. package/src/lib/editor/types/external-content.ts +2 -11
  36. package/src/lib/hooks/useCanvasEvents.ts +1 -0
  37. 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.f2737654a470",
4
+ "version": "3.14.0-canary.f27d19561239",
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.f2737654a470",
52
- "@tldraw/state-react": "3.14.0-canary.f2737654a470",
53
- "@tldraw/store": "3.14.0-canary.f2737654a470",
54
- "@tldraw/tlschema": "3.14.0-canary.f2737654a470",
55
- "@tldraw/utils": "3.14.0-canary.f2737654a470",
56
- "@tldraw/validate": "3.14.0-canary.f2737654a470",
51
+ "@tldraw/state": "3.14.0-canary.f27d19561239",
52
+ "@tldraw/state-react": "3.14.0-canary.f27d19561239",
53
+ "@tldraw/store": "3.14.0-canary.f27d19561239",
54
+ "@tldraw/tlschema": "3.14.0-canary.f27d19561239",
55
+ "@tldraw/utils": "3.14.0-canary.f27d19561239",
56
+ "@tldraw/validate": "3.14.0-canary.f27d19561239",
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,7 +174,6 @@ export {
174
174
  } from './lib/editor/managers/SnapManager/SnapManager'
175
175
  export {
176
176
  TextManager,
177
- type TLMeasureTextOpts,
178
177
  type TLMeasureTextSpanOpts,
179
178
  } from './lib/editor/managers/TextManager/TextManager'
180
179
  export { UserPreferencesManager } from './lib/editor/managers/UserPreferencesManager/UserPreferencesManager'
@@ -253,7 +252,6 @@ export {
253
252
  type TLExternalContent,
254
253
  type TLExternalContentSource,
255
254
  type TLFileExternalAsset,
256
- type TLFileReplaceExternalContent,
257
255
  type TLFilesExternalContent,
258
256
  type TLSvgTextExternalContent,
259
257
  type TLTextExternalContent,
@@ -348,8 +348,6 @@ 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
-
353
351
  this.fonts = new FontManager(this, fontAssetUrls)
354
352
 
355
353
  this._tickManager = new TickManager(this)
@@ -2122,20 +2120,6 @@ export class Editor extends EventEmitter<TLEventMap> {
2122
2120
  return this.getShapesPageBounds(this.getSelectedShapeIds())
2123
2121
  }
2124
2122
 
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
-
2139
2123
  /**
2140
2124
  * @internal
2141
2125
  */
@@ -6211,12 +6195,11 @@ export class Editor extends EventEmitter<TLEventMap> {
6211
6195
  */
6212
6196
  duplicateShapes(shapes: TLShapeId[] | TLShape[], offset?: VecLike): this {
6213
6197
  this.run(() => {
6214
- const _ids =
6198
+ const ids =
6215
6199
  typeof shapes[0] === 'string'
6216
6200
  ? (shapes as TLShapeId[])
6217
6201
  : (shapes as TLShape[]).map((s) => s.id)
6218
6202
 
6219
- const ids = this._shouldIgnoreShapeLock ? _ids : this._getUnlockedShapeIds(_ids)
6220
6203
  if (ids.length <= 0) return this
6221
6204
 
6222
6205
  const initialIds = new Set(ids)
@@ -8828,7 +8811,6 @@ export class Editor extends EventEmitter<TLEventMap> {
8828
8811
  } = {
8829
8812
  text: null,
8830
8813
  files: null,
8831
- 'file-replace': null,
8832
8814
  embed: null,
8833
8815
  'svg-text': null,
8834
8816
  url: null,
@@ -8878,15 +8860,6 @@ export class Editor extends EventEmitter<TLEventMap> {
8878
8860
  return this.externalContentHandlers[info.type]?.(info as any)
8879
8861
  }
8880
8862
 
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
-
8890
8863
  /**
8891
8864
  * Get content that can be exported for the given shape ids.
8892
8865
  *
@@ -99,7 +99,7 @@ describe('TextManager', () => {
99
99
  })
100
100
 
101
101
  it('should handle empty text', () => {
102
- const result = textManager.measureText('', { ...defaultOpts, measureScrollWidth: true })
102
+ const result = textManager.measureText('', defaultOpts)
103
103
  expect(result).toHaveProperty('x', 0)
104
104
  expect(result).toHaveProperty('y', 0)
105
105
  expect(result).toHaveProperty('w')
@@ -128,6 +128,7 @@ describe('TextManager', () => {
128
128
  y: 0,
129
129
  w: expect.any(Number),
130
130
  h: expect.any(Number),
131
+ scrollWidth: expect.any(Number),
131
132
  })
132
133
  })
133
134
 
@@ -140,6 +141,7 @@ describe('TextManager', () => {
140
141
  y: 0,
141
142
  w: expect.any(Number),
142
143
  h: expect.any(Number),
144
+ scrollWidth: expect.any(Number),
143
145
  })
144
146
  })
145
147
 
@@ -152,6 +154,7 @@ describe('TextManager', () => {
152
154
  y: 0,
153
155
  w: expect.any(Number),
154
156
  h: expect.any(Number),
157
+ scrollWidth: expect.any(Number),
155
158
  })
156
159
  })
157
160
 
@@ -170,6 +173,7 @@ describe('TextManager', () => {
170
173
  y: 0,
171
174
  w: expect.any(Number),
172
175
  h: expect.any(Number),
176
+ scrollWidth: expect.any(Number),
173
177
  })
174
178
  })
175
179
  })
@@ -20,27 +20,6 @@ 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
-
44
23
  /** @public */
45
24
  export interface TLMeasureTextSpanOpts {
46
25
  overflow: 'wrap' | 'truncate-ellipsis' | 'truncate-clip'
@@ -54,99 +33,96 @@ export interface TLMeasureTextSpanOpts {
54
33
  lineHeight: number
55
34
  textAlign: TLDefaultHorizontalAlignStyle
56
35
  otherStyles?: Record<string, string>
57
- measureScrollWidth?: boolean
58
36
  }
59
37
 
60
38
  const spaceCharacterRegex = /\s/
61
39
 
62
40
  /** @public */
63
41
  export class TextManager {
64
- private elm: HTMLDivElement
65
- private defaultStyles: Record<string, string | null>
42
+ private baseElem: HTMLDivElement
66
43
 
67
44
  constructor(public editor: Editor) {
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
87
- }
88
-
89
- dispose() {
90
- return this.elm.remove()
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
91
49
  }
92
50
 
93
- private resetElmStyles() {
94
- const { elm, defaultStyles } = this
95
- for (const key in defaultStyles) {
96
- elm.style.setProperty(key, defaultStyles[key])
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
97
68
  }
98
- }
99
-
100
- measureText(textToMeasure: string, opts: TLMeasureTextOpts): BoxModel & { scrollWidth: number } {
69
+ ): BoxModel & { scrollWidth: number } {
101
70
  const div = document.createElement('div')
102
71
  div.textContent = normalizeTextForDom(textToMeasure)
103
72
  return this.measureHtml(div.innerHTML, opts)
104
73
  }
105
74
 
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
- }
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
116
93
  }
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
-
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
+ )
142
117
  if (opts.otherStyles) {
143
118
  for (const [key, value] of Object.entries(opts.otherStyles)) {
144
- elm.style.setProperty(key, value)
119
+ wrapperElm.style.setProperty(key, value)
145
120
  }
146
121
  }
147
122
 
148
- const scrollWidth = opts.measureScrollWidth ? elm.scrollWidth : 0
149
- const rect = elm.getBoundingClientRect()
123
+ const scrollWidth = wrapperElm.scrollWidth
124
+ const rect = wrapperElm.getBoundingClientRect()
125
+ wrapperElm.remove()
150
126
 
151
127
  return {
152
128
  x: 0,
@@ -271,29 +247,27 @@ export class TextManager {
271
247
  ): { text: string; box: BoxModel }[] {
272
248
  if (textToMeasure === '') return []
273
249
 
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')
250
+ const elm = this.baseElem.cloneNode() as HTMLDivElement
251
+ this.editor.getContainer().appendChild(elm)
292
252
 
293
253
  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')
294
258
  elm.style.setProperty('width', `${elementWidth}px`)
295
259
  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`)
296
264
  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
+ }
297
271
 
298
272
  const shouldTruncateToFirstLine =
299
273
  opts.overflow === 'truncate-ellipsis' || opts.overflow === 'truncate-clip'
@@ -303,12 +277,6 @@ export class TextManager {
303
277
  elm.style.setProperty('word-break', 'break-all')
304
278
  }
305
279
 
306
- if (opts.otherStyles) {
307
- for (const [key, value] of Object.entries(opts.otherStyles)) {
308
- elm.style.setProperty(key, value)
309
- }
310
- }
311
-
312
280
  const normalizedText = normalizeTextForDom(textToMeasure)
313
281
 
314
282
  // Render the text into the measurement element:
@@ -345,10 +313,11 @@ export class TextManager {
345
313
  h: lastSpan.box.h,
346
314
  },
347
315
  })
348
-
349
316
  return truncatedSpans
350
317
  }
351
318
 
319
+ elm.remove()
320
+
352
321
  return spans
353
322
  }
354
323
  }
@@ -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, to: string) {
209
+ exit(info: any, from: 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, to)
214
+ this.onExit?.(info, from)
215
215
 
216
216
  if (!this.getIsActive()) {
217
- this.getCurrent()?.exit(info, to)
217
+ this.getCurrent()?.exit(info, from)
218
218
  }
219
219
  }
220
220
 
@@ -1,4 +1,4 @@
1
- import { TLAssetId, TLShapeId } from '@tldraw/tlschema'
1
+ import { TLAssetId } from '@tldraw/tlschema'
2
2
  import { VecLike } from '../../primitives/Vec'
3
3
  import { TLContent } from './clipboard-types'
4
4
 
@@ -52,15 +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
56
- }
57
-
58
- /** @public */
59
- export interface TLFileReplaceExternalContent extends TLBaseExternalContent {
60
- type: 'file-replace'
61
- file: File
62
- shapeId: TLShapeId
63
- isImage: boolean
55
+ ignoreParent: boolean
64
56
  }
65
57
 
66
58
  /** @public */
@@ -98,7 +90,6 @@ export interface TLExcalidrawExternalContent extends TLBaseExternalContent {
98
90
  export type TLExternalContent<EmbedDefinition> =
99
91
  | TLTextExternalContent
100
92
  | TLFilesExternalContent
101
- | TLFileReplaceExternalContent
102
93
  | TLUrlExternalContent
103
94
  | TLSvgTextExternalContent
104
95
  | TLEmbedExternalContent<EmbedDefinition>
@@ -137,6 +137,7 @@ export function useCanvasEvents() {
137
137
  type: 'files',
138
138
  files,
139
139
  point: editor.screenToPage({ x: e.clientX, y: e.clientY }),
140
+ ignoreParent: false,
140
141
  })
141
142
  return
142
143
  }
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.f2737654a470'
4
+ export const version = '3.14.0-canary.f27d19561239'
5
5
  export const publishDates = {
6
6
  major: '2024-09-13T14:36:29.063Z',
7
- minor: '2025-06-23T08:22:59.352Z',
8
- patch: '2025-06-23T08:22:59.352Z',
7
+ minor: '2025-06-12T10:52:09.975Z',
8
+ patch: '2025-06-12T10:52:09.975Z',
9
9
  }