@tldraw/editor 4.6.0-internal.e29318c66fb0 → 4.6.0-next.0eb36d65eec3

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 (134) hide show
  1. package/dist-cjs/index.d.ts +124 -6
  2. package/dist-cjs/index.js +1 -1
  3. package/dist-cjs/index.js.map +2 -2
  4. package/dist-cjs/lib/TldrawEditor.js.map +2 -2
  5. package/dist-cjs/lib/components/default-components/DefaultCanvas.js +1 -1
  6. package/dist-cjs/lib/components/default-components/DefaultCanvas.js.map +2 -2
  7. package/dist-cjs/lib/components/default-components/DefaultCollaboratorHint.js +1 -1
  8. package/dist-cjs/lib/components/default-components/DefaultCollaboratorHint.js.map +2 -2
  9. package/dist-cjs/lib/config/createTLStore.js +1 -1
  10. package/dist-cjs/lib/config/createTLStore.js.map +2 -2
  11. package/dist-cjs/lib/config/defaultShapes.js.map +2 -2
  12. package/dist-cjs/lib/editor/Editor.js +4 -4
  13. package/dist-cjs/lib/editor/Editor.js.map +2 -2
  14. package/dist-cjs/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.js +1 -1
  15. package/dist-cjs/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.js.map +2 -2
  16. package/dist-cjs/lib/editor/managers/SnapManager/BoundsSnaps.js +1 -1
  17. package/dist-cjs/lib/editor/managers/SnapManager/BoundsSnaps.js.map +2 -2
  18. package/dist-cjs/lib/editor/managers/SnapManager/HandleSnaps.js.map +2 -2
  19. package/dist-cjs/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.js +4 -4
  20. package/dist-cjs/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.js.map +2 -2
  21. package/dist-cjs/lib/editor/managers/TextManager/TextManager.js +102 -30
  22. package/dist-cjs/lib/editor/managers/TextManager/TextManager.js.map +2 -2
  23. package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js.map +2 -2
  24. package/dist-cjs/lib/editor/shapes/ShapeUtil.js.map +1 -1
  25. package/dist-cjs/lib/exports/StyleEmbedder.js +1 -1
  26. package/dist-cjs/lib/exports/StyleEmbedder.js.map +2 -2
  27. package/dist-cjs/lib/exports/getSvgJsx.js.map +2 -2
  28. package/dist-cjs/lib/hooks/useEditorComponents.js.map +2 -2
  29. package/dist-cjs/lib/hooks/useLocalStore.js.map +2 -2
  30. package/dist-cjs/lib/options.js +3 -0
  31. package/dist-cjs/lib/options.js.map +2 -2
  32. package/dist-cjs/lib/primitives/Box.js +1 -1
  33. package/dist-cjs/lib/primitives/Box.js.map +2 -2
  34. package/dist-cjs/lib/primitives/geometry/Arc2d.js +2 -2
  35. package/dist-cjs/lib/primitives/geometry/Arc2d.js.map +2 -2
  36. package/dist-cjs/lib/primitives/geometry/Circle2d.js +2 -2
  37. package/dist-cjs/lib/primitives/geometry/Circle2d.js.map +2 -2
  38. package/dist-cjs/lib/primitives/geometry/Ellipse2d.js +2 -2
  39. package/dist-cjs/lib/primitives/geometry/Ellipse2d.js.map +2 -2
  40. package/dist-cjs/lib/primitives/geometry/Geometry2d.js +4 -3
  41. package/dist-cjs/lib/primitives/geometry/Geometry2d.js.map +2 -2
  42. package/dist-cjs/lib/primitives/geometry/Polyline2d.js +1 -1
  43. package/dist-cjs/lib/primitives/geometry/Polyline2d.js.map +2 -2
  44. package/dist-cjs/lib/primitives/geometry/Stadium2d.js +1 -1
  45. package/dist-cjs/lib/primitives/geometry/Stadium2d.js.map +2 -2
  46. package/dist-cjs/lib/utils/getSvgPathFromPoints.js.map +2 -2
  47. package/dist-cjs/lib/utils/sync/TLLocalSyncClient.js +1 -1
  48. package/dist-cjs/lib/utils/sync/TLLocalSyncClient.js.map +2 -2
  49. package/dist-cjs/version.js +3 -3
  50. package/dist-cjs/version.js.map +1 -1
  51. package/dist-esm/index.d.mts +124 -6
  52. package/dist-esm/index.mjs +4 -2
  53. package/dist-esm/index.mjs.map +2 -2
  54. package/dist-esm/lib/TldrawEditor.mjs.map +2 -2
  55. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs +1 -1
  56. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs.map +2 -2
  57. package/dist-esm/lib/components/default-components/DefaultCollaboratorHint.mjs +1 -1
  58. package/dist-esm/lib/components/default-components/DefaultCollaboratorHint.mjs.map +2 -2
  59. package/dist-esm/lib/config/createTLStore.mjs +1 -1
  60. package/dist-esm/lib/config/createTLStore.mjs.map +2 -2
  61. package/dist-esm/lib/config/defaultShapes.mjs.map +2 -2
  62. package/dist-esm/lib/editor/Editor.mjs +6 -6
  63. package/dist-esm/lib/editor/Editor.mjs.map +2 -2
  64. package/dist-esm/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.mjs +1 -1
  65. package/dist-esm/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.mjs.map +2 -2
  66. package/dist-esm/lib/editor/managers/SnapManager/BoundsSnaps.mjs +1 -1
  67. package/dist-esm/lib/editor/managers/SnapManager/BoundsSnaps.mjs.map +2 -2
  68. package/dist-esm/lib/editor/managers/SnapManager/HandleSnaps.mjs.map +2 -2
  69. package/dist-esm/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.mjs +4 -4
  70. package/dist-esm/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.mjs.map +2 -2
  71. package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs +102 -30
  72. package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs.map +2 -2
  73. package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs.map +2 -2
  74. package/dist-esm/lib/editor/shapes/ShapeUtil.mjs.map +1 -1
  75. package/dist-esm/lib/exports/StyleEmbedder.mjs +1 -1
  76. package/dist-esm/lib/exports/StyleEmbedder.mjs.map +2 -2
  77. package/dist-esm/lib/exports/getSvgJsx.mjs.map +2 -2
  78. package/dist-esm/lib/hooks/useEditorComponents.mjs.map +2 -2
  79. package/dist-esm/lib/hooks/useLocalStore.mjs.map +2 -2
  80. package/dist-esm/lib/options.mjs +3 -0
  81. package/dist-esm/lib/options.mjs.map +2 -2
  82. package/dist-esm/lib/primitives/Box.mjs +1 -1
  83. package/dist-esm/lib/primitives/Box.mjs.map +2 -2
  84. package/dist-esm/lib/primitives/geometry/Arc2d.mjs +2 -2
  85. package/dist-esm/lib/primitives/geometry/Arc2d.mjs.map +2 -2
  86. package/dist-esm/lib/primitives/geometry/Circle2d.mjs +2 -2
  87. package/dist-esm/lib/primitives/geometry/Circle2d.mjs.map +2 -2
  88. package/dist-esm/lib/primitives/geometry/Ellipse2d.mjs +2 -2
  89. package/dist-esm/lib/primitives/geometry/Ellipse2d.mjs.map +2 -2
  90. package/dist-esm/lib/primitives/geometry/Geometry2d.mjs +4 -3
  91. package/dist-esm/lib/primitives/geometry/Geometry2d.mjs.map +2 -2
  92. package/dist-esm/lib/primitives/geometry/Polyline2d.mjs +1 -1
  93. package/dist-esm/lib/primitives/geometry/Polyline2d.mjs.map +2 -2
  94. package/dist-esm/lib/primitives/geometry/Stadium2d.mjs +1 -1
  95. package/dist-esm/lib/primitives/geometry/Stadium2d.mjs.map +2 -2
  96. package/dist-esm/lib/utils/getSvgPathFromPoints.mjs.map +2 -2
  97. package/dist-esm/lib/utils/sync/TLLocalSyncClient.mjs +1 -1
  98. package/dist-esm/lib/utils/sync/TLLocalSyncClient.mjs.map +2 -2
  99. package/dist-esm/version.mjs +3 -3
  100. package/dist-esm/version.mjs.map +1 -1
  101. package/package.json +7 -7
  102. package/src/index.ts +8 -1
  103. package/src/lib/TldrawEditor.tsx +1 -1
  104. package/src/lib/components/default-components/DefaultCanvas.tsx +1 -1
  105. package/src/lib/components/default-components/DefaultCollaboratorHint.tsx +1 -1
  106. package/src/lib/config/TLEditorSnapshot.test.ts +1 -1
  107. package/src/lib/config/createTLStore.ts +1 -1
  108. package/src/lib/config/defaultShapes.ts +1 -1
  109. package/src/lib/editor/Editor.ts +9 -8
  110. package/src/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.ts +1 -1
  111. package/src/lib/editor/managers/SnapManager/BoundsSnaps.ts +1 -1
  112. package/src/lib/editor/managers/SnapManager/HandleSnaps.ts +1 -1
  113. package/src/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.ts +5 -5
  114. package/src/lib/editor/managers/TextManager/TextManager.test.ts +18 -4
  115. package/src/lib/editor/managers/TextManager/TextManager.ts +140 -34
  116. package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.test.ts +1 -1
  117. package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.ts +1 -1
  118. package/src/lib/editor/shapes/ShapeUtil.ts +2 -2
  119. package/src/lib/exports/StyleEmbedder.ts +1 -1
  120. package/src/lib/exports/getSvgJsx.tsx +1 -1
  121. package/src/lib/hooks/useEditorComponents.tsx +1 -1
  122. package/src/lib/hooks/useLocalStore.ts +1 -1
  123. package/src/lib/options.ts +108 -0
  124. package/src/lib/primitives/Box.ts +1 -1
  125. package/src/lib/primitives/geometry/Arc2d.ts +2 -2
  126. package/src/lib/primitives/geometry/Circle2d.ts +2 -2
  127. package/src/lib/primitives/geometry/Ellipse2d.ts +2 -2
  128. package/src/lib/primitives/geometry/Geometry2d.ts +4 -3
  129. package/src/lib/primitives/geometry/Polyline2d.ts +1 -1
  130. package/src/lib/primitives/geometry/Stadium2d.ts +1 -1
  131. package/src/lib/utils/getSvgPathFromPoints.ts +1 -1
  132. package/src/lib/utils/sync/TLLocalSyncClient.test.ts +1 -1
  133. package/src/lib/utils/sync/TLLocalSyncClient.ts +1 -1
  134. package/src/version.ts +3 -3
