@tldraw/editor 3.16.0-next.df90ce0ff566 → 3.16.0-next.e57e478c23e0
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 +134 -110
- package/dist-cjs/index.js +3 -5
- package/dist-cjs/index.js.map +2 -2
- package/dist-cjs/lib/TldrawEditor.js +8 -6
- package/dist-cjs/lib/TldrawEditor.js.map +2 -2
- package/dist-cjs/lib/components/MenuClickCapture.js +0 -5
- package/dist-cjs/lib/components/MenuClickCapture.js.map +2 -2
- package/dist-cjs/lib/components/Shape.js +7 -10
- package/dist-cjs/lib/components/Shape.js.map +2 -2
- package/dist-cjs/lib/components/default-components/DefaultCanvas.js +4 -23
- 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 +1 -1
- package/dist-cjs/lib/components/default-components/DefaultErrorFallback.js +1 -1
- package/dist-cjs/lib/components/default-components/DefaultErrorFallback.js.map +2 -2
- package/dist-cjs/lib/components/default-components/DefaultScribble.js +1 -1
- package/dist-cjs/lib/components/default-components/DefaultScribble.js.map +2 -2
- package/dist-cjs/lib/components/default-components/DefaultShapeIndicator.js +9 -1
- package/dist-cjs/lib/components/default-components/DefaultShapeIndicator.js.map +2 -2
- package/dist-cjs/lib/config/TLUserPreferences.js +9 -3
- package/dist-cjs/lib/config/TLUserPreferences.js.map +2 -2
- package/dist-cjs/lib/editor/Editor.js +77 -133
- package/dist-cjs/lib/editor/Editor.js.map +2 -2
- package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js +9 -4
- package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js.map +2 -2
- package/dist-cjs/lib/editor/shapes/ShapeUtil.js +13 -0
- package/dist-cjs/lib/editor/shapes/ShapeUtil.js.map +2 -2
- package/dist-cjs/lib/editor/types/misc-types.js.map +1 -1
- package/dist-cjs/lib/exports/getSvgJsx.js +35 -16
- package/dist-cjs/lib/exports/getSvgJsx.js.map +2 -2
- package/dist-cjs/lib/hooks/useCanvasEvents.js +31 -25
- package/dist-cjs/lib/hooks/useCanvasEvents.js.map +2 -2
- package/dist-cjs/lib/hooks/usePassThroughMouseOverEvents.js +4 -1
- package/dist-cjs/lib/hooks/usePassThroughMouseOverEvents.js.map +2 -2
- package/dist-cjs/lib/hooks/usePassThroughWheelEvents.js +4 -1
- package/dist-cjs/lib/hooks/usePassThroughWheelEvents.js.map +2 -2
- package/dist-cjs/lib/{utils/nearestMultiple.js → hooks/useStateAttribute.js} +15 -14
- package/dist-cjs/lib/hooks/useStateAttribute.js.map +7 -0
- package/dist-cjs/lib/license/LicenseManager.js +17 -22
- package/dist-cjs/lib/license/LicenseManager.js.map +2 -2
- package/dist-cjs/lib/license/LicenseProvider.js +5 -0
- package/dist-cjs/lib/license/LicenseProvider.js.map +2 -2
- package/dist-cjs/lib/license/Watermark.js +8 -8
- package/dist-cjs/lib/license/Watermark.js.map +1 -1
- package/dist-cjs/lib/license/useLicenseManagerState.js.map +2 -2
- package/dist-cjs/lib/options.js +7 -0
- package/dist-cjs/lib/options.js.map +2 -2
- package/dist-cjs/lib/primitives/Box.js +3 -0
- package/dist-cjs/lib/primitives/Box.js.map +2 -2
- package/dist-cjs/lib/primitives/Vec.js +0 -4
- package/dist-cjs/lib/primitives/Vec.js.map +2 -2
- package/dist-cjs/lib/primitives/geometry/Geometry2d.js +26 -18
- package/dist-cjs/lib/primitives/geometry/Geometry2d.js.map +2 -2
- package/dist-cjs/lib/primitives/geometry/Group2d.js +3 -0
- package/dist-cjs/lib/primitives/geometry/Group2d.js.map +2 -2
- package/dist-cjs/lib/utils/EditorAtom.js +45 -0
- package/dist-cjs/lib/utils/EditorAtom.js.map +7 -0
- package/dist-cjs/lib/utils/reparenting.js +2 -35
- package/dist-cjs/lib/utils/reparenting.js.map +3 -3
- package/dist-cjs/version.js +3 -3
- package/dist-cjs/version.js.map +1 -1
- package/dist-esm/index.d.mts +134 -110
- package/dist-esm/index.mjs +3 -5
- package/dist-esm/index.mjs.map +2 -2
- package/dist-esm/lib/TldrawEditor.mjs +8 -6
- package/dist-esm/lib/TldrawEditor.mjs.map +2 -2
- package/dist-esm/lib/components/MenuClickCapture.mjs +0 -5
- package/dist-esm/lib/components/MenuClickCapture.mjs.map +2 -2
- package/dist-esm/lib/components/Shape.mjs +7 -10
- package/dist-esm/lib/components/Shape.mjs.map +2 -2
- package/dist-esm/lib/components/default-components/DefaultCanvas.mjs +4 -23
- 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 +1 -1
- package/dist-esm/lib/components/default-components/DefaultErrorFallback.mjs +1 -1
- package/dist-esm/lib/components/default-components/DefaultErrorFallback.mjs.map +2 -2
- package/dist-esm/lib/components/default-components/DefaultScribble.mjs +1 -1
- package/dist-esm/lib/components/default-components/DefaultScribble.mjs.map +2 -2
- package/dist-esm/lib/components/default-components/DefaultShapeIndicator.mjs +9 -1
- package/dist-esm/lib/components/default-components/DefaultShapeIndicator.mjs.map +2 -2
- package/dist-esm/lib/config/TLUserPreferences.mjs +9 -3
- package/dist-esm/lib/config/TLUserPreferences.mjs.map +2 -2
- package/dist-esm/lib/editor/Editor.mjs +77 -133
- package/dist-esm/lib/editor/Editor.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs +9 -4
- package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs.map +2 -2
- package/dist-esm/lib/editor/shapes/ShapeUtil.mjs +13 -0
- package/dist-esm/lib/editor/shapes/ShapeUtil.mjs.map +2 -2
- package/dist-esm/lib/exports/getSvgJsx.mjs +36 -16
- package/dist-esm/lib/exports/getSvgJsx.mjs.map +2 -2
- package/dist-esm/lib/hooks/useCanvasEvents.mjs +32 -26
- package/dist-esm/lib/hooks/useCanvasEvents.mjs.map +2 -2
- package/dist-esm/lib/hooks/usePassThroughMouseOverEvents.mjs +4 -1
- package/dist-esm/lib/hooks/usePassThroughMouseOverEvents.mjs.map +2 -2
- package/dist-esm/lib/hooks/usePassThroughWheelEvents.mjs +4 -1
- package/dist-esm/lib/hooks/usePassThroughWheelEvents.mjs.map +2 -2
- package/dist-esm/lib/hooks/useStateAttribute.mjs +15 -0
- package/dist-esm/lib/hooks/useStateAttribute.mjs.map +7 -0
- package/dist-esm/lib/license/LicenseManager.mjs +17 -22
- package/dist-esm/lib/license/LicenseManager.mjs.map +2 -2
- package/dist-esm/lib/license/LicenseProvider.mjs +5 -0
- package/dist-esm/lib/license/LicenseProvider.mjs.map +2 -2
- package/dist-esm/lib/license/Watermark.mjs +8 -8
- package/dist-esm/lib/license/Watermark.mjs.map +1 -1
- package/dist-esm/lib/license/useLicenseManagerState.mjs.map +2 -2
- package/dist-esm/lib/options.mjs +7 -0
- package/dist-esm/lib/options.mjs.map +2 -2
- package/dist-esm/lib/primitives/Box.mjs +4 -1
- package/dist-esm/lib/primitives/Box.mjs.map +2 -2
- package/dist-esm/lib/primitives/Vec.mjs +0 -4
- package/dist-esm/lib/primitives/Vec.mjs.map +2 -2
- package/dist-esm/lib/primitives/geometry/Geometry2d.mjs +29 -19
- package/dist-esm/lib/primitives/geometry/Geometry2d.mjs.map +2 -2
- package/dist-esm/lib/primitives/geometry/Group2d.mjs +3 -0
- package/dist-esm/lib/primitives/geometry/Group2d.mjs.map +2 -2
- package/dist-esm/lib/utils/EditorAtom.mjs +25 -0
- package/dist-esm/lib/utils/EditorAtom.mjs.map +7 -0
- package/dist-esm/lib/utils/reparenting.mjs +3 -40
- package/dist-esm/lib/utils/reparenting.mjs.map +2 -2
- package/dist-esm/version.mjs +3 -3
- package/dist-esm/version.mjs.map +1 -1
- package/editor.css +301 -288
- package/package.json +14 -37
- package/src/index.ts +3 -9
- package/src/lib/TldrawEditor.tsx +13 -17
- package/src/lib/components/MenuClickCapture.tsx +0 -8
- package/src/lib/components/Shape.tsx +6 -12
- package/src/lib/components/default-components/DefaultCanvas.tsx +5 -22
- package/src/lib/components/default-components/DefaultCollaboratorHint.tsx +1 -1
- package/src/lib/components/default-components/DefaultErrorFallback.tsx +1 -1
- package/src/lib/components/default-components/DefaultScribble.tsx +1 -1
- package/src/lib/components/default-components/DefaultShapeIndicator.tsx +5 -1
- package/src/lib/config/TLUserPreferences.ts +8 -1
- package/src/lib/editor/Editor.test.ts +12 -11
- package/src/lib/editor/Editor.ts +108 -193
- package/src/lib/editor/managers/ClickManager/ClickManager.test.ts +15 -14
- package/src/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.test.ts +16 -15
- package/src/lib/editor/managers/FocusManager/FocusManager.test.ts +49 -48
- package/src/lib/editor/managers/FontManager/FontManager.test.ts +24 -23
- package/src/lib/editor/managers/HistoryManager/HistoryManager.test.ts +7 -6
- package/src/lib/editor/managers/ScribbleManager/ScribbleManager.test.ts +12 -11
- package/src/lib/editor/managers/SnapManager/SnapManager.test.ts +57 -50
- package/src/lib/editor/managers/TextManager/TextManager.test.ts +51 -26
- package/src/lib/editor/managers/TickManager/TickManager.test.ts +14 -13
- package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.test.ts +34 -26
- package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.ts +6 -1
- package/src/lib/editor/shapes/ShapeUtil.ts +35 -0
- package/src/lib/editor/types/misc-types.ts +54 -7
- package/src/lib/exports/getSvgJsx.test.ts +868 -0
- package/src/lib/exports/getSvgJsx.tsx +78 -21
- package/src/lib/hooks/useCanvasEvents.ts +45 -38
- package/src/lib/hooks/usePassThroughMouseOverEvents.ts +4 -1
- package/src/lib/hooks/usePassThroughWheelEvents.ts +6 -1
- package/src/lib/hooks/useStateAttribute.ts +15 -0
- package/src/lib/license/LicenseManager.test.ts +61 -52
- package/src/lib/license/LicenseManager.ts +32 -24
- package/src/lib/license/LicenseProvider.tsx +8 -0
- package/src/lib/license/Watermark.test.tsx +2 -1
- package/src/lib/license/Watermark.tsx +8 -8
- package/src/lib/license/useLicenseManagerState.ts +2 -2
- package/src/lib/options.ts +8 -0
- package/src/lib/primitives/Box.test.ts +126 -0
- package/src/lib/primitives/Box.ts +10 -1
- package/src/lib/primitives/Vec.ts +0 -5
- package/src/lib/primitives/geometry/Geometry2d.ts +49 -19
- package/src/lib/primitives/geometry/Group2d.ts +4 -0
- package/src/lib/utils/EditorAtom.ts +37 -0
- package/src/lib/utils/reparenting.ts +3 -69
- package/src/lib/utils/sync/LocalIndexedDb.test.ts +2 -1
- package/src/lib/utils/sync/TLLocalSyncClient.test.ts +15 -15
- package/src/version.ts +3 -3
- package/dist-cjs/lib/utils/nearestMultiple.js.map +0 -7
- package/dist-esm/lib/utils/nearestMultiple.mjs +0 -14
- package/dist-esm/lib/utils/nearestMultiple.mjs.map +0 -7
- package/src/lib/utils/nearestMultiple.ts +0 -13
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
TLGroupShape,
|
|
5
5
|
TLShape,
|
|
6
6
|
TLShapeId,
|
|
7
|
+
getColorValue,
|
|
7
8
|
getDefaultColorTheme,
|
|
8
9
|
} from '@tldraw/tlschema'
|
|
9
10
|
import { hasOwnProperty, promiseWithResolve, uniqueId } from '@tldraw/utils'
|
|
@@ -56,33 +57,21 @@ export function getSvgJsx(editor: Editor, ids: TLShapeId[], opts: TLImageExportO
|
|
|
56
57
|
.filter(({ id }) => shapeIdsToInclude.has(id))
|
|
57
58
|
|
|
58
59
|
// --- Common bounding box of all shapes
|
|
60
|
+
const singleFrameShapeId =
|
|
61
|
+
ids.length === 1 && editor.isShapeOfType<TLFrameShape>(editor.getShape(ids[0])!, 'frame')
|
|
62
|
+
? ids[0]
|
|
63
|
+
: null
|
|
64
|
+
|
|
59
65
|
let bbox: null | Box = null
|
|
60
66
|
if (opts.bounds) {
|
|
61
|
-
bbox = opts.bounds
|
|
67
|
+
bbox = opts.bounds.clone().expandBy(padding)
|
|
62
68
|
} else {
|
|
63
|
-
|
|
64
|
-
const maskedPageBounds = editor.getShapeMaskedPageBounds(id)
|
|
65
|
-
if (!maskedPageBounds) continue
|
|
66
|
-
if (bbox) {
|
|
67
|
-
bbox.union(maskedPageBounds)
|
|
68
|
-
} else {
|
|
69
|
-
bbox = maskedPageBounds.clone()
|
|
70
|
-
}
|
|
71
|
-
}
|
|
69
|
+
bbox = getExportDefaultBounds(editor, renderingShapes, padding, singleFrameShapeId)
|
|
72
70
|
}
|
|
73
71
|
|
|
74
72
|
// no unmasked shapes to export
|
|
75
73
|
if (!bbox) return
|
|
76
74
|
|
|
77
|
-
const singleFrameShapeId =
|
|
78
|
-
ids.length === 1 && editor.isShapeOfType<TLFrameShape>(editor.getShape(ids[0])!, 'frame')
|
|
79
|
-
? ids[0]
|
|
80
|
-
: null
|
|
81
|
-
if (!singleFrameShapeId) {
|
|
82
|
-
// Expand by an extra 32 pixels
|
|
83
|
-
bbox.expandBy(padding)
|
|
84
|
-
}
|
|
85
|
-
|
|
86
75
|
// We want the svg image to be BIGGER THAN USUAL to account for image quality
|
|
87
76
|
const w = bbox.width * scale
|
|
88
77
|
const h = bbox.height * scale
|
|
@@ -119,6 +108,75 @@ export function getSvgJsx(editor: Editor, ids: TLShapeId[], opts: TLImageExportO
|
|
|
119
108
|
return { jsx: svg, width: w, height: h, exportDelay }
|
|
120
109
|
}
|
|
121
110
|
|
|
111
|
+
/**
|
|
112
|
+
* Calculates the default bounds for an SVG export. This function handles:
|
|
113
|
+
* 1. Computing masked page bounds for each shape
|
|
114
|
+
* 2. Container logic: if a shape is marked as an export bounds container and it
|
|
115
|
+
* contains all other shapes, use its bounds and skip padding
|
|
116
|
+
* 3. Otherwise, create a union of all shape bounds and apply padding
|
|
117
|
+
*
|
|
118
|
+
* The container logic is useful for cases like annotating on an image - if the image
|
|
119
|
+
* contains all annotations, we want to export exactly the image bounds without extra padding.
|
|
120
|
+
*
|
|
121
|
+
* @param editor - The editor instance
|
|
122
|
+
* @param renderingShapes - The shapes to include in the export
|
|
123
|
+
* @param padding - Padding to add around the bounds (only applied if no container bounds)
|
|
124
|
+
* @param singleFrameShapeId - If exporting a single frame, this is its ID (skips padding)
|
|
125
|
+
* @returns The calculated bounds box, or null if no shapes to export
|
|
126
|
+
*/
|
|
127
|
+
export function getExportDefaultBounds(
|
|
128
|
+
editor: Editor,
|
|
129
|
+
renderingShapes: TLRenderingShape[],
|
|
130
|
+
padding: number,
|
|
131
|
+
singleFrameShapeId: TLShapeId | null
|
|
132
|
+
) {
|
|
133
|
+
let isBoundedByContainer = false
|
|
134
|
+
let bbox: null | Box = null
|
|
135
|
+
|
|
136
|
+
for (const { id } of renderingShapes) {
|
|
137
|
+
const maskedPageBounds = editor.getShapeMaskedPageBounds(id)
|
|
138
|
+
if (!maskedPageBounds) continue
|
|
139
|
+
|
|
140
|
+
// Check if this shape is an export bounds container (e.g., an image being annotated)
|
|
141
|
+
const shape = editor.getShape(id)!
|
|
142
|
+
const isContainer = editor.getShapeUtil(shape).isExportBoundsContainer(shape)
|
|
143
|
+
|
|
144
|
+
if (bbox) {
|
|
145
|
+
// Container logic: if this is a container and it contains all shapes processed so far,
|
|
146
|
+
// use the container's bounds instead of the union. This prevents extra padding around
|
|
147
|
+
// things like annotated images.
|
|
148
|
+
if (isContainer && Box.ContainsApproximately(maskedPageBounds, bbox)) {
|
|
149
|
+
isBoundedByContainer = true
|
|
150
|
+
bbox = maskedPageBounds.clone()
|
|
151
|
+
} else {
|
|
152
|
+
// If we were previously bounded by a container but this shape extends outside it,
|
|
153
|
+
// we're no longer bounded by a container
|
|
154
|
+
if (isBoundedByContainer && !Box.ContainsApproximately(bbox, maskedPageBounds)) {
|
|
155
|
+
isBoundedByContainer = false
|
|
156
|
+
}
|
|
157
|
+
// Expand the bounding box to include this shape
|
|
158
|
+
bbox.union(maskedPageBounds)
|
|
159
|
+
}
|
|
160
|
+
} else {
|
|
161
|
+
// First shape sets the initial bounds
|
|
162
|
+
isBoundedByContainer = isContainer
|
|
163
|
+
bbox = maskedPageBounds.clone()
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// No unmasked shapes to export
|
|
168
|
+
if (!bbox) return null
|
|
169
|
+
|
|
170
|
+
// Only apply padding if:
|
|
171
|
+
// - Not exporting a single frame (frames have their own padding rules)
|
|
172
|
+
// - Not bounded by a container (containers define their own bounds precisely)
|
|
173
|
+
if (!singleFrameShapeId && !isBoundedByContainer) {
|
|
174
|
+
bbox.expandBy(padding)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return bbox
|
|
178
|
+
}
|
|
179
|
+
|
|
122
180
|
function SvgExport({
|
|
123
181
|
editor,
|
|
124
182
|
preserveAspectRatio,
|
|
@@ -373,8 +431,7 @@ function SvgExport({
|
|
|
373
431
|
| { options: { showColors: boolean } }
|
|
374
432
|
if (frameShapeUtil?.options.showColors) {
|
|
375
433
|
const shape = editor.getShape(singleFrameShapeId)! as TLFrameShape
|
|
376
|
-
|
|
377
|
-
backgroundColor = color.frame.fill
|
|
434
|
+
backgroundColor = getColorValue(theme, shape.props.color, 'frameFill')
|
|
378
435
|
} else {
|
|
379
436
|
backgroundColor = theme.solid
|
|
380
437
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useValue } from '@tldraw/state-react'
|
|
2
|
-
import React, { useMemo } from 'react'
|
|
2
|
+
import React, { useEffect, useMemo } from 'react'
|
|
3
3
|
import { RIGHT_MOUSE_BUTTON } from '../constants'
|
|
4
4
|
import {
|
|
5
5
|
preventDefault,
|
|
@@ -16,9 +16,6 @@ export function useCanvasEvents() {
|
|
|
16
16
|
|
|
17
17
|
const events = useMemo(
|
|
18
18
|
function canvasEvents() {
|
|
19
|
-
// Track the last screen point
|
|
20
|
-
let lastX: number, lastY: number
|
|
21
|
-
|
|
22
19
|
function onPointerDown(e: React.PointerEvent) {
|
|
23
20
|
if ((e as any).isKilled) return
|
|
24
21
|
|
|
@@ -44,35 +41,9 @@ export function useCanvasEvents() {
|
|
|
44
41
|
})
|
|
45
42
|
}
|
|
46
43
|
|
|
47
|
-
function onPointerMove(e: React.PointerEvent) {
|
|
48
|
-
if ((e as any).isKilled) return
|
|
49
|
-
|
|
50
|
-
if (e.clientX === lastX && e.clientY === lastY) return
|
|
51
|
-
lastX = e.clientX
|
|
52
|
-
lastY = e.clientY
|
|
53
|
-
|
|
54
|
-
// For tools that benefit from a higher fidelity of events,
|
|
55
|
-
// we dispatch the coalesced events.
|
|
56
|
-
// N.B. Sometimes getCoalescedEvents isn't present on iOS, ugh.
|
|
57
|
-
const events =
|
|
58
|
-
currentTool.useCoalescedEvents && e.nativeEvent.getCoalescedEvents
|
|
59
|
-
? e.nativeEvent.getCoalescedEvents()
|
|
60
|
-
: [e]
|
|
61
|
-
for (const singleEvent of events) {
|
|
62
|
-
editor.dispatch({
|
|
63
|
-
type: 'pointer',
|
|
64
|
-
target: 'canvas',
|
|
65
|
-
name: 'pointer_move',
|
|
66
|
-
...getPointerInfo(singleEvent),
|
|
67
|
-
})
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
44
|
function onPointerUp(e: React.PointerEvent) {
|
|
72
45
|
if ((e as any).isKilled) return
|
|
73
46
|
if (e.button !== 0 && e.button !== 1 && e.button !== 2 && e.button !== 5) return
|
|
74
|
-
lastX = e.clientX
|
|
75
|
-
lastY = e.clientY
|
|
76
47
|
|
|
77
48
|
releasePointerCapture(e.currentTarget, e)
|
|
78
49
|
|
|
@@ -108,15 +79,15 @@ export function useCanvasEvents() {
|
|
|
108
79
|
// check that e.target is an HTMLElement
|
|
109
80
|
if (!(e.target instanceof HTMLElement)) return
|
|
110
81
|
|
|
82
|
+
const editingShapeId = editor.getEditingShape()?.id
|
|
111
83
|
if (
|
|
84
|
+
// if the target is not inside the editing shape
|
|
85
|
+
!(editingShapeId && e.target.closest(`[data-shape-id="${editingShapeId}"]`)) &&
|
|
86
|
+
// and the target is not an clickable element
|
|
112
87
|
e.target.tagName !== 'A' &&
|
|
88
|
+
// or a TextArea.tsx ?
|
|
113
89
|
e.target.tagName !== 'TEXTAREA' &&
|
|
114
|
-
!e.target.isContentEditable
|
|
115
|
-
// When in EditingShape state, we are actually clicking on a 'DIV'
|
|
116
|
-
// not A/TEXTAREA/contenteditable element yet. So, to preserve cursor position
|
|
117
|
-
// for edit mode on mobile we need to not preventDefault.
|
|
118
|
-
// TODO: Find out if we still need this preventDefault in general though.
|
|
119
|
-
!(editor.getEditingShape() && e.target.className.includes('tl-text-content'))
|
|
90
|
+
!e.target.isContentEditable
|
|
120
91
|
) {
|
|
121
92
|
preventDefault(e)
|
|
122
93
|
}
|
|
@@ -158,7 +129,6 @@ export function useCanvasEvents() {
|
|
|
158
129
|
|
|
159
130
|
return {
|
|
160
131
|
onPointerDown,
|
|
161
|
-
onPointerMove,
|
|
162
132
|
onPointerUp,
|
|
163
133
|
onPointerEnter,
|
|
164
134
|
onPointerLeave,
|
|
@@ -169,8 +139,45 @@ export function useCanvasEvents() {
|
|
|
169
139
|
onClick,
|
|
170
140
|
}
|
|
171
141
|
},
|
|
172
|
-
[editor
|
|
142
|
+
[editor]
|
|
173
143
|
)
|
|
174
144
|
|
|
145
|
+
// onPointerMove is special: where we're only interested in the other events when they're
|
|
146
|
+
// happening _on_ the canvas (as opposed to outside of it, or on UI floating over it), we want
|
|
147
|
+
// the pointer position to be up to date regardless of whether it's over the tldraw canvas or
|
|
148
|
+
// not. So instead of returning a listener to be attached to the canvas, we directly attach a
|
|
149
|
+
// listener to the whole document instead.
|
|
150
|
+
useEffect(() => {
|
|
151
|
+
let lastX: number, lastY: number
|
|
152
|
+
|
|
153
|
+
function onPointerMove(e: PointerEvent) {
|
|
154
|
+
if ((e as any).isKilled) return
|
|
155
|
+
;(e as any).isKilled = true
|
|
156
|
+
|
|
157
|
+
if (e.clientX === lastX && e.clientY === lastY) return
|
|
158
|
+
lastX = e.clientX
|
|
159
|
+
lastY = e.clientY
|
|
160
|
+
|
|
161
|
+
// For tools that benefit from a higher fidelity of events,
|
|
162
|
+
// we dispatch the coalesced events.
|
|
163
|
+
// N.B. Sometimes getCoalescedEvents isn't present on iOS, ugh.
|
|
164
|
+
const events =
|
|
165
|
+
currentTool.useCoalescedEvents && e.getCoalescedEvents ? e.getCoalescedEvents() : [e]
|
|
166
|
+
for (const singleEvent of events) {
|
|
167
|
+
editor.dispatch({
|
|
168
|
+
type: 'pointer',
|
|
169
|
+
target: 'canvas',
|
|
170
|
+
name: 'pointer_move',
|
|
171
|
+
...getPointerInfo(singleEvent),
|
|
172
|
+
})
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
document.body.addEventListener('pointermove', onPointerMove)
|
|
177
|
+
return () => {
|
|
178
|
+
document.body.removeEventListener('pointermove', onPointerMove)
|
|
179
|
+
}
|
|
180
|
+
}, [editor, currentTool])
|
|
181
|
+
|
|
175
182
|
return events
|
|
176
183
|
}
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import { RefObject, useEffect } from 'react'
|
|
2
2
|
import { preventDefault } from '../utils/dom'
|
|
3
3
|
import { useContainer } from './useContainer'
|
|
4
|
+
import { useMaybeEditor } from './useEditor'
|
|
4
5
|
|
|
5
6
|
/** @public */
|
|
6
7
|
export function usePassThroughMouseOverEvents(ref: RefObject<HTMLElement>) {
|
|
7
8
|
if (!ref) throw Error('usePassThroughWheelEvents must be passed a ref')
|
|
8
9
|
const container = useContainer()
|
|
10
|
+
const editor = useMaybeEditor()
|
|
9
11
|
|
|
10
12
|
useEffect(() => {
|
|
11
13
|
function onMouseOver(e: MouseEvent) {
|
|
14
|
+
if (!editor?.getInstanceState().isFocused) return
|
|
12
15
|
if ((e as any).isSpecialRedispatchedEvent) return
|
|
13
16
|
preventDefault(e)
|
|
14
17
|
const cvs = container.querySelector('.tl-canvas')
|
|
@@ -25,5 +28,5 @@ export function usePassThroughMouseOverEvents(ref: RefObject<HTMLElement>) {
|
|
|
25
28
|
return () => {
|
|
26
29
|
elm.removeEventListener('mouseover', onMouseOver)
|
|
27
30
|
}
|
|
28
|
-
}, [container, ref])
|
|
31
|
+
}, [container, editor, ref])
|
|
29
32
|
}
|
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
import { RefObject, useEffect } from 'react'
|
|
2
2
|
import { preventDefault } from '../utils/dom'
|
|
3
3
|
import { useContainer } from './useContainer'
|
|
4
|
+
import { useMaybeEditor } from './useEditor'
|
|
4
5
|
|
|
5
6
|
/** @public */
|
|
6
7
|
export function usePassThroughWheelEvents(ref: RefObject<HTMLElement>) {
|
|
7
8
|
if (!ref) throw Error('usePassThroughWheelEvents must be passed a ref')
|
|
8
9
|
const container = useContainer()
|
|
10
|
+
const editor = useMaybeEditor()
|
|
9
11
|
|
|
10
12
|
useEffect(() => {
|
|
11
13
|
function onWheel(e: WheelEvent) {
|
|
14
|
+
// Only pass through wheel events if the editor is focused
|
|
15
|
+
if (!editor?.getInstanceState().isFocused) return
|
|
16
|
+
|
|
12
17
|
if ((e as any).isSpecialRedispatchedEvent) return
|
|
13
18
|
|
|
14
19
|
// if the element is scrollable, don't redispatch the event
|
|
@@ -32,5 +37,5 @@ export function usePassThroughWheelEvents(ref: RefObject<HTMLElement>) {
|
|
|
32
37
|
return () => {
|
|
33
38
|
elm.removeEventListener('wheel', onWheel)
|
|
34
39
|
}
|
|
35
|
-
}, [container, ref])
|
|
40
|
+
}, [container, editor, ref])
|
|
36
41
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { react } from '@tldraw/state'
|
|
2
|
+
import { useLayoutEffect } from 'react'
|
|
3
|
+
import { useEditor } from './useEditor'
|
|
4
|
+
|
|
5
|
+
export function useStateAttribute() {
|
|
6
|
+
const editor = useEditor()
|
|
7
|
+
|
|
8
|
+
// we use a layout effect because we don't want there to be any perceptible delay between the
|
|
9
|
+
// editor mounting and this attribute being applied, because styles may depend on it:
|
|
10
|
+
useLayoutEffect(() => {
|
|
11
|
+
return react('stateAttribute', () => {
|
|
12
|
+
editor.getContainer().setAttribute('data-state', editor.getPath())
|
|
13
|
+
})
|
|
14
|
+
}, [editor])
|
|
15
|
+
}
|
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
import crypto from 'crypto'
|
|
2
|
+
import { vi } from 'vitest'
|
|
2
3
|
import { publishDates } from '../../version'
|
|
3
4
|
import { str2ab } from '../utils/licensing'
|
|
4
5
|
import {
|
|
5
6
|
FLAGS,
|
|
6
|
-
|
|
7
|
+
getLicenseState,
|
|
7
8
|
LicenseManager,
|
|
8
9
|
PROPERTIES,
|
|
9
10
|
ValidLicenseKeyResult,
|
|
10
11
|
} from './LicenseManager'
|
|
11
12
|
|
|
12
|
-
|
|
13
|
+
vi.mock('../../version', () => {
|
|
13
14
|
return {
|
|
15
|
+
version: '3.15.1',
|
|
14
16
|
publishDates: {
|
|
15
17
|
major: '2024-06-28T10:56:07.893Z',
|
|
16
18
|
minor: '2024-07-02T16:49:50.397Z',
|
|
@@ -485,115 +487,122 @@ function getDefaultLicenseResult(overrides: Partial<ValidLicenseKeyResult>): Val
|
|
|
485
487
|
}
|
|
486
488
|
}
|
|
487
489
|
|
|
488
|
-
describe(
|
|
489
|
-
it('
|
|
490
|
+
describe('getLicenseState', () => {
|
|
491
|
+
it('returns "unlicensed" for unparseable license', () => {
|
|
490
492
|
const licenseResult = getDefaultLicenseResult({
|
|
491
493
|
// @ts-ignore
|
|
492
494
|
isLicenseParseable: false,
|
|
493
495
|
})
|
|
494
|
-
expect(
|
|
496
|
+
expect(getLicenseState(licenseResult)).toBe('unlicensed')
|
|
495
497
|
})
|
|
496
498
|
|
|
497
|
-
it('
|
|
499
|
+
it('returns "unlicensed" for invalid domain in production', () => {
|
|
498
500
|
const licenseResult = getDefaultLicenseResult({
|
|
499
501
|
isDomainValid: false,
|
|
502
|
+
isDevelopment: false,
|
|
500
503
|
})
|
|
501
|
-
expect(
|
|
504
|
+
expect(getLicenseState(licenseResult)).toBe('unlicensed')
|
|
502
505
|
})
|
|
503
506
|
|
|
504
|
-
it('
|
|
507
|
+
it('returns "licensed" for invalid domain in development mode', () => {
|
|
508
|
+
const licenseResult = getDefaultLicenseResult({
|
|
509
|
+
isDomainValid: false,
|
|
510
|
+
isDevelopment: true,
|
|
511
|
+
})
|
|
512
|
+
expect(getLicenseState(licenseResult)).toBe('licensed')
|
|
513
|
+
})
|
|
514
|
+
|
|
515
|
+
it('returns "unlicensed" for expired annual license', () => {
|
|
505
516
|
const licenseResult = getDefaultLicenseResult({
|
|
506
517
|
isAnnualLicense: true,
|
|
507
518
|
isAnnualLicenseExpired: true,
|
|
519
|
+
isInternalLicense: false,
|
|
508
520
|
})
|
|
509
|
-
expect(
|
|
521
|
+
expect(getLicenseState(licenseResult)).toBe('unlicensed')
|
|
510
522
|
})
|
|
511
523
|
|
|
512
|
-
it('
|
|
524
|
+
it('returns "unlicensed" for expired annual license even in dev mode', () => {
|
|
513
525
|
const licenseResult = getDefaultLicenseResult({
|
|
514
526
|
isAnnualLicense: true,
|
|
515
527
|
isAnnualLicenseExpired: true,
|
|
516
528
|
isDevelopment: true,
|
|
529
|
+
isInternalLicense: false,
|
|
517
530
|
})
|
|
518
|
-
expect(
|
|
531
|
+
expect(getLicenseState(licenseResult)).toBe('unlicensed')
|
|
519
532
|
})
|
|
520
533
|
|
|
521
|
-
it('
|
|
534
|
+
it('returns "unlicensed" for expired perpetual license', () => {
|
|
522
535
|
const licenseResult = getDefaultLicenseResult({
|
|
523
536
|
isPerpetualLicense: true,
|
|
524
537
|
isPerpetualLicenseExpired: true,
|
|
538
|
+
isInternalLicense: false,
|
|
525
539
|
})
|
|
526
|
-
expect(
|
|
540
|
+
expect(getLicenseState(licenseResult)).toBe('unlicensed')
|
|
527
541
|
})
|
|
528
542
|
|
|
529
|
-
it('
|
|
543
|
+
it('returns "internal-expired" for expired internal annual license with valid domain', () => {
|
|
544
|
+
const expiryDate = new Date(2023, 1, 1)
|
|
530
545
|
const licenseResult = getDefaultLicenseResult({
|
|
531
546
|
isAnnualLicense: true,
|
|
532
|
-
isAnnualLicenseExpired:
|
|
533
|
-
isInternalLicense:
|
|
547
|
+
isAnnualLicenseExpired: true,
|
|
548
|
+
isInternalLicense: true,
|
|
549
|
+
isDomainValid: true,
|
|
550
|
+
expiryDate,
|
|
534
551
|
})
|
|
535
|
-
expect(
|
|
552
|
+
expect(getLicenseState(licenseResult)).toBe('internal-expired')
|
|
536
553
|
})
|
|
537
554
|
|
|
538
|
-
it('
|
|
555
|
+
it('returns "internal-expired" for expired internal perpetual license with valid domain', () => {
|
|
556
|
+
const expiryDate = new Date(2023, 1, 1)
|
|
539
557
|
const licenseResult = getDefaultLicenseResult({
|
|
540
558
|
isPerpetualLicense: true,
|
|
541
|
-
isPerpetualLicenseExpired:
|
|
542
|
-
isInternalLicense:
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
})
|
|
546
|
-
|
|
547
|
-
it('does not show watermark when in development mode', () => {
|
|
548
|
-
const licenseResult = getDefaultLicenseResult({
|
|
549
|
-
isDevelopment: true,
|
|
559
|
+
isPerpetualLicenseExpired: true,
|
|
560
|
+
isInternalLicense: true,
|
|
561
|
+
isDomainValid: true,
|
|
562
|
+
expiryDate,
|
|
550
563
|
})
|
|
551
|
-
expect(
|
|
564
|
+
expect(getLicenseState(licenseResult)).toBe('internal-expired')
|
|
552
565
|
})
|
|
553
566
|
|
|
554
|
-
it('
|
|
567
|
+
it('returns "unlicensed" for expired internal license with invalid domain', () => {
|
|
568
|
+
const expiryDate = new Date(2023, 1, 1)
|
|
555
569
|
const licenseResult = getDefaultLicenseResult({
|
|
556
|
-
|
|
557
|
-
|
|
570
|
+
isAnnualLicense: true,
|
|
571
|
+
isAnnualLicenseExpired: true,
|
|
572
|
+
isInternalLicense: true,
|
|
573
|
+
isDomainValid: false,
|
|
574
|
+
expiryDate,
|
|
558
575
|
})
|
|
559
|
-
expect(
|
|
576
|
+
expect(getLicenseState(licenseResult)).toBe('unlicensed')
|
|
560
577
|
})
|
|
561
578
|
|
|
562
|
-
it('
|
|
579
|
+
it('returns "licensed-with-watermark" for watermarked license', () => {
|
|
563
580
|
const licenseResult = getDefaultLicenseResult({
|
|
564
|
-
|
|
565
|
-
isDomainValid: false,
|
|
566
|
-
isDevelopment: true,
|
|
581
|
+
isLicensedWithWatermark: true,
|
|
567
582
|
})
|
|
568
|
-
expect(
|
|
583
|
+
expect(getLicenseState(licenseResult)).toBe('licensed-with-watermark')
|
|
569
584
|
})
|
|
570
585
|
|
|
571
|
-
it('
|
|
572
|
-
const expiryDate = new Date(2023, 1, 1)
|
|
586
|
+
it('returns "licensed" for valid annual license', () => {
|
|
573
587
|
const licenseResult = getDefaultLicenseResult({
|
|
574
588
|
isAnnualLicense: true,
|
|
575
|
-
isAnnualLicenseExpired:
|
|
576
|
-
isInternalLicense: true,
|
|
577
|
-
expiryDate,
|
|
589
|
+
isAnnualLicenseExpired: false,
|
|
578
590
|
})
|
|
579
|
-
expect((
|
|
591
|
+
expect(getLicenseState(licenseResult)).toBe('licensed')
|
|
580
592
|
})
|
|
581
593
|
|
|
582
|
-
it('
|
|
583
|
-
const expiryDate = new Date(2023, 1, 1)
|
|
594
|
+
it('returns "licensed" for valid perpetual license', () => {
|
|
584
595
|
const licenseResult = getDefaultLicenseResult({
|
|
585
596
|
isPerpetualLicense: true,
|
|
586
|
-
isPerpetualLicenseExpired:
|
|
587
|
-
isInternalLicense: true,
|
|
588
|
-
expiryDate,
|
|
597
|
+
isPerpetualLicenseExpired: false,
|
|
589
598
|
})
|
|
590
|
-
expect((
|
|
599
|
+
expect(getLicenseState(licenseResult)).toBe('licensed')
|
|
591
600
|
})
|
|
592
601
|
|
|
593
|
-
it('
|
|
602
|
+
it('returns "licensed" for valid license in development mode', () => {
|
|
594
603
|
const licenseResult = getDefaultLicenseResult({
|
|
595
|
-
|
|
604
|
+
isDevelopment: true,
|
|
596
605
|
})
|
|
597
|
-
expect(
|
|
606
|
+
expect(getLicenseState(licenseResult)).toBe('licensed')
|
|
598
607
|
})
|
|
599
608
|
})
|
|
@@ -33,6 +33,14 @@ export interface LicenseInfo {
|
|
|
33
33
|
flags: number
|
|
34
34
|
expiryDate: string
|
|
35
35
|
}
|
|
36
|
+
|
|
37
|
+
/** @internal */
|
|
38
|
+
export type LicenseState =
|
|
39
|
+
| 'pending'
|
|
40
|
+
| 'licensed'
|
|
41
|
+
| 'licensed-with-watermark'
|
|
42
|
+
| 'unlicensed'
|
|
43
|
+
| 'internal-expired'
|
|
36
44
|
/** @internal */
|
|
37
45
|
export type InvalidLicenseReason =
|
|
38
46
|
| 'invalid-license-key'
|
|
@@ -73,10 +81,7 @@ export class LicenseManager {
|
|
|
73
81
|
public isDevelopment: boolean
|
|
74
82
|
public isTest: boolean
|
|
75
83
|
public isCryptoAvailable: boolean
|
|
76
|
-
state = atom<'
|
|
77
|
-
'license state',
|
|
78
|
-
'pending'
|
|
79
|
-
)
|
|
84
|
+
state = atom<LicenseState>('license state', 'pending')
|
|
80
85
|
public verbose = true
|
|
81
86
|
|
|
82
87
|
constructor(
|
|
@@ -89,21 +94,20 @@ export class LicenseManager {
|
|
|
89
94
|
this.publicKey = testPublicKey || this.publicKey
|
|
90
95
|
this.isCryptoAvailable = !!crypto.subtle
|
|
91
96
|
|
|
92
|
-
this.getLicenseFromKey(licenseKey)
|
|
93
|
-
|
|
97
|
+
this.getLicenseFromKey(licenseKey)
|
|
98
|
+
.then((result) => {
|
|
99
|
+
const licenseState = getLicenseState(result)
|
|
94
100
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
101
|
+
if (!this.isDevelopment && licenseState === 'unlicensed') {
|
|
102
|
+
fetch(WATERMARK_TRACK_SRC)
|
|
103
|
+
}
|
|
98
104
|
|
|
99
|
-
|
|
105
|
+
this.state.set(licenseState)
|
|
106
|
+
})
|
|
107
|
+
.catch((error) => {
|
|
108
|
+
console.error('License validation failed:', error)
|
|
100
109
|
this.state.set('unlicensed')
|
|
101
|
-
}
|
|
102
|
-
this.state.set('licensed-with-watermark')
|
|
103
|
-
} else {
|
|
104
|
-
this.state.set('licensed')
|
|
105
|
-
}
|
|
106
|
-
})
|
|
110
|
+
})
|
|
107
111
|
}
|
|
108
112
|
|
|
109
113
|
private getIsDevelopment(testEnvironment?: TestEnvironment) {
|
|
@@ -367,15 +371,19 @@ export class LicenseManager {
|
|
|
367
371
|
static className = 'tl-watermark_SEE-LICENSE'
|
|
368
372
|
}
|
|
369
373
|
|
|
370
|
-
export function
|
|
371
|
-
if (!result.isLicenseParseable) return
|
|
372
|
-
if (!result.isDomainValid && !result.isDevelopment) return
|
|
374
|
+
export function getLicenseState(result: LicenseFromKeyResult): LicenseState {
|
|
375
|
+
if (!result.isLicenseParseable) return 'unlicensed'
|
|
376
|
+
if (!result.isDomainValid && !result.isDevelopment) return 'unlicensed'
|
|
373
377
|
if (result.isPerpetualLicenseExpired || result.isAnnualLicenseExpired) {
|
|
374
|
-
if
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
+
// Check if it's an expired internal license with valid domain
|
|
379
|
+
const internalExpired = result.isInternalLicense && result.isDomainValid
|
|
380
|
+
return internalExpired ? 'internal-expired' : 'unlicensed'
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// License is valid, determine if it has watermark
|
|
384
|
+
if (result.isLicensedWithWatermark) {
|
|
385
|
+
return 'licensed-with-watermark'
|
|
378
386
|
}
|
|
379
387
|
|
|
380
|
-
return
|
|
388
|
+
return 'licensed'
|
|
381
389
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { useValue } from '@tldraw/state-react'
|
|
1
2
|
import { createContext, ReactNode, useContext, useState } from 'react'
|
|
2
3
|
import { LicenseManager } from './LicenseManager'
|
|
3
4
|
|
|
@@ -16,5 +17,12 @@ export function LicenseProvider({
|
|
|
16
17
|
children: ReactNode
|
|
17
18
|
}) {
|
|
18
19
|
const [licenseManager] = useState(() => new LicenseManager(licenseKey))
|
|
20
|
+
const licenseState = useValue(licenseManager.state)
|
|
21
|
+
|
|
22
|
+
// If internal license has expired, don't render the editor at all
|
|
23
|
+
if (licenseState === 'internal-expired') {
|
|
24
|
+
return <div data-testid="tl-license-expired" style={{ display: 'none' }} />
|
|
25
|
+
}
|
|
26
|
+
|
|
19
27
|
return <LicenseContext.Provider value={licenseManager}>{children}</LicenseContext.Provider>
|
|
20
28
|
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { act, render, waitFor } from '@testing-library/react'
|
|
2
|
+
import { vi } from 'vitest'
|
|
2
3
|
import { TldrawEditor } from '../TldrawEditor'
|
|
3
4
|
import { LicenseManager } from './LicenseManager'
|
|
4
5
|
|
|
5
6
|
let mockLicenseState = 'unlicensed'
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
vi.mock('./useLicenseManagerState', () => ({
|
|
8
9
|
useLicenseManagerState: () => mockLicenseState,
|
|
9
10
|
}))
|
|
10
11
|
|