tldraw 5.4.0-next.ef99ab47e5ce → 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/DOCS.md +2826 -3226
- package/RELEASE_NOTES.md +190 -2
- package/dist-cjs/index.d.ts +264 -275
- package/dist-cjs/index.js +1 -1
- package/dist-cjs/lib/defaultExternalContentHandlers.js +32 -16
- package/dist-cjs/lib/defaultExternalContentHandlers.js.map +2 -2
- package/dist-cjs/lib/shapes/arrow/curved-arrow.js +2 -2
- package/dist-cjs/lib/shapes/arrow/curved-arrow.js.map +2 -2
- package/dist-cjs/lib/shapes/arrow/elbow/getElbowArrowInfo.js +2 -2
- package/dist-cjs/lib/shapes/arrow/elbow/getElbowArrowInfo.js.map +2 -2
- package/dist-cjs/lib/shapes/arrow/getArrowInfo.js +1 -1
- package/dist-cjs/lib/shapes/arrow/getArrowInfo.js.map +2 -2
- package/dist-cjs/lib/shapes/arrow/straight-arrow.js +2 -2
- package/dist-cjs/lib/shapes/arrow/straight-arrow.js.map +2 -2
- package/dist-cjs/lib/shapes/arrow/toolStates/Idle.js +2 -3
- package/dist-cjs/lib/shapes/arrow/toolStates/Idle.js.map +2 -2
- package/dist-cjs/lib/shapes/arrow/toolStates/Pointing.js +7 -5
- package/dist-cjs/lib/shapes/arrow/toolStates/Pointing.js.map +2 -2
- package/dist-cjs/lib/shapes/draw/toolStates/Drawing.js +8 -6
- package/dist-cjs/lib/shapes/draw/toolStates/Drawing.js.map +2 -2
- package/dist-cjs/lib/shapes/geo/getGeoShapePath.js +3 -1
- package/dist-cjs/lib/shapes/geo/getGeoShapePath.js.map +2 -2
- package/dist-cjs/lib/shapes/geo/toolStates/Idle.js +2 -3
- package/dist-cjs/lib/shapes/geo/toolStates/Idle.js.map +2 -2
- package/dist-cjs/lib/shapes/geo/toolStates/Pointing.js +1 -1
- package/dist-cjs/lib/shapes/geo/toolStates/Pointing.js.map +2 -2
- package/dist-cjs/lib/shapes/line/toolStates/Pointing.js +2 -6
- package/dist-cjs/lib/shapes/line/toolStates/Pointing.js.map +2 -2
- package/dist-cjs/lib/shapes/note/NoteShapeUtil.js +4 -4
- package/dist-cjs/lib/shapes/note/NoteShapeUtil.js.map +2 -2
- package/dist-cjs/lib/shapes/note/noteHelpers.js +6 -5
- package/dist-cjs/lib/shapes/note/noteHelpers.js.map +2 -2
- package/dist-cjs/lib/shapes/note/toolStates/Pointing.js +1 -1
- package/dist-cjs/lib/shapes/note/toolStates/Pointing.js.map +2 -2
- package/dist-cjs/lib/shapes/shared/crop.js +1 -1
- package/dist-cjs/lib/shapes/shared/crop.js.map +2 -2
- package/dist-cjs/lib/shapes/text/toolStates/Idle.js +1 -1
- package/dist-cjs/lib/shapes/text/toolStates/Idle.js.map +2 -2
- package/dist-cjs/lib/shapes/text/toolStates/Pointing.js +1 -0
- package/dist-cjs/lib/shapes/text/toolStates/Pointing.js.map +2 -2
- package/dist-cjs/lib/shapes/video/VideoShapeUtil.js +3 -2
- package/dist-cjs/lib/shapes/video/VideoShapeUtil.js.map +2 -2
- package/dist-cjs/lib/tools/EraserTool/childStates/Erasing.js +2 -0
- package/dist-cjs/lib/tools/EraserTool/childStates/Erasing.js.map +2 -2
- package/dist-cjs/lib/tools/HandTool/childStates/OneFingerZooming.js +6 -0
- package/dist-cjs/lib/tools/HandTool/childStates/OneFingerZooming.js.map +2 -2
- package/dist-cjs/lib/tools/SelectTool/SelectTool.js +3 -0
- package/dist-cjs/lib/tools/SelectTool/SelectTool.js.map +2 -2
- package/dist-cjs/lib/tools/SelectTool/childStates/Brushing.js +1 -0
- package/dist-cjs/lib/tools/SelectTool/childStates/Brushing.js.map +2 -2
- package/dist-cjs/lib/tools/SelectTool/childStates/Crop/Crop.js +1 -0
- package/dist-cjs/lib/tools/SelectTool/childStates/Crop/Crop.js.map +2 -2
- package/dist-cjs/lib/tools/SelectTool/childStates/DraggingHandle.js +1 -3
- package/dist-cjs/lib/tools/SelectTool/childStates/DraggingHandle.js.map +2 -2
- package/dist-cjs/lib/tools/SelectTool/childStates/Idle.js +6 -3
- package/dist-cjs/lib/tools/SelectTool/childStates/Idle.js.map +2 -2
- package/dist-cjs/lib/tools/SelectTool/childStates/PointingArrowLabel.js +1 -1
- package/dist-cjs/lib/tools/SelectTool/childStates/PointingArrowLabel.js.map +2 -2
- package/dist-cjs/lib/tools/SelectTool/childStates/PointingCanvas.js +3 -0
- package/dist-cjs/lib/tools/SelectTool/childStates/PointingCanvas.js.map +2 -2
- package/dist-cjs/lib/tools/SelectTool/childStates/PointingHandle.js +1 -0
- package/dist-cjs/lib/tools/SelectTool/childStates/PointingHandle.js.map +2 -2
- package/dist-cjs/lib/tools/SelectTool/childStates/PointingShape.js +1 -1
- package/dist-cjs/lib/tools/SelectTool/childStates/PointingShape.js.map +2 -2
- package/dist-cjs/lib/tools/SelectTool/childStates/Resizing.js +22 -30
- package/dist-cjs/lib/tools/SelectTool/childStates/Resizing.js.map +2 -2
- package/dist-cjs/lib/tools/SelectTool/childStates/ScribbleBrushing.js +1 -3
- package/dist-cjs/lib/tools/SelectTool/childStates/ScribbleBrushing.js.map +2 -2
- package/dist-cjs/lib/tools/SelectTool/childStates/Translating.js +4 -1
- package/dist-cjs/lib/tools/SelectTool/childStates/Translating.js.map +2 -2
- package/dist-cjs/lib/tools/SelectTool/selectHelpers.js +10 -0
- package/dist-cjs/lib/tools/SelectTool/selectHelpers.js.map +2 -2
- package/dist-cjs/lib/ui/TldrawUi.js +2 -3
- package/dist-cjs/lib/ui/TldrawUi.js.map +2 -2
- package/dist-cjs/lib/ui/components/ActionsMenu/DefaultActionsMenu.js +1 -11
- package/dist-cjs/lib/ui/components/ActionsMenu/DefaultActionsMenu.js.map +2 -2
- package/dist-cjs/lib/ui/components/ContextMenu/DefaultContextMenu.js +6 -1
- package/dist-cjs/lib/ui/components/ContextMenu/DefaultContextMenu.js.map +2 -2
- package/dist-cjs/lib/ui/components/ContextMenu/DefaultContextMenuContent.js +1 -1
- package/dist-cjs/lib/ui/components/ContextMenu/DefaultContextMenuContent.js.map +2 -2
- package/dist-cjs/lib/ui/components/Dialogs.js +8 -0
- package/dist-cjs/lib/ui/components/Dialogs.js.map +2 -2
- package/dist-cjs/lib/ui/components/EditLinkDialog.js +5 -5
- package/dist-cjs/lib/ui/components/EditLinkDialog.js.map +2 -2
- package/dist-cjs/lib/ui/components/Minimap/DefaultMinimap.js +4 -20
- package/dist-cjs/lib/ui/components/Minimap/DefaultMinimap.js.map +2 -2
- package/dist-cjs/lib/ui/components/SharePanel/DefaultPeopleMenuItem.js +1 -2
- package/dist-cjs/lib/ui/components/SharePanel/DefaultPeopleMenuItem.js.map +2 -2
- package/dist-cjs/lib/ui/components/StylePanel/StylePanelDoubleDropdownPicker.js +2 -0
- package/dist-cjs/lib/ui/components/StylePanel/StylePanelDoubleDropdownPicker.js.map +2 -2
- package/dist-cjs/lib/ui/components/Toolbar/AltTextEditor.js +1 -0
- package/dist-cjs/lib/ui/components/Toolbar/AltTextEditor.js.map +2 -2
- package/dist-cjs/lib/ui/components/Toolbar/DefaultImageToolbarContent.js +10 -8
- package/dist-cjs/lib/ui/components/Toolbar/DefaultImageToolbarContent.js.map +2 -2
- package/dist-cjs/lib/ui/components/primitives/TldrawUiSlider.js +20 -1
- package/dist-cjs/lib/ui/components/primitives/TldrawUiSlider.js.map +2 -2
- package/dist-cjs/lib/ui/context/actions.js +8 -8
- package/dist-cjs/lib/ui/context/actions.js.map +2 -2
- package/dist-cjs/lib/ui/hooks/useClipboardEvents.js +2 -0
- package/dist-cjs/lib/ui/hooks/useClipboardEvents.js.map +2 -2
- package/dist-cjs/lib/ui/hooks/useCommentingEnabled.js +1 -1
- package/dist-cjs/lib/ui/hooks/useCommentingEnabled.js.map +2 -2
- package/dist-cjs/lib/ui/hooks/useFlatten.js +42 -23
- package/dist-cjs/lib/ui/hooks/useFlatten.js.map +2 -2
- package/dist-cjs/lib/ui/version.js +3 -3
- package/dist-cjs/lib/ui/version.js.map +1 -1
- package/dist-esm/index.d.mts +264 -275
- package/dist-esm/index.mjs +1 -1
- package/dist-esm/lib/defaultExternalContentHandlers.mjs +32 -16
- package/dist-esm/lib/defaultExternalContentHandlers.mjs.map +2 -2
- package/dist-esm/lib/shapes/arrow/curved-arrow.mjs +2 -2
- package/dist-esm/lib/shapes/arrow/curved-arrow.mjs.map +2 -2
- package/dist-esm/lib/shapes/arrow/elbow/getElbowArrowInfo.mjs +2 -2
- package/dist-esm/lib/shapes/arrow/elbow/getElbowArrowInfo.mjs.map +2 -2
- package/dist-esm/lib/shapes/arrow/getArrowInfo.mjs +1 -1
- package/dist-esm/lib/shapes/arrow/getArrowInfo.mjs.map +2 -2
- package/dist-esm/lib/shapes/arrow/straight-arrow.mjs +2 -2
- package/dist-esm/lib/shapes/arrow/straight-arrow.mjs.map +2 -2
- package/dist-esm/lib/shapes/arrow/toolStates/Idle.mjs +3 -4
- package/dist-esm/lib/shapes/arrow/toolStates/Idle.mjs.map +2 -2
- package/dist-esm/lib/shapes/arrow/toolStates/Pointing.mjs +7 -5
- package/dist-esm/lib/shapes/arrow/toolStates/Pointing.mjs.map +2 -2
- package/dist-esm/lib/shapes/draw/toolStates/Drawing.mjs +8 -6
- package/dist-esm/lib/shapes/draw/toolStates/Drawing.mjs.map +2 -2
- package/dist-esm/lib/shapes/geo/getGeoShapePath.mjs +3 -1
- package/dist-esm/lib/shapes/geo/getGeoShapePath.mjs.map +2 -2
- package/dist-esm/lib/shapes/geo/toolStates/Idle.mjs +3 -4
- package/dist-esm/lib/shapes/geo/toolStates/Idle.mjs.map +2 -2
- package/dist-esm/lib/shapes/geo/toolStates/Pointing.mjs +1 -1
- package/dist-esm/lib/shapes/geo/toolStates/Pointing.mjs.map +2 -2
- package/dist-esm/lib/shapes/line/toolStates/Pointing.mjs +2 -7
- package/dist-esm/lib/shapes/line/toolStates/Pointing.mjs.map +2 -2
- package/dist-esm/lib/shapes/note/NoteShapeUtil.mjs +4 -4
- package/dist-esm/lib/shapes/note/NoteShapeUtil.mjs.map +2 -2
- package/dist-esm/lib/shapes/note/noteHelpers.mjs +6 -5
- package/dist-esm/lib/shapes/note/noteHelpers.mjs.map +2 -2
- package/dist-esm/lib/shapes/note/toolStates/Pointing.mjs +1 -1
- package/dist-esm/lib/shapes/note/toolStates/Pointing.mjs.map +2 -2
- package/dist-esm/lib/shapes/shared/crop.mjs +1 -1
- package/dist-esm/lib/shapes/shared/crop.mjs.map +2 -2
- package/dist-esm/lib/shapes/text/toolStates/Idle.mjs +2 -2
- package/dist-esm/lib/shapes/text/toolStates/Idle.mjs.map +2 -2
- package/dist-esm/lib/shapes/text/toolStates/Pointing.mjs +1 -0
- package/dist-esm/lib/shapes/text/toolStates/Pointing.mjs.map +2 -2
- package/dist-esm/lib/shapes/video/VideoShapeUtil.mjs +3 -2
- package/dist-esm/lib/shapes/video/VideoShapeUtil.mjs.map +2 -2
- package/dist-esm/lib/tools/EraserTool/childStates/Erasing.mjs +2 -0
- package/dist-esm/lib/tools/EraserTool/childStates/Erasing.mjs.map +2 -2
- package/dist-esm/lib/tools/HandTool/childStates/OneFingerZooming.mjs +6 -0
- package/dist-esm/lib/tools/HandTool/childStates/OneFingerZooming.mjs.map +2 -2
- package/dist-esm/lib/tools/SelectTool/SelectTool.mjs +3 -0
- package/dist-esm/lib/tools/SelectTool/SelectTool.mjs.map +2 -2
- package/dist-esm/lib/tools/SelectTool/childStates/Brushing.mjs +1 -0
- package/dist-esm/lib/tools/SelectTool/childStates/Brushing.mjs.map +2 -2
- package/dist-esm/lib/tools/SelectTool/childStates/Crop/Crop.mjs +1 -0
- package/dist-esm/lib/tools/SelectTool/childStates/Crop/Crop.mjs.map +2 -2
- package/dist-esm/lib/tools/SelectTool/childStates/DraggingHandle.mjs +1 -3
- package/dist-esm/lib/tools/SelectTool/childStates/DraggingHandle.mjs.map +2 -2
- package/dist-esm/lib/tools/SelectTool/childStates/Idle.mjs +6 -3
- package/dist-esm/lib/tools/SelectTool/childStates/Idle.mjs.map +2 -2
- package/dist-esm/lib/tools/SelectTool/childStates/PointingArrowLabel.mjs +1 -1
- package/dist-esm/lib/tools/SelectTool/childStates/PointingArrowLabel.mjs.map +2 -2
- package/dist-esm/lib/tools/SelectTool/childStates/PointingCanvas.mjs +3 -0
- package/dist-esm/lib/tools/SelectTool/childStates/PointingCanvas.mjs.map +2 -2
- package/dist-esm/lib/tools/SelectTool/childStates/PointingHandle.mjs +5 -1
- package/dist-esm/lib/tools/SelectTool/childStates/PointingHandle.mjs.map +2 -2
- package/dist-esm/lib/tools/SelectTool/childStates/PointingShape.mjs +1 -1
- package/dist-esm/lib/tools/SelectTool/childStates/PointingShape.mjs.map +2 -2
- package/dist-esm/lib/tools/SelectTool/childStates/Resizing.mjs +22 -31
- package/dist-esm/lib/tools/SelectTool/childStates/Resizing.mjs.map +2 -2
- package/dist-esm/lib/tools/SelectTool/childStates/ScribbleBrushing.mjs +1 -3
- package/dist-esm/lib/tools/SelectTool/childStates/ScribbleBrushing.mjs.map +2 -2
- package/dist-esm/lib/tools/SelectTool/childStates/Translating.mjs +4 -1
- package/dist-esm/lib/tools/SelectTool/childStates/Translating.mjs.map +2 -2
- package/dist-esm/lib/tools/SelectTool/selectHelpers.mjs +10 -0
- package/dist-esm/lib/tools/SelectTool/selectHelpers.mjs.map +2 -2
- package/dist-esm/lib/ui/TldrawUi.mjs +2 -3
- package/dist-esm/lib/ui/TldrawUi.mjs.map +2 -2
- package/dist-esm/lib/ui/components/ActionsMenu/DefaultActionsMenu.mjs +2 -12
- package/dist-esm/lib/ui/components/ActionsMenu/DefaultActionsMenu.mjs.map +2 -2
- package/dist-esm/lib/ui/components/ContextMenu/DefaultContextMenu.mjs +7 -2
- package/dist-esm/lib/ui/components/ContextMenu/DefaultContextMenu.mjs.map +2 -2
- package/dist-esm/lib/ui/components/ContextMenu/DefaultContextMenuContent.mjs +1 -1
- package/dist-esm/lib/ui/components/ContextMenu/DefaultContextMenuContent.mjs.map +2 -2
- package/dist-esm/lib/ui/components/Dialogs.mjs +9 -1
- package/dist-esm/lib/ui/components/Dialogs.mjs.map +2 -2
- package/dist-esm/lib/ui/components/EditLinkDialog.mjs +5 -5
- package/dist-esm/lib/ui/components/EditLinkDialog.mjs.map +2 -2
- package/dist-esm/lib/ui/components/Minimap/DefaultMinimap.mjs +5 -22
- package/dist-esm/lib/ui/components/Minimap/DefaultMinimap.mjs.map +2 -2
- package/dist-esm/lib/ui/components/SharePanel/DefaultPeopleMenuItem.mjs +1 -2
- package/dist-esm/lib/ui/components/SharePanel/DefaultPeopleMenuItem.mjs.map +2 -2
- package/dist-esm/lib/ui/components/StylePanel/StylePanelDoubleDropdownPicker.mjs +2 -0
- package/dist-esm/lib/ui/components/StylePanel/StylePanelDoubleDropdownPicker.mjs.map +2 -2
- package/dist-esm/lib/ui/components/Toolbar/AltTextEditor.mjs +1 -0
- package/dist-esm/lib/ui/components/Toolbar/AltTextEditor.mjs.map +2 -2
- package/dist-esm/lib/ui/components/Toolbar/DefaultImageToolbarContent.mjs +10 -8
- package/dist-esm/lib/ui/components/Toolbar/DefaultImageToolbarContent.mjs.map +2 -2
- package/dist-esm/lib/ui/components/primitives/TldrawUiSlider.mjs +20 -1
- package/dist-esm/lib/ui/components/primitives/TldrawUiSlider.mjs.map +2 -2
- package/dist-esm/lib/ui/context/actions.mjs +8 -8
- package/dist-esm/lib/ui/context/actions.mjs.map +2 -2
- package/dist-esm/lib/ui/hooks/useClipboardEvents.mjs +2 -0
- package/dist-esm/lib/ui/hooks/useClipboardEvents.mjs.map +2 -2
- package/dist-esm/lib/ui/hooks/useCommentingEnabled.mjs +2 -2
- package/dist-esm/lib/ui/hooks/useCommentingEnabled.mjs.map +2 -2
- package/dist-esm/lib/ui/hooks/useFlatten.mjs +42 -23
- package/dist-esm/lib/ui/hooks/useFlatten.mjs.map +2 -2
- package/dist-esm/lib/ui/version.mjs +3 -3
- package/dist-esm/lib/ui/version.mjs.map +1 -1
- package/package.json +12 -6
- package/src/lib/defaultExternalContentHandlers.test.ts +43 -0
- package/src/lib/defaultExternalContentHandlers.ts +40 -18
- package/src/lib/shapes/arrow/ArrowShapeTool.test.ts +17 -0
- package/src/lib/shapes/arrow/curved-arrow.ts +2 -2
- package/src/lib/shapes/arrow/elbow/getElbowArrowInfo.tsx +5 -3
- package/src/lib/shapes/arrow/getArrowInfo.ts +2 -1
- package/src/lib/shapes/arrow/straight-arrow.ts +2 -2
- package/src/lib/shapes/arrow/toolStates/Idle.tsx +3 -4
- package/src/lib/shapes/arrow/toolStates/Pointing.tsx +10 -6
- package/src/lib/shapes/draw/DrawShapeTool.test.ts +19 -0
- package/src/lib/shapes/draw/toolStates/Drawing.ts +12 -6
- package/src/lib/shapes/frame/FrameShapeTool.test.ts +17 -0
- package/src/lib/shapes/geo/GeoShapeTool.test.ts +28 -0
- package/src/lib/shapes/geo/getGeoShapePath.ts +3 -1
- package/src/lib/shapes/geo/toolStates/Idle.ts +3 -4
- package/src/lib/shapes/geo/toolStates/Pointing.ts +2 -1
- package/src/lib/shapes/line/LineShapeTool.test.ts +30 -1
- package/src/lib/shapes/line/toolStates/Pointing.ts +6 -7
- package/src/lib/shapes/note/NoteShapeTool.test.ts +45 -0
- package/src/lib/shapes/note/NoteShapeUtil.tsx +7 -4
- package/src/lib/shapes/note/noteHelpers.ts +18 -8
- package/src/lib/shapes/note/toolStates/Pointing.ts +2 -1
- package/src/lib/shapes/shared/crop.ts +7 -1
- package/src/lib/shapes/text/TextShapeTool.test.ts +33 -0
- package/src/lib/shapes/text/toolStates/Idle.ts +2 -2
- package/src/lib/shapes/text/toolStates/Pointing.ts +3 -0
- package/src/lib/shapes/video/VideoShapeUtil.tsx +4 -2
- package/src/lib/tools/EraserTool/childStates/Erasing.ts +5 -0
- package/src/lib/tools/HandTool/childStates/OneFingerZooming.ts +10 -0
- package/src/lib/tools/SelectTool/SelectTool.ts +4 -0
- package/src/lib/tools/SelectTool/childStates/Brushing.ts +4 -0
- package/src/lib/tools/SelectTool/childStates/Crop/Crop.ts +4 -0
- package/src/lib/tools/SelectTool/childStates/DraggingHandle.tsx +3 -4
- package/src/lib/tools/SelectTool/childStates/Idle.ts +10 -3
- package/src/lib/tools/SelectTool/childStates/PointingArrowLabel.ts +1 -1
- package/src/lib/tools/SelectTool/childStates/PointingCanvas.ts +4 -0
- package/src/lib/tools/SelectTool/childStates/PointingHandle.ts +6 -1
- package/src/lib/tools/SelectTool/childStates/PointingShape.ts +4 -1
- package/src/lib/tools/SelectTool/childStates/Resizing.ts +30 -33
- package/src/lib/tools/SelectTool/childStates/ScribbleBrushing.ts +3 -3
- package/src/lib/tools/SelectTool/childStates/Translating.ts +6 -0
- package/src/lib/tools/SelectTool/selectHelpers.ts +21 -0
- package/src/lib/ui/TldrawUi.tsx +1 -2
- package/src/lib/ui/components/ActionsMenu/DefaultActionsMenu.tsx +4 -14
- package/src/lib/ui/components/ContextMenu/DefaultContextMenu.tsx +8 -2
- package/src/lib/ui/components/ContextMenu/DefaultContextMenuContent.tsx +5 -1
- package/src/lib/ui/components/Dialogs.tsx +17 -1
- package/src/lib/ui/components/EditLinkDialog.tsx +8 -7
- package/src/lib/ui/components/Minimap/DefaultMinimap.tsx +5 -23
- package/src/lib/ui/components/SharePanel/DefaultPeopleMenuItem.tsx +2 -3
- package/src/lib/ui/components/StylePanel/StylePanelDoubleDropdownPicker.tsx +2 -0
- package/src/lib/ui/components/Toolbar/AltTextEditor.tsx +1 -0
- package/src/lib/ui/components/Toolbar/DefaultImageToolbarContent.tsx +13 -8
- package/src/lib/ui/components/primitives/TldrawUiSlider.test.tsx +92 -0
- package/src/lib/ui/components/primitives/TldrawUiSlider.tsx +25 -1
- package/src/lib/ui/context/actions.tsx +25 -8
- package/src/lib/ui/hooks/useClipboardEvents.ts +2 -0
- package/src/lib/ui/hooks/useCommentingEnabled.ts +5 -3
- package/src/lib/ui/hooks/useFlatten.ts +56 -23
- package/src/lib/ui/version.ts +3 -3
- package/src/lib/ui.css +7 -16
- package/src/test/ClickManager.test.ts +18 -0
- package/src/test/Editor.test.tsx +3 -0
- package/src/test/EraserTool.test.ts +18 -2
- package/src/test/HandTool.test.ts +21 -0
- package/src/test/LaserTool.test.ts +46 -0
- package/src/test/ScribbleManager.test.ts +19 -0
- package/src/test/SelectTool.test.ts +110 -0
- package/src/test/TldrawEditor.test.tsx +18 -0
- package/src/test/VideoShapeUtil.test.tsx +126 -0
- package/src/test/actions-menu-readonly.test.tsx +37 -0
- package/src/test/bookmark-shapes.test.ts +110 -0
- package/src/test/commands/__snapshots__/packShapes.test.ts.snap +6 -6
- package/src/test/commands/clipboard.test.ts +36 -1
- package/src/test/commands/lockShapes.test.ts +11 -0
- package/src/test/commands/packShapes.test.ts +20 -0
- package/src/test/commands/putContent.test.ts +35 -1
- package/src/test/commands/setCamera.test.ts +69 -0
- package/src/test/commands/stackShapes.test.ts +12 -0
- package/src/test/commands/updateViewportPageBounds.test.ts +8 -0
- package/src/test/cropping.test.ts +162 -0
- package/src/test/customSnapping.test.tsx +32 -0
- package/src/test/defaultHandleExternalFileContent.test.ts +86 -0
- package/src/test/drawing.test.ts +60 -1
- package/src/test/duplicate.test.ts +22 -0
- package/src/test/flatten.test.ts +135 -0
- package/src/test/frames.test.ts +51 -0
- package/src/test/getShapeAtPoint.test.ts +44 -1
- package/src/test/handleDeepLink.test.tsx +15 -0
- package/src/test/minimap-wheel.test.tsx +48 -0
- package/src/test/resizing.test.ts +46 -0
- package/src/test/selection-omnibus.test.ts +50 -0
- package/src/test/spacebarPanning.test.ts +58 -0
- package/src/test/translating.test.ts +42 -0
- package/src/test/ui/AltTextEditor.test.tsx +51 -0
- package/src/test/ui/ContextMenu.test.tsx +61 -2
- package/src/test/ui/EditLinkDialog.test.tsx +77 -0
- package/src/test/ui/ImageToolbar.test.tsx +58 -0
- package/src/test/ui/StylePanel.test.tsx +33 -2
- package/src/test/ui/keyboardShortcuts.test.tsx +27 -0
- package/src/test/ui/presenceActivity.test.tsx +44 -0
- package/src/test/ui/unlockAll.test.tsx +55 -0
- package/src/test/ui-outside-editor.test.tsx +74 -0
- package/src/test/viewport-following.test.ts +29 -1
- package/tldraw.css +7 -16
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { MediaHelpers, TLImageShape } from '@tldraw/editor'
|
|
2
|
+
import { vi } from 'vitest'
|
|
3
|
+
import { defaultAssetUtils } from '../lib/defaultAssetUtils'
|
|
4
|
+
import {
|
|
5
|
+
defaultHandleExternalFileContent,
|
|
6
|
+
getAssetInfo,
|
|
7
|
+
} from '../lib/defaultExternalContentHandlers'
|
|
8
|
+
import { TestEditor } from './TestEditor'
|
|
9
|
+
|
|
10
|
+
let editor: TestEditor
|
|
11
|
+
let addToast: ReturnType<typeof vi.fn>
|
|
12
|
+
|
|
13
|
+
function makeOptions() {
|
|
14
|
+
addToast = vi.fn()
|
|
15
|
+
return {
|
|
16
|
+
toasts: { addToast } as any,
|
|
17
|
+
msg: ((key: string) => key) as any,
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
editor = new TestEditor({ assetUtils: defaultAssetUtils })
|
|
23
|
+
vi.spyOn(MediaHelpers, 'isAnimated').mockResolvedValue(false)
|
|
24
|
+
// Stand in for the upload stage so only the decode stage is under test.
|
|
25
|
+
vi.spyOn(editor, 'getAssetForExternalContent').mockImplementation(async (info) => {
|
|
26
|
+
if (info.type !== 'file') return undefined
|
|
27
|
+
const asset = await getAssetInfo(editor, info.file)
|
|
28
|
+
if (!asset) return undefined
|
|
29
|
+
return { ...asset, props: { ...asset.props, src: 'data:ok' } } as any
|
|
30
|
+
})
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
afterEach(() => {
|
|
34
|
+
editor?.dispose()
|
|
35
|
+
vi.restoreAllMocks()
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
describe('defaultHandleExternalFileContent', () => {
|
|
39
|
+
it('skips an undecodable image with a toast and still places the others', async () => {
|
|
40
|
+
const goodFile = new File(['good'], 'good.png', { type: 'image/png' })
|
|
41
|
+
const badFile = new File(['not really a png'], 'bad.png', { type: 'image/png' })
|
|
42
|
+
|
|
43
|
+
vi.spyOn(MediaHelpers, 'getImageSize').mockImplementation(async (file) => {
|
|
44
|
+
if (file === badFile) throw new Error('Could not load image')
|
|
45
|
+
return { w: 100, h: 50, pixelRatio: 1 }
|
|
46
|
+
})
|
|
47
|
+
vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
48
|
+
|
|
49
|
+
await defaultHandleExternalFileContent(
|
|
50
|
+
editor,
|
|
51
|
+
{ point: { x: 0, y: 0 }, files: [badFile, goodFile] },
|
|
52
|
+
makeOptions()
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
expect(addToast).toHaveBeenCalledTimes(1)
|
|
56
|
+
expect(addToast).toHaveBeenCalledWith({
|
|
57
|
+
title: 'assets.files.upload-failed',
|
|
58
|
+
severity: 'error',
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
const images = editor.getCurrentPageShapes().filter((s) => s.type === 'image')
|
|
62
|
+
expect(images).toHaveLength(1)
|
|
63
|
+
const asset = editor.getAsset((images[0] as TLImageShape).props.assetId!)
|
|
64
|
+
expect(asset?.props).toMatchObject({ name: 'good.png', w: 100, h: 50 })
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('shows a toast and creates nothing when the only file is undecodable', async () => {
|
|
68
|
+
const badFile = new File(['not really a png'], 'bad.png', { type: 'image/png' })
|
|
69
|
+
vi.spyOn(MediaHelpers, 'getImageSize').mockRejectedValue(new Error('Could not load image'))
|
|
70
|
+
vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
71
|
+
|
|
72
|
+
await defaultHandleExternalFileContent(
|
|
73
|
+
editor,
|
|
74
|
+
{ point: { x: 0, y: 0 }, files: [badFile] },
|
|
75
|
+
makeOptions()
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
expect(addToast).toHaveBeenCalledTimes(1)
|
|
79
|
+
expect(addToast).toHaveBeenCalledWith({
|
|
80
|
+
title: 'assets.files.upload-failed',
|
|
81
|
+
severity: 'error',
|
|
82
|
+
})
|
|
83
|
+
expect(editor.getCurrentPageShapes()).toHaveLength(0)
|
|
84
|
+
expect(editor.getAssets()).toHaveLength(0)
|
|
85
|
+
})
|
|
86
|
+
})
|
package/src/test/drawing.test.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { TLDrawShape, TLHighlightShape, last } from '@tldraw/editor'
|
|
1
|
+
import { PageRecordType, TLDrawShape, TLHighlightShape, last } from '@tldraw/editor'
|
|
2
2
|
import { vi } from 'vitest'
|
|
3
3
|
import { base64ToPoints } from '../lib/utils/test-helpers'
|
|
4
4
|
import { TEST_DRAW_SHAPE_SCREEN_POINTS } from './drawing.data'
|
|
@@ -121,6 +121,39 @@ for (const toolType of ['draw', 'highlight'] as const) {
|
|
|
121
121
|
expect(shape.props.segments[2].type).toBe('straight')
|
|
122
122
|
})
|
|
123
123
|
|
|
124
|
+
it('Switches segment types after the same screen distance regardless of zoom', () => {
|
|
125
|
+
// Without the zoom correction the threshold is 4 canvas units, which at 800% is
|
|
126
|
+
// 32 screen pixels of the stroke not following the pointer (#10392).
|
|
127
|
+
editor.setCamera({ x: 0, y: 0, z: 8 })
|
|
128
|
+
editor
|
|
129
|
+
.setCurrentTool(toolType)
|
|
130
|
+
.pointerDown(100, 100)
|
|
131
|
+
.pointerMove(120, 100)
|
|
132
|
+
.keyDown('Shift')
|
|
133
|
+
.pointerMove(125, 100)
|
|
134
|
+
|
|
135
|
+
let shape = editor.getCurrentPageShapes()[0] as DrawableShape
|
|
136
|
+
expect(shape.props.segments.map((s) => s.type)).toEqual(['free', 'straight'])
|
|
137
|
+
|
|
138
|
+
editor.keyUp('Shift').pointerMove(130, 100)
|
|
139
|
+
|
|
140
|
+
shape = editor.getCurrentPageShapes()[0] as DrawableShape
|
|
141
|
+
expect(shape.props.segments.map((s) => s.type)).toEqual(['free', 'straight', 'free'])
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
it('Does not switch segment types until the pointer has moved past the drag distance', () => {
|
|
145
|
+
editor.setCamera({ x: 0, y: 0, z: 8 })
|
|
146
|
+
editor
|
|
147
|
+
.setCurrentTool(toolType)
|
|
148
|
+
.pointerDown(100, 100)
|
|
149
|
+
.pointerMove(120, 100)
|
|
150
|
+
.keyDown('Shift')
|
|
151
|
+
.pointerMove(123, 100)
|
|
152
|
+
|
|
153
|
+
const shape = editor.getCurrentPageShapes()[0] as DrawableShape
|
|
154
|
+
expect(shape.props.segments.map((s) => s.type)).toEqual(['free'])
|
|
155
|
+
})
|
|
156
|
+
|
|
124
157
|
it('Extends previously drawn line when shift is held', () => {
|
|
125
158
|
editor
|
|
126
159
|
.setCurrentTool(toolType)
|
|
@@ -163,6 +196,32 @@ for (const toolType of ['draw', 'highlight'] as const) {
|
|
|
163
196
|
expect(shape2.props.segments[0].type).toBe('straight')
|
|
164
197
|
})
|
|
165
198
|
|
|
199
|
+
it('Does not extend previously drawn line after changing page', () => {
|
|
200
|
+
// regression for #10400: shift-clicking after a page change appended a
|
|
201
|
+
// segment to the shape on the old page instead of starting a new shape
|
|
202
|
+
const page1Id = editor.getCurrentPageId()
|
|
203
|
+
editor.setCurrentTool(toolType).pointerDown(10, 10).pointerMove(20, 20).pointerUp()
|
|
204
|
+
|
|
205
|
+
const shapeOnPage1 = editor.getCurrentPageShapes()[0] as DrawableShape
|
|
206
|
+
expect(shapeOnPage1.props.segments.length).toBe(1)
|
|
207
|
+
|
|
208
|
+
const page2Id = PageRecordType.createId()
|
|
209
|
+
editor.createPage({ id: page2Id, name: 'Page 2' })
|
|
210
|
+
editor.setCurrentPage(page2Id)
|
|
211
|
+
editor.keyDown('Shift').pointerDown(30, 30).pointerMove(40, 40).pointerUp()
|
|
212
|
+
|
|
213
|
+
expect(editor.getCurrentPageShapes()).toHaveLength(1)
|
|
214
|
+
const shapeOnPage2 = editor.getCurrentPageShapes()[0] as DrawableShape
|
|
215
|
+
expect(shapeOnPage2.id).not.toBe(shapeOnPage1.id)
|
|
216
|
+
expect(editor.isShapeInPage(shapeOnPage2, page2Id)).toBe(true)
|
|
217
|
+
expect(shapeOnPage2.props.segments.length).toBe(1)
|
|
218
|
+
expect(shapeOnPage2.props.segments[0].type).toBe('straight')
|
|
219
|
+
|
|
220
|
+
// The shape on the first page is untouched
|
|
221
|
+
expect(editor.getShape(shapeOnPage1.id)).toEqual(shapeOnPage1)
|
|
222
|
+
expect(editor.isShapeInPage(shapeOnPage1.id, page1Id)).toBe(true)
|
|
223
|
+
})
|
|
224
|
+
|
|
166
225
|
it('Snaps to 15 degree angle when shift is held', () => {
|
|
167
226
|
const magnitude = 10
|
|
168
227
|
const angle = (17 * Math.PI) / 180
|
|
@@ -41,6 +41,28 @@ it('duplicates a shape with an offset', () => {
|
|
|
41
41
|
expect(editor.getLastCreatedShape()).toMatchObject({ x: 10, y: 10 })
|
|
42
42
|
})
|
|
43
43
|
|
|
44
|
+
it('ignores ids of shapes that do not exist', () => {
|
|
45
|
+
editor.createShape({ id: ids.box1, type: 'geo', x: 0, y: 0, props: { w: 100, h: 100 } })
|
|
46
|
+
editor.duplicateShapes([ids.box1, createShapeId('nope')], { x: 10, y: 10 })
|
|
47
|
+
expect(editor.getCurrentPageShapes().length).toBe(2)
|
|
48
|
+
expect(editor.getLastCreatedShape()).toMatchObject({ x: 10, y: 10 })
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('offsets a duplicated descendant only once when its ancestor is also duplicated', () => {
|
|
52
|
+
editor.createShapes([
|
|
53
|
+
{ id: ids.box1, type: 'frame', x: 0, y: 0, props: { w: 500, h: 500 } },
|
|
54
|
+
{ id: ids.box2, type: 'geo', parentId: ids.box1, x: 50, y: 50, props: { w: 100, h: 100 } },
|
|
55
|
+
])
|
|
56
|
+
editor.duplicateShapes([ids.box1, ids.box2], { x: 1000, y: 0 })
|
|
57
|
+
const frameCopy = editor
|
|
58
|
+
.getCurrentPageShapes()
|
|
59
|
+
.find((s) => s.type === 'frame' && s.id !== ids.box1)!
|
|
60
|
+
const childCopy = editor.getSortedChildIdsForParent(frameCopy.id)
|
|
61
|
+
expect(childCopy).toHaveLength(1)
|
|
62
|
+
// the child follows its duplicated parent; it should not be offset a second time
|
|
63
|
+
expect(editor.getShapePageBounds(childCopy[0])).toMatchObject({ x: 1050, y: 50 })
|
|
64
|
+
})
|
|
65
|
+
|
|
44
66
|
it("doesn't duplicate locked shapes", () => {
|
|
45
67
|
editor
|
|
46
68
|
.createShape({ id: ids.box1, type: 'geo', x: 0, y: 0, props: { w: 100, h: 100 } })
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AssetRecordType,
|
|
3
|
+
TLImageAsset,
|
|
4
|
+
TLShapeId,
|
|
5
|
+
TLShapePartial,
|
|
6
|
+
createShapeId,
|
|
7
|
+
} from '@tldraw/editor'
|
|
8
|
+
import { vi } from 'vitest'
|
|
9
|
+
import { flattenShapesToImages } from '../lib/ui/hooks/useFlatten'
|
|
10
|
+
import { TestEditor } from './TestEditor'
|
|
11
|
+
|
|
12
|
+
let editor: TestEditor
|
|
13
|
+
let assetCount = 0
|
|
14
|
+
|
|
15
|
+
function installFlattenMocks(target: TestEditor) {
|
|
16
|
+
// flattenShapesToImages calls editor.getSvgString and
|
|
17
|
+
// editor.getAssetForExternalContent. Mock both to bypass the real export
|
|
18
|
+
// pipeline (jsdom can't render SVGs and we don't need real pixels — these
|
|
19
|
+
// tests only care about which shapes end up in which group).
|
|
20
|
+
vi.spyOn(target, 'getSvgString').mockResolvedValue({
|
|
21
|
+
svg: '<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10"></svg>',
|
|
22
|
+
width: 10,
|
|
23
|
+
height: 10,
|
|
24
|
+
trimPadding: 0,
|
|
25
|
+
})
|
|
26
|
+
vi.spyOn(target, 'getAssetForExternalContent').mockImplementation(async () => {
|
|
27
|
+
const asset: TLImageAsset = {
|
|
28
|
+
id: AssetRecordType.createId(`flat-${assetCount++}`),
|
|
29
|
+
typeName: 'asset',
|
|
30
|
+
type: 'image',
|
|
31
|
+
props: {
|
|
32
|
+
w: 10,
|
|
33
|
+
h: 10,
|
|
34
|
+
name: 'asset.svg',
|
|
35
|
+
isAnimated: false,
|
|
36
|
+
mimeType: 'image/svg+xml',
|
|
37
|
+
src: 'data:image/svg+xml;base64,',
|
|
38
|
+
},
|
|
39
|
+
meta: {},
|
|
40
|
+
}
|
|
41
|
+
return asset
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
beforeEach(() => {
|
|
46
|
+
editor = new TestEditor()
|
|
47
|
+
assetCount = 0
|
|
48
|
+
installFlattenMocks(editor)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
afterEach(() => {
|
|
52
|
+
editor?.dispose()
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
const box = (id: TLShapeId, x: number, y: number, w = 50, h = 50): TLShapePartial => ({
|
|
56
|
+
id,
|
|
57
|
+
type: 'geo',
|
|
58
|
+
x,
|
|
59
|
+
y,
|
|
60
|
+
props: { w, h, fill: 'solid' },
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
function getImageShapeCount() {
|
|
64
|
+
return editor.getCurrentPageShapes().filter((s) => s.type === 'image').length
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
describe('flattenShapesToImages clustering', () => {
|
|
68
|
+
const expand = 64
|
|
69
|
+
|
|
70
|
+
it('groups shapes within the expand threshold into one image', async () => {
|
|
71
|
+
const a = createShapeId('a')
|
|
72
|
+
const b = createShapeId('b')
|
|
73
|
+
// gap of 30 < 2 * 64 expand, so the expanded boxes intersect
|
|
74
|
+
editor.createShapes([box(a, 0, 0), box(b, 80, 0)])
|
|
75
|
+
|
|
76
|
+
await flattenShapesToImages(editor, [a, b], expand)
|
|
77
|
+
|
|
78
|
+
expect(getImageShapeCount()).toBe(1)
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('keeps shapes in separate images when their gap exceeds the expand threshold', async () => {
|
|
82
|
+
const a = createShapeId('a')
|
|
83
|
+
const b = createShapeId('b')
|
|
84
|
+
// gap of 200 > 2 * 64 expand, so the expanded boxes do not intersect
|
|
85
|
+
editor.createShapes([box(a, 0, 0), box(b, 250, 0)])
|
|
86
|
+
|
|
87
|
+
await flattenShapesToImages(editor, [a, b], expand)
|
|
88
|
+
|
|
89
|
+
expect(getImageShapeCount()).toBe(2)
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('merges all shapes into one group when a bridging shape connects them, independent of input order', async () => {
|
|
93
|
+
// Layout: A — gap — bridge — gap — C. A and C alone do not intersect
|
|
94
|
+
// each other's expanded bounds, but `bridge` sits in the middle and
|
|
95
|
+
// should pull them all into a single group regardless of where it
|
|
96
|
+
// appears in the list. The previous greedy pass would only merge
|
|
97
|
+
// `bridge` into the first group it touched, leaving C as a separate
|
|
98
|
+
// cluster when `bridge` came after C in the list — which made the
|
|
99
|
+
// result depend on selection order.
|
|
100
|
+
const a = createShapeId('a')
|
|
101
|
+
const bridge = createShapeId('bridge')
|
|
102
|
+
const c = createShapeId('c')
|
|
103
|
+
|
|
104
|
+
const orders: TLShapeId[][] = [
|
|
105
|
+
[a, bridge, c],
|
|
106
|
+
[a, c, bridge],
|
|
107
|
+
[bridge, a, c],
|
|
108
|
+
[c, bridge, a],
|
|
109
|
+
]
|
|
110
|
+
|
|
111
|
+
for (const order of orders) {
|
|
112
|
+
editor.dispose()
|
|
113
|
+
editor = new TestEditor()
|
|
114
|
+
assetCount = 0
|
|
115
|
+
installFlattenMocks(editor)
|
|
116
|
+
editor.createShapes([box(a, 0, 0), box(bridge, 100, 0), box(c, 200, 0)])
|
|
117
|
+
|
|
118
|
+
await flattenShapesToImages(editor, order, expand)
|
|
119
|
+
|
|
120
|
+
expect(getImageShapeCount(), `order ${order.join(',')}`).toBe(1)
|
|
121
|
+
}
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('uses a single Box.Common group when no expand value is provided', async () => {
|
|
125
|
+
// Two shapes far apart should still flatten to one image when called
|
|
126
|
+
// via the no-expand path (the default useFlatten() hook).
|
|
127
|
+
const a = createShapeId('a')
|
|
128
|
+
const b = createShapeId('b')
|
|
129
|
+
editor.createShapes([box(a, 0, 0), box(b, 1000, 0)])
|
|
130
|
+
|
|
131
|
+
await flattenShapesToImages(editor, [a, b])
|
|
132
|
+
|
|
133
|
+
expect(getImageShapeCount()).toBe(1)
|
|
134
|
+
})
|
|
135
|
+
})
|
package/src/test/frames.test.ts
CHANGED
|
@@ -450,6 +450,36 @@ describe('frame shapes', () => {
|
|
|
450
450
|
expect(editor.getOnlySelectedShape()!.parentId).toBe(frameId)
|
|
451
451
|
})
|
|
452
452
|
|
|
453
|
+
it('restores the original index of a child that is dragged out and back in', () => {
|
|
454
|
+
const frameId = createShapeId('frame')
|
|
455
|
+
const boxA = createShapeId('A')
|
|
456
|
+
const boxB = createShapeId('B')
|
|
457
|
+
const boxC = createShapeId('C')
|
|
458
|
+
editor.createShapes([
|
|
459
|
+
{ id: frameId, type: 'frame', x: 0, y: 0, props: { w: 200, h: 200 } },
|
|
460
|
+
// A second page-level shape, so that dragging B out lands it at page index a3: the same
|
|
461
|
+
// index as C inside the frame, which must not block restoring B to a2
|
|
462
|
+
{ id: createShapeId('X'), type: 'geo', x: 400, y: 400, props: { w: 50, h: 50 } },
|
|
463
|
+
{ id: boxA, type: 'geo', parentId: frameId, x: 10, y: 10, props: { w: 40, h: 40 } },
|
|
464
|
+
{ id: boxB, type: 'geo', parentId: frameId, x: 75, y: 75, props: { w: 50, h: 50 } },
|
|
465
|
+
{ id: boxC, type: 'geo', parentId: frameId, x: 150, y: 150, props: { w: 40, h: 40 } },
|
|
466
|
+
])
|
|
467
|
+
expect(editor.getSortedChildIdsForParent(frameId)).toEqual([boxA, boxB, boxC])
|
|
468
|
+
expect(editor.getShape(boxB)!.index).toBe('a2')
|
|
469
|
+
|
|
470
|
+
editor.setCurrentTool('select').select(boxB)
|
|
471
|
+
editor.pointerDown(100, 100).pointerMove(300, 300)
|
|
472
|
+
vi.advanceTimersByTime(300)
|
|
473
|
+
expect(editor.getShape(boxB)!.parentId).toBe(editor.getCurrentPageId())
|
|
474
|
+
expect(editor.getShape(boxB)!.index).toBe('a3')
|
|
475
|
+
|
|
476
|
+
editor.pointerMove(100, 100)
|
|
477
|
+
vi.advanceTimersByTime(300)
|
|
478
|
+
editor.pointerUp(100, 100)
|
|
479
|
+
expect(editor.getShape(boxB)!.parentId).toBe(frameId)
|
|
480
|
+
expect(editor.getSortedChildIdsForParent(frameId)).toEqual([boxA, boxB, boxC])
|
|
481
|
+
})
|
|
482
|
+
|
|
453
483
|
it('does not reparent shapes that are being dragged from within the frame', () => {
|
|
454
484
|
dragCreateFrame({ down: [0, 0], move: [200, 200], up: [200, 200] })
|
|
455
485
|
const frame = editor.getLastCreatedShape()
|
|
@@ -1718,6 +1748,27 @@ it('avoids crash when dragging into descendant', () => {
|
|
|
1718
1748
|
editor.pointerMove(30, 30)
|
|
1719
1749
|
})
|
|
1720
1750
|
|
|
1751
|
+
it('unparents a frame dragged out of its parent by a point over its own nested child', () => {
|
|
1752
|
+
const outer = createShapeId('outer')
|
|
1753
|
+
const mid = createShapeId('mid')
|
|
1754
|
+
const inner = createShapeId('inner')
|
|
1755
|
+
editor.createShapes([
|
|
1756
|
+
{ id: outer, type: 'frame', x: 0, y: 0, props: { w: 1000, h: 1000 } },
|
|
1757
|
+
{ id: mid, type: 'frame', parentId: outer, x: 100, y: 100, props: { w: 500, h: 500 } },
|
|
1758
|
+
{ id: inner, type: 'frame', parentId: mid, x: 100, y: 100, props: { w: 200, h: 200 } },
|
|
1759
|
+
])
|
|
1760
|
+
|
|
1761
|
+
// inner travels with mid and stays under the pointer the whole drag; it must not be taken
|
|
1762
|
+
// for the drop target, or outer never sees mid leave
|
|
1763
|
+
editor.select(mid)
|
|
1764
|
+
editor.pointerDown(250, 250)
|
|
1765
|
+
editor.pointerMove(2000, 2000)
|
|
1766
|
+
vi.advanceTimersByTime(300)
|
|
1767
|
+
|
|
1768
|
+
expect(editor.getShape(mid)!.parentId).toBe(editor.getCurrentPageId())
|
|
1769
|
+
expect(editor.getShape(inner)!.parentId).toBe(mid)
|
|
1770
|
+
})
|
|
1771
|
+
|
|
1721
1772
|
describe('When dragging groups or shapes within a group', () => {
|
|
1722
1773
|
it('Allows dragging groups into frames', () => {
|
|
1723
1774
|
editor.createShape({ type: 'frame', x: 100, y: 100, props: { w: 500, h: 500 } })
|
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
BaseFrameLikeShapeUtil,
|
|
3
|
+
RecordProps,
|
|
4
|
+
T,
|
|
5
|
+
TLBaseShape,
|
|
6
|
+
TLShape,
|
|
7
|
+
createShapeId,
|
|
8
|
+
toRichText,
|
|
9
|
+
} from '@tldraw/editor'
|
|
2
10
|
import { TestEditor } from './TestEditor'
|
|
3
11
|
|
|
4
12
|
let editor: TestEditor
|
|
@@ -212,3 +220,38 @@ describe('frames', () => {
|
|
|
212
220
|
expect(editor.getEditingShape()).toBe(frame)
|
|
213
221
|
})
|
|
214
222
|
})
|
|
223
|
+
|
|
224
|
+
// Not registered in TLGlobalShapePropsMap; see getSvgString.test.ts.
|
|
225
|
+
const PLAIN_FRAME_TYPE = 'plain-frame'
|
|
226
|
+
type PlainFrameShape = TLBaseShape<typeof PLAIN_FRAME_TYPE, { w: number; h: number }>
|
|
227
|
+
|
|
228
|
+
describe('frame-like shapes with a plain geometry', () => {
|
|
229
|
+
// inherits BaseBoxShapeUtil's Rectangle2d geometry, which has no label children
|
|
230
|
+
class PlainFrameUtil extends BaseFrameLikeShapeUtil<any> {
|
|
231
|
+
static override type = PLAIN_FRAME_TYPE
|
|
232
|
+
static override props: RecordProps<PlainFrameShape> = { w: T.number, h: T.number }
|
|
233
|
+
getDefaultProps() {
|
|
234
|
+
return { w: 200, h: 200 }
|
|
235
|
+
}
|
|
236
|
+
component() {
|
|
237
|
+
return null
|
|
238
|
+
}
|
|
239
|
+
getIndicatorPath(): undefined {
|
|
240
|
+
return undefined
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
it('can be hit tested', () => {
|
|
245
|
+
editor = new TestEditor({ shapeUtils: [PlainFrameUtil] })
|
|
246
|
+
editor.createShape({
|
|
247
|
+
id: ids.frame1,
|
|
248
|
+
type: PLAIN_FRAME_TYPE,
|
|
249
|
+
x: 100,
|
|
250
|
+
y: 100,
|
|
251
|
+
props: { w: 200, h: 200 },
|
|
252
|
+
} as any)
|
|
253
|
+
expect(() => editor.getShapeAtPoint({ x: 150, y: 150 })).not.toThrow()
|
|
254
|
+
// just outside the edge, within the margin
|
|
255
|
+
expect(editor.getShapeAtPoint({ x: 98, y: 150 }, { margin: 4 })?.id).toBe(ids.frame1)
|
|
256
|
+
})
|
|
257
|
+
})
|
|
@@ -73,6 +73,21 @@ describe('type: viewport', () => {
|
|
|
73
73
|
expect(editor.getViewportPageBounds()).toMatchObject(bounds)
|
|
74
74
|
})
|
|
75
75
|
|
|
76
|
+
it('falls back to fitting the page content when the bounds are malformed', () => {
|
|
77
|
+
editor.createShapes([
|
|
78
|
+
{ id: createShapeId(), type: 'geo', x: 2000, y: 2000, props: { w: 100, h: 100 } },
|
|
79
|
+
])
|
|
80
|
+
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
81
|
+
editor.navigateToDeepLink({ url: 'http://localhost/?d=vabc.def.ghi.jkl' })
|
|
82
|
+
warn.mockRestore()
|
|
83
|
+
|
|
84
|
+
expect(editor.getViewportPageBounds()).toMatchObject({
|
|
85
|
+
x: 2050 - editor.bounds.width / 2,
|
|
86
|
+
y: 2050 - editor.bounds.height / 2,
|
|
87
|
+
})
|
|
88
|
+
expect(editor.getZoomLevel()).toBe(1)
|
|
89
|
+
})
|
|
90
|
+
|
|
76
91
|
it('will not change the viewport if the specific page does not exist', () => {
|
|
77
92
|
const pageId = PageRecordType.createId('foo')
|
|
78
93
|
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { act } from '@testing-library/react'
|
|
2
|
+
import { TLEventInfo, deleteFromLocalStorage, setInLocalStorage } from '@tldraw/editor'
|
|
3
|
+
import { vi } from 'vitest'
|
|
4
|
+
import { Tldraw } from '../lib/Tldraw'
|
|
5
|
+
import { renderTldrawComponentWithEditor } from './testutils/renderTldrawComponent'
|
|
6
|
+
|
|
7
|
+
// jsdom has no 2D canvas context; without one the minimap manager throws on mount and the
|
|
8
|
+
// minimap reports a crash instead of rendering normally.
|
|
9
|
+
const getContext = vi.spyOn(HTMLCanvasElement.prototype, 'getContext')
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
setInLocalStorage('minimap', 'false') // start with the navigation panel expanded
|
|
13
|
+
getContext.mockImplementation(() => new Proxy({}, { get: () => () => {} }) as any)
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
deleteFromLocalStorage('minimap')
|
|
18
|
+
getContext.mockReset()
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
describe('wheel over the minimap', () => {
|
|
22
|
+
// Issue #10441: the minimap dispatched the wheel to the editor itself, and the navigation
|
|
23
|
+
// panel's wheel pass-through redispatched the same native event to the canvas, so the camera
|
|
24
|
+
// moved at double rate.
|
|
25
|
+
it('dispatches exactly one wheel event to the editor', async () => {
|
|
26
|
+
const { editor } = await renderTldrawComponentWithEditor(
|
|
27
|
+
(onMount) => <Tldraw onMount={onMount} />,
|
|
28
|
+
{ waitForPatterns: false }
|
|
29
|
+
)
|
|
30
|
+
const minimap = document.querySelector('[data-testid="minimap.canvas"]') as HTMLElement
|
|
31
|
+
expect(minimap).toBeTruthy()
|
|
32
|
+
|
|
33
|
+
const wheels: TLEventInfo[] = []
|
|
34
|
+
editor.on('event', (info) => {
|
|
35
|
+
if (info.type === 'wheel') wheels.push(info)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
await act(async () => {
|
|
39
|
+
editor.updateInstanceState({ isFocused: true })
|
|
40
|
+
minimap.dispatchEvent(
|
|
41
|
+
new WheelEvent('wheel', { bubbles: true, cancelable: true, deltaX: 0, deltaY: 40 })
|
|
42
|
+
)
|
|
43
|
+
editor.emit('tick', 16)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
expect(wheels).toHaveLength(1)
|
|
47
|
+
})
|
|
48
|
+
})
|
|
@@ -3582,6 +3582,25 @@ describe('shapes that have do not resize', () => {
|
|
|
3582
3582
|
expect(editor.getShapePageBounds(noteBId)).toMatchObject({ x: 100, y: 110, w: 200, h: 200 })
|
|
3583
3583
|
})
|
|
3584
3584
|
|
|
3585
|
+
it('are still translated if part of a selection when canResize is false', () => {
|
|
3586
|
+
const bookmarkId = createShapeId('bookmark')
|
|
3587
|
+
editor.createShapes([
|
|
3588
|
+
box(ids.boxA, 0, 0, 200, 320),
|
|
3589
|
+
{ id: bookmarkId, type: 'bookmark', x: 0, y: 0, props: { w: 200 } },
|
|
3590
|
+
])
|
|
3591
|
+
|
|
3592
|
+
// a bookmark without an asset is 320 tall
|
|
3593
|
+
expect(editor.getShapePageBounds(bookmarkId)).toMatchObject({ x: 0, y: 0, w: 200, h: 320 })
|
|
3594
|
+
|
|
3595
|
+
editor.select(ids.boxA, bookmarkId)
|
|
3596
|
+
|
|
3597
|
+
editor.resizeSelection({ scaleX: 2, scaleY: 2.1 }, 'bottom_right')
|
|
3598
|
+
|
|
3599
|
+
expect(editor.getShapePageBounds(ids.boxA)).toMatchObject({ x: 0, y: 0, w: 400, h: 672 })
|
|
3600
|
+
// the bookmark keeps its size but moves so its center scales with the selection
|
|
3601
|
+
expect(editor.getShapePageBounds(bookmarkId)).toMatchObject({ x: 100, y: 176, w: 200, h: 320 })
|
|
3602
|
+
})
|
|
3603
|
+
|
|
3585
3604
|
it('can flip', () => {
|
|
3586
3605
|
const noteBId = createShapeId('noteB')
|
|
3587
3606
|
const noteCId = createShapeId('noteC')
|
|
@@ -3867,6 +3886,33 @@ it('uses the cross cursor when create resizing', () => {
|
|
|
3867
3886
|
expect(editor.getInstanceState().cursor.rotation).toBe(0)
|
|
3868
3887
|
})
|
|
3869
3888
|
|
|
3889
|
+
describe('When brushing from a selection handle with the accel key', () => {
|
|
3890
|
+
it('resets the resize cursor when brushing starts from a resize handle', () => {
|
|
3891
|
+
editor.select(ids.boxA)
|
|
3892
|
+
editor.pointerDownOnHandle('bottom_right', { ctrlKey: true })
|
|
3893
|
+
editor.expectToBeIn('select.brushing')
|
|
3894
|
+
expect(editor.getInstanceState().cursor).toMatchObject({ type: 'default', rotation: 0 })
|
|
3895
|
+
|
|
3896
|
+
editor.pointerMoveBy(50, 50)
|
|
3897
|
+
expect(editor.getInstanceState().cursor).toMatchObject({ type: 'default', rotation: 0 })
|
|
3898
|
+
|
|
3899
|
+
editor.pointerUp()
|
|
3900
|
+
editor.expectToBeIn('select.idle')
|
|
3901
|
+
expect(editor.getInstanceState().cursor).toMatchObject({ type: 'default', rotation: 0 })
|
|
3902
|
+
})
|
|
3903
|
+
|
|
3904
|
+
it('resets the rotate cursor when brushing starts from a rotate handle', () => {
|
|
3905
|
+
editor.select(ids.boxA)
|
|
3906
|
+
editor.pointerDownOnHandle('top_right_rotate', { ctrlKey: true })
|
|
3907
|
+
editor.expectToBeIn('select.brushing')
|
|
3908
|
+
expect(editor.getInstanceState().cursor).toMatchObject({ type: 'default', rotation: 0 })
|
|
3909
|
+
|
|
3910
|
+
editor.pointerUp()
|
|
3911
|
+
editor.expectToBeIn('select.idle')
|
|
3912
|
+
expect(editor.getInstanceState().cursor).toMatchObject({ type: 'default', rotation: 0 })
|
|
3913
|
+
})
|
|
3914
|
+
})
|
|
3915
|
+
|
|
3870
3916
|
describe('Resizing text from the right edge', () => {
|
|
3871
3917
|
it('Resizes text from the right edge', () => {
|
|
3872
3918
|
const id = createShapeId()
|
|
@@ -1376,6 +1376,29 @@ for (const key of ['Shift', 'Control']) {
|
|
|
1376
1376
|
editor.pointerUp()
|
|
1377
1377
|
editor.expectToBeIn('select.idle')
|
|
1378
1378
|
})
|
|
1379
|
+
|
|
1380
|
+
it('does not remove a selected shape from the selection on pointer up when alt is also held', () => {
|
|
1381
|
+
editor.select(ids.box1, ids.box3)
|
|
1382
|
+
editor.keyDown(key)
|
|
1383
|
+
editor.keyDown('Alt')
|
|
1384
|
+
editor.pointerMove(450, 50) // inside of box 3
|
|
1385
|
+
expect(editor.getHoveredShapeId()).toBe(ids.box3)
|
|
1386
|
+
editor.pointerDown()
|
|
1387
|
+
expect(editor.getSelectedShapeIds()).toEqual([ids.box1, ids.box3])
|
|
1388
|
+
editor.pointerUp()
|
|
1389
|
+
// same as a plain click on a shape within the selection
|
|
1390
|
+
expect(editor.getSelectedShapeIds()).toEqual([ids.box3])
|
|
1391
|
+
})
|
|
1392
|
+
|
|
1393
|
+
it('does not add a selected shape to the selection on pointer up when alt is also held', () => {
|
|
1394
|
+
editor.keyDown(key)
|
|
1395
|
+
editor.keyDown('Alt')
|
|
1396
|
+
editor.pointerMove(450, 50) // inside of box 3
|
|
1397
|
+
expect(editor.getHoveredShapeId()).toBe(ids.box3)
|
|
1398
|
+
editor.pointerDown()
|
|
1399
|
+
editor.pointerUp()
|
|
1400
|
+
expect(editor.getSelectedShapeIds()).toEqual([ids.box3])
|
|
1401
|
+
})
|
|
1379
1402
|
})
|
|
1380
1403
|
}
|
|
1381
1404
|
|
|
@@ -1792,6 +1815,33 @@ describe('scribble brushes to add to the selection', () => {
|
|
|
1792
1815
|
expect(editor.getSelectedShapeIds()).toEqual([ids.box1, ids.box2])
|
|
1793
1816
|
})
|
|
1794
1817
|
|
|
1818
|
+
it('restores the prior selection when cancelled without shift', () => {
|
|
1819
|
+
editor.select(ids.box2)
|
|
1820
|
+
editor.pointerMove(-50, -50)
|
|
1821
|
+
editor.keyDown('Alt')
|
|
1822
|
+
// ctrl-press keeps the current selection and starts a brush on drag
|
|
1823
|
+
editor.pointerDown(-50, -50, { target: 'canvas', accelKey: true })
|
|
1824
|
+
editor.pointerMove(50, 50)
|
|
1825
|
+
editor.expectToBeIn('select.scribble_brushing')
|
|
1826
|
+
expect(editor.getSelectedShapeIds()).toEqual([ids.box1])
|
|
1827
|
+
editor.cancel()
|
|
1828
|
+
editor.expectToBeIn('select.idle')
|
|
1829
|
+
expect(editor.getSelectedShapeIds()).toEqual([ids.box2])
|
|
1830
|
+
})
|
|
1831
|
+
|
|
1832
|
+
it('restores the prior selection when cancelled with shift', () => {
|
|
1833
|
+
editor.select(ids.box2)
|
|
1834
|
+
editor.pointerMove(-50, -50)
|
|
1835
|
+
editor.keyDown('Alt')
|
|
1836
|
+
editor.keyDown('Shift')
|
|
1837
|
+
editor.pointerDown(-50, -50, { target: 'canvas', accelKey: true })
|
|
1838
|
+
editor.pointerMove(50, 50)
|
|
1839
|
+
editor.expectToBeIn('select.scribble_brushing')
|
|
1840
|
+
expect(editor.getSelectedShapeIds()).toEqual([ids.box1, ids.box2])
|
|
1841
|
+
editor.cancel()
|
|
1842
|
+
expect(editor.getSelectedShapeIds()).toEqual([ids.box2])
|
|
1843
|
+
})
|
|
1844
|
+
|
|
1795
1845
|
it('selects when switching between moves', () => {
|
|
1796
1846
|
editor.ungroupShapes([ids.group1]) // ungroup boxes 3 and 4
|
|
1797
1847
|
editor.pointerMove(650, 0)
|