@tldraw/editor 3.13.0-canary.a2884bb1bab2 → 3.13.0-canary.ad6c4f5526a8

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 (185) hide show
  1. package/dist-cjs/index.d.ts +129 -113
  2. package/dist-cjs/index.js +7 -22
  3. package/dist-cjs/index.js.map +2 -2
  4. package/dist-cjs/lib/TldrawEditor.js +2 -1
  5. package/dist-cjs/lib/TldrawEditor.js.map +2 -2
  6. package/dist-cjs/lib/components/Shape.js +12 -8
  7. package/dist-cjs/lib/components/Shape.js.map +2 -2
  8. package/dist-cjs/lib/components/default-components/DefaultCanvas.js +37 -8
  9. package/dist-cjs/lib/components/default-components/DefaultCanvas.js.map +2 -2
  10. package/dist-cjs/lib/components/default-components/DefaultErrorFallback.js +14 -12
  11. package/dist-cjs/lib/components/default-components/DefaultErrorFallback.js.map +2 -2
  12. package/dist-cjs/lib/components/default-components/DefaultShapeIndicator.js +17 -11
  13. package/dist-cjs/lib/components/default-components/DefaultShapeIndicator.js.map +2 -2
  14. package/dist-cjs/lib/components/default-components/DefaultSpinner.js +1 -1
  15. package/dist-cjs/lib/components/default-components/DefaultSpinner.js.map +2 -2
  16. package/dist-cjs/lib/editor/Editor.js +110 -44
  17. package/dist-cjs/lib/editor/Editor.js.map +3 -3
  18. package/dist-cjs/lib/editor/managers/SnapManager/HandleSnaps.js.map +2 -2
  19. package/dist-cjs/lib/editor/managers/TextManager.js +10 -0
  20. package/dist-cjs/lib/editor/managers/TextManager.js.map +2 -2
  21. package/dist-cjs/lib/editor/shapes/ShapeUtil.js +1 -1
  22. package/dist-cjs/lib/editor/shapes/ShapeUtil.js.map +2 -2
  23. package/dist-cjs/lib/editor/shapes/group/GroupShapeUtil.js +0 -3
  24. package/dist-cjs/lib/editor/shapes/group/GroupShapeUtil.js.map +2 -2
  25. package/dist-cjs/lib/editor/shapes/shared/getPerfectDashProps.js.map +2 -2
  26. package/dist-cjs/lib/exports/getSvgJsx.js +12 -3
  27. package/dist-cjs/lib/exports/getSvgJsx.js.map +2 -2
  28. package/dist-cjs/lib/hooks/useDocumentEvents.js +3 -2
  29. package/dist-cjs/lib/hooks/useDocumentEvents.js.map +2 -2
  30. package/dist-cjs/lib/hooks/useEditorComponents.js +16 -16
  31. package/dist-cjs/lib/hooks/useEditorComponents.js.map +2 -2
  32. package/dist-cjs/lib/license/LicenseManager.js +8 -1
  33. package/dist-cjs/lib/license/LicenseManager.js.map +2 -2
  34. package/dist-cjs/lib/options.js.map +2 -2
  35. package/dist-cjs/lib/primitives/Box.js +16 -0
  36. package/dist-cjs/lib/primitives/Box.js.map +2 -2
  37. package/dist-cjs/lib/primitives/Mat.js +1 -1
  38. package/dist-cjs/lib/primitives/Mat.js.map +2 -2
  39. package/dist-cjs/lib/primitives/Vec.js +20 -0
  40. package/dist-cjs/lib/primitives/Vec.js.map +2 -2
  41. package/dist-cjs/lib/primitives/geometry/Arc2d.js +2 -2
  42. package/dist-cjs/lib/primitives/geometry/Arc2d.js.map +2 -2
  43. package/dist-cjs/lib/primitives/geometry/Circle2d.js +1 -1
  44. package/dist-cjs/lib/primitives/geometry/Circle2d.js.map +2 -2
  45. package/dist-cjs/lib/primitives/geometry/CubicBezier2d.js +1 -1
  46. package/dist-cjs/lib/primitives/geometry/CubicBezier2d.js.map +2 -2
  47. package/dist-cjs/lib/primitives/geometry/CubicSpline2d.js.map +2 -2
  48. package/dist-cjs/lib/primitives/geometry/Edge2d.js +1 -1
  49. package/dist-cjs/lib/primitives/geometry/Edge2d.js.map +2 -2
  50. package/dist-cjs/lib/primitives/geometry/Ellipse2d.js.map +2 -2
  51. package/dist-cjs/lib/primitives/geometry/Geometry2d.js +91 -20
  52. package/dist-cjs/lib/primitives/geometry/Geometry2d.js.map +2 -2
  53. package/dist-cjs/lib/primitives/geometry/Group2d.js +55 -2
  54. package/dist-cjs/lib/primitives/geometry/Group2d.js.map +2 -2
  55. package/dist-cjs/lib/primitives/geometry/Point2d.js.map +2 -2
  56. package/dist-cjs/lib/primitives/geometry/Polyline2d.js.map +2 -2
  57. package/dist-cjs/lib/primitives/geometry/Stadium2d.js.map +2 -2
  58. package/dist-cjs/lib/utils/areShapesContentEqual.js +25 -0
  59. package/dist-cjs/lib/utils/areShapesContentEqual.js.map +7 -0
  60. package/dist-cjs/lib/utils/debug-flags.js +5 -2
  61. package/dist-cjs/lib/utils/debug-flags.js.map +2 -2
  62. package/dist-cjs/lib/utils/dom.js +3 -3
  63. package/dist-cjs/lib/utils/dom.js.map +2 -2
  64. package/dist-cjs/lib/utils/nearestMultiple.js +34 -0
  65. package/dist-cjs/lib/utils/nearestMultiple.js.map +7 -0
  66. package/dist-cjs/lib/utils/rotation.js +5 -5
  67. package/dist-cjs/lib/utils/rotation.js.map +2 -2
  68. package/dist-cjs/version.js +3 -3
  69. package/dist-cjs/version.js.map +1 -1
  70. package/dist-esm/index.d.mts +129 -113
  71. package/dist-esm/index.mjs +9 -41
  72. package/dist-esm/index.mjs.map +2 -2
  73. package/dist-esm/lib/TldrawEditor.mjs +2 -1
  74. package/dist-esm/lib/TldrawEditor.mjs.map +2 -2
  75. package/dist-esm/lib/components/Shape.mjs +12 -8
  76. package/dist-esm/lib/components/Shape.mjs.map +2 -2
  77. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs +37 -8
  78. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs.map +2 -2
  79. package/dist-esm/lib/components/default-components/DefaultErrorFallback.mjs +14 -12
  80. package/dist-esm/lib/components/default-components/DefaultErrorFallback.mjs.map +2 -2
  81. package/dist-esm/lib/components/default-components/DefaultShapeIndicator.mjs +17 -11
  82. package/dist-esm/lib/components/default-components/DefaultShapeIndicator.mjs.map +2 -2
  83. package/dist-esm/lib/components/default-components/DefaultSpinner.mjs +1 -1
  84. package/dist-esm/lib/components/default-components/DefaultSpinner.mjs.map +2 -2
  85. package/dist-esm/lib/editor/Editor.mjs +110 -44
  86. package/dist-esm/lib/editor/Editor.mjs.map +3 -3
  87. package/dist-esm/lib/editor/managers/SnapManager/HandleSnaps.mjs.map +2 -2
  88. package/dist-esm/lib/editor/managers/TextManager.mjs +10 -0
  89. package/dist-esm/lib/editor/managers/TextManager.mjs.map +2 -2
  90. package/dist-esm/lib/editor/shapes/ShapeUtil.mjs +1 -1
  91. package/dist-esm/lib/editor/shapes/ShapeUtil.mjs.map +2 -2
  92. package/dist-esm/lib/editor/shapes/group/GroupShapeUtil.mjs +0 -3
  93. package/dist-esm/lib/editor/shapes/group/GroupShapeUtil.mjs.map +2 -2
  94. package/dist-esm/lib/editor/shapes/shared/getPerfectDashProps.mjs.map +2 -2
  95. package/dist-esm/lib/exports/getSvgJsx.mjs +12 -3
  96. package/dist-esm/lib/exports/getSvgJsx.mjs.map +2 -2
  97. package/dist-esm/lib/hooks/useDocumentEvents.mjs +3 -2
  98. package/dist-esm/lib/hooks/useDocumentEvents.mjs.map +2 -2
  99. package/dist-esm/lib/hooks/useEditorComponents.mjs +16 -18
  100. package/dist-esm/lib/hooks/useEditorComponents.mjs.map +2 -2
  101. package/dist-esm/lib/license/LicenseManager.mjs +8 -1
  102. package/dist-esm/lib/license/LicenseManager.mjs.map +2 -2
  103. package/dist-esm/lib/options.mjs.map +2 -2
  104. package/dist-esm/lib/primitives/Box.mjs +16 -0
  105. package/dist-esm/lib/primitives/Box.mjs.map +2 -2
  106. package/dist-esm/lib/primitives/Mat.mjs +1 -1
  107. package/dist-esm/lib/primitives/Mat.mjs.map +2 -2
  108. package/dist-esm/lib/primitives/Vec.mjs +20 -0
  109. package/dist-esm/lib/primitives/Vec.mjs.map +2 -2
  110. package/dist-esm/lib/primitives/geometry/Arc2d.mjs +2 -2
  111. package/dist-esm/lib/primitives/geometry/Arc2d.mjs.map +2 -2
  112. package/dist-esm/lib/primitives/geometry/Circle2d.mjs +1 -1
  113. package/dist-esm/lib/primitives/geometry/Circle2d.mjs.map +2 -2
  114. package/dist-esm/lib/primitives/geometry/CubicBezier2d.mjs +1 -1
  115. package/dist-esm/lib/primitives/geometry/CubicBezier2d.mjs.map +2 -2
  116. package/dist-esm/lib/primitives/geometry/CubicSpline2d.mjs.map +2 -2
  117. package/dist-esm/lib/primitives/geometry/Edge2d.mjs +1 -1
  118. package/dist-esm/lib/primitives/geometry/Edge2d.mjs.map +2 -2
  119. package/dist-esm/lib/primitives/geometry/Ellipse2d.mjs.map +2 -2
  120. package/dist-esm/lib/primitives/geometry/Geometry2d.mjs +92 -21
  121. package/dist-esm/lib/primitives/geometry/Geometry2d.mjs.map +2 -2
  122. package/dist-esm/lib/primitives/geometry/Group2d.mjs +55 -2
  123. package/dist-esm/lib/primitives/geometry/Group2d.mjs.map +2 -2
  124. package/dist-esm/lib/primitives/geometry/Point2d.mjs.map +2 -2
  125. package/dist-esm/lib/primitives/geometry/Polyline2d.mjs.map +2 -2
  126. package/dist-esm/lib/primitives/geometry/Stadium2d.mjs.map +2 -2
  127. package/dist-esm/lib/utils/areShapesContentEqual.mjs +5 -0
  128. package/dist-esm/lib/utils/areShapesContentEqual.mjs.map +7 -0
  129. package/dist-esm/lib/utils/debug-flags.mjs +5 -2
  130. package/dist-esm/lib/utils/debug-flags.mjs.map +2 -2
  131. package/dist-esm/lib/utils/dom.mjs +3 -3
  132. package/dist-esm/lib/utils/dom.mjs.map +2 -2
  133. package/dist-esm/lib/utils/nearestMultiple.mjs +14 -0
  134. package/dist-esm/lib/utils/nearestMultiple.mjs.map +7 -0
  135. package/dist-esm/lib/utils/rotation.mjs +5 -5
  136. package/dist-esm/lib/utils/rotation.mjs.map +2 -2
  137. package/dist-esm/version.mjs +3 -3
  138. package/dist-esm/version.mjs.map +1 -1
  139. package/editor.css +47 -4
  140. package/package.json +7 -7
  141. package/src/index.ts +16 -31
  142. package/src/lib/TldrawEditor.tsx +6 -1
  143. package/src/lib/components/Shape.tsx +14 -10
  144. package/src/lib/components/default-components/DefaultCanvas.tsx +43 -8
  145. package/src/lib/components/default-components/DefaultErrorFallback.tsx +25 -14
  146. package/src/lib/components/default-components/DefaultShapeIndicator.tsx +17 -8
  147. package/src/lib/components/default-components/DefaultSpinner.tsx +1 -1
  148. package/src/lib/editor/Editor.test.ts +1 -1
  149. package/src/lib/editor/Editor.ts +118 -43
  150. package/src/lib/editor/managers/SnapManager/HandleSnaps.ts +0 -1
  151. package/src/lib/editor/managers/TextManager.ts +12 -0
  152. package/src/lib/editor/shapes/ShapeUtil.ts +23 -3
  153. package/src/lib/editor/shapes/group/GroupShapeUtil.tsx +0 -4
  154. package/src/lib/editor/shapes/shared/getPerfectDashProps.ts +9 -9
  155. package/src/lib/exports/getSvgJsx.tsx +16 -7
  156. package/src/lib/hooks/useDocumentEvents.ts +7 -2
  157. package/src/lib/hooks/useEditorComponents.tsx +33 -32
  158. package/src/lib/license/LicenseManager.test.ts +40 -0
  159. package/src/lib/license/LicenseManager.ts +13 -1
  160. package/src/lib/options.ts +4 -0
  161. package/src/lib/primitives/Box.ts +20 -0
  162. package/src/lib/primitives/Mat.ts +5 -4
  163. package/src/lib/primitives/Vec.ts +23 -0
  164. package/src/lib/primitives/geometry/Arc2d.ts +5 -5
  165. package/src/lib/primitives/geometry/Circle2d.ts +4 -4
  166. package/src/lib/primitives/geometry/CubicBezier2d.ts +4 -4
  167. package/src/lib/primitives/geometry/CubicSpline2d.ts +3 -3
  168. package/src/lib/primitives/geometry/Edge2d.ts +3 -3
  169. package/src/lib/primitives/geometry/Ellipse2d.ts +3 -3
  170. package/src/lib/primitives/geometry/Geometry2d.test.ts +42 -0
  171. package/src/lib/primitives/geometry/Geometry2d.ts +123 -35
  172. package/src/lib/primitives/geometry/Group2d.ts +70 -7
  173. package/src/lib/primitives/geometry/Point2d.ts +2 -2
  174. package/src/lib/primitives/geometry/Polyline2d.ts +3 -3
  175. package/src/lib/primitives/geometry/Stadium2d.ts +3 -3
  176. package/src/lib/test/currentToolIdMask.test.ts +1 -1
  177. package/src/lib/test/user.test.ts +1 -1
  178. package/src/lib/utils/areShapesContentEqual.ts +4 -0
  179. package/src/lib/utils/debug-flags.ts +7 -2
  180. package/src/lib/utils/dom.ts +4 -4
  181. package/src/lib/utils/nearestMultiple.ts +13 -0
  182. package/src/lib/utils/rotation.ts +8 -6
  183. package/src/lib/utils/sync/LocalIndexedDb.test.ts +1 -1
  184. package/src/lib/utils/sync/TLLocalSyncClient.test.ts +1 -1
  185. package/src/version.ts +3 -3
