@tldraw/editor 3.15.0-next.f1dfcef63951 → 3.15.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 (143) hide show
  1. package/dist-cjs/index.d.ts +58 -44
  2. package/dist-cjs/index.js +18 -16
  3. package/dist-cjs/index.js.map +2 -2
  4. package/dist-cjs/lib/components/SVGContainer.js +1 -1
  5. package/dist-cjs/lib/components/SVGContainer.js.map +2 -2
  6. package/dist-cjs/lib/components/default-components/DefaultBrush.js +1 -1
  7. package/dist-cjs/lib/components/default-components/DefaultBrush.js.map +2 -2
  8. package/dist-cjs/lib/components/default-components/DefaultCanvas.js +1 -1
  9. package/dist-cjs/lib/components/default-components/DefaultCanvas.js.map +2 -2
  10. package/dist-cjs/lib/components/default-components/DefaultCollaboratorHint.js +1 -1
  11. package/dist-cjs/lib/components/default-components/DefaultCollaboratorHint.js.map +2 -2
  12. package/dist-cjs/lib/components/default-components/DefaultCursor.js +1 -1
  13. package/dist-cjs/lib/components/default-components/DefaultCursor.js.map +2 -2
  14. package/dist-cjs/lib/components/default-components/DefaultGrid.js +1 -1
  15. package/dist-cjs/lib/components/default-components/DefaultGrid.js.map +2 -2
  16. package/dist-cjs/lib/components/default-components/DefaultHandles.js +1 -1
  17. package/dist-cjs/lib/components/default-components/DefaultHandles.js.map +2 -2
  18. package/dist-cjs/lib/components/default-components/DefaultShapeIndicator.js +1 -1
  19. package/dist-cjs/lib/components/default-components/DefaultShapeIndicator.js.map +2 -2
  20. package/dist-cjs/lib/components/default-components/DefaultSnapIndictor.js +1 -1
  21. package/dist-cjs/lib/components/default-components/DefaultSnapIndictor.js.map +2 -2
  22. package/dist-cjs/lib/components/default-components/DefaultSpinner.js +27 -15
  23. package/dist-cjs/lib/components/default-components/DefaultSpinner.js.map +3 -3
  24. package/dist-cjs/lib/config/TLUserPreferences.js +7 -1
  25. package/dist-cjs/lib/config/TLUserPreferences.js.map +2 -2
  26. package/dist-cjs/lib/editor/Editor.js +24 -8
  27. package/dist-cjs/lib/editor/Editor.js.map +2 -2
  28. package/dist-cjs/lib/editor/managers/TextManager/TextManager.js +96 -101
  29. package/dist-cjs/lib/editor/managers/TextManager/TextManager.js.map +2 -2
  30. package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js +7 -2
  31. package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js.map +2 -2
  32. package/dist-cjs/lib/editor/tools/StateNode.js +20 -1
  33. package/dist-cjs/lib/editor/tools/StateNode.js.map +2 -2
  34. package/dist-cjs/lib/hooks/useEditorComponents.js.map +1 -1
  35. package/dist-cjs/lib/license/Watermark.js +2 -2
  36. package/dist-cjs/lib/license/Watermark.js.map +2 -2
  37. package/dist-cjs/lib/primitives/geometry/Arc2d.js +1 -1
  38. package/dist-cjs/lib/primitives/geometry/Arc2d.js.map +2 -2
  39. package/dist-cjs/lib/primitives/geometry/Circle2d.js +1 -1
  40. package/dist-cjs/lib/primitives/geometry/Circle2d.js.map +2 -2
  41. package/dist-cjs/lib/primitives/geometry/CubicBezier2d.js +3 -1
  42. package/dist-cjs/lib/primitives/geometry/CubicBezier2d.js.map +2 -2
  43. package/dist-cjs/lib/primitives/geometry/Ellipse2d.js +1 -1
  44. package/dist-cjs/lib/primitives/geometry/Ellipse2d.js.map +2 -2
  45. package/dist-cjs/lib/primitives/geometry/geometry-constants.js +2 -2
  46. package/dist-cjs/lib/primitives/geometry/geometry-constants.js.map +2 -2
  47. package/dist-cjs/lib/primitives/intersect.js +4 -4
  48. package/dist-cjs/lib/primitives/intersect.js.map +2 -2
  49. package/dist-cjs/lib/primitives/utils.js +4 -0
  50. package/dist-cjs/lib/primitives/utils.js.map +2 -2
  51. package/dist-cjs/lib/utils/sync/TLLocalSyncClient.js +0 -1
  52. package/dist-cjs/lib/utils/sync/TLLocalSyncClient.js.map +2 -2
  53. package/dist-cjs/version.js +3 -3
  54. package/dist-cjs/version.js.map +1 -1
  55. package/dist-esm/index.d.mts +58 -44
  56. package/dist-esm/index.mjs +43 -41
  57. package/dist-esm/index.mjs.map +2 -2
  58. package/dist-esm/lib/components/SVGContainer.mjs +1 -1
  59. package/dist-esm/lib/components/SVGContainer.mjs.map +2 -2
  60. package/dist-esm/lib/components/default-components/DefaultBrush.mjs +1 -1
  61. package/dist-esm/lib/components/default-components/DefaultBrush.mjs.map +2 -2
  62. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs +1 -1
  63. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs.map +2 -2
  64. package/dist-esm/lib/components/default-components/DefaultCollaboratorHint.mjs +1 -1
  65. package/dist-esm/lib/components/default-components/DefaultCollaboratorHint.mjs.map +2 -2
  66. package/dist-esm/lib/components/default-components/DefaultCursor.mjs +1 -1
  67. package/dist-esm/lib/components/default-components/DefaultCursor.mjs.map +2 -2
  68. package/dist-esm/lib/components/default-components/DefaultGrid.mjs +1 -1
  69. package/dist-esm/lib/components/default-components/DefaultGrid.mjs.map +2 -2
  70. package/dist-esm/lib/components/default-components/DefaultHandles.mjs +1 -1
  71. package/dist-esm/lib/components/default-components/DefaultHandles.mjs.map +2 -2
  72. package/dist-esm/lib/components/default-components/DefaultShapeIndicator.mjs +1 -1
  73. package/dist-esm/lib/components/default-components/DefaultShapeIndicator.mjs.map +2 -2
  74. package/dist-esm/lib/components/default-components/DefaultSnapIndictor.mjs +1 -1
  75. package/dist-esm/lib/components/default-components/DefaultSnapIndictor.mjs.map +2 -2
  76. package/dist-esm/lib/components/default-components/DefaultSpinner.mjs +17 -15
  77. package/dist-esm/lib/components/default-components/DefaultSpinner.mjs.map +2 -2
  78. package/dist-esm/lib/config/TLUserPreferences.mjs +7 -1
  79. package/dist-esm/lib/config/TLUserPreferences.mjs.map +2 -2
  80. package/dist-esm/lib/editor/Editor.mjs +24 -8
  81. package/dist-esm/lib/editor/Editor.mjs.map +2 -2
  82. package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs +96 -101
  83. package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs.map +2 -2
  84. package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs +7 -2
  85. package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs.map +2 -2
  86. package/dist-esm/lib/editor/tools/StateNode.mjs +20 -1
  87. package/dist-esm/lib/editor/tools/StateNode.mjs.map +2 -2
  88. package/dist-esm/lib/hooks/useEditorComponents.mjs.map +1 -1
  89. package/dist-esm/lib/license/Watermark.mjs +2 -2
  90. package/dist-esm/lib/license/Watermark.mjs.map +2 -2
  91. package/dist-esm/lib/primitives/geometry/Arc2d.mjs +2 -2
  92. package/dist-esm/lib/primitives/geometry/Arc2d.mjs.map +2 -2
  93. package/dist-esm/lib/primitives/geometry/Circle2d.mjs +2 -2
  94. package/dist-esm/lib/primitives/geometry/Circle2d.mjs.map +2 -2
  95. package/dist-esm/lib/primitives/geometry/CubicBezier2d.mjs +3 -1
  96. package/dist-esm/lib/primitives/geometry/CubicBezier2d.mjs.map +2 -2
  97. package/dist-esm/lib/primitives/geometry/Ellipse2d.mjs +2 -2
  98. package/dist-esm/lib/primitives/geometry/Ellipse2d.mjs.map +2 -2
  99. package/dist-esm/lib/primitives/geometry/geometry-constants.mjs +2 -2
  100. package/dist-esm/lib/primitives/geometry/geometry-constants.mjs.map +2 -2
  101. package/dist-esm/lib/primitives/intersect.mjs +5 -5
  102. package/dist-esm/lib/primitives/intersect.mjs.map +2 -2
  103. package/dist-esm/lib/primitives/utils.mjs +4 -0
  104. package/dist-esm/lib/primitives/utils.mjs.map +2 -2
  105. package/dist-esm/lib/utils/sync/TLLocalSyncClient.mjs +0 -1
  106. package/dist-esm/lib/utils/sync/TLLocalSyncClient.mjs.map +2 -2
  107. package/dist-esm/version.mjs +3 -3
  108. package/dist-esm/version.mjs.map +1 -1
  109. package/editor.css +17 -4
  110. package/package.json +9 -8
  111. package/src/index.ts +63 -62
  112. package/src/lib/components/SVGContainer.tsx +1 -1
  113. package/src/lib/components/default-components/DefaultBrush.tsx +1 -1
  114. package/src/lib/components/default-components/DefaultCanvas.tsx +1 -1
  115. package/src/lib/components/default-components/DefaultCollaboratorHint.tsx +1 -1
  116. package/src/lib/components/default-components/DefaultCursor.tsx +1 -1
  117. package/src/lib/components/default-components/DefaultGrid.tsx +1 -1
  118. package/src/lib/components/default-components/DefaultHandles.tsx +5 -1
  119. package/src/lib/components/default-components/DefaultShapeIndicator.tsx +1 -1
  120. package/src/lib/components/default-components/DefaultSnapIndictor.tsx +1 -1
  121. package/src/lib/components/default-components/DefaultSpinner.tsx +12 -12
  122. package/src/lib/config/TLUserPreferences.ts +7 -0
  123. package/src/lib/editor/Editor.test.ts +407 -0
  124. package/src/lib/editor/Editor.ts +35 -9
  125. package/src/lib/editor/managers/TextManager/TextManager.ts +108 -128
  126. package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.test.ts +21 -0
  127. package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.ts +8 -0
  128. package/src/lib/editor/tools/StateNode.test.ts +285 -0
  129. package/src/lib/editor/tools/StateNode.ts +27 -1
  130. package/src/lib/hooks/useEditorComponents.tsx +1 -1
  131. package/src/lib/license/LicenseManager.test.ts +1 -1
  132. package/src/lib/license/Watermark.tsx +2 -2
  133. package/src/lib/primitives/geometry/Arc2d.ts +2 -2
  134. package/src/lib/primitives/geometry/Circle2d.ts +2 -2
  135. package/src/lib/primitives/geometry/CubicBezier2d.ts +4 -1
  136. package/src/lib/primitives/geometry/Ellipse2d.ts +2 -2
  137. package/src/lib/primitives/geometry/geometry-constants.ts +2 -1
  138. package/src/lib/primitives/intersect.test.ts +946 -0
  139. package/src/lib/primitives/intersect.ts +12 -5
  140. package/src/lib/primitives/utils.ts +11 -0
  141. package/src/lib/utils/sync/TLLocalSyncClient.ts +0 -1
  142. package/src/version.ts +3 -3
  143. package/src/lib/test/currentToolIdMask.test.ts +0 -49
