@tldraw/editor 3.15.0-canary.bc8278557679 → 3.15.0-canary.c59a4cbb7d60
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.
- package/dist-cjs/index.d.ts +54 -44
- package/dist-cjs/index.js +18 -16
- package/dist-cjs/index.js.map +2 -2
- package/dist-cjs/lib/components/SVGContainer.js +1 -1
- package/dist-cjs/lib/components/SVGContainer.js.map +2 -2
- package/dist-cjs/lib/components/default-components/DefaultBrush.js +1 -1
- package/dist-cjs/lib/components/default-components/DefaultBrush.js.map +2 -2
- package/dist-cjs/lib/components/default-components/DefaultCanvas.js +1 -1
- package/dist-cjs/lib/components/default-components/DefaultCanvas.js.map +2 -2
- package/dist-cjs/lib/components/default-components/DefaultCollaboratorHint.js +1 -1
- package/dist-cjs/lib/components/default-components/DefaultCollaboratorHint.js.map +2 -2
- package/dist-cjs/lib/components/default-components/DefaultCursor.js +1 -1
- package/dist-cjs/lib/components/default-components/DefaultCursor.js.map +2 -2
- package/dist-cjs/lib/components/default-components/DefaultGrid.js +1 -1
- package/dist-cjs/lib/components/default-components/DefaultGrid.js.map +2 -2
- package/dist-cjs/lib/components/default-components/DefaultHandles.js +1 -1
- package/dist-cjs/lib/components/default-components/DefaultHandles.js.map +2 -2
- package/dist-cjs/lib/components/default-components/DefaultShapeIndicator.js +1 -1
- package/dist-cjs/lib/components/default-components/DefaultShapeIndicator.js.map +2 -2
- package/dist-cjs/lib/components/default-components/DefaultSnapIndictor.js +1 -1
- package/dist-cjs/lib/components/default-components/DefaultSnapIndictor.js.map +2 -2
- package/dist-cjs/lib/components/default-components/DefaultSpinner.js +27 -15
- package/dist-cjs/lib/components/default-components/DefaultSpinner.js.map +3 -3
- package/dist-cjs/lib/editor/Editor.js +24 -8
- package/dist-cjs/lib/editor/Editor.js.map +2 -2
- package/dist-cjs/lib/editor/managers/TextManager/TextManager.js +96 -101
- package/dist-cjs/lib/editor/managers/TextManager/TextManager.js.map +2 -2
- package/dist-cjs/lib/editor/tools/StateNode.js +20 -1
- package/dist-cjs/lib/editor/tools/StateNode.js.map +2 -2
- package/dist-cjs/lib/hooks/useEditorComponents.js.map +1 -1
- package/dist-cjs/lib/license/Watermark.js +2 -2
- package/dist-cjs/lib/license/Watermark.js.map +2 -2
- package/dist-cjs/lib/primitives/geometry/Arc2d.js +1 -1
- package/dist-cjs/lib/primitives/geometry/Arc2d.js.map +2 -2
- package/dist-cjs/lib/primitives/geometry/Circle2d.js +1 -1
- package/dist-cjs/lib/primitives/geometry/Circle2d.js.map +2 -2
- package/dist-cjs/lib/primitives/geometry/CubicBezier2d.js +3 -1
- package/dist-cjs/lib/primitives/geometry/CubicBezier2d.js.map +2 -2
- package/dist-cjs/lib/primitives/geometry/Ellipse2d.js +1 -1
- package/dist-cjs/lib/primitives/geometry/Ellipse2d.js.map +2 -2
- package/dist-cjs/lib/primitives/geometry/geometry-constants.js +2 -2
- package/dist-cjs/lib/primitives/geometry/geometry-constants.js.map +2 -2
- package/dist-cjs/lib/primitives/intersect.js +4 -4
- package/dist-cjs/lib/primitives/intersect.js.map +2 -2
- package/dist-cjs/lib/primitives/utils.js +4 -0
- package/dist-cjs/lib/primitives/utils.js.map +2 -2
- package/dist-cjs/lib/utils/sync/TLLocalSyncClient.js +0 -1
- package/dist-cjs/lib/utils/sync/TLLocalSyncClient.js.map +2 -2
- package/dist-cjs/version.js +3 -3
- package/dist-cjs/version.js.map +1 -1
- package/dist-esm/index.d.mts +54 -44
- package/dist-esm/index.mjs +43 -41
- package/dist-esm/index.mjs.map +2 -2
- package/dist-esm/lib/components/SVGContainer.mjs +1 -1
- package/dist-esm/lib/components/SVGContainer.mjs.map +2 -2
- package/dist-esm/lib/components/default-components/DefaultBrush.mjs +1 -1
- package/dist-esm/lib/components/default-components/DefaultBrush.mjs.map +2 -2
- package/dist-esm/lib/components/default-components/DefaultCanvas.mjs +1 -1
- package/dist-esm/lib/components/default-components/DefaultCanvas.mjs.map +2 -2
- package/dist-esm/lib/components/default-components/DefaultCollaboratorHint.mjs +1 -1
- package/dist-esm/lib/components/default-components/DefaultCollaboratorHint.mjs.map +2 -2
- package/dist-esm/lib/components/default-components/DefaultCursor.mjs +1 -1
- package/dist-esm/lib/components/default-components/DefaultCursor.mjs.map +2 -2
- package/dist-esm/lib/components/default-components/DefaultGrid.mjs +1 -1
- package/dist-esm/lib/components/default-components/DefaultGrid.mjs.map +2 -2
- package/dist-esm/lib/components/default-components/DefaultHandles.mjs +1 -1
- package/dist-esm/lib/components/default-components/DefaultHandles.mjs.map +2 -2
- package/dist-esm/lib/components/default-components/DefaultShapeIndicator.mjs +1 -1
- package/dist-esm/lib/components/default-components/DefaultShapeIndicator.mjs.map +2 -2
- package/dist-esm/lib/components/default-components/DefaultSnapIndictor.mjs +1 -1
- package/dist-esm/lib/components/default-components/DefaultSnapIndictor.mjs.map +2 -2
- package/dist-esm/lib/components/default-components/DefaultSpinner.mjs +17 -15
- package/dist-esm/lib/components/default-components/DefaultSpinner.mjs.map +2 -2
- package/dist-esm/lib/editor/Editor.mjs +24 -8
- package/dist-esm/lib/editor/Editor.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs +96 -101
- package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs.map +2 -2
- package/dist-esm/lib/editor/tools/StateNode.mjs +20 -1
- package/dist-esm/lib/editor/tools/StateNode.mjs.map +2 -2
- package/dist-esm/lib/hooks/useEditorComponents.mjs.map +1 -1
- package/dist-esm/lib/license/Watermark.mjs +2 -2
- package/dist-esm/lib/license/Watermark.mjs.map +2 -2
- package/dist-esm/lib/primitives/geometry/Arc2d.mjs +2 -2
- package/dist-esm/lib/primitives/geometry/Arc2d.mjs.map +2 -2
- package/dist-esm/lib/primitives/geometry/Circle2d.mjs +2 -2
- package/dist-esm/lib/primitives/geometry/Circle2d.mjs.map +2 -2
- package/dist-esm/lib/primitives/geometry/CubicBezier2d.mjs +3 -1
- package/dist-esm/lib/primitives/geometry/CubicBezier2d.mjs.map +2 -2
- package/dist-esm/lib/primitives/geometry/Ellipse2d.mjs +2 -2
- package/dist-esm/lib/primitives/geometry/Ellipse2d.mjs.map +2 -2
- package/dist-esm/lib/primitives/geometry/geometry-constants.mjs +2 -2
- package/dist-esm/lib/primitives/geometry/geometry-constants.mjs.map +2 -2
- package/dist-esm/lib/primitives/intersect.mjs +5 -5
- package/dist-esm/lib/primitives/intersect.mjs.map +2 -2
- package/dist-esm/lib/primitives/utils.mjs +4 -0
- package/dist-esm/lib/primitives/utils.mjs.map +2 -2
- package/dist-esm/lib/utils/sync/TLLocalSyncClient.mjs +0 -1
- package/dist-esm/lib/utils/sync/TLLocalSyncClient.mjs.map +2 -2
- package/dist-esm/version.mjs +3 -3
- package/dist-esm/version.mjs.map +1 -1
- package/editor.css +17 -4
- package/package.json +9 -8
- package/src/index.ts +63 -62
- package/src/lib/components/SVGContainer.tsx +1 -1
- package/src/lib/components/default-components/DefaultBrush.tsx +1 -1
- package/src/lib/components/default-components/DefaultCanvas.tsx +1 -1
- package/src/lib/components/default-components/DefaultCollaboratorHint.tsx +1 -1
- package/src/lib/components/default-components/DefaultCursor.tsx +1 -1
- package/src/lib/components/default-components/DefaultGrid.tsx +1 -1
- package/src/lib/components/default-components/DefaultHandles.tsx +5 -1
- package/src/lib/components/default-components/DefaultShapeIndicator.tsx +1 -1
- package/src/lib/components/default-components/DefaultSnapIndictor.tsx +1 -1
- package/src/lib/components/default-components/DefaultSpinner.tsx +12 -12
- package/src/lib/editor/Editor.test.ts +407 -0
- package/src/lib/editor/Editor.ts +35 -9
- package/src/lib/editor/managers/TextManager/TextManager.ts +108 -128
- package/src/lib/editor/tools/StateNode.test.ts +285 -0
- package/src/lib/editor/tools/StateNode.ts +27 -1
- package/src/lib/hooks/useEditorComponents.tsx +1 -1
- package/src/lib/license/LicenseManager.test.ts +1 -1
- package/src/lib/license/Watermark.tsx +2 -2
- package/src/lib/primitives/geometry/Arc2d.ts +2 -2
- package/src/lib/primitives/geometry/Circle2d.ts +2 -2
- package/src/lib/primitives/geometry/CubicBezier2d.ts +4 -1
- package/src/lib/primitives/geometry/Ellipse2d.ts +2 -2
- package/src/lib/primitives/geometry/geometry-constants.ts +2 -1
- package/src/lib/primitives/intersect.test.ts +946 -0
- package/src/lib/primitives/intersect.ts +12 -5
- package/src/lib/primitives/utils.ts +11 -0
- package/src/lib/utils/sync/TLLocalSyncClient.ts +0 -1
- package/src/version.ts +3 -3
- 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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
135
|
+
const restoreStyles = this.setElementStyles(newStyles)
|
|
120
136
|
|
|
121
|
-
|
|
122
|
-
|
|
137
|
+
try {
|
|
138
|
+
elm.innerHTML = html
|
|
123
139
|
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
332
|
-
|
|
333
|
-
}
|
|
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
|
-
|
|
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
|
-
|
|
330
|
+
return spans
|
|
331
|
+
} finally {
|
|
332
|
+
restoreStyles()
|
|
333
|
+
}
|
|
354
334
|
}
|
|
355
335
|
}
|
|
@@ -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 (
|
|
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
|
|
@@ -69,7 +69,7 @@ export interface TLEditorComponents {
|
|
|
69
69
|
ShapeIndicator?: ComponentType<TLShapeIndicatorProps> | null
|
|
70
70
|
ShapeIndicators?: ComponentType | null
|
|
71
71
|
SnapIndicator?: ComponentType<TLSnapIndicatorProps> | null
|
|
72
|
-
Spinner?: ComponentType | null
|
|
72
|
+
Spinner?: ComponentType<React.SVGProps<SVGSVGElement>> | null
|
|
73
73
|
SvgDefs?: ComponentType | null
|
|
74
74
|
ZoomBrush?: ComponentType<TLBrushProps> | null
|
|
75
75
|
|
|
@@ -417,7 +417,7 @@ function importPrivateKey(pemContents: string) {
|
|
|
417
417
|
// base64 decode the string to get the binary data
|
|
418
418
|
const binaryDerString = atob(pemContents)
|
|
419
419
|
// convert from a binary string to an ArrayBuffer
|
|
420
|
-
const binaryDer = str2ab(binaryDerString)
|
|
420
|
+
const binaryDer = str2ab(binaryDerString)
|
|
421
421
|
|
|
422
422
|
return crypto.subtle.importKey(
|
|
423
423
|
'pkcs8',
|
|
@@ -143,7 +143,7 @@ To remove the watermark, please purchase a license at tldraw.dev.
|
|
|
143
143
|
}
|
|
144
144
|
|
|
145
145
|
.${className}:hover > button {
|
|
146
|
-
animation:
|
|
146
|
+
animation: ${className}_delayed_link 0.2s forwards ease-in-out;
|
|
147
147
|
animation-delay: 0.32s;
|
|
148
148
|
}
|
|
149
149
|
|
|
@@ -153,7 +153,7 @@ To remove the watermark, please purchase a license at tldraw.dev.
|
|
|
153
153
|
}
|
|
154
154
|
|
|
155
155
|
|
|
156
|
-
@keyframes
|
|
156
|
+
@keyframes ${className}_delayed_link {
|
|
157
157
|
0% {
|
|
158
158
|
cursor: inherit;
|
|
159
159
|
opacity: .38;
|
|
@@ -2,7 +2,7 @@ import { Vec, VecLike } from '../Vec'
|
|
|
2
2
|
import { intersectLineSegmentCircle } from '../intersect'
|
|
3
3
|
import { getArcMeasure, getPointInArcT, getPointOnCircle } from '../utils'
|
|
4
4
|
import { Geometry2d, Geometry2dOptions } from './Geometry2d'
|
|
5
|
-
import {
|
|
5
|
+
import { getVerticesCountForArcLength } from './geometry-constants'
|
|
6
6
|
|
|
7
7
|
/** @public */
|
|
8
8
|
export class Arc2d extends Geometry2d {
|
|
@@ -94,7 +94,7 @@ export class Arc2d extends Geometry2d {
|
|
|
94
94
|
getVertices(): Vec[] {
|
|
95
95
|
const { _center, _measure: measure, length, _radius: radius, _angleStart: angleStart } = this
|
|
96
96
|
const vertices: Vec[] = []
|
|
97
|
-
for (let i = 0, n =
|
|
97
|
+
for (let i = 0, n = getVerticesCountForArcLength(Math.abs(length)); i < n + 1; i++) {
|
|
98
98
|
const t = (i / n) * measure
|
|
99
99
|
const angle = angleStart + t
|
|
100
100
|
vertices.push(getPointOnCircle(_center, radius, angle))
|