586 lines
17 KiB
TypeScript
586 lines
17 KiB
TypeScript
import type {
|
|
OccupiedSpace,
|
|
WidgetSpace,
|
|
} from "constants/CanvasEditorConstants";
|
|
import { GridDefaults } from "constants/WidgetConstants";
|
|
import { isEmpty } from "lodash";
|
|
import type { CanvasWidgetsReduxState } from "reducers/entityReducers/canvasWidgetsReducer";
|
|
import type { DraggingGroupCenter } from "reducers/uiReducers/dragResizeReducer";
|
|
import type {
|
|
MovementLimitMap,
|
|
ReflowedSpaceMap,
|
|
SpaceMap,
|
|
} from "reflow/reflowTypes";
|
|
import {
|
|
HORIZONTAL_RESIZE_MIN_LIMIT,
|
|
ReflowDirection,
|
|
VERTICAL_RESIZE_MIN_LIMIT,
|
|
} from "reflow/reflowTypes";
|
|
import type { WidgetType } from "WidgetProvider/factory";
|
|
import {
|
|
getDraggingSpacesFromBlocks,
|
|
getDropZoneOffsets,
|
|
noCollision,
|
|
} from "utils/WidgetPropsUtils";
|
|
import type { WidgetDraggingBlock, XYCord } from "../canvasArenas/ArenaTypes";
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
|
|
interface NewWidgetBlock {
|
|
columns: number;
|
|
rows: number;
|
|
widgetId: string;
|
|
detachFromLayout: boolean;
|
|
isDynamicHeight: boolean;
|
|
type: WidgetType;
|
|
}
|
|
|
|
/**
|
|
* This method returns blocks and dragging spaces of the widgets being dragged on canvas..
|
|
* @param newWidget details about teh new widget that is being dragged on canvas
|
|
* @param allWidgets widgets properties of all the widgets on the page
|
|
* @param isNewWidget indicates if the widget is a new widget
|
|
* @param snapColumnSpace distance between each grid columns in pixels
|
|
* @param snapRowSpace distance between each grid rows in pixels
|
|
* @param childrenOccupiedSpaces array of grid positions of all the widgets on canvas
|
|
* @param selectedWidgets array of selected widget ids
|
|
* @param containerPadding padding in pixels
|
|
* @returns blocksToDraw and draggingSpaces,
|
|
* blocksToDraw contains information regarding the widget and positions in pixels
|
|
* draggingSpaces only contains the position of the widget in grid columns and rows
|
|
*/
|
|
export const getBlocksToDraw = (
|
|
newWidget: NewWidgetBlock,
|
|
allWidgets: CanvasWidgetsReduxState,
|
|
isNewWidget: boolean,
|
|
snapColumnSpace: number,
|
|
snapRowSpace: number,
|
|
childrenOccupiedSpaces: WidgetSpace[],
|
|
selectedWidgets: string[],
|
|
containerPadding: number,
|
|
): {
|
|
blocksToDraw: WidgetDraggingBlock[];
|
|
draggingSpaces: OccupiedSpace[];
|
|
} => {
|
|
if (isNewWidget) {
|
|
return {
|
|
blocksToDraw: [
|
|
{
|
|
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,
|
|
fixedHeight: newWidget.isDynamicHeight
|
|
? newWidget.rows * snapRowSpace
|
|
: undefined,
|
|
type: newWidget.type,
|
|
},
|
|
],
|
|
draggingSpaces: [
|
|
{
|
|
top: 0,
|
|
left: 0,
|
|
right: newWidget.columns,
|
|
bottom: newWidget.rows,
|
|
id: newWidget.widgetId,
|
|
},
|
|
],
|
|
};
|
|
} else {
|
|
const draggingSpaces = childrenOccupiedSpaces.filter((each) =>
|
|
selectedWidgets.includes(each.id),
|
|
);
|
|
return {
|
|
draggingSpaces,
|
|
blocksToDraw: draggingSpaces.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,
|
|
fixedHeight: each.fixedHeight,
|
|
type: allWidgets[each.id].type,
|
|
})),
|
|
};
|
|
}
|
|
};
|
|
|
|
/**
|
|
* This method returns the bound updateRelativeRows method with the arguments bounded
|
|
* @param updateDropTargetRows method to update drop target rows
|
|
* @param snapColumnSpace distance between each grid columns in pixels
|
|
* @param snapRowSpace distance between each grid rows in pixels
|
|
* @returns
|
|
*/
|
|
export const getBoundUpdateRelativeRowsMethod = (
|
|
updateDropTargetRows:
|
|
| ((
|
|
widgetIdsToExclude: string[],
|
|
widgetBottomRow: number,
|
|
) => number | false)
|
|
| undefined,
|
|
snapColumnSpace: number,
|
|
snapRowSpace: number,
|
|
) => {
|
|
return (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 },
|
|
);
|
|
const widgetIdsToExclude = drawingBlocks.map((a) => a.widgetId);
|
|
return updateBottomRow(
|
|
top,
|
|
rows,
|
|
widgetIdsToExclude,
|
|
updateDropTargetRows,
|
|
);
|
|
}
|
|
};
|
|
};
|
|
|
|
/**
|
|
* This method helps in updating rows for dropTarget/canvas by using calling updateDropTargetRows passed down
|
|
* @param bottom new bottom most row of canvas
|
|
* @param rows number of rows of canvas
|
|
* @param widgetIdsToExclude array of widget ids to be excluded
|
|
* @param updateDropTargetRows method to update drop target rows
|
|
* @returns
|
|
*/
|
|
export const updateBottomRow = (
|
|
bottom: number,
|
|
rows: number,
|
|
widgetIdsToExclude: string[],
|
|
updateDropTargetRows:
|
|
| ((
|
|
widgetIdsToExclude: string[],
|
|
widgetBottomRow: number,
|
|
) => number | false)
|
|
| undefined,
|
|
) => {
|
|
if (bottom > rows - GridDefaults.CANVAS_EXTENSION_OFFSET) {
|
|
return (
|
|
updateDropTargetRows && updateDropTargetRows(widgetIdsToExclude, bottom)
|
|
);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Calculates positions in pixels that needs to be offsetted with the widget's positions to get it's actual position on parent canvas
|
|
* @param dragCenterSpace position of the current widget that was grabbed while dragging
|
|
* @param isDragging indicates if currently in dragging state
|
|
* @param isChildOfCanvas indicates if the dragging widgets are the original child of the canvas
|
|
* @param snapRowSpace distance between each grid columns in pixels
|
|
* @param snapColumnSpace distance between each grid rows in pixels
|
|
* @param containerPadding padding in pixels
|
|
* @returns
|
|
*/
|
|
export const getParentDiff = (
|
|
dragCenterSpace: { top: number; left: number },
|
|
isDragging: boolean,
|
|
isChildOfCanvas: boolean,
|
|
snapRowSpace: number,
|
|
snapColumnSpace: number,
|
|
containerPadding: number,
|
|
) => {
|
|
let parentDiff = {
|
|
top: 0,
|
|
left: 0,
|
|
};
|
|
|
|
if (isDragging) {
|
|
const shouldCalculateParentDiff =
|
|
!isChildOfCanvas && !isEmpty(dragCenterSpace);
|
|
|
|
if (shouldCalculateParentDiff) {
|
|
parentDiff = {
|
|
top: dragCenterSpace.top * snapRowSpace + containerPadding,
|
|
left: dragCenterSpace.left * snapColumnSpace + containerPadding,
|
|
};
|
|
} else {
|
|
parentDiff = {
|
|
top: containerPadding,
|
|
left: containerPadding,
|
|
};
|
|
}
|
|
}
|
|
|
|
return parentDiff;
|
|
};
|
|
|
|
/**
|
|
* returns the relative drag start points of the dragging blocks with respect to the dragging group's center
|
|
* @param dragCenterSpace position of the current widget that was grabbed while dragging
|
|
* @param dragOffset offset of the positions at which the widgets were grabbed for dragging
|
|
* @param defaultHandlePositions default handle positions in pixels
|
|
* @param isDragging indicates if currently in dragging state
|
|
* @param isChildOfCanvas indicates if the dragging widgets are the original child of the canvas
|
|
* @param snapRowSpace distance between each grid columns in pixels
|
|
* @param snapColumnSpace distance between each grid rows in pixels
|
|
* @param containerPadding padding in pixels
|
|
* @returns
|
|
*/
|
|
export const getRelativeStartPoints = (
|
|
dragCenterSpace: { top: number; left: number },
|
|
dragOffset: { top: number; left: number },
|
|
defaultHandlePositions: { top: number; left: number },
|
|
isDragging: boolean,
|
|
isChildOfCanvas: boolean,
|
|
snapRowSpace: number,
|
|
snapColumnSpace: number,
|
|
containerPadding: number,
|
|
) => {
|
|
let relativeStartPoints = defaultHandlePositions;
|
|
|
|
if (isDragging && !isEmpty(dragCenterSpace)) {
|
|
const dragLeft = isChildOfCanvas ? dragCenterSpace.left : 0;
|
|
const dragTop = isChildOfCanvas ? dragCenterSpace.top : 0;
|
|
|
|
relativeStartPoints = {
|
|
left:
|
|
(dragLeft + dragOffset?.left || 0) * snapColumnSpace +
|
|
2 * containerPadding,
|
|
top:
|
|
(dragTop + dragOffset?.top || 0) * snapRowSpace + 2 * containerPadding,
|
|
};
|
|
}
|
|
|
|
return relativeStartPoints;
|
|
};
|
|
|
|
/**
|
|
* returns the position of the current widget that was grabbed while dragging
|
|
* @param dragCenter
|
|
* @param childrenOccupiedSpaces
|
|
* @returns
|
|
*/
|
|
export const getDragCenterSpace = (
|
|
dragCenter: DraggingGroupCenter | undefined,
|
|
childrenOccupiedSpaces: WidgetSpace[],
|
|
): { top: number; left: number } => {
|
|
const defaultDragCenterSpace = { left: 0, top: 0 };
|
|
|
|
if (dragCenter && dragCenter.widgetId) {
|
|
// Dragging by widget
|
|
return (
|
|
childrenOccupiedSpaces.find((each) => each.id === dragCenter.widgetId) ||
|
|
defaultDragCenterSpace
|
|
);
|
|
} else if (dragCenter && dragCenter.top && dragCenter.left) {
|
|
// Dragging by Widget selection box
|
|
return { top: dragCenter.top, left: dragCenter.left };
|
|
} else {
|
|
return defaultDragCenterSpace;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 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 | null,
|
|
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,
|
|
modifyBlock: 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;
|
|
if (!modifyBlock) {
|
|
// 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;
|
|
};
|