@tldraw/editor 4.2.0-next.47462e908ff5 → 4.2.0-next.67908ea044c6

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 (49) hide show
  1. package/dist-cjs/index.d.ts +56 -6
  2. package/dist-cjs/index.js +2 -1
  3. package/dist-cjs/index.js.map +2 -2
  4. package/dist-cjs/lib/components/default-components/DefaultCanvas.js +11 -11
  5. package/dist-cjs/lib/components/default-components/DefaultCanvas.js.map +2 -2
  6. package/dist-cjs/lib/editor/Editor.js +31 -0
  7. package/dist-cjs/lib/editor/Editor.js.map +2 -2
  8. package/dist-cjs/lib/license/LicenseManager.js +1 -0
  9. package/dist-cjs/lib/license/LicenseManager.js.map +2 -2
  10. package/dist-cjs/lib/license/Watermark.js +8 -4
  11. package/dist-cjs/lib/license/Watermark.js.map +2 -2
  12. package/dist-cjs/lib/utils/debug-flags.js +1 -0
  13. package/dist-cjs/lib/utils/debug-flags.js.map +2 -2
  14. package/dist-cjs/lib/utils/runtime.js +2 -2
  15. package/dist-cjs/lib/utils/runtime.js.map +2 -2
  16. package/dist-cjs/lib/utils/window-open.js +2 -2
  17. package/dist-cjs/lib/utils/window-open.js.map +2 -2
  18. package/dist-cjs/version.js +3 -3
  19. package/dist-cjs/version.js.map +1 -1
  20. package/dist-esm/index.d.mts +56 -6
  21. package/dist-esm/index.mjs +3 -1
  22. package/dist-esm/index.mjs.map +2 -2
  23. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs +11 -11
  24. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs.map +2 -2
  25. package/dist-esm/lib/editor/Editor.mjs +31 -0
  26. package/dist-esm/lib/editor/Editor.mjs.map +2 -2
  27. package/dist-esm/lib/license/LicenseManager.mjs +1 -0
  28. package/dist-esm/lib/license/LicenseManager.mjs.map +2 -2
  29. package/dist-esm/lib/license/Watermark.mjs +8 -4
  30. package/dist-esm/lib/license/Watermark.mjs.map +2 -2
  31. package/dist-esm/lib/utils/debug-flags.mjs +1 -0
  32. package/dist-esm/lib/utils/debug-flags.mjs.map +2 -2
  33. package/dist-esm/lib/utils/runtime.mjs +2 -2
  34. package/dist-esm/lib/utils/runtime.mjs.map +2 -2
  35. package/dist-esm/lib/utils/window-open.mjs +2 -2
  36. package/dist-esm/lib/utils/window-open.mjs.map +2 -2
  37. package/dist-esm/version.mjs +3 -3
  38. package/dist-esm/version.mjs.map +1 -1
  39. package/package.json +10 -10
  40. package/src/index.ts +1 -0
  41. package/src/lib/components/default-components/DefaultCanvas.tsx +9 -9
  42. package/src/lib/editor/Editor.test.ts +268 -0
  43. package/src/lib/editor/Editor.ts +33 -0
  44. package/src/lib/license/LicenseManager.ts +1 -0
  45. package/src/lib/license/Watermark.tsx +8 -5
  46. package/src/lib/utils/debug-flags.ts +5 -4
  47. package/src/lib/utils/runtime.ts +3 -3
  48. package/src/lib/utils/window-open.ts +13 -3
  49. package/src/version.ts +3 -3
@@ -11,6 +11,7 @@ import {
11
11
  createTLStore,
12
12
  } from '../..'
13
13
  import { Editor } from './Editor'
14
+ import { StateNode } from './tools/StateNode'
14
15
 
15
16
  type ICustomShape = TLBaseShape<
16
17
  'my-custom-shape',
@@ -923,3 +924,270 @@ describe('replaceExternalContent', () => {
923
924
  expect(mockHandler).toHaveBeenCalledWith(info)
924
925
  })
