PromucFlow_constructor/app/client/src/utils/hooks/useCanvasDragging.ts
Ashok Kumar M f19ebbafe9
[Feature] Widget grouping - Allow Drag and Drop of multiple widgets. (#5389)
* dip

* dip

* scroll

* fixes

* dip

* dip

* dip

* dip

* dip

* dip

* dip

* dip

* dip

* dip

* dip

* dip

* dip

* dip

* dip

* dip

* dip

* dip

* solve for canvas glitches

* dip

* dip

* dip

* adjust scroll speed

* dip

* dip(code clean up)

* dip

* dip

* ---dip

* dip

* dip

* dip

* middle ware for dropping multiple widgets.

* adding scroll to drag for canvas selection.

* fixing drag disabled and modal widget(detach from layout) drops

* firefox and safari fixes

* rebase conflicts.

* fixing broken specs.

* fixing specs and adding jest tests.

* show border and disable resize when multiple widgets are selected.

* selection box grab cursor

* merge conflicts.

* code clean up

* fixing specs.

* fixed a bug and failed specs.

* fixing rerenders.

* code clean up

* code review comments

* always have the drag point inside the widget.

* fetching snap spaces instead of calculating.

* remove widget_move action

* fixing bugs with add widget parent height updation.

* fixing specs.

* List widget conflict fixes.

* fixing canvas drop persistence.

* Adding click to drag for modals and fixing few issues.
2021-08-12 11:15:38 +05:30

431 lines
14 KiB
TypeScript

import {
CONTAINER_GRID_PADDING,
GridDefaults,
} from "constants/WidgetConstants";
import { debounce, throttle } from "lodash";
import { CanvasDraggingArenaProps } from "pages/common/CanvasDraggingArena";
import { useEffect } from "react";
import { getNearestParentCanvas } from "utils/generators";
import { noCollision } from "utils/WidgetPropsUtils";
import { useWidgetDragResize } from "./dragResizeHooks";
import {
useBlocksToBeDraggedOnCanvas,
WidgetDraggingBlock,
} from "./useBlocksToBeDraggedOnCanvas";
import { useCanvasDragToScroll } from "./useCanvasDragToScroll";
export const useCanvasDragging = (
canvasRef: React.RefObject<HTMLDivElement>,
canvasDrawRef: React.RefObject<HTMLCanvasElement>,
{
canExtend,
dropDisabled,
noPad,
snapColumnSpace,
snapRows,
snapRowSpace,
widgetId,
}: CanvasDraggingArenaProps,
) => {
const { devicePixelRatio: scale = 1 } = window;
const {
blocksToDraw,
defaultHandlePositions,
getSnappedXY,
isChildOfCanvas,
isCurrentDraggedCanvas,
isDragging,
isNewWidget,
isNewWidgetInitialTargetCanvas,
isResizing,
occSpaces,
onDrop,
parentDiff,
relativeStartPoints,
rowRef,
updateRows,
} = useBlocksToBeDraggedOnCanvas({
canExtend,
noPad,
snapColumnSpace,
snapRows,
snapRowSpace,
widgetId,
});
const {
setDraggingCanvas,
setDraggingNewWidget,
setDraggingState,
} = useWidgetDragResize();
const updateCanvasStyles = () => {
const parentCanvas: Element | null = getNearestParentCanvas(
canvasRef.current,
);
if (parentCanvas && canvasDrawRef.current && canvasRef.current) {
const { height } = parentCanvas.getBoundingClientRect();
const { width } = canvasRef.current.getBoundingClientRect();
canvasDrawRef.current.style.width = width + "px";
canvasDrawRef.current.style.position = canExtend ? "absolute" : "sticky";
canvasDrawRef.current.style.left = "0px";
canvasDrawRef.current.style.height = height + "px";
canvasDrawRef.current.style.top =
(canExtend ? parentCanvas.scrollTop : 0) + "px";
}
};
const canScroll = useCanvasDragToScroll(
canvasRef,
isCurrentDraggedCanvas,
isDragging,
snapRows,
canExtend,
);
useEffect(() => {
if (
canvasRef.current &&
!isResizing &&
isDragging &&
blocksToDraw.length > 0
) {
const scrollParent: Element | null = getNearestParentCanvas(
canvasRef.current,
);
let canvasIsDragging = false;
let isUpdatingRows = false;
let currentRectanglesToDraw: WidgetDraggingBlock[] = [];
const scrollObj: any = {};
const resetCanvasState = () => {
if (canvasDrawRef.current && canvasRef.current) {
const canvasCtx: any = canvasDrawRef.current.getContext("2d");
canvasCtx.clearRect(
0,
0,
canvasDrawRef.current.width,
canvasDrawRef.current.height,
);
canvasRef.current.style.zIndex = "";
canvasIsDragging = false;
}
};
if (isDragging) {
const startPoints = defaultHandlePositions;
const onMouseUp = () => {
if (isDragging && canvasIsDragging) {
onDrop(currentRectanglesToDraw);
}
startPoints.top = defaultHandlePositions.top;
startPoints.left = defaultHandlePositions.left;
resetCanvasState();
if (isCurrentDraggedCanvas) {
if (isNewWidget) {
setDraggingNewWidget(false, undefined);
} else {
setDraggingState({
isDragging: false,
});
}
setDraggingCanvas();
}
};
const onFirstMoveOnCanvas = (e: any) => {
if (
!isResizing &&
isDragging &&
!canvasIsDragging &&
canvasRef.current
) {
if (!isNewWidget) {
startPoints.left =
relativeStartPoints.left || defaultHandlePositions.left;
startPoints.top =
relativeStartPoints.top || defaultHandlePositions.top;
}
if (!isCurrentDraggedCanvas) {
// we can just use canvasIsDragging but this is needed to render the relative DragLayerComponent
setDraggingCanvas(widgetId);
}
canvasIsDragging = true;
canvasRef.current.style.zIndex = "2";
onMouseMove(e);
}
};
const onMouseMove = (e: any) => {
if (isDragging && canvasIsDragging && canvasRef.current) {
const delta = {
left: e.offsetX - startPoints.left - parentDiff.left,
top: e.offsetY - startPoints.top - parentDiff.top,
};
const drawingBlocks = blocksToDraw.map((each) => ({
...each,
left: each.left + delta.left,
top: each.top + delta.top,
}));
const newRows = updateRows(drawingBlocks, rowRef.current);
const rowDelta = newRows ? newRows - rowRef.current : 0;
rowRef.current = newRows ? newRows : rowRef.current;
currentRectanglesToDraw = drawingBlocks.map((each) => ({
...each,
isNotColliding:
!dropDisabled &&
noCollision(
{ x: each.left, y: each.top },
snapColumnSpace,
snapRowSpace,
{ x: 0, y: 0 },
each.columnWidth,
each.rowHeight,
each.widgetId,
occSpaces,
rowRef.current,
GridDefaults.DEFAULT_GRID_COLUMNS,
each.detachFromLayout,
),
}));
if (rowDelta && canvasRef.current) {
isUpdatingRows = true;
canScroll.current = false;
renderNewRows(delta);
} else if (!isUpdatingRows) {
renderBlocks();
}
scrollObj.lastMouseMoveEvent = {
offsetX: e.offsetX,
offsetY: e.offsetY,
};
scrollObj.lastScrollTop = scrollParent?.scrollTop;
scrollObj.lastScrollHeight = scrollParent?.scrollHeight;
} else {
onFirstMoveOnCanvas(e);
}
};
const renderNewRows = debounce((delta) => {
isUpdatingRows = true;
if (canvasRef.current && canvasDrawRef.current) {
const canvasCtx: any = canvasDrawRef.current.getContext("2d");
currentRectanglesToDraw = blocksToDraw.map((each) => {
return {
...each,
left: each.left + delta.left,
top: each.top + delta.top,
isNotColliding:
!dropDisabled &&
noCollision(
{ x: each.left + delta.left, y: each.top + delta.top },
snapColumnSpace,
snapRowSpace,
{ x: 0, y: 0 },
each.columnWidth,
each.rowHeight,
each.widgetId,
occSpaces,
rowRef.current,
GridDefaults.DEFAULT_GRID_COLUMNS,
each.detachFromLayout,
),
};
});
canvasCtx.save();
canvasCtx.scale(scale, scale);
canvasCtx.clearRect(
0,
0,
canvasDrawRef.current.width,
canvasDrawRef.current.height,
);
canvasCtx.restore();
renderBlocks();
canScroll.current = false;
endRenderRows.cancel();
endRenderRows();
}
});
const endRenderRows = throttle(
() => {
canScroll.current = true;
},
50,
{
leading: false,
trailing: true,
},
);
const renderBlocks = () => {
if (
canvasRef.current &&
isCurrentDraggedCanvas &&
canvasIsDragging &&
canvasDrawRef.current
) {
const canvasCtx: any = canvasDrawRef.current.getContext("2d");
canvasCtx.save();
canvasCtx.clearRect(
0,
0,
canvasDrawRef.current.width,
canvasDrawRef.current.height,
);
isUpdatingRows = false;
if (canvasIsDragging) {
currentRectanglesToDraw.forEach((each) => {
drawBlockOnCanvas(each);
});
}
canvasCtx.restore();
}
};
const drawBlockOnCanvas = (blockDimensions: WidgetDraggingBlock) => {
if (
canvasDrawRef.current &&
canvasRef.current &&
scrollParent &&
isCurrentDraggedCanvas &&
canvasIsDragging
) {
const canvasCtx: any = canvasDrawRef.current.getContext("2d");
const topOffset = canExtend ? scrollParent.scrollTop : 0;
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 + (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 + strokeWidth + (noPad ? 0 : CONTAINER_GRID_PADDING),
snappedXY.Y -
topOffset +
strokeWidth +
(noPad ? 0 : CONTAINER_GRID_PADDING),
blockDimensions.width - strokeWidth,
blockDimensions.height - strokeWidth,
);
}
};
const onScroll = () => {
const {
lastMouseMoveEvent,
lastScrollHeight,
lastScrollTop,
} = scrollObj;
if (
lastMouseMoveEvent &&
Number.isInteger(lastScrollHeight) &&
Number.isInteger(lastScrollTop) &&
scrollParent &&
canScroll.current
) {
const delta =
scrollParent?.scrollHeight +
scrollParent?.scrollTop -
(lastScrollHeight + lastScrollTop);
onMouseMove({
offsetX: lastMouseMoveEvent.offsetX,
offsetY: lastMouseMoveEvent.offsetY + delta,
});
}
};
const initializeListeners = () => {
canvasRef.current?.addEventListener("mousemove", onMouseMove, false);
canvasRef.current?.addEventListener("mouseup", onMouseUp, false);
scrollParent?.addEventListener("scroll", updateCanvasStyles, false);
scrollParent?.addEventListener("scroll", onScroll, false);
canvasRef.current?.addEventListener(
"mouseover",
onFirstMoveOnCanvas,
false,
);
canvasRef.current?.addEventListener(
"mouseout",
resetCanvasState,
false,
);
canvasRef.current?.addEventListener(
"mouseleave",
resetCanvasState,
false,
);
document.body.addEventListener("mouseup", onMouseUp, false);
window.addEventListener("mouseup", onMouseUp, false);
};
const startDragging = () => {
if (canvasRef.current && canvasDrawRef.current && scrollParent) {
const { height } = scrollParent.getBoundingClientRect();
const { width } = canvasRef.current.getBoundingClientRect();
const canvasCtx: any = canvasDrawRef.current.getContext("2d");
canvasDrawRef.current.width = width * scale;
canvasDrawRef.current.height = height * scale;
canvasCtx.scale(scale, scale);
updateCanvasStyles();
initializeListeners();
if (
(isChildOfCanvas || isNewWidgetInitialTargetCanvas) &&
canvasRef.current
) {
canvasRef.current.style.zIndex = "2";
}
}
};
startDragging();
return () => {
canvasRef.current?.removeEventListener("mousemove", onMouseMove);
canvasRef.current?.removeEventListener("mouseup", onMouseUp);
scrollParent?.removeEventListener("scroll", updateCanvasStyles);
scrollParent?.removeEventListener("scroll", onScroll);
canvasRef.current?.removeEventListener(
"mouseover",
onFirstMoveOnCanvas,
);
canvasRef.current?.removeEventListener("mouseout", resetCanvasState);
canvasRef.current?.removeEventListener(
"mouseleave",
resetCanvasState,
);
document.body.removeEventListener("mouseup", onMouseUp);
window.removeEventListener("mouseup", onMouseUp);
};
} else {
resetCanvasState();
}
}
}, [isDragging, isResizing, blocksToDraw, snapRows, canExtend]);
return {
showCanvas: isDragging && !isResizing,
};
};