@tldraw/editor 3.14.0-canary.2f3caa391d5d → 3.14.0-canary.306affbc4326

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 (40) hide show
  1. package/dist-cjs/index.d.ts +27 -34
  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 -0
  5. package/dist-cjs/lib/editor/Editor.js.map +2 -2
  6. package/dist-cjs/lib/editor/managers/FontManager/FontManager.js +1 -2
  7. package/dist-cjs/lib/editor/managers/FontManager/FontManager.js.map +2 -2
  8. package/dist-cjs/lib/editor/managers/TextManager/TextManager.js +72 -42
  9. package/dist-cjs/lib/editor/managers/TextManager/TextManager.js.map +2 -2
  10. package/dist-cjs/lib/editor/shapes/group/GroupShapeUtil.js +1 -1
  11. package/dist-cjs/lib/editor/shapes/group/GroupShapeUtil.js.map +1 -1
  12. package/dist-cjs/lib/utils/richText.js +7 -2
  13. package/dist-cjs/lib/utils/richText.js.map +2 -2
  14. package/dist-cjs/version.js +3 -3
  15. package/dist-cjs/version.js.map +1 -1
  16. package/dist-esm/index.d.mts +27 -34
  17. package/dist-esm/index.mjs +1 -1
  18. package/dist-esm/index.mjs.map +2 -2
  19. package/dist-esm/lib/editor/Editor.mjs +1 -0
  20. package/dist-esm/lib/editor/Editor.mjs.map +2 -2
  21. package/dist-esm/lib/editor/managers/FontManager/FontManager.mjs +1 -2
  22. package/dist-esm/lib/editor/managers/FontManager/FontManager.mjs.map +2 -2
  23. package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs +72 -42
  24. package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs.map +2 -2
  25. package/dist-esm/lib/editor/shapes/group/GroupShapeUtil.mjs +1 -1
  26. package/dist-esm/lib/editor/shapes/group/GroupShapeUtil.mjs.map +1 -1
  27. package/dist-esm/lib/utils/richText.mjs +8 -3
  28. package/dist-esm/lib/utils/richText.mjs.map +2 -2
  29. package/dist-esm/version.mjs +3 -3
  30. package/dist-esm/version.mjs.map +1 -1
  31. package/editor.css +458 -523
  32. package/package.json +7 -7
  33. package/src/index.ts +1 -0
  34. package/src/lib/editor/Editor.ts +2 -0
  35. package/src/lib/editor/managers/FontManager/FontManager.ts +1 -2
  36. package/src/lib/editor/managers/TextManager/TextManager.test.ts +1 -5
  37. package/src/lib/editor/managers/TextManager/TextManager.ts +116 -86
  38. package/src/lib/editor/shapes/group/GroupShapeUtil.tsx +1 -1
  39. package/src/lib/utils/richText.ts +9 -3
  40. 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.2f3caa391d5d",
4
+ "version": "3.14.0-canary.306affbc4326",
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.2f3caa391d5d",
52
- "@tldraw/state-react": "3.14.0-canary.2f3caa391d5d",
53
- "@tldraw/store": "3.14.0-canary.2f3caa391d5d",
54
- "@tldraw/tlschema": "3.14.0-canary.2f3caa391d5d",
55
- "@tldraw/utils": "3.14.0-canary.2f3caa391d5d",
56
- "@tldraw/validate": "3.14.0-canary.2f3caa391d5d",
51
+ "@tldraw/state": "3.14.0-canary.306affbc4326",
52
+ "@tldraw/state-react": "3.14.0-canary.306affbc4326",
53
+ "@tldraw/store": "3.14.0-canary.306affbc4326",
54
+ "@tldraw/tlschema": "3.14.0-canary.306affbc4326",
55
+ "@tldraw/utils": "3.14.0-canary.306affbc4326",
56
+ "@tldraw/validate": "3.14.0-canary.306affbc4326",
57
57
  "@types/core-js": "^2.5.8",
58
58
  "@use-gesture/react": "^10.3.1",
59
59
  "classnames": "^2.5.1",
package/src/index.ts CHANGED
@@ -174,6 +174,7 @@ export {
174
174
  } from './lib/editor/managers/SnapManager/SnapManager'
175
175
  export {
176
176
  TextManager,
177
+ type TLMeasureTextOpts,
177
178
  type TLMeasureTextSpanOpts,
178
179
  } from './lib/editor/managers/TextManager/TextManager'
179
180
  export { UserPreferencesManager } from './lib/editor/managers/UserPreferencesManager/UserPreferencesManager'
@@ -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)
@@ -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() {
@@ -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.2f3caa391d5d'
4
+ export const version = '3.14.0-canary.306affbc4326'
5
5
  export const publishDates = {
6
6
  major: '2024-09-13T14:36:29.063Z',
7
- minor: '2025-06-11T14:23:44.452Z',
8
- patch: '2025-06-11T14:23:44.452Z',
7
+ minor: '2025-06-16T13:31:38.369Z',
8
+ patch: '2025-06-16T13:31:38.369Z',
9
9
  }