@tldraw/editor 4.6.0-internal.e29318c66fb0 → 4.6.0-next.0c36db38afea
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 +184 -24
- package/dist-cjs/index.js +5 -4
- package/dist-cjs/index.js.map +2 -2
- package/dist-cjs/lib/TldrawEditor.js +4 -2
- package/dist-cjs/lib/TldrawEditor.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/config/{createTLUser.js → createTLCurrentUser.js} +9 -9
- package/dist-cjs/lib/config/createTLCurrentUser.js.map +7 -0
- package/dist-cjs/lib/config/createTLStore.js +24 -1
- package/dist-cjs/lib/config/createTLStore.js.map +2 -2
- package/dist-cjs/lib/config/defaultShapes.js.map +2 -2
- package/dist-cjs/lib/editor/Editor.js +115 -8
- package/dist-cjs/lib/editor/Editor.js.map +3 -3
- package/dist-cjs/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.js +1 -1
- package/dist-cjs/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.js.map +2 -2
- package/dist-cjs/lib/editor/managers/SnapManager/BoundsSnaps.js +1 -1
- package/dist-cjs/lib/editor/managers/SnapManager/BoundsSnaps.js.map +2 -2
- package/dist-cjs/lib/editor/managers/SnapManager/HandleSnaps.js.map +2 -2
- package/dist-cjs/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.js +4 -4
- package/dist-cjs/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.js.map +2 -2
- package/dist-cjs/lib/editor/managers/TextManager/TextManager.js +102 -30
- package/dist-cjs/lib/editor/managers/TextManager/TextManager.js.map +2 -2
- package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js.map +2 -2
- package/dist-cjs/lib/editor/shapes/ShapeUtil.js +10 -0
- package/dist-cjs/lib/editor/shapes/ShapeUtil.js.map +2 -2
- package/dist-cjs/lib/editor/types/clipboard-types.js.map +1 -1
- package/dist-cjs/lib/exports/StyleEmbedder.js +1 -1
- package/dist-cjs/lib/exports/StyleEmbedder.js.map +2 -2
- package/dist-cjs/lib/exports/getSvgJsx.js.map +2 -2
- package/dist-cjs/lib/hooks/useEditorComponents.js.map +2 -2
- package/dist-cjs/lib/hooks/useGestureEvents.js +171 -127
- package/dist-cjs/lib/hooks/useGestureEvents.js.map +3 -3
- package/dist-cjs/lib/hooks/useLocalStore.js.map +2 -2
- package/dist-cjs/lib/options.js +3 -0
- package/dist-cjs/lib/options.js.map +2 -2
- package/dist-cjs/lib/primitives/Box.js +1 -1
- package/dist-cjs/lib/primitives/Box.js.map +2 -2
- package/dist-cjs/lib/primitives/geometry/Arc2d.js +2 -2
- package/dist-cjs/lib/primitives/geometry/Arc2d.js.map +2 -2
- package/dist-cjs/lib/primitives/geometry/Circle2d.js +2 -2
- package/dist-cjs/lib/primitives/geometry/Circle2d.js.map +2 -2
- package/dist-cjs/lib/primitives/geometry/Ellipse2d.js +2 -2
- package/dist-cjs/lib/primitives/geometry/Ellipse2d.js.map +2 -2
- package/dist-cjs/lib/primitives/geometry/Geometry2d.js +4 -3
- package/dist-cjs/lib/primitives/geometry/Geometry2d.js.map +2 -2
- package/dist-cjs/lib/primitives/geometry/Polyline2d.js +1 -1
- package/dist-cjs/lib/primitives/geometry/Polyline2d.js.map +2 -2
- package/dist-cjs/lib/primitives/geometry/Stadium2d.js +1 -1
- package/dist-cjs/lib/primitives/geometry/Stadium2d.js.map +2 -2
- package/dist-cjs/lib/utils/getSvgPathFromPoints.js.map +2 -2
- package/dist-cjs/lib/utils/sync/TLLocalSyncClient.js +1 -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 +184 -24
- package/dist-esm/index.mjs +12 -5
- package/dist-esm/index.mjs.map +2 -2
- package/dist-esm/lib/TldrawEditor.mjs +4 -2
- package/dist-esm/lib/TldrawEditor.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/config/{createTLUser.mjs → createTLCurrentUser.mjs} +6 -6
- package/dist-esm/lib/config/createTLCurrentUser.mjs.map +7 -0
- package/dist-esm/lib/config/createTLStore.mjs +28 -2
- package/dist-esm/lib/config/createTLStore.mjs.map +2 -2
- package/dist-esm/lib/config/defaultShapes.mjs.map +2 -2
- package/dist-esm/lib/editor/Editor.mjs +118 -9
- package/dist-esm/lib/editor/Editor.mjs.map +3 -3
- package/dist-esm/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.mjs +1 -1
- package/dist-esm/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/SnapManager/BoundsSnaps.mjs +1 -1
- package/dist-esm/lib/editor/managers/SnapManager/BoundsSnaps.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/SnapManager/HandleSnaps.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.mjs +4 -4
- package/dist-esm/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs +102 -30
- package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs.map +2 -2
- package/dist-esm/lib/editor/shapes/ShapeUtil.mjs +10 -0
- package/dist-esm/lib/editor/shapes/ShapeUtil.mjs.map +2 -2
- package/dist-esm/lib/exports/StyleEmbedder.mjs +1 -1
- package/dist-esm/lib/exports/StyleEmbedder.mjs.map +2 -2
- package/dist-esm/lib/exports/getSvgJsx.mjs.map +2 -2
- package/dist-esm/lib/hooks/useEditorComponents.mjs.map +2 -2
- package/dist-esm/lib/hooks/useGestureEvents.mjs +171 -127
- package/dist-esm/lib/hooks/useGestureEvents.mjs.map +3 -3
- package/dist-esm/lib/hooks/useLocalStore.mjs.map +2 -2
- package/dist-esm/lib/options.mjs +3 -0
- package/dist-esm/lib/options.mjs.map +2 -2
- package/dist-esm/lib/primitives/Box.mjs +1 -1
- package/dist-esm/lib/primitives/Box.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/Ellipse2d.mjs +2 -2
- package/dist-esm/lib/primitives/geometry/Ellipse2d.mjs.map +2 -2
- package/dist-esm/lib/primitives/geometry/Geometry2d.mjs +4 -3
- package/dist-esm/lib/primitives/geometry/Geometry2d.mjs.map +2 -2
- package/dist-esm/lib/primitives/geometry/Polyline2d.mjs +1 -1
- package/dist-esm/lib/primitives/geometry/Polyline2d.mjs.map +2 -2
- package/dist-esm/lib/primitives/geometry/Stadium2d.mjs +1 -1
- package/dist-esm/lib/primitives/geometry/Stadium2d.mjs.map +2 -2
- package/dist-esm/lib/utils/getSvgPathFromPoints.mjs.map +2 -2
- package/dist-esm/lib/utils/sync/TLLocalSyncClient.mjs +1 -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 +13 -0
- package/package.json +8 -9
- package/src/index.ts +14 -2
- package/src/lib/TldrawEditor.tsx +9 -7
- package/src/lib/components/default-components/DefaultCanvas.tsx +1 -1
- package/src/lib/components/default-components/DefaultCollaboratorHint.tsx +1 -1
- package/src/lib/config/TLEditorSnapshot.test.ts +1 -1
- package/src/lib/config/{createTLUser.ts → createTLCurrentUser.ts} +6 -6
- package/src/lib/config/createTLStore.ts +36 -2
- package/src/lib/config/defaultShapes.ts +1 -1
- package/src/lib/editor/Editor.ts +148 -10
- package/src/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.ts +1 -1
- package/src/lib/editor/managers/SnapManager/BoundsSnaps.ts +1 -1
- package/src/lib/editor/managers/SnapManager/HandleSnaps.ts +1 -1
- package/src/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.ts +5 -5
- package/src/lib/editor/managers/TextManager/TextManager.test.ts +18 -4
- package/src/lib/editor/managers/TextManager/TextManager.ts +140 -34
- package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.test.ts +2 -2
- package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.ts +2 -2
- package/src/lib/editor/shapes/ShapeUtil.ts +13 -2
- package/src/lib/editor/types/clipboard-types.ts +2 -1
- package/src/lib/exports/StyleEmbedder.ts +1 -1
- package/src/lib/exports/getSvgJsx.tsx +1 -1
- package/src/lib/hooks/useEditorComponents.tsx +1 -1
- package/src/lib/hooks/useGestureEvents.ts +240 -168
- package/src/lib/hooks/useLocalStore.ts +1 -1
- package/src/lib/options.ts +108 -0
- package/src/lib/primitives/Box.test.ts +30 -0
- package/src/lib/primitives/Box.ts +1 -1
- package/src/lib/primitives/geometry/Arc2d.ts +2 -2
- package/src/lib/primitives/geometry/Circle2d.ts +2 -2
- package/src/lib/primitives/geometry/Ellipse2d.ts +2 -2
- package/src/lib/primitives/geometry/Geometry2d.test.ts +21 -0
- package/src/lib/primitives/geometry/Geometry2d.ts +4 -3
- package/src/lib/primitives/geometry/Polyline2d.ts +1 -1
- package/src/lib/primitives/geometry/Stadium2d.ts +1 -1
- package/src/lib/utils/getSvgPathFromPoints.ts +1 -1
- package/src/lib/utils/sync/TLLocalSyncClient.test.ts +1 -1
- package/src/lib/utils/sync/TLLocalSyncClient.ts +1 -1
- package/src/version.ts +3 -3
- package/dist-cjs/lib/config/createTLUser.js.map +0 -7
- package/dist-esm/lib/config/createTLUser.mjs.map +0 -7
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import type { AnyHandlerEventTypes, EventTypes, GestureKey, Handler } from '@use-gesture/core/types'
|
|
2
|
-
import { createUseGesture, pinchAction, wheelAction } from '@use-gesture/react'
|
|
3
1
|
import * as React from 'react'
|
|
4
2
|
import { TLWheelEventInfo } from '../editor/types/event-types'
|
|
3
|
+
import { tlenv } from '../globals/environment'
|
|
5
4
|
import { Vec } from '../primitives/Vec'
|
|
6
5
|
import { preventDefault } from '../utils/dom'
|
|
7
6
|
import { isAccelKey } from '../utils/keyboard'
|
|
@@ -12,27 +11,27 @@ import { useEditor } from './useEditor'
|
|
|
12
11
|
|
|
13
12
|
# How does pinching work?
|
|
14
13
|
|
|
15
|
-
The pinching handler is fired under two circumstances:
|
|
14
|
+
The pinching handler is fired under two circumstances:
|
|
16
15
|
- when a user is on a MacBook trackpad and is ZOOMING with a two-finger pinch
|
|
17
16
|
- when a user is on a touch device and is ZOOMING with a two-finger pinch
|
|
18
17
|
- when a user is on a touch device and is PANNING with two fingers
|
|
19
18
|
|
|
20
|
-
Zooming is much more expensive than panning (because it causes shapes to render),
|
|
21
|
-
so we want to be sure that we don't zoom while two-finger panning.
|
|
19
|
+
Zooming is much more expensive than panning (because it causes shapes to render),
|
|
20
|
+
so we want to be sure that we don't zoom while two-finger panning.
|
|
22
21
|
|
|
23
22
|
In order to do this, we keep track of a "pinchState", which is either:
|
|
24
23
|
- "zooming"
|
|
25
24
|
- "panning"
|
|
26
25
|
- "not sure"
|
|
27
26
|
|
|
28
|
-
If a user is on a trackpad, the pinchState will be set to "zooming".
|
|
27
|
+
If a user is on a trackpad, the pinchState will be set to "zooming".
|
|
29
28
|
|
|
30
29
|
If the user is on a touch screen, then we start in the "not sure" state and switch back and forth
|
|
31
30
|
between "zooming", "panning", and "not sure" based on what the user is doing with their fingers.
|
|
32
31
|
|
|
33
32
|
In the "not sure" state, we examine whether the user has moved the center of the gesture far enough
|
|
34
33
|
to suggest that they're panning; or else that they've moved their fingers further apart or closer
|
|
35
|
-
together enough to suggest that they're zooming.
|
|
34
|
+
together enough to suggest that they're zooming.
|
|
36
35
|
|
|
37
36
|
In the "panning" state, we check whether the user's fingers have moved far enough apart to suggest
|
|
38
37
|
that they're zooming. If they have, we switch to the "zooming" state.
|
|
@@ -42,62 +41,37 @@ In the "zooming" state, we just stay zooming—it's not YET possible to switch b
|
|
|
42
41
|
todo: compare velocities of change in order to determine whether the user has switched back to panning
|
|
43
42
|
*/
|
|
44
43
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
:
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
* UseGesture fires a wheel event 140ms after the gesture actually ends, with a momentum-adjusted
|
|
56
|
-
* delta. This creates a messed up interaction where after you stop scrolling suddenly the dang page
|
|
57
|
-
* jumps a tick. why do they do this? you are asking the wrong person. it seems intentional though.
|
|
58
|
-
* anyway we want to ignore that last event, but there's no way to directly detect it so we need to
|
|
59
|
-
* keep track of timestamps. Yes this is awful, I am sorry.
|
|
60
|
-
*/
|
|
61
|
-
let lastWheelTime = undefined as undefined | number
|
|
62
|
-
|
|
63
|
-
const isWheelEndEvent = (time: number) => {
|
|
64
|
-
if (lastWheelTime === undefined) {
|
|
65
|
-
lastWheelTime = time
|
|
66
|
-
return false
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
if (time - lastWheelTime > 120 && time - lastWheelTime < 160) {
|
|
70
|
-
lastWheelTime = time
|
|
71
|
-
return true
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
lastWheelTime = time
|
|
75
|
-
return false
|
|
44
|
+
/** Safari's non-standard GestureEvent */
|
|
45
|
+
interface GestureEvent extends Event {
|
|
46
|
+
scale: number
|
|
47
|
+
rotation: number
|
|
48
|
+
clientX: number
|
|
49
|
+
clientY: number
|
|
50
|
+
shiftKey: boolean
|
|
51
|
+
altKey: boolean
|
|
52
|
+
metaKey: boolean
|
|
53
|
+
ctrlKey: boolean
|
|
76
54
|
}
|
|
77
55
|
|
|
78
56
|
export function useGestureEvents(ref: React.RefObject<HTMLDivElement | null>) {
|
|
79
57
|
const editor = useEditor()
|
|
80
58
|
|
|
81
|
-
|
|
59
|
+
React.useEffect(() => {
|
|
60
|
+
const elm = ref.current
|
|
61
|
+
if (!elm) return
|
|
62
|
+
|
|
82
63
|
let pinchState = 'not sure' as 'not sure' | 'zooming' | 'panning'
|
|
83
64
|
|
|
84
|
-
|
|
65
|
+
// --- Wheel handling ---
|
|
66
|
+
|
|
67
|
+
function onWheel(event: WheelEvent) {
|
|
85
68
|
if (!editor.getInstanceState().isFocused) {
|
|
86
69
|
return
|
|
87
70
|
}
|
|
88
71
|
|
|
89
72
|
pinchState = 'not sure'
|
|
90
73
|
|
|
91
|
-
|
|
92
|
-
// ignore wheelEnd events
|
|
93
|
-
return
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// Awful tht we need to put this logic here, but basically
|
|
97
|
-
// we don't want to handle the the wheel event (or call prevent
|
|
98
|
-
// default on the evnet) if the user is wheeling over an a shape
|
|
99
|
-
// that is scrollable which they're currently editing.
|
|
100
|
-
|
|
74
|
+
// Don't handle wheel events over a scrollable editing shape
|
|
101
75
|
const editingShapeId = editor.getEditingShapeId()
|
|
102
76
|
if (editingShapeId) {
|
|
103
77
|
const shape = editor.getShape(editingShapeId)
|
|
@@ -133,43 +107,38 @@ export function useGestureEvents(ref: React.RefObject<HTMLDivElement | null>) {
|
|
|
133
107
|
editor.dispatch(info)
|
|
134
108
|
}
|
|
135
109
|
|
|
110
|
+
// --- Touch pinch handling ---
|
|
111
|
+
|
|
136
112
|
let initDistanceBetweenFingers = 1 // the distance between the two fingers when the pinch starts
|
|
137
|
-
let initZoom = 1 // the
|
|
113
|
+
let initZoom = 1 // the zoom level when the pinch starts
|
|
138
114
|
let currDistanceBetweenFingers = 0
|
|
139
115
|
const initPointBetweenFingers = new Vec()
|
|
140
116
|
const prevPointBetweenFingers = new Vec()
|
|
141
117
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
initDistanceBetweenFingers = da[0]
|
|
156
|
-
initZoom = editor.getZoomLevel()
|
|
118
|
+
// Track active touches
|
|
119
|
+
let activeTouches: Touch[] = []
|
|
120
|
+
|
|
121
|
+
function getScaleBounds() {
|
|
122
|
+
const baseZoom = editor.getBaseZoom()
|
|
123
|
+
const { zoomSteps, zoomSpeed } = editor.getCameraOptions()
|
|
124
|
+
const zoomMin = zoomSteps[0] * baseZoom
|
|
125
|
+
const zoomMax = zoomSteps[zoomSteps.length - 1] * baseZoom
|
|
126
|
+
return {
|
|
127
|
+
min: zoomMin ** (1 / zoomSpeed),
|
|
128
|
+
max: zoomMax ** (1 / zoomSpeed),
|
|
129
|
+
}
|
|
130
|
+
}
|
|
157
131
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
point: { x: origin[0], y: origin[1], z: editor.getZoomLevel() },
|
|
162
|
-
delta: { x: 0, y: 0 },
|
|
163
|
-
shiftKey: event.shiftKey,
|
|
164
|
-
altKey: event.altKey,
|
|
165
|
-
ctrlKey: event.metaKey || event.ctrlKey,
|
|
166
|
-
metaKey: event.metaKey,
|
|
167
|
-
accelKey: isAccelKey(event),
|
|
168
|
-
})
|
|
132
|
+
function getScaleFrom() {
|
|
133
|
+
const { zoomSpeed } = editor.getCameraOptions()
|
|
134
|
+
return editor.getZoomLevel() ** (1 / zoomSpeed)
|
|
169
135
|
}
|
|
170
136
|
|
|
171
|
-
//
|
|
172
|
-
|
|
137
|
+
// Accumulated scale offset, clamped to bounds — replaces @use-gesture's offset[0]
|
|
138
|
+
let scaleOffset = 1
|
|
139
|
+
let initScaleFrom = 1 // the scale-space zoom level when the pinch started
|
|
140
|
+
|
|
141
|
+
function updatePinchState(isSafariTrackpadPinch: boolean) {
|
|
173
142
|
if (isSafariTrackpadPinch) {
|
|
174
143
|
pinchState = 'zooming'
|
|
175
144
|
}
|
|
@@ -183,7 +152,7 @@ export function useGestureEvents(ref: React.RefObject<HTMLDivElement | null>) {
|
|
|
183
152
|
// |----| |------------|
|
|
184
153
|
// originDistance ^ ^ touchDistance
|
|
185
154
|
|
|
186
|
-
// How far have the two touch points moved towards or away from
|
|
155
|
+
// How far have the two touch points moved towards or away from each other?
|
|
187
156
|
const touchDistance = Math.abs(currDistanceBetweenFingers - initDistanceBetweenFingers)
|
|
188
157
|
// How far has the point between the touches moved?
|
|
189
158
|
const originDistance = Vec.Dist(initPointBetweenFingers, prevPointBetweenFingers)
|
|
@@ -207,123 +176,226 @@ export function useGestureEvents(ref: React.RefObject<HTMLDivElement | null>) {
|
|
|
207
176
|
}
|
|
208
177
|
}
|
|
209
178
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
179
|
+
function dispatchPinchEvent(
|
|
180
|
+
name: 'pinch_start' | 'pinch' | 'pinch_end',
|
|
181
|
+
origin: { x: number; y: number },
|
|
182
|
+
delta: { x: number; y: number },
|
|
183
|
+
zoom: number,
|
|
184
|
+
event: TouchEvent | GestureEvent
|
|
185
|
+
) {
|
|
186
|
+
editor.dispatch({
|
|
187
|
+
type: 'pinch',
|
|
188
|
+
name,
|
|
189
|
+
point: { x: origin.x, y: origin.y, z: zoom },
|
|
190
|
+
delta,
|
|
191
|
+
shiftKey: event.shiftKey,
|
|
192
|
+
altKey: event.altKey,
|
|
193
|
+
ctrlKey: event.metaKey || event.ctrlKey,
|
|
194
|
+
metaKey: event.metaKey,
|
|
195
|
+
accelKey: isAccelKey(event),
|
|
196
|
+
})
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function getOriginAndDistance(t0: Touch, t1: Touch) {
|
|
200
|
+
const origin = {
|
|
201
|
+
x: (t0.clientX + t1.clientX) / 2,
|
|
202
|
+
y: (t0.clientY + t1.clientY) / 2,
|
|
203
|
+
}
|
|
204
|
+
const distance = Math.hypot(t1.clientX - t0.clientX, t1.clientY - t0.clientY)
|
|
205
|
+
return { origin, distance }
|
|
206
|
+
}
|
|
213
207
|
|
|
214
|
-
|
|
208
|
+
function onTouchStart(event: TouchEvent) {
|
|
215
209
|
if (!(event.target === elm || elm?.contains(event.target as Node))) return
|
|
216
210
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
211
|
+
activeTouches = Array.from(event.touches)
|
|
212
|
+
|
|
213
|
+
if (activeTouches.length === 2) {
|
|
214
|
+
// Two fingers down — start pinch
|
|
215
|
+
pinchState = 'not sure'
|
|
216
|
+
const { origin, distance } = getOriginAndDistance(activeTouches[0], activeTouches[1])
|
|
217
|
+
|
|
218
|
+
prevPointBetweenFingers.x = origin.x
|
|
219
|
+
prevPointBetweenFingers.y = origin.y
|
|
220
|
+
initPointBetweenFingers.x = origin.x
|
|
221
|
+
initPointBetweenFingers.y = origin.y
|
|
222
|
+
initDistanceBetweenFingers = Math.max(distance, 1)
|
|
223
|
+
currDistanceBetweenFingers = distance
|
|
224
|
+
initZoom = editor.getZoomLevel()
|
|
225
|
+
initScaleFrom = getScaleFrom()
|
|
226
|
+
scaleOffset = initScaleFrom
|
|
222
227
|
|
|
223
|
-
|
|
224
|
-
|
|
228
|
+
dispatchPinchEvent('pinch_start', origin, { x: 0, y: 0 }, editor.getZoomLevel(), event)
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function onTouchMove(event: TouchEvent) {
|
|
233
|
+
activeTouches = Array.from(event.touches)
|
|
234
|
+
|
|
235
|
+
if (activeTouches.length < 2) return
|
|
236
|
+
|
|
237
|
+
const { origin, distance } = getOriginAndDistance(activeTouches[0], activeTouches[1])
|
|
238
|
+
currDistanceBetweenFingers = distance
|
|
239
|
+
|
|
240
|
+
const dx = origin.x - prevPointBetweenFingers.x
|
|
241
|
+
const dy = origin.y - prevPointBetweenFingers.y
|
|
242
|
+
|
|
243
|
+
prevPointBetweenFingers.x = origin.x
|
|
244
|
+
prevPointBetweenFingers.y = origin.y
|
|
245
|
+
|
|
246
|
+
updatePinchState(false)
|
|
225
247
|
|
|
226
248
|
// Only update the zoom if the pointers are far enough apart;
|
|
227
249
|
// a very small touchDistance means that the user has probably
|
|
228
250
|
// pinched out and their fingers are touching; this produces
|
|
229
251
|
// very unstable zooming behavior.
|
|
230
|
-
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
prevPointBetweenFingers.x = origin[0]
|
|
235
|
-
prevPointBetweenFingers.y = origin[1]
|
|
236
|
-
|
|
237
|
-
updatePinchState(isSafariTrackpadPinch)
|
|
252
|
+
const bounds = getScaleBounds()
|
|
253
|
+
const rawScale = initScaleFrom * (distance / initDistanceBetweenFingers)
|
|
254
|
+
scaleOffset = Math.min(bounds.max, Math.max(bounds.min, rawScale))
|
|
238
255
|
|
|
239
256
|
switch (pinchState) {
|
|
240
257
|
case 'zooming': {
|
|
241
|
-
const currZoom =
|
|
242
|
-
|
|
243
|
-
editor.dispatch({
|
|
244
|
-
type: 'pinch',
|
|
245
|
-
name: 'pinch',
|
|
246
|
-
point: { x: origin[0], y: origin[1], z: currZoom },
|
|
247
|
-
delta: { x: dx, y: dy },
|
|
248
|
-
shiftKey: event.shiftKey,
|
|
249
|
-
altKey: event.altKey,
|
|
250
|
-
ctrlKey: event.metaKey || event.ctrlKey,
|
|
251
|
-
metaKey: event.metaKey,
|
|
252
|
-
accelKey: isAccelKey(event),
|
|
253
|
-
})
|
|
258
|
+
const currZoom = scaleOffset ** editor.getCameraOptions().zoomSpeed
|
|
259
|
+
dispatchPinchEvent('pinch', origin, { x: dx, y: dy }, currZoom, event)
|
|
254
260
|
break
|
|
255
261
|
}
|
|
256
262
|
case 'panning': {
|
|
257
|
-
|
|
258
|
-
type: 'pinch',
|
|
259
|
-
name: 'pinch',
|
|
260
|
-
point: { x: origin[0], y: origin[1], z: initZoom },
|
|
261
|
-
delta: { x: dx, y: dy },
|
|
262
|
-
shiftKey: event.shiftKey,
|
|
263
|
-
altKey: event.altKey,
|
|
264
|
-
ctrlKey: event.metaKey || event.ctrlKey,
|
|
265
|
-
metaKey: event.metaKey,
|
|
266
|
-
accelKey: isAccelKey(event),
|
|
267
|
-
})
|
|
263
|
+
dispatchPinchEvent('pinch', origin, { x: dx, y: dy }, initZoom, event)
|
|
268
264
|
break
|
|
269
265
|
}
|
|
270
266
|
}
|
|
271
267
|
}
|
|
272
268
|
|
|
273
|
-
|
|
274
|
-
const
|
|
275
|
-
|
|
269
|
+
function onTouchEnd(event: TouchEvent) {
|
|
270
|
+
const wasPinching = activeTouches.length >= 2
|
|
271
|
+
activeTouches = Array.from(event.touches)
|
|
276
272
|
|
|
277
|
-
if (
|
|
278
|
-
|
|
273
|
+
if (wasPinching && activeTouches.length < 2) {
|
|
274
|
+
// Pinch ended
|
|
275
|
+
const scale = scaleOffset ** editor.getCameraOptions().zoomSpeed
|
|
276
|
+
const origin = { ...prevPointBetweenFingers }
|
|
277
|
+
pinchState = 'not sure'
|
|
278
|
+
|
|
279
|
+
editor.timers.requestAnimationFrame(() => {
|
|
280
|
+
dispatchPinchEvent('pinch_end', origin, { x: origin.x, y: origin.y }, scale, event)
|
|
281
|
+
})
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// --- Safari trackpad pinch (GestureEvent) ---
|
|
286
|
+
|
|
287
|
+
let safariGestureInitialScale = 1
|
|
288
|
+
|
|
289
|
+
function onGestureStart(event: Event) {
|
|
290
|
+
const e = event as GestureEvent
|
|
291
|
+
if (!(e.target === elm || elm?.contains(e.target as Node))) return
|
|
292
|
+
|
|
293
|
+
preventDefault(e)
|
|
294
|
+
e.stopPropagation()
|
|
295
|
+
|
|
296
|
+
pinchState = 'not sure'
|
|
297
|
+
safariGestureInitialScale = getScaleFrom()
|
|
298
|
+
scaleOffset = safariGestureInitialScale
|
|
299
|
+
initZoom = editor.getZoomLevel()
|
|
300
|
+
|
|
301
|
+
prevPointBetweenFingers.x = e.clientX
|
|
302
|
+
prevPointBetweenFingers.y = e.clientY
|
|
303
|
+
initPointBetweenFingers.x = e.clientX
|
|
304
|
+
initPointBetweenFingers.y = e.clientY
|
|
305
|
+
initDistanceBetweenFingers = 1
|
|
306
|
+
currDistanceBetweenFingers = 1
|
|
307
|
+
|
|
308
|
+
dispatchPinchEvent(
|
|
309
|
+
'pinch_start',
|
|
310
|
+
{ x: e.clientX, y: e.clientY },
|
|
311
|
+
{ x: 0, y: 0 },
|
|
312
|
+
editor.getZoomLevel(),
|
|
313
|
+
e
|
|
314
|
+
)
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function onGestureChange(event: Event) {
|
|
318
|
+
const e = event as GestureEvent
|
|
319
|
+
if (!(e.target === elm || elm?.contains(e.target as Node))) return
|
|
320
|
+
|
|
321
|
+
preventDefault(e)
|
|
322
|
+
e.stopPropagation()
|
|
323
|
+
|
|
324
|
+
const dx = e.clientX - prevPointBetweenFingers.x
|
|
325
|
+
const dy = e.clientY - prevPointBetweenFingers.y
|
|
326
|
+
|
|
327
|
+
prevPointBetweenFingers.x = e.clientX
|
|
328
|
+
prevPointBetweenFingers.y = e.clientY
|
|
329
|
+
|
|
330
|
+
// Safari GestureEvent.scale is a multiplier relative to gesture start
|
|
331
|
+
const bounds = getScaleBounds()
|
|
332
|
+
const rawScale = safariGestureInitialScale * e.scale
|
|
333
|
+
scaleOffset = Math.min(bounds.max, Math.max(bounds.min, rawScale))
|
|
334
|
+
|
|
335
|
+
// Update distance tracking for pinch state (treat scale change as distance change)
|
|
336
|
+
currDistanceBetweenFingers = e.scale * initDistanceBetweenFingers
|
|
337
|
+
|
|
338
|
+
updatePinchState(true)
|
|
279
339
|
|
|
280
|
-
const
|
|
340
|
+
const currZoom = scaleOffset ** editor.getCameraOptions().zoomSpeed
|
|
281
341
|
|
|
342
|
+
dispatchPinchEvent('pinch', { x: e.clientX, y: e.clientY }, { x: dx, y: dy }, currZoom, e)
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function onGestureEnd(event: Event) {
|
|
346
|
+
const e = event as GestureEvent
|
|
347
|
+
if (!(e.target === elm || elm?.contains(e.target as Node))) return
|
|
348
|
+
|
|
349
|
+
preventDefault(e)
|
|
350
|
+
e.stopPropagation()
|
|
351
|
+
|
|
352
|
+
const scale = scaleOffset ** editor.getCameraOptions().zoomSpeed
|
|
282
353
|
pinchState = 'not sure'
|
|
283
354
|
|
|
284
355
|
editor.timers.requestAnimationFrame(() => {
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
ctrlKey: event.metaKey || event.ctrlKey,
|
|
293
|
-
metaKey: event.metaKey,
|
|
294
|
-
accelKey: isAccelKey(event),
|
|
295
|
-
})
|
|
356
|
+
dispatchPinchEvent(
|
|
357
|
+
'pinch_end',
|
|
358
|
+
{ x: e.clientX, y: e.clientY },
|
|
359
|
+
{ x: e.clientX, y: e.clientY },
|
|
360
|
+
scale,
|
|
361
|
+
e
|
|
362
|
+
)
|
|
296
363
|
})
|
|
297
364
|
}
|
|
298
365
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
366
|
+
// --- Attach event listeners ---
|
|
367
|
+
|
|
368
|
+
elm.addEventListener('wheel', onWheel, { passive: false })
|
|
369
|
+
|
|
370
|
+
// On touch devices (iOS), use pointer events for pinch.
|
|
371
|
+
// On non-touch Safari (macOS trackpad), use GestureEvent.
|
|
372
|
+
// Never use both simultaneously — on iOS Safari, both event types fire
|
|
373
|
+
// for the same pinch gesture, causing conflicting state updates.
|
|
374
|
+
const useGestureEvents = !tlenv.isIos && 'GestureEvent' in window
|
|
375
|
+
|
|
376
|
+
if (useGestureEvents) {
|
|
377
|
+
elm.addEventListener('gesturestart', onGestureStart)
|
|
378
|
+
elm.addEventListener('gesturechange', onGestureChange)
|
|
379
|
+
elm.addEventListener('gestureend', onGestureEnd)
|
|
380
|
+
} else {
|
|
381
|
+
elm.addEventListener('touchstart', onTouchStart)
|
|
382
|
+
elm.addEventListener('touchmove', onTouchMove)
|
|
383
|
+
elm.addEventListener('touchend', onTouchEnd)
|
|
384
|
+
elm.addEventListener('touchcancel', onTouchEnd)
|
|
304
385
|
}
|
|
305
|
-
}, [editor, ref])
|
|
306
386
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
return {
|
|
323
|
-
max: zoomMax ** (1 / zoomSpeed),
|
|
324
|
-
min: zoomMin ** (1 / zoomSpeed),
|
|
325
|
-
}
|
|
326
|
-
},
|
|
327
|
-
},
|
|
328
|
-
})
|
|
387
|
+
return () => {
|
|
388
|
+
elm.removeEventListener('wheel', onWheel)
|
|
389
|
+
if (useGestureEvents) {
|
|
390
|
+
elm.removeEventListener('gesturestart', onGestureStart)
|
|
391
|
+
elm.removeEventListener('gesturechange', onGestureChange)
|
|
392
|
+
elm.removeEventListener('gestureend', onGestureEnd)
|
|
393
|
+
} else {
|
|
394
|
+
elm.removeEventListener('touchstart', onTouchStart)
|
|
395
|
+
elm.removeEventListener('touchmove', onTouchMove)
|
|
396
|
+
elm.removeEventListener('touchend', onTouchEnd)
|
|
397
|
+
elm.removeEventListener('touchcancel', onTouchEnd)
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}, [editor, ref])
|
|
329
401
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { TLAsset, TLAssetStore, TLStoreSnapshot } from '@tldraw/tlschema'
|
|
2
2
|
import { WeakCache } from '@tldraw/utils'
|
|
3
3
|
import { useEffect } from 'react'
|
|
4
|
-
import { TLEditorSnapshot } from '../config/TLEditorSnapshot'
|
|
5
4
|
import { TLStoreOptions, createTLStore } from '../config/createTLStore'
|
|
5
|
+
import { TLEditorSnapshot } from '../config/TLEditorSnapshot'
|
|
6
6
|
import { TLStoreWithStatus } from '../utils/sync/StoreWithStatus'
|
|
7
7
|
import { TLLocalSyncClient } from '../utils/sync/TLLocalSyncClient'
|
|
8
8
|
import { useShallowObjectIdentity } from './useIdentity'
|
package/src/lib/options.ts
CHANGED
|
@@ -1,10 +1,50 @@
|
|
|
1
|
+
import { Awaitable } from '@tldraw/utils'
|
|
1
2
|
import { ComponentType, Fragment } from 'react'
|
|
2
3
|
import { DEFAULT_CAMERA_OPTIONS } from './constants'
|
|
4
|
+
import type { Editor } from './editor/Editor'
|
|
5
|
+
import { TLContent } from './editor/types/clipboard-types'
|
|
6
|
+
import { TLExternalContent } from './editor/types/external-content'
|
|
3
7
|
import { TLCameraOptions } from './editor/types/misc-types'
|
|
4
8
|
import { VecLike } from './primitives/Vec'
|
|
5
9
|
import { TLDeepLinkOptions } from './utils/deepLinks'
|
|
6
10
|
import { TLTextOptions } from './utils/richText'
|
|
7
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Identifies how a clipboard write was triggered (copy vs cut, keyboard vs menu).
|
|
14
|
+
*
|
|
15
|
+
* @public
|
|
16
|
+
*/
|
|
17
|
+
export interface TLClipboardWriteInfo {
|
|
18
|
+
readonly operation: 'copy' | 'cut'
|
|
19
|
+
readonly source: 'native' | 'menu'
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Raw clipboard paste payload, before tldraw parses clipboard contents into {@link TLExternalContent}.
|
|
24
|
+
*
|
|
25
|
+
* - `native-event`: from the `paste` event — `clipboardData` is available synchronously (unlike async
|
|
26
|
+
* `navigator.clipboard.read()`).
|
|
27
|
+
* - `clipboard-read`: from an explicit `navigator.clipboard.read()` call — only `ClipboardItem[]`
|
|
28
|
+
* exists
|
|
29
|
+
* (no `DataTransfer`).
|
|
30
|
+
*
|
|
31
|
+
* @public
|
|
32
|
+
*/
|
|
33
|
+
export type TLClipboardPasteRawInfo =
|
|
34
|
+
| {
|
|
35
|
+
readonly editor: Editor
|
|
36
|
+
readonly source: 'native-event'
|
|
37
|
+
readonly event: ClipboardEvent
|
|
38
|
+
readonly clipboardData: DataTransfer | null
|
|
39
|
+
readonly point: VecLike | undefined
|
|
40
|
+
}
|
|
41
|
+
| {
|
|
42
|
+
readonly editor: Editor
|
|
43
|
+
readonly source: 'clipboard-read'
|
|
44
|
+
readonly clipboardItems: readonly ClipboardItem[]
|
|
45
|
+
readonly point: VecLike | undefined
|
|
46
|
+
}
|
|
47
|
+
|
|
8
48
|
/**
|
|
9
49
|
* Options for configuring tldraw. For defaults, see {@link defaultTldrawOptions}.
|
|
10
50
|
*
|
|
@@ -156,6 +196,71 @@ export interface TldrawOptions {
|
|
|
156
196
|
* viewport's page dimensions regardless of overview zoom changes.
|
|
157
197
|
*/
|
|
158
198
|
readonly quickZoomPreservesScreenBounds: boolean
|
|
199
|
+
/**
|
|
200
|
+
* Called before content is written to the clipboard during a copy or cut operation.
|
|
201
|
+
* Receives the serialized content (shapes, bindings, assets) and can filter or transform
|
|
202
|
+
* it before it reaches the clipboard.
|
|
203
|
+
*
|
|
204
|
+
* Return a modified `TLContent` object to change what is copied or cut. Return `false` to
|
|
205
|
+
* cancel the clipboard write (for cut, the selected shapes are not removed). Return `void`
|
|
206
|
+
* (or `undefined`) to pass through unchanged. You may return a `Promise` of those values if
|
|
207
|
+
* the hook is async.
|
|
208
|
+
*
|
|
209
|
+
* @example
|
|
210
|
+
* ```tsx
|
|
211
|
+
* // Filter out "locked" shapes from copy
|
|
212
|
+
* onBeforeCopyToClipboard({ content, operation }) {
|
|
213
|
+
* return {
|
|
214
|
+
* ...content,
|
|
215
|
+
* shapes: content.shapes.filter(s => !s.meta.locked),
|
|
216
|
+
* rootShapeIds: content.rootShapeIds.filter(id =>
|
|
217
|
+
* content.shapes.find(s => s.id === id && !s.meta.locked)
|
|
218
|
+
* ),
|
|
219
|
+
* }
|
|
220
|
+
* }
|
|
221
|
+
* ```
|
|
222
|
+
*/
|
|
223
|
+
onBeforeCopyToClipboard?(
|
|
224
|
+
info: { editor: Editor; content: TLContent } & TLClipboardWriteInfo
|
|
225
|
+
): Awaitable<TLContent | false | void>
|
|
226
|
+
/**
|
|
227
|
+
* Called before pasted content is processed and shapes are created. Receives the parsed
|
|
228
|
+
* external content from the clipboard and can filter, transform, or cancel it.
|
|
229
|
+
*
|
|
230
|
+
* Return `false` to cancel the paste. Return a modified content object to transform it.
|
|
231
|
+
* Return `void` (or `undefined`) to pass through unchanged. You may return a `Promise` of
|
|
232
|
+
* those values if the hook is async.
|
|
233
|
+
*
|
|
234
|
+
* This only fires for clipboard paste operations (keyboard shortcuts and menu actions),
|
|
235
|
+
* not for file drops or programmatic `putExternalContent` calls.
|
|
236
|
+
*
|
|
237
|
+
* @example
|
|
238
|
+
* ```tsx
|
|
239
|
+
* // Block pasting of image files
|
|
240
|
+
* onBeforePasteFromClipboard({ content }) {
|
|
241
|
+
* if (content.type === 'files') {
|
|
242
|
+
* const nonImages = content.files.filter(f => !f.type.startsWith('image/'))
|
|
243
|
+
* if (nonImages.length === 0) return false
|
|
244
|
+
* return { ...content, files: nonImages }
|
|
245
|
+
* }
|
|
246
|
+
* }
|
|
247
|
+
* ```
|
|
248
|
+
*/
|
|
249
|
+
onBeforePasteFromClipboard?(info: {
|
|
250
|
+
editor: Editor
|
|
251
|
+
content: TLExternalContent<unknown>
|
|
252
|
+
source: 'native-event' | 'clipboard-read'
|
|
253
|
+
point?: VecLike
|
|
254
|
+
}): Awaitable<TLExternalContent<unknown> | false | void>
|
|
255
|
+
/**
|
|
256
|
+
* Called first for keyboard and menu paste, **before** tldraw handles or parses clipboard data
|
|
257
|
+
* (and before {@link TldrawOptions.onBeforePasteFromClipboard}).
|
|
258
|
+
*
|
|
259
|
+
* Return `false` to cancel tldraw's default paste handling for this gesture (same convention as
|
|
260
|
+
* {@link TldrawOptions.onBeforePasteFromClipboard}). Use this when you handle paste yourself from
|
|
261
|
+
* raw clipboard data, or to block the gesture entirely. Return `void` (or `undefined`) to continue.
|
|
262
|
+
*/
|
|
263
|
+
onClipboardPasteRaw?(info: TLClipboardPasteRawInfo): false | void
|
|
159
264
|
/**
|
|
160
265
|
* Called when content is dropped on the canvas. Provides the page position
|
|
161
266
|
* where the drop occurred and the underlying drag event object.
|
|
@@ -227,5 +332,8 @@ export const defaultTldrawOptions = {
|
|
|
227
332
|
text: {},
|
|
228
333
|
deepLinks: undefined,
|
|
229
334
|
quickZoomPreservesScreenBounds: true,
|
|
335
|
+
onBeforeCopyToClipboard: undefined,
|
|
336
|
+
onBeforePasteFromClipboard: undefined,
|
|
337
|
+
onClipboardPasteRaw: undefined,
|
|
230
338
|
experimental__onDropOnCanvas: undefined,
|
|
231
339
|
} as const satisfies TldrawOptions
|