@@ -1,4 +1,5 @@
1
1
  import { BoxModel, TLDefaultHorizontalAlignStyle } from '@tldraw/tlschema'
2
+ import { objectMapKeys } from '@tldraw/utils'
2
3
  import { Editor } from '../../Editor'
3
4
 
4
5
  const fixNewLines = /\r?\n|\r/g
@@ -60,10 +61,18 @@ export interface TLMeasureTextSpanOpts {
60
61
 
61
62
  const spaceCharacterRegex = /\s/
62
63
 
64
+ const initialDefaultStyles = Object.freeze({
65
+ 'overflow-wrap': 'break-word',
66
+ 'word-break': 'auto',
67
+ width: null,
68
+ height: null,
69
+ 'max-width': null,
70
+ 'min-width': null,
71
+ })
72
+
63
73
  /** @public */
64
74
  export class TextManager {
65
75
  private elm: HTMLDivElement
66
- private defaultStyles: Record<string, string | null>
67
76
 
68
77
  constructor(public editor: Editor) {
69
78
  const elm = document.createElement('div')
@@ -73,31 +82,34 @@ export class TextManager {
73
82
  elm.tabIndex = -1
74
83
  this.editor.getContainer().appendChild(elm)
75
84
 
76
- // we need to save the default styles so that we can restore them when we're done
77
- // these must be the css names, not the js names for the styles
78
- this.defaultStyles = {
79
- 'overflow-wrap': 'break-word',
80
- 'word-break': 'auto',
81
- width: null,
82
- height: null,
83
- 'max-width': null,
84
- 'min-width': null,
85
+ this.elm = elm
86
+
87
+ for (const key of objectMapKeys(initialDefaultStyles)) {
88
+ elm.style.setProperty(key, initialDefaultStyles[key])
85
89
  }
90
+ }
86
91
 
87
- this.elm = elm
92
+ private setElementStyles(styles: Record<string, string | undefined>) {
93
+ const stylesToReinstate = {} as any
94
+ for (const key of objectMapKeys(styles)) {
95
+ if (typeof styles[key] === 'string') {
96
+ const oldValue = this.elm.style.getPropertyValue(key)
97
+ if (oldValue === styles[key]) continue
98
+ stylesToReinstate[key] = oldValue
99
+ this.elm.style.setProperty(key, styles[key])
100
+ }
101
+ }
102
+ return () => {
103
+ for (const key of objectMapKeys(stylesToReinstate)) {
104
+ this.elm.style.setProperty(key, stylesToReinstate[key])
105
+ }
106
+ }
88
107
  }
89
108
 
90
109
  dispose() {
91
110
  return this.elm.remove()
92
111
  }
93
112
 
94
- private resetElmStyles() {
95
- const { elm, defaultStyles } = this
96
- for (const key in defaultStyles) {
97
- elm.style.setProperty(key, defaultStyles[key])
98
- }
99
- }
100
-
101
113
  measureText(textToMeasure: string, opts: TLMeasureTextOpts): BoxModel & { scrollWidth: number } {
102
114
  const div = document.createElement('div')
103
115
  div.textContent = normalizeTextForDom(textToMeasure)
@@ -107,54 +119,36 @@ export class TextManager {
107
119
  measureHtml(html: string, opts: TLMeasureTextOpts): BoxModel & { scrollWidth: number } {
108
120
  const { elm } = this
109
121
 
110
- if (opts.otherStyles) {
111
- for (const key in opts.otherStyles) {
112
- if (!this.defaultStyles[key]) {
113
- // we need to save the original style so that we can restore it when we're done
114
- this.defaultStyles[key] = elm.style.getPropertyValue(key)
115
- }
116
- }
122
+ const newStyles = {
123
+ 'font-family': opts.fontFamily,
124
+ 'font-style': opts.fontStyle,
125
+ 'font-weight': opts.fontWeight,
126
+ 'font-size': opts.fontSize + 'px',
127
+ 'line-height': opts.lineHeight.toString(),
128
+ padding: opts.padding,
129
+ 'max-width': opts.maxWidth ? opts.maxWidth + 'px' : undefined,
130
+ 'min-width': opts.minWidth ? opts.minWidth + 'px' : undefined,
131
+ 'overflow-wrap': opts.disableOverflowWrapBreaking ? 'normal' : undefined,
132
+ ...opts.otherStyles,
117
133
  }
118
134
 
119
- elm.innerHTML = html
135
+ const restoreStyles = this.setElementStyles(newStyles)
120
136
 
121
- // Apply the default styles to the element (for all styles here or that were ever seen in opts.otherStyles)
122
- this.resetElmStyles()
137
+ try {
138
+ elm.innerHTML = html
123
139
 
124
- elm.style.setProperty('font-family', opts.fontFamily)
125
- elm.style.setProperty('font-style', opts.fontStyle)
126
- elm.style.setProperty('font-weight', opts.fontWeight)
127
- elm.style.setProperty('font-size', opts.fontSize + 'px')
128
- elm.style.setProperty('line-height', opts.lineHeight.toString())
129
- elm.style.setProperty('padding', opts.padding)
140
+ const scrollWidth = opts.measureScrollWidth ? elm.scrollWidth : 0
141
+ const rect = elm.getBoundingClientRect()
130
142
 
131
- if (opts.maxWidth) {
132
- elm.style.setProperty('max-width', opts.maxWidth + 'px')
133
- }
134
-
135
- if (opts.minWidth) {
136
- elm.style.setProperty('min-width', opts.minWidth + 'px')
137
- }
138
-
139
- if (opts.disableOverflowWrapBreaking) {
140
- elm.style.setProperty('overflow-wrap', 'normal')
141
- }
142
-
143
- if (opts.otherStyles) {
144
- for (const [key, value] of Object.entries(opts.otherStyles)) {
145
- elm.style.setProperty(key, value)
143
+ return {
144
+ x: 0,
145
+ y: 0,
146
+ w: rect.width,
147
+ h: rect.height,
148
+ scrollWidth,
146
149
  }
147
- }
148
-
149
- const scrollWidth = opts.measureScrollWidth ? elm.scrollWidth : 0
150
- const rect = elm.getBoundingClientRect()
151
-
152
- return {
153
- x: 0,
154
- y: 0,
155
- w: rect.width,
156
- h: rect.height,
157
- scrollWidth,
150
+ } finally {
151
+ restoreStyles()
158
152
  }
159
153
  }
160
154
 
@@ -274,82 +268,68 @@ export class TextManager {
274
268
 
275
269
  const { elm } = this
276
270
 
277
- if (opts.otherStyles) {
278
- for (const key in opts.otherStyles) {
279
- if (!this.defaultStyles[key]) {
280
- // we need to save the original style so that we can restore it when we're done
281
- this.defaultStyles[key] = elm.style.getPropertyValue(key)
282
- }
283
- }
284
- }
285
-
286
- this.resetElmStyles()
287
-
288
- elm.style.setProperty('font-family', opts.fontFamily)
289
- elm.style.setProperty('font-style', opts.fontStyle)
290
- elm.style.setProperty('font-weight', opts.fontWeight)
291
- elm.style.setProperty('font-size', opts.fontSize + 'px')
292
- elm.style.setProperty('line-height', opts.lineHeight.toString())
293
-
294
- const elementWidth = Math.ceil(opts.width - opts.padding * 2)
295
- elm.style.setProperty('width', `${elementWidth}px`)
296
- elm.style.setProperty('height', 'min-content')
297
- elm.style.setProperty('text-align', textAlignmentsForLtr[opts.textAlign])
298
-
299
271
  const shouldTruncateToFirstLine =
300
272
  opts.overflow === 'truncate-ellipsis' || opts.overflow === 'truncate-clip'
301
-
302
- if (shouldTruncateToFirstLine) {
303
- elm.style.setProperty('overflow-wrap', 'anywhere')
304
- elm.style.setProperty('word-break', 'break-all')
305
- }
306
-
307
- if (opts.otherStyles) {
308
- for (const [key, value] of Object.entries(opts.otherStyles)) {
309
- elm.style.setProperty(key, value)
310
- }
273
+ const elementWidth = Math.ceil(opts.width - opts.padding * 2)
274
+ const newStyles = {
275
+ 'font-family': opts.fontFamily,
276
+ 'font-style': opts.fontStyle,
277
+ 'font-weight': opts.fontWeight,
278
+ 'font-size': opts.fontSize + 'px',
279
+ 'line-height': opts.lineHeight.toString(),
280
+ width: `${elementWidth}px`,
281
+ height: 'min-content',
282
+ 'text-align': textAlignmentsForLtr[opts.textAlign],
283
+ 'overflow-wrap': shouldTruncateToFirstLine ? 'anywhere' : undefined,
284
+ 'word-break': shouldTruncateToFirstLine ? 'break-all' : undefined,
285
+ ...opts.otherStyles,
311
286
  }
287
+ const restoreStyles = this.setElementStyles(newStyles)
312
288
 
313
- const normalizedText = normalizeTextForDom(textToMeasure)
314
-
315
- // Render the text into the measurement element:
316
- elm.textContent = normalizedText
317
-
318
- // actually measure the text:
319
- const { spans, didTruncate } = this.measureElementTextNodeSpans(elm, {
320
- shouldTruncateToFirstLine,
321
- })
289
+ try {
290
+ const normalizedText = normalizeTextForDom(textToMeasure)
322
291
 
323
- if (opts.overflow === 'truncate-ellipsis' && didTruncate) {
324
- // we need to measure the ellipsis to know how much space it takes up
325
- elm.textContent = '…'
326
- const ellipsisWidth = Math.ceil(this.measureElementTextNodeSpans(elm).spans[0].box.w)
327
-
328
- // then, we need to subtract that space from the width we have and measure again:
329
- elm.style.setProperty('width', `${elementWidth - ellipsisWidth}px`)
292
+ // Render the text into the measurement element:
330
293
  elm.textContent = normalizedText
331
- const truncatedSpans = this.measureElementTextNodeSpans(elm, {
332
- shouldTruncateToFirstLine: true,
333
- }).spans
334
-
335
- // Finally, we add in our ellipsis at the end of the last span. We
336
- // have to do this after measuring, not before, because adding the
337
- // ellipsis changes how whitespace might be getting collapsed by the
338
- // browser.
339
- const lastSpan = truncatedSpans[truncatedSpans.length - 1]!
340
- truncatedSpans.push({
341
- text: '…',
342
- box: {
343
- x: Math.min(lastSpan.box.x + lastSpan.box.w, opts.width - opts.padding - ellipsisWidth),
344
- y: lastSpan.box.y,
345
- w: ellipsisWidth,
346
- h: lastSpan.box.h,
347
- },
294
+
295
+ // actually measure the text:
296
+ const { spans, didTruncate } = this.measureElementTextNodeSpans(elm, {
297
+ shouldTruncateToFirstLine,
348
298
  })
349
299
 
350
- return truncatedSpans
351
- }
300
+ if (opts.overflow === 'truncate-ellipsis' && didTruncate) {
301
+ // we need to measure the ellipsis to know how much space it takes up
302
+ elm.textContent = '…'
303
+ const ellipsisWidth = Math.ceil(this.measureElementTextNodeSpans(elm).spans[0].box.w)
304
+
305
+ // then, we need to subtract that space from the width we have and measure again:
306
+ elm.style.setProperty('width', `${elementWidth - ellipsisWidth}px`)
307
+ elm.textContent = normalizedText
308
+ const truncatedSpans = this.measureElementTextNodeSpans(elm, {
309
+ shouldTruncateToFirstLine: true,
310
+ }).spans
311
+
312
+ // Finally, we add in our ellipsis at the end of the last span. We
313
+ // have to do this after measuring, not before, because adding the
314
+ // ellipsis changes how whitespace might be getting collapsed by the
315
+ // browser.
316
+ const lastSpan = truncatedSpans[truncatedSpans.length - 1]!
317
+ truncatedSpans.push({
318
+ text: '…',
319
+ box: {
320
+ x: Math.min(lastSpan.box.x + lastSpan.box.w, opts.width - opts.padding - ellipsisWidth),
321
+ y: lastSpan.box.y,
322
+ w: ellipsisWidth,
323
+ h: lastSpan.box.h,
324
+ },
325
+ })
326
+
327
+ return truncatedSpans
328
+ }
352
329
 
353
- return spans
330
+ return spans
331
+ } finally {
332
+ restoreStyles()
333
+ }
354
334
  }
355
335
  }
@@ -24,6 +24,7 @@ describe('UserPreferencesManager', () => {
24
24
  color: '#FF802B',
25
25
  locale: 'en',
26
26
  animationSpeed: 1,
27
+ areKeyboardShortcutsEnabled: true,
27
28
  edgeScrollSpeed: 1,
28
29
  colorScheme: 'light',
29
30
  isSnapMode: false,
@@ -229,6 +230,7 @@ describe('UserPreferencesManager', () => {
229
230
  locale: mockUserPreferences.locale,
230
231
  color: mockUserPreferences.color,
231
232
  animationSpeed: mockUserPreferences.animationSpeed,
233
+ areKeyboardShortcutsEnabled: mockUserPreferences.areKeyboardShortcutsEnabled,
232
234
  isSnapMode: mockUserPreferences.isSnapMode,
233
235
  colorScheme: mockUserPreferences.colorScheme,
234
236
  isDarkMode: false, // light mode
@@ -362,6 +364,21 @@ describe('UserPreferencesManager', () => {
362
364
  })
363
365
  })
364
366
 
367
+ describe('getAreKeyboardShortcutsEnabled', () => {
368
+ it('should return user keyboard shortcuts', () => {
369
+ expect(userPreferencesManager.getAreKeyboardShortcutsEnabled()).toBe(
370
+ mockUserPreferences.areKeyboardShortcutsEnabled
371
+ )
372
+ })
373
+
374
+ it('should return default keyboard shortcuts when null', () => {
375
+ userPreferencesAtom.set({ ...mockUserPreferences, areKeyboardShortcutsEnabled: null })
376
+ expect(userPreferencesManager.getAreKeyboardShortcutsEnabled()).toBe(
377
+ defaultUserPreferences.areKeyboardShortcutsEnabled
378
+ )
379
+ })
380
+ })
381
+
365
382
  describe('getEdgeScrollSpeed', () => {
366
383
  it('should return user edge scroll speed', () => {
367
384
  expect(userPreferencesManager.getEdgeScrollSpeed()).toBe(
@@ -483,6 +500,7 @@ describe('UserPreferencesManager', () => {
483
500
  color: null,
484
501
  locale: null,
485
502
  animationSpeed: null,
503
+ areKeyboardShortcutsEnabled: null,
486
504
  edgeScrollSpeed: null,
487
505
  isSnapMode: null,
488
506
  isWrapMode: null,
@@ -496,6 +514,9 @@ describe('UserPreferencesManager', () => {
496
514
  expect(userPreferencesManager.getColor()).toBe(defaultUserPreferences.color)
497
515
  expect(userPreferencesManager.getLocale()).toBe(defaultUserPreferences.locale)
498
516
  expect(userPreferencesManager.getAnimationSpeed()).toBe(defaultUserPreferences.animationSpeed)
517
+ expect(userPreferencesManager.getAreKeyboardShortcutsEnabled()).toBe(
518
+ defaultUserPreferences.areKeyboardShortcutsEnabled
519
+ )
499
520
  expect(userPreferencesManager.getEdgeScrollSpeed()).toBe(
500
521
  defaultUserPreferences.edgeScrollSpeed
501
522
  )
@@ -43,6 +43,7 @@ export class UserPreferencesManager {
43
43
  locale: this.getLocale(),
44
44
  color: this.getColor(),
45
45
  animationSpeed: this.getAnimationSpeed(),
46
+ areKeyboardShortcutsEnabled: this.getAreKeyboardShortcutsEnabled(),
46
47
  isSnapMode: this.getIsSnapMode(),
47
48
  colorScheme: this.user.userPreferences.get().colorScheme,
48
49
  isDarkMode: this.getIsDarkMode(),
@@ -75,6 +76,13 @@ export class UserPreferencesManager {
75
76
  return this.user.userPreferences.get().animationSpeed ?? defaultUserPreferences.animationSpeed
76
77
  }
77
78
 
79
+ @computed getAreKeyboardShortcutsEnabled() {
80
+ return (
81
+ this.user.userPreferences.get().areKeyboardShortcutsEnabled ??
82
+ defaultUserPreferences.areKeyboardShortcutsEnabled
83
+ )
84
+ }
85
+
78
86
  @computed getId() {
79
87
  return this.user.userPreferences.get().id
80
88
  }
@@ -0,0 +1,285 @@
1
+ import { createTLStore } from '../../config/createTLStore'
2
+ import { Editor } from '../Editor'
3
+ import { StateNode } from './StateNode'
4
+
5
+ describe('StateNode.addChild', () => {
6
+ // Test state node classes for addChild tests
7
+ class ParentState extends StateNode {
8
+ static override id = 'parent'
9
+ static override initial = 'child1'
10
+ static override children() {
11
+ return [ChildState1]
12
+ }
13
+ }
14
+
15
+ class ChildState1 extends StateNode {
16
+ static override id = 'child1'
17
+ }
18
+
19
+ class ChildState2 extends StateNode {
20
+ static override id = 'child2'
21
+ }
22
+
23
+ class ChildState3 extends StateNode {
24
+ static override id = 'child3'
25
+ }
26
+
27
+ class LeafState extends StateNode {
28
+ static override id = 'leaf'
29
+ }
30
+
31
+ class RootState extends StateNode {
32
+ static override id = 'root'
33
+ static override initial = 'child1'
34
+ static override children() {
35
+ return [ChildState1]
36
+ }
37
+ }
38
+
39
+ class RootStateWithoutChildren extends StateNode {
40
+ static override id = 'rootWithoutChildren'
41
+ }
42
+
43
+ let editor: Editor
44
+
45
+ beforeEach(() => {
46
+ editor = new Editor({
47
+ initialState: 'parent',
48
+ shapeUtils: [],
49
+ bindingUtils: [],
50
+ tools: [
51
+ ParentState,
52
+ ChildState1,
53
+ ChildState2,
54
+ ChildState3,
55
+ LeafState,
56
+ RootState,
57
+ RootStateWithoutChildren,
58
+ ],
59
+ store: createTLStore({ shapeUtils: [], bindingUtils: [] }),
60
+ getContainer: () => document.body,
61
+ })
62
+ })
63
+
64
+ it('should add a child to a branch state node', () => {
65
+ const parentState = editor.root.children!['parent'] as ParentState
66
+
67
+ // Initially should have one child
68
+ expect(Object.keys(parentState.children!)).toHaveLength(1)
69
+ expect(parentState.children!['child1']).toBeDefined()
70
+
71
+ // Add a new child
72
+ parentState.addChild(ChildState2)
73
+
74
+ // Should now have two children
75
+ expect(Object.keys(parentState.children!)).toHaveLength(2)
76
+ expect(parentState.children!['child1']).toBeDefined()
77
+ expect(parentState.children!['child2']).toBeDefined()
78
+ expect(parentState.children!['child2']).toBeInstanceOf(ChildState2)
79
+ })
80
+
81
+ it('should add a child to a root state node', () => {
82
+ const rootState = editor.root.children!['root'] as RootState
83
+
84
+ // Initially should have one child
85
+ expect(Object.keys(rootState.children!)).toHaveLength(1)
86
+ expect(rootState.children!['child1']).toBeDefined()
87
+
88
+ // Add a new child
89
+ rootState.addChild(ChildState2)
90
+
91
+ // Should now have two children
92
+ expect(Object.keys(rootState.children!)).toHaveLength(2)
93
+ expect(rootState.children!['child1']).toBeDefined()
94
+ expect(rootState.children!['child2']).toBeDefined()
95
+ expect(rootState.children!['child2']).toBeInstanceOf(ChildState2)
96
+ })
97
+
98
+ it('should throw an error when trying to add a child to a leaf state node', () => {
99
+ const leafState = editor.root.children!['leaf'] as LeafState
100
+
101
+ // Leaf state should not have children
102
+ expect(leafState.children).toBeUndefined()
103
+
104
+ // Should throw an error when trying to add a child
105
+ expect(() => {
106
+ leafState.addChild(ChildState2)
107
+ }).toThrow('StateNode.addChild: cannot add child to a leaf node')
108
+ })
109
+
110
+ it('should return the parent state node for chaining', () => {
111
+ const parentState = editor.root.children!['parent'] as ParentState
112
+
113
+ const result = parentState.addChild(ChildState2)
114
+
115
+ expect(result).toBe(parentState)
116
+ })
117
+
118
+ it('should create the child with the correct editor and parent', () => {
119
+ const parentState = editor.root.children!['parent'] as ParentState
120
+
121
+ parentState.addChild(ChildState2)
122
+ const childState = parentState.children!['child2'] as ChildState2
123
+
124
+ expect(childState.editor).toBe(editor)
125
+ expect(childState.parent).toBe(parentState)
126
+ })
127
+
128
+ it('should allow adding multiple children', () => {
129
+ const parentState = editor.root.children!['parent'] as ParentState
130
+
131
+ // Add multiple children
132
+ parentState.addChild(ChildState2).addChild(ChildState3)
133
+
134
+ // Should have three children
135
+ expect(Object.keys(parentState.children!)).toHaveLength(3)
136
+ expect(parentState.children!['child1']).toBeDefined()
137
+ expect(parentState.children!['child2']).toBeDefined()
138
+ expect(parentState.children!['child3']).toBeDefined()
139
+ expect(parentState.children!['child2']).toBeInstanceOf(ChildState2)
140
+ expect(parentState.children!['child3']).toBeInstanceOf(ChildState3)
141
+ })
142
+
143
+ it('should allow transitioning to added children', () => {
144
+ const parentState = editor.root.children!['parent'] as ParentState
145
+
146
+ // Add a new child
147
+ parentState.addChild(ChildState2)
148
+
149
+ // Should be able to transition to the new child
150
+ expect(() => {
151
+ parentState.transition('child2')
152
+ }).not.toThrow()
153
+
154
+ // The current state should be the new child
155
+ expect(parentState.getCurrent()?.id).toBe('child2')
156
+ })
157
+
158
+ it('should maintain existing children when adding new ones', () => {
159
+ const parentState = editor.root.children!['parent'] as ParentState
160
+ const originalChild = parentState.children!['child1']
161
+
162
+ // Add a new child
163
+ parentState.addChild(ChildState2)
164
+
165
+ // Original child should still exist and be the same instance
166
+ expect(parentState.children!['child1']).toBe(originalChild)
167
+ expect(parentState.children!['child1']).toBeInstanceOf(ChildState1)
168
+ })
169
+
170
+ it('should initialize children object for root nodes without static children', () => {
171
+ // Create a StateNode directly as a root node (no parent)
172
+ const mockEditor = {} as Editor
173
+ const rootStateWithoutChildren = new RootStateWithoutChildren(mockEditor, undefined)
174
+
175
+ // Root state without static children should not have children initially
176
+ expect(rootStateWithoutChildren.children).toBeUndefined()
177
+
178
+ // Adding a child should initialize the children object
179
+ rootStateWithoutChildren.addChild(ChildState2)
180
+
181
+ // Should now have children object with the added child
182
+ expect(rootStateWithoutChildren.children).toBeDefined()
183
+ expect(Object.keys(rootStateWithoutChildren.children!)).toHaveLength(1)
184
+ expect(rootStateWithoutChildren.children!['child2']).toBeDefined()
185
+ expect(rootStateWithoutChildren.children!['child2']).toBeInstanceOf(ChildState2)
186
+ })
187
+
188
+ it('should throw an error when trying to add a child with a duplicate ID', () => {
189
+ const parentState = editor.root.children!['parent'] as ParentState
190
+
191
+ // Initially should have one child
192
+ expect(Object.keys(parentState.children!)).toHaveLength(1)
193
+ expect(parentState.children!['child1']).toBeDefined()
194
+
195
+ // Should throw an error when trying to add a child with the same ID
196
+ expect(() => {
197
+ parentState.addChild(ChildState1)
198
+ }).toThrow("StateNode.addChild: a child with id 'child1' already exists")
199
+
200
+ // Should still have only one child
201
+ expect(Object.keys(parentState.children!)).toHaveLength(1)
202
+ expect(parentState.children!['child1']).toBeDefined()
203
+ })
204
+
205
+ it('should throw an error when trying to add a child with a duplicate ID to a root state', () => {
206
+ const rootState = editor.root.children!['root'] as RootState
207
+
208
+ // Initially should have one child
209
+ expect(Object.keys(rootState.children!)).toHaveLength(1)
210
+ expect(rootState.children!['child1']).toBeDefined()
211
+
212
+ // Should throw an error when trying to add a child with the same ID
213
+ expect(() => {
214
+ rootState.addChild(ChildState1)
215
+ }).toThrow("StateNode.addChild: a child with id 'child1' already exists")
216
+
217
+ // Should still have only one child
218
+ expect(Object.keys(rootState.children!)).toHaveLength(1)
219
+ expect(rootState.children!['child1']).toBeDefined()
220
+ })
221
+
222
+ it('should throw an error when trying to add a child with a duplicate ID to a root state without static children', () => {
223
+ // Create a StateNode directly as a root node (no parent)
224
+ const mockEditor = {} as Editor
225
+ const rootStateWithoutChildren = new RootStateWithoutChildren(mockEditor, undefined)
226
+
227
+ // Add a child first
228
+ rootStateWithoutChildren.addChild(ChildState1)
229
+
230
+ // Should throw an error when trying to add a child with the same ID
231
+ expect(() => {
232
+ rootStateWithoutChildren.addChild(ChildState1)
233
+ }).toThrow("StateNode.addChild: a child with id 'child1' already exists")
234
+
235
+ // Should still have only one child
236
+ expect(Object.keys(rootStateWithoutChildren.children!)).toHaveLength(1)
237
+ expect(rootStateWithoutChildren.children!['child1']).toBeDefined()
238
+ })
239
+ })
240
+
241
+ describe('current tool id mask', () => {
242
+ // Tool mask test classes
243
+ class ToolA extends StateNode {
244
+ static override id = 'A'
245
+ }
246
+
247
+ class ToolB extends StateNode {
248
+ static override id = 'B'
249
+ }
250
+
251
+ class ToolC extends StateNode {
252
+ static override id = 'C'
253
+
254
+ override onEnter() {
255
+ this.setCurrentToolIdMask('A')
256
+ }
257
+ }
258
+
259
+ let toolMaskEditor: Editor
260
+
261
+ beforeEach(() => {
262
+ toolMaskEditor = new Editor({
263
+ initialState: 'A',
264
+ shapeUtils: [],
265
+ bindingUtils: [],
266
+ tools: [ToolA, ToolB, ToolC],
267
+ store: createTLStore({ shapeUtils: [], bindingUtils: [] }),
268
+ getContainer: () => document.body,
269
+ })
270
+ })
271
+
272
+ it('starts with the correct tool id', () => {
273
+ expect(toolMaskEditor.getCurrentToolId()).toBe('A')
274
+ })
275
+
276
+ it('updates the current tool id', () => {
277
+ toolMaskEditor.setCurrentTool('B')
278
+ expect(toolMaskEditor.getCurrentToolId()).toBe('B')
279
+ })
280
+
281
+ it('masks the current tool id', () => {
282
+ toolMaskEditor.setCurrentTool('C')
283
+ expect(toolMaskEditor.getCurrentToolId()).toBe('A')
284
+ })
285
+ })
@@ -62,7 +62,7 @@ export abstract class StateNode implements Partial<TLEventHandlers> {
62
62
 
63
63
  this.parent = parent ?? ({} as any)
64
64
 
65
- if (this.parent) {
65
+ if (parent) {
66
66
  if (children && initial) {
67
67
  this.type = 'branch'
68
68
  this.initial = initial
@@ -238,6 +238,32 @@ export abstract class StateNode implements Partial<TLEventHandlers> {
238
238
  this._currentToolIdMask.set(id)
239
239
  }
240
240
 
241
+ /**
242
+ * Add a child node to this state node.
243
+ *
244
+ * @public
245
+ */
246
+ addChild(childConstructor: TLStateNodeConstructor): this {
247
+ if (this.type === 'leaf') {
248
+ throw new Error('StateNode.addChild: cannot add child to a leaf node')
249
+ }
250
+
251
+ // Initialize children if it's undefined (for root nodes without static children)
252
+ if (!this.children) {
253
+ this.children = {}
254
+ }
255
+
256
+ const child = new childConstructor(this.editor, this)
257
+
258
+ // Check if a child with this ID already exists
259
+ if (this.children[child.id]) {
260
+ throw new Error(`StateNode.addChild: a child with id '${child.id}' already exists`)
261
+ }
262
+
263
+ this.children[child.id] = child
264
+ return this
265
+ }
266
+
241
267
  onWheel?(info: TLWheelEventInfo): void
242
268
  onPointerDown?(info: TLPointerEventInfo): void
243
269
  onPointerMove?(info: TLPointerEventInfo): void