@tldraw/editor 5.4.0-next.ebc95cf01337 → 5.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist-cjs/index.d.ts +67 -1
- package/dist-cjs/index.js +4 -1
- package/dist-cjs/index.js.map +2 -2
- package/dist-cjs/lib/TldrawEditor.js +7 -2
- package/dist-cjs/lib/TldrawEditor.js.map +2 -2
- package/dist-cjs/lib/components/MenuClickCapture.js +6 -8
- package/dist-cjs/lib/components/MenuClickCapture.js.map +2 -2
- package/dist-cjs/lib/components/default-components/CanvasOverlays.js +1 -3
- package/dist-cjs/lib/components/default-components/CanvasOverlays.js.map +2 -2
- package/dist-cjs/lib/components/default-components/DefaultErrorFallback.js +1 -1
- package/dist-cjs/lib/components/default-components/DefaultErrorFallback.js.map +1 -1
- package/dist-cjs/lib/config/TLUserPreferences.js +6 -2
- package/dist-cjs/lib/config/TLUserPreferences.js.map +2 -2
- package/dist-cjs/lib/config/createTLStore.js +1 -1
- package/dist-cjs/lib/config/createTLStore.js.map +2 -2
- package/dist-cjs/lib/editor/Editor.js +164 -280
- package/dist-cjs/lib/editor/Editor.js.map +2 -2
- package/dist-cjs/lib/editor/managers/ClickManager/ClickManager.js +3 -3
- package/dist-cjs/lib/editor/managers/ClickManager/ClickManager.js.map +2 -2
- package/dist-cjs/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.js +8 -8
- package/dist-cjs/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.js.map +2 -2
- package/dist-cjs/lib/editor/managers/FontManager/FontManager.js +2 -1
- package/dist-cjs/lib/editor/managers/FontManager/FontManager.js.map +2 -2
- package/dist-cjs/lib/editor/managers/InputsManager/InputsManager.js +79 -6
- package/dist-cjs/lib/editor/managers/InputsManager/InputsManager.js.map +2 -2
- package/dist-cjs/lib/editor/managers/ScribbleManager/ScribbleManager.js +7 -0
- package/dist-cjs/lib/editor/managers/ScribbleManager/ScribbleManager.js.map +2 -2
- package/dist-cjs/lib/editor/managers/SnapManager/BoundsSnaps.js +0 -2
- package/dist-cjs/lib/editor/managers/SnapManager/BoundsSnaps.js.map +2 -2
- package/dist-cjs/lib/editor/managers/TextManager/TextManager.js +1 -2
- package/dist-cjs/lib/editor/managers/TextManager/TextManager.js.map +2 -2
- package/dist-cjs/lib/editor/managers/TickManager/TickManager.js +6 -3
- package/dist-cjs/lib/editor/managers/TickManager/TickManager.js.map +2 -2
- package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js +12 -1
- package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js.map +2 -2
- package/dist-cjs/lib/editor/overlays/OverlayManager.js +3 -7
- package/dist-cjs/lib/editor/overlays/OverlayManager.js.map +2 -2
- package/dist-cjs/lib/editor/overlays/strokeShapeIndicators.js +0 -1
- package/dist-cjs/lib/editor/overlays/strokeShapeIndicators.js.map +2 -2
- package/dist-cjs/lib/editor/shapes/BaseFrameLikeShapeUtil.js +3 -1
- package/dist-cjs/lib/editor/shapes/BaseFrameLikeShapeUtil.js.map +2 -2
- package/dist-cjs/lib/editor/shapes/shared/getPerfectDashProps.js +1 -0
- package/dist-cjs/lib/editor/shapes/shared/getPerfectDashProps.js.map +2 -2
- package/dist-cjs/lib/editor/tools/BaseBoxShapeTool/children/Pointing.js +1 -1
- package/dist-cjs/lib/editor/tools/BaseBoxShapeTool/children/Pointing.js.map +2 -2
- package/dist-cjs/lib/editor/tools/StateNode.js +3 -2
- package/dist-cjs/lib/editor/tools/StateNode.js.map +2 -2
- package/dist-cjs/lib/globals/editors.js +57 -0
- package/dist-cjs/lib/globals/editors.js.map +7 -0
- package/dist-cjs/lib/globals/menus.js +9 -4
- package/dist-cjs/lib/globals/menus.js.map +2 -2
- package/dist-cjs/lib/hooks/useCanvasEvents.js +2 -0
- package/dist-cjs/lib/hooks/useCanvasEvents.js.map +2 -2
- package/dist-cjs/lib/hooks/useDocumentEvents.js +39 -9
- package/dist-cjs/lib/hooks/useDocumentEvents.js.map +2 -2
- package/dist-cjs/lib/hooks/useLocalStore.js +10 -6
- package/dist-cjs/lib/hooks/useLocalStore.js.map +2 -2
- package/dist-cjs/lib/hooks/usePassThroughWheelEvents.js +1 -0
- package/dist-cjs/lib/hooks/usePassThroughWheelEvents.js.map +2 -2
- package/dist-cjs/lib/hooks/usePeerIds.js +2 -1
- package/dist-cjs/lib/hooks/usePeerIds.js.map +2 -2
- package/dist-cjs/lib/license/LicenseManager.js +9 -2
- package/dist-cjs/lib/license/LicenseManager.js.map +2 -2
- package/dist-cjs/lib/license/LicenseProvider.js +16 -3
- package/dist-cjs/lib/license/LicenseProvider.js.map +2 -2
- package/dist-cjs/lib/license/useLicenseManagerState.js +5 -4
- package/dist-cjs/lib/license/useLicenseManagerState.js.map +2 -2
- package/dist-cjs/lib/primitives/Box.js +2 -1
- package/dist-cjs/lib/primitives/Box.js.map +2 -2
- package/dist-cjs/lib/primitives/Vec.js +4 -3
- package/dist-cjs/lib/primitives/Vec.js.map +2 -2
- package/dist-cjs/lib/primitives/utils.js +2 -1
- package/dist-cjs/lib/primitives/utils.js.map +2 -2
- package/dist-cjs/lib/utils/deepLinks.js +5 -1
- package/dist-cjs/lib/utils/deepLinks.js.map +2 -2
- package/dist-cjs/lib/utils/rotation.js +1 -9
- package/dist-cjs/lib/utils/rotation.js.map +2 -2
- package/dist-cjs/lib/utils/sync/TLLocalSyncClient.js +17 -0
- package/dist-cjs/lib/utils/sync/TLLocalSyncClient.js.map +2 -2
- package/dist-cjs/lib/utils/sync/alerts.js +1 -1
- package/dist-cjs/lib/utils/sync/alerts.js.map +1 -1
- package/dist-cjs/version.js +3 -3
- package/dist-cjs/version.js.map +1 -1
- package/dist-esm/index.d.mts +67 -1
- package/dist-esm/index.mjs +9 -2
- package/dist-esm/index.mjs.map +2 -2
- package/dist-esm/lib/TldrawEditor.mjs +9 -4
- package/dist-esm/lib/TldrawEditor.mjs.map +2 -2
- package/dist-esm/lib/components/MenuClickCapture.mjs +6 -8
- package/dist-esm/lib/components/MenuClickCapture.mjs.map +2 -2
- package/dist-esm/lib/components/default-components/CanvasOverlays.mjs +1 -3
- package/dist-esm/lib/components/default-components/CanvasOverlays.mjs.map +2 -2
- package/dist-esm/lib/components/default-components/DefaultErrorFallback.mjs +1 -1
- package/dist-esm/lib/components/default-components/DefaultErrorFallback.mjs.map +1 -1
- package/dist-esm/lib/config/TLUserPreferences.mjs +6 -2
- package/dist-esm/lib/config/TLUserPreferences.mjs.map +2 -2
- package/dist-esm/lib/config/createTLStore.mjs +1 -1
- package/dist-esm/lib/config/createTLStore.mjs.map +2 -2
- package/dist-esm/lib/editor/Editor.mjs +172 -281
- package/dist-esm/lib/editor/Editor.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/ClickManager/ClickManager.mjs +3 -3
- package/dist-esm/lib/editor/managers/ClickManager/ClickManager.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.mjs +9 -9
- package/dist-esm/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/FontManager/FontManager.mjs +2 -1
- package/dist-esm/lib/editor/managers/FontManager/FontManager.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/InputsManager/InputsManager.mjs +81 -8
- package/dist-esm/lib/editor/managers/InputsManager/InputsManager.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/ScribbleManager/ScribbleManager.mjs +7 -0
- package/dist-esm/lib/editor/managers/ScribbleManager/ScribbleManager.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/SnapManager/BoundsSnaps.mjs +0 -2
- package/dist-esm/lib/editor/managers/SnapManager/BoundsSnaps.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs +2 -3
- package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/TickManager/TickManager.mjs +6 -3
- package/dist-esm/lib/editor/managers/TickManager/TickManager.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs +12 -1
- package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs.map +2 -2
- package/dist-esm/lib/editor/overlays/OverlayManager.mjs +3 -7
- package/dist-esm/lib/editor/overlays/OverlayManager.mjs.map +2 -2
- package/dist-esm/lib/editor/overlays/strokeShapeIndicators.mjs +0 -1
- package/dist-esm/lib/editor/overlays/strokeShapeIndicators.mjs.map +2 -2
- package/dist-esm/lib/editor/shapes/BaseFrameLikeShapeUtil.mjs +3 -1
- package/dist-esm/lib/editor/shapes/BaseFrameLikeShapeUtil.mjs.map +2 -2
- package/dist-esm/lib/editor/shapes/shared/getPerfectDashProps.mjs +1 -0
- package/dist-esm/lib/editor/shapes/shared/getPerfectDashProps.mjs.map +2 -2
- package/dist-esm/lib/editor/tools/BaseBoxShapeTool/children/Pointing.mjs +1 -1
- package/dist-esm/lib/editor/tools/BaseBoxShapeTool/children/Pointing.mjs.map +2 -2
- package/dist-esm/lib/editor/tools/StateNode.mjs +3 -2
- package/dist-esm/lib/editor/tools/StateNode.mjs.map +2 -2
- package/dist-esm/lib/globals/editors.mjs +37 -0
- package/dist-esm/lib/globals/editors.mjs.map +7 -0
- package/dist-esm/lib/globals/menus.mjs +9 -4
- package/dist-esm/lib/globals/menus.mjs.map +2 -2
- package/dist-esm/lib/hooks/useCanvasEvents.mjs +2 -0
- package/dist-esm/lib/hooks/useCanvasEvents.mjs.map +2 -2
- package/dist-esm/lib/hooks/useDocumentEvents.mjs +40 -10
- package/dist-esm/lib/hooks/useDocumentEvents.mjs.map +2 -2
- package/dist-esm/lib/hooks/useLocalStore.mjs +11 -7
- package/dist-esm/lib/hooks/useLocalStore.mjs.map +2 -2
- package/dist-esm/lib/hooks/usePassThroughWheelEvents.mjs +1 -0
- package/dist-esm/lib/hooks/usePassThroughWheelEvents.mjs.map +2 -2
- package/dist-esm/lib/hooks/usePeerIds.mjs +2 -1
- package/dist-esm/lib/hooks/usePeerIds.mjs.map +2 -2
- package/dist-esm/lib/license/LicenseManager.mjs +9 -2
- package/dist-esm/lib/license/LicenseManager.mjs.map +2 -2
- package/dist-esm/lib/license/LicenseProvider.mjs +16 -3
- package/dist-esm/lib/license/LicenseProvider.mjs.map +2 -2
- package/dist-esm/lib/license/useLicenseManagerState.mjs +5 -4
- package/dist-esm/lib/license/useLicenseManagerState.mjs.map +2 -2
- package/dist-esm/lib/primitives/Box.mjs +2 -1
- package/dist-esm/lib/primitives/Box.mjs.map +2 -2
- package/dist-esm/lib/primitives/Vec.mjs +4 -3
- package/dist-esm/lib/primitives/Vec.mjs.map +2 -2
- package/dist-esm/lib/primitives/utils.mjs +2 -1
- package/dist-esm/lib/primitives/utils.mjs.map +2 -2
- package/dist-esm/lib/utils/deepLinks.mjs +5 -1
- package/dist-esm/lib/utils/deepLinks.mjs.map +2 -2
- package/dist-esm/lib/utils/rotation.mjs +1 -9
- package/dist-esm/lib/utils/rotation.mjs.map +2 -2
- package/dist-esm/lib/utils/sync/TLLocalSyncClient.mjs +17 -0
- package/dist-esm/lib/utils/sync/TLLocalSyncClient.mjs.map +2 -2
- package/dist-esm/lib/utils/sync/alerts.mjs +1 -1
- package/dist-esm/lib/utils/sync/alerts.mjs.map +1 -1
- package/dist-esm/version.mjs +3 -3
- package/dist-esm/version.mjs.map +1 -1
- package/package.json +13 -7
- package/src/index.ts +6 -1
- package/src/lib/TldrawEditor.tsx +16 -4
- package/src/lib/components/MenuClickCapture.tsx +19 -18
- package/src/lib/components/default-components/CanvasOverlays.tsx +1 -3
- package/src/lib/components/default-components/DefaultErrorFallback.tsx +1 -1
- package/src/lib/config/TLUserPreferences.test.ts +18 -1
- package/src/lib/config/TLUserPreferences.ts +8 -4
- package/src/lib/config/createTLStore.ts +1 -1
- package/src/lib/editor/Editor.ts +241 -359
- package/src/lib/editor/managers/ClickManager/ClickManager.ts +5 -3
- package/src/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.ts +9 -9
- package/src/lib/editor/managers/FontManager/FontManager.ts +2 -1
- package/src/lib/editor/managers/InputsManager/InputsManager.test.ts +199 -3
- package/src/lib/editor/managers/InputsManager/InputsManager.ts +128 -9
- package/src/lib/editor/managers/ScribbleManager/ScribbleManager.ts +12 -1
- package/src/lib/editor/managers/SnapManager/BoundsSnaps.ts +0 -2
- package/src/lib/editor/managers/TextManager/TextManager.ts +2 -6
- package/src/lib/editor/managers/TickManager/TickManager.test.ts +13 -0
- package/src/lib/editor/managers/TickManager/TickManager.ts +9 -3
- package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.test.ts +64 -3
- package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.ts +18 -1
- package/src/lib/editor/overlays/OverlayManager.ts +3 -7
- package/src/lib/editor/overlays/strokeShapeIndicators.ts +0 -1
- package/src/lib/editor/shapes/BaseFrameLikeShapeUtil.tsx +5 -1
- package/src/lib/editor/shapes/shared/getPerfectDashProps.ts +3 -0
- package/src/lib/editor/tools/BaseBoxShapeTool/children/Pointing.ts +4 -1
- package/src/lib/editor/tools/StateNode.test.ts +64 -0
- package/src/lib/editor/tools/StateNode.ts +12 -2
- package/src/lib/globals/editors.test.ts +76 -0
- package/src/lib/globals/editors.ts +53 -0
- package/src/lib/globals/menus.test.ts +33 -0
- package/src/lib/globals/menus.ts +11 -4
- package/src/lib/hooks/useCanvasEvents.ts +2 -0
- package/src/lib/hooks/useDocumentEvents.ts +60 -16
- package/src/lib/hooks/useLocalStore.ts +16 -8
- package/src/lib/hooks/usePassThroughWheelEvents.ts +3 -0
- package/src/lib/hooks/usePeerIds.ts +2 -1
- package/src/lib/license/LicenseManager.test.ts +160 -0
- package/src/lib/license/LicenseManager.ts +13 -4
- package/src/lib/license/LicenseProvider.tsx +24 -3
- package/src/lib/license/useLicenseManagerState.ts +8 -6
- package/src/lib/primitives/Box.test.ts +9 -1
- package/src/lib/primitives/Box.ts +3 -1
- package/src/lib/primitives/Vec.test.ts +10 -0
- package/src/lib/primitives/Vec.ts +4 -4
- package/src/lib/primitives/geometry/Arc2d.test.ts +226 -3
- package/src/lib/primitives/geometry/CubicBezier2d.test.ts +173 -3
- package/src/lib/primitives/geometry/CubicSpline2d.test.ts +160 -3
- package/src/lib/primitives/geometry/Ellipse2d.test.ts +243 -3
- package/src/lib/primitives/geometry/Geometry2d.test.ts +553 -1
- package/src/lib/primitives/geometry/Group2d.test.ts +398 -3
- package/src/lib/primitives/geometry/Polyline2d.test.ts +207 -0
- package/src/lib/primitives/geometry/Stadium2d.test.ts +191 -3
- package/src/lib/primitives/utils.test.ts +10 -1
- package/src/lib/primitives/utils.ts +4 -1
- package/src/lib/test/useCanvasEvents.test.tsx +74 -0
- package/src/lib/test/useDocumentEvents.test.tsx +107 -0
- package/src/lib/test/usePassThroughWheelEvents.test.tsx +25 -2
- package/src/lib/utils/deepLinks.test.ts +15 -0
- package/src/lib/utils/deepLinks.ts +7 -1
- package/src/lib/utils/rotation.ts +4 -12
- package/src/lib/utils/sync/TLLocalSyncClient.test.ts +56 -0
- package/src/lib/utils/sync/TLLocalSyncClient.ts +24 -0
- package/src/lib/utils/sync/alerts.ts +1 -1
- package/src/version.ts +3 -3
- package/src/lib/primitives/geometry/Polyline.test.ts +0 -5
|
@@ -7,9 +7,11 @@ import { getGlobalWindow } from '../../../utils/dom'
|
|
|
7
7
|
/** @public */
|
|
8
8
|
export class UserPreferencesManager {
|
|
9
9
|
systemColorScheme = atom<'dark' | 'light'>('systemColorScheme', 'light')
|
|
10
|
+
private readonly systemPrefersReducedMotion = atom('systemPrefersReducedMotion', false)
|
|
10
11
|
disposables = new Set<() => void>()
|
|
11
12
|
dispose() {
|
|
12
13
|
this.disposables.forEach((d) => d())
|
|
14
|
+
this.disposables.clear()
|
|
13
15
|
}
|
|
14
16
|
constructor(
|
|
15
17
|
private readonly user: TLCurrentUser,
|
|
@@ -30,6 +32,16 @@ export class UserPreferencesManager {
|
|
|
30
32
|
}
|
|
31
33
|
darkModeMediaQuery?.addEventListener('change', handleChange)
|
|
32
34
|
this.disposables.add(() => darkModeMediaQuery?.removeEventListener('change', handleChange))
|
|
35
|
+
|
|
36
|
+
const reducedMotionMediaQuery = getGlobalWindow().matchMedia('(prefers-reduced-motion: reduce)')
|
|
37
|
+
this.systemPrefersReducedMotion.set(!!reducedMotionMediaQuery?.matches)
|
|
38
|
+
const handleReducedMotionChange = (e: MediaQueryListEvent) => {
|
|
39
|
+
this.systemPrefersReducedMotion.set(e.matches)
|
|
40
|
+
}
|
|
41
|
+
reducedMotionMediaQuery?.addEventListener('change', handleReducedMotionChange)
|
|
42
|
+
this.disposables.add(() =>
|
|
43
|
+
reducedMotionMediaQuery?.removeEventListener('change', handleReducedMotionChange)
|
|
44
|
+
)
|
|
33
45
|
}
|
|
34
46
|
|
|
35
47
|
updateUserPreferences(userPreferences: Partial<TLUserPreferences>) {
|
|
@@ -80,7 +92,12 @@ export class UserPreferencesManager {
|
|
|
80
92
|
}
|
|
81
93
|
|
|
82
94
|
@computed getAnimationSpeed() {
|
|
83
|
-
|
|
95
|
+
// Until the user explicitly sets a speed, follow the OS reduce-motion setting live rather
|
|
96
|
+
// than the value `defaultUserPreferences` captured at module load.
|
|
97
|
+
return (
|
|
98
|
+
this.user.userPreferences.get().animationSpeed ??
|
|
99
|
+
(this.systemPrefersReducedMotion.get() ? 0 : 1)
|
|
100
|
+
)
|
|
84
101
|
}
|
|
85
102
|
|
|
86
103
|
@computed getAreKeyboardShortcutsEnabled() {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { atom, computed } from '@tldraw/state'
|
|
2
|
+
import { WeakCache } from '@tldraw/utils'
|
|
2
3
|
import { Geometry2d } from '../../primitives/geometry/Geometry2d'
|
|
3
4
|
import { VecLike } from '../../primitives/Vec'
|
|
4
5
|
import type { Editor } from '../Editor'
|
|
@@ -119,7 +120,7 @@ export class OverlayManager {
|
|
|
119
120
|
// while getActiveOverlayEntries() keeps returning the same overlay
|
|
120
121
|
// instances; when its reactive deps change, getOverlays() emits fresh
|
|
121
122
|
// objects and stale entries fall out by GC.
|
|
122
|
-
private _geometryCache = new
|
|
123
|
+
private _geometryCache = new WeakCache<TLOverlay, Geometry2d | null>()
|
|
123
124
|
|
|
124
125
|
/**
|
|
125
126
|
* Get hit-test geometry for an overlay, cached by overlay identity. Lets
|
|
@@ -130,12 +131,7 @@ export class OverlayManager {
|
|
|
130
131
|
* @public
|
|
131
132
|
*/
|
|
132
133
|
getOverlayGeometry(overlay: TLOverlay): Geometry2d | null {
|
|
133
|
-
|
|
134
|
-
if (cached !== undefined) return cached
|
|
135
|
-
const util = this.getOverlayUtil(overlay)
|
|
136
|
-
const geometry = util.getGeometry(overlay)
|
|
137
|
-
this._geometryCache.set(overlay, geometry)
|
|
138
|
-
return geometry
|
|
134
|
+
return this._geometryCache.get(overlay, (o) => this.getOverlayUtil(o).getGeometry(o))
|
|
139
135
|
}
|
|
140
136
|
|
|
141
137
|
/**
|
|
@@ -40,7 +40,6 @@ export function strokeShapeIndicators(
|
|
|
40
40
|
if (!shape || shape.isLocked) continue
|
|
41
41
|
|
|
42
42
|
const pageTransform = editor.getShapePageTransform(shape)
|
|
43
|
-
if (!pageTransform) continue
|
|
44
43
|
|
|
45
44
|
const indicatorPath = indicatorPathCache.get(editor, shape.id)
|
|
46
45
|
if (!indicatorPath) continue
|
|
@@ -93,7 +93,11 @@ export abstract class BaseFrameLikeShapeUtil<
|
|
|
93
93
|
const currentChildren = compact(
|
|
94
94
|
editor.getSortedChildIdsForParent(shape).map((id) => editor.getShape(id))
|
|
95
95
|
)
|
|
96
|
-
if (
|
|
96
|
+
if (
|
|
97
|
+
previousChildren.every(
|
|
98
|
+
(s) => !currentChildren.find((c) => c.index === initialIndices.get(s.id))
|
|
99
|
+
)
|
|
100
|
+
) {
|
|
97
101
|
canRestoreOriginalIndices = true
|
|
98
102
|
}
|
|
99
103
|
}
|
|
@@ -87,6 +87,9 @@ export function getPerfectDashProps(
|
|
|
87
87
|
|
|
88
88
|
dashCount = Math.floor(totalLength / dashLength / (2 * ratio))
|
|
89
89
|
dashCount -= dashCount % snap
|
|
90
|
+
// A path shorter than one dash period (or of zero length, where the count is NaN) would
|
|
91
|
+
// otherwise divide by zero below and produce an invalid dasharray; draw a single dash instead
|
|
92
|
+
if (!(dashCount >= 1)) dashCount = 1
|
|
90
93
|
|
|
91
94
|
if (dashCount < 3 && style === 'dashed') {
|
|
92
95
|
if (totalLength / strokeWidth < 4) {
|
|
@@ -75,7 +75,10 @@ export class Pointing extends StateNode {
|
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
override onComplete() {
|
|
78
|
-
|
|
78
|
+
// `complete` arrives mid-press from undo, menus opening, page changes, and focus loss,
|
|
79
|
+
// none of which is a release; committing here would drop a default-size shape the user
|
|
80
|
+
// never asked for. Match the text and arrow tools and leave nothing behind.
|
|
81
|
+
this.cancel()
|
|
79
82
|
}
|
|
80
83
|
|
|
81
84
|
override onInterrupt() {
|
|
@@ -238,6 +238,70 @@ describe('StateNode.addChild', () => {
|
|
|
238
238
|
})
|
|
239
239
|
})
|
|
240
240
|
|
|
241
|
+
describe('StateNode.enter', () => {
|
|
242
|
+
let idleExits = 0
|
|
243
|
+
|
|
244
|
+
class Idle extends StateNode {
|
|
245
|
+
static override id = 'idle'
|
|
246
|
+
override onExit() {
|
|
247
|
+
idleExits++
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
class Pointing extends StateNode {
|
|
252
|
+
static override id = 'pointing'
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
class ToolWithEnterTransition extends StateNode {
|
|
256
|
+
static override id = 'enterTransition'
|
|
257
|
+
static override initial = 'idle'
|
|
258
|
+
static override children() {
|
|
259
|
+
return [Idle, Pointing]
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
override onEnter(info: { startPointing?: boolean }) {
|
|
263
|
+
if (info?.startPointing) this.transition('pointing', info)
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
class OtherTool extends StateNode {
|
|
268
|
+
static override id = 'other'
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
let editor: Editor
|
|
272
|
+
|
|
273
|
+
beforeEach(() => {
|
|
274
|
+
idleExits = 0
|
|
275
|
+
editor = new Editor({
|
|
276
|
+
initialState: 'other',
|
|
277
|
+
shapeUtils: [],
|
|
278
|
+
bindingUtils: [],
|
|
279
|
+
tools: [OtherTool, ToolWithEnterTransition],
|
|
280
|
+
store: createTLStore({ shapeUtils: [], bindingUtils: [] }),
|
|
281
|
+
getContainer: () => document.body,
|
|
282
|
+
})
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
it('enters the initial child when onEnter does not transition', () => {
|
|
286
|
+
editor.setCurrentTool('enterTransition')
|
|
287
|
+
expect(editor.root.getPath()).toBe('root.enterTransition.idle')
|
|
288
|
+
expect(editor.root.children!['enterTransition'].children!['idle'].getIsActive()).toBe(true)
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
it('does not enter the initial child on top of a child that onEnter transitioned to', () => {
|
|
292
|
+
editor.setCurrentTool('enterTransition', { startPointing: true })
|
|
293
|
+
const tool = editor.root.children!['enterTransition']
|
|
294
|
+
expect(editor.root.getPath()).toBe('root.enterTransition.pointing')
|
|
295
|
+
expect(tool.children!['pointing'].getIsActive()).toBe(true)
|
|
296
|
+
expect(tool.children!['idle'].getIsActive()).toBe(false)
|
|
297
|
+
// The stale (never re-entered) idle child must not get a spurious onExit
|
|
298
|
+
expect(idleExits).toBe(0)
|
|
299
|
+
|
|
300
|
+
editor.setCurrentTool('other')
|
|
301
|
+
expect(tool.children!['pointing'].getIsActive()).toBe(false)
|
|
302
|
+
})
|
|
303
|
+
})
|
|
304
|
+
|
|
241
305
|
describe('current tool id mask', () => {
|
|
242
306
|
// Tool mask test classes
|
|
243
307
|
class ToolA extends StateNode {
|
|
@@ -152,7 +152,9 @@ export abstract class StateNode implements Partial<TLEventHandlers> {
|
|
|
152
152
|
}
|
|
153
153
|
|
|
154
154
|
if (prevChildState?.id !== nextChildState.id) {
|
|
155
|
-
|
|
155
|
+
// The previous child is inactive when transitioning from a parent's onEnter (or from a
|
|
156
|
+
// node that was already exited); exiting it again would fire a spurious onExit
|
|
157
|
+
if (prevChildState?.getIsActive()) prevChildState.exit(info, id)
|
|
156
158
|
currState._current.set(nextChildState)
|
|
157
159
|
nextChildState.enter(info, prevChildState?.id || 'initial')
|
|
158
160
|
if (!nextChildState.getIsActive()) break
|
|
@@ -188,10 +190,18 @@ export abstract class StateNode implements Partial<TLEventHandlers> {
|
|
|
188
190
|
this.editor.performance._notifyInteractionStart(this.id, this.getPath())
|
|
189
191
|
}
|
|
190
192
|
|
|
193
|
+
const currentBeforeEnter = this.getCurrent()
|
|
191
194
|
this._isActive.set(true)
|
|
192
195
|
this.onEnter?.(info, from)
|
|
193
196
|
|
|
194
|
-
|
|
197
|
+
// If onEnter already transitioned to a child, entering the initial child on top of it would
|
|
198
|
+
// leave that child active but orphaned
|
|
199
|
+
if (
|
|
200
|
+
this.children &&
|
|
201
|
+
this.initial &&
|
|
202
|
+
this.getIsActive() &&
|
|
203
|
+
this.getCurrent() === currentBeforeEnter
|
|
204
|
+
) {
|
|
195
205
|
const initial = this.children[this.initial]
|
|
196
206
|
this._current.set(initial)
|
|
197
207
|
initial.enter(info, from)
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { createTLStore } from '../..'
|
|
2
|
+
import { Editor } from '../editor/Editor'
|
|
3
|
+
import { tleditors } from './editors'
|
|
4
|
+
|
|
5
|
+
const editors: Editor[] = []
|
|
6
|
+
|
|
7
|
+
function createTestEditor() {
|
|
8
|
+
const editor = new Editor({
|
|
9
|
+
shapeUtils: [],
|
|
10
|
+
bindingUtils: [],
|
|
11
|
+
tools: [],
|
|
12
|
+
store: createTLStore({ shapeUtils: [], bindingUtils: [] }),
|
|
13
|
+
getContainer: () => document.body,
|
|
14
|
+
})
|
|
15
|
+
editors.push(editor)
|
|
16
|
+
return editor
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
for (const editor of editors) {
|
|
21
|
+
editor.dispose()
|
|
22
|
+
}
|
|
23
|
+
editors.length = 0
|
|
24
|
+
expect(tleditors.getMounted()).toEqual([])
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
describe('tleditors', () => {
|
|
28
|
+
it('tracks editors as they mount and unmount', () => {
|
|
29
|
+
const editor = createTestEditor()
|
|
30
|
+
expect(tleditors.getMounted()).toEqual([])
|
|
31
|
+
|
|
32
|
+
editor.emit('mount')
|
|
33
|
+
expect(tleditors.getMounted()).toEqual([editor])
|
|
34
|
+
|
|
35
|
+
editor.emit('unmount')
|
|
36
|
+
expect(tleditors.getMounted()).toEqual([])
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('does not add the same editor twice', () => {
|
|
40
|
+
const editor = createTestEditor()
|
|
41
|
+
editor.emit('mount')
|
|
42
|
+
editor.emit('mount')
|
|
43
|
+
expect(tleditors.getMounted()).toEqual([editor])
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('removes an editor when it is disposed while mounted', () => {
|
|
47
|
+
const editor = createTestEditor()
|
|
48
|
+
editor.emit('mount')
|
|
49
|
+
expect(tleditors.getMounted()).toEqual([editor])
|
|
50
|
+
|
|
51
|
+
editor.dispose()
|
|
52
|
+
expect(tleditors.getMounted()).toEqual([])
|
|
53
|
+
expect(editor.getIsMounted()).toBe(false)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('removes an editor on dispose even if the unmount listener was removed', () => {
|
|
57
|
+
const editor = createTestEditor()
|
|
58
|
+
editor.emit('mount')
|
|
59
|
+
editor.removeAllListeners('unmount')
|
|
60
|
+
|
|
61
|
+
editor.dispose()
|
|
62
|
+
expect(tleditors.getMounted()).toEqual([])
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('tracks multiple mounted editors', () => {
|
|
66
|
+
const editorA = createTestEditor()
|
|
67
|
+
const editorB = createTestEditor()
|
|
68
|
+
|
|
69
|
+
editorA.emit('mount')
|
|
70
|
+
editorB.emit('mount')
|
|
71
|
+
expect(tleditors.getMounted()).toEqual([editorA, editorB])
|
|
72
|
+
|
|
73
|
+
editorA.emit('unmount')
|
|
74
|
+
expect(tleditors.getMounted()).toEqual([editorB])
|
|
75
|
+
})
|
|
76
|
+
})
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { Signal, atom } from '@tldraw/state'
|
|
2
|
+
import type { Editor } from '../editor/Editor'
|
|
3
|
+
|
|
4
|
+
const mounted = atom<readonly Editor[]>('mounted editors', [])
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A global registry of currently mounted editor instances. Use it to discover live editors from
|
|
8
|
+
* outside the React tree, for example from sidebar chrome, keyboard shortcuts, or multi-editor
|
|
9
|
+
* layouts.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```ts
|
|
13
|
+
* tleditors.getMounted() // readonly Editor[]
|
|
14
|
+
*
|
|
15
|
+
* const editors = useValue('mounted editors', () => tleditors.getMounted(), [])
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* @public
|
|
19
|
+
*/
|
|
20
|
+
export const tleditors = {
|
|
21
|
+
/**
|
|
22
|
+
* A reactive list of currently mounted editor instances.
|
|
23
|
+
*
|
|
24
|
+
* An editor is added when it emits the `mount` event and removed when it emits `unmount`
|
|
25
|
+
* (including when the editor is disposed while still mounted).
|
|
26
|
+
*
|
|
27
|
+
* @public
|
|
28
|
+
*/
|
|
29
|
+
mounted: mounted as Signal<readonly Editor[]>,
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Get the currently mounted editor instances.
|
|
33
|
+
*
|
|
34
|
+
* @public
|
|
35
|
+
*/
|
|
36
|
+
getMounted(): readonly Editor[] {
|
|
37
|
+
return mounted.get()
|
|
38
|
+
},
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** @internal */
|
|
42
|
+
export function registerMountedEditor(editor: Editor) {
|
|
43
|
+
const current = mounted.get()
|
|
44
|
+
if (current.includes(editor)) return
|
|
45
|
+
mounted.set([...current, editor])
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** @internal */
|
|
49
|
+
export function unregisterMountedEditor(editor: Editor) {
|
|
50
|
+
const current = mounted.get()
|
|
51
|
+
if (!current.includes(editor)) return
|
|
52
|
+
mounted.set(current.filter((e) => e !== editor))
|
|
53
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { tlmenus } from './menus'
|
|
2
|
+
|
|
3
|
+
describe('tlmenus', () => {
|
|
4
|
+
afterEach(() => {
|
|
5
|
+
tlmenus.clearOpenMenus()
|
|
6
|
+
tlmenus._hiddenMenus = []
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
it('reports whether a menu is open with and without a context', () => {
|
|
10
|
+
tlmenus.addOpenMenu('main')
|
|
11
|
+
tlmenus.addOpenMenu('main', 'editor-1')
|
|
12
|
+
|
|
13
|
+
expect(tlmenus.isMenuOpen('main')).toBe(true)
|
|
14
|
+
expect(tlmenus.isMenuOpen('main', 'editor-1')).toBe(true)
|
|
15
|
+
expect(tlmenus.isMenuOpen('main', 'editor-2')).toBe(false)
|
|
16
|
+
expect(tlmenus.isMenuOpen('other')).toBe(false)
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('hides and restores the open menus of a context', () => {
|
|
20
|
+
tlmenus.addOpenMenu('main', 'editor-1')
|
|
21
|
+
tlmenus.addOpenMenu('main', 'editor-2')
|
|
22
|
+
|
|
23
|
+
tlmenus.hideOpenMenus('editor-1')
|
|
24
|
+
expect(tlmenus.getOpenMenus()).toEqual(['main-editor-2'])
|
|
25
|
+
|
|
26
|
+
tlmenus.showOpenMenus('editor-1')
|
|
27
|
+
expect(tlmenus.getOpenMenus().sort()).toEqual(['main-editor-1', 'main-editor-2'])
|
|
28
|
+
|
|
29
|
+
// closing the real menu afterwards leaves nothing behind
|
|
30
|
+
tlmenus.deleteOpenMenu('main', 'editor-1')
|
|
31
|
+
expect(tlmenus.hasOpenMenus('editor-1')).toBe(false)
|
|
32
|
+
})
|
|
33
|
+
})
|
package/src/lib/globals/menus.ts
CHANGED
|
@@ -106,10 +106,12 @@ export const tlmenus = {
|
|
|
106
106
|
* @public
|
|
107
107
|
*/
|
|
108
108
|
hideOpenMenus(contextId?: string) {
|
|
109
|
+
// ids read back from the store already carry their context suffix, so they are deleted
|
|
110
|
+
// (and later re-added) as-is rather than suffixed again
|
|
109
111
|
this._hiddenMenus = [...this.getOpenMenus(contextId)]
|
|
110
112
|
if (this._hiddenMenus.length === 0) return
|
|
111
113
|
for (const menu of this._hiddenMenus) {
|
|
112
|
-
this.deleteOpenMenu(menu
|
|
114
|
+
this.deleteOpenMenu(menu)
|
|
113
115
|
}
|
|
114
116
|
},
|
|
115
117
|
|
|
@@ -128,10 +130,15 @@ export const tlmenus = {
|
|
|
128
130
|
*/
|
|
129
131
|
showOpenMenus(contextId?: string) {
|
|
130
132
|
if (this._hiddenMenus.length === 0) return
|
|
133
|
+
const stillHidden: string[] = []
|
|
131
134
|
for (const menu of this._hiddenMenus) {
|
|
132
|
-
|
|
135
|
+
if (!contextId || menu.endsWith('-' + contextId)) {
|
|
136
|
+
this.addOpenMenu(menu)
|
|
137
|
+
} else {
|
|
138
|
+
stillHidden.push(menu)
|
|
139
|
+
}
|
|
133
140
|
}
|
|
134
|
-
this._hiddenMenus =
|
|
141
|
+
this._hiddenMenus = stillHidden
|
|
135
142
|
},
|
|
136
143
|
|
|
137
144
|
/**
|
|
@@ -148,7 +155,7 @@ export const tlmenus = {
|
|
|
148
155
|
* @public
|
|
149
156
|
*/
|
|
150
157
|
isMenuOpen(id: string, contextId?: string): boolean {
|
|
151
|
-
return this.getOpenMenus(contextId).includes(`${id}-${contextId}`)
|
|
158
|
+
return this.getOpenMenus(contextId).includes(contextId ? `${id}-${contextId}` : id)
|
|
152
159
|
},
|
|
153
160
|
|
|
154
161
|
/**
|
|
@@ -153,6 +153,7 @@ export function useCanvasEvents() {
|
|
|
153
153
|
if (e.dataTransfer?.files?.length) {
|
|
154
154
|
const files = Array.from(e.dataTransfer.files)
|
|
155
155
|
|
|
156
|
+
editor.markHistoryStoppingPoint('drop')
|
|
156
157
|
await editor.putExternalContent({
|
|
157
158
|
type: 'files',
|
|
158
159
|
files,
|
|
@@ -163,6 +164,7 @@ export function useCanvasEvents() {
|
|
|
163
164
|
|
|
164
165
|
const url = e.dataTransfer.getData('url')
|
|
165
166
|
if (url) {
|
|
167
|
+
editor.markHistoryStoppingPoint('drop')
|
|
166
168
|
await editor.putExternalContent({
|
|
167
169
|
type: 'url',
|
|
168
170
|
url,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useValue } from '@tldraw/state-react'
|
|
2
|
-
import { useEffect } from 'react'
|
|
2
|
+
import { useEffect, useRef } from 'react'
|
|
3
3
|
import { Editor } from '../editor/Editor'
|
|
4
4
|
import { TLKeyboardEventInfo } from '../editor/types/event-types'
|
|
5
5
|
import { activeElementShouldCaptureKeys, preventDefault } from '../utils/dom'
|
|
@@ -14,6 +14,10 @@ export function useDocumentEvents() {
|
|
|
14
14
|
const isEditing = useValue('isEditing', () => editor.getEditingShapeId(), [editor])
|
|
15
15
|
const isAppFocused = useValue('isFocused', () => editor.getIsFocused(), [editor])
|
|
16
16
|
|
|
17
|
+
// `inputs.keys` only stores codes, but tools match `onKeyUp` on `info.key`, so the window
|
|
18
|
+
// blur release below needs the key remembered per code.
|
|
19
|
+
const heldKeysRef = useRef(new Map<string, string>())
|
|
20
|
+
|
|
17
21
|
// Prevent the browser's default drag and drop behavior on our container (UI, etc)
|
|
18
22
|
useEffect(() => {
|
|
19
23
|
if (!container) return
|
|
@@ -29,8 +33,12 @@ export function useDocumentEvents() {
|
|
|
29
33
|
// re-dispatched, which would lead to an infinite loop.
|
|
30
34
|
if ((e as any).isSpecialRedispatchedEvent) return
|
|
31
35
|
preventDefault(e)
|
|
32
|
-
e.stopPropagation()
|
|
33
36
|
const cvs = container.querySelector('.tl-canvas')
|
|
37
|
+
// A drop on the canvas itself already reaches the canvas (and any shape's own React
|
|
38
|
+
// drop handlers) by bubbling; stopping it here would hide it from React, which
|
|
39
|
+
// delegates from the root above this container.
|
|
40
|
+
if (cvs?.contains(e.target as Node | null)) return
|
|
41
|
+
e.stopPropagation()
|
|
34
42
|
if (!cvs) return
|
|
35
43
|
const newEvent = new DragEvent(e.type, e)
|
|
36
44
|
;(newEvent as any).isSpecialRedispatchedEvent = true
|
|
@@ -208,6 +216,7 @@ export function useDocumentEvents() {
|
|
|
208
216
|
accelKey: isAccelKey(e),
|
|
209
217
|
}
|
|
210
218
|
|
|
219
|
+
heldKeysRef.current.set(e.code, e.key)
|
|
211
220
|
editor.dispatch(info)
|
|
212
221
|
}
|
|
213
222
|
|
|
@@ -235,24 +244,26 @@ export function useDocumentEvents() {
|
|
|
235
244
|
accelKey: isAccelKey(e),
|
|
236
245
|
}
|
|
237
246
|
|
|
247
|
+
heldKeysRef.current.delete(e.code)
|
|
238
248
|
editor.dispatch(info)
|
|
239
249
|
}
|
|
240
250
|
|
|
241
251
|
function handleTouchStart(e: TouchEvent) {
|
|
242
252
|
if (container.contains(e.target as Node)) {
|
|
243
|
-
// Center point of the touch area
|
|
244
|
-
|
|
253
|
+
// Center point of the touch area, measured from the edge of the window the touch
|
|
254
|
+
// has to reach to trigger the navigation
|
|
255
|
+
const touchXPosition = e.touches[0].clientX
|
|
245
256
|
// Size of the touch area
|
|
246
257
|
const touchXRadius = e.touches[0].radiusX || 0
|
|
247
258
|
|
|
248
|
-
//
|
|
249
|
-
//
|
|
250
|
-
//
|
|
251
|
-
//
|
|
252
|
-
|
|
259
|
+
// If the touch area overlaps with the screen edges it's likely to trigger the
|
|
260
|
+
// navigation. We prevent the touchstart event in that case. The gesture belongs to
|
|
261
|
+
// the window, so an editor inset from the window's edges — beside a sidebar, say —
|
|
262
|
+
// is not near an edge the system reacts to.
|
|
263
|
+
const windowWidth = editor.getContainerWindow().innerWidth
|
|
253
264
|
if (
|
|
254
265
|
touchXPosition - touchXRadius < 10 ||
|
|
255
|
-
touchXPosition + touchXRadius >
|
|
266
|
+
touchXPosition + touchXRadius > windowWidth - 10
|
|
256
267
|
) {
|
|
257
268
|
if ((e.target as HTMLElement)?.tagName === 'BUTTON') {
|
|
258
269
|
// Force a click before bailing
|
|
@@ -276,10 +287,13 @@ export function useDocumentEvents() {
|
|
|
276
287
|
|
|
277
288
|
container.addEventListener('wheel', handleWheel, { passive: false })
|
|
278
289
|
|
|
290
|
+
// Not the shared `preventDefault` reference: the DOM dedupes identical listeners, so with two
|
|
291
|
+
// editors in one document the first cleanup would otherwise remove it for both.
|
|
292
|
+
const handleGesture = (e: Event) => preventDefault(e)
|
|
279
293
|
const ownerDoc = container.ownerDocument
|
|
280
|
-
ownerDoc.addEventListener('gesturestart',
|
|
281
|
-
ownerDoc.addEventListener('gesturechange',
|
|
282
|
-
ownerDoc.addEventListener('gestureend',
|
|
294
|
+
ownerDoc.addEventListener('gesturestart', handleGesture)
|
|
295
|
+
ownerDoc.addEventListener('gesturechange', handleGesture)
|
|
296
|
+
ownerDoc.addEventListener('gestureend', handleGesture)
|
|
283
297
|
|
|
284
298
|
container.addEventListener('keydown', handleKeyDown)
|
|
285
299
|
container.addEventListener('keyup', handleKeyUp)
|
|
@@ -289,14 +303,44 @@ export function useDocumentEvents() {
|
|
|
289
303
|
|
|
290
304
|
container.removeEventListener('wheel', handleWheel)
|
|
291
305
|
|
|
292
|
-
ownerDoc.removeEventListener('gesturestart',
|
|
293
|
-
ownerDoc.removeEventListener('gesturechange',
|
|
294
|
-
ownerDoc.removeEventListener('gestureend',
|
|
306
|
+
ownerDoc.removeEventListener('gesturestart', handleGesture)
|
|
307
|
+
ownerDoc.removeEventListener('gesturechange', handleGesture)
|
|
308
|
+
ownerDoc.removeEventListener('gestureend', handleGesture)
|
|
295
309
|
|
|
296
310
|
container.removeEventListener('keydown', handleKeyDown)
|
|
297
311
|
container.removeEventListener('keyup', handleKeyUp)
|
|
298
312
|
}
|
|
299
313
|
}, [editor, container, isAppFocused, isEditing])
|
|
314
|
+
|
|
315
|
+
// Alt+Tab / Cmd+Tab away while holding a key delivers the keyup to the other app, so without
|
|
316
|
+
// this Space-panning (grab cursor, drags pan) stuck until the key was pressed again (#10442).
|
|
317
|
+
// Replaying a key_up per held key runs the normal release path, including tool onKeyUp.
|
|
318
|
+
useEffect(() => {
|
|
319
|
+
const win = editor.getContainerWindow()
|
|
320
|
+
|
|
321
|
+
const handleWindowBlur = () => {
|
|
322
|
+
const heldKeys = heldKeysRef.current
|
|
323
|
+
for (const code of [...editor.inputs.keys]) {
|
|
324
|
+
editor.dispatch({
|
|
325
|
+
type: 'keyboard',
|
|
326
|
+
name: 'key_up',
|
|
327
|
+
key: heldKeys.get(code) ?? code,
|
|
328
|
+
code,
|
|
329
|
+
shiftKey: false,
|
|
330
|
+
altKey: false,
|
|
331
|
+
ctrlKey: false,
|
|
332
|
+
metaKey: false,
|
|
333
|
+
accelKey: false,
|
|
334
|
+
})
|
|
335
|
+
}
|
|
336
|
+
heldKeys.clear()
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
win.addEventListener('blur', handleWindowBlur)
|
|
340
|
+
return () => {
|
|
341
|
+
win.removeEventListener('blur', handleWindowBlur)
|
|
342
|
+
}
|
|
343
|
+
}, [editor])
|
|
300
344
|
}
|
|
301
345
|
|
|
302
346
|
function areShortcutsDisabled(editor: Editor) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { TLAssetId, TLAssetStore, TLStoreSnapshot } from '@tldraw/tlschema'
|
|
2
|
+
import { noop } from '@tldraw/utils'
|
|
3
3
|
import { useEffect } from 'react'
|
|
4
4
|
import { TLStoreOptions, createTLStore } from '../config/createTLStore'
|
|
5
5
|
import { TLEditorSnapshot } from '../config/TLEditorSnapshot'
|
|
@@ -33,7 +33,9 @@ export function useLocalStore(
|
|
|
33
33
|
|
|
34
34
|
setState({ status: 'loading' })
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
// Keyed by asset id rather than record identity so that a record update doesn't mint
|
|
37
|
+
// another object url for the same blob; every url minted here is revoked on cleanup.
|
|
38
|
+
const objectURLCache = new Map<TLAssetId, Promise<string | null>>()
|
|
37
39
|
const assets: TLAssetStore = {
|
|
38
40
|
upload: async (asset, file) => {
|
|
39
41
|
await client.db.storeAsset(asset.id, file)
|
|
@@ -43,11 +45,14 @@ export function useLocalStore(
|
|
|
43
45
|
if (!asset.props.src) return null
|
|
44
46
|
|
|
45
47
|
if (asset.props.src.startsWith('asset:')) {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
48
|
+
let objectURL = objectURLCache.get(asset.id)
|
|
49
|
+
if (!objectURL) {
|
|
50
|
+
objectURL = client.db
|
|
51
|
+
.getAsset(asset.id)
|
|
52
|
+
.then((blob) => (blob ? URL.createObjectURL(blob) : null))
|
|
53
|
+
objectURLCache.set(asset.id, objectURL)
|
|
54
|
+
}
|
|
55
|
+
return await objectURL
|
|
51
56
|
}
|
|
52
57
|
|
|
53
58
|
return asset.props.src
|
|
@@ -78,6 +83,9 @@ export function useLocalStore(
|
|
|
78
83
|
return () => {
|
|
79
84
|
isClosed = true
|
|
80
85
|
client.close()
|
|
86
|
+
for (const objectURL of objectURLCache.values()) {
|
|
87
|
+
objectURL.then((url) => url && URL.revokeObjectURL(url), noop)
|
|
88
|
+
}
|
|
81
89
|
}
|
|
82
90
|
}, [options, setState])
|
|
83
91
|
|
|
@@ -70,6 +70,9 @@ export function usePassThroughWheelEvents(ref: RefObject<HTMLElement | null>) {
|
|
|
70
70
|
const newEvent = new WheelEvent('wheel', e as any)
|
|
71
71
|
;(newEvent as any).isSpecialRedispatchedEvent = true
|
|
72
72
|
cvs.dispatchEvent(newEvent)
|
|
73
|
+
// Flag the original too: it keeps bubbling, and an enclosing pass-through root (the
|
|
74
|
+
// navigation panel around the minimap) would otherwise redispatch it a second time.
|
|
75
|
+
;(e as any).isSpecialRedispatchedEvent = true
|
|
73
76
|
})
|
|
74
77
|
|
|
75
78
|
// The element the listener is currently on, which isn't necessarily `ref.current`: a `RefObject`
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useComputed, useValue } from '@tldraw/state-react'
|
|
2
|
+
import { areArraysShallowEqual } from '@tldraw/utils'
|
|
2
3
|
import { uniq } from '../utils/uniq'
|
|
3
4
|
import { useEditor } from './useEditor'
|
|
4
5
|
|
|
@@ -17,7 +18,7 @@ export function usePeerIds() {
|
|
|
17
18
|
const $userIds = useComputed(
|
|
18
19
|
'userIds',
|
|
19
20
|
() => uniq(editor.getVisibleCollaborators().map((p) => p.userId)).sort(),
|
|
20
|
-
{ isEqual:
|
|
21
|
+
{ isEqual: areArraysShallowEqual },
|
|
21
22
|
[editor]
|
|
22
23
|
)
|
|
23
24
|
|