@tldraw/editor 4.2.0-next.bff7e3992d58 → 4.2.0-next.d1adb18fb8da

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.
@@ -1,8 +1,8 @@
1
- const version = "4.2.0-next.bff7e3992d58";
1
+ const version = "4.2.0-next.d1adb18fb8da";
2
2
  const publishDates = {
3
3
  major: "2025-09-18T14:39:22.803Z",
4
- minor: "2025-11-12T17:36:59.531Z",
5
- patch: "2025-11-12T17:36:59.531Z"
4
+ minor: "2025-11-19T11:44:04.877Z",
5
+ patch: "2025-11-19T11:44:04.877Z"
6
6
  };
7
7
  export {
8
8
  publishDates,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/version.ts"],
4
- "sourcesContent": ["// This file is automatically generated by internal/scripts/refresh-assets.ts.\n// Do not edit manually. Or do, I'm a comment, not a cop.\n\nexport const version = '4.2.0-next.bff7e3992d58'\nexport const publishDates = {\n\tmajor: '2025-09-18T14:39:22.803Z',\n\tminor: '2025-11-12T17:36:59.531Z',\n\tpatch: '2025-11-12T17:36:59.531Z',\n}\n"],
4
+ "sourcesContent": ["// This file is automatically generated by internal/scripts/refresh-assets.ts.\n// Do not edit manually. Or do, I'm a comment, not a cop.\n\nexport const version = '4.2.0-next.d1adb18fb8da'\nexport const publishDates = {\n\tmajor: '2025-09-18T14:39:22.803Z',\n\tminor: '2025-11-19T11:44:04.877Z',\n\tpatch: '2025-11-19T11:44:04.877Z',\n}\n"],
5
5
  "mappings": "AAGO,MAAM,UAAU;AAChB,MAAM,eAAe;AAAA,EAC3B,OAAO;AAAA,EACP,OAAO;AAAA,EACP,OAAO;AACR;",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tldraw/editor",
3
3
  "description": "tldraw infinite canvas SDK (editor).",
4
- "version": "4.2.0-next.bff7e3992d58",
4
+ "version": "4.2.0-next.d1adb18fb8da",
5
5
  "author": {
6
6
  "name": "tldraw Inc.",
7
7
  "email": "hello@tldraw.com"
@@ -50,12 +50,12 @@
50
50
  "@tiptap/core": "3.6.2",
51
51
  "@tiptap/pm": "3.6.2",
52
52
  "@tiptap/react": "3.6.2",
53
- "@tldraw/state": "4.2.0-next.bff7e3992d58",
54
- "@tldraw/state-react": "4.2.0-next.bff7e3992d58",
55
- "@tldraw/store": "4.2.0-next.bff7e3992d58",
56
- "@tldraw/tlschema": "4.2.0-next.bff7e3992d58",
57
- "@tldraw/utils": "4.2.0-next.bff7e3992d58",
58
- "@tldraw/validate": "4.2.0-next.bff7e3992d58",
53
+ "@tldraw/state": "4.2.0-next.d1adb18fb8da",
54
+ "@tldraw/state-react": "4.2.0-next.d1adb18fb8da",
55
+ "@tldraw/store": "4.2.0-next.d1adb18fb8da",
56
+ "@tldraw/tlschema": "4.2.0-next.d1adb18fb8da",
57
+ "@tldraw/utils": "4.2.0-next.d1adb18fb8da",
58
+ "@tldraw/validate": "4.2.0-next.d1adb18fb8da",
59
59
  "@types/core-js": "^2.5.8",
60
60
  "@use-gesture/react": "^10.3.1",
61
61
  "classnames": "^2.5.1",
@@ -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
+ })
@@ -842,14 +842,16 @@ export class Editor extends EventEmitter<TLEventMap> {
842
842
  * after the editor has already been initialized.
843
843
  *
844
844
  * @param Tool - The tool to set.
845
+ * @param parent - The parent state node to set the tool on.
845
846
  *
846
847
  * @public
847
848
  */
848
- setTool(Tool: TLStateNodeConstructor) {
849
- if (hasOwnProperty(this.root.children!, Tool.id)) {
849
+ setTool(Tool: TLStateNodeConstructor, parent?: StateNode) {
850
+ parent ??= this.root
851
+ if (hasOwnProperty(parent.children!, Tool.id)) {
850
852
  throw Error(`Can't override tool with id "${Tool.id}"`)
851
853
  }
852
- this.root.children![Tool.id] = new Tool(this, this.root)
854
+ parent.children![Tool.id] = new Tool(this, parent)
853
855
  }
854
856
 
855
857
  /**
@@ -857,12 +859,14 @@ export class Editor extends EventEmitter<TLEventMap> {
857
859
  * after the editor has already been initialized.
858
860
  *
859
861
  * @param Tool - The tool to delete.
862
+ * @param parent - The parent state node to remove the tool from.
860
863
  *
861
864
  * @public
862
865
  */
863
- removeTool(Tool: TLStateNodeConstructor) {
864
- if (hasOwnProperty(this.root.children!, Tool.id)) {
865
- delete this.root.children![Tool.id]
866
+ removeTool(Tool: TLStateNodeConstructor, parent?: StateNode) {
867
+ parent ??= this.root
868
+ if (hasOwnProperty(parent.children!, Tool.id)) {
869
+ delete parent.children![Tool.id]
866
870
  }
867
871
  }
868
872
 
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.bff7e3992d58'
4
+ export const version = '4.2.0-next.d1adb18fb8da'
5
5
  export const publishDates = {
6
6
  major: '2025-09-18T14:39:22.803Z',
7
- minor: '2025-11-12T17:36:59.531Z',
8
- patch: '2025-11-12T17:36:59.531Z',
7
+ minor: '2025-11-19T11:44:04.877Z',
8
+ patch: '2025-11-19T11:44:04.877Z',
9
9
  }