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
This commit is contained in:
rahulramesha 2023-01-06 22:27:40 +05:30 committed by GitHub
parent 00019fc0a7
commit 717b2c1610
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 2187 additions and 682 deletions

View File

@ -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(

View File

@ -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();

View File

@ -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");
});

View File

@ -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,

View File

@ -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 {

View File

@ -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 };

View File

@ -15,6 +15,7 @@ export type WidgetSpace = {
id: string;
type: string;
parentId?: string;
isDropTarget?: boolean;
fixedHeight?: number;
};

View File

@ -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";

View File

@ -1,20 +0,0 @@
//class to maintain containerJump metrics across containers.
export default class ContainerJumpMetrics<T> {
private containerJumpValues: T = {} as T;
public setMetrics(args: T) {
this.containerJumpValues = {
...args,
};
}
public getMetrics() {
return {
...this.containerJumpValues,
};
}
public clearMetrics() {
this.containerJumpValues = {} as T;
}
}

View File

@ -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);
});
});
});

View File

@ -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;
};

View File

@ -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,

View File

@ -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<HTMLDivElement>,
@ -57,7 +45,6 @@ export const useCanvasDragging = (
widgetId,
}: CanvasDraggingArenaProps,
) => {
const canvasZoomLevel = useSelector(getZoomLevel);
const currentDirection = useRef<ReflowDirection>(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<ReflowInterface>();
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();

View File

@ -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<HTMLDivElement>,
stickyCanvasRef: React.RefObject<HTMLCanvasElement>,
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;
};

View File

@ -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,

View File

@ -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,
};
}

View File

@ -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 = {

View File

@ -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]: {

View File

@ -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;
}

View File

@ -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,
});

View File

@ -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);
});
});

View File

@ -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,

View File

@ -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,

View File

@ -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]),
);

View File

@ -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;

View File

@ -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<OccupiedSpace[] | undefined>(OGPositions);
const prevCollidingSpaces = useRef<WidgetCollidingSpaceMap>();
const prevMovementMap = useRef<ReflowedSpaceMap>({});
const prevSecondOrderCollisionMap = useRef<SecondOrderCollisionMap>({});
// Indicates if the Containers should be reflowed
const shouldReflowDropTargets = useRef<boolean>(false);
// ref of timeout method
const timeOutFunction = useRef<any>();
// store exit container and mouse position at exit, so that it can be used during timeout
const exitContainer = useRef<string | undefined>(undefined);
const mousePointerAtContainerExit = useRef<OccupiedSpace | undefined>(
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;
},
};
};