@@ -9,13 +9,21 @@ import { useEditorComponents } from '../../hooks/useEditorComponents'
9
9
  import { OptionalErrorBoundary } from '../ErrorBoundary'
10
10
 
11
11
  // need an extra layer of indirection here to allow hooks to be used inside the indicator render
12
- const EvenInnererIndicator = memo(({ shape, util }: { shape: TLShape; util: ShapeUtil<any> }) => {
13
- return useStateTracking('Indicator: ' + shape.type, () =>
14
- // always fetch the latest shape from the store even if the props/meta have not changed, to avoid
15
- // calling the render method with stale data.
16
- util.indicator(util.editor.store.unsafeGetWithoutCapture(shape.id) as TLShape)
17
- )
18
- })
12
+ const EvenInnererIndicator = memo(
13
+ ({ shape, util }: { shape: TLShape; util: ShapeUtil<any> }) => {
14
+ return useStateTracking('Indicator: ' + shape.type, () =>
15
+ // always fetch the latest shape from the store even if the props/meta have not changed, to avoid
16
+ // calling the render method with stale data.
17
+ util.indicator(util.editor.store.unsafeGetWithoutCapture(shape.id) as TLShape)
18
+ )
19
+ },
20
+ (prevProps, nextProps) => {
21
+ return (
22
+ prevProps.shape.props === nextProps.shape.props &&
23
+ prevProps.shape.meta === nextProps.shape.meta
24
+ )
25
+ }
26
+ )
19
27
 