@@ -10,10 +10,10 @@ import {
10
10
  import {
11
11
  ComputedCache,
12
12
  RecordType,
13
- StoreSideEffects,
14
- StoreSnapshot,
15
13
  UnknownRecord,
16
14
  reverseRecordsDiff,
15
+ StoreSnapshot,
16
+ StoreSideEffects,
17
17
  } from '@tldraw/store'
18
18
  import {
19
19
  CameraRecordType,
@@ -90,15 +90,15 @@ import {
90
90
  uniqueId,
91
91
  } from '@tldraw/utils'
92
92
  import EventEmitter from 'eventemitter3'
93
+ import { TLUser, createTLUser } from '../config/createTLUser'
94
+ import { TLAnyBindingUtilConstructor, checkBindings } from '../config/defaultBindings'
95
+ import { TLAnyShapeUtilConstructor, checkShapesAndAddCore } from '../config/defaultShapes'
93
96
  import {
94
97
  TLEditorSnapshot,
95
98
  TLLoadSnapshotOptions,
96
99
  getSnapshot,
97
100
  loadSnapshot,
98
101
  } from '../config/TLEditorSnapshot'
99
- import { TLUser, createTLUser } from '../config/createTLUser'
100
- import { TLAnyBindingUtilConstructor, checkBindings } from '../config/defaultBindings'
101
- import { TLAnyShapeUtilConstructor, checkShapesAndAddCore } from '../config/defaultShapes'
102
102
  import {
103
103
  DEFAULT_ANIMATION_OPTIONS,
104
104
  DEFAULT_CAMERA_OPTIONS,
@@ -115,14 +115,13 @@ import { tlmenus } from '../globals/menus'
115
115
  import { tltime } from '../globals/time'
116
116
  import { TldrawOptions, defaultTldrawOptions } from '../options'
117
117
  import { Box, BoxLike } from '../primitives/Box'
118
- import { Mat, MatLike } from '../primitives/Mat'
119
- import { Vec, VecLike } from '../primitives/Vec'
120
118
  import { EASINGS } from '../primitives/easings'
121
119
  import { Geometry2d } from '../primitives/geometry/Geometry2d'
122
120
  import { Group2d } from '../primitives/geometry/Group2d'
123
121
  import { intersectPolygonPolygon } from '../primitives/intersect'
122
+ import { Mat, MatLike } from '../primitives/Mat'
124
123
  import { PI, approximately, areAnglesCompatible, clamp, pointInPolygon } from '../primitives/utils'
125
- import { ReadonlySharedStyleMap, SharedStyle, SharedStyleMap } from '../utils/SharedStylesMap'
124
+ import { Vec, VecLike } from '../primitives/Vec'
126
125
  import { areShapesContentEqual } from '../utils/areShapesContentEqual'
127
126
  import { dataUrlToFile } from '../utils/assets'
128
127
  import { debugFlags } from '../utils/debug-flags'
@@ -137,6 +136,7 @@ import { getReorderingShapesChanges } from '../utils/reorderShapes'
137
136
  import { getDroppedShapesToNewParents, kickoutOccludedShapes } from '../utils/reparenting'
138
137
  import { TLTextOptions, TiptapEditor } from '../utils/richText'
139
138
  import { applyRotationToSnapshotShapes, getRotationSnapshot } from '../utils/rotation'
139
+ import { ReadonlySharedStyleMap, SharedStyle, SharedStyleMap } from '../utils/SharedStylesMap'
140
140
  import { BindingOnDeleteOptions, BindingUtil } from './bindings/BindingUtil'
141
141
  import { bindingsIndex } from './derivations/bindingsIndex'
142
142
  import { notVisibleShapes } from './derivations/notVisibleShapes'
@@ -9071,6 +9071,7 @@ export class Editor extends EventEmitter<TLEventMap> {
9071
9071
  opts = {} as { force?: boolean }
9072
9072
  ): Promise<void> {
9073
9073
  if (!opts.force && this.getIsReadonly()) return
9074
+
9074
9075
  return this.externalContentHandlers[info.type]?.(info as any)
9075
9076
  }
9076
9077
 
@@ -1,5 +1,5 @@
1
- import { Vec } from '../../../primitives/Vec'
2
1
  import { EASINGS } from '../../../primitives/easings'
2
+ import { Vec } from '../../../primitives/Vec'
3
3
  import type { Editor } from '../../Editor'
4
4
 
5
5
  /** @public */
@@ -10,8 +10,8 @@ import {
10
10
  isSelectionCorner,
11
11
  } from '../../../primitives/Box'
12
12
  import { Mat } from '../../../primitives/Mat'
13
- import { Vec } from '../../../primitives/Vec'
14
13
  import { rangeIntersection, rangesOverlap } from '../../../primitives/utils'
14
+ import { Vec } from '../../../primitives/Vec'
15
15
  import type { Editor } from '../../Editor'
16
16
  import type {
17
17
  GapsSnapIndicator,
@@ -1,8 +1,8 @@
1
1
  import { computed } from '@tldraw/state'
2
2
  import { TLHandle, TLShape, TLShapeId, VecModel } from '@tldraw/tlschema'
3
3
  import { assertExists, uniqueId } from '@tldraw/utils'
4
- import { Vec } from '../../../primitives/Vec'
5
4
  import { Geometry2d } from '../../../primitives/geometry/Geometry2d'
5
+ import { Vec } from '../../../primitives/Vec'
6
6
  import type { Editor } from '../../Editor'
7
7
  import type { PointsSnapIndicator, SnapData, SnapManager } from './SnapManager'
8
8
 
@@ -1,7 +1,7 @@
1
1
  import { Computed, RESET_VALUE, computed, isUninitialized } from '@tldraw/state'
2
2
  import type { RecordsDiff } from '@tldraw/store'
3
- import type { TLRecord } from '@tldraw/tlschema'
4
3
  import { TLPageId, TLShape, TLShapeId, isShape } from '@tldraw/tlschema'
4
+ import type { TLRecord } from '@tldraw/tlschema'
5
5
  import { objectMapValues } from '@tldraw/utils'
6
6
  import { Box } from '../../../primitives/Box'
7
7
  import type { Editor } from '../../Editor'
@@ -74,7 +74,7 @@ export class SpatialIndexManager {
74
74
  // Collect all shape elements for bulk loading
75
75
  for (const shape of shapes) {
76
76
  const bounds = this.editor.getShapePageBounds(shape.id)
77
- if (bounds) {
77
+ if (bounds && bounds.isValid()) {
78
78
  elements.push({
79
79
  minX: bounds.minX,
80
80
  minY: bounds.minY,
@@ -101,7 +101,7 @@ export class SpatialIndexManager {
101
101
  for (const shape of objectMapValues(changes.added) as TLShape[]) {
102
102
  if (isShape(shape) && this.editor.getAncestorPageId(shape) === this.lastPageId) {
103
103
  const bounds = this.editor.getShapePageBounds(shape.id)
104
- if (bounds) {
104
+ if (bounds && bounds.isValid()) {
105
105
  this.rbush.upsert(shape.id, bounds)
106
106
  }
107
107
  processedShapeIds.add(shape.id)
@@ -125,7 +125,7 @@ export class SpatialIndexManager {
125
125
 
126
126
  if (isOnPage) {
127
127
  const bounds = this.editor.getShapePageBounds(to.id)
128
- if (bounds) {
128
+ if (bounds && bounds.isValid()) {
129
129
  this.rbush.upsert(to.id, bounds)
130
130
  }
131
131
  } else {
@@ -145,7 +145,7 @@ export class SpatialIndexManager {
145
145
  const indexedBounds = this.rbush.getBounds(shapeId)
146
146
 
147
147
  if (!this.areBoundsEqual(currentBounds, indexedBounds)) {
148
- if (currentBounds) {
148
+ if (currentBounds && currentBounds.isValid()) {
149
149
  this.rbush.upsert(shapeId, currentBounds)
150
150
  } else {
151
151
  this.rbush.remove(shapeId)
@@ -3,6 +3,22 @@ import { Editor } from '../../Editor'
3
3
  import { TextManager, TLMeasureTextSpanOpts } from './TextManager'
4
4
 
5
5
  // Create a simple mock DOM environment
6
+ function createMockStyle() {
7
+ const properties = new Map<string, string>()
8
+
9
+ return {
10
+ setProperty: vi.fn((key: string, value: string) => {
11
+ properties.set(key, value)
12
+ }),
13
+ getPropertyValue: vi.fn((key: string) => properties.get(key) ?? ''),
14
+ removeProperty: vi.fn((key: string) => {
15
+ const previous = properties.get(key) ?? ''
16
+ properties.delete(key)
17
+ return previous
18
+ }),
19
+ }
20
+ }
21
+
6
22
  const mockElement = {
7
23
  classList: { add: vi.fn() },
8
24
  tabIndex: -1,
@@ -10,10 +26,7 @@ const mockElement = {
10
26
  innerHTML: '',
11
27
  textContent: '',
12
28
  setAttribute: vi.fn(),
13
- style: {
14
- setProperty: vi.fn(),
15
- getPropertyValue: vi.fn(() => ''),
16
- },
29
+ style: createMockStyle(),
17
30
  scrollWidth: 100,
18
31
  getBoundingClientRect: vi.fn(() => ({
19
32
  width: 100,
@@ -31,6 +44,7 @@ const mockElement = {
31
44
  // Mock document.createElement to return our mock element
32
45
  const mockCreateElement = vi.fn(() => {
33
46
  const element = { ...mockElement }
47
+ element.style = createMockStyle()
34
48
  element.cloneNode = vi.fn(() => ({ ...element }))
35
49
 
36
50
  // Make textContent and innerHTML reactive like real DOM elements
@@ -21,6 +21,23 @@ const textAlignmentsForLtr = {
21
21
  'end-legacy': 'right',
22
22
  }
23
23
 
24
+ interface PoolItem {
25
+ el: HTMLDivElement
26
+ html: string
27
+ appliedStyleKeys: string[]
28
+ }
29
+
30
+ /** @public */
31
+ export interface BatchMeasurementRequest {
32
+ html: string
33
+ opts: TLMeasureTextOpts
34
+ }
35
+
36
+ /** @public */
37
+ export type TLMeasuredTextSize = BoxModel & {
38
+ scrollWidth: number
39
+ }
40
+
24
41
  /** @public */
25
42
  export interface TLMeasureTextOpts {
26
43
  fontStyle: string
@@ -73,53 +90,66 @@ const initialDefaultStyles = Object.freeze({
73
90
  /** @public */
74
91
  export class TextManager {
75
92
  private elm: HTMLDivElement
93
+ private poolElms: PoolItem[] = []
76
94
 
77
95
  constructor(public editor: Editor) {
78
- const elm = editor.getContainerDocument().createElement('div')
96
+ this.elm = this.createMeasurementEl()
97
+ this.editor.getContainer().appendChild(this.elm)
98
+ }
99
+
100
+ private createMeasurementEl(): HTMLDivElement {
101
+ const elm = this.editor.getContainerDocument().createElement('div')
79
102
  elm.classList.add('tl-text')
80
103
  elm.classList.add('tl-text-measure')
81
104
  elm.setAttribute('dir', 'auto')
82
105
  elm.tabIndex = -1
83
- this.editor.getContainer().appendChild(elm)
84
-
85
- this.elm = elm
86
-
87
106
  for (const key of objectMapKeys(initialDefaultStyles)) {
88
107
  elm.style.setProperty(key, initialDefaultStyles[key])
89
108
  }
109
+
110
+ return elm
90
111
  }
91
112
 
92
- private setElementStyles(styles: Record<string, string | undefined>) {
93
- const stylesToReinstate = {} as any
94
- for (const key of objectMapKeys(styles)) {
95
- if (typeof styles[key] === 'string') {
96
- const oldValue = this.elm.style.getPropertyValue(key)
97
- if (oldValue === styles[key]) continue
98
- stylesToReinstate[key] = oldValue
99
- this.elm.style.setProperty(key, styles[key])
100
- }
101
- }
102
- return () => {
103
- for (const key of objectMapKeys(stylesToReinstate)) {
104
- this.elm.style.setProperty(key, stylesToReinstate[key])
113
+ private resetElementStyles(el: HTMLElement, appliedStyleKeys: string[]) {
114
+ for (const key of appliedStyleKeys) {
115
+ if (key in initialDefaultStyles) {
116
+ el.style.setProperty(key, initialDefaultStyles[key as keyof typeof initialDefaultStyles])
117
+ } else {
118
+ el.style.removeProperty(key)
105
119
  }
106
120
  }
107
121
  }
108
122
 
109
- dispose() {
110
- return this.elm.remove()
111
- }
123
+ private setElementStyles(el: HTMLElement, styles: Record<string, string | undefined | null>) {
124
+ type StyleValue = string | null
125
+ type RestoreEntry = [prop: string, value: StyleValue]
112
126
 
113
- measureText(textToMeasure: string, opts: TLMeasureTextOpts): BoxModel & { scrollWidth: number } {
114
- const div = this.editor.getContainerDocument().createElement('div')
115
- div.textContent = normalizeTextForDom(textToMeasure)
116
- return this.measureHtml(div.innerHTML, opts)
117
- }
127
+ const restore: RestoreEntry[] = []
118
128
 
119
- measureHtml(html: string, opts: TLMeasureTextOpts): BoxModel & { scrollWidth: number } {
120
- const { elm } = this
129
+ for (const [key, nextValue] of Object.entries(styles)) {
130
+ const oldValue = el.style.getPropertyValue(key)
121
131
 
122
- const newStyles = {
132
+ if (typeof nextValue === 'string') {
133
+ if (oldValue === nextValue) continue
134
+ restore.push([key, oldValue || null])
135
+ el.style.setProperty(key, nextValue)
136
+ } else {
137
+ if (!oldValue) continue
138
+ restore.push([key, oldValue])
139
+ el.style.removeProperty(key)
140
+ }
141
+ }
142
+
143
+ return () => {
144
+ for (const [key, value] of restore) {
145
+ if (value === null || value === '') el.style.removeProperty(key)
146
+ else el.style.setProperty(key, value)
147
+ }
148
+ }
149
+ }
150
+
151
+ private getMeasureStyles(opts: TLMeasureTextOpts): Record<string, string | undefined> {
152
+ return {
123
153
  'font-family': opts.fontFamily,
124
154
  'font-style': opts.fontStyle,
125
155
  'font-weight': opts.fontWeight,
@@ -128,11 +158,87 @@ export class TextManager {
128
158
  padding: opts.padding,
129
159
  'max-width': opts.maxWidth ? opts.maxWidth + 'px' : undefined,
130
160
  'min-width': opts.minWidth ? opts.minWidth + 'px' : undefined,
131
- 'overflow-wrap': opts.disableOverflowWrapBreaking ? 'normal' : undefined,
161
+ 'overflow-wrap': opts.disableOverflowWrapBreaking ? 'normal' : 'break-word',
132
162
  ...opts.otherStyles,
133
163
  }
164
+ }
165
+
166
+ dispose() {
167
+ this.elm.remove()
168
+ for (const { el } of this.poolElms) {
169
+ el.remove()
170
+ }
171
+ this.poolElms.length = 0
172
+ }
173
+
174
+ private ensurePoolSize(size: number) {
175
+ if (this.poolElms.length >= size) return
176
+
177
+ const fragment = this.editor.getContainerDocument().createDocumentFragment()
178
+ while (this.poolElms.length < size) {
179
+ const el = this.createMeasurementEl()
180
+ this.poolElms.push({ el, html: '', appliedStyleKeys: [] })
181
+ fragment.appendChild(el)
182
+ }
183
+ this.editor.getContainer().appendChild(fragment)
184
+ }
185
+
186
+ private getPoolItem(index: number): PoolItem {
187
+ this.ensurePoolSize(index + 1)
188
+ return this.poolElms[index]
189
+ }
190
+
191
+ measureHtmlBatch(requests: BatchMeasurementRequest[]): TLMeasuredTextSize[] {
192
+ if (requests.length === 0) return []
193
+
194
+ while (this.poolElms.length > requests.length) {
195
+ const { el } = this.poolElms.pop()!
196
+ el.remove()
197
+ }
198
+
199
+ for (let i = 0; i < requests.length; i++) {
200
+ const { html, opts } = requests[i]
201
+ const poolItem = this.getPoolItem(i)
202
+
203
+ const { el } = poolItem
204
+ this.resetElementStyles(el, poolItem.appliedStyleKeys)
205
+ const styles = this.getMeasureStyles(opts)
206
+ this.setElementStyles(el, styles)
207
+ poolItem.appliedStyleKeys = Object.keys(styles)
208
+ // Skip innerHTML parsing if the content hasn't changed
209
+ if (poolItem.html !== html) {
210
+ el.innerHTML = html
211
+ poolItem.html = html
212
+ }
213
+ }
214
+
215
+ const results: TLMeasuredTextSize[] = []
216
+ for (let i = 0; i < requests.length; i++) {
217
+ const el = this.getPoolItem(i).el
218
+ const scrollWidth = requests[i].opts.measureScrollWidth ? el.scrollWidth : 0
219
+ const rect = el.getBoundingClientRect()
220
+ results.push({
221
+ x: 0,
222
+ y: 0,
223
+ w: rect.width,
224
+ h: rect.height,
225
+ scrollWidth,
226
+ })
227
+ }
228
+
229
+ return results
230
+ }
231
+
232
+ measureText(textToMeasure: string, opts: TLMeasureTextOpts): TLMeasuredTextSize {
233
+ const div = this.editor.getContainerDocument().createElement('div')
234
+ div.textContent = normalizeTextForDom(textToMeasure)
235
+ return this.measureHtml(div.innerHTML, opts)
236
+ }
237
+
238
+ measureHtml(html: string, opts: TLMeasureTextOpts): TLMeasuredTextSize {
239
+ const { elm } = this
134
240
 
135
- const restoreStyles = this.setElementStyles(newStyles)
241
+ const restoreStyles = this.setElementStyles(elm, this.getMeasureStyles(opts))
136
242
 
137
243
  try {
138
244
  elm.innerHTML = html
@@ -280,11 +386,11 @@ export class TextManager {
280
386
  width: `${elementWidth}px`,
281
387
  height: 'min-content',
282
388
  'text-align': textAlignmentsForLtr[opts.textAlign],
283
- 'overflow-wrap': shouldTruncateToFirstLine ? 'anywhere' : undefined,
284
- 'word-break': shouldTruncateToFirstLine ? 'break-all' : undefined,
389
+ 'overflow-wrap': shouldTruncateToFirstLine ? 'anywhere' : 'break-word',
390
+ 'word-break': shouldTruncateToFirstLine ? 'break-all' : 'normal',
285
391
  ...opts.otherStyles,
286
392
  }
287
- const restoreStyles = this.setElementStyles(newStyles)
393
+ const restoreStyles = this.setElementStyles(elm, newStyles)
288
394
 
289
395
  try {
290
396
  const normalizedText = normalizeTextForDom(textToMeasure)
@@ -1,7 +1,7 @@
1
1
  import { atom } from '@tldraw/state'
2
2
  import { Mocked, vi } from 'vitest'
3
- import { TLUserPreferences, defaultUserPreferences } from '../../../config/TLUserPreferences'
4
3
  import { TLUser } from '../../../config/createTLUser'
4
+ import { TLUserPreferences, defaultUserPreferences } from '../../../config/TLUserPreferences'
5
5
  import { UserPreferencesManager } from './UserPreferencesManager'
6
6
 
7
7
  // Mock window.matchMedia
@@ -1,6 +1,6 @@
1
1
  import { atom, computed } from '@tldraw/state'
2
- import { TLUserPreferences, defaultUserPreferences } from '../../../config/TLUserPreferences'
3
2
  import { TLUser } from '../../../config/createTLUser'
3
+ import { TLUserPreferences, defaultUserPreferences } from '../../../config/TLUserPreferences'
4
4
  import { getGlobalWindow } from '../../../utils/dom'
5
5
 
6
6
  /** @public */
@@ -15,15 +15,15 @@ import {
15
15
  import { IndexKey } from '@tldraw/utils'
16
16
  import { ReactElement } from 'react'
17
17
  import { Box, SelectionHandle } from '../../primitives/Box'
18
- import { Vec } from '../../primitives/Vec'
19
18
  import { Geometry2d } from '../../primitives/geometry/Geometry2d'
19
+ import { Vec } from '../../primitives/Vec'
20
20
  import type { Editor } from '../Editor'
21
21
  import { TLFontFace } from '../managers/FontManager/FontManager'
22
22
  import { BoundsSnapGeometry } from '../managers/SnapManager/BoundsSnaps'
23
23
  import { HandleSnapGeometry } from '../managers/SnapManager/HandleSnaps'
24
- import { SvgExportContext } from '../types/SvgExportContext'
25
24
  import { TLClickEventInfo } from '../types/event-types'
26
25
  import { TLResizeHandle } from '../types/selection-types'
26
+ import { SvgExportContext } from '../types/SvgExportContext'
27
27
 
28
28
  /** @public */
29
29
  export interface TLShapeUtilConstructor<T extends TLShape, U extends ShapeUtil<T> = ShapeUtil<T>> {
@@ -1,5 +1,4 @@
1
1
  import { assertExists, getOwnProperty, objectMapValues, uniqueId } from '@tldraw/utils'
2
- import { FontEmbedder } from './FontEmbedder'
3
2
  import { ReadonlyStyles, Styles, cssRules } from './cssRules'
4
3
  import {
5
4
  elementStyle,
@@ -9,6 +8,7 @@ import {
9
8
  isElement,
10
9
  } from './domUtils'
11
10
  import { resourceToDataUrl } from './fetchCache'
11
+ import { FontEmbedder } from './FontEmbedder'
12
12
  import { parseCssValueUrls, shouldIncludeCssProperty } from './parseCss'
13
13
 
14
14
  const NO_STYLES = {} as const
@@ -23,12 +23,12 @@ import { InnerShape, InnerShapeBackground } from '../components/Shape'
23
23
  import type { Editor, TLRenderingShape } from '../editor/Editor'
24
24
  import { TLFontFace } from '../editor/managers/FontManager/FontManager'
25
25
  import { ShapeUtil } from '../editor/shapes/ShapeUtil'
26
+ import { TLImageExportOptions } from '../editor/types/misc-types'
26
27
  import {
27
28
  SvgExportContext,
28
29
  SvgExportContextProvider,
29
30
  SvgExportDef,
30
31
  } from '../editor/types/SvgExportContext'
31
- import { TLImageExportOptions } from '../editor/types/misc-types'
32
32
  import { useEditor } from '../hooks/useEditor'
33
33
  import { useEvent } from '../hooks/useEvent'
34
34
  import { suffixSafeId, useUniqueSafeId } from '../hooks/useSafeId'
@@ -19,8 +19,8 @@ import { DefaultShapeWrapper } from '../components/default-components/DefaultSha
19
19
  import { DefaultSnapIndicator } from '../components/default-components/DefaultSnapIndictor'
20
20
  import { DefaultSpinner } from '../components/default-components/DefaultSpinner'
21
21
  import { DefaultSvgDefs } from '../components/default-components/DefaultSvgDefs'
22
- import type { TLEditorComponents } from './EditorComponentsContext'
23
22
  import { EditorComponentsContext } from './EditorComponentsContext'
23
+ import type { TLEditorComponents } from './EditorComponentsContext'
24
24
  import { useShallowObjectIdentity } from './useIdentity'
25
25
 
26
26
  export { useEditorComponents } from './EditorComponentsContext'
@@ -1,8 +1,8 @@
1
1
  import { TLAsset, TLAssetStore, TLStoreSnapshot } from '@tldraw/tlschema'
2
2
  import { WeakCache } from '@tldraw/utils'
3
3
  import { useEffect } from 'react'
4
- import { TLEditorSnapshot } from '../config/TLEditorSnapshot'
5
4
  import { TLStoreOptions, createTLStore } from '../config/createTLStore'
5
+ import { TLEditorSnapshot } from '../config/TLEditorSnapshot'
6
6
  import { TLStoreWithStatus } from '../utils/sync/StoreWithStatus'
7
7
  import { TLLocalSyncClient } from '../utils/sync/TLLocalSyncClient'
8
8
  import { useShallowObjectIdentity } from './useIdentity'
@@ -1,10 +1,50 @@
1
+ import { Awaitable } from '@tldraw/utils'
1
2
  import { ComponentType, Fragment } from 'react'
2
3
  import { DEFAULT_CAMERA_OPTIONS } from './constants'
4
+ import type { Editor } from './editor/Editor'
5
+ import { TLContent } from './editor/types/clipboard-types'
6
+ import { TLExternalContent } from './editor/types/external-content'
3
7
  import { TLCameraOptions } from './editor/types/misc-types'
4
8
  import { VecLike } from './primitives/Vec'
5
9
  import { TLDeepLinkOptions } from './utils/deepLinks'
6
10
  import { TLTextOptions } from './utils/richText'
7
11
 
12
+ /**
13
+ * Identifies how a clipboard write was triggered (copy vs cut, keyboard vs menu).
14
+ *
15
+ * @public
16
+ */
17
+ export interface TLClipboardWriteInfo {
18
+ readonly operation: 'copy' | 'cut'
19
+ readonly source: 'native' | 'menu'
20
+ }
21
+
22
+ /**
23
+ * Raw clipboard paste payload, before tldraw parses clipboard contents into {@link TLExternalContent}.
24
+ *
25
+ * - `native-event`: from the `paste` event — `clipboardData` is available synchronously (unlike async
26
+ * `navigator.clipboard.read()`).
27
+ * - `clipboard-read`: from an explicit `navigator.clipboard.read()` call — only `ClipboardItem[]`
28
+ * exists
29
+ * (no `DataTransfer`).
30
+ *
31
+ * @public
32
+ */
33
+ export type TLClipboardPasteRawInfo =
34
+ | {
35
+ readonly editor: Editor
36
+ readonly source: 'native-event'
37
+ readonly event: ClipboardEvent
38
+ readonly clipboardData: DataTransfer | null
39
+ readonly point: VecLike | undefined
40
+ }
41
+ | {
42
+ readonly editor: Editor
43
+ readonly source: 'clipboard-read'
44
+ readonly clipboardItems: readonly ClipboardItem[]
45
+ readonly point: VecLike | undefined
46
+ }
47
+
8
48
  /**
9
49
  * Options for configuring tldraw. For defaults, see {@link defaultTldrawOptions}.
10
50
  *
@@ -156,6 +196,71 @@ export interface TldrawOptions {
156
196
  * viewport's page dimensions regardless of overview zoom changes.
157
197
  */
158
198
  readonly quickZoomPreservesScreenBounds: boolean
199
+ /**
200
+ * Called before content is written to the clipboard during a copy or cut operation.
201
+ * Receives the serialized content (shapes, bindings, assets) and can filter or transform
202
+ * it before it reaches the clipboard.
203
+ *
204
+ * Return a modified `TLContent` object to change what is copied or cut. Return `false` to
205
+ * cancel the clipboard write (for cut, the selected shapes are not removed). Return `void`
206
+ * (or `undefined`) to pass through unchanged. You may return a `Promise` of those values if
207
+ * the hook is async.
208
+ *
209
+ * @example
210
+ * ```tsx
211
+ * // Filter out "locked" shapes from copy
212
+ * onBeforeCopyToClipboard({ content, operation }) {
213
+ * return {
214
+ * ...content,
215
+ * shapes: content.shapes.filter(s => !s.meta.locked),
216
+ * rootShapeIds: content.rootShapeIds.filter(id =>
217
+ * content.shapes.find(s => s.id === id && !s.meta.locked)
218
+ * ),
219
+ * }
220
+ * }
221
+ * ```
222
+ */
223
+ onBeforeCopyToClipboard?(
224
+ info: { editor: Editor; content: TLContent } & TLClipboardWriteInfo
225
+ ): Awaitable<TLContent | false | void>
226
+ /**
227
+ * Called before pasted content is processed and shapes are created. Receives the parsed
228
+ * external content from the clipboard and can filter, transform, or cancel it.
229
+ *
230
+ * Return `false` to cancel the paste. Return a modified content object to transform it.
231
+ * Return `void` (or `undefined`) to pass through unchanged. You may return a `Promise` of
232
+ * those values if the hook is async.
233
+ *
234
+ * This only fires for clipboard paste operations (keyboard shortcuts and menu actions),
235
+ * not for file drops or programmatic `putExternalContent` calls.
236
+ *
237
+ * @example
238
+ * ```tsx
239
+ * // Block pasting of image files
240
+ * onBeforePasteFromClipboard({ content }) {
241
+ * if (content.type === 'files') {
242
+ * const nonImages = content.files.filter(f => !f.type.startsWith('image/'))
243
+ * if (nonImages.length === 0) return false
244
+ * return { ...content, files: nonImages }
245
+ * }
246
+ * }
247
+ * ```
248
+ */
249
+ onBeforePasteFromClipboard?(info: {
250
+ editor: Editor
251
+ content: TLExternalContent<unknown>
252
+ source: 'native-event' | 'clipboard-read'
253
+ point?: VecLike
254
+ }): Awaitable<TLExternalContent<unknown> | false | void>
255
+ /**
256
+ * Called first for keyboard and menu paste, **before** tldraw handles or parses clipboard data
257
+ * (and before {@link TldrawOptions.onBeforePasteFromClipboard}).
258
+ *
259
+ * Return `false` to cancel tldraw's default paste handling for this gesture (same convention as
260
+ * {@link TldrawOptions.onBeforePasteFromClipboard}). Use this when you handle paste yourself from
261
+ * raw clipboard data, or to block the gesture entirely. Return `void` (or `undefined`) to continue.
262
+ */
263
+ onClipboardPasteRaw?(info: TLClipboardPasteRawInfo): false | void
159
264
  /**
160
265
  * Called when content is dropped on the canvas. Provides the page position
161
266
  * where the drop occurred and the underlying drag event object.
@@ -227,5 +332,8 @@ export const defaultTldrawOptions = {
227
332
  text: {},
228
333
  deepLinks: undefined,
229
334
  quickZoomPreservesScreenBounds: true,
335
+ onBeforeCopyToClipboard: undefined,
336
+ onBeforePasteFromClipboard: undefined,
337
+ onClipboardPasteRaw: undefined,
230
338
  experimental__onDropOnCanvas: undefined,
231
339
  } as const satisfies TldrawOptions
@@ -1,6 +1,6 @@
1
1
  import { BoxModel } from '@tldraw/tlschema'
2
- import { Vec, VecLike } from './Vec'
3
2
  import { approximatelyLte, PI, PI2, toPrecision } from './utils'
3
+ import { Vec, VecLike } from './Vec'
4
4
 
5
5
  /** @public */
6
6
  export type BoxLike = BoxModel | Box
@@ -1,8 +1,8 @@
1
- import { Vec, VecLike } from '../Vec'
2
1
  import { intersectLineSegmentCircle } from '../intersect'
3
2
  import { getArcMeasure, getPointInArcT, getPointOnCircle } from '../utils'
4
- import { Geometry2d, Geometry2dOptions } from './Geometry2d'
3
+ import { Vec, VecLike } from '../Vec'
5
4
  import { getVerticesCountForArcLength } from './geometry-constants'
5
+ import { Geometry2d, Geometry2dOptions } from './Geometry2d'
6
6
 
7
7
  /** @public */
8
8
  export class Arc2d extends Geometry2d {
@@ -1,9 +1,9 @@
1
1
  import { Box } from '../Box'
2
- import { Vec, VecLike } from '../Vec'
3
2
  import { intersectLineSegmentCircle } from '../intersect'
4
3
  import { PI2, getPointOnCircle } from '../utils'
5
- import { Geometry2d, Geometry2dOptions } from './Geometry2d'
4
+ import { Vec, VecLike } from '../Vec'
6
5
  import { getVerticesCountForArcLength } from './geometry-constants'
6
+ import { Geometry2d, Geometry2dOptions } from './Geometry2d'
7
7
 
8
8
  /** @public */
9
9
  export class Circle2d extends Geometry2d {