@tldraw/editor 5.2.0-next.e2b8d10bf10e → 5.2.0

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 (172) hide show
  1. package/README.md +7 -1
  2. package/dist-cjs/index.d.ts +112 -53
  3. package/dist-cjs/index.js +7 -8
  4. package/dist-cjs/index.js.map +2 -2
  5. package/dist-cjs/lib/components/MenuClickCapture.js +8 -5
  6. package/dist-cjs/lib/components/MenuClickCapture.js.map +2 -2
  7. package/dist-cjs/lib/components/Shape.js +47 -1
  8. package/dist-cjs/lib/components/Shape.js.map +3 -3
  9. package/dist-cjs/lib/components/default-components/DefaultCanvas.js +1 -16
  10. package/dist-cjs/lib/components/default-components/DefaultCanvas.js.map +2 -2
  11. package/dist-cjs/lib/editor/Editor.js +97 -45
  12. package/dist-cjs/lib/editor/Editor.js.map +2 -2
  13. package/dist-cjs/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.js +3 -3
  14. package/dist-cjs/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.js.map +2 -2
  15. package/dist-cjs/lib/editor/managers/FontManager/FontManager.js +28 -4
  16. package/dist-cjs/lib/editor/managers/FontManager/FontManager.js.map +3 -3
  17. package/dist-cjs/lib/editor/managers/HistoryManager/HistoryManager.js +24 -2
  18. package/dist-cjs/lib/editor/managers/HistoryManager/HistoryManager.js.map +2 -2
  19. package/dist-cjs/lib/editor/managers/InputsManager/InputsManager.js +14 -3
  20. package/dist-cjs/lib/editor/managers/InputsManager/InputsManager.js.map +2 -2
  21. package/dist-cjs/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.js +4 -2
  22. package/dist-cjs/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.js.map +2 -2
  23. package/dist-cjs/lib/editor/managers/TextManager/TextManager.js +7 -3
  24. package/dist-cjs/lib/editor/managers/TextManager/TextManager.js.map +2 -2
  25. package/dist-cjs/lib/editor/managers/TickManager/TickManager.js +0 -1
  26. package/dist-cjs/lib/editor/managers/TickManager/TickManager.js.map +2 -2
  27. package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js +15 -2
  28. package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js.map +2 -2
  29. package/dist-cjs/lib/editor/overlays/strokeShapeIndicators.js +79 -0
  30. package/dist-cjs/lib/editor/overlays/strokeShapeIndicators.js.map +7 -0
  31. package/dist-cjs/lib/editor/shapes/ShapeUtil.js.map +2 -2
  32. package/dist-cjs/lib/editor/tools/BaseBoxShapeTool/children/Pointing.js +4 -0
  33. package/dist-cjs/lib/editor/tools/BaseBoxShapeTool/children/Pointing.js.map +2 -2
  34. package/dist-cjs/lib/{hooks/useTransform.js → editor/tools/cancelShapeCreationOnLongPress.js} +8 -23
  35. package/dist-cjs/lib/editor/tools/cancelShapeCreationOnLongPress.js.map +7 -0
  36. package/dist-cjs/lib/editor/types/event-types.js.map +2 -2
  37. package/dist-cjs/lib/globals/environment.js +6 -1
  38. package/dist-cjs/lib/globals/environment.js.map +2 -2
  39. package/dist-cjs/lib/hooks/EditorComponentsContext.js.map +2 -2
  40. package/dist-cjs/lib/hooks/useCanvasEvents.js +17 -8
  41. package/dist-cjs/lib/hooks/useCanvasEvents.js.map +2 -2
  42. package/dist-cjs/lib/hooks/useEditorComponents.js +0 -1
  43. package/dist-cjs/lib/hooks/useEditorComponents.js.map +2 -2
  44. package/dist-cjs/lib/hooks/usePresence.js.map +2 -2
  45. package/dist-cjs/lib/hooks/useScreenBounds.js +1 -0
  46. package/dist-cjs/lib/hooks/useScreenBounds.js.map +2 -2
  47. package/dist-cjs/lib/license/LicenseManager.js +5 -1
  48. package/dist-cjs/lib/license/LicenseManager.js.map +2 -2
  49. package/dist-cjs/lib/primitives/Vec.js +3 -2
  50. package/dist-cjs/lib/primitives/Vec.js.map +2 -2
  51. package/dist-cjs/lib/utils/dom.js +12 -0
  52. package/dist-cjs/lib/utils/dom.js.map +2 -2
  53. package/dist-cjs/lib/utils/getPointerInfo.js +2 -1
  54. package/dist-cjs/lib/utils/getPointerInfo.js.map +2 -2
  55. package/dist-cjs/lib/utils/pointer.js +37 -0
  56. package/dist-cjs/lib/utils/pointer.js.map +7 -0
  57. package/dist-cjs/version.js +3 -3
  58. package/dist-cjs/version.js.map +1 -1
  59. package/dist-esm/index.d.mts +112 -53
  60. package/dist-esm/index.mjs +9 -13
  61. package/dist-esm/index.mjs.map +2 -2
  62. package/dist-esm/lib/components/MenuClickCapture.mjs +8 -5
  63. package/dist-esm/lib/components/MenuClickCapture.mjs.map +2 -2
  64. package/dist-esm/lib/components/Shape.mjs +48 -2
  65. package/dist-esm/lib/components/Shape.mjs.map +3 -3
  66. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs +1 -16
  67. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs.map +2 -2
  68. package/dist-esm/lib/editor/Editor.mjs +97 -45
  69. package/dist-esm/lib/editor/Editor.mjs.map +2 -2
  70. package/dist-esm/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.mjs +3 -3
  71. package/dist-esm/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.mjs.map +2 -2
  72. package/dist-esm/lib/editor/managers/FontManager/FontManager.mjs +28 -4
  73. package/dist-esm/lib/editor/managers/FontManager/FontManager.mjs.map +3 -3
  74. package/dist-esm/lib/editor/managers/HistoryManager/HistoryManager.mjs +24 -2
  75. package/dist-esm/lib/editor/managers/HistoryManager/HistoryManager.mjs.map +2 -2
  76. package/dist-esm/lib/editor/managers/InputsManager/InputsManager.mjs +14 -3
  77. package/dist-esm/lib/editor/managers/InputsManager/InputsManager.mjs.map +2 -2
  78. package/dist-esm/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.mjs +4 -2
  79. package/dist-esm/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.mjs.map +2 -2
  80. package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs +7 -3
  81. package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs.map +2 -2
  82. package/dist-esm/lib/editor/managers/TickManager/TickManager.mjs +0 -1
  83. package/dist-esm/lib/editor/managers/TickManager/TickManager.mjs.map +2 -2
  84. package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs +15 -2
  85. package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs.map +2 -2
  86. package/dist-esm/lib/editor/overlays/strokeShapeIndicators.mjs +59 -0
  87. package/dist-esm/lib/editor/overlays/strokeShapeIndicators.mjs.map +7 -0
  88. package/dist-esm/lib/editor/shapes/ShapeUtil.mjs.map +2 -2
  89. package/dist-esm/lib/editor/tools/BaseBoxShapeTool/children/Pointing.mjs +4 -0
  90. package/dist-esm/lib/editor/tools/BaseBoxShapeTool/children/Pointing.mjs.map +2 -2
  91. package/dist-esm/lib/editor/tools/cancelShapeCreationOnLongPress.mjs +8 -0
  92. package/dist-esm/lib/editor/tools/cancelShapeCreationOnLongPress.mjs.map +7 -0
  93. package/dist-esm/lib/editor/types/event-types.mjs.map +2 -2
  94. package/dist-esm/lib/globals/environment.mjs +6 -1
  95. package/dist-esm/lib/globals/environment.mjs.map +2 -2
  96. package/dist-esm/lib/hooks/EditorComponentsContext.mjs.map +2 -2
  97. package/dist-esm/lib/hooks/useCanvasEvents.mjs +17 -8
  98. package/dist-esm/lib/hooks/useCanvasEvents.mjs.map +2 -2
  99. package/dist-esm/lib/hooks/useEditorComponents.mjs +0 -1
  100. package/dist-esm/lib/hooks/useEditorComponents.mjs.map +2 -2
  101. package/dist-esm/lib/hooks/usePresence.mjs.map +2 -2
  102. package/dist-esm/lib/hooks/useScreenBounds.mjs +1 -0
  103. package/dist-esm/lib/hooks/useScreenBounds.mjs.map +2 -2
  104. package/dist-esm/lib/license/LicenseManager.mjs +5 -1
  105. package/dist-esm/lib/license/LicenseManager.mjs.map +2 -2
  106. package/dist-esm/lib/primitives/Vec.mjs +3 -2
  107. package/dist-esm/lib/primitives/Vec.mjs.map +2 -2
  108. package/dist-esm/lib/utils/dom.mjs +12 -0
  109. package/dist-esm/lib/utils/dom.mjs.map +2 -2
  110. package/dist-esm/lib/utils/getPointerInfo.mjs +2 -1
  111. package/dist-esm/lib/utils/getPointerInfo.mjs.map +2 -2
  112. package/dist-esm/lib/utils/pointer.mjs +17 -0
  113. package/dist-esm/lib/utils/pointer.mjs.map +7 -0
  114. package/dist-esm/version.mjs +3 -3
  115. package/dist-esm/version.mjs.map +1 -1
  116. package/editor.css +15 -3
  117. package/package.json +11 -8
  118. package/src/index.ts +4 -10
  119. package/src/lib/components/MenuClickCapture.tsx +8 -4
  120. package/src/lib/components/Shape.tsx +88 -1
  121. package/src/lib/components/default-components/DefaultCanvas.tsx +1 -17
  122. package/src/lib/editor/Editor.test.ts +36 -0
  123. package/src/lib/editor/Editor.ts +145 -55
  124. package/src/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.test.ts +43 -16
  125. package/src/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.ts +8 -5
  126. package/src/lib/editor/managers/FontManager/FontManager.test.ts +37 -10
  127. package/src/lib/editor/managers/FontManager/FontManager.ts +46 -1
  128. package/src/lib/editor/managers/HistoryManager/HistoryManager.test.ts +32 -0
  129. package/src/lib/editor/managers/HistoryManager/HistoryManager.ts +34 -4
  130. package/src/lib/editor/managers/InputsManager/InputsManager.test.ts +61 -0
  131. package/src/lib/editor/managers/InputsManager/InputsManager.ts +16 -4
  132. package/src/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.ts +9 -2
  133. package/src/lib/editor/managers/TextManager/TextManager.test.ts +16 -14
  134. package/src/lib/editor/managers/TextManager/TextManager.ts +17 -2
  135. package/src/lib/editor/managers/TickManager/TickManager.test.ts +0 -40
  136. package/src/lib/editor/managers/TickManager/TickManager.ts +0 -1
  137. package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.test.ts +12 -2
  138. package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.ts +27 -2
  139. package/src/lib/editor/overlays/strokeShapeIndicators.ts +86 -0
  140. package/src/lib/editor/shapes/ShapeUtil.ts +41 -0
  141. package/src/lib/editor/tools/BaseBoxShapeTool/children/Pointing.ts +5 -0
  142. package/src/lib/editor/tools/cancelShapeCreationOnLongPress.ts +31 -0
  143. package/src/lib/editor/types/event-types.ts +7 -0
  144. package/src/lib/globals/environment.ts +7 -0
  145. package/src/lib/hooks/EditorComponentsContext.tsx +0 -2
  146. package/src/lib/hooks/useCanvasEvents.ts +24 -12
  147. package/src/lib/hooks/useEditorComponents.tsx +0 -1
  148. package/src/lib/hooks/usePresence.ts +2 -2
  149. package/src/lib/hooks/useScreenBounds.ts +3 -0
  150. package/src/lib/license/LicenseManager.test.ts +24 -16
  151. package/src/lib/license/LicenseManager.ts +8 -2
  152. package/src/lib/primitives/Vec.ts +5 -2
  153. package/src/lib/test/ContentElementSlot.test.tsx +201 -0
  154. package/src/lib/utils/dom.ts +34 -0
  155. package/src/lib/utils/getPointerInfo.ts +2 -1
  156. package/src/lib/utils/pointer.test.ts +76 -0
  157. package/src/lib/utils/pointer.ts +42 -0
  158. package/src/version.ts +3 -3
  159. package/dist-cjs/lib/components/default-components/DefaultSelectionBackground.js +0 -49
  160. package/dist-cjs/lib/components/default-components/DefaultSelectionBackground.js.map +0 -7
  161. package/dist-cjs/lib/editor/overlays/ShapeIndicatorOverlayUtil.js +0 -161
  162. package/dist-cjs/lib/editor/overlays/ShapeIndicatorOverlayUtil.js.map +0 -7
  163. package/dist-cjs/lib/hooks/useTransform.js.map +0 -7
  164. package/dist-esm/lib/components/default-components/DefaultSelectionBackground.mjs +0 -19
  165. package/dist-esm/lib/components/default-components/DefaultSelectionBackground.mjs.map +0 -7
  166. package/dist-esm/lib/editor/overlays/ShapeIndicatorOverlayUtil.mjs +0 -141
  167. package/dist-esm/lib/editor/overlays/ShapeIndicatorOverlayUtil.mjs.map +0 -7
  168. package/dist-esm/lib/hooks/useTransform.mjs +0 -23
  169. package/dist-esm/lib/hooks/useTransform.mjs.map +0 -7
  170. package/src/lib/components/default-components/DefaultSelectionBackground.tsx +0 -25
  171. package/src/lib/editor/overlays/ShapeIndicatorOverlayUtil.ts +0 -216
  172. package/src/lib/hooks/useTransform.ts +0 -30