20
28
  const InnerIndicator = memo(({ editor, id }: { editor: Editor; id: TLShapeId }) => {
21
29
  const shape = useValue('shape for indicator', () => editor.store.get(id), [editor, id])
@@ -61,13 +69,14 @@ export const DefaultShapeIndicator = memo(function DefaultShapeIndicator({
61
69
  useQuickReactor(
62
70
  'indicator transform',
63
71
  () => {
72
+ if (hidden) return
64
73
  const elm = rIndicator.current
65
74
  if (!elm) return
66
75
  const pageTransform = editor.getShapePageTransform(shapeId)
67
76
  if (!pageTransform) return
68
77
  elm.style.setProperty('transform', pageTransform.toCssString())
69
78
  },
70
- [editor, shapeId]
79
+ [editor, shapeId, hidden]
71
80
  )
72
81
 
73
82
  useLayoutEffect(() => {
@@ -1,7 +1,7 @@
1
1
  /** @public @react */
2
2
  export function DefaultSpinner() {
3
3
  return (
4
- <svg width={16} height={16} viewBox="0 0 16 16">
4
+ <svg width={16} height={16} viewBox="0 0 16 16" aria-hidden="false">
5
5
  <g strokeWidth={2} fill="none" fillRule="evenodd">
6
6
  <circle strokeOpacity={0.25} cx={8} cy={8} r={7} stroke="currentColor" />
7
7
  <path strokeLinecap="round" d="M15 8c0-4.5-4.5-7-7-7" stroke="currentColor">
@@ -52,7 +52,7 @@ beforeEach(() => {
52
52
  shapeUtils: [CustomShape],
53
53
  bindingUtils: [],
54
54
  tools: [],
55
- store: createTLStore({ shapeUtils: [CustomShape] }),
55
+ store: createTLStore({ shapeUtils: [CustomShape], bindingUtils: [] }),
56
56
  getContainer: () => document.body,
57
57
  })
58
58
  editor.setCameraOptions({ isLocked: true })
@@ -42,6 +42,7 @@ import {
42
42
  TLImageAsset,
43
43
  TLInstance,
44
44
  TLInstancePageState,
45
+ TLInstancePresence,
45
46
  TLNoteShape,
46
47
  TLPOINTER_ID,
47
48
  TLPage,
@@ -128,6 +129,7 @@ import { Group2d } from '../primitives/geometry/Group2d'
128
129
  import { intersectPolygonPolygon } from '../primitives/intersect'
129
130
  import { PI, approximately, areAnglesCompatible, clamp, pointInPolygon } from '../primitives/utils'
130
131
  import { ReadonlySharedStyleMap, SharedStyle, SharedStyleMap } from '../utils/SharedStylesMap'
132
+ import { areShapesContentEqual } from '../utils/areShapesContentEqual'
131
133
  import { dataUrlToFile } from '../utils/assets'
132
134
  import { debugFlags } from '../utils/debug-flags'
133
135
  import {
@@ -324,7 +326,6 @@ export class Editor extends EventEmitter<TLEventMap> {
324
326
  this.options = { ...defaultTldrawOptions, ...options }
325
327
 
326
328
  this.store = store
327
- this.disposables.add(this.store.dispose.bind(this.store))
328
329
  this.history = new HistoryManager<TLRecord>({
329
330
  store,
330
331
  annotateError: (error) => {
@@ -954,6 +955,7 @@ export class Editor extends EventEmitter<TLEventMap> {
954
955
  dispose() {
955
956
  this.disposables.forEach((dispose) => dispose())
956
957
  this.disposables.clear()
958
+ this.store.dispose()
957
959
  this.isDisposed = true
958
960
  }
959
961
 
@@ -1704,8 +1706,7 @@ export class Editor extends EventEmitter<TLEventMap> {
1704
1706
  * @readonly
1705
1707
  */
1706
1708
  @computed getSelectedShapes(): TLShape[] {
1707
- const { selectedShapeIds } = this.getCurrentPageState()
1708
- return compact(selectedShapeIds.map((id) => this.store.get(id)))
1709
+ return compact(this.getSelectedShapeIds().map((id) => this.store.get(id)))
1709
1710
  }
1710
1711
 
1711
1712
  /**
@@ -1814,9 +1815,28 @@ export class Editor extends EventEmitter<TLEventMap> {
1814
1815
  return this
1815
1816
  }
1816
1817
 
1818
+ /**
1819
+ * Select the next shape in the reading order or in cardinal order.
1820
+ *
1821
+ * @example
1822
+ * ```ts
1823
+ * editor.selectAdjacentShape('next')
1824
+ * ```
1825
+ *
1826
+ * @public
1827
+ */
1817
1828
  selectAdjacentShape(direction: TLAdjacentDirection) {
1818
- const readingOrderShapes = this.getCurrentPageShapesInReadingOrder()
1819
1829
  const selectedShapeIds = this.getSelectedShapeIds()
1830
+ const firstParentId = selectedShapeIds[0] ? this.getShape(selectedShapeIds[0])?.parentId : null
1831
+ const isSelectedWithinContainer =
1832
+ firstParentId &&
1833
+ selectedShapeIds.every((shapeId) => this.getShape(shapeId)?.parentId === firstParentId) &&
1834
+ !isPageId(firstParentId)
1835
+ const readingOrderShapes = isSelectedWithinContainer
1836
+ ? this._getShapesInReadingOrder(
1837
+ this.getCurrentPageShapes().filter((shape) => shape.parentId === firstParentId)
1838
+ )
1839
+ : this.getCurrentPageShapesInReadingOrder()
1820
1840
  const currentShapeId: TLShapeId | undefined =
1821
1841
  selectedShapeIds.length === 1
1822
1842
  ? selectedShapeIds[0]
@@ -1838,13 +1858,7 @@ export class Editor extends EventEmitter<TLEventMap> {
1838
1858
  const shape = this.getShape(adjacentShapeId)
1839
1859
  if (!shape) return
1840
1860
 
1841
- this.setSelectedShapes([shape.id])
1842
- this.zoomToSelectionIfOffscreen(256, {
1843
- animation: {
1844
- duration: this.options.animationMediumMs,
1845
- },
1846
- inset: 0,
1847
- })
1861
+ this._selectShapesAndZoom([shape.id])
1848
1862
  }
1849
1863
 
1850
1864
  /**
@@ -1854,10 +1868,14 @@ export class Editor extends EventEmitter<TLEventMap> {
1854
1868
  * @public
1855
1869
  */
1856
1870
  @computed getCurrentPageShapesInReadingOrder(): TLShape[] {
1871
+ const shapes = this.getCurrentPageShapes().filter((shape) => isPageId(shape.parentId))
1872
+ return this._getShapesInReadingOrder(shapes)
1873
+ }
1874
+
1875
+ private _getShapesInReadingOrder(shapes: TLShape[]): TLShape[] {
1857
1876
  const SHALLOW_ANGLE = 20
1858
1877
  const ROW_THRESHOLD = 100
1859
1878
 
1860
- const shapes = this.getCurrentPageShapes()
1861
1879
  const tabbableShapes = shapes.filter((shape) => this.getShapeUtil(shape).canTabTo(shape))
1862
1880
 
1863
1881
  if (tabbableShapes.length <= 1) return tabbableShapes
@@ -2003,6 +2021,36 @@ export class Editor extends EventEmitter<TLEventMap> {
2003
2021
  return lowestScoringShape!.shape.id
2004
2022
  }
2005
2023
 
2024
+ selectParentShape() {
2025
+ const selectedShape = this.getOnlySelectedShape()
2026
+ if (!selectedShape) return
2027
+ const parentShape = this.getShape(selectedShape.parentId)
2028
+ if (!parentShape) return
2029
+ this._selectShapesAndZoom([parentShape.id])
2030
+ }
2031
+
2032
+ selectFirstChildShape() {
2033
+ const selectedShapes = this.getSelectedShapes()
2034
+ if (!selectedShapes.length) return
2035
+ const selectedShape = selectedShapes[0]
2036
+ const children = this.getSortedChildIdsForParent(selectedShape.id)
2037
+ .map((id) => this.getShape(id))
2038
+ .filter((i) => i) as TLShape[]
2039
+ const sortedChildren = this._getShapesInReadingOrder(children)
2040
+ if (sortedChildren.length === 0) return
2041
+ this._selectShapesAndZoom([sortedChildren[0].id])
2042
+ }
2043
+
2044
+ private _selectShapesAndZoom(ids: TLShapeId[]) {
2045
+ this.setSelectedShapes(ids)
2046
+ this.zoomToSelectionIfOffscreen(256, {
2047
+ animation: {
2048
+ duration: this.options.animationMediumMs,
2049
+ },
2050
+ inset: 0,
2051
+ })
2052
+ }
2053
+
2006
2054
  /**
2007
2055
  * Clear the selection.
2008
2056
  *
@@ -2275,13 +2323,21 @@ export class Editor extends EventEmitter<TLEventMap> {
2275
2323
  setEditingShape(shape: TLShapeId | TLShape | null): this {
2276
2324
  const id = typeof shape === 'string' ? shape : (shape?.id ?? null)
2277
2325
  this.setRichTextEditor(null)
2278
- if (id !== this.getEditingShapeId()) {
2326
+ const prevEditingShapeId = this.getEditingShapeId()
2327
+ if (id !== prevEditingShapeId) {
2279
2328
  if (id) {
2280
2329
  const shape = this.getShape(id)
2281
2330
  if (shape && this.getShapeUtil(shape).canEdit(shape)) {
2282
2331
  this.run(
2283
2332
  () => {
2284
2333
  this._updateCurrentPageState({ editingShapeId: id })
2334
+ if (prevEditingShapeId) {
2335
+ const prevEditingShape = this.getShape(prevEditingShapeId)
2336
+ if (prevEditingShape) {
2337
+ this.getShapeUtil(prevEditingShape).onEditEnd?.(prevEditingShape)
2338
+ }
2339
+ }
2340
+ this.getShapeUtil(shape).onEditStart?.(shape)
2285
2341
  },
2286
2342
  { history: 'ignore' }
2287
2343
  )
@@ -2294,6 +2350,12 @@ export class Editor extends EventEmitter<TLEventMap> {
2294
2350
  () => {
2295
2351
  this._updateCurrentPageState({ editingShapeId: null })
2296
2352
  this._currentRichTextEditor.set(null)
2353
+ if (prevEditingShapeId) {
2354
+ const prevEditingShape = this.getShape(prevEditingShapeId)
2355
+ if (prevEditingShape) {
2356
+ this.getShapeUtil(prevEditingShape).onEditEnd?.(prevEditingShape)
2357
+ }
2358
+ }
2297
2359
  },
2298
2360
  { history: 'ignore' }
2299
2361
  )
@@ -2575,14 +2637,25 @@ export class Editor extends EventEmitter<TLEventMap> {
2575
2637
  return baseCamera
2576
2638
  }
2577
2639
 
2640
+ private _getFollowingPresence(targetUserId: string | null) {
2641
+ const visited = [this.user.getId()]
2642
+ const collaborators = this.getCollaborators()
2643
+ let leaderPresence = null as null | TLInstancePresence
2644
+ while (targetUserId && !visited.includes(targetUserId)) {
2645
+ leaderPresence = collaborators.find((c) => c.userId === targetUserId) ?? null
2646
+ targetUserId = leaderPresence?.followingUserId ?? null
2647
+ if (leaderPresence) {
2648
+ visited.push(leaderPresence.userId)
2649
+ }
2650
+ }
2651
+ return leaderPresence
2652
+ }
2653
+
2578
2654
  @computed
2579
2655
  private getViewportPageBoundsForFollowing(): null | Box {
2580
- const followingUserId = this.getInstanceState().followingUserId
2581
- if (!followingUserId) return null
2582
- const leaderPresence = this.getCollaborators().find((c) => c.userId === followingUserId)
2583
- if (!leaderPresence) return null
2656
+ const leaderPresence = this._getFollowingPresence(this.getInstanceState().followingUserId)
2584
2657
 
2585
- if (!leaderPresence.camera || !leaderPresence.screenBounds) return null
2658
+ if (!leaderPresence?.camera || !leaderPresence?.screenBounds) return null
2586
2659
 
2587
2660
  // Fit their viewport inside of our screen bounds
2588
2661
  // 1. calculate their viewport in page space
@@ -3781,15 +3854,6 @@ export class Editor extends EventEmitter<TLEventMap> {
3781
3854
  // if we were already following someone, stop following them
3782
3855
  this.stopFollowingUser()
3783
3856
 
3784
- const leaderPresences = this._getCollaboratorsQuery()
3785
- .get()
3786
- .filter((p) => p.userId === userId)
3787
-
3788
- if (!leaderPresences.length) {
3789
- console.warn('User not found')
3790
- return this
3791
- }
3792
-
3793
3857
  const thisUserId = this.user.getId()
3794
3858
 
3795
3859
  if (!thisUserId) {
@@ -3797,13 +3861,14 @@ export class Editor extends EventEmitter<TLEventMap> {
3797
3861
  // allow to continue since it's probably fine most of the time.
3798
3862
  }
3799
3863
 
3800
- // If the leader is following us, then we can't follow them
3801
- if (leaderPresences.some((p) => p.followingUserId === thisUserId)) {
3864
+ const leaderPresence = this._getFollowingPresence(userId)
3865
+
3866
+ if (!leaderPresence) {
3802
3867
  return this
3803
3868
  }
3804
3869
 
3805
3870
  const latestLeaderPresence = computed('latestLeaderPresence', () => {
3806
- return this.getCollaborators().find((p) => p.userId === userId)
3871
+ return this._getFollowingPresence(userId)
3807
3872
  })
3808
3873
 
3809
3874
  transact(() => {
@@ -4571,7 +4636,7 @@ export class Editor extends EventEmitter<TLEventMap> {
4571
4636
  this.fonts.trackFontsForShape(shape)
4572
4637
  return this.getShapeUtil(shape).getGeometry(shape, opts)
4573
4638
  },
4574
- { areRecordsEqual: (a, b) => a.props === b.props }
4639
+ { areRecordsEqual: areShapesContentEqual }
4575
4640
  )
4576
4641
  }
4577
4642
  return this._shapeGeometryCaches[context].get(
@@ -4619,9 +4684,15 @@ export class Editor extends EventEmitter<TLEventMap> {
4619
4684
 
4620
4685
  /** @internal */
4621
4686
  @computed private _getShapeHandlesCache(): ComputedCache<TLHandle[] | undefined, TLShape> {
4622
- return this.store.createComputedCache('handles', (shape) => {
4623
- return this.getShapeUtil(shape).getHandles?.(shape)
4624
- })
4687
+ return this.store.createComputedCache(
4688
+ 'handles',
4689
+ (shape) => {
4690
+ return this.getShapeUtil(shape).getHandles?.(shape)
4691
+ },
4692
+ {
4693
+ areRecordsEqual: areShapesContentEqual,
4694
+ }
4695
+ )
4625
4696
  }
4626
4697
 
4627
4698
  /**
@@ -5842,9 +5913,15 @@ export class Editor extends EventEmitter<TLEventMap> {
5842
5913
  @computed
5843
5914
  private _getBindingsIndexCache() {
5844
5915
  const index = bindingsIndex(this)
5845
- return this.store.createComputedCache<TLBinding[], TLShape>('bindingsIndex', (shape) => {
5846
- return index.get().get(shape.id)
5847
- })
5916
+ return this.store.createComputedCache<TLBinding[], TLShape>(
5917
+ 'bindingsIndex',
5918
+ (shape) => {
5919
+ return index.get().get(shape.id)
5920
+ },
5921
+ // we can ignore the shape equality check here because the index is
5922
+ // computed incrementally based on what bindings are in the store
5923
+ { areRecordsEqual: () => true }
5924
+ )
5848
5925
  }
5849
5926
 
5850
5927
  /**
@@ -10211,7 +10288,7 @@ export class Editor extends EventEmitter<TLEventMap> {
10211
10288
 
10212
10289
  // If the camera behavior is "zoom" and the ctrl key is pressed, then pan;
10213
10290
  // If the camera behavior is "pan" and the ctrl key is not pressed, then zoom
10214
- if (inputs.ctrlKey) behavior = wheelBehavior === 'pan' ? 'zoom' : 'pan'
10291
+ if (info.ctrlKey) behavior = wheelBehavior === 'pan' ? 'zoom' : 'pan'
10215
10292
 
10216
10293
  switch (behavior) {
10217
10294
  case 'zoom': {
@@ -10327,12 +10404,10 @@ export class Editor extends EventEmitter<TLEventMap> {
10327
10404
  if (this.inputs.isPanning && this.inputs.isPointing) {
10328
10405
  // Handle spacebar / middle mouse button panning
10329
10406
  const { currentScreenPoint, previousScreenPoint } = this.inputs
10330
- const { panSpeed } = cameraOptions
10331
10407
  const offset = Vec.Sub(currentScreenPoint, previousScreenPoint)
10332
- this.setCamera(
10333
- new Vec(cx + (offset.x * panSpeed) / cz, cy + (offset.y * panSpeed) / cz, cz),
10334
- { immediate: true }
10335
- )
10408
+ this.setCamera(new Vec(cx + offset.x / cz, cy + offset.y / cz, cz), {
10409
+ immediate: true,
10410
+ })
10336
10411
  this.maybeTrackPerformance('Panning')
10337
10412
  return
10338
10413
  }
@@ -45,7 +45,6 @@ export interface HandleSnapGeometry {
45
45
 
46
46
  const defaultGetSelfSnapOutline = () => null
47
47
  const defaultGetSelfSnapPoints = () => []
48
-
49
48
  /** @public */
50
49
  export class HandleSnaps {
51
50
  readonly editor: Editor
@@ -32,6 +32,7 @@ export interface TLMeasureTextSpanOpts {
32
32
  fontStyle: string
33
33
  lineHeight: number
34
34
  textAlign: TLDefaultHorizontalAlignStyle
35
+ otherStyles?: Record<string, string>
35
36
  }
36
37
 
37
38
  const spaceCharacterRegex = /\s/
@@ -86,6 +87,7 @@ export class TextManager {
86
87
  */
87
88
  maxWidth: null | number
88
89
  minWidth?: null | number
90
+ otherStyles?: Record<string, string>
89
91
  padding: string
90
92
  disableOverflowWrapBreaking?: boolean
91
93
  }
@@ -112,6 +114,11 @@ export class TextManager {
112
114
  'overflow-wrap',
113
115
  opts.disableOverflowWrapBreaking ? 'normal' : 'break-word'
114
116
  )
117
+ if (opts.otherStyles) {
118
+ for (const [key, value] of Object.entries(opts.otherStyles)) {
119
+ wrapperElm.style.setProperty(key, value)
120
+ }
121
+ }
115
122
 
116
123
  const scrollWidth = wrapperElm.scrollWidth
117
124
  const rect = wrapperElm.getBoundingClientRect()
@@ -256,6 +263,11 @@ export class TextManager {
256
263
  elm.style.setProperty('line-height', `${opts.lineHeight * opts.fontSize}px`)
257
264
  elm.style.setProperty('text-align', textAlignmentsForLtr[opts.textAlign])
258
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
+ }
259
271
 
260
272
  const shouldTruncateToFirstLine =
261
273
  opts.overflow === 'truncate-ellipsis' || opts.overflow === 'truncate-clip'
@@ -19,6 +19,7 @@ import { TLFontFace } from '../managers/FontManager'
19
19
  import { BoundsSnapGeometry } from '../managers/SnapManager/BoundsSnaps'
20
20
  import { HandleSnapGeometry } from '../managers/SnapManager/HandleSnaps'
21
21
  import { SvgExportContext } from '../types/SvgExportContext'
22
+ import { TLClickEventInfo } from '../types/event-types'
22
23
  import { TLResizeHandle } from '../types/selection-types'
23
24
 
24
25
  /** @public */
@@ -244,7 +245,7 @@ export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
244
245
  *
245
246
  * @public
246
247
  */
247
- canEditInReadOnly(_shape: Shape): boolean {
248
+ canEditInReadonly(_shape: Shape): boolean {
248
249
  return false
249
250
  }
250
251
 
@@ -671,10 +672,21 @@ export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
671
672
  * A callback called when a shape's edge is double clicked.
672
673
  *
673
674
  * @param shape - The shape.
675
+ * @param info - Info about the edge.
674
676
  * @returns A change to apply to the shape, or void.
675
677
  * @public
676
678
  */
677
- onDoubleClickEdge?(shape: Shape): TLShapePartial<Shape> | void
679
+ onDoubleClickEdge?(shape: Shape, info: TLClickEventInfo): TLShapePartial<Shape> | void
680
+
681
+ /**
682
+ * A callback called when a shape's corner is double clicked.
683
+ *
684
+ * @param shape - The shape.
685
+ * @param info - Info about the corner.
686
+ * @returns A change to apply to the shape, or void.
687
+ * @public
688
+ */
689
+ onDoubleClickCorner?(shape: Shape, info: TLClickEventInfo): TLShapePartial<Shape> | void
678
690
 
679
691
  /**
680
692
  * A callback called when a shape is double clicked.
@@ -695,7 +707,15 @@ export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
695
707
  onClick?(shape: Shape): TLShapePartial<Shape> | void
696
708
 
697
709
  /**
698
- * A callback called when a shape finishes being editing.
710
+ * A callback called when a shape starts being edited.
711
+ *
712
+ * @param shape - The shape.
713
+ * @public
714
+ */
715
+ onEditStart?(shape: Shape): void
716
+
717
+ /**
718
+ * A callback called when a shape finishes being edited.
699
719
  *
700
720
  * @param shape - The shape.
701
721
  * @public
@@ -12,10 +12,6 @@ export class GroupShapeUtil extends ShapeUtil<TLGroupShape> {
12
12
  static override props = groupShapeProps
13
13
  static override migrations = groupShapeMigrations
14
14
 
15
- override canTabTo() {
16
- return false
17
- }
18
-
19
15
  override hideSelectionBoundsFg() {
20
16
  return true
21
17
  }
@@ -4,15 +4,15 @@ import { TLDefaultDashStyle } from '@tldraw/tlschema'
4
4
  export function getPerfectDashProps(
5
5
  totalLength: number,
6
6
  strokeWidth: number,
7
- opts = {} as Partial<{
8
- style: TLDefaultDashStyle
9
- snap: number
10
- end: 'skip' | 'outset' | 'none'
11
- start: 'skip' | 'outset' | 'none'
12
- lengthRatio: number
13
- closed: boolean
14
- forceSolid: boolean
15
- }>
7
+ opts: {
8
+ style?: TLDefaultDashStyle
9
+ snap?: number
10
+ end?: 'skip' | 'outset' | 'none'
11
+ start?: 'skip' | 'outset' | 'none'
12
+ lengthRatio?: number
13
+ closed?: boolean
14
+ forceSolid?: boolean
15
+ } = {}
16
16
  ): {
17
17
  strokeDasharray: string
18
18
  strokeDashoffset: string
@@ -365,6 +365,21 @@ function SvgExport({
365
365
  onMount()
366
366
  }, [onMount, shapeElements])
367
367
 
368
+ let backgroundColor = background ? theme.background : 'transparent'
369
+
370
+ if (singleFrameShapeId && background) {
371
+ const frameShapeUtil = editor.getShapeUtil('frame') as any as
372
+ | undefined
373
+ | { options: { showColors: boolean } }
374
+ if (frameShapeUtil?.options.showColors) {
375
+ const shape = editor.getShape(singleFrameShapeId)! as TLFrameShape
376
+ const color = theme[shape.props.color]
377
+ backgroundColor = color.frame.fill
378
+ } else {
379
+ backgroundColor = theme.solid
380
+ }
381
+ }
382
+
368
383
  return (
369
384
  <SvgExportContextProvider editor={editor} context={exportContext}>
370
385
  <svg
@@ -375,13 +390,7 @@ function SvgExport({
375
390
  viewBox={`${bbox.minX} ${bbox.minY} ${bbox.width} ${bbox.height}`}
376
391
  strokeLinecap="round"
377
392
  strokeLinejoin="round"
378
- style={{
379
- backgroundColor: background
380
- ? singleFrameShapeId
381
- ? theme.solid
382
- : theme.background
383
- : 'transparent',
384
- }}
393
+ style={{ backgroundColor }}
385
394
  data-color-mode={isDarkMode ? 'dark' : 'light'}
386
395
  className={`tl-container tl-theme__force-sRGB ${isDarkMode ? 'tl-theme__dark' : 'tl-theme__light'}`}
387
396
  >
@@ -11,6 +11,7 @@ export function useDocumentEvents() {
11
11
  const editor = useEditor()
12
12
  const container = useContainer()
13
13
 
14
+ const isEditing = useValue('isEditing', () => editor.getEditingShapeId(), [editor])
14
15
  const isAppFocused = useValue('isFocused', () => editor.getIsFocused(), [editor])
15
16
 
16
17
  // Prevent the browser's default drag and drop behavior on our container (UI, etc)
@@ -125,7 +126,11 @@ export function useDocumentEvents() {
125
126
  if (areShortcutsDisabled(editor)) {
126
127
  return
127
128
  }
128
- if (hasSelectedShapes) {
129
+ // isEditing here sounds like it's about text editing
130
+ // but more specifically, this is so you can tab into an
131
+ // embed that's being 'edited'. In our world,
132
+ // editing an embed, means it's interactive.
133
+ if (hasSelectedShapes && !isEditing) {
129
134
  // This is used in tandem with shape navigation.
130
135
  preventDefault(e)
131
136
  }
@@ -289,7 +294,7 @@ export function useDocumentEvents() {
289
294
  container.removeEventListener('keydown', handleKeyDown)
290
295
  container.removeEventListener('keyup', handleKeyUp)
291
296
  }
292
- }, [editor, container, isAppFocused])
297
+ }, [editor, container, isAppFocused, isEditing])
293
298
  }
294
299
 
295
300
  function areShortcutsDisabled(editor: Editor) {