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
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { ReactNode, memo, useRef } from 'react'
|
|
1
|
+
import { ReactNode, memo } from 'react'
|
|
3
2
|
import { PORTRAIT_BREAKPOINT } from '../../constants'
|
|
4
3
|
import { useBreakpoint } from '../../context/breakpoints'
|
|
5
4
|
import { useReadonly } from '../../hooks/useReadonly'
|
|
@@ -29,22 +28,14 @@ export const DefaultActionsMenu = memo(function DefaultActionsMenu({
|
|
|
29
28
|
const isReadonlyMode = useReadonly()
|
|
30
29
|
const { orientation } = useTldrawUiOrientation()
|
|
31
30
|
|
|
32
|
-
const ref = useRef<HTMLDivElement>(null)
|
|
33
|
-
usePassThroughWheelEvents(ref)
|
|
34
|
-
|
|
35
|
-
const editor = useEditor()
|
|
36
|
-
const isInAcceptableReadonlyState = useValue(
|
|
37
|
-
'should display quick actions when in readonly',
|
|
38
|
-
() => editor.isInAny('hand', 'zoom'),
|
|
39
|
-
[editor]
|
|
40
|
-
)
|
|
41
|
-
|
|
42
31
|
// Get the actions menu content, either the default component or the user's
|
|
43
32
|
// override. If there's no menu content, then the user has set it to null,
|
|
44
33
|
// so skip rendering the menu.
|
|
45
34
|
|
|
46
35
|
const content = children ?? <DefaultActionsMenuContent />
|
|
47
|
-
|
|
36
|
+
|
|
37
|
+
// Every default action edits shapes, so in readonly mode the menu would open empty
|
|
38
|
+
if (isReadonlyMode) return
|
|
48
39
|
|
|
49
40
|
return (
|
|
50
41
|
<TldrawUiPopover id="actions-menu">
|
|
@@ -71,7 +62,6 @@ export const DefaultActionsMenu = memo(function DefaultActionsMenu({
|
|
|
71
62
|
sideOffset={6}
|
|
72
63
|
>
|
|
73
64
|
<TldrawUiToolbar
|
|
74
|
-
ref={ref}
|
|
75
65
|
label={msg('actions-menu.title')}
|
|
76
66
|
className="tlui-actions-menu"
|
|
77
67
|
data-testid="actions-menu.content"
|
|
@@ -6,7 +6,8 @@ import {
|
|
|
6
6
|
useValue,
|
|
7
7
|
} from '@tldraw/editor'
|
|
8
8
|
import { ContextMenu as _ContextMenu } from 'radix-ui'
|
|
9
|
-
import { ReactNode, memo, useCallback, useEffect, useRef } from 'react'
|
|
9
|
+
import { ReactNode, memo, useCallback, useContext, useEffect, useRef } from 'react'
|
|
10
|
+
import { ContextMenuPagePointContext } from '../../context/actions'
|
|
10
11
|
import { useMenuIsOpen } from '../../hooks/useMenuIsOpen'
|
|
11
12
|
import { useDirection, useTranslation } from '../../hooks/useTranslation/useTranslation'
|
|
12
13
|
import { TldrawUiMenuContextProvider } from '../primitives/menus/TldrawUiMenuContext'
|
|
@@ -74,9 +75,14 @@ export const DefaultContextMenu = memo(function DefaultContextMenu({
|
|
|
74
75
|
// interacts again.
|
|
75
76
|
const suppressDismissUntilRef = useRef(0)
|
|
76
77
|
|
|
78
|
+
const rContextMenuPagePoint = useContext(ContextMenuPagePointContext)
|
|
79
|
+
|
|
77
80
|
const cb = useCallback(
|
|
78
81
|
(isOpen: boolean) => {
|
|
79
82
|
const body = editor.getContainerDocument().body
|
|
83
|
+
if (rContextMenuPagePoint) {
|
|
84
|
+
rContextMenuPagePoint.current = isOpen ? editor.inputs.getCurrentPagePoint().clone() : null
|
|
85
|
+
}
|
|
80
86
|
if (!isOpen) {
|
|
81
87
|
const onlySelectedShape = editor.getOnlySelectedShape()
|
|
82
88
|
|
|
@@ -121,7 +127,7 @@ export const DefaultContextMenu = memo(function DefaultContextMenu({
|
|
|
121
127
|
}
|
|
122
128
|
}
|
|
123
129
|
},
|
|
124
|
-
[editor, preventEscapeFromLosingShapeFocus]
|
|
130
|
+
[editor, preventEscapeFromLosingShapeFocus, rContextMenuPagePoint]
|
|
125
131
|
)
|
|
126
132
|
|
|
127
133
|
const container = useContainer()
|
|
@@ -29,7 +29,11 @@ export function DefaultContextMenuContent() {
|
|
|
29
29
|
|
|
30
30
|
return (
|
|
31
31
|
<>
|
|
32
|
-
{showCollaborationUi &&
|
|
32
|
+
{showCollaborationUi && (
|
|
33
|
+
<TldrawUiMenuGroup id="cursor-chat">
|
|
34
|
+
<CursorChatItem />
|
|
35
|
+
</TldrawUiMenuGroup>
|
|
36
|
+
)}
|
|
33
37
|
<TldrawUiMenuGroup id="modify">
|
|
34
38
|
<EditMenuSubmenu />
|
|
35
39
|
<ArrangeMenuSubmenu />
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useContainer, useValue } from '@tldraw/editor'
|
|
2
2
|
import { Dialog as _Dialog } from 'radix-ui'
|
|
3
|
-
import { memo, useCallback } from 'react'
|
|
3
|
+
import { memo, useCallback, useRef } from 'react'
|
|
4
4
|
import { TLUiDialog, useDialogs } from '../context/dialogs'
|
|
5
5
|
import { useDirection } from '../hooks/useTranslation/useTranslation'
|
|
6
6
|
|
|
@@ -10,6 +10,7 @@ const TldrawUiDialog = ({ id, component: ModalContent, preventBackgroundClose }:
|
|
|
10
10
|
|
|
11
11
|
const container = useContainer()
|
|
12
12
|
const dir = useDirection()
|
|
13
|
+
const contentRef = useRef<HTMLDivElement>(null)
|
|
13
14
|
|
|
14
15
|
const handleOpenChange = useCallback(
|
|
15
16
|
(isOpen: boolean) => {
|
|
@@ -29,6 +30,7 @@ const TldrawUiDialog = ({ id, component: ModalContent, preventBackgroundClose }:
|
|
|
29
30
|
<_Dialog.Overlay dir={dir} className="tlui-dialog__overlay" />
|
|
30
31
|
<div dir={dir} className="tlui-dialog__positioner">
|
|
31
32
|
<_Dialog.Content
|
|
33
|
+
ref={contentRef}
|
|
32
34
|
dir={dir}
|
|
33
35
|
className="tlui-dialog__content"
|
|
34
36
|
aria-describedby={undefined}
|
|
@@ -39,6 +41,20 @@ const TldrawUiDialog = ({ id, component: ModalContent, preventBackgroundClose }:
|
|
|
39
41
|
// both a pointer press and a focus shift outside the dialog; opt out of both
|
|
40
42
|
// when the dialog asks to stay open on background interactions (escape still
|
|
41
43
|
// closes it).
|
|
44
|
+
|
|
45
|
+
// A touch press is checked against the layers at the following click rather
|
|
46
|
+
// than at the press itself, so a press that closes a dialog stacked on top of
|
|
47
|
+
// this one arrives here once this one is topmost, and reads as a press
|
|
48
|
+
// outside it. A press inside another dialog is never outside this one, and
|
|
49
|
+
// stays so after that dialog unmounts and takes the press's target with it.
|
|
50
|
+
const target = e.detail.originalEvent.target
|
|
51
|
+
const pressedDialog =
|
|
52
|
+
target instanceof Element ? target.closest('.tlui-dialog__content') : null
|
|
53
|
+
if (pressedDialog && pressedDialog !== contentRef.current) {
|
|
54
|
+
e.preventDefault()
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
|
|
42
58
|
if (preventBackgroundClose) {
|
|
43
59
|
e.preventDefault()
|
|
44
60
|
}
|
|
@@ -41,8 +41,15 @@ export const EditLinkDialog = track(function EditLinkDialog({ onClose }: TLUiDia
|
|
|
41
41
|
const editor = useEditor()
|
|
42
42
|
|
|
43
43
|
const selectedShape = editor.getOnlySelectedShape()
|
|
44
|
+
const hasShapeWithUrl = isShapeWithUrl(selectedShape)
|
|
44
45
|
|
|
45
|
-
|
|
46
|
+
// The shape can be deleted or deselected from under the open dialog (a collaborator, or a
|
|
47
|
+
// keypress reaching the canvas). Returning null on its own would leave an empty dialog frame.
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
if (!hasShapeWithUrl) onClose()
|
|
50
|
+
}, [hasShapeWithUrl, onClose])
|
|
51
|
+
|
|
52
|
+
if (!hasShapeWithUrl) {
|
|
46
53
|
return null
|
|
47
54
|
}
|
|
48
55
|
|
|
@@ -139,12 +146,6 @@ export const EditLinkDialogInner = track(function EditLinkDialogInner({
|
|
|
139
146
|
onClose()
|
|
140
147
|
}, [onClose])
|
|
141
148
|
|
|
142
|
-
if (!selectedShape) {
|
|
143
|
-
// dismiss modal
|
|
144
|
-
onClose()
|
|
145
|
-
return null
|
|
146
|
-
}
|
|
147
|
-
|
|
148
149
|
// Are we going from a valid state to an invalid state?
|
|
149
150
|
const isRemoving = rInitialValue.current && !urlInputState.valid
|
|
150
151
|
|
|
@@ -3,12 +3,11 @@ import {
|
|
|
3
3
|
TLPointerEventInfo,
|
|
4
4
|
Vec,
|
|
5
5
|
getPointerInfo,
|
|
6
|
-
isAccelKey,
|
|
7
|
-
normalizeWheel,
|
|
8
6
|
setPointerCapture,
|
|
9
7
|
useContainer,
|
|
10
8
|
useColorMode,
|
|
11
9
|
useEditor,
|
|
10
|
+
usePassThroughWheelEvents,
|
|
12
11
|
useValue,
|
|
13
12
|
} from '@tldraw/editor'
|
|
14
13
|
import * as React from 'react'
|
|
@@ -26,6 +25,9 @@ export function DefaultMinimap() {
|
|
|
26
25
|
const container = useContainer()
|
|
27
26
|
const msg = useTranslation()
|
|
28
27
|
|
|
28
|
+
const rMinimap = React.useRef<HTMLDivElement>(null)
|
|
29
|
+
usePassThroughWheelEvents(rMinimap)
|
|
30
|
+
|
|
29
31
|
const rCanvas = React.useRef<HTMLCanvasElement>(null!)
|
|
30
32
|
const rPointing = React.useRef(false)
|
|
31
33
|
const rActivePointerId = React.useRef<number | null>(null)
|
|
@@ -177,25 +179,6 @@ export function DefaultMinimap() {
|
|
|
177
179
|
[editor]
|
|
178
180
|
)
|
|
179
181
|
|
|
180
|
-
const onWheel = React.useCallback(
|
|
181
|
-
(e: React.WheelEvent<HTMLCanvasElement>) => {
|
|
182
|
-
const offset = normalizeWheel(e)
|
|
183
|
-
|
|
184
|
-
editor.dispatch({
|
|
185
|
-
type: 'wheel',
|
|
186
|
-
name: 'wheel',
|
|
187
|
-
delta: offset,
|
|
188
|
-
point: new Vec(e.clientX, e.clientY),
|
|
189
|
-
shiftKey: e.shiftKey,
|
|
190
|
-
altKey: e.altKey,
|
|
191
|
-
ctrlKey: e.metaKey || e.ctrlKey,
|
|
192
|
-
metaKey: e.metaKey,
|
|
193
|
-
accelKey: isAccelKey(e),
|
|
194
|
-
})
|
|
195
|
-
},
|
|
196
|
-
[editor]
|
|
197
|
-
)
|
|
198
|
-
|
|
199
182
|
const colorMode = useColorMode()
|
|
200
183
|
const currentThemeId = useValue('current theme id', () => editor.getCurrentThemeId(), [editor])
|
|
201
184
|
|
|
@@ -209,7 +192,7 @@ export function DefaultMinimap() {
|
|
|
209
192
|
}, [colorMode, currentThemeId, editor])
|
|
210
193
|
|
|
211
194
|
return (
|
|
212
|
-
<div className="tlui-minimap">
|
|
195
|
+
<div ref={rMinimap} className="tlui-minimap">
|
|
213
196
|
<canvas
|
|
214
197
|
ref={rCanvas}
|
|
215
198
|
role="img"
|
|
@@ -219,7 +202,6 @@ export function DefaultMinimap() {
|
|
|
219
202
|
onDoubleClick={onDoubleClick}
|
|
220
203
|
onPointerMove={onPointerMove}
|
|
221
204
|
onPointerDown={onPointerDown}
|
|
222
|
-
onWheelCapture={onWheel}
|
|
223
205
|
/>
|
|
224
206
|
</div>
|
|
225
207
|
)
|
|
@@ -4,7 +4,6 @@ import { useUiEvents } from '../../context/events'
|
|
|
4
4
|
import { useTranslation } from '../../hooks/useTranslation/useTranslation'
|
|
5
5
|
import { TldrawUiButton } from '../primitives/Button/TldrawUiButton'
|
|
6
6
|
import { TldrawUiButtonIcon } from '../primitives/Button/TldrawUiButtonIcon'
|
|
7
|
-
import { TldrawUiRow } from '../primitives/layout'
|
|
8
7
|
import { TldrawUiIcon } from '../primitives/TldrawUiIcon'
|
|
9
8
|
|
|
10
9
|
/** @public */
|
|
@@ -38,7 +37,7 @@ export const DefaultPeopleMenuItem = track(function DefaultPeopleMenuItem({
|
|
|
38
37
|
if (!presence) return null
|
|
39
38
|
|
|
40
39
|
return (
|
|
41
|
-
<
|
|
40
|
+
<div
|
|
42
41
|
className="tlui-people-menu__item"
|
|
43
42
|
data-follow={youAreFollowingThem || theyAreFollowingYou}
|
|
44
43
|
>
|
|
@@ -70,6 +69,6 @@ export const DefaultPeopleMenuItem = track(function DefaultPeopleMenuItem({
|
|
|
70
69
|
icon={theyAreFollowingYou ? 'leading' : youAreFollowingThem ? 'following' : 'follow'}
|
|
71
70
|
/>
|
|
72
71
|
</TldrawUiButton>
|
|
73
|
-
</
|
|
72
|
+
</div>
|
|
74
73
|
)
|
|
75
74
|
})
|
|
@@ -112,6 +112,7 @@ function StylePanelDoubleDropdownPickerInlineInner<T extends string>(
|
|
|
112
112
|
type="icon"
|
|
113
113
|
key={item.value}
|
|
114
114
|
onClick={() => {
|
|
115
|
+
ctx.onHistoryMark('select style dropdown item')
|
|
115
116
|
onValueChange(styleA, item.value)
|
|
116
117
|
tlmenus.deleteOpenMenu(idA, editor.contextId)
|
|
117
118
|
setIsOpenA(false)
|
|
@@ -153,6 +154,7 @@ function StylePanelDoubleDropdownPickerInlineInner<T extends string>(
|
|
|
153
154
|
title={`${msg(labelB)} — ${msg(`${uiTypeB}-style.${item.value}` as TLUiTranslationKey)}`}
|
|
154
155
|
data-testid={`style.${uiTypeB}.${item.value}`}
|
|
155
156
|
onClick={() => {
|
|
157
|
+
ctx.onHistoryMark('select style dropdown item')
|
|
156
158
|
onValueChange(styleB, item.value)
|
|
157
159
|
tlmenus.deleteOpenMenu(idB, editor.contextId)
|
|
158
160
|
setIsOpenB(false)
|
|
@@ -33,6 +33,7 @@ export function AltTextEditor({ shapeId, onClose, source }: AltTextEditorProps)
|
|
|
33
33
|
trackEvent('set-alt-text', { source })
|
|
34
34
|
const shape = editor.getShape<ExtractShapeByProps<{ altText: string }>>(shapeId)
|
|
35
35
|
if (!shape) return
|
|
36
|
+
editor.markHistoryStoppingPoint('set alt text')
|
|
36
37
|
editor.updateShapes([
|
|
37
38
|
{
|
|
38
39
|
id: shape.id,
|
|
@@ -189,15 +189,20 @@ export const DefaultImageToolbarContent = track(function DefaultImageToolbarCont
|
|
|
189
189
|
|
|
190
190
|
useEffect(() => {
|
|
191
191
|
function handleKeyDown(e: KeyboardEvent) {
|
|
192
|
-
if (isManipulating)
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
}
|
|
192
|
+
if (!isManipulating) return
|
|
193
|
+
if (e.key === 'Escape') {
|
|
194
|
+
editor.cancel()
|
|
195
|
+
} else if (e.key === 'Enter') {
|
|
196
|
+
editor.complete()
|
|
197
|
+
} else {
|
|
198
|
+
return
|
|
200
199
|
}
|
|
200
|
+
onManipulatingEnd()
|
|
201
|
+
// Otherwise the keydown bubbles to the container's handler, which cancels again
|
|
202
|
+
// against select.idle and clears the selection (#10405). Leaving crop mode also
|
|
203
|
+
// unmounts the slider, so refocus the container to keep shortcuts working.
|
|
204
|
+
e.stopPropagation()
|
|
205
|
+
editor.getContainer().focus()
|
|
201
206
|
}
|
|
202
207
|
const elm = sliderRef.current
|
|
203
208
|
if (elm) {
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { fireEvent, render } from '@testing-library/react'
|
|
2
|
+
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
3
|
+
import { TldrawUiSlider } from './TldrawUiSlider'
|
|
4
|
+
|
|
5
|
+
function renderSlider() {
|
|
6
|
+
const calls: string[] = []
|
|
7
|
+
const onValueChange = vi.fn((value: number) => {
|
|
8
|
+
calls.push(`change:${value}`)
|
|
9
|
+
})
|
|
10
|
+
const onHistoryMark = vi.fn((id: string) => {
|
|
11
|
+
calls.push(`mark:${id}`)
|
|
12
|
+
})
|
|
13
|
+
const result = render(
|
|
14
|
+
<TldrawUiSlider
|
|
15
|
+
data-testid="slider"
|
|
16
|
+
value={2}
|
|
17
|
+
steps={4}
|
|
18
|
+
label="style-panel.opacity"
|
|
19
|
+
title="Opacity"
|
|
20
|
+
onValueChange={onValueChange}
|
|
21
|
+
onHistoryMark={onHistoryMark}
|
|
22
|
+
/>
|
|
23
|
+
)
|
|
24
|
+
const thumb = result.getByRole('slider')
|
|
25
|
+
return { ...result, thumb, calls, onValueChange, onHistoryMark }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe('TldrawUiSlider', () => {
|
|
29
|
+
afterEach(() => {
|
|
30
|
+
vi.clearAllMocks()
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('marks a history stopping point before a keyboard change', () => {
|
|
34
|
+
const { thumb, calls } = renderSlider()
|
|
35
|
+
|
|
36
|
+
fireEvent.keyDown(thumb, { key: 'ArrowLeft' })
|
|
37
|
+
|
|
38
|
+
expect(calls).toEqual(['mark:keyboard slider', 'change:1'])
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it.each(['ArrowRight', 'ArrowUp', 'ArrowDown', 'PageUp', 'PageDown', 'Home', 'End'])(
|
|
42
|
+
'marks a history stopping point for %s',
|
|
43
|
+
(key) => {
|
|
44
|
+
const { thumb, onHistoryMark, onValueChange } = renderSlider()
|
|
45
|
+
|
|
46
|
+
fireEvent.keyDown(thumb, { key })
|
|
47
|
+
|
|
48
|
+
expect(onHistoryMark).toHaveBeenCalledTimes(1)
|
|
49
|
+
expect(onValueChange).toHaveBeenCalledTimes(1)
|
|
50
|
+
}
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
it('marks once for a held key so the repeat run is a single undo step', () => {
|
|
54
|
+
const { thumb, onHistoryMark, onValueChange } = renderSlider()
|
|
55
|
+
|
|
56
|
+
fireEvent.keyDown(thumb, { key: 'ArrowLeft' })
|
|
57
|
+
fireEvent.keyDown(thumb, { key: 'ArrowLeft', repeat: true })
|
|
58
|
+
fireEvent.keyDown(thumb, { key: 'ArrowLeft', repeat: true })
|
|
59
|
+
|
|
60
|
+
expect(onHistoryMark).toHaveBeenCalledTimes(1)
|
|
61
|
+
expect(onValueChange).toHaveBeenCalledTimes(3)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('marks each separate key press', () => {
|
|
65
|
+
const { thumb, onHistoryMark } = renderSlider()
|
|
66
|
+
|
|
67
|
+
fireEvent.keyDown(thumb, { key: 'ArrowLeft' })
|
|
68
|
+
fireEvent.keyDown(thumb, { key: 'ArrowLeft' })
|
|
69
|
+
fireEvent.keyDown(thumb, { key: 'ArrowLeft' })
|
|
70
|
+
|
|
71
|
+
expect(onHistoryMark).toHaveBeenCalledTimes(3)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('does not mark for keys that do not change the value', () => {
|
|
75
|
+
const { thumb, onHistoryMark, onValueChange } = renderSlider()
|
|
76
|
+
|
|
77
|
+
fireEvent.keyDown(thumb, { key: 'Tab' })
|
|
78
|
+
fireEvent.keyDown(thumb, { key: 'a' })
|
|
79
|
+
fireEvent.keyDown(thumb, { key: 'Enter' })
|
|
80
|
+
|
|
81
|
+
expect(onHistoryMark).not.toHaveBeenCalled()
|
|
82
|
+
expect(onValueChange).not.toHaveBeenCalled()
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('still marks a history stopping point on pointer down', () => {
|
|
86
|
+
const { getByTestId, onHistoryMark } = renderSlider()
|
|
87
|
+
|
|
88
|
+
fireEvent.pointerDown(getByTestId('slider'))
|
|
89
|
+
|
|
90
|
+
expect(onHistoryMark).toHaveBeenCalledWith('click slider')
|
|
91
|
+
})
|
|
92
|
+
})
|
|
@@ -5,6 +5,17 @@ import { TLUiTranslationKey } from '../../hooks/useTranslation/TLUiTranslationKe
|
|
|
5
5
|
import { useDirection, useTranslation } from '../../hooks/useTranslation/useTranslation'
|
|
6
6
|
import { hideAllTooltips, TldrawUiTooltip } from './TldrawUiTooltip'
|
|
7
7
|
|
|
8
|
+
const SLIDER_VALUE_KEYS = new Set([
|
|
9
|
+
'ArrowUp',
|
|
10
|
+
'ArrowDown',
|
|
11
|
+
'ArrowLeft',
|
|
12
|
+
'ArrowRight',
|
|
13
|
+
'PageUp',
|
|
14
|
+
'PageDown',
|
|
15
|
+
'Home',
|
|
16
|
+
'End',
|
|
17
|
+
])
|
|
18
|
+
|
|
8
19
|
/** @public */
|
|
9
20
|
export interface TLUiSliderProps {
|
|
10
21
|
min?: number
|
|
@@ -82,6 +93,19 @@ export const TldrawUiSlider = React.forwardRef<HTMLDivElement, TLUiSliderProps>(
|
|
|
82
93
|
}
|
|
83
94
|
}, [])
|
|
84
95
|
|
|
96
|
+
// Capture phase so the mark lands before Radix's bubble-phase onKeyDown changes the value;
|
|
97
|
+
// otherwise keyboard changes squash into the preceding history entry and undo skips past them.
|
|
98
|
+
// Repeats from a held key are skipped so the run is one undo step, like a pointer drag.
|
|
99
|
+
const handleKeyDown = useCallback(
|
|
100
|
+
(event: React.KeyboardEvent) => {
|
|
101
|
+
handleKeyEvent(event)
|
|
102
|
+
if (SLIDER_VALUE_KEYS.has(event.key) && !event.repeat) {
|
|
103
|
+
onHistoryMark?.('keyboard slider')
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
[handleKeyEvent, onHistoryMark]
|
|
107
|
+
)
|
|
108
|
+
|
|
85
109
|
return (
|
|
86
110
|
<div className="tlui-slider__container">
|
|
87
111
|
<TldrawUiTooltip content={titleAndLabel}>
|
|
@@ -95,7 +119,7 @@ export const TldrawUiSlider = React.forwardRef<HTMLDivElement, TLUiSliderProps>(
|
|
|
95
119
|
value={value !== null ? [value] : undefined}
|
|
96
120
|
onPointerDown={handlePointerDown}
|
|
97
121
|
onValueChange={handleValueChange}
|
|
98
|
-
onKeyDownCapture={
|
|
122
|
+
onKeyDownCapture={handleKeyDown}
|
|
99
123
|
onKeyUpCapture={handleKeyEvent}
|
|
100
124
|
>
|
|
101
125
|
<_Slider.Track className="tlui-slider__track" dir={dir}>
|
|
@@ -63,6 +63,15 @@ export type TLUiActionsContextType = Record<string, TLUiActionItem>
|
|
|
63
63
|
/** @internal */
|
|
64
64
|
export const ActionsContext = React.createContext<TLUiActionsContextType | null>(null)
|
|
65
65
|
|
|
66
|
+
/**
|
|
67
|
+
* The page point the context menu opened at, or null while it is closed. The context menu
|
|
68
|
+
* writes it; actions that place content (paste) read it, since the pointer keeps moving
|
|
69
|
+
* over the menu after it opens (#10423).
|
|
70
|
+
*
|
|
71
|
+
* @internal
|
|
72
|
+
*/
|
|
73
|
+
export const ContextMenuPagePointContext = React.createContext<{ current: Vec | null } | null>(null)
|
|
74
|
+
|
|
66
75
|
/** @public */
|
|
67
76
|
export interface ActionsProviderProps {
|
|
68
77
|
overrides?(
|
|
@@ -109,6 +118,8 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|
|
109
118
|
|
|
110
119
|
const defaultDocumentName = helpers.msg('document.default-name')
|
|
111
120
|
|
|
121
|
+
const rContextMenuPagePoint = React.useRef<Vec | null>(null)
|
|
122
|
+
|
|
112
123
|
// should this be a useMemo? looks like it doesn't actually deref any reactive values
|
|
113
124
|
const actions = React.useMemo<TLUiActionsContextType>(() => {
|
|
114
125
|
const editor = _editor as Editor
|
|
@@ -587,7 +598,7 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|
|
587
598
|
{
|
|
588
599
|
id: 'frame-selection',
|
|
589
600
|
label: 'action.frame-selection',
|
|
590
|
-
kbd: 'cmd+alt+g',
|
|
601
|
+
kbd: 'cmd+alt+g,ctrl+alt+g',
|
|
591
602
|
onSelect(source) {
|
|
592
603
|
if (!canApplySelectionAction()) return
|
|
593
604
|
if (mustGoBackToSelectToolFirst()) return
|
|
@@ -1031,7 +1042,6 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|
|
1031
1042
|
if (!canApplySelectionAction()) return
|
|
1032
1043
|
if (mustGoBackToSelectToolFirst()) return
|
|
1033
1044
|
|
|
1034
|
-
editor.markHistoryStoppingPoint('cut')
|
|
1035
1045
|
helpers.cut(source)
|
|
1036
1046
|
},
|
|
1037
1047
|
},
|
|
@@ -1052,14 +1062,16 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|
|
1052
1062
|
label: 'action.paste',
|
|
1053
1063
|
kbd: 'cmd+v,ctrl+v',
|
|
1054
1064
|
onSelect(source) {
|
|
1065
|
+
// Resolve the point before the clipboard read: the menu closes, and clears
|
|
1066
|
+
// its point, before the read settles.
|
|
1067
|
+
const point =
|
|
1068
|
+
source === 'context-menu'
|
|
1069
|
+
? (rContextMenuPagePoint.current ?? editor.inputs.getCurrentPagePoint())
|
|
1070
|
+
: undefined
|
|
1055
1071
|
navigator.clipboard
|
|
1056
1072
|
?.read()
|
|
1057
1073
|
.then((clipboardItems) => {
|
|
1058
|
-
helpers.paste(
|
|
1059
|
-
clipboardItems,
|
|
1060
|
-
source,
|
|
1061
|
-
source === 'context-menu' ? editor.inputs.getCurrentPagePoint() : undefined
|
|
1062
|
-
)
|
|
1074
|
+
helpers.paste(clipboardItems, source, point)
|
|
1063
1075
|
})
|
|
1064
1076
|
.catch(() => {
|
|
1065
1077
|
helpers.addToast({
|
|
@@ -1481,6 +1493,7 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|
|
1481
1493
|
}
|
|
1482
1494
|
}
|
|
1483
1495
|
if (updates.length > 0) {
|
|
1496
|
+
editor.markHistoryStoppingPoint('unlock all')
|
|
1484
1497
|
editor.updateShapes(updates)
|
|
1485
1498
|
}
|
|
1486
1499
|
},
|
|
@@ -1978,7 +1991,11 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|
|
1978
1991
|
components,
|
|
1979
1992
|
])
|
|
1980
1993
|
|
|
1981
|
-
return
|
|
1994
|
+
return (
|
|
1995
|
+
<ContextMenuPagePointContext.Provider value={rContextMenuPagePoint}>
|
|
1996
|
+
<ActionsContext.Provider value={asActions(actions)}>{children}</ActionsContext.Provider>
|
|
1997
|
+
</ContextMenuPagePointContext.Provider>
|
|
1998
|
+
)
|
|
1982
1999
|
}
|
|
1983
2000
|
|
|
1984
2001
|
/** @public */
|
|
@@ -803,6 +803,7 @@ export function useMenuClipboardEvents() {
|
|
|
803
803
|
|
|
804
804
|
const didCopy = await handleNativeOrMenuCopy(editor, { operation: 'cut', source: 'menu' })
|
|
805
805
|
if (didCopy) {
|
|
806
|
+
editor.markHistoryStoppingPoint('cut')
|
|
806
807
|
editor.deleteShapes(editor.getSelectedShapeIds())
|
|
807
808
|
trackEvent('cut', { source })
|
|
808
809
|
}
|
|
@@ -900,6 +901,7 @@ export function useNativeClipboardEvents() {
|
|
|
900
901
|
|
|
901
902
|
const didCopy = await handleNativeOrMenuCopy(editor, { operation: 'cut', source: 'native' })
|
|
902
903
|
if (didCopy) {
|
|
904
|
+
editor.markHistoryStoppingPoint('cut')
|
|
903
905
|
editor.deleteShapes(editor.getSelectedShapeIds())
|
|
904
906
|
trackEvent('cut', { source: 'kbd' })
|
|
905
907
|
}
|
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useLicenseFeatureFlag, useMaybeLicenseManager } from '@tldraw/editor'
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Whether the commenting feature is licensed for this editor. The default comment quick action and
|
|
5
5
|
* its keyboard shortcut are hidden/disabled unless this is true. Enabled in development; in
|
|
6
6
|
* production it requires a license that includes the commenting feature (or the collaboration
|
|
7
|
-
* umbrella that grants it). Reactive: re-reads when license validation resolves.
|
|
7
|
+
* umbrella that grants it). Reactive: re-reads when license validation resolves. Works outside
|
|
8
|
+
* `<Tldraw />` too: UI mounted via `EditorProvider` resolves the license through the editor, and
|
|
9
|
+
* with no editor at all this is `false`.
|
|
8
10
|
*
|
|
9
11
|
* @internal
|
|
10
12
|
*/
|
|
11
13
|
export function useCommentingEnabled(): boolean {
|
|
12
|
-
return useLicenseFeatureFlag(
|
|
14
|
+
return useLicenseFeatureFlag(useMaybeLicenseManager(), 'commenting')
|
|
13
15
|
}
|
|
@@ -49,34 +49,70 @@ export async function flattenShapesToImages(
|
|
|
49
49
|
}
|
|
50
50
|
})
|
|
51
51
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
52
|
+
// Cluster shapes by connected components: any two shapes whose expanded
|
|
53
|
+
// bounds intersect end up in the same group. Using union-find here
|
|
54
|
+
// instead of a greedy first-fit pass means the result is independent of
|
|
55
|
+
// the order shapes appear in `shapeIds`, and a shape that bridges two
|
|
56
|
+
// otherwise-disjoint clusters merges them in one pass.
|
|
57
|
+
const parents = expandedBounds.map((_, i) => i)
|
|
58
|
+
const find = (i: number): number => {
|
|
59
|
+
let root = i
|
|
60
|
+
while (parents[root] !== root) root = parents[root]
|
|
61
|
+
while (parents[i] !== root) {
|
|
62
|
+
const next = parents[i]
|
|
63
|
+
parents[i] = root
|
|
64
|
+
i = next
|
|
60
65
|
}
|
|
66
|
+
return root
|
|
67
|
+
}
|
|
68
|
+
const union = (i: number, j: number) => {
|
|
69
|
+
const ri = find(i)
|
|
70
|
+
const rj = find(j)
|
|
71
|
+
if (ri !== rj) parents[ri] = rj
|
|
72
|
+
}
|
|
61
73
|
|
|
62
|
-
|
|
74
|
+
// Broad-phase sweep: sort by minX and, for each box, only compare against
|
|
75
|
+
// later boxes until one starts past the current box's maxX. Boxes that
|
|
76
|
+
// don't overlap on the x-axis can't intersect, so this skips the bulk of
|
|
77
|
+
// the pairwise checks for spread-out selections while still finding every
|
|
78
|
+
// intersecting pair (Box.includes is symmetric, so each pair is tested once).
|
|
79
|
+
const order = expandedBounds
|
|
80
|
+
.map((_, i) => i)
|
|
81
|
+
.sort((a, b) => {
|
|
82
|
+
return expandedBounds[a].bounds.minX - expandedBounds[b].bounds.minX
|
|
83
|
+
})
|
|
63
84
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
85
|
+
for (let a = 0; a < order.length; a++) {
|
|
86
|
+
const i = order[a]
|
|
87
|
+
const boundsI = expandedBounds[i].bounds
|
|
88
|
+
for (let b = a + 1; b < order.length; b++) {
|
|
89
|
+
const j = order[b]
|
|
90
|
+
// sorted by minX, so once a box starts past i's maxX no later box can overlap i
|
|
91
|
+
if (expandedBounds[j].bounds.minX > boundsI.maxX) break
|
|
92
|
+
if (find(i) === find(j)) continue
|
|
93
|
+
if (boundsI.includes(expandedBounds[j].bounds)) {
|
|
94
|
+
union(i, j)
|
|
70
95
|
}
|
|
71
96
|
}
|
|
97
|
+
}
|
|
72
98
|
|
|
73
|
-
|
|
74
|
-
|
|
99
|
+
const groupsByRoot = new Map<number, { shapes: TLShape[]; bounds: Box }>()
|
|
100
|
+
for (let i = 0; i < expandedBounds.length; i++) {
|
|
101
|
+
const root = find(i)
|
|
102
|
+
const item = expandedBounds[i]
|
|
103
|
+
const existing = groupsByRoot.get(root)
|
|
104
|
+
if (existing) {
|
|
105
|
+
existing.shapes.push(item.shape)
|
|
106
|
+
existing.bounds.expand(item.bounds)
|
|
107
|
+
} else {
|
|
108
|
+
groupsByRoot.set(root, {
|
|
75
109
|
shapes: [item.shape],
|
|
76
|
-
bounds: item.bounds,
|
|
110
|
+
bounds: item.bounds.clone(),
|
|
77
111
|
})
|
|
78
112
|
}
|
|
79
113
|
}
|
|
114
|
+
|
|
115
|
+
groups.push(...groupsByRoot.values())
|
|
80
116
|
} else {
|
|
81
117
|
const bounds = Box.Common(shapes.map((shape) => editor.getShapeMaskedPageBounds(shape)!))
|
|
82
118
|
groups.push({
|
|
@@ -123,11 +159,8 @@ export async function flattenShapesToImages(
|
|
|
123
159
|
|
|
124
160
|
let index: IndexKey = 'a1' as IndexKey
|
|
125
161
|
for (const shape of shapes) {
|
|
126
|
-
if (shape.parentId === commonAncestorId) {
|
|
127
|
-
|
|
128
|
-
index = shape.index
|
|
129
|
-
}
|
|
130
|
-
break
|
|
162
|
+
if (shape.parentId === commonAncestorId && shape.index > index) {
|
|
163
|
+
index = shape.index
|
|
131
164
|
}
|
|
132
165
|
}
|
|
133
166
|
|