@@ -1,5 +1,4 @@
1
1
  import { Mock, Mocked, vi } from 'vitest'
2
- import { Vec } from '../../../primitives/Vec'
3
2
  import { Editor } from '../../Editor'
4
3
  import { TickManager } from './TickManager'
5
4
 
@@ -21,17 +20,6 @@ describe('TickManager', () => {
21
20
  let tickManager: TickManager
22
21
  let mockEmit: Mock
23
22
  let mockDisposablesAdd: Mock
24
- let mockInputs: {
25
- _currentScreenPoint: Vec
26
- getCurrentScreenPoint(): Vec
27
- currentScreenPoint: Vec
28
- setCurrentScreenPoint(value: Vec): void
29
- _pointerVelocity: Vec
30
- getPointerVelocity(): Vec
31
- pointerVelocity: Vec
32
- setPointerVelocity(value: Vec): void
33
- updatePointerVelocity(elapsed: number): void
34
- }
35
23
 
36
24
  beforeEach(() => {
37
25
  vi.clearAllMocks()
@@ -52,39 +40,11 @@ describe('TickManager', () => {
52
40
  mockEmit = vi.fn()
53
41
  mockDisposablesAdd = vi.fn()
54
42
 
55
- // Create a mock inputs object with getters and setters
56
- mockInputs = {
57
- _currentScreenPoint: new Vec(100, 100),
58
- getCurrentScreenPoint() {
59
- return this._currentScreenPoint
60
- },
61
- get currentScreenPoint() {
62
- return this.getCurrentScreenPoint()
63
- },
64
- setCurrentScreenPoint(value: Vec) {
65
- this._currentScreenPoint = value
66
- },
67
- _pointerVelocity: new Vec(0, 0),
68
- getPointerVelocity() {
69
- return this._pointerVelocity
70
- },
71
- get pointerVelocity() {
72
- return this.getPointerVelocity()
73
- },
74
- setPointerVelocity(value: Vec) {
75
- this._pointerVelocity = value
76
- },
77
- updatePointerVelocity(_elapsed: number) {
78
- // Mock implementation - no-op for tests
79
- },
80
- }
81
-
82
43
  editor = {
83
44
  emit: mockEmit,
84
45
  disposables: {
85
46
  add: mockDisposablesAdd,
86
47
  },
87
- inputs: mockInputs as unknown as Editor['inputs'],
88
48
  } as unknown as Mocked<Editor>
89
49
 
90
50
  tickManager = new TickManager(editor)
@@ -40,7 +40,6 @@ export class TickManager {
40
40
  const elapsed = now - this.now
41
41
  this.now = now
42
42
 
43
- this.editor.inputs.updatePointerVelocity(elapsed)
44
43
  this.editor.emit('frame', elapsed)
45
44
  this.editor.emit('tick', elapsed)
46
45
  this.cancelRaf = throttleToNextFrame(this.tick)
@@ -1,4 +1,5 @@
1
1
  import { atom } from '@tldraw/state'
2
+ import { createUserId } from '@tldraw/tlschema'
2
3
  import { Mocked, vi } from 'vitest'
3
4
  import { TLCurrentUser } from '../../../config/createTLCurrentUser'
4
5
  import { TLUserPreferences, defaultUserPreferences } from '../../../config/TLUserPreferences'
@@ -296,8 +297,17 @@ describe('UserPreferencesManager', () => {
296
297
  userPreferencesManager = new UserPreferencesManager(mockUser, 'light')
297
298
  })
298
299
 
299
- describe('getId', () => {
300
- it('should return user id', () => {
300
+ describe('getExternalId / getRecordId', () => {
301
+ it('should return the raw external user id', () => {
302
+ expect(userPreferencesManager.getExternalId()).toBe(mockUserPreferences.id)
303
+ })
304
+
305
+ it('should return the prefixed record id', () => {
306
+ expect(userPreferencesManager.getRecordId()).toBe(createUserId(mockUserPreferences.id))
307
+ })
308
+
309
+ it('getId() still returns the external id', () => {
310
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
301
311
  expect(userPreferencesManager.getId()).toBe(mockUserPreferences.id)
302
312
  })
303
313
  })
@@ -1,4 +1,5 @@
1
1
  import { atom, computed } from '@tldraw/state'
2
+ import { createUserId, TLUserId } from '@tldraw/tlschema'
2
3
  import { TLCurrentUser } from '../../../config/createTLCurrentUser'
3
4
  import { TLUserPreferences, defaultUserPreferences } from '../../../config/TLUserPreferences'
4
5
  import { getGlobalWindow } from '../../../utils/dom'
@@ -39,7 +40,7 @@ export class UserPreferencesManager {
39
40
  }
40
41
  @computed getUserPreferences() {
41
42
  return {
42
- id: this.getId(),
43
+ id: this.getExternalId(),
43
44
  name: this.getName(),
44
45
  locale: this.getLocale(),
45
46
  color: this.getColor(),
@@ -89,10 +90,34 @@ export class UserPreferencesManager {
89
90
  )
90
91
  }
91
92
 
92
- @computed getId() {
93
+ /**
94
+ * The current user's raw, app-provided id — the value set in the user's
95
+ * {@link @tldraw/editor#TLUserPreferences}. Use this when you need the id your application
96
+ * assigned to the user. To compare against or look up store records, use
97
+ * {@link UserPreferencesManager.getRecordId} instead.
98
+ */
99
+ @computed getExternalId(): string {
93
100
  return this.user.userPreferences.get().id
94
101
  }
95
102
 
103
+ /**
104
+ * @deprecated Use {@link UserPreferencesManager.getExternalId} for the raw app-provided id, or
105
+ * {@link UserPreferencesManager.getRecordId} for the prefixed `TLUserId` record id.
106
+ */
107
+ @computed getId() {
108
+ return this.getExternalId()
109
+ }
110
+
111
+ /**
112
+ * The current user's id as a tldraw {@link @tldraw/tlschema#TLUserId} record id (prefixed
113
+ * with `user:`). Use this when comparing against or looking up store records, such as a
114
+ * presence record's `userId` or `followingUserId`. For the raw, app-provided id, use
115
+ * {@link UserPreferencesManager.getExternalId}.
116
+ */
117
+ @computed getRecordId(): TLUserId {
118
+ return createUserId(this.getExternalId())
119
+ }
120
+
96
121
  @computed getName() {
97
122
  return this.user.userPreferences.get().name?.trim() ?? defaultUserPreferences.name
98
123
  }
@@ -0,0 +1,86 @@
1
+ import { createComputedCache } from '@tldraw/store'
2
+ import { TLShape, TLShapeId } from '@tldraw/tlschema'
3
+ import type { Editor } from '../Editor'
4
+
5
+ const indicatorPathCache = createComputedCache(
6
+ 'shapeIndicatorPath',
7
+ (editor: Editor, shape: TLShape) => {
8
+ const util = editor.getShapeUtil(shape)
9
+ return util.getIndicatorPath(shape)
10
+ },
11
+ {
12
+ areRecordsEqual(a, b) {
13
+ return a.props === b.props
14
+ },
15
+ }
16
+ )
17
+
18
+ /**
19
+ * Combine every batchable shape indicator into a single page-space `Path2D` and
20
+ * emit one stroke call. Shapes whose indicator needs an evenodd clip (e.g.
21
+ * arrows with labels or complex arrowheads) can't be batched — they still
22
+ * stroke individually inside a save/restore with `ctx.clip` applied.
23
+ *
24
+ * Shared by any overlay util that paints shape indicators (e.g. collaborator
25
+ * selections).
26
+ *
27
+ * @public
28
+ */
29
+ export function strokeShapeIndicators(
30
+ editor: Editor,
31
+ ctx: CanvasRenderingContext2D,
32
+ shapeIds: TLShapeId[]
33
+ ): void {
34
+ if (shapeIds.length === 0) return
35
+
36
+ const batched = new Path2D()
37
+
38
+ for (const shapeId of shapeIds) {
39
+ const shape = editor.getShape(shapeId)
40
+ if (!shape || shape.isLocked) continue
41
+
42
+ const pageTransform = editor.getShapePageTransform(shape)
43
+ if (!pageTransform) continue
44
+
45
+ const indicatorPath = indicatorPathCache.get(editor, shape.id)
46
+ if (!indicatorPath) continue
47
+
48
+ if (indicatorPath instanceof Path2D) {
49
+ batched.addPath(indicatorPath, pageTransform)
50
+ continue
51
+ }
52
+
53
+ const { path, clipPath, additionalPaths } = indicatorPath
54
+
55
+ if (!clipPath) {
56
+ batched.addPath(path, pageTransform)
57
+ if (additionalPaths) {
58
+ for (const p of additionalPaths) batched.addPath(p, pageTransform)
59
+ }
60
+ continue
61
+ }
62
+
63
+ // Clipped case: fall back to an individual stroke. Rare (arrows with
64
+ // labels / complex arrowheads), so the extra save/restore/stroke
65
+ // pair per such shape isn't worth batching away.
66
+ ctx.save()
67
+ ctx.transform(
68
+ pageTransform.a,
69
+ pageTransform.b,
70
+ pageTransform.c,
71
+ pageTransform.d,
72
+ pageTransform.e,
73
+ pageTransform.f
74
+ )
75
+ ctx.save()
76
+ ctx.clip(clipPath, 'evenodd')
77
+ ctx.stroke(path)
78
+ ctx.restore()
79
+ if (additionalPaths) {
80
+ for (const p of additionalPaths) ctx.stroke(p)
81
+ }
82
+ ctx.restore()
83
+ }
84
+
85
+ ctx.stroke(batched)
86
+ }
@@ -982,6 +982,47 @@ export abstract class ShapeUtil<Shape extends TLShape = TLShape> {
982
982
  * @public
983
983
  */
984
984
  onEditEnd?(shape: Shape): void
985
+
986
+ /**
987
+ * Provide an app-owned element to be rendered inside the shape, alongside the output of
988
+ * {@link ShapeUtil.component}. While the shape remains mounted, tldraw guarantees the
989
+ * element keeps the same DOM position: it is never unmounted, recreated, or relocated by
990
+ * reordering, reparenting, culling, or re-renders. When adopting the element, tldraw uses
991
+ * `Node.moveBefore` where available so stateful content like cross-origin iframes keeps
992
+ * its state across the move, falling back to `appendChild` elsewhere.
993
+ *
994
+ * Pair this with {@link ShapeUtil.onReleaseAppOwnedElement} to reclaim the element before
995
+ * the shape or editor unmounts.
996
+ *
997
+ * This is called once per shape mount, not on every prop change, so the adopted element is
998
+ * not refreshed when the shape's props change. If the element's content depends on props,
999
+ * return a stable element and mutate it in place (for example from {@link ShapeUtil.component}
1000
+ * or an effect) rather than returning a different element.
1001
+ *
1002
+ * @param shape - The shape.
1003
+ * @returns The element to adopt, or null to render nothing.
1004
+ * @public
1005
+ */
1006
+ getAppOwnedElement?(shape: Shape): HTMLElement | null
1007
+
1008
+ /**
1009
+ * A callback called before the shape's app-owned element slot is destroyed: when the shape
1010
+ * unmounts (for example when it is deleted or the current page changes) or when the whole
1011
+ * editor unmounts, including error teardown. The slot is still connected to the document
1012
+ * when this is called, so the app can move the element to another connected parent with
1013
+ * `Node.moveBefore` to preserve its state. An element left in the slot is destroyed along
1014
+ * with it.
1015
+ *
1016
+ * This is not only a "shape deleted" signal: it also fires on page changes, editor unmount,
1017
+ * and (in dev under React StrictMode) on the throwaway mount/unmount cycle while the shape
1018
+ * still exists. Make it safe to call when the shape is still present, and check the store
1019
+ * (`this.editor.getShape(shape.id)`) if you need to distinguish deletion from a remount.
1020
+ *
1021
+ * @param shape - The shape.
1022
+ * @param element - The element returned by {@link ShapeUtil.getAppOwnedElement}.
1023
+ * @public
1024
+ */
1025
+ onReleaseAppOwnedElement?(shape: Shape, element: HTMLElement): void
985
1026
  }
986
1027
 
987
1028
  /**
@@ -4,6 +4,7 @@ import { Vec } from '../../../../primitives/Vec'
4
4
  import type { Editor } from '../../../Editor'
5
5
  import { TLBaseBoxShape } from '../../../shapes/BaseBoxShapeUtil'
6
6
  import { TLPointerEventInfo } from '../../../types/event-types'
7
+ import { cancelShapeCreationOnLongPress } from '../../cancelShapeCreationOnLongPress'
7
8
  import { StateNode } from '../../StateNode'
8
9
  import type { BaseBoxShapeTool } from '../BaseBoxShapeTool'
9
10
 
@@ -65,6 +66,10 @@ export class Pointing extends StateNode {
65
66
  this.complete()
66
67
  }
67
68
 
69
+ override onLongPress() {
70
+ cancelShapeCreationOnLongPress(this.editor, () => this.cancel())
71
+ }
72
+
68
73
  override onCancel() {
69
74
  this.cancel()
70
75
  }
@@ -0,0 +1,31 @@
1
+ import type { Editor } from '../Editor'
2
+
3
+ /**
4
+ * Shared handler for a long-press in a discrete shape-creation tool's pointing
5
+ * state (geo, note, line, text, arrow, frame). On a coarse pointer this cancels
6
+ * any pending creation so the long-press leaves no shape behind (issue #8277).
7
+ * It does not open a context menu — that is a select-tool surface, reached on
8
+ * touch only from the select tool, and on desktop from a right-click (which the
9
+ * editor routes through the select tool).
10
+ *
11
+ * Gated on `isCoarsePointer`: on a fine pointer (mouse) the long-press timer
12
+ * powers the deliberate "pause before drag to start a precise arrow" gesture,
13
+ * which we must not disturb. The bug only manifests on coarse pointers, because
14
+ * that is also where the browser fires `contextmenu` at the long-press mark.
15
+ *
16
+ * The `cancelPendingCreation` callback is the tool's own cleanup — it knows
17
+ * whether it needs to `bailToMark` a shape it created early or simply transition
18
+ * back to idle. Continuous-gesture tools (draw, highlight, eraser, laser) are
19
+ * excluded: their press is their own action, so they handle (or ignore) the
20
+ * long-press themselves.
21
+ *
22
+ * @internal
23
+ */
24
+ export function cancelShapeCreationOnLongPress(
25
+ editor: Editor,
26
+ cancelPendingCreation: () => void
27
+ ): void {
28
+ if (!editor.getInstanceState().isCoarsePointer) return
29
+
30
+ cancelPendingCreation()
31
+ }
@@ -63,6 +63,13 @@ export type TLPointerEventInfo = TLBaseEventInfo & {
63
63
  pointerId: number
64
64
  button: number
65
65
  isPen: boolean
66
+ /**
67
+ * Whether this pen event appears to be direct manipulation on the display (e.g. Apple Pencil on
68
+ * an iPad or a Surface Pen on a touchscreen) rather than indirect input from a desktop graphics
69
+ * tablet (e.g. a Wacom Intuos). Only direct-display pens should auto-enable pen mode. Drawing and
70
+ * pressure behavior is driven by `isPen` and applies to all pens regardless of this flag.
71
+ */
72
+ isPenDirect?: boolean
66
73
  } & TLPointerEventTarget
67
74
 
68
75
  /** @public */
@@ -17,6 +17,10 @@ const tlenv = {
17
17
  isWebview: false,
18
18
  isDarwin: false,
19
19
  hasCanvasSupport: false,
20
+ // Whether the device has a touch screen (an integrated coarse pointer, e.g. an iPad or a
21
+ // touchscreen laptop). Unlike `isCoarsePointer`, this reflects the device's hardware rather than
22
+ // the pointer currently in use, so it stays stable when a pen or mouse is used.
23
+ isTouchDevice: false,
20
24
  }
21
25
 
22
26
  let isForcedFinePointer = false
@@ -32,6 +36,9 @@ if (typeof window !== 'undefined') {
32
36
  }
33
37
  tlenv.hasCanvasSupport = 'Promise' in window && 'HTMLCanvasElement' in window
34
38
  isForcedFinePointer = tlenv.isFirefox && !tlenv.isAndroid && !tlenv.isIos
39
+ tlenv.isTouchDevice =
40
+ ('navigator' in window && window.navigator.maxTouchPoints > 0) ||
41
+ (typeof window.matchMedia === 'function' && window.matchMedia('(any-pointer: coarse)').matches)
35
42
  }
36
43
 
37
44
  /**
@@ -2,7 +2,6 @@ import { ComponentType, RefAttributes, createContext, useContext } from 'react'
2
2
  import type { TLCanvasComponentProps } from '../components/default-components/DefaultCanvas'
3
3
  import type { TLErrorFallbackComponent } from '../components/default-components/DefaultErrorFallback'
4
4
  import type { TLGridProps } from '../components/default-components/DefaultGrid'
5
- import type { TLSelectionBackgroundProps } from '../components/default-components/DefaultSelectionBackground'
6
5
  import type { TLShapeErrorFallbackComponent } from '../components/default-components/DefaultShapeErrorFallback'
7
6
  import type { TLShapeWrapperProps } from '../components/default-components/DefaultShapeWrapper'
8
7
 
@@ -14,7 +13,6 @@ export interface TLEditorComponents {
14
13
  InFrontOfTheCanvas?: ComponentType | null
15
14
  LoadingScreen?: ComponentType | null
16
15
  OnTheCanvas?: ComponentType | null
17
- SelectionBackground?: ComponentType<TLSelectionBackgroundProps> | null
18
16
  ShapeWrapper?: ComponentType<TLShapeWrapperProps & RefAttributes<HTMLDivElement>> | null
19
17
  Spinner?: ComponentType<React.SVGProps<SVGSVGElement>> | null
20
18
  SvgDefs?: ComponentType | null
@@ -8,6 +8,7 @@ import {
8
8
  setPointerCapture,
9
9
  } from '../utils/dom'
10
10
  import { getPointerInfo } from '../utils/getPointerInfo'
11
+ import { getPointerEventButton, isDirectDisplayPen, isSecondaryClickEvent } from '../utils/pointer'
11
12
  import { useEditor } from './useEditor'
12
13
 
13
14
  export function useCanvasEvents() {
@@ -17,12 +18,16 @@ export function useCanvasEvents() {
17
18
 
18
19
  const events = useMemo(
19
20
  function canvasEvents() {
21
+ let isSecondaryClickPointerDown = false
22
+
20
23
  function onPointerDown(e: React.PointerEvent) {
21
24
  if (editor.wasEventAlreadyHandled(e)) return
25
+ const button = getPointerEventButton(e)
26
+ isSecondaryClickPointerDown = button === 2
22
27
 
23
28
  // With right-click panning disabled, fire right_click on press and let the
24
29
  // native contextmenu through so the menu opens at the pointer-down location.
25
- if (e.button === 2 && !editor.options.rightClickPanning) {
30
+ if (button === 2 && !editor.options.rightClickPanning) {
26
31
  editor.dispatch({
27
32
  type: 'pointer',
28
33
  target: 'canvas',
@@ -32,7 +37,11 @@ export function useCanvasEvents() {
32
37
  return
33
38
  }
34
39
 
35
- if (e.button !== 0 && e.button !== 1 && e.button !== 2 && e.button !== 5) return
40
+ if (button !== 0 && button !== 1 && button !== 2 && button !== 5) return
41
+
42
+ // Detect direct-display pen input (Apple Pencil, Surface Pen on a touchscreen) so we
43
+ // only auto-enable pen mode for it, not for an indirect desktop tablet stylus.
44
+ const isPenDirect = isDirectDisplayPen(e)
36
45
 
37
46
  setPointerCapture(e.currentTarget, e)
38
47
 
@@ -41,17 +50,19 @@ export function useCanvasEvents() {
41
50
  target: 'canvas',
42
51
  name: 'pointer_down',
43
52
  ...getPointerInfo(editor, e),
53
+ isPenDirect,
44
54
  })
45
55
  }
46
56
 
47
57
  function onPointerUp(e: React.PointerEvent) {
48
58
  if (editor.wasEventAlreadyHandled(e)) return
49
- if (e.button !== 0 && e.button !== 1 && e.button !== 2 && e.button !== 5) return
59
+ const button = isSecondaryClickPointerDown ? 2 : getPointerEventButton(e)
60
+ if (button !== 0 && button !== 1 && button !== 2 && button !== 5) return
50
61
 
51
62
  const rightClickPanning = editor.options.rightClickPanning
52
63
  // Check before dispatch (which resets isPanning)
53
64
  const wasRightClickPanning =
54
- rightClickPanning && e.button === 2 && editor.inputs.getIsPanning()
65
+ rightClickPanning && button === 2 && editor.inputs.getIsPanning()
55
66
 
56
67
  releasePointerCapture(e.currentTarget, e)
57
68
 
@@ -60,10 +71,11 @@ export function useCanvasEvents() {
60
71
  target: 'canvas',
61
72
  name: 'pointer_up',
62
73
  ...getPointerInfo(editor, e),
74
+ button,
63
75
  })
64
76
 
65
77
  // Static right-click: fire contextmenu at the pointer-up location
66
- if (rightClickPanning && e.button === 2 && !wasRightClickPanning) {
78
+ if (rightClickPanning && button === 2 && !wasRightClickPanning) {
67
79
  const contextMenuEvent = new PointerEvent('contextmenu', {
68
80
  bubbles: true,
69
81
  clientX: e.clientX,
@@ -76,6 +88,7 @@ export function useCanvasEvents() {
76
88
  })
77
89
  e.currentTarget.dispatchEvent(contextMenuEvent)
78
90
  }
91
+ isSecondaryClickPointerDown = false
79
92
  }
80
93
 
81
94
  function onPointerEnter(e: React.PointerEvent) {
@@ -171,16 +184,15 @@ export function useCanvasEvents() {
171
184
  // Synthetic events — our own dispatch from onPointerUp, or tests using
172
185
  // fireEvent.contextMenu — pass through so Radix can open the menu.
173
186
  if (!e.nativeEvent.isTrusted) return
174
- // Only suppress the native browser contextmenu when it follows a real
175
- // right-click (button=2 with no ctrl modifier). For those, our pointer
176
- // handling has already decided what to do (either we'll dispatch a
177
- // synthetic contextmenu on pointerup to open the menu at the release
178
- // position, or we panned and don't want a menu at all).
187
+ // Only suppress the native browser contextmenu when it follows a
188
+ // secondary click. For those, our pointer handling has already
189
+ // decided what to do (either we'll dispatch a synthetic contextmenu on
190
+ // pointerup to open the menu at the release position, or we panned and
191
+ // don't want a menu at all).
179
192
  //
180
193
  // Other contextmenu sources must reach Radix so the menu opens:
181
- // - ctrl+click on macOS (button=0, or button=2 with ctrlKey=true)
182
194
  // - long-press on touch devices (button=0, pointerType=touch)
183
- if (e.button !== 2 || e.ctrlKey) return
195
+ if (!isSecondaryClickEvent(e)) return
184
196
  preventDefault(e)
185
197
  }
186
198
 
@@ -33,7 +33,6 @@ export function EditorComponentsProvider({
33
33
  InFrontOfTheCanvas: null,
34
34
  LoadingScreen: DefaultLoadingScreen,
35
35
  OnTheCanvas: null,
36
- SelectionBackground: null,
37
36
  ShapeWrapper: DefaultShapeWrapper,
38
37
  Spinner: DefaultSpinner,
39
38
  SvgDefs: DefaultSvgDefs,
@@ -1,5 +1,5 @@
1
1
  import { useValue } from '@tldraw/state-react'
2
- import { TLInstancePresence } from '@tldraw/tlschema'
2
+ import { TLInstancePresence, TLUserId } from '@tldraw/tlschema'
3
3
  import { useEditor } from './useEditor'
4
4
 
5
5
  // TODO: maybe move this to a computed property on the App class?
@@ -7,7 +7,7 @@ import { useEditor } from './useEditor'
7
7
  * @returns The latest presence of the user matching userId
8
8
  * @public
9
9
  */
10
- export function usePresence(userId: string): TLInstancePresence | null {
10
+ export function usePresence(userId: TLUserId): TLInstancePresence | null {
11
11
  const editor = useEditor()
12
12
 
13
13
  const latestPresence = useValue(
@@ -11,6 +11,9 @@ export function useScreenBounds(ref: React.RefObject<HTMLElement | null>) {
11
11
  const updateBounds = throttle(
12
12
  () => {
13
13
  if (!ref.current) return
14
+ // If the container's document has been torn down (e.g. an iframe
15
+ // being removed), there's nothing to measure — bail.
16
+ if (!ref.current.ownerDocument.body) return
14
17
  editor.updateViewportScreenBounds(ref.current)
15
18
  },
16
19
  200,
@@ -60,22 +60,30 @@ describe('LicenseManager', () => {
60
60
  expect(result).toMatchObject({ isLicenseParseable: false, reason: 'no-key-provided' })
61
61
  })
62
62
 
63
- it('Signals that it is development mode when localhost', async () => {
64
- const schemes = ['http', 'https']
65
- for (const scheme of schemes) {
66
- // @ts-ignore
67
- delete window.location
68
- // @ts-ignore
69
- window.location = new URL(`${scheme}://localhost:3000`)
70
-
71
- const testEnvLicenseManager = new LicenseManager('', keyPair.publicKey)
72
- const licenseKey = await generateLicenseKey(STANDARD_LICENSE_INFO, keyPair)
73
- const result = await testEnvLicenseManager.getLicenseFromKey(licenseKey)
74
- expect(result).toMatchObject({
75
- isLicenseParseable: true,
76
- isDomainValid: false,
77
- isDevelopment: true,
78
- })
63
+ it('Signals that it is development mode for loopback hosts', async () => {
64
+ process.env.NODE_ENV = 'production'
65
+ try {
66
+ const schemes = ['http', 'https']
67
+ const hosts = ['localhost', '127.0.0.1', '127.0.0.2', '[::1]']
68
+ for (const scheme of schemes) {
69
+ for (const host of hosts) {
70
+ // @ts-ignore
71
+ delete window.location
72
+ // @ts-ignore
73
+ window.location = new URL(`${scheme}://${host}:3000`)
74
+
75
+ const testEnvLicenseManager = new LicenseManager('', keyPair.publicKey)
76
+ const licenseKey = await generateLicenseKey(STANDARD_LICENSE_INFO, keyPair)
77
+ const result = await testEnvLicenseManager.getLicenseFromKey(licenseKey)
78
+ expect(result).toMatchObject({
79
+ isLicenseParseable: true,
80
+ isDomainValid: false,
81
+ isDevelopment: true,
82
+ })
83
+ }
84
+ }
85
+ } finally {
86
+ process.env.NODE_ENV = 'test'
79
87
  }
80
88
  })
81
89
 
@@ -125,14 +125,20 @@ export class LicenseManager {
125
125
  }
126
126
 
127
127
  private getIsDevelopment() {
128
- // If we are using https on a non-localhost domain we assume it's a production env and a development one otherwise
128
+ // If we are using https on a non-loopback domain we assume it's a production env and a development one otherwise
129
129
  return (
130
130
  !['https:', 'vscode-webview:'].includes(window.location.protocol) ||
131
- window.location.hostname === 'localhost' ||
131
+ this.isLoopbackHost(window.location.hostname) ||
132
132
  process.env.NODE_ENV !== 'production'
133
133
  )
134
134
  }
135
135
 
136
+ private isLoopbackHost(hostname: string) {
137
+ // localhost, IPv4 loopback (127.0.0.0/8) and IPv6 loopback (::1) are all local development hosts
138
+ const host = hostname.toLowerCase().replace(/^\[|\]$/g, '')
139
+ return host === 'localhost' || host === '::1' || /^127(?:\.\d{1,3}){3}$/.test(host)
140
+ }
141
+
136
142
  private getTrackType(result: LicenseFromKeyResult, licenseState: LicenseState): TrackType {
137
143
  // Track watermark for unlicensed production deployments
138
144
  if (licenseState === 'unlicensed-production') {
@@ -1,5 +1,8 @@
1
1
  import { VecModel } from '@tldraw/tlschema'
2
2
  import { EASINGS } from './easings'
3
+
4
+ const { easeInQuad, easeInOutQuad } = EASINGS
5
+
3
6
  function clamp(n: number, min: number, max?: number): number {
4
7
  return Math.max(min, typeof max !== 'undefined' ? Math.min(n, max) : n)
5
8
  }
@@ -634,13 +637,13 @@ export class Vec {
634
637
  * @param steps - The number of points to return.
635
638
  * @param ease - The easing to use.
636
639
  */
637
- static PointsBetween(A: VecModel, B: VecModel, steps = 6, ease = EASINGS.easeInQuad): Vec[] {
640
+ static PointsBetween(A: VecModel, B: VecModel, steps = 6, ease = easeInQuad): Vec[] {
638
641
  const results: Vec[] = []
639
642
 
640
643
  for (let i = 0; i < steps; i++) {
641
644
  const t = ease(i / (steps - 1))
642
645
  const point = Vec.Lrp(A, B, t)
643
- point.z = Math.min(1, 0.5 + Math.abs(0.5 - EASINGS.easeInOutQuad(t)) * 0.65)
646
+ point.z = Math.min(1, 0.5 + Math.abs(0.5 - easeInOutQuad(t)) * 0.65)
644
647
  results.push(point)
645
648
  }
646
649