From 717b2c1610e7a94b8c640ebd98867fcdd67f1f3a Mon Sep 17 00:00:00 2001 From: rahulramesha <71900764+rahulramesha@users.noreply.github.com> Date: Fri, 6 Jan 2023 22:27:40 +0530 Subject: [PATCH] feat: drag n drop and Container jump enhancements (#19047) ## Description This PR includes Changes for Drag and drop Improvements, That includes, - Resizing dragging widgets along Container edges. - Initially resize widgets against Container/Droptarget widgets. - While holding close to Container/Droptarget widgets for half a second, start to reflow the widget. Fixes #19139 Fixes #12892 Media https://user-images.githubusercontent.com/71900764/209154834-66acecbb-2df8-4598-86d5-4fe7843dd21b.mp4 ## Type of change - Bug fix (non-breaking change which fixes an issue) - New feature (non-breaking change which adds functionality) ## How Has This Been Tested? - Manual - Jest ### Test Plan > Add Testsmith test cases links that relate to this PR ### Issues raised during DP testing > Link issues raised during DP testing for better visiblity and tracking (copy link from comments dropped on this PR) ## Checklist: ### Dev activity - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my own code - [x] I have commented my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] PR is being merged under a feature flag ### QA activity: - [ ] Test plan has been approved by relevant developers - [ ] Test plan has been peer reviewed by QA - [ ] Cypress test cases have been added and approved by either SDET or manual QA - [ ] Organized project review call with relevant stakeholders after Round 1/2 of QA - [ ] Added Test Plan Approved label after reveiwing all Cypress test --- .../BugTests/InputTruncateCheck_Spec.ts | 6 +- .../Onboarding/GuidedTour_spec.js | 4 +- .../Widgets/Others/Divider_spec.js | 2 +- .../src/actions/canvasSelectionActions.ts | 2 +- .../editorComponents/ResizableComponent.tsx | 2 +- .../editorComponents/ResizableUtils.ts | 2 +- .../src/constants/CanvasEditorConstants.tsx | 1 + .../CanvasArenas/CanvasSelectionArena.tsx | 2 +- .../CanvasArenas/hooks/ContainerJumpMetric.ts | 20 - .../hooks/canvasDraggingUtils.test.ts | 392 ++++++++++++ .../CanvasArenas/hooks/canvasDraggingUtils.ts | 316 ++++++++++ .../hooks/useBlocksToBeDraggedOnCanvas.ts | 92 +-- .../CanvasArenas/hooks/useCanvasDragging.ts | 556 ++++++------------ .../hooks/useRenderBlocksOnCanvas.ts | 159 +++++ .../uiReducers/canvasSelectionReducer.ts | 2 +- app/client/src/reflow/index.ts | 86 ++- app/client/src/reflow/reflowHelpers.ts | 20 +- app/client/src/reflow/reflowTypes.ts | 20 +- app/client/src/reflow/reflowUtils.ts | 416 ++++++++++++- .../src/reflow/tests/reflowHelpers.test.js | 14 +- .../src/reflow/tests/reflowUtils.test.js | 447 ++++++++++++-- .../src/resizable/resizenreflow/index.tsx | 2 +- app/client/src/sagas/selectors.tsx | 9 + app/client/src/selectors/editorSelectors.tsx | 9 +- app/client/src/utils/WidgetPropsUtils.tsx | 10 +- app/client/src/utils/hooks/useReflow.ts | 278 ++++++--- 26 files changed, 2187 insertions(+), 682 deletions(-) delete mode 100644 app/client/src/pages/common/CanvasArenas/hooks/ContainerJumpMetric.ts create mode 100644 app/client/src/pages/common/CanvasArenas/hooks/canvasDraggingUtils.test.ts create mode 100644 app/client/src/pages/common/CanvasArenas/hooks/canvasDraggingUtils.ts create mode 100644 app/client/src/pages/common/CanvasArenas/hooks/useRenderBlocksOnCanvas.ts diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/BugTests/InputTruncateCheck_Spec.ts b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/BugTests/InputTruncateCheck_Spec.ts index d4af3b2ba6..3e25d85733 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/BugTests/InputTruncateCheck_Spec.ts +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/BugTests/InputTruncateCheck_Spec.ts @@ -90,8 +90,8 @@ Object.entries(widgetsToTest).forEach(([widgetSelector, testConfig], index) => { configureApi(); } ee.PinUnpinEntityExplorer(false); - ee.DragDropWidgetNVerify(widgetSelector, 100, 200); - ee.DragDropWidgetNVerify(WIDGET.BUTTON, 400, 200); + ee.DragDropWidgetNVerify(widgetSelector, 300, 200); + ee.DragDropWidgetNVerify(WIDGET.BUTTON, 600, 200); //ee.SelectEntityByName(WIDGET.BUTTONNAME("1")); // Set onClick action, storing value propPane.EnterJSContext( @@ -99,7 +99,7 @@ Object.entries(widgetsToTest).forEach(([widgetSelector, testConfig], index) => { `{{storeValue('textPayloadOnSubmit',${testConfig.widgetPrefixName}1.text); FirstAPI.run({ value: ${testConfig.widgetPrefixName}1.text })}}`, ); - ee.DragDropWidgetNVerify(WIDGET.TEXT, 300, 300); + ee.DragDropWidgetNVerify(WIDGET.TEXT, 500, 300); //ee.SelectEntityByName(WIDGET.TEXTNAME("1")); // Display the bound store value propPane.UpdatePropertyFieldValue( diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Onboarding/GuidedTour_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Onboarding/GuidedTour_spec.js index 1d7c13f3ea..f5fba9fd0f 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Onboarding/GuidedTour_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Onboarding/GuidedTour_spec.js @@ -53,8 +53,8 @@ describe("Guided Tour", function() { cy.get(guidedTourLocators.successButton).click(); // Step 6: Drag and drop a widget cy.dragAndDropToCanvas("buttonwidget", { - x: 700, - y: 400, + x: 800, + y: 750, }); cy.get(guidedTourLocators.successButton).click(); cy.get(guidedTourLocators.infoButton).click(); diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Widgets/Others/Divider_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Widgets/Others/Divider_spec.js index b25485bc5d..9d79f3e63c 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Widgets/Others/Divider_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Widgets/Others/Divider_spec.js @@ -8,7 +8,7 @@ describe("Divider Widget Functionality", function() { it("Add new Divider", () => { cy.get(explorer.addWidget).click(); - cy.dragAndDropToCanvas("dividerwidget", { x: 320, y: 300 }); + cy.dragAndDropToCanvas("dividerwidget", { x: 320, y: 200 }); cy.get(".t--divider-widget").should("exist"); }); diff --git a/app/client/src/actions/canvasSelectionActions.ts b/app/client/src/actions/canvasSelectionActions.ts index b68d723e67..a296229281 100644 --- a/app/client/src/actions/canvasSelectionActions.ts +++ b/app/client/src/actions/canvasSelectionActions.ts @@ -3,7 +3,7 @@ import { ReduxActionTypes, } from "@appsmith/constants/ReduxActionConstants"; import { SelectedArenaDimensions } from "pages/common/CanvasArenas/CanvasSelectionArena"; -import { XYCord } from "pages/common/CanvasArenas/hooks/useCanvasDragging"; +import { XYCord } from "pages/common/CanvasArenas/hooks/useRenderBlocksOnCanvas"; export const setCanvasSelectionFromEditor = ( start: boolean, diff --git a/app/client/src/components/editorComponents/ResizableComponent.tsx b/app/client/src/components/editorComponents/ResizableComponent.tsx index fad2e5f6ab..913028d58a 100644 --- a/app/client/src/components/editorComponents/ResizableComponent.tsx +++ b/app/client/src/components/editorComponents/ResizableComponent.tsx @@ -40,7 +40,7 @@ import { useWidgetSelection } from "utils/hooks/useWidgetSelection"; import { focusWidget } from "actions/widgetActions"; import { GridDefaults } from "constants/WidgetConstants"; import { DropTargetContext } from "./DropTargetComponent"; -import { XYCord } from "pages/common/CanvasArenas/hooks/useCanvasDragging"; +import { XYCord } from "pages/common/CanvasArenas/hooks/useRenderBlocksOnCanvas"; import { isAutoHeightEnabledForWidget } from "widgets/WidgetUtils"; import { getParentToOpenSelector } from "selectors/widgetSelectors"; import { diff --git a/app/client/src/components/editorComponents/ResizableUtils.ts b/app/client/src/components/editorComponents/ResizableUtils.ts index d1395858d4..32bb9e0c22 100644 --- a/app/client/src/components/editorComponents/ResizableUtils.ts +++ b/app/client/src/components/editorComponents/ResizableUtils.ts @@ -1,6 +1,6 @@ import { WidgetProps, WidgetRowCols } from "widgets/BaseWidget"; import { GridDefaults } from "constants/WidgetConstants"; -import { XYCord } from "pages/common/CanvasArenas/hooks/useCanvasDragging"; +import { XYCord } from "pages/common/CanvasArenas/hooks/useRenderBlocksOnCanvas"; import { ReflowDirection } from "reflow/reflowTypes"; export type UIElementSize = { height: number; width: number }; diff --git a/app/client/src/constants/CanvasEditorConstants.tsx b/app/client/src/constants/CanvasEditorConstants.tsx index a368c0274c..15751d845f 100644 --- a/app/client/src/constants/CanvasEditorConstants.tsx +++ b/app/client/src/constants/CanvasEditorConstants.tsx @@ -15,6 +15,7 @@ export type WidgetSpace = { id: string; type: string; parentId?: string; + isDropTarget?: boolean; fixedHeight?: number; }; diff --git a/app/client/src/pages/common/CanvasArenas/CanvasSelectionArena.tsx b/app/client/src/pages/common/CanvasArenas/CanvasSelectionArena.tsx index 8002817ad4..95eda99ac0 100644 --- a/app/client/src/pages/common/CanvasArenas/CanvasSelectionArena.tsx +++ b/app/client/src/pages/common/CanvasArenas/CanvasSelectionArena.tsx @@ -17,7 +17,7 @@ import { import { getNearestParentCanvas } from "utils/generators"; import { useCanvasDragToScroll } from "./hooks/useCanvasDragToScroll"; import { MAIN_CONTAINER_WIDGET_ID } from "constants/WidgetConstants"; -import { XYCord } from "./hooks/useCanvasDragging"; +import { XYCord } from "pages/common/CanvasArenas/hooks/useRenderBlocksOnCanvas"; import { theme } from "constants/DefaultTheme"; import { getIsDraggingForSelection } from "selectors/canvasSelectors"; import { StickyCanvasArena } from "./StickyCanvasArena"; diff --git a/app/client/src/pages/common/CanvasArenas/hooks/ContainerJumpMetric.ts b/app/client/src/pages/common/CanvasArenas/hooks/ContainerJumpMetric.ts deleted file mode 100644 index 8c39cbcfdc..0000000000 --- a/app/client/src/pages/common/CanvasArenas/hooks/ContainerJumpMetric.ts +++ /dev/null @@ -1,20 +0,0 @@ -//class to maintain containerJump metrics across containers. -export default class ContainerJumpMetrics { - private containerJumpValues: T = {} as T; - - public setMetrics(args: T) { - this.containerJumpValues = { - ...args, - }; - } - - public getMetrics() { - return { - ...this.containerJumpValues, - }; - } - - public clearMetrics() { - this.containerJumpValues = {} as T; - } -} diff --git a/app/client/src/pages/common/CanvasArenas/hooks/canvasDraggingUtils.test.ts b/app/client/src/pages/common/CanvasArenas/hooks/canvasDraggingUtils.test.ts new file mode 100644 index 0000000000..589ed20aae --- /dev/null +++ b/app/client/src/pages/common/CanvasArenas/hooks/canvasDraggingUtils.test.ts @@ -0,0 +1,392 @@ +import { GridDefaults } from "constants/WidgetConstants"; +import { + HORIZONTAL_RESIZE_MIN_LIMIT, + ReflowDirection, + VERTICAL_RESIZE_MIN_LIMIT, +} from "reflow/reflowTypes"; +import { + getEdgeDirection, + getMoveDirection, + getReflowedSpaces, + modifyBlockDimension, + modifyDrawingRectangles, + updateRectanglesPostReflow, +} from "./canvasDraggingUtils"; + +describe("test canvasDraggingUtils Methods", () => { + describe("test getEdgeDirection method", () => { + it("should return RIGHT if closest to left edge", () => { + expect(getEdgeDirection(5, 10, 100, ReflowDirection.UNSET)).toEqual( + ReflowDirection.RIGHT, + ); + }); + it("should return BOTTOM if closest to left edge", () => { + expect(getEdgeDirection(10, 5, 100, ReflowDirection.UNSET)).toEqual( + ReflowDirection.BOTTOM, + ); + }); + it("should return LEFT if closest to left edge", () => { + expect(getEdgeDirection(95, 10, 100, ReflowDirection.UNSET)).toEqual( + ReflowDirection.LEFT, + ); + }); + it("should return current direction if width is undefined", () => { + expect(getEdgeDirection(5, 10, undefined, ReflowDirection.UNSET)).toEqual( + ReflowDirection.UNSET, + ); + }); + }); + + it("test getReflowedSpaces method, should return reflowed spaces", () => { + const occupiedSpace = { + id: "id", + left: 10, + top: 10, + right: 50, + bottom: 70, + }; + + const reflowingWidgets = { + id: { + X: 30, + Y: 40, + width: 300, + height: 500, + }, + }; + + const reflowedSpace = { + id: "id", + left: 13, + top: 14, + right: 43, + bottom: 64, + }; + + expect(getReflowedSpaces(occupiedSpace, reflowingWidgets, 10, 10)).toEqual( + reflowedSpace, + ); + }); + + it("test modifyDrawingRectangles method, should return widgetDraggingBlock with dimensions of the space widget", () => { + const drawingRectangles = { + left: 104, + top: 102, + width: 600, + height: 900, + columnWidth: 60, + rowHeight: 90, + widgetId: "id", + isNotColliding: true, + }; + const spaceMap = { + id: { + left: 25, + top: 30, + right: 65, + bottom: 80, + id: "id", + }, + }; + const modifiedRectangle = { + left: 254, + top: 302, + width: 400, + height: 500, + columnWidth: 40, + rowHeight: 50, + widgetId: "id", + isNotColliding: true, + }; + + expect( + modifyDrawingRectangles([drawingRectangles], spaceMap, 10, 10), + ).toEqual([modifiedRectangle]); + }); + + describe("test getMoveDirection method", () => { + const prevPosition = { + id: "id", + left: 10, + top: 20, + right: 30, + bottom: 40, + }; + + it("should return RIGHT when moved to Right", () => { + const currentPosition = { + id: "id", + left: 11, + top: 20, + right: 31, + bottom: 40, + }; + expect( + getMoveDirection(prevPosition, currentPosition, ReflowDirection.UNSET), + ).toEqual(ReflowDirection.RIGHT); + }); + it("should return BOTTOM when moved to bottom", () => { + const currentPosition = { + id: "id", + left: 10, + top: 21, + right: 30, + bottom: 41, + }; + expect( + getMoveDirection(prevPosition, currentPosition, ReflowDirection.UNSET), + ).toEqual(ReflowDirection.BOTTOM); + }); + it("should return LEFT when moved to left", () => { + const currentPosition = { + id: "id", + left: 9, + top: 20, + right: 29, + bottom: 40, + }; + expect( + getMoveDirection(prevPosition, currentPosition, ReflowDirection.UNSET), + ).toEqual(ReflowDirection.LEFT); + }); + it("should return TOP when moved to top", () => { + const currentPosition = { + id: "id", + left: 10, + top: 19, + right: 30, + bottom: 39, + }; + expect( + getMoveDirection(prevPosition, currentPosition, ReflowDirection.UNSET), + ).toEqual(ReflowDirection.TOP); + }); + }); + + describe("test modifyBlockDimension method", () => { + it("should return resized dragging blocks while colliding with canvas edges, for top Left", () => { + const draggingBlock = { + left: -300, + top: -700, + width: 600, + height: 900, + columnWidth: 60, + rowHeight: 90, + widgetId: "id", + isNotColliding: true, + }; + const modifiedBlock = { + left: 0, + top: 0, + width: 300, + height: 200, + columnWidth: 30, + rowHeight: 20, + widgetId: "id", + isNotColliding: true, + }; + expect(modifyBlockDimension(draggingBlock, 10, 10, 100, true)).toEqual( + modifiedBlock, + ); + }); + + it("should return resized dragging blocks while colliding with canvas edges to it's limits, for top Left", () => { + const draggingBlock = { + left: -300, + top: -700, + width: 310, + height: 720, + columnWidth: 31, + rowHeight: 72, + widgetId: "id", + isNotColliding: true, + }; + const modifiedBlock = { + left: -10, + top: -20, + width: HORIZONTAL_RESIZE_MIN_LIMIT * 10, + height: VERTICAL_RESIZE_MIN_LIMIT * 10, + columnWidth: HORIZONTAL_RESIZE_MIN_LIMIT, + rowHeight: VERTICAL_RESIZE_MIN_LIMIT, + widgetId: "id", + isNotColliding: true, + }; + expect(modifyBlockDimension(draggingBlock, 10, 10, 100, true)).toEqual( + modifiedBlock, + ); + }); + + it("should return resized dragging blocks while colliding with canvas edges, for bottom right", () => { + const draggingBlock = { + left: 400, + top: 600, + width: 600, + height: 900, + columnWidth: 60, + rowHeight: 90, + widgetId: "id", + isNotColliding: true, + }; + const modifiedBlock = { + left: 400, + top: 600, + width: GridDefaults.DEFAULT_GRID_COLUMNS * 10 - 400, + height: 400, + columnWidth: GridDefaults.DEFAULT_GRID_COLUMNS - 40, + rowHeight: 40, + widgetId: "id", + isNotColliding: true, + }; + expect(modifyBlockDimension(draggingBlock, 10, 10, 100, false)).toEqual( + modifiedBlock, + ); + }); + + it("should return resized dragging blocks while colliding with canvas edges to it's limits, for bottom right", () => { + const draggingBlock = { + left: 630, + top: 600, + width: 600, + height: 900, + columnWidth: 60, + rowHeight: 90, + widgetId: "id", + isNotColliding: true, + fixedHeight: 90, + }; + const modifiedBlock = { + left: 630, + top: 600, + width: HORIZONTAL_RESIZE_MIN_LIMIT * 10, + height: 900, + columnWidth: HORIZONTAL_RESIZE_MIN_LIMIT, + rowHeight: 90, + widgetId: "id", + isNotColliding: true, + fixedHeight: 90, + }; + expect(modifyBlockDimension(draggingBlock, 10, 10, 100, false)).toEqual( + modifiedBlock, + ); + }); + }); + + describe("should test updateRectanglesPostReflow method", () => { + it("should update noCollision properties based on respective rectangles", () => { + const rectanglesToDraw = [ + { + left: -10, + top: 200, + width: 600, + height: 900, + columnWidth: 60, + rowHeight: 90, + widgetId: "1", + isNotColliding: true, + }, + { + left: 100, + top: 200, + width: 700, + height: 950, + columnWidth: 70, + rowHeight: 95, + widgetId: "2", + isNotColliding: true, + }, + { + left: 300, + top: 100, + width: 200, + height: 340, + columnWidth: 20, + rowHeight: 34, + widgetId: "3", + isNotColliding: true, + }, + { + left: 400, + top: 500, + width: 200, + height: 120, + columnWidth: 20, + rowHeight: 12, + widgetId: "4", + isNotColliding: true, + }, + ]; + + const result = [ + { + left: -10, + top: 200, + width: 600, + height: 900, + columnWidth: 60, + rowHeight: 90, + widgetId: "1", + isNotColliding: false, + }, + { + left: 100, + top: 200, + width: 700, + height: 950, + columnWidth: 70, + rowHeight: 95, + widgetId: "2", + isNotColliding: false, + }, + { + left: 300, + top: 100, + width: 200, + height: 340, + columnWidth: 20, + rowHeight: 34, + widgetId: "3", + isNotColliding: false, + }, + { + left: 400, + top: 500, + width: 200, + height: 120, + columnWidth: 20, + rowHeight: 12, + widgetId: "4", + isNotColliding: true, + }, + ]; + + const movementLimitMap = { + "1": { + canHorizontalMove: true, + canVerticalMove: true, + }, + "2": { + canHorizontalMove: true, + canVerticalMove: true, + }, + "3": { + canHorizontalMove: true, + canVerticalMove: false, + }, + "4": { + canHorizontalMove: true, + canVerticalMove: true, + }, + }; + + expect( + updateRectanglesPostReflow( + movementLimitMap, + rectanglesToDraw, + 10, + 10, + 2000, + ), + ).toEqual(result); + }); + }); +}); diff --git a/app/client/src/pages/common/CanvasArenas/hooks/canvasDraggingUtils.ts b/app/client/src/pages/common/CanvasArenas/hooks/canvasDraggingUtils.ts new file mode 100644 index 0000000000..12b7230707 --- /dev/null +++ b/app/client/src/pages/common/CanvasArenas/hooks/canvasDraggingUtils.ts @@ -0,0 +1,316 @@ +import { OccupiedSpace } from "constants/CanvasEditorConstants"; +import { GridDefaults } from "constants/WidgetConstants"; +import { + HORIZONTAL_RESIZE_MIN_LIMIT, + MovementLimitMap, + ReflowDirection, + ReflowedSpaceMap, + SpaceMap, + VERTICAL_RESIZE_MIN_LIMIT, +} from "reflow/reflowTypes"; +import { + getDraggingSpacesFromBlocks, + getDropZoneOffsets, + noCollision, +} from "utils/WidgetPropsUtils"; +import { WidgetDraggingBlock } from "./useBlocksToBeDraggedOnCanvas"; + +/** + * Method to get the Direction appropriate to closest edge of the canvas + * @param x x coordinate of mouse position + * @param y y coordinate of mouse position + * @param width width of canvas + * @param currentDirection current direction based on mouse movement + * @returns closest edge + */ +export const getEdgeDirection = ( + x: number, + y: number, + width: number | undefined, + currentDirection: ReflowDirection, +) => { + if (width === undefined) return currentDirection; + const topEdgeDist = Math.abs(y); + const leftEdgeDist = Math.abs(x); + const rightEdgeDist = Math.abs(width - x); + const min = Math.min(topEdgeDist, leftEdgeDist, rightEdgeDist); + switch (min) { + case leftEdgeDist: + return ReflowDirection.RIGHT; + case rightEdgeDist: + return ReflowDirection.LEFT; + case topEdgeDist: + return ReflowDirection.BOTTOM; + default: + return currentDirection; + } +}; + +/** + * Modify the existing space to the reflowed positions + * @param draggingSpace position object of dragging Space + * @param reflowingWidgets reflowed parameters of widgets + * @param snapColumnSpace width between columns + * @param snapRowSpace height between rows + * @returns Modified position + */ +export function getReflowedSpaces( + draggingSpace: OccupiedSpace, + reflowingWidgets: ReflowedSpaceMap, + snapColumnSpace: number, + snapRowSpace: number, +) { + const reflowedWidget = reflowingWidgets[draggingSpace.id]; + if ( + reflowedWidget.X !== undefined && + (Math.abs(reflowedWidget.X) || reflowedWidget.width) + ) { + const movement = reflowedWidget.X / snapColumnSpace; + const newWidth = reflowedWidget.width + ? reflowedWidget.width / snapColumnSpace + : draggingSpace.right - draggingSpace.left; + draggingSpace = { + ...draggingSpace, + left: draggingSpace.left + movement, + right: draggingSpace.left + movement + newWidth, + }; + } + if ( + reflowedWidget.Y !== undefined && + (Math.abs(reflowedWidget.Y) || reflowedWidget.height) + ) { + const movement = reflowedWidget.Y / snapRowSpace; + const newHeight = reflowedWidget.height + ? reflowedWidget.height / snapRowSpace + : draggingSpace.bottom - draggingSpace.top; + draggingSpace = { + ...draggingSpace, + top: draggingSpace.top + movement, + bottom: draggingSpace.top + movement + newHeight, + }; + } + return draggingSpace; +} + +/** + * Modify the rectangles to draw object to match the positions of spaceMap + * @param rectanglesToDraw dragging parameters of widget + * @param spaceMap Widget Position + * @param snapColumnSpace width between columns + * @param snapRowSpace height between rows + * @returns modified rectangles to draw + */ +export function modifyDrawingRectangles( + rectanglesToDraw: WidgetDraggingBlock[], + spaceMap: SpaceMap | undefined, + snapColumnSpace: number, + snapRowSpace: number, +): WidgetDraggingBlock[] { + const rectangleToDraw = rectanglesToDraw?.[0]; + + if (rectanglesToDraw.length !== 1 || !spaceMap?.[rectangleToDraw?.widgetId]) + return rectanglesToDraw; + + const { bottom, left, right, top } = spaceMap[rectangleToDraw.widgetId]; + + const resizedPosition = getDraggingSpacesFromBlocks( + rectanglesToDraw, + snapColumnSpace, + snapRowSpace, + )[0]; + + return [ + { + ...rectanglesToDraw[0], + left: + (left - resizedPosition.left) * snapColumnSpace + + rectanglesToDraw[0].left, + top: (top - resizedPosition.top) * snapRowSpace + rectanglesToDraw[0].top, + width: (right - left) * snapColumnSpace, + height: (bottom - top) * snapRowSpace, + rowHeight: bottom - top, + columnWidth: right - left, + }, + ]; +} + +/** + * Direction of movement based on previous position of dragging widget + * @param prevPosition + * @param currentPosition + * @param currentDirection + * @returns movement direction + */ +export function getMoveDirection( + prevPosition: OccupiedSpace, + currentPosition: OccupiedSpace, + currentDirection: ReflowDirection, +) { + if (!prevPosition || !currentPosition) return currentDirection; + + if ( + currentPosition.right - prevPosition.right > 0 || + currentPosition.left - prevPosition.left > 0 + ) + return ReflowDirection.RIGHT; + + if ( + currentPosition.right - prevPosition.right < 0 || + currentPosition.left - prevPosition.left < 0 + ) + return ReflowDirection.LEFT; + + if ( + currentPosition.bottom - prevPosition.bottom > 0 || + currentPosition.top - prevPosition.top > 0 + ) + return ReflowDirection.BOTTOM; + + if ( + currentPosition.bottom - prevPosition.bottom < 0 || + currentPosition.top - prevPosition.top < 0 + ) + return ReflowDirection.TOP; + + return currentDirection; +} + +/** + * Modify the dragging Blocks to resize against canvas edges + * @param draggingBlock + * @param snapColumnSpace + * @param snapRowSpace + * @param parentBottomRow + * @param canExtend + * @returns + */ +export const modifyBlockDimension = ( + draggingBlock: WidgetDraggingBlock, + snapColumnSpace: number, + snapRowSpace: number, + parentBottomRow: number, + canExtend: boolean, +) => { + const { + columnWidth, + fixedHeight, + height, + left, + rowHeight, + top, + width, + } = draggingBlock; + + //get left and top of widget on canvas grid + const [leftColumn, topRow] = getDropZoneOffsets( + snapColumnSpace, + snapRowSpace, + { + x: left, + y: top, + }, + { + x: 0, + y: 0, + }, + ); + + let leftOffset = 0, + rightOffset = 0, + topOffset = 0, + bottomOffset = 0; + + // calculate offsets based on collisions and limits + if (leftColumn < 0) { + leftOffset = + leftColumn + columnWidth > HORIZONTAL_RESIZE_MIN_LIMIT + ? leftColumn + : HORIZONTAL_RESIZE_MIN_LIMIT - columnWidth; + } else if (leftColumn + columnWidth > GridDefaults.DEFAULT_GRID_COLUMNS) { + rightOffset = GridDefaults.DEFAULT_GRID_COLUMNS - leftColumn - columnWidth; + rightOffset = + columnWidth + rightOffset >= HORIZONTAL_RESIZE_MIN_LIMIT + ? rightOffset + : HORIZONTAL_RESIZE_MIN_LIMIT - columnWidth; + } + + if (topRow < 0 && fixedHeight === undefined) { + topOffset = + topRow + rowHeight > VERTICAL_RESIZE_MIN_LIMIT + ? topRow + : VERTICAL_RESIZE_MIN_LIMIT - rowHeight; + } + + if ( + topRow + rowHeight > parentBottomRow && + !canExtend && + fixedHeight === undefined + ) { + bottomOffset = parentBottomRow - topRow - rowHeight; + bottomOffset = + rowHeight + bottomOffset >= VERTICAL_RESIZE_MIN_LIMIT + ? bottomOffset + : VERTICAL_RESIZE_MIN_LIMIT - rowHeight; + } + + return { + ...draggingBlock, + left: left - leftOffset * snapColumnSpace, + top: top - topOffset * snapRowSpace, + width: width + (leftOffset + rightOffset) * snapColumnSpace, + height: height + (topOffset + bottomOffset) * snapRowSpace, + columnWidth: columnWidth + leftOffset + rightOffset, + rowHeight: rowHeight + topOffset + bottomOffset, + }; +}; + +/** + * updates isColliding of each block based on movementLimitMap post reflow + * @param movementLimitMap limits of each widgets + * @param currentRectanglesToDraw dragging parameters of widget + * @param snapColumnSpace width between each columns + * @param snapRowSpace height between each rows + * @param rows number of rows in canvas + * @returns array of rectangle blocks to draw + */ +export const updateRectanglesPostReflow = ( + movementLimitMap: MovementLimitMap | undefined, + currentRectanglesToDraw: WidgetDraggingBlock[], + snapColumnSpace: number, + snapRowSpace: number, + rows: number, +) => { + const rectanglesToDraw: WidgetDraggingBlock[] = []; + for (const block of currentRectanglesToDraw) { + const isWithinParentBoundaries = noCollision( + { x: block.left, y: block.top }, + snapColumnSpace, + snapRowSpace, + { x: 0, y: 0 }, + block.columnWidth, + block.rowHeight, + block.widgetId, + [], + rows, + GridDefaults.DEFAULT_GRID_COLUMNS, + block.detachFromLayout, + ); + + let isNotReachedLimit = true; + const currentBlockLimit = + movementLimitMap && movementLimitMap[block.widgetId]; + + if (currentBlockLimit) { + isNotReachedLimit = + currentBlockLimit.canHorizontalMove && + currentBlockLimit.canVerticalMove; + } + + rectanglesToDraw.push({ + ...block, + isNotColliding: isWithinParentBoundaries && isNotReachedLimit, + }); + } + + return rectanglesToDraw; +}; diff --git a/app/client/src/pages/common/CanvasArenas/hooks/useBlocksToBeDraggedOnCanvas.ts b/app/client/src/pages/common/CanvasArenas/hooks/useBlocksToBeDraggedOnCanvas.ts index 4c607ae298..795acb617a 100644 --- a/app/client/src/pages/common/CanvasArenas/hooks/useBlocksToBeDraggedOnCanvas.ts +++ b/app/client/src/pages/common/CanvasArenas/hooks/useBlocksToBeDraggedOnCanvas.ts @@ -8,7 +8,7 @@ import { AppState } from "@appsmith/reducers"; import { getSelectedWidgets } from "selectors/ui"; import { getOccupiedSpacesWhileMoving } from "selectors/editorSelectors"; import { getTableFilterState } from "selectors/tableFilterSelectors"; -import { OccupiedSpace } from "constants/CanvasEditorConstants"; +import { OccupiedSpace, WidgetSpace } from "constants/CanvasEditorConstants"; import { getDragDetails, getWidgetByID, getWidgets } from "sagas/selectors"; import { getDropZoneOffsets, @@ -28,8 +28,7 @@ import { snapToGrid } from "utils/helpers"; import { stopReflowAction } from "actions/reflowActions"; import { DragDetails } from "reducers/uiReducers/dragResizeReducer"; import { getIsReflowing } from "selectors/widgetReflowSelectors"; -import { XYCord } from "./useCanvasDragging"; -import ContainerJumpMetrics from "./ContainerJumpMetric"; +import { XYCord } from "pages/common/CanvasArenas/hooks/useRenderBlocksOnCanvas"; export interface WidgetDraggingUpdateParams extends WidgetDraggingBlock { updateWidgetParams: WidgetOperationParams; @@ -45,28 +44,7 @@ export type WidgetDraggingBlock = { widgetId: string; isNotColliding: boolean; detachFromLayout?: boolean; -}; - -const containerJumpMetrics = new ContainerJumpMetrics<{ - speed?: number; - acceleration?: number; - movingInto?: string; -}>(); - -// This method is called on drop, -// This method logs the metrics container jump and marks it as successful container jump, -// If widget has moves into a container and drops there. -const logContainerJumpOnDrop = () => { - const { acceleration, movingInto, speed } = containerJumpMetrics.getMetrics(); - // If it is dropped into a container after jumping, then - if (movingInto) { - AnalyticsUtil.logEvent("CONTAINER_JUMP", { - speed: speed, - acceleration: acceleration, - isAccidental: false, - }); - } - containerJumpMetrics.clearMetrics(); + fixedHeight?: number; }; export const useBlocksToBeDraggedOnCanvas = ({ @@ -119,7 +97,7 @@ export const useBlocksToBeDraggedOnCanvas = ({ const selectedWidgets = useSelector(getSelectedWidgets); const occupiedSpaces = useSelector(getOccupiedSpacesWhileMoving, equal) || {}; const isNewWidget = !!newWidget && !dragParent; - const childrenOccupiedSpaces: OccupiedSpace[] = + const childrenOccupiedSpaces: WidgetSpace[] = (dragParent && occupiedSpaces[dragParent]) || []; const isDragging = useSelector( (state: AppState) => state.ui.widgetDragResize.isDragging, @@ -128,51 +106,11 @@ export const useBlocksToBeDraggedOnCanvas = ({ const allWidgets = useSelector(getWidgets); - //This method is called whenever a there is a canvas change. - //canvas is the Layer inside the widgets or on main container where widgets are positioned or dragged. - //This method records the container jump metrics when a widget moves into a container from main Canvas, - // if the widget moves back to the main Canvas then, it is marked as accidental container jump. - const logContainerJump = ( - dropTargetWidgetId: string, - dragSpeed?: number, - dragAcceleration?: number, - ) => { - //If triggered on the same canvas that it started dragging on return - if (!dragDetails.draggedOn || dropTargetWidgetId === dragDetails.draggedOn) - return; - - const { - acceleration, - movingInto, - speed, - } = containerJumpMetrics.getMetrics(); - - // record Only - // if it was not previously recorded - // if not moving into mainContainer - // dragSpeed and dragAcceleration is not undefined - if ( - !movingInto && - dropTargetWidgetId !== MAIN_CONTAINER_WIDGET_ID && - dragSpeed && - dragAcceleration - ) { - containerJumpMetrics.setMetrics({ - speed: dragSpeed, - acceleration: dragAcceleration, - movingInto: dropTargetWidgetId, - }); - } // record only for mainContainer jumps, - //If it is coming back to main canvas after moving into a container then it is a accidental container jump - else if (movingInto && dropTargetWidgetId === MAIN_CONTAINER_WIDGET_ID) { - AnalyticsUtil.logEvent("CONTAINER_JUMP", { - speed: speed, - acceleration: acceleration, - isAccidental: true, - }); - containerJumpMetrics.clearMetrics(); - } - }; + // modify the positions to have grab position on the right side for new widgets + if (isNewWidget) { + defaultHandlePositions.left = + newWidget.columns * snapColumnSpace - defaultHandlePositions.left; + } const getDragCenterSpace = () => { if (dragCenter && dragCenter.widgetId) { // Dragging by widget @@ -227,6 +165,9 @@ export const useBlocksToBeDraggedOnCanvas = ({ widgetId: newWidget.widgetId, detachFromLayout: newWidget.detachFromLayout, isNotColliding: true, + fixedHeight: newWidget.isDynamicHeight + ? newWidget.rows * snapRowSpace + : undefined, }, ], draggingSpaces: [ @@ -254,6 +195,7 @@ export const useBlocksToBeDraggedOnCanvas = ({ rowHeight: each.bottom - each.top, widgetId: each.id, isNotColliding: true, + fixedHeight: each.fixedHeight, })), }; } @@ -272,7 +214,6 @@ export const useBlocksToBeDraggedOnCanvas = ({ drawingBlocks: WidgetDraggingBlock[], reflowedPositionsUpdatesWidgets: OccupiedSpace[], ) => { - logContainerJumpOnDrop(); const reflowedBlocks: WidgetDraggingBlock[] = reflowedPositionsUpdatesWidgets.map( (each) => { const widget = allWidgets[each.id]; @@ -303,7 +244,11 @@ export const useBlocksToBeDraggedOnCanvas = ({ .map((each) => { const widget = newWidget && !reflowedIds.includes(each.widgetId) - ? newWidget + ? { + ...newWidget, + columns: each.columnWidth, + rows: each.rowHeight, + } : allWidgets[each.widgetId]; const updateWidgetParams = widgetOperationParams( widget, @@ -482,7 +427,6 @@ export const useBlocksToBeDraggedOnCanvas = ({ isNewWidgetInitialTargetCanvas, isResizing, lastDraggedCanvas, - logContainerJump, occSpaces, draggingSpaces, onDrop, diff --git a/app/client/src/pages/common/CanvasArenas/hooks/useCanvasDragging.ts b/app/client/src/pages/common/CanvasArenas/hooks/useCanvasDragging.ts index 849f1db654..f372afacef 100644 --- a/app/client/src/pages/common/CanvasArenas/hooks/useCanvasDragging.ts +++ b/app/client/src/pages/common/CanvasArenas/hooks/useCanvasDragging.ts @@ -1,48 +1,36 @@ import { OccupiedSpace } from "constants/CanvasEditorConstants"; -import { - CONTAINER_GRID_PADDING, - GridDefaults, -} from "constants/WidgetConstants"; +import { GridDefaults } from "constants/WidgetConstants"; import { debounce, isEmpty, throttle } from "lodash"; import { CanvasDraggingArenaProps } from "pages/common/CanvasArenas/CanvasDraggingArena"; import { useEffect, useRef } from "react"; -import { useSelector } from "react-redux"; import { MovementLimitMap, ReflowDirection, ReflowedSpaceMap, + SpaceMap, } from "reflow/reflowTypes"; -import { getZoomLevel } from "selectors/editorSelectors"; import { getNearestParentCanvas } from "utils/generators"; -import { getAbsolutePixels } from "utils/helpers"; import { useWidgetDragResize } from "utils/hooks/dragResizeHooks"; import { ReflowInterface, useReflow } from "utils/hooks/useReflow"; import { getDraggingSpacesFromBlocks, - getDropZoneOffsets, getMousePositionsOnCanvas, noCollision, } from "utils/WidgetPropsUtils"; +import { + getEdgeDirection, + getMoveDirection, + getReflowedSpaces, + modifyBlockDimension, + modifyDrawingRectangles, + updateRectanglesPostReflow, +} from "./canvasDraggingUtils"; import { useBlocksToBeDraggedOnCanvas, WidgetDraggingBlock, } from "./useBlocksToBeDraggedOnCanvas"; import { useCanvasDragToScroll } from "./useCanvasDragToScroll"; -import ContainerJumpMetrics from "./ContainerJumpMetric"; - -export interface XYCord { - x: number; - y: number; -} - -const CONTAINER_JUMP_ACC_THRESHOLD = 8000; -const CONTAINER_JUMP_SPEED_THRESHOLD = 800; - -//Since useCanvasDragging's Instance changes during container jump, metrics is stored outside -const containerJumpThresholdMetrics = new ContainerJumpMetrics<{ - speed?: number; - acceleration?: number; -}>(); +import { useRenderBlocksOnCanvas } from "./useRenderBlocksOnCanvas"; export const useCanvasDragging = ( slidingArenaRef: React.RefObject, @@ -57,7 +45,6 @@ export const useCanvasDragging = ( widgetId, }: CanvasDraggingArenaProps, ) => { - const canvasZoomLevel = useSelector(getZoomLevel); const currentDirection = useRef(ReflowDirection.UNSET); const { devicePixelRatio: scale = 1 } = window; const { @@ -72,7 +59,6 @@ export const useCanvasDragging = ( isNewWidgetInitialTargetCanvas, isResizing, lastDraggedCanvas, - logContainerJump, occSpaces, onDrop, parentDiff, @@ -96,7 +82,10 @@ export const useCanvasDragging = ( paddingOffset: 0, }; - const reflow = useRef(); + const reflow = useRef<{ + reflowSpaces: ReflowInterface; + resetReflow: () => void; + }>(); reflow.current = useReflow(draggingSpaces, widgetId || "", gridProps); const { @@ -105,32 +94,6 @@ export const useCanvasDragging = ( setDraggingState, } = useWidgetDragResize(); - const mouseAttributesRef = useRef<{ - prevEvent: any; - currentEvent: any; - prevSpeed: number; - prevAcceleration: number; - maxPositiveAcc: number; - maxNegativeAcc: number; - maxSpeed: number; - lastMousePositionOutsideCanvas: { - x: number; - y: number; - }; - }>({ - prevSpeed: 0, - prevAcceleration: 0, - maxPositiveAcc: 0, - maxNegativeAcc: 0, - maxSpeed: 0, - prevEvent: null, - currentEvent: null, - lastMousePositionOutsideCanvas: { - x: 0, - y: 0, - }, - }); - const canScroll = useCanvasDragToScroll( slidingArenaRef, isCurrentDraggedCanvas, @@ -139,55 +102,15 @@ export const useCanvasDragging = ( canExtend, ); - useEffect(() => { - const speedCalculationInterval = setInterval(function() { - const { - currentEvent, - maxNegativeAcc, - maxPositiveAcc, - maxSpeed, - prevEvent, - prevSpeed, - } = mouseAttributesRef.current; - if (prevEvent && currentEvent) { - const movementX = Math.abs(currentEvent.screenX - prevEvent.screenX); - const movementY = Math.abs(currentEvent.screenY - prevEvent.screenY); - const movement = Math.sqrt( - movementX * movementX + movementY * movementY, - ); - - const speed = 10 * movement; //current speed - - const acceleration = 10 * (speed - prevSpeed); - mouseAttributesRef.current.prevAcceleration = acceleration; - mouseAttributesRef.current.prevSpeed = speed; - if (speed > maxSpeed) { - mouseAttributesRef.current.maxSpeed = speed; - } - if (acceleration > 0 && acceleration > maxPositiveAcc) { - mouseAttributesRef.current.maxPositiveAcc = acceleration; - } else if (acceleration < 0 && acceleration < maxNegativeAcc) { - mouseAttributesRef.current.maxNegativeAcc = acceleration; - } - } - mouseAttributesRef.current.prevEvent = currentEvent; - }, 100); - const stopSpeedCalculation = () => { - clearInterval(speedCalculationInterval); - }; - const registerMouseMoveEvent = (e: any) => { - mouseAttributesRef.current.currentEvent = e; - mouseAttributesRef.current.lastMousePositionOutsideCanvas = { - x: e.clientX, - y: e.clientY, - }; - }; - window.addEventListener("mousemove", registerMouseMoveEvent); - return () => { - stopSpeedCalculation(); - window.removeEventListener("mousemove", registerMouseMoveEvent); - }; - }, []); + const renderBlocks = useRenderBlocksOnCanvas( + slidingArenaRef, + stickyCanvasRef, + !!noPad, + snapColumnSpace, + snapRowSpace, + getSnappedXY, + isCurrentDraggedCanvas, + ); useEffect(() => { if ( @@ -210,24 +133,24 @@ export const useCanvasDragging = ( movementLimitMap?: MovementLimitMap; bottomMostRow: number; movementMap: ReflowedSpaceMap; - isIdealToJumpContainer: boolean; + spacePositionMap: SpaceMap | undefined; } = { movementLimitMap: {}, bottomMostRow: 0, movementMap: {}, - isIdealToJumpContainer: false, + spacePositionMap: {}, }; - let lastMousePosition = { - x: 0, - y: 0, - }; - let lastSnappedPosition = { - leftColumn: 0, - topRow: 0, + let lastSnappedPosition: OccupiedSpace = { + left: 0, + right: 0, + top: 0, + bottom: 0, + id: "", }; const resetCanvasState = () => { throttledStopReflowing(); + reflow.current?.resetReflow(); if (stickyCanvasRef.current && slidingArenaRef.current) { const canvasCtx: any = stickyCanvasRef.current.getContext("2d"); canvasCtx.clearRect( @@ -240,6 +163,7 @@ export const useCanvasDragging = ( canvasIsDragging = false; } }; + if (isDragging) { const startPoints = defaultHandlePositions; const onMouseUp = () => { @@ -247,40 +171,24 @@ export const useCanvasDragging = ( const { movementMap: reflowingWidgets } = currentReflowParams; const reflowedPositionsUpdatesWidgets: OccupiedSpace[] = occSpaces .filter((each) => !!reflowingWidgets[each.id]) - .map((each) => { - const reflowedWidget = reflowingWidgets[each.id]; - if ( - reflowedWidget.X !== undefined && - (Math.abs(reflowedWidget.X) || reflowedWidget.width) - ) { - const movement = reflowedWidget.X / snapColumnSpace; - const newWidth = reflowedWidget.width - ? reflowedWidget.width / snapColumnSpace - : each.right - each.left; - each = { - ...each, - left: each.left + movement, - right: each.left + movement + newWidth, - }; - } - if ( - reflowedWidget.Y !== undefined && - (Math.abs(reflowedWidget.Y) || reflowedWidget.height) - ) { - const movement = reflowedWidget.Y / snapRowSpace; - const newHeight = reflowedWidget.height - ? reflowedWidget.height / snapRowSpace - : each.bottom - each.top; - each = { - ...each, - top: each.top + movement, - bottom: each.top + movement + newHeight, - }; - } - return each; - }); + .map((each) => + getReflowedSpaces( + each, + reflowingWidgets, + snapColumnSpace, + snapRowSpace, + ), + ); - onDrop(currentRectanglesToDraw, reflowedPositionsUpdatesWidgets); + onDrop( + modifyDrawingRectangles( + currentRectanglesToDraw, + currentReflowParams.spacePositionMap, + snapColumnSpace, + snapRowSpace, + ), + reflowedPositionsUpdatesWidgets, + ); } startPoints.top = defaultHandlePositions.top; startPoints.left = defaultHandlePositions.left; @@ -312,181 +220,99 @@ export const useCanvasDragging = ( relativeStartPoints.top || defaultHandlePositions.top; } if (!isCurrentDraggedCanvas) { - //Called when canvas Changes - const { - acceleration, - speed, - } = containerJumpThresholdMetrics.getMetrics(); - logContainerJump(widgetId, speed, acceleration); - containerJumpThresholdMetrics.clearMetrics(); // we can just use canvasIsDragging but this is needed to render the relative DragLayerComponent setDraggingCanvas(widgetId); } canvasIsDragging = true; slidingArenaRef.current.style.zIndex = "2"; - if (over) { - lastMousePosition = { - ...mouseAttributesRef.current.lastMousePositionOutsideCanvas, - }; - } else { - lastMousePosition = { - x: e.clientX, - y: e.clientY, - }; - } - onMouseMove(e, over); } }; - const canReflowForCurrentMouseMove = () => { - const { prevAcceleration, prevSpeed } = mouseAttributesRef.current; - const acceleration = Math.abs(prevAcceleration); - return ( - acceleration < CONTAINER_JUMP_ACC_THRESHOLD || - prevSpeed < CONTAINER_JUMP_SPEED_THRESHOLD - ); - }; - const getMouseMoveDirection = (event: any) => { - if (lastMousePosition) { - const deltaX = lastMousePosition.x - event.clientX, - deltaY = lastMousePosition.y - event.clientY; - lastMousePosition = { - x: event.clientX, - y: event.clientY, - }; - if ( - deltaX === 0 && - ["TOP", "BOTTOM"].includes(currentDirection.current) - ) { - return currentDirection.current; - } - if (Math.abs(deltaY) > Math.abs(deltaX) && deltaY > 0) { - return ReflowDirection.TOP; - } else if (Math.abs(deltaY) > Math.abs(deltaX) && deltaY < 0) { - return ReflowDirection.BOTTOM; - } - if ( - deltaY === 0 && - ["LEFT", "RIGHT"].includes(currentDirection.current) - ) { - return currentDirection.current; - } - if (Math.abs(deltaX) > Math.abs(deltaY) && deltaX > 0) { - return ReflowDirection.LEFT; - } else if (Math.abs(deltaX) > Math.abs(deltaY) && deltaX < 0) { - return ReflowDirection.RIGHT; - } - } - return currentDirection.current; - }; const triggerReflow = (e: any, firstMove: boolean) => { - const canReflowBasedOnMouseSpeed = canReflowForCurrentMouseMove(); - const isReflowing = !isEmpty(currentReflowParams.movementMap); const canReflow = !currentRectanglesToDraw[0].detachFromLayout && !dropDisabled; - const currentBlock = currentRectanglesToDraw[0]; - const [leftColumn, topRow] = getDropZoneOffsets( + const isReflowing = + !isEmpty(currentReflowParams.movementMap) || + (!isEmpty(currentReflowParams.movementLimitMap) && + currentRectanglesToDraw.length === 1); + //The position array of dragging Widgets. + const resizedPositions = getDraggingSpacesFromBlocks( + currentRectanglesToDraw, snapColumnSpace, snapRowSpace, - { - x: currentBlock.left, - y: currentBlock.top, - }, - { - x: 0, - y: 0, - }, ); + const currentBlock = resizedPositions[0]; const mousePosition = getMousePositionsOnCanvas(e, gridProps); const needsReflow = !( - lastSnappedPosition.leftColumn === leftColumn && - lastSnappedPosition.topRow === topRow + lastSnappedPosition.left === currentBlock.left && + lastSnappedPosition.top === currentBlock.top && + lastSnappedPosition.bottom === currentBlock.bottom && + lastSnappedPosition.right === currentBlock.right ); - lastSnappedPosition = { - leftColumn, - topRow, - }; if (canReflow && reflow.current) { if (needsReflow) { - //The position array of dragging Widgets. - const resizedPositions = getDraggingSpacesFromBlocks( - currentRectanglesToDraw, - snapColumnSpace, - snapRowSpace, + currentDirection.current = getMoveDirection( + lastSnappedPosition, + currentBlock, + currentDirection.current, ); - currentDirection.current = getMouseMoveDirection(e); - const immediateExitContainer = lastDraggedCanvas.current; + if (firstMove) { + currentDirection.current = getEdgeDirection( + e.offsetX, + e.offsetY, + slidingArenaRef.current?.clientWidth, + currentDirection.current, + ); + } + lastSnappedPosition = { ...currentBlock }; + let immediateExitContainer; if (lastDraggedCanvas.current) { + immediateExitContainer = lastDraggedCanvas.current; lastDraggedCanvas.current = undefined; } - currentReflowParams = reflow.current( + currentReflowParams = reflow.current?.reflowSpaces( resizedPositions, currentDirection.current, false, - !canReflowBasedOnMouseSpeed, + true, firstMove, immediateExitContainer, mousePosition, + reflowAfterTimeoutCallback, ); } if (isReflowing) { - const { - isIdealToJumpContainer, - movementLimitMap, - } = currentReflowParams; - - if (isIdealToJumpContainer) { - const { - prevAcceleration, - prevSpeed: speed, - } = mouseAttributesRef.current; - const acceleration = Math.abs(prevAcceleration); - containerJumpThresholdMetrics.setMetrics({ - speed, - acceleration, - }); - } - - for (const block of currentRectanglesToDraw) { - const isWithinParentBoundaries = noCollision( - { x: block.left, y: block.top }, - snapColumnSpace, - snapRowSpace, - { x: 0, y: 0 }, - block.columnWidth, - block.rowHeight, - block.widgetId, - [], - rowRef.current, - GridDefaults.DEFAULT_GRID_COLUMNS, - block.detachFromLayout, - ); - - let isNotReachedLimit = true; - const currentBlockLimit = - movementLimitMap && movementLimitMap[block.widgetId]; - if (currentBlockLimit) { - isNotReachedLimit = - currentBlockLimit.canHorizontalMove && - currentBlockLimit.canVerticalMove; - } - block.isNotColliding = - isWithinParentBoundaries && isNotReachedLimit; - } - const widgetIdsToExclude = currentRectanglesToDraw.map( - (a) => a.widgetId, - ); - const newRows = updateBottomRow( - currentReflowParams.bottomMostRow, - rowRef.current, - widgetIdsToExclude, - ); - rowRef.current = newRows ? newRows : rowRef.current; + updateParamsPostReflow(); } } }; + + //update blocks after reflow + const updateParamsPostReflow = () => { + const { movementLimitMap } = currentReflowParams; + + // update isColliding of each block based on movementLimitMap + currentRectanglesToDraw = updateRectanglesPostReflow( + movementLimitMap, + currentRectanglesToDraw, + snapColumnSpace, + snapRowSpace, + rowRef.current, + ); + + const widgetIdsToExclude = currentRectanglesToDraw.map( + (a) => a.widgetId, + ); + const newRows = updateBottomRow( + currentReflowParams.bottomMostRow, + rowRef.current, + widgetIdsToExclude, + ); + rowRef.current = newRows ? newRows : rowRef.current; + }; + const onMouseMove = (e: any, firstMove = false) => { if (isDragging && canvasIsDragging && slidingArenaRef.current) { const delta = { @@ -494,11 +320,19 @@ export const useCanvasDragging = ( top: e.offsetY - startPoints.top - parentDiff.top, }; - const drawingBlocks = blocksToDraw.map((each) => ({ - ...each, - left: each.left + delta.left, - top: each.top + delta.top, - })); + const drawingBlocks = blocksToDraw.map((each) => + modifyBlockDimension( + { + ...each, + left: each.left + delta.left, + top: each.top + delta.top, + }, + snapColumnSpace, + snapRowSpace, + rowRef.current - 1, + canExtend, + ), + ); const newRows = updateRelativeRows(drawingBlocks, rowRef.current); const rowDelta = newRows ? newRows - rowRef.current : 0; rowRef.current = newRows ? newRows : rowRef.current; @@ -526,8 +360,14 @@ export const useCanvasDragging = ( renderNewRows(delta); } else if (!isUpdatingRows) { triggerReflow(e, firstMove); - renderBlocks(); } + isUpdatingRows = renderBlocks( + currentRectanglesToDraw, + currentReflowParams.spacePositionMap, + isUpdatingRows, + canvasIsDragging, + scrollParent, + ); scrollObj.lastMouseMoveEvent = { offsetX: e.offsetX, offsetY: e.offsetY, @@ -544,24 +384,33 @@ export const useCanvasDragging = ( const canvasCtx: any = stickyCanvasRef.current.getContext("2d"); currentRectanglesToDraw = blocksToDraw.map((each) => { + const block = modifyBlockDimension( + { + ...each, + left: each.left + delta.left, + top: each.top + delta.top, + }, + snapColumnSpace, + snapRowSpace, + rowRef.current - 1, + canExtend, + ); return { - ...each, - left: each.left + delta.left, - top: each.top + delta.top, + ...block, isNotColliding: !dropDisabled && noCollision( - { x: each.left + delta.left, y: each.top + delta.top }, + { x: block.left, y: block.top }, snapColumnSpace, snapRowSpace, { x: 0, y: 0 }, - each.columnWidth, - each.rowHeight, - each.widgetId, + block.columnWidth, + block.rowHeight, + block.widgetId, occSpaces, rowRef.current, GridDefaults.DEFAULT_GRID_COLUMNS, - each.detachFromLayout, + block.detachFromLayout, ), }; }); @@ -574,7 +423,13 @@ export const useCanvasDragging = ( stickyCanvasRef.current.height, ); canvasCtx.restore(); - renderBlocks(); + isUpdatingRows = renderBlocks( + currentRectanglesToDraw, + currentReflowParams.spacePositionMap, + isUpdatingRows, + canvasIsDragging, + scrollParent, + ); canScroll.current = false; endRenderRows.cancel(); endRenderRows(); @@ -592,93 +447,21 @@ export const useCanvasDragging = ( }, ); - const renderBlocks = () => { - if ( - slidingArenaRef.current && - isCurrentDraggedCanvas && - canvasIsDragging && - stickyCanvasRef.current - ) { - const canvasCtx: any = stickyCanvasRef.current.getContext("2d"); - canvasCtx.save(); - canvasCtx.clearRect( - 0, - 0, - stickyCanvasRef.current.width, - stickyCanvasRef.current.height, - ); - isUpdatingRows = false; - canvasCtx.transform(canvasZoomLevel, 0, 0, canvasZoomLevel, 0, 0); - if (canvasIsDragging) { - currentRectanglesToDraw.forEach((each) => { - drawBlockOnCanvas(each); - }); - } - canvasCtx.restore(); - } + const reflowAfterTimeoutCallback = (reflowParams: { + movementMap: ReflowedSpaceMap; + spacePositionMap: SpaceMap | undefined; + }) => { + currentReflowParams = { ...currentReflowParams, ...reflowParams }; + updateParamsPostReflow(); + isUpdatingRows = renderBlocks( + currentRectanglesToDraw, + currentReflowParams.spacePositionMap, + isUpdatingRows, + canvasIsDragging, + scrollParent, + ); }; - const drawBlockOnCanvas = (blockDimensions: WidgetDraggingBlock) => { - if ( - stickyCanvasRef.current && - slidingArenaRef.current && - scrollParent && - isCurrentDraggedCanvas && - canvasIsDragging - ) { - const canvasCtx: any = stickyCanvasRef.current.getContext("2d"); - const topOffset = getAbsolutePixels( - stickyCanvasRef.current.style.top, - ); - const leftOffset = getAbsolutePixels( - stickyCanvasRef.current.style.left, - ); - const snappedXY = getSnappedXY( - snapColumnSpace, - snapRowSpace, - { - x: blockDimensions.left, - y: blockDimensions.top, - }, - { - x: 0, - y: 0, - }, - ); - - canvasCtx.fillStyle = `${ - blockDimensions.isNotColliding ? "rgb(104, 113, 239, 0.6)" : "red" - }`; - canvasCtx.fillRect( - blockDimensions.left - - leftOffset + - (noPad ? 0 : CONTAINER_GRID_PADDING), - blockDimensions.top - - topOffset + - (noPad ? 0 : CONTAINER_GRID_PADDING), - blockDimensions.width, - blockDimensions.height, - ); - canvasCtx.fillStyle = `${ - blockDimensions.isNotColliding ? "rgb(233, 250, 243, 0.6)" : "red" - }`; - const strokeWidth = 1; - canvasCtx.setLineDash([3]); - canvasCtx.strokeStyle = "rgb(104, 113, 239)"; - canvasCtx.strokeRect( - snappedXY.X - - leftOffset + - strokeWidth + - (noPad ? 0 : CONTAINER_GRID_PADDING), - snappedXY.Y - - topOffset + - strokeWidth + - (noPad ? 0 : CONTAINER_GRID_PADDING), - blockDimensions.width - strokeWidth, - blockDimensions.height - strokeWidth, - ); - } - }; // Adding setTimeout to make sure this gets called after // the onscroll that resets intersectionObserver in StickyCanvasArena.tsx const onScroll = () => @@ -705,12 +488,11 @@ export const useCanvasDragging = ( }); } }, 0); - const captureMousePosition = (e: any) => { - if (isDragging && !canvasIsDragging) { - currentDirection.current = getMouseMoveDirection(e); - } + const onMouseOver = (e: any) => { + onFirstMoveOnCanvas(e, true); }; - const onMouseOver = (e: any) => onFirstMoveOnCanvas(e, true); + + //Initialize Listeners const initializeListeners = () => { slidingArenaRef.current?.addEventListener( "mousemove", @@ -741,7 +523,6 @@ export const useCanvasDragging = ( ); document.body.addEventListener("mouseup", onMouseUp, false); window.addEventListener("mouseup", onMouseUp, false); - window.addEventListener("mousemove", captureMousePosition); }; const startDragging = () => { if ( @@ -781,7 +562,6 @@ export const useCanvasDragging = ( ); document.body.removeEventListener("mouseup", onMouseUp); window.removeEventListener("mouseup", onMouseUp); - window.removeEventListener("mousemove", captureMousePosition); }; } else { resetCanvasState(); diff --git a/app/client/src/pages/common/CanvasArenas/hooks/useRenderBlocksOnCanvas.ts b/app/client/src/pages/common/CanvasArenas/hooks/useRenderBlocksOnCanvas.ts new file mode 100644 index 0000000000..1e3cc861bf --- /dev/null +++ b/app/client/src/pages/common/CanvasArenas/hooks/useRenderBlocksOnCanvas.ts @@ -0,0 +1,159 @@ +import { CONTAINER_GRID_PADDING } from "constants/WidgetConstants"; +import { useSelector } from "react-redux"; +import { SpaceMap } from "reflow/reflowTypes"; +import { getZoomLevel } from "selectors/editorSelectors"; +import { getAbsolutePixels } from "utils/helpers"; +import { modifyDrawingRectangles } from "./canvasDraggingUtils"; +import { WidgetDraggingBlock } from "./useBlocksToBeDraggedOnCanvas"; + +export interface XYCord { + x: number; + y: number; +} + +/** + * returns a method that renders dragging blocks on canvas + * @param slidingArenaRef DOM ref of Sliding Canvas + * @param stickyCanvasRef DOM ref of Sticky Canvas + * @param noPad Boolean to indicate if the container type widget has padding + * @param snapColumnSpace width between columns + * @param snapRowSpace height between rows + * @param getSnappedXY Method that returns XY on the canvas Grid + * @param isCurrentDraggedCanvas boolean if the current canvas is being dragged on + * @returns + */ +export const useRenderBlocksOnCanvas = ( + slidingArenaRef: React.RefObject, + stickyCanvasRef: React.RefObject, + noPad: boolean, + snapColumnSpace: number, + snapRowSpace: number, + getSnappedXY: ( + parentColumnWidth: number, + parentRowHeight: number, + currentOffset: XYCord, + parentOffset: XYCord, + ) => { + X: number; + Y: number; + }, + isCurrentDraggedCanvas: boolean, +) => { + const canvasZoomLevel = useSelector(getZoomLevel); + + /** + * draws the block on canvas + * @param blockDimensions Dimensions of block to be drawn + * @param scrollParent DOM element of parent + */ + const drawBlockOnCanvas = ( + blockDimensions: WidgetDraggingBlock, + scrollParent: Element | null, + ) => { + if ( + stickyCanvasRef.current && + slidingArenaRef.current && + scrollParent && + isCurrentDraggedCanvas + ) { + const canvasCtx: any = stickyCanvasRef.current.getContext("2d"); + const topOffset = getAbsolutePixels(stickyCanvasRef.current.style.top); + const leftOffset = getAbsolutePixels(stickyCanvasRef.current.style.left); + const snappedXY = getSnappedXY( + snapColumnSpace, + snapRowSpace, + { + x: blockDimensions.left, + y: blockDimensions.top, + }, + { + x: 0, + y: 0, + }, + ); + + canvasCtx.fillStyle = `${ + blockDimensions.isNotColliding + ? "rgb(104, 113, 239, 0.6)" + : "rgb(255, 55, 35, 0.6)" + }`; + canvasCtx.fillRect( + blockDimensions.left - + leftOffset + + (noPad ? 0 : CONTAINER_GRID_PADDING), + blockDimensions.top - topOffset + (noPad ? 0 : CONTAINER_GRID_PADDING), + blockDimensions.width, + blockDimensions.height, + ); + const strokeWidth = 1; + canvasCtx.setLineDash([3]); + canvasCtx.strokeStyle = blockDimensions.isNotColliding + ? "rgb(104, 113, 239)" + : "red"; + canvasCtx.strokeRect( + snappedXY.X - + leftOffset + + strokeWidth + + (noPad ? 0 : CONTAINER_GRID_PADDING), + snappedXY.Y - + topOffset + + strokeWidth + + (noPad ? 0 : CONTAINER_GRID_PADDING), + blockDimensions.width - strokeWidth, + blockDimensions.height - strokeWidth, + ); + } + }; + + /** + * renders blocks on Canvas + * @param rectanglesToDraw Rectangles that are to be drawn + * @param spacePositionMap current dimensions of the dragging widgets + * @param isUpdatingRows boolean + * @param canvasIsDragging + * @param scrollParent DOM element of parent + * @returns + */ + const renderBlocks = ( + rectanglesToDraw: WidgetDraggingBlock[], + spacePositionMap: SpaceMap | undefined, + isUpdatingRows: boolean, + canvasIsDragging: boolean, + scrollParent: Element | null, + ) => { + let isCurrUpdatingRows = isUpdatingRows; + const modifiedRectanglesToDraw = modifyDrawingRectangles( + rectanglesToDraw, + spacePositionMap, + snapColumnSpace, + snapRowSpace, + ); + if ( + slidingArenaRef.current && + isCurrentDraggedCanvas && + canvasIsDragging && + stickyCanvasRef.current + ) { + const canvasCtx: any = stickyCanvasRef.current.getContext("2d"); + canvasCtx.save(); + canvasCtx.clearRect( + 0, + 0, + stickyCanvasRef.current.width, + stickyCanvasRef.current.height, + ); + isCurrUpdatingRows = false; + canvasCtx.transform(canvasZoomLevel, 0, 0, canvasZoomLevel, 0, 0); + if (canvasIsDragging) { + modifiedRectanglesToDraw.forEach((each) => { + drawBlockOnCanvas(each, scrollParent); + }); + } + canvasCtx.restore(); + } + + return isCurrUpdatingRows; + }; + + return renderBlocks; +}; diff --git a/app/client/src/reducers/uiReducers/canvasSelectionReducer.ts b/app/client/src/reducers/uiReducers/canvasSelectionReducer.ts index e501ad050b..bd64133818 100644 --- a/app/client/src/reducers/uiReducers/canvasSelectionReducer.ts +++ b/app/client/src/reducers/uiReducers/canvasSelectionReducer.ts @@ -4,7 +4,7 @@ import { ReduxActionTypes, } from "@appsmith/constants/ReduxActionConstants"; import { MAIN_CONTAINER_WIDGET_ID } from "constants/WidgetConstants"; -import { XYCord } from "pages/common/CanvasArenas/hooks/useCanvasDragging"; +import { XYCord } from "pages/common/CanvasArenas/hooks/useRenderBlocksOnCanvas"; const initialState: CanvasSelectionState = { isDraggingForSelection: false, diff --git a/app/client/src/reflow/index.ts b/app/client/src/reflow/index.ts index 93940c0930..03ee360d40 100644 --- a/app/client/src/reflow/index.ts +++ b/app/client/src/reflow/index.ts @@ -1,6 +1,7 @@ import { OccupiedSpace } from "constants/CanvasEditorConstants"; import { getMovementMap } from "./reflowHelpers"; import { + BlockSpace, CollidingSpaceMap, CollisionMap, GridProps, @@ -31,6 +32,7 @@ import { getCalculatedDirection, getOrientationAccessor, initializeMovementLimitMap, + verifyMovementLimits, } from "./reflowUtils"; /** @@ -46,18 +48,24 @@ import { * @param shouldResize boolean to indicate if colliding spaces should resize * @param prevReflowState this contains a map of reference to the key values of previous reflow method call to back trace widget movements * @param exitContainerId sting, Id of recent exit container + * @param mousePosition mouse Position on canvas grid + * @param shouldReflowDropTarget boolean which indicates if we should reflow drop targets + * @param onTimeout indicates if the reflow is called on timeout * @returns movement information of the dragging/resizing space and other colliding spaces */ export function reflow( - newSpacePositions: OccupiedSpace[], + newSpacePositions: BlockSpace[], OGSpacePositions: OccupiedSpace[], - occupiedSpaces: OccupiedSpace[], + occupiedSpaces: BlockSpace[], direction: ReflowDirection, gridProps: GridProps, forceDirection = false, shouldResize = true, prevReflowState: PrevReflowState = {} as PrevReflowState, exitContainerId?: string, + mousePosition?: OccupiedSpace, + shouldReflowDropTarget = true, + onTimeout = false, ) { const newSpacePositionsMap = getSpacesMapFromArray(newSpacePositions); const OGSpacePositionsMap = getSpacesMapFromArray(OGSpacePositions); @@ -70,7 +78,7 @@ export function reflow( ); //initializing variables - const movementLimitMap: MovementLimitMap = initializeMovementLimitMap( + let movementLimitMap: MovementLimitMap = initializeMovementLimitMap( newSpacePositions, ); const globalCollidingSpaces: CollidingSpaceMap = { @@ -112,10 +120,12 @@ export function reflow( //Reflow in the primary orientation const { collidingSpaces: primaryCollidingSpaces, + currSpacePositionMap: primarySpacePositionMap, isColliding: primaryIsColliding, movementMap: primaryMovementMap, movementVariablesMap: primaryMovementVariablesMap, secondOrderCollisionMap: primarySecondOrderCollisionMap, + shouldRegisterContainerTimeout: primaryShouldRegisterContainerTimeout, } = getOrientationalMovementInfo( newSpacePositionsMap, occupiedSpacesMap, @@ -126,6 +136,9 @@ export function reflow( shouldResize, forceDirection, exitContainerId, + shouldReflowDropTarget, + onTimeout, + mousePosition, maxSpaceAttributes.primary, prevReflowState, ); @@ -141,12 +154,14 @@ export function reflow( const { collidingSpaces: secondaryCollidingSpaces, + currSpacePositionMap: secondarySpacePositionMap, isColliding: secondaryIsColliding, movementMap, movementVariablesMap: secondaryMovementVariablesMap, secondOrderCollisionMap, + shouldRegisterContainerTimeout: secondaryShouldRegisterContainerTimeout, } = getOrientationalMovementInfo( - newSpacePositionsMap, + primarySpacePositionMap, occupiedSpacesMap, currentDirection, !isHorizontal, @@ -155,6 +170,9 @@ export function reflow( shouldResize, forceDirection, exitContainerId, + shouldReflowDropTarget, + onTimeout, + mousePosition, maxSpaceAttributes.secondary, prevReflowState, primaryMovementMap || {}, @@ -169,8 +187,23 @@ export function reflow( getShouldReflow(movementLimitMap, secondaryMovementVariablesMap, delta); } + // If we are not reflowing drop targets, verify the limits of dragging widget + if (!shouldReflowDropTarget && newSpacePositions.length === 1) { + movementLimitMap = verifyMovementLimits( + movementLimitMap, + secondarySpacePositionMap, + occupiedSpacesMap, + ); + } + if (!primaryIsColliding && !secondaryIsColliding) { - return { movementLimitMap }; + return { + movementLimitMap, + spacePositionMap: secondarySpacePositionMap, + shouldRegisterContainerTimeout: + primaryShouldRegisterContainerTimeout || + secondaryShouldRegisterContainerTimeout, + }; } return { @@ -179,6 +212,10 @@ export function reflow( collidingSpaceMap: globalCollidingSpaces, secondOrderCollisionMap: secondOrderCollisionMap || primarySecondOrderCollisionMap, + shouldRegisterContainerTimeout: + primaryShouldRegisterContainerTimeout || + secondaryShouldRegisterContainerTimeout, + spacePositionMap: secondarySpacePositionMap, }; } @@ -195,6 +232,9 @@ export function reflow( * @param shouldResize boolean to indicate if colliding spaces should resize * @param forceDirection boolean to force the direction on certain scenarios * @param exitContainerId string, Id of recent exit container + * @param shouldReflowDropTarget boolean which indicates if we should reflow drop targets + * @param onTimeout indicates if the reflow is called on timeout + * @param mousePosition mouse Position on canvas grid * @param maxSpaceAttributes object containing accessors for maximum and minimum dimensions in a particular direction * @param prevReflowState this contains a map of reference to the key values of previous reflow method call to back trace widget movements * @param primaryMovementMap movement map/information from previous run of the algorithm @@ -211,6 +251,9 @@ function getOrientationalMovementInfo( shouldResize: boolean, forceDirection: boolean, exitContainerId: string | undefined, + shouldReflowDropTarget = true, + onTimeout = false, + mousePosition: OccupiedSpace | undefined, maxSpaceAttributes: { max: SpaceAttributes; min: SpaceAttributes }, prevReflowState: PrevReflowState, primaryMovementMap?: ReflowedSpaceMap, @@ -250,7 +293,12 @@ function getOrientationalMovementInfo( (prevCollidingSpaceMap && prevCollidingSpaceMap[orientationAccessor]) || {}; //gets a map of all colliding spaces of the current dragging spaces - const { collidingSpaceMap, isColliding } = getCollidingSpaceMap( + const { + collidingSpaceMap, + currSpacePositions, + isColliding, + shouldRegisterContainerTimeout, + } = getCollidingSpaceMap( newSpacePositions, sortedOccupiedSpaces, direction, @@ -259,18 +307,32 @@ function getOrientationalMovementInfo( prevSpacesMap, forceDirection, primaryCollisionMap, + shouldReflowDropTarget, + onTimeout, + mousePosition, ); + const currSpacePositionMap = getSpacesMapFromArray(currSpacePositions); const collidingSpaces = getSortedCollidingSpaces( collidingSpaceMap, isHorizontal, prevCollisionMap, ); - if (!collidingSpaces.length) return {}; + if (!collidingSpaces.length) + return { currSpacePositionMap, shouldRegisterContainerTimeout }; - if (!primaryMovementMap) { - changeExitContainerDirection(collidingSpaceMap, exitContainerId, direction); + if ( + !primaryMovementMap && + shouldReflowDropTarget && + Object.keys(currSpacePositionMap).length === 1 + ) { + changeExitContainerDirection( + collidingSpaceMap, + exitContainerId, + mousePosition, + currSpacePositionMap, + ); } //if it is the first orientation, we use the original positions of the occupiedSpaces @@ -286,8 +348,8 @@ function getOrientationalMovementInfo( movementVariablesMap, secondOrderCollisionMap, } = getMovementMap( - newSpacePositions, - newSpacePositionsMap, + currSpacePositions, + currSpacePositionMap, currentOccupiedSpaces, currentOccupiedSpacesMap, occupiedSpacesMap, @@ -309,5 +371,7 @@ function getOrientationalMovementInfo( secondOrderCollisionMap, isColliding, collidingSpaces, + currSpacePositionMap, + shouldRegisterContainerTimeout, }; } diff --git a/app/client/src/reflow/reflowHelpers.ts b/app/client/src/reflow/reflowHelpers.ts index 07a27d2987..e1d03c7dd5 100644 --- a/app/client/src/reflow/reflowHelpers.ts +++ b/app/client/src/reflow/reflowHelpers.ts @@ -11,14 +11,14 @@ import { DirectionalMovement, DirectionalVariables, GridProps, - HORIZONTAL_RESIZE_LIMIT, + HORIZONTAL_RESIZE_MIN_LIMIT, PrevReflowState, ReflowDirection, ReflowedSpaceMap, SecondOrderCollisionMap, SpaceMap, SpaceMovementMap, - VERTICAL_RESIZE_LIMIT, + VERTICAL_RESIZE_MIN_LIMIT, } from "./reflowTypes"; import { checkReCollisionWithOtherNewSpacePositions, @@ -498,10 +498,10 @@ function getCollisionTreeHelper( occupiedLength: occupiedLength + (accessors.isHorizontal - ? HORIZONTAL_RESIZE_LIMIT + ? HORIZONTAL_RESIZE_MIN_LIMIT : collidingSpace.fixedHeight && accessors.directionIndicator < 0 ? collidingSpace.fixedHeight - : VERTICAL_RESIZE_LIMIT), + : VERTICAL_RESIZE_MIN_LIMIT), }; } @@ -737,7 +737,7 @@ function getMovementMapHelper( collisionTree[accessors.parallelMin], occupiedLength: (movementMap[collisionTree.id].horizontalOccupiedLength || 0) + - HORIZONTAL_RESIZE_LIMIT, + HORIZONTAL_RESIZE_MIN_LIMIT, currentEmptySpaces: (movementMap[collisionTree.id].horizontalEmptySpaces as number) || 0, @@ -751,7 +751,7 @@ function getMovementMapHelper( (movementMap[collisionTree.id].verticalOccupiedLength || 0) + (collisionTree.fixedHeight && accessors.directionIndicator < 0 ? collisionTree.fixedHeight - : VERTICAL_RESIZE_LIMIT), + : VERTICAL_RESIZE_MIN_LIMIT), currentEmptySpaces: (movementMap[collisionTree.id].verticalEmptySpaces as number) || 0, }; @@ -770,10 +770,10 @@ function getMovementMapHelper( occupiedLength: occupiedLength + (accessors.isHorizontal - ? HORIZONTAL_RESIZE_LIMIT + ? HORIZONTAL_RESIZE_MIN_LIMIT : collisionTree.fixedHeight && accessors.directionIndicator < 0 ? collisionTree.fixedHeight - : VERTICAL_RESIZE_LIMIT), + : VERTICAL_RESIZE_MIN_LIMIT), currentEmptySpaces, }; } @@ -822,7 +822,7 @@ export function getHorizontalSpaceMovement( distanceBeforeCollision, gridProps.parentColumnSpace, emptySpaces, - HORIZONTAL_RESIZE_LIMIT, + HORIZONTAL_RESIZE_MIN_LIMIT, shouldResize, ); const spaceMovement = { @@ -891,7 +891,7 @@ export function getVerticalSpaceMovement( distanceBeforeCollision, gridProps.parentRowSpace, emptySpaces, - VERTICAL_RESIZE_LIMIT, + VERTICAL_RESIZE_MIN_LIMIT, shouldResize, ); const spaceMovement = { diff --git a/app/client/src/reflow/reflowTypes.ts b/app/client/src/reflow/reflowTypes.ts index 48d86c7cb8..744b51eb5f 100644 --- a/app/client/src/reflow/reflowTypes.ts +++ b/app/client/src/reflow/reflowTypes.ts @@ -1,7 +1,7 @@ import { OccupiedSpace } from "constants/CanvasEditorConstants"; -export const HORIZONTAL_RESIZE_LIMIT = 2; -export const VERTICAL_RESIZE_LIMIT = 4; +export const HORIZONTAL_RESIZE_MIN_LIMIT = 2; +export const VERTICAL_RESIZE_MIN_LIMIT = 4; export enum ReflowDirection { LEFT = "LEFT", @@ -42,6 +42,7 @@ export type CollisionAccessors = { parallelMax: SpaceAttributes; parallelMin: SpaceAttributes; mathComparator: MathComparators; + oppositeMathComparator: MathComparators; directionIndicator: 1 | -1; isHorizontal: boolean; plane: "vertical" | "horizontal"; @@ -52,7 +53,7 @@ export type Delta = { Y: number; }; -export type CollidingSpace = OccupiedSpace & { +export type CollidingSpace = BlockSpace & { direction: ReflowDirection; collidingValue: number; collidingId: string; @@ -61,9 +62,9 @@ export type CollidingSpace = OccupiedSpace & { fixedHeight?: number; }; -export type SecondOrderCollision = OccupiedSpace & { +export type SecondOrderCollision = BlockSpace & { children: { - [key: string]: OccupiedSpace & { + [key: string]: BlockSpace & { direction: ReflowDirection; isHorizontal: boolean; processed?: boolean; @@ -87,7 +88,7 @@ export type CollisionMap = { [key: string]: CollidingSpace; }; -export type CollisionTree = OccupiedSpace & { +export type CollisionTree = BlockSpace & { direction: ReflowDirection; children?: { [key: string]: CollisionTree; @@ -154,7 +155,12 @@ export type PrevReflowState = { prevSecondOrderCollisionMap: SecondOrderCollisionMap; }; -export type SpaceMap = { [id: string]: OccupiedSpace }; +export type BlockSpace = OccupiedSpace & { + isDropTarget?: boolean; + fixedHeight?: number; +}; + +export type SpaceMap = { [id: string]: BlockSpace }; export type DirectionalVariables = { [key: string]: { diff --git a/app/client/src/reflow/reflowUtils.ts b/app/client/src/reflow/reflowUtils.ts index 6a8d8f57fd..addad6489e 100644 --- a/app/client/src/reflow/reflowUtils.ts +++ b/app/client/src/reflow/reflowUtils.ts @@ -1,7 +1,8 @@ import { OccupiedSpace } from "constants/CanvasEditorConstants"; import { cloneDeep, isUndefined } from "lodash"; -import { Rect } from "utils/boxHelpers"; +import { areIntersecting, Rect } from "utils/boxHelpers"; import { + BlockSpace, CollidingSpace, CollidingSpaceMap, CollisionAccessors, @@ -9,6 +10,7 @@ import { CollisionTree, CollisionTreeCache, GridProps, + HORIZONTAL_RESIZE_MIN_LIMIT, MathComparators, MovementLimitMap, OrientationAccessors, @@ -20,6 +22,7 @@ import { SpaceAttributes, SpaceMap, SpaceMovementMap, + VERTICAL_RESIZE_MIN_LIMIT, } from "./reflowTypes"; /** @@ -276,17 +279,24 @@ export function getDelta( * @param prevSpacesMap * @param forceDirection * @param primaryCollisionMap + * @param shouldReflowDropTarget boolean which indicates if we should reflow drop targets + * @param onTimeout indicates if the reflow is called on timeout + * @param mousePosition mouse Position on canvas grid * @returns collision spaces Map */ + export function getCollidingSpaceMap( - newSpacePositions: OccupiedSpace[], - occupiedSpaces: OccupiedSpace[], + newSpacePositions: BlockSpace[], + occupiedSpaces: BlockSpace[], direction: ReflowDirection, prevCollidingSpaceMap: CollidingSpaceMap, isHorizontalMove?: boolean, prevSpacesMap?: SpaceMap, forceDirection = false, primaryCollisionMap?: CollisionMap, + shouldReflowDropTarget = true, + onTimeOut = false, + mousePosition?: OccupiedSpace | undefined, ) { let isColliding = false; const collidingSpaceMap: CollisionMap = {}; @@ -296,8 +306,34 @@ export function getCollidingSpaceMap( !isHorizontalMove, ); - for (const newSpacePosition of newSpacePositions) { - for (const occupiedSpace of occupiedSpaces) { + let reflowableOccSpaces = [...occupiedSpaces], + currSpacePositions = [...newSpacePositions]; + + let shouldRegisterContainerTimeout = false; + + //if droptargets are not to be reflowed, resize space positions + // and omit drop targets from the spaces + if (!shouldReflowDropTarget) { + // reset values based on function's result + ({ + currSpacePositions, + reflowableOccSpaces, + shouldRegisterContainerTimeout, + } = resizeOnContainerCollision( + newSpacePositions, + occupiedSpaces, + mousePosition, + direction, + orientationalAccessor, + prevCollidingSpaceMap, + forceDirection, + isHorizontalMove, + prevSpacesMap, + )); + } + + for (const newSpacePosition of currSpacePositions) { + for (const occupiedSpace of reflowableOccSpaces) { if (areOverlapping(occupiedSpace, newSpacePosition)) { isColliding = true; const currentSpaceId = occupiedSpace.id; @@ -342,6 +378,14 @@ export function getCollidingSpaceMap( ].direction; } + if (occupiedSpace.isDropTarget && onTimeOut && !forceDirection) { + movementDirection = getCollisionDirectionOfDropTarget( + occupiedSpace, + movementDirection, + mousePosition, + ); + } + const { direction: directionAccessor, directionIndicator, @@ -379,6 +423,8 @@ export function getCollidingSpaceMap( return { isColliding, collidingSpaceMap, + currSpacePositions, + shouldRegisterContainerTimeout, }; } @@ -403,13 +449,13 @@ export function getCollidingSpaceMap( */ export function getCollidingSpacesInDirection( newSpacePosition: CollidingSpace, - OGPosition: OccupiedSpace, + OGPosition: BlockSpace, globalDirection: ReflowDirection, direction: ReflowDirection, gridProps: GridProps, prevReflowState: PrevReflowState, globalCollisionMap: CollisionMap, - occupiedSpaces?: OccupiedSpace[], + occupiedSpaces?: BlockSpace[], isDirectCollidingSpace = false, ) { const collidingSpaces: CollidingSpace[] = []; @@ -513,7 +559,7 @@ export function getCollidingSpacesInDirection( */ export function ShouldAddToCollisionSpacesArray( newSpacePosition: CollidingSpace, - OGPosition: OccupiedSpace, + OGPosition: BlockSpace, collidingSpace: OccupiedSpace, direction: ReflowDirection, accessor: CollisionAccessors, @@ -659,11 +705,11 @@ export function ShouldAddToCollisionSpacesArray( * @returns filtered array of occupied space */ export function filterSpaceByDirection( - newSpacePosition: OccupiedSpace, - occupiedSpaces: OccupiedSpace[] | undefined, + newSpacePosition: BlockSpace, + occupiedSpaces: BlockSpace[] | undefined, direction: ReflowDirection, -): OccupiedSpace[] { - let filteredSpaces: OccupiedSpace[] = []; +): BlockSpace[] { + let filteredSpaces: BlockSpace[] = []; const { direction: directionAccessor, @@ -931,6 +977,7 @@ export function getAccessor(direction: ReflowDirection): CollisionAccessors { parallelMax: SpaceAttributes.right, parallelMin: SpaceAttributes.left, mathComparator: MathComparators.max, + oppositeMathComparator: MathComparators.min, directionIndicator: -1, isHorizontal: true, plane: "horizontal", @@ -944,6 +991,7 @@ export function getAccessor(direction: ReflowDirection): CollisionAccessors { parallelMax: SpaceAttributes.right, parallelMin: SpaceAttributes.left, mathComparator: MathComparators.min, + oppositeMathComparator: MathComparators.max, directionIndicator: 1, isHorizontal: true, plane: "horizontal", @@ -957,6 +1005,7 @@ export function getAccessor(direction: ReflowDirection): CollisionAccessors { parallelMax: SpaceAttributes.bottom, parallelMin: SpaceAttributes.top, mathComparator: MathComparators.max, + oppositeMathComparator: MathComparators.min, directionIndicator: -1, isHorizontal: false, plane: "vertical", @@ -970,6 +1019,7 @@ export function getAccessor(direction: ReflowDirection): CollisionAccessors { parallelMax: SpaceAttributes.bottom, parallelMin: SpaceAttributes.top, mathComparator: MathComparators.min, + oppositeMathComparator: MathComparators.max, directionIndicator: 1, isHorizontal: false, plane: "vertical", @@ -983,6 +1033,7 @@ export function getAccessor(direction: ReflowDirection): CollisionAccessors { parallelMax: SpaceAttributes.bottom, parallelMin: SpaceAttributes.top, mathComparator: MathComparators.min, + oppositeMathComparator: MathComparators.max, directionIndicator: 1, isHorizontal: false, plane: "vertical", @@ -1237,28 +1288,46 @@ function replaceMovementMapByDirection( * * @param collidingSpaceMap * @param exitContainerId + * @param mousePosition mouse Position on canvas grid + * @param spacePositionMap * @param direction * changes reference of collidingSpaceMap */ export function changeExitContainerDirection( collidingSpaceMap: CollisionMap, exitContainerId: string | undefined, - direction: ReflowDirection, + mousePosition: OccupiedSpace | undefined, + spacePositionMap: SpaceMap, ) { - if (!exitContainerId || !collidingSpaceMap[exitContainerId]) { + if ( + !exitContainerId || + !collidingSpaceMap[exitContainerId] || + !mousePosition + ) { return; } - const oppDirection = getOppositeDirection(direction); - const { directionIndicator, oppositeDirection } = getAccessor(oppDirection); + const exitEdgeDirection = getContainerExitEdge( + collidingSpaceMap[exitContainerId], + mousePosition, + ); + + if (!exitEdgeDirection) return; + + const { + direction: directionAccessor, + directionIndicator, + oppositeDirection: exitDirectionAccessor, + } = getAccessor(exitEdgeDirection); const collidingSpaces: CollidingSpace[] = Object.values(collidingSpaceMap); - const oppositeFrom = collidingSpaceMap[exitContainerId][oppositeDirection]; + const oppositeFrom = + collidingSpaceMap[exitContainerId][exitDirectionAccessor]; const oppositeSpaceIds = collidingSpaces .filter((collidingSpace: CollidingSpace) => { return compareNumbers( - collidingSpace[oppositeDirection], + collidingSpace[exitDirectionAccessor], oppositeFrom, directionIndicator > 0, true, @@ -1267,8 +1336,20 @@ export function changeExitContainerDirection( .map((collidingSpace: CollidingSpace) => collidingSpace.id); for (const spaceId of oppositeSpaceIds) { - collidingSpaceMap[spaceId].direction = oppDirection; + collidingSpaceMap[spaceId].direction = exitEdgeDirection; + collidingSpaceMap[spaceId].collidingValue = + spacePositionMap[collidingSpaceMap[spaceId].collidingId][ + directionAccessor + ]; } + + collidingSpaceMap[exitContainerId].direction = getOppositeDirection( + exitEdgeDirection, + ); + collidingSpaceMap[exitContainerId].collidingValue = + spacePositionMap[collidingSpaceMap[exitContainerId].collidingId][ + exitDirectionAccessor + ]; } /** @@ -1277,9 +1358,7 @@ export function changeExitContainerDirection( * @param spacesArray * @returns space map */ -export function getSpacesMapFromArray( - spacesArray: OccupiedSpace[] | undefined, -) { +export function getSpacesMapFromArray(spacesArray: BlockSpace[] | undefined) { if (!spacesArray) return {}; const spacesMap: SpaceMap = {}; for (const space of spacesArray) { @@ -1445,10 +1524,10 @@ export function getModifiedCollidingSpace( */ export function checkReCollisionWithOtherNewSpacePositions( collidingSpace: CollidingSpace, - OGCollidingSpacePosition: OccupiedSpace, + OGCollidingSpacePosition: BlockSpace, globalDirection: ReflowDirection, direction: ReflowDirection, - newSpacePositions: OccupiedSpace[], + newSpacePositions: BlockSpace[], globalCollidingSpaces: CollidingSpace[], insertionIndex: number, globalProcessedNodes: CollisionTreeCache, @@ -1898,3 +1977,292 @@ export function getRelativeCollidingValue( collidingValue, ); } + +/** + * Get the edge from which the widget just exited + * @param exitContainer Id of the container that was just exited + * @param mousePosition position of mouse on Canvas Grid + * @returns + */ +export function getContainerExitEdge( + exitContainer: OccupiedSpace, + mousePosition: OccupiedSpace, +) { + if ( + mousePosition.top > exitContainer.top && + mousePosition.top < exitContainer.bottom + ) { + if (mousePosition.left >= exitContainer.right) return ReflowDirection.RIGHT; + if (mousePosition.left <= exitContainer.left) return ReflowDirection.LEFT; + } + + if ( + mousePosition.left > exitContainer.left && + mousePosition.left < exitContainer.right + ) { + if (mousePosition.top >= exitContainer.bottom) + return ReflowDirection.BOTTOM; + if (mousePosition.top <= exitContainer.top) return ReflowDirection.TOP; + } +} + +/** + * If we are not reflowing the drop targets, + * then we will have to figure out the direction in which it is colliding with dragging Spaces + * @param containerSpace Space positions of Container/Droptargets + * @param currentDirection current Direction + * @param mousePosition Position of mouse on Canvas Grid + * @returns + */ +export function getCollisionDirectionOfDropTarget( + containerSpace: BlockSpace, + currentDirection: ReflowDirection, + mousePosition?: OccupiedSpace, +): ReflowDirection { + const possiblePushDirections = getPossiblePushDirections( + containerSpace, + mousePosition, + ); + + if ( + possiblePushDirections.length < 1 || + possiblePushDirections.includes(currentDirection) + ) { + return currentDirection; + } + + return possiblePushDirections[0]; +} + +/** + * Get the possible directions the Containers/drop targets can be pushed based on mousePosition + * @param containerSpace Space positions of Container/Droptargets + * @param mousePosition Position of mouse on Canvas Grid + * @returns Array of Possible directions, at the max two directions based on mouse positions, + * sorted by distance to container of mouse position + */ +function getPossiblePushDirections( + containerSpace: BlockSpace, + mousePosition?: OccupiedSpace, +): ReflowDirection[] { + if (!mousePosition) return []; + const directionsWithDistance: { + distance: number; + direction: ReflowDirection; + }[] = []; + + if (containerSpace.left >= mousePosition.left) { + directionsWithDistance.push({ + distance: containerSpace.left - mousePosition.left, + direction: ReflowDirection.RIGHT, + }); + } else if (mousePosition.left >= containerSpace.right) { + directionsWithDistance.push({ + distance: mousePosition.left - containerSpace.right, + direction: ReflowDirection.LEFT, + }); + } + + if (containerSpace.top >= mousePosition.top) { + directionsWithDistance.push({ + distance: containerSpace.top - mousePosition.top, + direction: ReflowDirection.BOTTOM, + }); + } else if (mousePosition.top >= containerSpace.bottom) { + directionsWithDistance.push({ + distance: mousePosition.top - containerSpace.bottom, + direction: ReflowDirection.TOP, + }); + } + + return directionsWithDistance + .sort((a, b) => { + return b.distance - a.distance; + }) + .map((obj) => obj.direction); +} + +/** + * Resize the dragging widget on collision with Container/Droptarget widget + * @param newSpacePositions positions of dragging spaces + * @param occupiedSpaces occupied spaces of other blocks on the canvas + * @param mousePosition position of mouse on canvas grid + * @param direction ReflowDirection + * @param orientationalAccessor "vertical" or "horizontal" + * @param prevCollidingSpaceMap previous colliding map + * @param forceDirection boolean to indicate if direction should be forced + * @param isHorizontalMove boolean + * @param prevSpacesMap previous position maps + * @returns + */ +export function resizeOnContainerCollision( + newSpacePositions: BlockSpace[], + occupiedSpaces: BlockSpace[], + mousePosition: OccupiedSpace | undefined, + direction: ReflowDirection, + orientationalAccessor: "horizontal" | "vertical", + prevCollidingSpaceMap: CollidingSpaceMap, + forceDirection: boolean, + isHorizontalMove?: boolean, + prevSpacesMap?: SpaceMap, +): { + reflowableOccSpaces: BlockSpace[]; + currSpacePositions: BlockSpace[]; + shouldRegisterContainerTimeout: boolean; +} { + const reflowableOccSpaces = []; + + //boolean to indicate if this run should be registered for timeout + let shouldRegisterContainerTimeout = false; + // resize space positions only is single space is being dragged + if (newSpacePositions.length > 1) { + return { + reflowableOccSpaces: occupiedSpaces.filter( + (occSpace) => !occSpace.isDropTarget, + ), + currSpacePositions: newSpacePositions, + shouldRegisterContainerTimeout: occupiedSpaces.some( + (space) => space.isDropTarget, + ), + }; + } + + let currSpacePosition = { ...newSpacePositions[0] }; + for (const occupiedSpace of occupiedSpaces) { + if (areOverlapping(occupiedSpace, currSpacePosition)) { + if (!occupiedSpace.isDropTarget) { + reflowableOccSpaces.push(occupiedSpace); + continue; + } + + //get calculated direction + let movementDirection = getCorrectedDirection( + occupiedSpace, + prevSpacesMap && prevSpacesMap[currSpacePosition.id] + ? prevSpacesMap[currSpacePosition.id] + : undefined, + direction, + false, + prevCollidingSpaceMap && prevCollidingSpaceMap[orientationalAccessor], + isHorizontalMove, + ); + + //check if direction could be changed because of mouse Position + movementDirection = getCollisionDirectionOfDropTarget( + occupiedSpace, + movementDirection, + mousePosition, + ); + + //modify/resize dragging position if required + currSpacePosition = modifyResizePosition( + currSpacePosition, + occupiedSpace, + forceDirection ? direction : movementDirection, + ); + + // setting it to true as it should register a timeout function + shouldRegisterContainerTimeout = true; + } + } + + return { + reflowableOccSpaces, + currSpacePositions: [currSpacePosition], + shouldRegisterContainerTimeout, + }; +} + +/** + * Modify the Space position when colliding with + * @param newSpacePositions positions of dragging spaces + * @param collidingContainer Space positions of Container/Droptargets + * @param direction ReflowDirection + * @returns + */ +export function modifyResizePosition( + newSpacePosition: BlockSpace, + collidingContainer: BlockSpace, + direction: ReflowDirection, +): BlockSpace { + if (!direction || direction === ReflowDirection.UNSET) { + return newSpacePosition; + } + + const spacePosition = { ...newSpacePosition }; + const { + direction: directionAccessor, + directionIndicator, + isHorizontal, + mathComparator, + oppositeDirection, + oppositeMathComparator, + } = getAccessor(direction); + + spacePosition[directionAccessor] = Math[mathComparator]( + spacePosition[directionAccessor], + collidingContainer[oppositeDirection], + ); + + const minDimension = isHorizontal + ? HORIZONTAL_RESIZE_MIN_LIMIT + : newSpacePosition.fixedHeight === undefined + ? VERTICAL_RESIZE_MIN_LIMIT + : newSpacePosition.fixedHeight; + + spacePosition[directionAccessor] = Math[oppositeMathComparator]( + spacePosition[directionAccessor], + spacePosition[oppositeDirection] + directionIndicator * minDimension, + ); + + return spacePosition; +} + +/** + * Checks if any of the widget has reached it's movements limit and returns true if it has + * @param movementLimitMap + * @returns boolean + */ +export function willItCauseUndroppableState( + movementLimitMap: MovementLimitMap | undefined, +) { + if (!movementLimitMap) return true; + + const movementLimits = Object.values(movementLimitMap); + + return movementLimits.some( + (limit) => !(limit.canHorizontalMove && limit.canVerticalMove), + ); +} + +/** + * verify the widget being dragged is colliding with drop targets when they are not being reflowed + * @param movementLimitMap + * @param spacePositionMap + * @param occupiedSpacesMap + * @returns + */ +export function verifyMovementLimits( + movementLimitMap: MovementLimitMap, + spacePositionMap: SpaceMap, + occupiedSpacesMap: SpaceMap, +) { + for (const spaceId in spacePositionMap) { + for (const occupiedSpaceId in occupiedSpacesMap) { + if ( + occupiedSpacesMap[occupiedSpaceId].isDropTarget && + areIntersecting( + occupiedSpacesMap[occupiedSpaceId], + spacePositionMap[spaceId], + ) + ) { + movementLimitMap[spaceId] = { + canHorizontalMove: false, + canVerticalMove: false, + }; + } + } + } + + return movementLimitMap; +} diff --git a/app/client/src/reflow/tests/reflowHelpers.test.js b/app/client/src/reflow/tests/reflowHelpers.test.js index ca99b5355f..f034016505 100644 --- a/app/client/src/reflow/tests/reflowHelpers.test.js +++ b/app/client/src/reflow/tests/reflowHelpers.test.js @@ -1,7 +1,7 @@ import { - HORIZONTAL_RESIZE_LIMIT, + HORIZONTAL_RESIZE_MIN_LIMIT, ReflowDirection, - VERTICAL_RESIZE_LIMIT, + VERTICAL_RESIZE_MIN_LIMIT, } from "reflow/reflowTypes"; import { getAccessor } from "reflow/reflowUtils"; import { @@ -368,7 +368,7 @@ describe("Test reflow helper methods", () => { directionY: "BOTTOM", height: 200, maxY: Infinity, - verticalOccupiedLength: VERTICAL_RESIZE_LIMIT, + verticalOccupiedLength: VERTICAL_RESIZE_MIN_LIMIT, verticalEmptySpaces: 0, verticalMaxOccupiedSpace: 20, }, @@ -574,7 +574,7 @@ describe("Test reflow helper methods", () => { gridProps, ReflowDirection.RIGHT, 20, - 3 * HORIZONTAL_RESIZE_LIMIT, + 3 * HORIZONTAL_RESIZE_MIN_LIMIT, -10, 7, 7, @@ -585,7 +585,7 @@ describe("Test reflow helper methods", () => { X: 30, dimensionXBeforeCollision: -10, directionX: "RIGHT", - horizontalOccupiedLength: 3 * HORIZONTAL_RESIZE_LIMIT, + horizontalOccupiedLength: 3 * HORIZONTAL_RESIZE_MIN_LIMIT, horizontalEmptySpaces: 7, horizontalMaxOccupiedSpace: 20, maxX: 80, @@ -599,7 +599,7 @@ describe("Test reflow helper methods", () => { gridProps, ReflowDirection.BOTTOM, 20, - 3 * VERTICAL_RESIZE_LIMIT, + 3 * VERTICAL_RESIZE_MIN_LIMIT, -10, 7, 7, @@ -612,7 +612,7 @@ describe("Test reflow helper methods", () => { directionY: "BOTTOM", height: 200, maxY: Infinity, - verticalOccupiedLength: 3 * VERTICAL_RESIZE_LIMIT, + verticalOccupiedLength: 3 * VERTICAL_RESIZE_MIN_LIMIT, verticalEmptySpaces: 7, verticalMaxOccupiedSpace: 20, }); diff --git a/app/client/src/reflow/tests/reflowUtils.test.js b/app/client/src/reflow/tests/reflowUtils.test.js index 2280c7254f..33aae4f5e4 100644 --- a/app/client/src/reflow/tests/reflowUtils.test.js +++ b/app/client/src/reflow/tests/reflowUtils.test.js @@ -26,8 +26,16 @@ import { initializeMovementLimitMap, checkProcessNodeForTree, getRelativeCollidingValue, + getContainerExitEdge, + getCollisionDirectionOfDropTarget, + modifyResizePosition, + willItCauseUndroppableState, + verifyMovementLimits, } from "../reflowUtils"; -import { HORIZONTAL_RESIZE_LIMIT, VERTICAL_RESIZE_LIMIT } from "../reflowTypes"; +import { + HORIZONTAL_RESIZE_MIN_LIMIT, + VERTICAL_RESIZE_MIN_LIMIT, +} from "../reflowTypes"; const gridProps = { parentColumnSpace: 20, @@ -532,7 +540,7 @@ describe("Test reflow util methods", () => { collisionTree, gridProps, ReflowDirection.LEFT, - depth * HORIZONTAL_RESIZE_LIMIT, + depth * HORIZONTAL_RESIZE_MIN_LIMIT, 30, false, ), @@ -545,13 +553,13 @@ describe("Test reflow util methods", () => { collisionTree, gridProps, ReflowDirection.LEFT, - depth * HORIZONTAL_RESIZE_LIMIT, + depth * HORIZONTAL_RESIZE_MIN_LIMIT, 30, true, ), ).toBe( -1 * - (collisionTree.left - depth * HORIZONTAL_RESIZE_LIMIT) * + (collisionTree.left - depth * HORIZONTAL_RESIZE_MIN_LIMIT) * gridProps.parentColumnSpace, ); }); @@ -562,7 +570,7 @@ describe("Test reflow util methods", () => { collisionTree, gridProps, ReflowDirection.RIGHT, - depth * HORIZONTAL_RESIZE_LIMIT, + depth * HORIZONTAL_RESIZE_MIN_LIMIT, 30, false, ), @@ -578,14 +586,14 @@ describe("Test reflow util methods", () => { collisionTree, gridProps, ReflowDirection.RIGHT, - depth * HORIZONTAL_RESIZE_LIMIT, + depth * HORIZONTAL_RESIZE_MIN_LIMIT, 30, true, ), ).toBe( (gridProps.maxGridColumns - collisionTree.right - - depth * HORIZONTAL_RESIZE_LIMIT) * + depth * HORIZONTAL_RESIZE_MIN_LIMIT) * gridProps.parentColumnSpace, ); }); @@ -608,7 +616,7 @@ describe("Test reflow util methods", () => { collisionTree, gridProps, ReflowDirection.TOP, - depth * VERTICAL_RESIZE_LIMIT, + depth * VERTICAL_RESIZE_MIN_LIMIT, 20, false, ), @@ -621,13 +629,13 @@ describe("Test reflow util methods", () => { collisionTree, gridProps, ReflowDirection.TOP, - depth * VERTICAL_RESIZE_LIMIT, + depth * VERTICAL_RESIZE_MIN_LIMIT, 20, true, ), ).toBe( -1 * - (collisionTree.top - depth * VERTICAL_RESIZE_LIMIT) * + (collisionTree.top - depth * VERTICAL_RESIZE_MIN_LIMIT) * gridProps.parentRowSpace, ); }); @@ -638,7 +646,7 @@ describe("Test reflow util methods", () => { collisionTree, gridProps, ReflowDirection.BOTTOM, - depth * VERTICAL_RESIZE_LIMIT, + depth * VERTICAL_RESIZE_MIN_LIMIT, 230, false, ), @@ -801,7 +809,7 @@ describe("Test reflow util methods", () => { dimensionBeforeCollision, gridProps.parentRowSpace, emptySpaces, - VERTICAL_RESIZE_LIMIT, + VERTICAL_RESIZE_MIN_LIMIT, ), ).toBe(height); }); @@ -816,7 +824,7 @@ describe("Test reflow util methods", () => { dimensionBeforeCollision, gridProps.parentRowSpace, emptySpaces, - VERTICAL_RESIZE_LIMIT, + VERTICAL_RESIZE_MIN_LIMIT, true, ), ).toBe(height); @@ -835,7 +843,7 @@ describe("Test reflow util methods", () => { dimensionBeforeCollision, gridProps.parentRowSpace, emptySpaces, - VERTICAL_RESIZE_LIMIT, + VERTICAL_RESIZE_MIN_LIMIT, true, ), ).toBe(resizedHeight); @@ -851,10 +859,10 @@ describe("Test reflow util methods", () => { dimensionBeforeCollision, gridProps.parentRowSpace, emptySpaces, - VERTICAL_RESIZE_LIMIT, + VERTICAL_RESIZE_MIN_LIMIT, true, ), - ).toBe(VERTICAL_RESIZE_LIMIT * gridProps.parentRowSpace); + ).toBe(VERTICAL_RESIZE_MIN_LIMIT * gridProps.parentRowSpace); }); }); @@ -874,7 +882,7 @@ describe("Test reflow util methods", () => { dimensionBeforeCollision, gridProps.parentColumnSpace, emptySpaces, - HORIZONTAL_RESIZE_LIMIT, + HORIZONTAL_RESIZE_MIN_LIMIT, ), ).toBe(width); }); @@ -889,7 +897,7 @@ describe("Test reflow util methods", () => { dimensionBeforeCollision, gridProps.parentColumnSpace, emptySpaces, - HORIZONTAL_RESIZE_LIMIT, + HORIZONTAL_RESIZE_MIN_LIMIT, true, ), ).toBe(width); @@ -910,7 +918,7 @@ describe("Test reflow util methods", () => { dimensionBeforeCollision, gridProps.parentColumnSpace, emptySpaces, - HORIZONTAL_RESIZE_LIMIT, + HORIZONTAL_RESIZE_MIN_LIMIT, true, ), ).toBe(resizedWidth); @@ -926,10 +934,10 @@ describe("Test reflow util methods", () => { dimensionBeforeCollision, gridProps.parentColumnSpace, emptySpaces, - HORIZONTAL_RESIZE_LIMIT, + HORIZONTAL_RESIZE_MIN_LIMIT, true, ), - ).toBe(HORIZONTAL_RESIZE_LIMIT * gridProps.parentColumnSpace); + ).toBe(HORIZONTAL_RESIZE_MIN_LIMIT * gridProps.parentColumnSpace); }); }); @@ -949,7 +957,7 @@ describe("Test reflow util methods", () => { dimensionBeforeCollision, gridProps.parentColumnSpace, emptySpaces, - HORIZONTAL_RESIZE_LIMIT, + HORIZONTAL_RESIZE_MIN_LIMIT, ), ).toBe(width); }); @@ -964,7 +972,7 @@ describe("Test reflow util methods", () => { dimensionBeforeCollision, gridProps.parentColumnSpace, emptySpaces, - HORIZONTAL_RESIZE_LIMIT, + HORIZONTAL_RESIZE_MIN_LIMIT, true, ), ).toBe(width); @@ -986,7 +994,7 @@ describe("Test reflow util methods", () => { dimensionBeforeCollision, gridProps.parentColumnSpace, emptySpaces, - HORIZONTAL_RESIZE_LIMIT, + HORIZONTAL_RESIZE_MIN_LIMIT, true, ), ).toBe(resizedWidth); @@ -1002,10 +1010,10 @@ describe("Test reflow util methods", () => { dimensionBeforeCollision, gridProps.parentColumnSpace, emptySpaces, - HORIZONTAL_RESIZE_LIMIT, + HORIZONTAL_RESIZE_MIN_LIMIT, true, ), - ).toBe(HORIZONTAL_RESIZE_LIMIT * gridProps.parentColumnSpace); + ).toBe(HORIZONTAL_RESIZE_MIN_LIMIT * gridProps.parentColumnSpace); }); }); }); @@ -1913,7 +1921,7 @@ describe("Test reflow util methods", () => { "1234": { BOTTOM: { value: 10, - occupiedLength: 5 * VERTICAL_RESIZE_LIMIT, + occupiedLength: 5 * VERTICAL_RESIZE_MIN_LIMIT, occupiedSpace: 10, currentEmptySpaces: 10, }, @@ -1921,7 +1929,7 @@ describe("Test reflow util methods", () => { }; expect(checkProcessNodeForTree(collidingSpace, processedNodes)).toEqual({ shouldProcessNode: false, - occupiedLength: 5 * VERTICAL_RESIZE_LIMIT, + occupiedLength: 5 * VERTICAL_RESIZE_MIN_LIMIT, occupiedSpace: 10, currentEmptySpaces: 10, }); @@ -1943,7 +1951,7 @@ describe("Test reflow util methods", () => { collidingValue, direction, gridProps, - depth * VERTICAL_RESIZE_LIMIT, + depth * VERTICAL_RESIZE_MIN_LIMIT, ), ).toBe(collidingValue); }); @@ -1958,7 +1966,7 @@ describe("Test reflow util methods", () => { collidingValue, direction, gridProps, - depth * VERTICAL_RESIZE_LIMIT, + depth * VERTICAL_RESIZE_MIN_LIMIT, ), ).toBe(collidingValue); }); @@ -1973,9 +1981,9 @@ describe("Test reflow util methods", () => { collidingValue, direction, gridProps, - depth * VERTICAL_RESIZE_LIMIT, + depth * VERTICAL_RESIZE_MIN_LIMIT, ), - ).toBe(depth * VERTICAL_RESIZE_LIMIT); + ).toBe(depth * VERTICAL_RESIZE_MIN_LIMIT); }); it("should return calculated colliding value if depth is high compared to colliding value in LEFT direction", () => { const direction = ReflowDirection.LEFT; @@ -1988,9 +1996,9 @@ describe("Test reflow util methods", () => { collidingValue, direction, gridProps, - depth * HORIZONTAL_RESIZE_LIMIT, + depth * HORIZONTAL_RESIZE_MIN_LIMIT, ), - ).toBe(depth * HORIZONTAL_RESIZE_LIMIT); + ).toBe(depth * HORIZONTAL_RESIZE_MIN_LIMIT); }); it("should return calculated colliding value if depth is high compared to colliding value in RIGHT direction", () => { const direction = ReflowDirection.RIGHT; @@ -2003,9 +2011,376 @@ describe("Test reflow util methods", () => { collidingValue, direction, gridProps, - depth * HORIZONTAL_RESIZE_LIMIT, + depth * HORIZONTAL_RESIZE_MIN_LIMIT, ), - ).toBe(gridProps.maxGridColumns - depth * HORIZONTAL_RESIZE_LIMIT); + ).toBe(gridProps.maxGridColumns - depth * HORIZONTAL_RESIZE_MIN_LIMIT); }); }); + + describe("while testing getContainerExitEdge, it should return edge direction that is closest to mouse", () => { + const exitContainer = { + id: "exit", + left: 20, + right: 60, + top: 20, + bottom: 70, + }; + it("should return RIGHT if closer to right container edge", () => { + const mousePointer = { + left: 62, + top: 40, + }; + expect(getContainerExitEdge(exitContainer, mousePointer)).toEqual( + ReflowDirection.RIGHT, + ); + }); + + it("should return LEFT if closer to right container edge", () => { + const mousePointer = { + left: 19, + top: 40, + }; + expect(getContainerExitEdge(exitContainer, mousePointer)).toEqual( + ReflowDirection.LEFT, + ); + }); + + it("should return TOP if closer to top container edge", () => { + const mousePointer = { + left: 40, + top: 19, + }; + expect(getContainerExitEdge(exitContainer, mousePointer)).toEqual( + ReflowDirection.TOP, + ); + }); + + it("should return BOTTOM if closer to bottom container edge", () => { + const mousePointer = { + left: 40, + top: 72, + }; + expect(getContainerExitEdge(exitContainer, mousePointer)).toEqual( + ReflowDirection.BOTTOM, + ); + }); + }); + + describe("test getCollisionDirectionOfDropTarget method", () => { + const containerSpace = { + id: "container", + left: 20, + right: 60, + top: 20, + bottom: 70, + }; + + it("should return current direction if it is possible push direction", () => { + const mousePosition = { + left: 63, + top: 75, + }; + expect( + getCollisionDirectionOfDropTarget( + containerSpace, + ReflowDirection.LEFT, + mousePosition, + ), + ).toBe(ReflowDirection.LEFT); + + expect( + getCollisionDirectionOfDropTarget( + containerSpace, + ReflowDirection.TOP, + mousePosition, + ), + ).toBe(ReflowDirection.TOP); + }); + + it("should return push direction if mouse is only on one edge even if direction sent is not the same", () => { + let mousePosition = { + left: 40, + top: 14, + }; + expect( + getCollisionDirectionOfDropTarget( + containerSpace, + ReflowDirection.UNSET, + mousePosition, + ), + ).toBe(ReflowDirection.BOTTOM); + + mousePosition = { + left: 67, + top: 40, + }; + expect( + getCollisionDirectionOfDropTarget( + containerSpace, + ReflowDirection.UNSET, + mousePosition, + ), + ).toBe(ReflowDirection.LEFT); + }); + + it("should return push direction which is farthest from edge, if it has 2 possible directions and the direction sent is not one of them", () => { + let mousePosition = { + left: 18, + top: 14, + }; + expect( + getCollisionDirectionOfDropTarget( + containerSpace, + ReflowDirection.UNSET, + mousePosition, + ), + ).toBe(ReflowDirection.BOTTOM); + }); + }); + + describe("test modifyResizePosition method", () => { + const containerSpace = { + id: "container", + left: 20, + right: 60, + top: 20, + bottom: 70, + }; + it("should return resized position based on direction of collision with Container's left side", () => { + const spacePosition = { + id: "id", + left: 10, + right: 30, + top: 15, + bottom: 40, + }; + const resizedPosition = { + id: "id", + left: 10, + right: 20, + top: 15, + bottom: 40, + }; + + expect( + modifyResizePosition( + spacePosition, + containerSpace, + ReflowDirection.RIGHT, + ), + ).toEqual(resizedPosition); + }); + + it("should return resized position based on direction of collision with Container's right side", () => { + const spacePosition = { + id: "id", + left: 50, + right: 90, + top: 15, + bottom: 40, + }; + const resizedPosition = { + id: "id", + left: 60, + right: 90, + top: 15, + bottom: 40, + }; + + expect( + modifyResizePosition( + spacePosition, + containerSpace, + ReflowDirection.LEFT, + ), + ).toEqual(resizedPosition); + }); + + it("should return resized position based on direction of collision with Container's top side", () => { + const spacePosition = { + id: "id", + left: 15, + right: 40, + top: 5, + bottom: 40, + }; + const resizedPosition = { + id: "id", + left: 15, + right: 40, + top: 5, + bottom: 20, + }; + + expect( + modifyResizePosition( + spacePosition, + containerSpace, + ReflowDirection.BOTTOM, + ), + ).toEqual(resizedPosition); + }); + + it("should return resized position based on direction of collision with Container's bottom side", () => { + const spacePosition = { + id: "id", + left: 15, + right: 40, + top: 60, + bottom: 95, + }; + const resizedPosition = { + id: "id", + left: 15, + right: 40, + top: 70, + bottom: 95, + }; + + expect( + modifyResizePosition( + spacePosition, + containerSpace, + ReflowDirection.TOP, + ), + ).toEqual(resizedPosition); + }); + + it("should return resized position based on direction of collision but with min heights and widths", () => { + let spacePosition = { + id: "id", + left: 19, + right: 40, + top: 19, + bottom: 95, + }; + let resizedPosition = { + id: "id", + left: 19, + right: 40, + top: 19, + bottom: 19 + VERTICAL_RESIZE_MIN_LIMIT, + }; + + expect( + modifyResizePosition( + spacePosition, + containerSpace, + ReflowDirection.BOTTOM, + ), + ).toEqual(resizedPosition); + + resizedPosition = { + id: "id", + left: 19, + right: 19 + HORIZONTAL_RESIZE_MIN_LIMIT, + top: 19, + bottom: 95, + }; + + expect( + modifyResizePosition( + spacePosition, + containerSpace, + ReflowDirection.RIGHT, + ), + ).toEqual(resizedPosition); + + spacePosition.fixedHeight = 95 - 19; + expect( + modifyResizePosition( + spacePosition, + containerSpace, + ReflowDirection.BOTTOM, + ), + ).toEqual(spacePosition); + }); + }); + + it("should test willItCauseUndroppableState method, it should return true if any value is false", () => { + const movementLimitMap = { + "1": { + canVerticalMove: true, + canHorizontalMove: true, + }, + "2": { + canVerticalMove: true, + canHorizontalMove: true, + }, + }; + + expect(willItCauseUndroppableState(movementLimitMap)).toEqual(false); + + movementLimitMap["3"] = { + canVerticalMove: true, + canHorizontalMove: false, + }; + expect(willItCauseUndroppableState(movementLimitMap)).toEqual(true); + }); + + it("verifyMovementLimits should check if space is colliding with any container and return movementLimits based on that", () => { + const movementLimits = { + "1": { + canVerticalMove: true, + canHorizontalMove: true, + }, + "2": { + canVerticalMove: true, + canHorizontalMove: true, + }, + "3": { + canVerticalMove: false, + canHorizontalMove: true, + }, + }; + + const occupiedSpacesMap = { + "4": { + left: 50, + right: 70, + top: 60, + bottom: 90, + isDropTarget: true, + }, + }; + const spacePositionMap = { + "1": { + left: 10, + right: 40, + top: 20, + bottom: 50, + }, + "2": { + left: 20, + right: 65, + top: 20, + bottom: 50, + }, + "3": { + left: 90, + right: 110, + top: 20, + bottom: 50, + }, + }; + + const verifiedMovementLimits = { + "1": { + canVerticalMove: true, + canHorizontalMove: true, + }, + "2": { + canVerticalMove: true, + canHorizontalMove: true, + }, + "3": { + canVerticalMove: false, + canHorizontalMove: true, + }, + }; + + expect( + verifyMovementLimits(movementLimits, spacePositionMap, occupiedSpacesMap), + ).toEqual(verifiedMovementLimits); + }); }); diff --git a/app/client/src/resizable/resizenreflow/index.tsx b/app/client/src/resizable/resizenreflow/index.tsx index f9097e0aca..4341e5fe41 100644 --- a/app/client/src/resizable/resizenreflow/index.tsx +++ b/app/client/src/resizable/resizenreflow/index.tsx @@ -257,7 +257,7 @@ export function ReflowResizable(props: ResizableProps) { if (resizedPositions) { //calling reflow to update movements of reflowing widgets and get movementLimit of current resizing widget - ({ bottomMostRow, movementLimitMap } = reflow( + ({ bottomMostRow, movementLimitMap } = reflow.reflowSpaces( [resizedPositions], direction, true, diff --git a/app/client/src/sagas/selectors.tsx b/app/client/src/sagas/selectors.tsx index 2a384b8d75..6479f6177e 100644 --- a/app/client/src/sagas/selectors.tsx +++ b/app/client/src/sagas/selectors.tsx @@ -15,6 +15,7 @@ import { ActionData } from "reducers/entityReducers/actionsReducer"; import { Page } from "@appsmith/constants/ReduxActionConstants"; import { getActions, getPlugins } from "selectors/entitiesSelector"; import { Plugin } from "api/PluginApi"; +import { DragDetails } from "reducers/uiReducers/dragResizeReducer"; import { DataTreeForActionCreator } from "components/editorComponents/ActionCreator/types"; export const getWidgets = (state: AppState): CanvasWidgetsReduxState => { @@ -174,6 +175,14 @@ export const getPluginIdOfPackageName = ( export const getDragDetails = (state: AppState) => { return state.ui.widgetDragResize.dragDetails; }; +export const isCurrentCanvasDragging = createSelector( + (state: AppState) => state.ui.widgetDragResize.isDragging, + getDragDetails, + (state: AppState, canvasId: string) => canvasId, + (isDragging: boolean, dragDetails: DragDetails, canvasId: string) => { + return dragDetails?.draggedOn === canvasId && isDragging; + }, +); export const getSelectedWidget = ( state: AppState, diff --git a/app/client/src/selectors/editorSelectors.tsx b/app/client/src/selectors/editorSelectors.tsx index f5bc3a8b62..b3b8d799eb 100644 --- a/app/client/src/selectors/editorSelectors.tsx +++ b/app/client/src/selectors/editorSelectors.tsx @@ -35,6 +35,7 @@ import { createCanvasWidget, createLoadingWidget, } from "utils/widgetRenderUtils"; +import { checkIsDropTarget } from "components/designSystems/appsmith/PositionedContainer"; import WidgetFactory, { NonSerialisableWidgetConfigs, } from "utils/WidgetFactory"; @@ -286,6 +287,7 @@ export const getWidgetCards = createSelector( displayName, icon: iconSVG, searchTags, + isDynamicHeight: isAutoHeightEnabledForWidget(config as WidgetProps), }; }); const sortedCards = sortBy(_cards, ["displayName"]); @@ -407,6 +409,7 @@ const getWidgetSpacesForContainer = ( bottom: widget.bottomRow, right: widget.rightColumn, type: widget.type, + isDropTarget: checkIsDropTarget(widget.type), fixedHeight, }; return occupiedSpace; @@ -423,9 +426,9 @@ const getWidgetSpacesForContainer = ( const generateOccupiedSpacesMap = ( widgets: CanvasWidgetsReduxState, fetchNow = true, -): { [containerWidgetId: string]: OccupiedSpace[] } | undefined => { +): { [containerWidgetId: string]: WidgetSpace[] } | undefined => { const occupiedSpaces: { - [containerWidgetId: string]: OccupiedSpace[]; + [containerWidgetId: string]: WidgetSpace[]; } = {}; if (!fetchNow) return; // Get all widgets with type "CONTAINER_WIDGET" and has children @@ -446,7 +449,7 @@ const generateOccupiedSpacesMap = ( ); // Get the occupied spaces in this container // Assign it to the containerWidgetId key in occupiedSpaces - occupiedSpaces[containerWidgetId] = getOccupiedSpacesForContainer( + occupiedSpaces[containerWidgetId] = getWidgetSpacesForContainer( containerWidgetId, childWidgets.map((widgetId) => widgets[widgetId]), ); diff --git a/app/client/src/utils/WidgetPropsUtils.tsx b/app/client/src/utils/WidgetPropsUtils.tsx index 8b5c1a982a..2acd2e6ee1 100644 --- a/app/client/src/utils/WidgetPropsUtils.tsx +++ b/app/client/src/utils/WidgetPropsUtils.tsx @@ -19,9 +19,9 @@ import { transformDSL } from "./DSLMigrations"; import { WidgetType } from "./WidgetFactory"; import { DSLWidget } from "widgets/constants"; import { WidgetDraggingBlock } from "pages/common/CanvasArenas/hooks/useBlocksToBeDraggedOnCanvas"; -import { XYCord } from "pages/common/CanvasArenas/hooks/useCanvasDragging"; +import { XYCord } from "pages/common/CanvasArenas/hooks/useRenderBlocksOnCanvas"; import { ContainerWidgetProps } from "widgets/ContainerWidget/widget"; -import { GridProps } from "reflow/reflowTypes"; +import { BlockSpace, GridProps } from "reflow/reflowTypes"; import { areIntersecting, Rect } from "./boxHelpers"; export type WidgetOperationParams = { @@ -54,7 +54,7 @@ export function getDraggingSpacesFromBlocks( draggingBlocks: WidgetDraggingBlock[], snapColumnSpace: number, snapRowSpace: number, -): OccupiedSpace[] { +): BlockSpace[] { const draggingSpaces = []; for (const draggingBlock of draggingBlocks) { //gets top and left position of the block @@ -76,6 +76,10 @@ export function getDraggingSpacesFromBlocks( right: leftColumn + draggingBlock.width / snapColumnSpace, bottom: topRow + draggingBlock.height / snapRowSpace, id: draggingBlock.widgetId, + fixedHeight: + draggingBlock.fixedHeight !== undefined + ? draggingBlock.rowHeight + : undefined, }); } return draggingSpaces; diff --git a/app/client/src/utils/hooks/useReflow.ts b/app/client/src/utils/hooks/useReflow.ts index 9fab88c4db..c7c352ba97 100644 --- a/app/client/src/utils/hooks/useReflow.ts +++ b/app/client/src/utils/hooks/useReflow.ts @@ -6,6 +6,7 @@ import { useDispatch, useSelector } from "react-redux"; import { getContainerWidgetSpacesSelectorWhileMoving } from "selectors/editorSelectors"; import { reflow } from "reflow"; import { + BlockSpace, CollidingSpace, CollidingSpaceMap, GridProps, @@ -14,20 +15,22 @@ import { ReflowDirection, ReflowedSpaceMap, SecondOrderCollisionMap, + SpaceMap, } from "reflow/reflowTypes"; import { getBottomMostRow, getLimitedMovementMap, getSpacesMapFromArray, + willItCauseUndroppableState, } from "reflow/reflowUtils"; import { getBottomRowAfterReflow } from "utils/reflowHookUtils"; -import { checkIsDropTarget } from "components/designSystems/appsmith/PositionedContainer"; import { getIsReflowing } from "selectors/widgetReflowSelectors"; import { AppState } from "@appsmith/reducers"; -import { areIntersecting } from "utils/boxHelpers"; +import { isCurrentCanvasDragging } from "sagas/selectors"; type WidgetCollidingSpace = CollidingSpace & { type: string; + isDropTarget: boolean; }; type WidgetCollidingSpaceMap = { @@ -40,18 +43,22 @@ export type WidgetCollisionMap = { export interface ReflowInterface { ( - newPositions: OccupiedSpace[], + newPositions: BlockSpace[], direction: ReflowDirection, stopMoveAfterLimit?: boolean, shouldSkipContainerReflow?: boolean, forceDirection?: boolean, immediateExitContainer?: string, mousePosition?: OccupiedSpace, + reflowAfterTimeoutCallback?: (reflowParams: { + movementMap: ReflowedSpaceMap; + spacePositionMap: SpaceMap | undefined; + }) => void, ): { movementLimitMap?: MovementLimitMap; movementMap: ReflowedSpaceMap; bottomMostRow: number; - isIdealToJumpContainer: boolean; + spacePositionMap: SpaceMap | undefined; }; } @@ -59,11 +66,12 @@ export const useReflow = ( OGPositions: OccupiedSpace[], parentId: string, gridProps: GridProps, -): ReflowInterface => { +): { reflowSpaces: ReflowInterface; resetReflow: () => void } => { const dispatch = useDispatch(); const isReflowingGlobal = useSelector(getIsReflowing); - const isDragging = useSelector( - (state: AppState) => state.ui.widgetDragResize.isDragging, + + const isDraggingCanvas = useSelector((state: AppState) => + isCurrentCanvasDragging(state, parentId), ); const throttledDispatch = throttle(dispatch, 50); @@ -75,115 +83,211 @@ export const useReflow = ( ); const widgetSpaces: WidgetSpace[] = useSelector(reflowSpacesSelector) || []; + // Store previous values of reflow results const prevPositions = useRef(OGPositions); const prevCollidingSpaces = useRef(); const prevMovementMap = useRef({}); const prevSecondOrderCollisionMap = useRef({}); + // Indicates if the Containers should be reflowed + const shouldReflowDropTargets = useRef(false); + // ref of timeout method + const timeOutFunction = useRef(); + // store exit container and mouse position at exit, so that it can be used during timeout + const exitContainer = useRef(undefined); + const mousePointerAtContainerExit = useRef( + undefined, + ); + useEffect(() => { //only have it run when the user has completely stopped dragging and stopped Reflowing - if (!isReflowingGlobal && !isDragging) { + if (!isReflowingGlobal && !isDraggingCanvas) { isReflowing.current = false; prevPositions.current = [...OGPositions]; prevCollidingSpaces.current = { horizontal: {}, vertical: {} }; prevMovementMap.current = {}; prevSecondOrderCollisionMap.current = {}; + shouldReflowDropTargets.current = false; } - }, [isReflowingGlobal, isDragging]); + + if (!isDraggingCanvas) { + clearTimeout(timeOutFunction.current); + exitContainer.current = undefined; + mousePointerAtContainerExit.current = undefined; + } + }, [isReflowingGlobal, isDraggingCanvas]); // will become a state if we decide that resize should be a "toggle on-demand" feature const shouldResize = true; - return function reflowSpaces( - newPositions: OccupiedSpace[], - direction: ReflowDirection, - stopMoveAfterLimit = false, - shouldSkipContainerReflow = false, - forceDirection = false, - immediateExitContainer?: string, - mousePosition?: OccupiedSpace, - ) { - const prevReflowState: PrevReflowState = { - prevSpacesMap: getSpacesMapFromArray(prevPositions.current), - prevCollidingSpaceMap: prevCollidingSpaces.current as CollidingSpaceMap, - prevMovementMap: prevMovementMap.current, - prevSecondOrderCollisionMap: prevSecondOrderCollisionMap.current, - }; + return { + reflowSpaces: ( + newPositions: BlockSpace[], + direction: ReflowDirection, + stopMoveAfterLimit = false, + shouldSkipContainerReflow = false, + forceDirection = false, + immediateExitContainer?: string, + mousePosition?: OccupiedSpace, + reflowAfterTimeoutCallback?: (reflowParams: { + movementMap: ReflowedSpaceMap; + spacePositionMap: SpaceMap | undefined; + }) => void, + ) => { + clearTimeout(timeOutFunction.current); - // To track container jumps - let isIdealToJumpContainer = false; + const prevReflowState: PrevReflowState = { + prevSpacesMap: getSpacesMapFromArray(prevPositions.current), + prevCollidingSpaceMap: prevCollidingSpaces.current as CollidingSpaceMap, + prevMovementMap: prevMovementMap.current, + prevSecondOrderCollisionMap: prevSecondOrderCollisionMap.current, + }; - const { - collidingSpaceMap, - movementLimitMap, - movementMap, - secondOrderCollisionMap, - } = reflow( - newPositions, - OGPositions, - widgetSpaces, - direction, - gridProps, - forceDirection, - shouldResize, - prevReflowState, - immediateExitContainer, - ); - - prevPositions.current = newPositions; - prevCollidingSpaces.current = collidingSpaceMap as WidgetCollidingSpaceMap; - prevSecondOrderCollisionMap.current = secondOrderCollisionMap || {}; - - let correctedMovementMap = movementMap || {}; - - if (stopMoveAfterLimit) - correctedMovementMap = getLimitedMovementMap( + const { + collidingSpaceMap, + movementLimitMap, movementMap, - prevMovementMap.current, - { canHorizontalMove: true, canVerticalMove: true }, + secondOrderCollisionMap, + shouldRegisterContainerTimeout, + spacePositionMap, + } = reflow( + newPositions, + OGPositions, + widgetSpaces, + direction, + gridProps, + forceDirection, + shouldResize, + prevReflowState, + immediateExitContainer, + mousePosition, + !shouldSkipContainerReflow || shouldReflowDropTargets.current, ); - if (shouldSkipContainerReflow && collidingSpaceMap) { + prevPositions.current = newPositions; + prevCollidingSpaces.current = collidingSpaceMap as WidgetCollidingSpaceMap; + prevSecondOrderCollisionMap.current = secondOrderCollisionMap || {}; + + //store exit container and mouse pointer if we are not reflowing drop targets and it doesn't already have a value + if (!shouldReflowDropTargets.current && !exitContainer.current) { + exitContainer.current = immediateExitContainer; + mousePointerAtContainerExit.current = mousePosition; + } + + let correctedMovementMap = movementMap || {}; + + if (stopMoveAfterLimit) { + correctedMovementMap = getLimitedMovementMap( + movementMap, + prevMovementMap.current, + { canHorizontalMove: true, canVerticalMove: true }, + ); + } + + prevMovementMap.current = correctedMovementMap; const collidingSpaces = [ - ...Object.values(collidingSpaceMap.horizontal), - ...Object.values(collidingSpaceMap.vertical), + ...Object.values(collidingSpaceMap?.horizontal || []), + ...Object.values(collidingSpaceMap?.vertical || []), ] as WidgetCollidingSpace[]; - for (const collidingSpace of collidingSpaces) { - if ( - checkIsDropTarget(collidingSpace.type) && - mousePosition && - areIntersecting(mousePosition, collidingSpace) + // Logic for container jump + if (shouldSkipContainerReflow) { + if (shouldRegisterContainerTimeout) { + // register a timeout method to trigger reflow if widget is not moved and is colliding with Droptargets + timeOutFunction.current = setTimeout(() => { + //call reflow again + const { + collidingSpaceMap, + movementLimitMap, + movementMap, + secondOrderCollisionMap, + } = reflow( + newPositions, + OGPositions, + widgetSpaces, + direction, + gridProps, + forceDirection, + shouldResize, + prevReflowState, + exitContainer.current, + mousePointerAtContainerExit.current || mousePosition, + true, + true, + ); + exitContainer.current = undefined; + mousePointerAtContainerExit.current = undefined; + + //if the result causes an undroppable state return + if (willItCauseUndroppableState(movementLimitMap)) return; + + // trigger reflow action with result of reflow algorithm + if (!isEmpty(movementMap)) { + shouldReflowDropTargets.current = true; + isReflowing.current = true; + dispatch(reflowMoveAction(movementMap || {})); + //trigger callback if reflow action is called + reflowAfterTimeoutCallback && + reflowAfterTimeoutCallback({ + movementMap: prevMovementMap.current, + spacePositionMap: undefined, + }); + + prevCollidingSpaces.current = collidingSpaceMap as WidgetCollidingSpaceMap; + prevSecondOrderCollisionMap.current = + secondOrderCollisionMap || {}; + prevMovementMap.current = movementMap || {}; + } else if (isReflowing.current) { + isReflowing.current = false; + throttledDispatch.cancel(); + dispatch(stopReflowAction()); + shouldReflowDropTargets.current = false; + } + }, 500); + } // This checks if colliding space does not contain any drop targets + else if ( + !collidingSpaces.some( + (collidingSpaces) => collidingSpaces.isDropTarget, + ) ) { - isIdealToJumpContainer = true; - correctedMovementMap = {}; + shouldReflowDropTargets.current = false; + mousePointerAtContainerExit.current = undefined; + exitContainer.current = undefined; } } - } - prevMovementMap.current = correctedMovementMap; + //Trigger reflow action + if (!isEmpty(correctedMovementMap)) { + isReflowing.current = true; + if (forceDirection) dispatch(reflowMoveAction(correctedMovementMap)); + else throttledDispatch(reflowMoveAction(correctedMovementMap)); + } else if (isReflowing.current) { + isReflowing.current = false; + throttledDispatch.cancel(); + dispatch(stopReflowAction()); + shouldReflowDropTargets.current = false; + } - if (!isEmpty(correctedMovementMap)) { - isReflowing.current = true; - if (forceDirection) dispatch(reflowMoveAction(correctedMovementMap)); - else throttledDispatch(reflowMoveAction(correctedMovementMap)); - } else if (isReflowing.current) { - isReflowing.current = false; - throttledDispatch.cancel(); - dispatch(stopReflowAction()); - } + //calculate bottom row + const bottomMostRow = getBottomRowAfterReflow( + movementMap, + getBottomMostRow(newPositions), + widgetSpaces, + gridProps, + ); - const bottomMostRow = getBottomRowAfterReflow( - movementMap, - getBottomMostRow(newPositions), - widgetSpaces, - gridProps, - ); - - return { - movementLimitMap, - movementMap: correctedMovementMap, - bottomMostRow, - isIdealToJumpContainer, - }; + return { + movementLimitMap, + movementMap: correctedMovementMap, + bottomMostRow, + spacePositionMap, + }; + }, + //reset Reflow parameters when this is called, usually while resetting canvas + resetReflow: () => { + clearTimeout(timeOutFunction.current); + shouldReflowDropTargets.current = false; + mousePointerAtContainerExit.current = undefined; + exitContainer.current = undefined; + }, }; };