@tldraw/editor 5.2.0-next.ee0fa4d6244f → 5.2.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/README.md +7 -1
- package/dist-cjs/index.d.ts +117 -60
- package/dist-cjs/index.js +7 -8
- package/dist-cjs/index.js.map +2 -2
- package/dist-cjs/lib/components/MenuClickCapture.js +8 -5
- package/dist-cjs/lib/components/MenuClickCapture.js.map +2 -2
- package/dist-cjs/lib/components/Shape.js +47 -1
- package/dist-cjs/lib/components/Shape.js.map +3 -3
- package/dist-cjs/lib/components/default-components/DefaultCanvas.js +1 -16
- package/dist-cjs/lib/components/default-components/DefaultCanvas.js.map +2 -2
- package/dist-cjs/lib/editor/Editor.js +131 -57
- package/dist-cjs/lib/editor/Editor.js.map +3 -3
- package/dist-cjs/lib/editor/managers/ClickManager/ClickManager.js +8 -58
- package/dist-cjs/lib/editor/managers/ClickManager/ClickManager.js.map +2 -2
- package/dist-cjs/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.js +3 -3
- package/dist-cjs/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.js.map +2 -2
- package/dist-cjs/lib/editor/managers/FocusManager/FocusManager.js +1 -2
- package/dist-cjs/lib/editor/managers/FocusManager/FocusManager.js.map +2 -2
- package/dist-cjs/lib/editor/managers/FontManager/FontManager.js +28 -4
- package/dist-cjs/lib/editor/managers/FontManager/FontManager.js.map +3 -3
- package/dist-cjs/lib/editor/managers/HistoryManager/HistoryManager.js +24 -2
- package/dist-cjs/lib/editor/managers/HistoryManager/HistoryManager.js.map +2 -2
- package/dist-cjs/lib/editor/managers/InputsManager/InputsManager.js +14 -3
- package/dist-cjs/lib/editor/managers/InputsManager/InputsManager.js.map +2 -2
- package/dist-cjs/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.js +4 -2
- package/dist-cjs/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.js.map +2 -2
- package/dist-cjs/lib/editor/managers/TextManager/TextManager.js +7 -3
- package/dist-cjs/lib/editor/managers/TextManager/TextManager.js.map +2 -2
- package/dist-cjs/lib/editor/managers/TickManager/TickManager.js +0 -1
- package/dist-cjs/lib/editor/managers/TickManager/TickManager.js.map +2 -2
- package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js +15 -2
- package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js.map +2 -2
- package/dist-cjs/lib/editor/overlays/strokeShapeIndicators.js +79 -0
- package/dist-cjs/lib/editor/overlays/strokeShapeIndicators.js.map +7 -0
- package/dist-cjs/lib/editor/shapes/ShapeUtil.js.map +2 -2
- package/dist-cjs/lib/editor/tools/BaseBoxShapeTool/children/Pointing.js +4 -0
- package/dist-cjs/lib/editor/tools/BaseBoxShapeTool/children/Pointing.js.map +2 -2
- package/dist-cjs/lib/editor/tools/StateNode.js.map +2 -2
- package/dist-cjs/lib/{hooks/useTransform.js → editor/tools/cancelShapeCreationOnLongPress.js} +8 -23
- package/dist-cjs/lib/editor/tools/cancelShapeCreationOnLongPress.js.map +7 -0
- package/dist-cjs/lib/editor/types/event-types.js +0 -2
- package/dist-cjs/lib/editor/types/event-types.js.map +2 -2
- package/dist-cjs/lib/globals/environment.js +6 -1
- package/dist-cjs/lib/globals/environment.js.map +2 -2
- package/dist-cjs/lib/hooks/EditorComponentsContext.js.map +2 -2
- package/dist-cjs/lib/hooks/useCanvasEvents.js +17 -8
- package/dist-cjs/lib/hooks/useCanvasEvents.js.map +2 -2
- package/dist-cjs/lib/hooks/useEditorComponents.js +0 -1
- package/dist-cjs/lib/hooks/useEditorComponents.js.map +2 -2
- package/dist-cjs/lib/hooks/usePresence.js.map +2 -2
- package/dist-cjs/lib/hooks/useScreenBounds.js +1 -0
- package/dist-cjs/lib/hooks/useScreenBounds.js.map +2 -2
- package/dist-cjs/lib/license/LicenseManager.js +5 -1
- package/dist-cjs/lib/license/LicenseManager.js.map +2 -2
- package/dist-cjs/lib/primitives/Vec.js +3 -2
- package/dist-cjs/lib/primitives/Vec.js.map +2 -2
- package/dist-cjs/lib/utils/dom.js +12 -0
- package/dist-cjs/lib/utils/dom.js.map +2 -2
- package/dist-cjs/lib/utils/getPointerInfo.js +2 -1
- package/dist-cjs/lib/utils/getPointerInfo.js.map +2 -2
- package/dist-cjs/lib/utils/pointer.js +37 -0
- package/dist-cjs/lib/utils/pointer.js.map +7 -0
- package/dist-cjs/version.js +3 -3
- package/dist-cjs/version.js.map +1 -1
- package/dist-esm/index.d.mts +117 -60
- package/dist-esm/index.mjs +9 -13
- package/dist-esm/index.mjs.map +2 -2
- package/dist-esm/lib/components/MenuClickCapture.mjs +8 -5
- package/dist-esm/lib/components/MenuClickCapture.mjs.map +2 -2
- package/dist-esm/lib/components/Shape.mjs +48 -2
- package/dist-esm/lib/components/Shape.mjs.map +3 -3
- package/dist-esm/lib/components/default-components/DefaultCanvas.mjs +1 -16
- package/dist-esm/lib/components/default-components/DefaultCanvas.mjs.map +2 -2
- package/dist-esm/lib/editor/Editor.mjs +131 -57
- package/dist-esm/lib/editor/Editor.mjs.map +3 -3
- package/dist-esm/lib/editor/managers/ClickManager/ClickManager.mjs +8 -58
- package/dist-esm/lib/editor/managers/ClickManager/ClickManager.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.mjs +3 -3
- package/dist-esm/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/FocusManager/FocusManager.mjs +1 -2
- package/dist-esm/lib/editor/managers/FocusManager/FocusManager.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/FontManager/FontManager.mjs +28 -4
- package/dist-esm/lib/editor/managers/FontManager/FontManager.mjs.map +3 -3
- package/dist-esm/lib/editor/managers/HistoryManager/HistoryManager.mjs +24 -2
- package/dist-esm/lib/editor/managers/HistoryManager/HistoryManager.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/InputsManager/InputsManager.mjs +14 -3
- package/dist-esm/lib/editor/managers/InputsManager/InputsManager.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.mjs +4 -2
- package/dist-esm/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs +7 -3
- package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/TickManager/TickManager.mjs +0 -1
- package/dist-esm/lib/editor/managers/TickManager/TickManager.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs +15 -2
- package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs.map +2 -2
- package/dist-esm/lib/editor/overlays/strokeShapeIndicators.mjs +59 -0
- package/dist-esm/lib/editor/overlays/strokeShapeIndicators.mjs.map +7 -0
- package/dist-esm/lib/editor/shapes/ShapeUtil.mjs.map +2 -2
- package/dist-esm/lib/editor/tools/BaseBoxShapeTool/children/Pointing.mjs +4 -0
- package/dist-esm/lib/editor/tools/BaseBoxShapeTool/children/Pointing.mjs.map +2 -2
- package/dist-esm/lib/editor/tools/StateNode.mjs.map +2 -2
- package/dist-esm/lib/editor/tools/cancelShapeCreationOnLongPress.mjs +8 -0
- package/dist-esm/lib/editor/tools/cancelShapeCreationOnLongPress.mjs.map +7 -0
- package/dist-esm/lib/editor/types/event-types.mjs +0 -2
- package/dist-esm/lib/editor/types/event-types.mjs.map +2 -2
- package/dist-esm/lib/globals/environment.mjs +6 -1
- package/dist-esm/lib/globals/environment.mjs.map +2 -2
- package/dist-esm/lib/hooks/EditorComponentsContext.mjs.map +2 -2
- package/dist-esm/lib/hooks/useCanvasEvents.mjs +17 -8
- package/dist-esm/lib/hooks/useCanvasEvents.mjs.map +2 -2
- package/dist-esm/lib/hooks/useEditorComponents.mjs +0 -1
- package/dist-esm/lib/hooks/useEditorComponents.mjs.map +2 -2
- package/dist-esm/lib/hooks/usePresence.mjs.map +2 -2
- package/dist-esm/lib/hooks/useScreenBounds.mjs +1 -0
- package/dist-esm/lib/hooks/useScreenBounds.mjs.map +2 -2
- package/dist-esm/lib/license/LicenseManager.mjs +5 -1
- package/dist-esm/lib/license/LicenseManager.mjs.map +2 -2
- package/dist-esm/lib/primitives/Vec.mjs +3 -2
- package/dist-esm/lib/primitives/Vec.mjs.map +2 -2
- package/dist-esm/lib/utils/dom.mjs +12 -0
- package/dist-esm/lib/utils/dom.mjs.map +2 -2
- package/dist-esm/lib/utils/getPointerInfo.mjs +2 -1
- package/dist-esm/lib/utils/getPointerInfo.mjs.map +2 -2
- package/dist-esm/lib/utils/pointer.mjs +17 -0
- package/dist-esm/lib/utils/pointer.mjs.map +7 -0
- package/dist-esm/version.mjs +3 -3
- package/dist-esm/version.mjs.map +1 -1
- package/editor.css +15 -3
- package/package.json +11 -8
- package/src/index.ts +4 -10
- package/src/lib/components/MenuClickCapture.tsx +8 -4
- package/src/lib/components/Shape.tsx +88 -1
- package/src/lib/components/default-components/DefaultCanvas.tsx +1 -17
- package/src/lib/editor/Editor.test.ts +36 -0
- package/src/lib/editor/Editor.ts +204 -75
- package/src/lib/editor/managers/ClickManager/ClickManager.test.ts +54 -74
- package/src/lib/editor/managers/ClickManager/ClickManager.ts +15 -65
- package/src/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.test.ts +43 -16
- package/src/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.ts +8 -5
- package/src/lib/editor/managers/FocusManager/FocusManager.test.ts +4 -4
- package/src/lib/editor/managers/FocusManager/FocusManager.ts +1 -2
- package/src/lib/editor/managers/FontManager/FontManager.test.ts +37 -10
- package/src/lib/editor/managers/FontManager/FontManager.ts +46 -1
- package/src/lib/editor/managers/HistoryManager/HistoryManager.test.ts +32 -0
- package/src/lib/editor/managers/HistoryManager/HistoryManager.ts +34 -4
- package/src/lib/editor/managers/InputsManager/InputsManager.test.ts +61 -0
- package/src/lib/editor/managers/InputsManager/InputsManager.ts +16 -4
- package/src/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.ts +9 -2
- package/src/lib/editor/managers/TextManager/TextManager.test.ts +16 -14
- package/src/lib/editor/managers/TextManager/TextManager.ts +17 -2
- package/src/lib/editor/managers/TickManager/TickManager.test.ts +0 -40
- package/src/lib/editor/managers/TickManager/TickManager.ts +0 -1
- package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.test.ts +12 -2
- package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.ts +27 -2
- package/src/lib/editor/overlays/strokeShapeIndicators.ts +86 -0
- package/src/lib/editor/shapes/ShapeUtil.ts +41 -0
- package/src/lib/editor/tools/BaseBoxShapeTool/children/Pointing.ts +5 -0
- package/src/lib/editor/tools/StateNode.ts +0 -2
- package/src/lib/editor/tools/cancelShapeCreationOnLongPress.ts +31 -0
- package/src/lib/editor/types/event-types.ts +9 -6
- package/src/lib/globals/environment.ts +7 -0
- package/src/lib/hooks/EditorComponentsContext.tsx +0 -2
- package/src/lib/hooks/useCanvasEvents.ts +24 -12
- package/src/lib/hooks/useEditorComponents.tsx +0 -1
- package/src/lib/hooks/usePresence.ts +2 -2
- package/src/lib/hooks/useScreenBounds.ts +3 -0
- package/src/lib/license/LicenseManager.test.ts +24 -16
- package/src/lib/license/LicenseManager.ts +8 -2
- package/src/lib/primitives/Vec.ts +5 -2
- package/src/lib/test/ContentElementSlot.test.tsx +201 -0
- package/src/lib/utils/dom.ts +34 -0
- package/src/lib/utils/getPointerInfo.ts +2 -1
- package/src/lib/utils/pointer.test.ts +76 -0
- package/src/lib/utils/pointer.ts +42 -0
- package/src/version.ts +3 -3
- package/dist-cjs/lib/components/default-components/DefaultSelectionBackground.js +0 -49
- package/dist-cjs/lib/components/default-components/DefaultSelectionBackground.js.map +0 -7
- package/dist-cjs/lib/editor/overlays/ShapeIndicatorOverlayUtil.js +0 -161
- package/dist-cjs/lib/editor/overlays/ShapeIndicatorOverlayUtil.js.map +0 -7
- package/dist-cjs/lib/hooks/useTransform.js.map +0 -7
- package/dist-esm/lib/components/default-components/DefaultSelectionBackground.mjs +0 -19
- package/dist-esm/lib/components/default-components/DefaultSelectionBackground.mjs.map +0 -7
- package/dist-esm/lib/editor/overlays/ShapeIndicatorOverlayUtil.mjs +0 -141
- package/dist-esm/lib/editor/overlays/ShapeIndicatorOverlayUtil.mjs.map +0 -7
- package/dist-esm/lib/hooks/useTransform.mjs +0 -23
- package/dist-esm/lib/hooks/useTransform.mjs.map +0 -7
- package/src/lib/components/default-components/DefaultSelectionBackground.tsx +0 -25
- package/src/lib/editor/overlays/ShapeIndicatorOverlayUtil.ts +0 -216
- package/src/lib/hooks/useTransform.ts +0 -30
|
@@ -982,6 +982,47 @@ export abstract class ShapeUtil<Shape extends TLShape = TLShape> {
|
|
|
982
982
|
* @public
|
|
983
983
|
*/
|
|
984
984
|
onEditEnd?(shape: Shape): void
|
|
985
|
+
|
|
986
|
+
/**
|
|
987
|
+
* Provide an app-owned element to be rendered inside the shape, alongside the output of
|
|
988
|
+
* {@link ShapeUtil.component}. While the shape remains mounted, tldraw guarantees the
|
|
989
|
+
* element keeps the same DOM position: it is never unmounted, recreated, or relocated by
|
|
990
|
+
* reordering, reparenting, culling, or re-renders. When adopting the element, tldraw uses
|
|
991
|
+
* `Node.moveBefore` where available so stateful content like cross-origin iframes keeps
|
|
992
|
+
* its state across the move, falling back to `appendChild` elsewhere.
|
|
993
|
+
*
|
|
994
|
+
* Pair this with {@link ShapeUtil.onReleaseAppOwnedElement} to reclaim the element before
|
|
995
|
+
* the shape or editor unmounts.
|
|
996
|
+
*
|
|
997
|
+
* This is called once per shape mount, not on every prop change, so the adopted element is
|
|
998
|
+
* not refreshed when the shape's props change. If the element's content depends on props,
|
|
999
|
+
* return a stable element and mutate it in place (for example from {@link ShapeUtil.component}
|
|
1000
|
+
* or an effect) rather than returning a different element.
|
|
1001
|
+
*
|
|
1002
|
+
* @param shape - The shape.
|
|
1003
|
+
* @returns The element to adopt, or null to render nothing.
|
|
1004
|
+
* @public
|
|
1005
|
+
*/
|
|
1006
|
+
getAppOwnedElement?(shape: Shape): HTMLElement | null
|
|
1007
|
+
|
|
1008
|
+
/**
|
|
1009
|
+
* A callback called before the shape's app-owned element slot is destroyed: when the shape
|
|
1010
|
+
* unmounts (for example when it is deleted or the current page changes) or when the whole
|
|
1011
|
+
* editor unmounts, including error teardown. The slot is still connected to the document
|
|
1012
|
+
* when this is called, so the app can move the element to another connected parent with
|
|
1013
|
+
* `Node.moveBefore` to preserve its state. An element left in the slot is destroyed along
|
|
1014
|
+
* with it.
|
|
1015
|
+
*
|
|
1016
|
+
* This is not only a "shape deleted" signal: it also fires on page changes, editor unmount,
|
|
1017
|
+
* and (in dev under React StrictMode) on the throwaway mount/unmount cycle while the shape
|
|
1018
|
+
* still exists. Make it safe to call when the shape is still present, and check the store
|
|
1019
|
+
* (`this.editor.getShape(shape.id)`) if you need to distinguish deletion from a remount.
|
|
1020
|
+
*
|
|
1021
|
+
* @param shape - The shape.
|
|
1022
|
+
* @param element - The element returned by {@link ShapeUtil.getAppOwnedElement}.
|
|
1023
|
+
* @public
|
|
1024
|
+
*/
|
|
1025
|
+
onReleaseAppOwnedElement?(shape: Shape, element: HTMLElement): void
|
|
985
1026
|
}
|
|
986
1027
|
|
|
987
1028
|
/**
|
|
@@ -4,6 +4,7 @@ import { Vec } from '../../../../primitives/Vec'
|
|
|
4
4
|
import type { Editor } from '../../../Editor'
|
|
5
5
|
import { TLBaseBoxShape } from '../../../shapes/BaseBoxShapeUtil'
|
|
6
6
|
import { TLPointerEventInfo } from '../../../types/event-types'
|
|
7
|
+
import { cancelShapeCreationOnLongPress } from '../../cancelShapeCreationOnLongPress'
|
|
7
8
|
import { StateNode } from '../../StateNode'
|
|
8
9
|
import type { BaseBoxShapeTool } from '../BaseBoxShapeTool'
|
|
9
10
|
|
|
@@ -65,6 +66,10 @@ export class Pointing extends StateNode {
|
|
|
65
66
|
this.complete()
|
|
66
67
|
}
|
|
67
68
|
|
|
69
|
+
override onLongPress() {
|
|
70
|
+
cancelShapeCreationOnLongPress(this.editor, () => this.cancel())
|
|
71
|
+
}
|
|
72
|
+
|
|
68
73
|
override onCancel() {
|
|
69
74
|
this.cancel()
|
|
70
75
|
}
|
|
@@ -268,8 +268,6 @@ export abstract class StateNode implements Partial<TLEventHandlers> {
|
|
|
268
268
|
onLongPress?(info: TLPointerEventInfo): void
|
|
269
269
|
onPointerUp?(info: TLPointerEventInfo): void
|
|
270
270
|
onDoubleClick?(info: TLClickEventInfo): void
|
|
271
|
-
onTripleClick?(info: TLClickEventInfo): void
|
|
272
|
-
onQuadrupleClick?(info: TLClickEventInfo): void
|
|
273
271
|
onRightClick?(info: TLPointerEventInfo): void
|
|
274
272
|
onMiddleClick?(info: TLPointerEventInfo): void
|
|
275
273
|
onKeyDown?(info: TLKeyboardEventInfo): void
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Editor } from '../Editor'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Shared handler for a long-press in a discrete shape-creation tool's pointing
|
|
5
|
+
* state (geo, note, line, text, arrow, frame). On a coarse pointer this cancels
|
|
6
|
+
* any pending creation so the long-press leaves no shape behind (issue #8277).
|
|
7
|
+
* It does not open a context menu — that is a select-tool surface, reached on
|
|
8
|
+
* touch only from the select tool, and on desktop from a right-click (which the
|
|
9
|
+
* editor routes through the select tool).
|
|
10
|
+
*
|
|
11
|
+
* Gated on `isCoarsePointer`: on a fine pointer (mouse) the long-press timer
|
|
12
|
+
* powers the deliberate "pause before drag to start a precise arrow" gesture,
|
|
13
|
+
* which we must not disturb. The bug only manifests on coarse pointers, because
|
|
14
|
+
* that is also where the browser fires `contextmenu` at the long-press mark.
|
|
15
|
+
*
|
|
16
|
+
* The `cancelPendingCreation` callback is the tool's own cleanup — it knows
|
|
17
|
+
* whether it needs to `bailToMark` a shape it created early or simply transition
|
|
18
|
+
* back to idle. Continuous-gesture tools (draw, highlight, eraser, laser) are
|
|
19
|
+
* excluded: their press is their own action, so they handle (or ignore) the
|
|
20
|
+
* long-press themselves.
|
|
21
|
+
*
|
|
22
|
+
* @internal
|
|
23
|
+
*/
|
|
24
|
+
export function cancelShapeCreationOnLongPress(
|
|
25
|
+
editor: Editor,
|
|
26
|
+
cancelPendingCreation: () => void
|
|
27
|
+
): void {
|
|
28
|
+
if (!editor.getInstanceState().isCoarsePointer) return
|
|
29
|
+
|
|
30
|
+
cancelPendingCreation()
|
|
31
|
+
}
|
|
@@ -24,7 +24,7 @@ export type TLPointerEventName =
|
|
|
24
24
|
| 'middle_click'
|
|
25
25
|
|
|
26
26
|
/** @public */
|
|
27
|
-
export type TLCLickEventName = 'double_click'
|
|
27
|
+
export type TLCLickEventName = 'double_click'
|
|
28
28
|
|
|
29
29
|
/** @public */
|
|
30
30
|
export type TLPinchEventName = 'pinch_start' | 'pinch' | 'pinch_end'
|
|
@@ -63,6 +63,13 @@ export type TLPointerEventInfo = TLBaseEventInfo & {
|
|
|
63
63
|
pointerId: number
|
|
64
64
|
button: number
|
|
65
65
|
isPen: boolean
|
|
66
|
+
/**
|
|
67
|
+
* Whether this pen event appears to be direct manipulation on the display (e.g. Apple Pencil on
|
|
68
|
+
* an iPad or a Surface Pen on a touchscreen) rather than indirect input from a desktop graphics
|
|
69
|
+
* tablet (e.g. a Wacom Intuos). Only direct-display pens should auto-enable pen mode. Drawing and
|
|
70
|
+
* pressure behavior is driven by `isPen` and applies to all pens regardless of this flag.
|
|
71
|
+
*/
|
|
72
|
+
isPenDirect?: boolean
|
|
66
73
|
} & TLPointerEventTarget
|
|
67
74
|
|
|
68
75
|
/** @public */
|
|
@@ -72,7 +79,7 @@ export type TLClickEventInfo = TLBaseEventInfo & {
|
|
|
72
79
|
point: VecLike
|
|
73
80
|
pointerId: number
|
|
74
81
|
button: number
|
|
75
|
-
phase: 'down' | 'up' | 'settle'
|
|
82
|
+
phase: 'down' | 'up' | 'settle-down' | 'settle-up'
|
|
76
83
|
} & TLPointerEventTarget
|
|
77
84
|
|
|
78
85
|
/** @public */
|
|
@@ -173,8 +180,6 @@ export interface TLEventHandlers {
|
|
|
173
180
|
onLongPress: TLPointerEvent
|
|
174
181
|
onRightClick: TLPointerEvent
|
|
175
182
|
onDoubleClick: TLClickEvent
|
|
176
|
-
onTripleClick: TLClickEvent
|
|
177
|
-
onQuadrupleClick: TLClickEvent
|
|
178
183
|
onMiddleClick: TLPointerEvent
|
|
179
184
|
onPointerUp: TLPointerEvent
|
|
180
185
|
onKeyDown: TLKeyboardEvent
|
|
@@ -206,7 +211,5 @@ export const EVENT_NAME_MAP: Record<
|
|
|
206
211
|
complete: 'onComplete',
|
|
207
212
|
interrupt: 'onInterrupt',
|
|
208
213
|
double_click: 'onDoubleClick',
|
|
209
|
-
triple_click: 'onTripleClick',
|
|
210
|
-
quadruple_click: 'onQuadrupleClick',
|
|
211
214
|
tick: 'onTick',
|
|
212
215
|
}
|
|
@@ -17,6 +17,10 @@ const tlenv = {
|
|
|
17
17
|
isWebview: false,
|
|
18
18
|
isDarwin: false,
|
|
19
19
|
hasCanvasSupport: false,
|
|
20
|
+
// Whether the device has a touch screen (an integrated coarse pointer, e.g. an iPad or a
|
|
21
|
+
// touchscreen laptop). Unlike `isCoarsePointer`, this reflects the device's hardware rather than
|
|
22
|
+
// the pointer currently in use, so it stays stable when a pen or mouse is used.
|
|
23
|
+
isTouchDevice: false,
|
|
20
24
|
}
|
|
21
25
|
|
|
22
26
|
let isForcedFinePointer = false
|
|
@@ -32,6 +36,9 @@ if (typeof window !== 'undefined') {
|
|
|
32
36
|
}
|
|
33
37
|
tlenv.hasCanvasSupport = 'Promise' in window && 'HTMLCanvasElement' in window
|
|
34
38
|
isForcedFinePointer = tlenv.isFirefox && !tlenv.isAndroid && !tlenv.isIos
|
|
39
|
+
tlenv.isTouchDevice =
|
|
40
|
+
('navigator' in window && window.navigator.maxTouchPoints > 0) ||
|
|
41
|
+
(typeof window.matchMedia === 'function' && window.matchMedia('(any-pointer: coarse)').matches)
|
|
35
42
|
}
|
|
36
43
|
|
|
37
44
|
/**
|
|
@@ -2,7 +2,6 @@ import { ComponentType, RefAttributes, createContext, useContext } from 'react'
|
|
|
2
2
|
import type { TLCanvasComponentProps } from '../components/default-components/DefaultCanvas'
|
|
3
3
|
import type { TLErrorFallbackComponent } from '../components/default-components/DefaultErrorFallback'
|
|
4
4
|
import type { TLGridProps } from '../components/default-components/DefaultGrid'
|
|
5
|
-
import type { TLSelectionBackgroundProps } from '../components/default-components/DefaultSelectionBackground'
|
|
6
5
|
import type { TLShapeErrorFallbackComponent } from '../components/default-components/DefaultShapeErrorFallback'
|
|
7
6
|
import type { TLShapeWrapperProps } from '../components/default-components/DefaultShapeWrapper'
|
|
8
7
|
|
|
@@ -14,7 +13,6 @@ export interface TLEditorComponents {
|
|
|
14
13
|
InFrontOfTheCanvas?: ComponentType | null
|
|
15
14
|
LoadingScreen?: ComponentType | null
|
|
16
15
|
OnTheCanvas?: ComponentType | null
|
|
17
|
-
SelectionBackground?: ComponentType<TLSelectionBackgroundProps> | null
|
|
18
16
|
ShapeWrapper?: ComponentType<TLShapeWrapperProps & RefAttributes<HTMLDivElement>> | null
|
|
19
17
|
Spinner?: ComponentType<React.SVGProps<SVGSVGElement>> | null
|
|
20
18
|
SvgDefs?: ComponentType | null
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
setPointerCapture,
|
|
9
9
|
} from '../utils/dom'
|
|
10
10
|
import { getPointerInfo } from '../utils/getPointerInfo'
|
|
11
|
+
import { getPointerEventButton, isDirectDisplayPen, isSecondaryClickEvent } from '../utils/pointer'
|
|
11
12
|
import { useEditor } from './useEditor'
|
|
12
13
|
|
|
13
14
|
export function useCanvasEvents() {
|
|
@@ -17,12 +18,16 @@ export function useCanvasEvents() {
|
|
|
17
18
|
|
|
18
19
|
const events = useMemo(
|
|
19
20
|
function canvasEvents() {
|
|
21
|
+
let isSecondaryClickPointerDown = false
|
|
22
|
+
|
|
20
23
|
function onPointerDown(e: React.PointerEvent) {
|
|
21
24
|
if (editor.wasEventAlreadyHandled(e)) return
|
|
25
|
+
const button = getPointerEventButton(e)
|
|
26
|
+
isSecondaryClickPointerDown = button === 2
|
|
22
27
|
|
|
23
28
|
// With right-click panning disabled, fire right_click on press and let the
|
|
24
29
|
// native contextmenu through so the menu opens at the pointer-down location.
|
|
25
|
-
if (
|
|
30
|
+
if (button === 2 && !editor.options.rightClickPanning) {
|
|
26
31
|
editor.dispatch({
|
|
27
32
|
type: 'pointer',
|
|
28
33
|
target: 'canvas',
|
|
@@ -32,7 +37,11 @@ export function useCanvasEvents() {
|
|
|
32
37
|
return
|
|
33
38
|
}
|
|
34
39
|
|
|
35
|
-
if (
|
|
40
|
+
if (button !== 0 && button !== 1 && button !== 2 && button !== 5) return
|
|
41
|
+
|
|
42
|
+
// Detect direct-display pen input (Apple Pencil, Surface Pen on a touchscreen) so we
|
|
43
|
+
// only auto-enable pen mode for it, not for an indirect desktop tablet stylus.
|
|
44
|
+
const isPenDirect = isDirectDisplayPen(e)
|
|
36
45
|
|
|
37
46
|
setPointerCapture(e.currentTarget, e)
|
|
38
47
|
|
|
@@ -41,17 +50,19 @@ export function useCanvasEvents() {
|
|
|
41
50
|
target: 'canvas',
|
|
42
51
|
name: 'pointer_down',
|
|
43
52
|
...getPointerInfo(editor, e),
|
|
53
|
+
isPenDirect,
|
|
44
54
|
})
|
|
45
55
|
}
|
|
46
56
|
|
|
47
57
|
function onPointerUp(e: React.PointerEvent) {
|
|
48
58
|
if (editor.wasEventAlreadyHandled(e)) return
|
|
49
|
-
|
|
59
|
+
const button = isSecondaryClickPointerDown ? 2 : getPointerEventButton(e)
|
|
60
|
+
if (button !== 0 && button !== 1 && button !== 2 && button !== 5) return
|
|
50
61
|
|
|
51
62
|
const rightClickPanning = editor.options.rightClickPanning
|
|
52
63
|
// Check before dispatch (which resets isPanning)
|
|
53
64
|
const wasRightClickPanning =
|
|
54
|
-
rightClickPanning &&
|
|
65
|
+
rightClickPanning && button === 2 && editor.inputs.getIsPanning()
|
|
55
66
|
|
|
56
67
|
releasePointerCapture(e.currentTarget, e)
|
|
57
68
|
|
|
@@ -60,10 +71,11 @@ export function useCanvasEvents() {
|
|
|
60
71
|
target: 'canvas',
|
|
61
72
|
name: 'pointer_up',
|
|
62
73
|
...getPointerInfo(editor, e),
|
|
74
|
+
button,
|
|
63
75
|
})
|
|
64
76
|
|
|
65
77
|
// Static right-click: fire contextmenu at the pointer-up location
|
|
66
|
-
if (rightClickPanning &&
|
|
78
|
+
if (rightClickPanning && button === 2 && !wasRightClickPanning) {
|
|
67
79
|
const contextMenuEvent = new PointerEvent('contextmenu', {
|
|
68
80
|
bubbles: true,
|
|
69
81
|
clientX: e.clientX,
|
|
@@ -76,6 +88,7 @@ export function useCanvasEvents() {
|
|
|
76
88
|
})
|
|
77
89
|
e.currentTarget.dispatchEvent(contextMenuEvent)
|
|
78
90
|
}
|
|
91
|
+
isSecondaryClickPointerDown = false
|
|
79
92
|
}
|
|
80
93
|
|
|
81
94
|
function onPointerEnter(e: React.PointerEvent) {
|
|
@@ -171,16 +184,15 @@ export function useCanvasEvents() {
|
|
|
171
184
|
// Synthetic events — our own dispatch from onPointerUp, or tests using
|
|
172
185
|
// fireEvent.contextMenu — pass through so Radix can open the menu.
|
|
173
186
|
if (!e.nativeEvent.isTrusted) return
|
|
174
|
-
// Only suppress the native browser contextmenu when it follows a
|
|
175
|
-
//
|
|
176
|
-
//
|
|
177
|
-
//
|
|
178
|
-
//
|
|
187
|
+
// Only suppress the native browser contextmenu when it follows a
|
|
188
|
+
// secondary click. For those, our pointer handling has already
|
|
189
|
+
// decided what to do (either we'll dispatch a synthetic contextmenu on
|
|
190
|
+
// pointerup to open the menu at the release position, or we panned and
|
|
191
|
+
// don't want a menu at all).
|
|
179
192
|
//
|
|
180
193
|
// Other contextmenu sources must reach Radix so the menu opens:
|
|
181
|
-
// - ctrl+click on macOS (button=0, or button=2 with ctrlKey=true)
|
|
182
194
|
// - long-press on touch devices (button=0, pointerType=touch)
|
|
183
|
-
if (e
|
|
195
|
+
if (!isSecondaryClickEvent(e)) return
|
|
184
196
|
preventDefault(e)
|
|
185
197
|
}
|
|
186
198
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useValue } from '@tldraw/state-react'
|
|
2
|
-
import { TLInstancePresence } from '@tldraw/tlschema'
|
|
2
|
+
import { TLInstancePresence, TLUserId } from '@tldraw/tlschema'
|
|
3
3
|
import { useEditor } from './useEditor'
|
|
4
4
|
|
|
5
5
|
// TODO: maybe move this to a computed property on the App class?
|
|
@@ -7,7 +7,7 @@ import { useEditor } from './useEditor'
|
|
|
7
7
|
* @returns The latest presence of the user matching userId
|
|
8
8
|
* @public
|
|
9
9
|
*/
|
|
10
|
-
export function usePresence(userId:
|
|
10
|
+
export function usePresence(userId: TLUserId): TLInstancePresence | null {
|
|
11
11
|
const editor = useEditor()
|
|
12
12
|
|
|
13
13
|
const latestPresence = useValue(
|
|
@@ -11,6 +11,9 @@ export function useScreenBounds(ref: React.RefObject<HTMLElement | null>) {
|
|
|
11
11
|
const updateBounds = throttle(
|
|
12
12
|
() => {
|
|
13
13
|
if (!ref.current) return
|
|
14
|
+
// If the container's document has been torn down (e.g. an iframe
|
|
15
|
+
// being removed), there's nothing to measure — bail.
|
|
16
|
+
if (!ref.current.ownerDocument.body) return
|
|
14
17
|
editor.updateViewportScreenBounds(ref.current)
|
|
15
18
|
},
|
|
16
19
|
200,
|
|
@@ -60,22 +60,30 @@ describe('LicenseManager', () => {
|
|
|
60
60
|
expect(result).toMatchObject({ isLicenseParseable: false, reason: 'no-key-provided' })
|
|
61
61
|
})
|
|
62
62
|
|
|
63
|
-
it('Signals that it is development mode
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
63
|
+
it('Signals that it is development mode for loopback hosts', async () => {
|
|
64
|
+
process.env.NODE_ENV = 'production'
|
|
65
|
+
try {
|
|
66
|
+
const schemes = ['http', 'https']
|
|
67
|
+
const hosts = ['localhost', '127.0.0.1', '127.0.0.2', '[::1]']
|
|
68
|
+
for (const scheme of schemes) {
|
|
69
|
+
for (const host of hosts) {
|
|
70
|
+
// @ts-ignore
|
|
71
|
+
delete window.location
|
|
72
|
+
// @ts-ignore
|
|
73
|
+
window.location = new URL(`${scheme}://${host}:3000`)
|
|
74
|
+
|
|
75
|
+
const testEnvLicenseManager = new LicenseManager('', keyPair.publicKey)
|
|
76
|
+
const licenseKey = await generateLicenseKey(STANDARD_LICENSE_INFO, keyPair)
|
|
77
|
+
const result = await testEnvLicenseManager.getLicenseFromKey(licenseKey)
|
|
78
|
+
expect(result).toMatchObject({
|
|
79
|
+
isLicenseParseable: true,
|
|
80
|
+
isDomainValid: false,
|
|
81
|
+
isDevelopment: true,
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
} finally {
|
|
86
|
+
process.env.NODE_ENV = 'test'
|
|
79
87
|
}
|
|
80
88
|
})
|
|
81
89
|
|
|
@@ -125,14 +125,20 @@ export class LicenseManager {
|
|
|
125
125
|
}
|
|
126
126
|
|
|
127
127
|
private getIsDevelopment() {
|
|
128
|
-
// If we are using https on a non-
|
|
128
|
+
// If we are using https on a non-loopback domain we assume it's a production env and a development one otherwise
|
|
129
129
|
return (
|
|
130
130
|
!['https:', 'vscode-webview:'].includes(window.location.protocol) ||
|
|
131
|
-
window.location.hostname
|
|
131
|
+
this.isLoopbackHost(window.location.hostname) ||
|
|
132
132
|
process.env.NODE_ENV !== 'production'
|
|
133
133
|
)
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
+
private isLoopbackHost(hostname: string) {
|
|
137
|
+
// localhost, IPv4 loopback (127.0.0.0/8) and IPv6 loopback (::1) are all local development hosts
|
|
138
|
+
const host = hostname.toLowerCase().replace(/^\[|\]$/g, '')
|
|
139
|
+
return host === 'localhost' || host === '::1' || /^127(?:\.\d{1,3}){3}$/.test(host)
|
|
140
|
+
}
|
|
141
|
+
|
|
136
142
|
private getTrackType(result: LicenseFromKeyResult, licenseState: LicenseState): TrackType {
|
|
137
143
|
// Track watermark for unlicensed production deployments
|
|
138
144
|
if (licenseState === 'unlicensed-production') {
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { VecModel } from '@tldraw/tlschema'
|
|
2
2
|
import { EASINGS } from './easings'
|
|
3
|
+
|
|
4
|
+
const { easeInQuad, easeInOutQuad } = EASINGS
|
|
5
|
+
|
|
3
6
|
function clamp(n: number, min: number, max?: number): number {
|
|
4
7
|
return Math.max(min, typeof max !== 'undefined' ? Math.min(n, max) : n)
|
|
5
8
|
}
|
|
@@ -634,13 +637,13 @@ export class Vec {
|
|
|
634
637
|
* @param steps - The number of points to return.
|
|
635
638
|
* @param ease - The easing to use.
|
|
636
639
|
*/
|
|
637
|
-
static PointsBetween(A: VecModel, B: VecModel, steps = 6, ease =
|
|
640
|
+
static PointsBetween(A: VecModel, B: VecModel, steps = 6, ease = easeInQuad): Vec[] {
|
|
638
641
|
const results: Vec[] = []
|
|
639
642
|
|
|
640
643
|
for (let i = 0; i < steps; i++) {
|
|
641
644
|
const t = ease(i / (steps - 1))
|
|
642
645
|
const point = Vec.Lrp(A, B, t)
|
|
643
|
-
point.z = Math.min(1, 0.5 + Math.abs(0.5 -
|
|
646
|
+
point.z = Math.min(1, 0.5 + Math.abs(0.5 - easeInOutQuad(t)) * 0.65)
|
|
644
647
|
results.push(point)
|
|
645
648
|
}
|
|
646
649
|
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { act, render } from '@testing-library/react'
|
|
2
|
+
import { BaseBoxShapeUtil, createShapeId, Editor, RecordProps, T, TLShape } from '../..'
|
|
3
|
+
import { TldrawEditor } from '../TldrawEditor'
|
|
4
|
+
|
|
5
|
+
const PERSISTENT_SHAPE_TYPE = 'test-persistent'
|
|
6
|
+
|
|
7
|
+
declare module '@tldraw/tlschema' {
|
|
8
|
+
export interface TLGlobalShapePropsMap {
|
|
9
|
+
[PERSISTENT_SHAPE_TYPE]: { w: number; h: number }
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
type IPersistentShape = TLShape<typeof PERSISTENT_SHAPE_TYPE>
|
|
14
|
+
|
|
15
|
+
// App-owned element registry and parking lot, the way an app using
|
|
16
|
+
// getContentElement would keep elements alive across editor sessions.
|
|
17
|
+
const elements = new Map<string, HTMLElement>()
|
|
18
|
+
let parkingLot: HTMLDivElement
|
|
19
|
+
const releaseCalls: Array<{ shapeId: string; element: HTMLElement; slotWasConnected: boolean }> = []
|
|
20
|
+
|
|
21
|
+
class PersistentShapeUtil extends BaseBoxShapeUtil<IPersistentShape> {
|
|
22
|
+
static override type = PERSISTENT_SHAPE_TYPE
|
|
23
|
+
static override props: RecordProps<IPersistentShape> = {
|
|
24
|
+
w: T.number,
|
|
25
|
+
h: T.number,
|
|
26
|
+
}
|
|
27
|
+
getDefaultProps(): IPersistentShape['props'] {
|
|
28
|
+
return { w: 100, h: 100 }
|
|
29
|
+
}
|
|
30
|
+
getIndicatorPath() {
|
|
31
|
+
return undefined
|
|
32
|
+
}
|
|
33
|
+
component() {
|
|
34
|
+
return null
|
|
35
|
+
}
|
|
36
|
+
override getAppOwnedElement(shape: IPersistentShape) {
|
|
37
|
+
let element = elements.get(shape.id)
|
|
38
|
+
if (!element) {
|
|
39
|
+
element = document.createElement('div')
|
|
40
|
+
element.dataset.testElementFor = shape.id
|
|
41
|
+
elements.set(shape.id, element)
|
|
42
|
+
}
|
|
43
|
+
return element
|
|
44
|
+
}
|
|
45
|
+
override onReleaseAppOwnedElement(shape: IPersistentShape, element: HTMLElement) {
|
|
46
|
+
releaseCalls.push({
|
|
47
|
+
shapeId: shape.id,
|
|
48
|
+
element,
|
|
49
|
+
slotWasConnected: element.parentElement?.isConnected ?? false,
|
|
50
|
+
})
|
|
51
|
+
parkingLot.appendChild(element)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function getSlotFor(shapeId: string) {
|
|
56
|
+
return document.querySelector(`[data-shape-id="${shapeId}"] .tl-content-slot`)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
describe('content element slot', () => {
|
|
60
|
+
beforeEach(() => {
|
|
61
|
+
elements.clear()
|
|
62
|
+
releaseCalls.length = 0
|
|
63
|
+
parkingLot = document.createElement('div')
|
|
64
|
+
document.body.appendChild(parkingLot)
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
afterEach(() => {
|
|
68
|
+
parkingLot.remove()
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
async function renderEditor() {
|
|
72
|
+
let editor!: Editor
|
|
73
|
+
const result = await act(async () =>
|
|
74
|
+
render(
|
|
75
|
+
<TldrawEditor
|
|
76
|
+
shapeUtils={[PersistentShapeUtil]}
|
|
77
|
+
onMount={(e) => {
|
|
78
|
+
editor = e
|
|
79
|
+
}}
|
|
80
|
+
/>
|
|
81
|
+
)
|
|
82
|
+
)
|
|
83
|
+
return { ...result, editor }
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
it('adopts the app-owned element into the shape and releases it before editor unmount', async () => {
|
|
87
|
+
const { editor, unmount } = await renderEditor()
|
|
88
|
+
const id = createShapeId()
|
|
89
|
+
|
|
90
|
+
await act(async () => {
|
|
91
|
+
editor.createShape<IPersistentShape>({ id, type: PERSISTENT_SHAPE_TYPE, x: 0, y: 0 })
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
const element = elements.get(id)!
|
|
95
|
+
expect(element).toBeDefined()
|
|
96
|
+
expect(element.parentElement).toBe(getSlotFor(id))
|
|
97
|
+
expect(element.isConnected).toBe(true)
|
|
98
|
+
|
|
99
|
+
await act(async () => {
|
|
100
|
+
unmount()
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
// The release callback fired exactly once, while the slot was still
|
|
104
|
+
// connected to the document, so a state-preserving move was possible.
|
|
105
|
+
expect(releaseCalls).toHaveLength(1)
|
|
106
|
+
expect(releaseCalls[0]).toMatchObject({ shapeId: id, element, slotWasConnected: true })
|
|
107
|
+
// The app reclaimed the element into its parking lot and it survived teardown.
|
|
108
|
+
expect(element.parentElement).toBe(parkingLot)
|
|
109
|
+
expect(element.isConnected).toBe(true)
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it('releases the element when the shape is deleted', async () => {
|
|
113
|
+
const { editor, unmount } = await renderEditor()
|
|
114
|
+
const id = createShapeId()
|
|
115
|
+
|
|
116
|
+
await act(async () => {
|
|
117
|
+
editor.createShape<IPersistentShape>({ id, type: PERSISTENT_SHAPE_TYPE, x: 0, y: 0 })
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
const element = elements.get(id)!
|
|
121
|
+
expect(element.parentElement).toBe(getSlotFor(id))
|
|
122
|
+
|
|
123
|
+
await act(async () => {
|
|
124
|
+
editor.deleteShape(id)
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
expect(releaseCalls).toHaveLength(1)
|
|
128
|
+
expect(releaseCalls[0]).toMatchObject({ shapeId: id, element, slotWasConnected: true })
|
|
129
|
+
expect(element.parentElement).toBe(parkingLot)
|
|
130
|
+
|
|
131
|
+
await act(async () => {
|
|
132
|
+
unmount()
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
// The slot is gone; no second release for the deleted shape.
|
|
136
|
+
expect(releaseCalls).toHaveLength(1)
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
it('re-adopts a parked element when the editor remounts, without recreating it', async () => {
|
|
140
|
+
const first = await renderEditor()
|
|
141
|
+
const id = createShapeId()
|
|
142
|
+
|
|
143
|
+
await act(async () => {
|
|
144
|
+
first.editor.createShape<IPersistentShape>({ id, type: PERSISTENT_SHAPE_TYPE, x: 0, y: 0 })
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
const element = elements.get(id)!
|
|
148
|
+
|
|
149
|
+
await act(async () => {
|
|
150
|
+
first.unmount()
|
|
151
|
+
})
|
|
152
|
+
expect(element.parentElement).toBe(parkingLot)
|
|
153
|
+
|
|
154
|
+
const second = await renderEditor()
|
|
155
|
+
await act(async () => {
|
|
156
|
+
second.editor.createShape<IPersistentShape>({ id, type: PERSISTENT_SHAPE_TYPE, x: 0, y: 0 })
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
// The same element instance was adopted into the new editor's slot.
|
|
160
|
+
expect(elements.get(id)).toBe(element)
|
|
161
|
+
expect(element.parentElement).toBe(getSlotFor(id))
|
|
162
|
+
|
|
163
|
+
await act(async () => {
|
|
164
|
+
second.unmount()
|
|
165
|
+
})
|
|
166
|
+
expect(element.parentElement).toBe(parkingLot)
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
it('keeps the element in the same DOM position when z-order changes', async () => {
|
|
170
|
+
const { editor, unmount } = await renderEditor()
|
|
171
|
+
const id = createShapeId()
|
|
172
|
+
const otherId = createShapeId()
|
|
173
|
+
|
|
174
|
+
await act(async () => {
|
|
175
|
+
editor.createShape<IPersistentShape>({ id, type: PERSISTENT_SHAPE_TYPE, x: 0, y: 0 })
|
|
176
|
+
editor.createShape<IPersistentShape>({
|
|
177
|
+
id: otherId,
|
|
178
|
+
type: PERSISTENT_SHAPE_TYPE,
|
|
179
|
+
x: 50,
|
|
180
|
+
y: 50,
|
|
181
|
+
})
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
const element = elements.get(id)!
|
|
185
|
+
const slot = getSlotFor(id)
|
|
186
|
+
expect(element.parentElement).toBe(slot)
|
|
187
|
+
|
|
188
|
+
await act(async () => {
|
|
189
|
+
editor.sendToBack([id])
|
|
190
|
+
editor.bringToFront([id])
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
// No release fired and the element is still in the same slot node.
|
|
194
|
+
expect(releaseCalls).toHaveLength(0)
|
|
195
|
+
expect(element.parentElement).toBe(slot)
|
|
196
|
+
|
|
197
|
+
await act(async () => {
|
|
198
|
+
unmount()
|
|
199
|
+
})
|
|
200
|
+
})
|
|
201
|
+
})
|
package/src/lib/utils/dom.ts
CHANGED
|
@@ -100,6 +100,40 @@ export function setStyleProperty(
|
|
|
100
100
|
elm.style.setProperty(property, String(value))
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
+
/**
|
|
104
|
+
* Move an element into a new parent, preserving its state where the platform allows it.
|
|
105
|
+
*
|
|
106
|
+
* Uses `Node.moveBefore` (Chromium 133+, Firefox 144+) when both the element and parent are
|
|
107
|
+
* connected to the same document — this moves the element without resetting its state, so
|
|
108
|
+
* iframes don't reload and media keeps playing. Otherwise (older browsers, disconnected nodes,
|
|
109
|
+
* or a cross-document move) it falls back to `appendChild`, which moves the element but resets
|
|
110
|
+
* its state like an iframe reload.
|
|
111
|
+
*
|
|
112
|
+
* This is the primitive tldraw uses to adopt `ShapeUtil.getAppOwnedElement` elements, exposed so
|
|
113
|
+
* apps can perform symmetric state-preserving moves from `ShapeUtil.onReleaseAppOwnedElement` —
|
|
114
|
+
* for example moving an element to an off-canvas parking lot between editor sessions.
|
|
115
|
+
*
|
|
116
|
+
* @param parent - The element to move `element` into, as its last child.
|
|
117
|
+
* @param element - The element to move.
|
|
118
|
+
* @public
|
|
119
|
+
*/
|
|
120
|
+
export function moveElementInto(parent: HTMLElement, element: HTMLElement) {
|
|
121
|
+
if (
|
|
122
|
+
element.isConnected &&
|
|
123
|
+
parent.isConnected &&
|
|
124
|
+
typeof (parent as any).moveBefore === 'function' &&
|
|
125
|
+
element.ownerDocument === parent.ownerDocument
|
|
126
|
+
) {
|
|
127
|
+
try {
|
|
128
|
+
;(parent as any).moveBefore(element, null)
|
|
129
|
+
return
|
|
130
|
+
} catch {
|
|
131
|
+
// fall through to appendChild
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
parent.appendChild(element)
|
|
135
|
+
}
|
|
136
|
+
|
|
103
137
|
/** @internal */
|
|
104
138
|
export function elementShouldCaptureKeys(el: Element | null, includeButtonsAndMenus = true) {
|
|
105
139
|
if (!el) return false
|