PromucFlow_constructor/app/client/src/utils/hooks/useBlocksToBeDraggedOnCanvas.ts
Ashok Kumar M 0149085bf8
feat: Reflow and Resize while Dragging and Resizing widgets. (#9054)
* resize n reflow rough cut

* removing warnings

* relatively stable changes

* minor bug fix

* reflow relative collision

* working dp cut

* fix for reflow of widgets closer next to each other

* disabling scroll

* Drag with reflow

* reflow fix

* overlap and retracing fix

* On Drop updates.

* bug when no displacement but resize update.

* temp fix for new widget addition.

* reflow bug fixes

* new widget addition bug.

* stop reflow on leave.

* fix corner case overlap

* update bottom row when reflowed widgets go beyond bottom boundary.

* capture mouse positions on enter

* enable container jumps with faster mouse movements.

* reflow only for snap changes.

* restructured reflow Algorithm

* collision check and bug fixes

* undo redo fix for new widget drop

* resizable fix snapRows fix

* directional stability

* self collision fix

* first round of perf fixes

* update bottom row while resizing and resize-reflowing

* performance fix and overlapping fix

* Remove eslint warning

* remove eslint warning

* eslint warning

* can reflowed Drop Indication Stability

* container jumps and force direction on entering canvas

* fixing scroll on resize jitters.

* reflow when jumping into container.

* reflow ux fixes while leaving container

* resizing fixes.

* fixes for edge move.

* restrict container jumps into reflowed containers.

* container jump direction reflow

* checkbox dimensions fix.

* Excess bottom rows not lost post dragging or resizing widgets.

* fixing the after drop css glitch.

* double first move trigger bug fix.

* stop reflow only if reflowing

* stabilize container exit directions

* using acceleration and speed instead of movement covered to restrict reflow.

* fixing modal drops.

* remove warnings.

* reflow resize styles

* moving acceleration and movement logic to a monitoring effect.

* adding beta flag for reflow.

* fixing jest tests

* Adding analytics to beta flag toggle.

* Adding placeholder for reflow beta screens.

* fixing initial load's screen

* few more crashes.

* force close onboarding for the session.

* fixing bugs in reset canvas.

* Beta flag bug fixes.

* fixing bugs.

* restrict reflow screens during onboarding.

* disabling reflow screens in tests.

* code review comments.

* fixing store based specs.

* fixing cypress failures.

* fixing specs.

* code cleanup

* reverting yarn lock changes

* removing onboarding screens.

* more cleanup and function descriptors

* keeping reflow under the hood.

Co-authored-by: rahulramesha <rahul@appsmith.com>
Co-authored-by: Arpit Mohan <arpit@appsmith.com>
2022-01-13 18:51:57 +05:30

403 lines
13 KiB
TypeScript

import { useContext, useEffect, useRef } from "react";
import {
CONTAINER_GRID_PADDING,
GridDefaults,
MAIN_CONTAINER_WIDGET_ID,
} from "constants/WidgetConstants";
import { useSelector } from "store";
import { AppState } from "reducers";
import { getSelectedWidgets } from "selectors/ui";
import { getOccupiedSpaces } from "selectors/editorSelectors";
import { getTableFilterState } from "selectors/tableFilterSelectors";
import { OccupiedSpace } from "constants/CanvasEditorConstants";
import { getDragDetails, getWidgetByID, getWidgets } from "sagas/selectors";
import {
getDropZoneOffsets,
WidgetOperationParams,
widgetOperationParams,
} from "utils/WidgetPropsUtils";
import { DropTargetContext } from "components/editorComponents/DropTargetComponent";
import { XYCord } from "utils/hooks/useCanvasDragging";
import { isEmpty, isEqual } from "lodash";
import { CanvasDraggingArenaProps } from "pages/common/CanvasDraggingArena";
import { useDispatch } from "react-redux";
import { ReduxActionTypes } from "constants/ReduxActionConstants";
import { EditorContext } from "components/editorComponents/EditorContextProvider";
import { useWidgetSelection } from "./useWidgetSelection";
import AnalyticsUtil from "utils/AnalyticsUtil";
import { snapToGrid } from "utils/helpers";
import { stopReflowAction } from "actions/reflowActions";
import { DragDetails } from "reducers/uiReducers/dragResizeReducer";
import { getIsReflowing } from "selectors/widgetReflowSelectors";
export interface WidgetDraggingUpdateParams extends WidgetDraggingBlock {
updateWidgetParams: WidgetOperationParams;
}
export type WidgetDraggingBlock = {
left: number;
top: number;
width: number;
height: number;
columnWidth: number;
rowHeight: number;
widgetId: string;
isNotColliding: boolean;
detachFromLayout?: boolean;
};
export const useBlocksToBeDraggedOnCanvas = ({
noPad,
snapColumnSpace,
snapRows,
snapRowSpace,
widgetId,
}: CanvasDraggingArenaProps) => {
const dispatch = useDispatch();
const { selectWidget } = useWidgetSelection();
const containerPadding = noPad ? 0 : CONTAINER_GRID_PADDING;
const lastDraggedCanvas = useRef<string | undefined>(undefined);
// check any table filter is open or not
// if filter pane open, close before property pane open
const tableFilterPaneState = useSelector(getTableFilterState);
// dragDetails contains of info needed for a container jump:
// which parent the dragging widget belongs,
// which canvas is active(being dragged on),
// which widget is grabbed while dragging started,
// relative position of mouse pointer wrt to the last grabbed widget.
const dragDetails: DragDetails = useSelector(getDragDetails);
const draggingCanvas = useSelector(
getWidgetByID(dragDetails.draggedOn || ""),
);
const isReflowing = useSelector(getIsReflowing);
useEffect(() => {
if (
dragDetails.draggedOn &&
draggingCanvas &&
draggingCanvas.parentId &&
![widgetId, MAIN_CONTAINER_WIDGET_ID].includes(dragDetails.draggedOn)
) {
lastDraggedCanvas.current = draggingCanvas.parentId;
}
}, [dragDetails.draggedOn]);
const defaultHandlePositions = {
top: 20,
left: 20,
};
const {
draggingGroupCenter: dragCenter,
dragGroupActualParent: dragParent,
newWidget,
} = dragDetails;
const isResizing = useSelector(
(state: AppState) => state.ui.widgetDragResize.isResizing,
);
const selectedWidgets = useSelector(getSelectedWidgets);
const occupiedSpaces = useSelector(getOccupiedSpaces, isEqual) || {};
const isNewWidget = !!newWidget && !dragParent;
const childrenOccupiedSpaces: OccupiedSpace[] =
(dragParent && occupiedSpaces[dragParent]) || [];
const isDragging = useSelector(
(state: AppState) => state.ui.widgetDragResize.isDragging,
);
const { updateWidget } = useContext(EditorContext);
const allWidgets = useSelector(getWidgets);
const getDragCenterSpace = () => {
if (dragCenter && dragCenter.widgetId) {
// Dragging by widget
return (
childrenOccupiedSpaces.find(
(each) => each.id === dragCenter.widgetId,
) || {}
);
} else if (
dragCenter &&
Number.isInteger(dragCenter.top) &&
Number.isInteger(dragCenter.left)
) {
// Dragging by Widget selection box
return dragCenter;
} else {
return {};
}
};
const getSnappedXY = (
parentColumnWidth: number,
parentRowHeight: number,
currentOffset: XYCord,
parentOffset: XYCord,
) => {
// TODO(abhinav): There is a simpler math to use.
const [leftColumn, topRow] = snapToGrid(
parentColumnWidth,
parentRowHeight,
currentOffset.x - parentOffset.x,
currentOffset.y - parentOffset.y,
);
return {
X: leftColumn * parentColumnWidth,
Y: topRow * parentRowHeight,
};
};
const getBlocksToDraw = (): WidgetDraggingBlock[] => {
if (isNewWidget) {
return [
{
top: 0,
left: 0,
width: newWidget.columns * snapColumnSpace,
height: newWidget.rows * snapRowSpace,
columnWidth: newWidget.columns,
rowHeight: newWidget.rows,
widgetId: newWidget.widgetId,
detachFromLayout: newWidget.detachFromLayout,
isNotColliding: true,
},
];
} else {
return childrenOccupiedSpaces
.filter((each) => selectedWidgets.includes(each.id))
.map((each) => ({
top: each.top * snapRowSpace + containerPadding,
left: each.left * snapColumnSpace + containerPadding,
width: (each.right - each.left) * snapColumnSpace,
height: (each.bottom - each.top) * snapRowSpace,
columnWidth: each.right - each.left,
rowHeight: each.bottom - each.top,
widgetId: each.id,
isNotColliding: true,
}));
}
};
const blocksToDraw = getBlocksToDraw();
const dragCenterSpace: any = getDragCenterSpace();
const filteredChildOccupiedSpaces = childrenOccupiedSpaces.filter(
(each) => !selectedWidgets.includes(each.id),
);
const { updateDropTargetRows } = useContext(DropTargetContext);
const stopReflowing = () => {
if (isReflowing) dispatch(stopReflowAction());
};
const onDrop = (
drawingBlocks: WidgetDraggingBlock[],
reflowedPositionsUpdatesWidgets: OccupiedSpace[],
) => {
const reflowedBlocks: WidgetDraggingBlock[] = reflowedPositionsUpdatesWidgets.map(
(each) => {
const widget = allWidgets[each.id];
return {
left: each.left * snapColumnSpace,
top: each.top * snapRowSpace,
width: (each.right - each.left) * snapColumnSpace,
height: (each.bottom - each.top) * snapRowSpace,
columnWidth: snapColumnSpace,
rowHeight: snapRowSpace,
widgetId: widget.widgetId,
isNotColliding: true,
detachFromLayout: widget.detachFromLayout,
};
},
);
const reflowedIds = reflowedPositionsUpdatesWidgets.map((each) => each.id);
const allUpdatedBlocks = [...drawingBlocks, ...reflowedBlocks];
const cannotDrop = allUpdatedBlocks.some((each) => {
return !each.isNotColliding;
});
if (!cannotDrop) {
const draggedBlocksToUpdate = allUpdatedBlocks
.sort(
(each1, each2) =>
each1.top + each1.height - (each2.top + each2.height),
)
.map((each) => {
const widget =
newWidget && !reflowedIds.includes(each.widgetId)
? newWidget
: allWidgets[each.widgetId];
const updateWidgetParams = widgetOperationParams(
widget,
{ x: each.left, y: each.top },
{ x: 0, y: 0 },
snapColumnSpace,
snapRowSpace,
widget.detachFromLayout ? MAIN_CONTAINER_WIDGET_ID : widgetId,
{ width: each.width, height: each.height },
);
return {
...each,
updateWidgetParams,
};
});
dispatchDrop(draggedBlocksToUpdate);
}
};
const dispatchDrop = (
draggedBlocksToUpdate: WidgetDraggingUpdateParams[],
) => {
if (isNewWidget) {
const newWidget = draggedBlocksToUpdate.find(
(each) => each.updateWidgetParams.operation === "ADD_CHILD",
);
const movedWidgets = draggedBlocksToUpdate.filter(
(each) => each.updateWidgetParams.operation !== "ADD_CHILD",
);
if (newWidget) {
addNewWidget(newWidget, movedWidgets);
}
} else {
bulkMoveWidgets(draggedBlocksToUpdate);
}
};
const bulkMoveWidgets = (
draggedBlocksToUpdate: WidgetDraggingUpdateParams[],
) => {
dispatch({
type: ReduxActionTypes.WIDGETS_MOVE,
payload: {
draggedBlocksToUpdate,
canvasId: widgetId,
},
});
};
const addNewWidget = (
newWidget: WidgetDraggingUpdateParams,
movedWidgets: WidgetDraggingUpdateParams[],
) => {
const { updateWidgetParams } = newWidget;
if (movedWidgets && movedWidgets.length) {
dispatch({
type: ReduxActionTypes.WIDGETS_ADD_CHILD_AND_MOVE,
payload: {
newWidget: updateWidgetParams.payload,
draggedBlocksToUpdate: movedWidgets,
canvasId: widgetId,
},
});
} else {
updateWidget &&
updateWidget(
updateWidgetParams.operation,
updateWidgetParams.widgetId,
updateWidgetParams.payload,
);
}
// close filter pane if any open, before property pane open
tableFilterPaneState.isVisible &&
dispatch({
type: ReduxActionTypes.HIDE_TABLE_FILTER_PANE,
payload: { widgetId: tableFilterPaneState.widgetId },
});
// Adding setTimeOut to allow property pane to open only after widget is loaded.
// Not needed for most widgets except for Modal Widget.
setTimeout(() => {
selectWidget(updateWidgetParams.payload.newWidgetId);
}, 100);
AnalyticsUtil.logEvent("WIDGET_CARD_DRAG", {
widgetType: dragDetails.newWidget.type,
widgetName: dragDetails.newWidget.widgetCardName,
didDrop: true,
});
};
const updateRelativeRows = (
drawingBlocks: WidgetDraggingBlock[],
rows: number,
) => {
if (drawingBlocks.length) {
const sortedByTopBlocks = drawingBlocks.sort(
(each1, each2) => each2.top + each2.height - (each1.top + each1.height),
);
const bottomMostBlock = sortedByTopBlocks[0];
const [, top] = getDropZoneOffsets(
snapColumnSpace,
snapRowSpace,
{
x: bottomMostBlock.left,
y: bottomMostBlock.top + bottomMostBlock.height,
} as XYCord,
{ x: 0, y: 0 },
);
return updateBottomRow(top, rows);
}
};
const updateBottomRow = (bottom: number, rows: number) => {
if (bottom > rows - GridDefaults.CANVAS_EXTENSION_OFFSET) {
return updateDropTargetRows && updateDropTargetRows(widgetId, bottom);
}
};
const rowRef = useRef(snapRows);
useEffect(() => {
rowRef.current = snapRows;
}, [snapRows, isDragging]);
const isChildOfCanvas = dragParent === widgetId;
const isCurrentDraggedCanvas = dragDetails.draggedOn === widgetId;
const isNewWidgetInitialTargetCanvas =
isNewWidget && widgetId === MAIN_CONTAINER_WIDGET_ID;
const parentDiff = isDragging
? {
top:
!isChildOfCanvas && !isEmpty(dragCenterSpace)
? dragCenterSpace.top * snapRowSpace + containerPadding
: containerPadding,
left:
!isChildOfCanvas && !isEmpty(dragCenterSpace)
? dragCenterSpace.left * snapColumnSpace + containerPadding
: containerPadding,
}
: {
top: 0,
left: 0,
};
const relativeStartPoints =
isDragging && !isEmpty(dragCenterSpace)
? {
left:
((isChildOfCanvas ? dragCenterSpace.left : 0) +
dragDetails.dragOffset.left) *
snapColumnSpace +
2 * containerPadding,
top:
((isChildOfCanvas ? dragCenterSpace.top : 0) +
dragDetails.dragOffset.top) *
snapRowSpace +
2 * containerPadding,
}
: defaultHandlePositions;
const currentOccSpaces = occupiedSpaces[widgetId] || [];
const occSpaces: OccupiedSpace[] = isChildOfCanvas
? filteredChildOccupiedSpaces
: currentOccSpaces;
return {
blocksToDraw,
defaultHandlePositions,
getSnappedXY,
isChildOfCanvas,
isCurrentDraggedCanvas,
isDragging,
isNewWidget,
isNewWidgetInitialTargetCanvas,
isResizing,
lastDraggedCanvas,
occSpaces,
onDrop,
parentDiff,
relativeStartPoints,
rowRef,
stopReflowing,
updateBottomRow,
updateRelativeRows,
widgetOccupiedSpace: childrenOccupiedSpaces.filter(
(each) => each.id === dragCenter?.widgetId,
)[0],
};
};