925
926
  })
927
+
928
+ describe('setTool', () => {
929
+ class CustomToolA extends StateNode {
930
+ static override id = 'custom-tool-a'
931
+ }
932
+
933
+ class CustomToolB extends StateNode {
934
+ static override id = 'custom-tool-b'
935
+ }
936
+
937
+ class CustomToolC extends StateNode {
938
+ static override id = 'custom-tool-c'
939
+ }
940
+
941
+ class ParentTool extends StateNode {
942
+ static override id = 'parent-tool'
943
+ static override initial = 'child-tool-1'
944
+ static override children() {
945
+ return [ChildTool1]
946
+ }
947
+ }
948
+
949
+ class ChildTool1 extends StateNode {
950
+ static override id = 'child-tool-1'
951
+ }
952
+
953
+ class ChildTool2 extends StateNode {
954
+ static override id = 'child-tool-2'
955
+ }
956
+
957
+ let toolEditor: Editor
958
+
959
+ beforeEach(() => {
960
+ toolEditor = new Editor({
961
+ shapeUtils: [],
962
+ bindingUtils: [],
963
+ tools: [CustomToolA, ParentTool],
964
+ store: createTLStore({ shapeUtils: [], bindingUtils: [] }),
965
+ getContainer: () => document.body,
966
+ })
967
+ })
968
+
969
+ it('should add a tool to the root state', () => {
970
+ // Initially CustomToolB should not exist
971
+ expect(toolEditor.root.children!['custom-tool-b']).toBeUndefined()
972
+
973
+ // Add CustomToolB
974
+ toolEditor.setTool(CustomToolB)
975
+
976
+ // CustomToolB should now exist in root
977
+ expect(toolEditor.root.children!['custom-tool-b']).toBeDefined()
978
+ expect(toolEditor.root.children!['custom-tool-b']).toBeInstanceOf(CustomToolB)
979
+ })
980
+
981
+ it('should add a tool to a specific parent state', () => {
982
+ const parentTool = toolEditor.root.children!['parent-tool'] as ParentTool
983
+
984
+ // Initially should only have child-tool-1
985
+ expect(Object.keys(parentTool.children!)).toHaveLength(1)
986
+ expect(parentTool.children!['child-tool-1']).toBeDefined()
987
+ expect(parentTool.children!['child-tool-2']).toBeUndefined()
988
+
989
+ // Add ChildTool2 to ParentTool
990
+ toolEditor.setTool(ChildTool2, parentTool)
991
+
992
+ // Should now have both children
993
+ expect(Object.keys(parentTool.children!)).toHaveLength(2)
994
+ expect(parentTool.children!['child-tool-1']).toBeDefined()
995
+ expect(parentTool.children!['child-tool-2']).toBeDefined()
996
+ expect(parentTool.children!['child-tool-2']).toBeInstanceOf(ChildTool2)
997
+ })
998
+
999
+ it('should throw an error when trying to override an existing tool', () => {
1000
+ // CustomToolA is already in the root (added in beforeEach)
1001
+ expect(toolEditor.root.children!['custom-tool-a']).toBeDefined()
1002
+
1003
+ // Should throw error when trying to add another tool with the same ID
1004
+ expect(() => {
1005
+ toolEditor.setTool(CustomToolA)
1006
+ }).toThrow('Can\'t override tool with id "custom-tool-a"')
1007
+ })
1008
+
1009
+ it('should allow transitioning to a newly added tool', () => {
1010
+ // Add CustomToolB
1011
+ toolEditor.setTool(CustomToolB)
1012
+
1013
+ // Should be able to transition to the new tool
1014
+ expect(() => {
1015
+ toolEditor.setCurrentTool('custom-tool-b')
1016
+ }).not.toThrow()
1017
+
1018
+ // Should now be on the new tool
1019
+ expect(toolEditor.getCurrentToolId()).toBe('custom-tool-b')
1020
+ })
1021
+
1022
+ it('should create the tool with the correct editor and parent', () => {
1023
+ // Add CustomToolB to root
1024
+ toolEditor.setTool(CustomToolB)
1025
+
1026
+ const customToolB = toolEditor.root.children!['custom-tool-b'] as CustomToolB
1027
+
1028
+ expect(customToolB.editor).toBe(toolEditor)
1029
+ expect(customToolB.parent).toBe(toolEditor.root)
1030
+ })
1031
+
1032
+ it('should maintain existing tools when adding new ones', () => {
1033
+ const originalTool = toolEditor.root.children!['custom-tool-a']
1034
+
1035
+ // Add CustomToolB
1036
+ toolEditor.setTool(CustomToolB)
1037
+
1038
+ // Original tool should still exist
1039
+ expect(toolEditor.root.children!['custom-tool-a']).toBe(originalTool)
1040
+ expect(toolEditor.root.children!['custom-tool-a']).toBeInstanceOf(CustomToolA)
1041
+ })
1042
+
1043
+ it('should allow adding multiple tools', () => {
1044
+ // Add multiple tools
1045
+ toolEditor.setTool(CustomToolB)
1046
+ toolEditor.setTool(CustomToolC)
1047
+
1048
+ // All tools should exist
1049
+ expect(toolEditor.root.children!['custom-tool-a']).toBeDefined()
1050
+ expect(toolEditor.root.children!['custom-tool-b']).toBeDefined()
1051
+ expect(toolEditor.root.children!['custom-tool-c']).toBeDefined()
1052
+ expect(toolEditor.root.children!['custom-tool-b']).toBeInstanceOf(CustomToolB)
1053
+ expect(toolEditor.root.children!['custom-tool-c']).toBeInstanceOf(CustomToolC)
1054
+ })
1055
+ })
1056
+
1057
+ describe('removeTool', () => {
1058
+ class CustomToolA extends StateNode {
1059
+ static override id = 'custom-tool-a'
1060
+ }
1061
+
1062
+ class CustomToolB extends StateNode {
1063
+ static override id = 'custom-tool-b'
1064
+ }
1065
+
1066
+ class CustomToolC extends StateNode {
1067
+ static override id = 'custom-tool-c'
1068
+ }
1069
+
1070
+ class ParentTool extends StateNode {
1071
+ static override id = 'parent-tool'
1072
+ static override initial = 'child-tool-1'
1073
+ static override children() {
1074
+ return [ChildTool1, ChildTool2]
1075
+ }
1076
+ }
1077
+
1078
+ class ChildTool1 extends StateNode {
1079
+ static override id = 'child-tool-1'
1080
+ }
1081
+
1082
+ class ChildTool2 extends StateNode {
1083
+ static override id = 'child-tool-2'
1084
+ }
1085
+
1086
+ let toolEditor: Editor
1087
+
1088
+ beforeEach(() => {
1089
+ toolEditor = new Editor({
1090
+ shapeUtils: [],
1091
+ bindingUtils: [],
1092
+ tools: [CustomToolA, CustomToolB, CustomToolC, ParentTool],
1093
+ store: createTLStore({ shapeUtils: [], bindingUtils: [] }),
1094
+ getContainer: () => document.body,
1095
+ })
1096
+ })
1097
+
1098
+ it('should remove a tool from the root state', () => {
1099
+ // CustomToolB should exist initially
1100
+ expect(toolEditor.root.children!['custom-tool-b']).toBeDefined()
1101
+
1102
+ // Remove CustomToolB
1103
+ toolEditor.removeTool(CustomToolB)
1104
+
1105
+ // CustomToolB should no longer exist
1106
+ expect(toolEditor.root.children!['custom-tool-b']).toBeUndefined()
1107
+ })
1108
+
1109
+ it('should remove a tool from a specific parent state', () => {
1110
+ const parentTool = toolEditor.root.children!['parent-tool'] as ParentTool
1111
+
1112
+ // Initially should have both children
1113
+ expect(Object.keys(parentTool.children!)).toHaveLength(2)
1114
+ expect(parentTool.children!['child-tool-1']).toBeDefined()
1115
+ expect(parentTool.children!['child-tool-2']).toBeDefined()
1116
+
1117
+ // Remove ChildTool2 from ParentTool
1118
+ toolEditor.removeTool(ChildTool2, parentTool)
1119
+
1120
+ // Should now only have child-tool-1
1121
+ expect(Object.keys(parentTool.children!)).toHaveLength(1)
1122
+ expect(parentTool.children!['child-tool-1']).toBeDefined()
1123
+ expect(parentTool.children!['child-tool-2']).toBeUndefined()
1124
+ })
1125
+
1126
+ it('should not throw an error when trying to remove a non-existent tool', () => {
1127
+ // First remove CustomToolB
1128
+ toolEditor.removeTool(CustomToolB)
1129
+ expect(toolEditor.root.children!['custom-tool-b']).toBeUndefined()
1130
+
1131
+ // Trying to remove it again should not throw
1132
+ expect(() => {
1133
+ toolEditor.removeTool(CustomToolB)
1134
+ }).not.toThrow()
1135
+ })
1136
+
1137
+ it('should maintain other tools when removing one', () => {
1138
+ const originalToolA = toolEditor.root.children!['custom-tool-a']
1139
+ const originalToolC = toolEditor.root.children!['custom-tool-c']
1140
+
1141
+ // Remove CustomToolB
1142
+ toolEditor.removeTool(CustomToolB)
1143
+
1144
+ // Other tools should still exist
1145
+ expect(toolEditor.root.children!['custom-tool-a']).toBe(originalToolA)
1146
+ expect(toolEditor.root.children!['custom-tool-c']).toBe(originalToolC)
1147
+ expect(toolEditor.root.children!['custom-tool-a']).toBeInstanceOf(CustomToolA)
1148
+ expect(toolEditor.root.children!['custom-tool-c']).toBeInstanceOf(CustomToolC)
1149
+ })
1150
+
1151
+ it('should not be able to transition to a removed tool', () => {
1152
+ // Remove CustomToolB
1153
+ toolEditor.removeTool(CustomToolB)
1154
+
1155
+ // Should throw when trying to transition to removed tool
1156
+ expect(() => {
1157
+ toolEditor.setCurrentTool('custom-tool-b')
1158
+ }).toThrow()
1159
+ })
1160
+
1161
+ it('should allow removing multiple tools', () => {
1162
+ // Remove multiple tools
1163
+ toolEditor.removeTool(CustomToolB)
1164
+ toolEditor.removeTool(CustomToolC)
1165
+
1166
+ // Removed tools should not exist
1167
+ expect(toolEditor.root.children!['custom-tool-b']).toBeUndefined()
1168
+ expect(toolEditor.root.children!['custom-tool-c']).toBeUndefined()
1169
+
1170
+ // Other tools should still exist
1171
+ expect(toolEditor.root.children!['custom-tool-a']).toBeDefined()
1172
+ expect(toolEditor.root.children!['parent-tool']).toBeDefined()
1173
+ })
1174
+
1175
+ it('should allow re-adding a tool after removing it', () => {
1176
+ // Remove CustomToolB
1177
+ toolEditor.removeTool(CustomToolB)
1178
+ expect(toolEditor.root.children!['custom-tool-b']).toBeUndefined()
1179
+
1180
+ // Re-add CustomToolB
1181
+ toolEditor.setTool(CustomToolB)
1182
+
1183
+ // CustomToolB should exist again
1184
+ expect(toolEditor.root.children!['custom-tool-b']).toBeDefined()
1185
+ expect(toolEditor.root.children!['custom-tool-b']).toBeInstanceOf(CustomToolB)
1186
+
1187
+ // Should be able to transition to it
1188
+ expect(() => {
1189
+ toolEditor.setCurrentTool('custom-tool-b')
1190
+ }).not.toThrow()
1191
+ expect(toolEditor.getCurrentToolId()).toBe('custom-tool-b')
1192
+ })
1193
+ })
@@ -837,6 +837,39 @@ export class Editor extends EventEmitter<TLEventMap> {
837
837
  */
838
838
  readonly root: StateNode
839
839
 
840
+ /**
841
+ * Set a tool. Useful if you need to add a tool to the state chart on demand,
842
+ * after the editor has already been initialized.
843
+ *
844
+ * @param Tool - The tool to set.
845
+ * @param parent - The parent state node to set the tool on.
846
+ *
847
+ * @public
848
+ */
849
+ setTool(Tool: TLStateNodeConstructor, parent?: StateNode) {
850
+ parent ??= this.root
851
+ if (hasOwnProperty(parent.children!, Tool.id)) {
852
+ throw Error(`Can't override tool with id "${Tool.id}"`)
853
+ }
854
+ parent.children![Tool.id] = new Tool(this, parent)
855
+ }
856
+
857
+ /**
858
+ * Remove a tool. Useful if you need to remove a tool from the state chart on demand,
859
+ * after the editor has already been initialized.
860
+ *
861
+ * @param Tool - The tool to delete.
862
+ * @param parent - The parent state node to remove the tool from.
863
+ *
864
+ * @public
865
+ */
866
+ removeTool(Tool: TLStateNodeConstructor, parent?: StateNode) {
867
+ parent ??= this.root
868
+ if (hasOwnProperty(parent.children!, Tool.id)) {
869
+ delete parent.children![Tool.id]
870
+ }
871
+ }
872
+
840
873
  /**
841
874
  * A set of functions to call when the app is disposed.
842
875
  *
@@ -180,6 +180,7 @@ export class LicenseManager {
180
180
  : 'unknown'
181
181
  url.searchParams.set('sku', sku)
182
182
  }
183
+ url.searchParams.set('url', window.location.href)
183
184
  if (process.env.NODE_ENV) {
184
185
  url.searchParams.set('environment', process.env.NODE_ENV)
185
186
  }
@@ -48,8 +48,7 @@ const UnlicensedWatermark = memo(function UnlicensedWatermark({
48
48
  const ref = useRef<HTMLDivElement>(null)
49
49
  usePassThroughWheelEvents(ref)
50
50
 
51
- const url =
52
- 'https://tldraw.dev/pricing?utm_source=dotcom&utm_medium=organic&utm_campaign=watermark'
51
+ const url = 'https://tldraw.dev/pricing?utm_source=sdk&utm_medium=organic&utm_campaign=watermark'
53
52
 
54
53
  return (
55
54
  <div
@@ -70,7 +69,9 @@ const UnlicensedWatermark = memo(function UnlicensedWatermark({
70
69
  preventDefault(e)
71
70
  }}
72
71
  title="The tldraw SDK requires a license key to work in production. You can get a free 100-day trial license at tldraw.dev/pricing."
73
- onClick={() => runtime.openWindow(url, '_blank')}
72
+ onClick={() => {
73
+ runtime.openWindow(url, '_blank', true)
74
+ }} // allow referrer
74
75
  >
75
76
  Get a license for production
76
77
  </button>
@@ -96,7 +97,7 @@ const WatermarkInner = memo(function WatermarkInner({
96
97
  usePassThroughWheelEvents(ref)
97
98
 
98
99
  const maskCss = `url('${src}') center 100% / 100% no-repeat`
99
- const url = 'https://tldraw.dev/?utm_source=dotcom&utm_medium=organic&utm_campaign=watermark'
100
+ const url = 'https://tldraw.dev/?utm_source=sdk&utm_medium=organic&utm_campaign=watermark'
100
101
 
101
102
  if (isUnlicensed) {
102
103
  return <UnlicensedWatermark isDebugMode={isDebugMode} isMobile={isMobile} />
@@ -120,7 +121,9 @@ const WatermarkInner = memo(function WatermarkInner({
120
121
  preventDefault(e)
121
122
  }}
122
123
  title="Build infinite canvas applications with the tldraw SDK. Learn more at https://tldraw.dev."
123
- onClick={() => runtime.openWindow(url, '_blank')}
124
+ onClick={() => {
125
+ runtime.openWindow(url, '_blank')
126
+ }}
124
127
  style={{ mask: maskCss, WebkitMask: maskCss }}
125
128
  />
126
129
  </div>
@@ -92,7 +92,8 @@ if (typeof Element !== 'undefined') {
92
92
 
93
93
  // --- IMPLEMENTATION ---
94
94
  // you probably don't need to read this if you're just using the debug values system
95
- function createDebugValue<T>(
95
+ /** @public */
96
+ export function createDebugValue<T>(
96
97
  name: string,
97
98
  {
98
99
  defaults,
@@ -193,7 +194,7 @@ function getDefaultValue<T>(def: DebugFlagDef<T>): T {
193
194
  }
194
195
  }
195
196
 
196
- /** @internal */
197
+ /** @public */
197
198
  export interface DebugFlagDefaults<T> {
198
199
  development?: T
199
200
  staging?: T
@@ -201,14 +202,14 @@ export interface DebugFlagDefaults<T> {
201
202
  all: T
202
203
  }
203
204
 
204
- /** @internal */
205
+ /** @public */
205
206
  export interface DebugFlagDef<T> {
206
207
  name: string
207
208
  defaults: DebugFlagDefaults<T>
208
209
  shouldStoreForSession: boolean
209
210
  }
210
211
 
211
- /** @internal */
212
+ /** @public */
212
213
  export interface DebugFlag<T> extends DebugFlagDef<T>, Atom<T> {
213
214
  reset(): void
214
215
  }
@@ -1,11 +1,11 @@
1
1
  /** @public */
2
2
  export const runtime: {
3
- openWindow(url: string, target: string): void
3
+ openWindow(url: string, target: string, allowReferrer?: boolean): void
4
4
  refreshPage(): void
5
5
  hardReset(): void
6
6
  } = {
7
- openWindow(url, target) {
8
- window.open(url, target, 'noopener noreferrer')
7
+ openWindow(url, target, allowReferrer = false) {
8
+ return window.open(url, target, allowReferrer ? 'noopener' : 'noopener noreferrer')
9
9
  },
10
10
  refreshPage() {
11
11
  window.location.reload()
@@ -1,6 +1,16 @@
1
1
  import { runtime } from './runtime'
2
2
 
3
- /** @public */
4
- export function openWindow(url: string, target = '_blank') {
5
- runtime.openWindow(url, target)
3
+ /**
4
+ * Open a new window with the given URL and target. Prefer this to the window.open function, as it
5
+ * will work more reliably in embedded scenarios, such as our VS Code extension. See the runtime
6
+ * object in tldraw/editor for more details.
7
+ *
8
+ * @param url - The URL to open.
9
+ * @param target - The target window to open the URL in.
10
+ * @param allowReferrer - Whether to allow the referrer to be sent to the new window.
11
+ * @returns The new window object.
12
+ * @public
13
+ */
14
+ export function openWindow(url: string, target = '_blank', allowReferrer?: boolean) {
15
+ return runtime.openWindow(url, target, allowReferrer)
6
16
  }
package/src/version.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  // This file is automatically generated by internal/scripts/refresh-assets.ts.
2
2
  // Do not edit manually. Or do, I'm a comment, not a cop.
3
3
 
4
- export const version = '4.2.0-next.47462e908ff5'
4
+ export const version = '4.2.0-next.67908ea044c6'
5
5
  export const publishDates = {
6
6
  major: '2025-09-18T14:39:22.803Z',
7
- minor: '2025-10-17T10:30:50.909Z',
8
- patch: '2025-10-17T10:30:50.909Z',
7
+ minor: '2025-11-17T12:37:50.945Z',
8
+ patch: '2025-11-17T12:37:50.945Z',
9
9
  }