PromucFlow_constructor/app/client/src/components/editorComponents/DropTargetComponent.tsx
Abhinav Jha 99d660370d Cleanup
DragLayerComponent
2020-01-16 11:46:21 +00:00

277 lines
8.9 KiB
TypeScript

import React, {
useState,
useContext,
ReactNode,
Context,
createContext,
useEffect,
} from "react";
import styled from "styled-components";
import { useDrop, XYCoord, DropTargetMonitor } from "react-dnd";
import { WidgetProps } from "widgets/BaseWidget";
import { WidgetConfigProps } from "reducers/entityReducers/widgetConfigReducer";
import WidgetFactory from "utils/WidgetFactory";
import { widgetOperationParams, noCollision } from "utils/WidgetPropsUtils";
import { EditorContext } from "components/editorComponents/EditorContextProvider";
import { FocusContext, DragResizeContext } from "pages/Editor/CanvasContexts";
import {
MAIN_CONTAINER_WIDGET_ID,
WIDGET_PADDING,
} from "constants/WidgetConstants";
import { calculateDropTargetRows } from "./DropTargetUtils";
import DragLayerComponent from "./DragLayerComponent";
import { AppState } from "reducers";
import { useSelector } from "react-redux";
import { theme } from "constants/DefaultTheme";
type DropTargetComponentProps = WidgetProps & {
children?: ReactNode;
snapColumnSpace: number;
snapRowSpace: number;
};
type DropTargetBounds = {
x: number;
y: number;
width: number;
height: number;
};
const StyledDropTarget = styled.div`
transition: height 100ms ease-in;
`;
/*
This context will provide the function which will help the draglayer and resizablecomponents trigger
an update of the main container's rows
*/
export const DropTargetContext: Context<{
updateDropTargetRows?: (row: number) => boolean;
persistDropTargetRows?: (widgetId: string, rows: number) => void;
}> = createContext({});
export const DropTargetComponent = (props: DropTargetComponentProps) => {
// Hook to keep the offset of the drop target container in state
const [dropTargetOffset, setDropTargetOffset] = useState({ x: 0, y: 0 });
const [rows, setRows] = useState(props.snapRows);
useEffect(() => {
setRows(props.snapRows);
}, [props.snapRows]);
const { updateWidget, occupiedSpaces, updateWidgetProperty } = useContext(
EditorContext,
);
const { selectWidget, showPropertyPane, selectedWidget } = useContext(
FocusContext,
);
const { isResizing } = useContext(DragResizeContext);
const spacesOccupiedBySiblingWidgets =
occupiedSpaces && occupiedSpaces[props.widgetId]
? occupiedSpaces[props.widgetId]
: undefined;
const childWidgets = useSelector(
(state: AppState) => state.entities.canvasWidgets[props.widgetId].children,
);
const persistDropTargetRows = (widgetId: string, widgetBottomRow: number) => {
if (props.widgetId === MAIN_CONTAINER_WIDGET_ID) {
const occupiedSpacesByChildren =
occupiedSpaces && occupiedSpaces[MAIN_CONTAINER_WIDGET_ID];
const rowsToPersist = calculateDropTargetRows(
widgetId,
widgetBottomRow,
rows,
occupiedSpacesByChildren,
);
setRows(rowsToPersist);
/* Update the main container's rows, ONLY if it has changed since the last render */
if (props.snapRows !== rowsToPersist) {
updateWidgetProperty &&
updateWidgetProperty(props.widgetId, "snapRows", rowsToPersist);
updateWidgetProperty &&
updateWidgetProperty(
props.widgetId,
"bottomRow",
Math.round(
(rowsToPersist * props.snapRowSpace) / props.parentRowSpace,
),
);
}
}
};
/* Update the rows of the main container based on the current widget's (dragging/resizing) bottom row */
const updateDropTargetRows = (widgetBottomRow: number) => {
if (props.widgetId === MAIN_CONTAINER_WIDGET_ID) {
/* If the widget has reached the penultimate row of the main container */
if (widgetBottomRow > rows - 1) {
setRows(rows + 2);
return true;
// If the current widget's (dragging/resizing) bottom row has moved back up
} else if (widgetBottomRow < rows - 2 && rows - props.snapRows >= 2) {
setRows(rows - 2);
return true;
}
return false;
}
return false;
};
const isChildFocused =
!!childWidgets &&
!!selectedWidget &&
childWidgets.length > 0 &&
childWidgets.indexOf(selectedWidget) > -1;
const isChildResizing = !!isResizing && isChildFocused;
// Make this component a drop target
const [{ isExactlyOver }, drop] = useDrop({
accept: Object.values(WidgetFactory.getWidgetTypes()),
drop(widget: WidgetProps & Partial<WidgetConfigProps>, monitor) {
// Make sure we're dropping in this container.
if (isExactlyOver) {
const updateWidgetParams = widgetOperationParams(
widget,
monitor.getSourceClientOffset() as XYCoord,
dropTargetOffset,
props.snapColumnSpace,
props.snapRowSpace,
props.widgetId,
);
// Only show propertypane if this is a new widget.
// If it is not a new widget, then let the DraggableComponent handle it.
showPropertyPane &&
updateWidgetParams.payload.newWidgetId &&
showPropertyPane(updateWidgetParams.payload.newWidgetId);
// Select the widget if it is a new widget
selectWidget &&
updateWidgetParams.payload.newWidgetId &&
selectWidget(updateWidgetParams.payload.newWidgetId);
/* currently dropped widget's bottom row */
const droppedWidgetBottomRow = updateWidgetParams.payload.rows
? updateWidgetParams.payload.topRow + updateWidgetParams.payload.rows
: updateWidgetParams.payload.topRow +
(widget.bottomRow - widget.topRow);
persistDropTargetRows(
widget.widgetId || updateWidgetParams.payload.newWidgetId,
droppedWidgetBottomRow,
);
/* Finally update the widget */
updateWidget &&
updateWidget(
updateWidgetParams.operation,
updateWidgetParams.widgetId,
updateWidgetParams.payload,
);
}
return undefined;
},
// Collect isOver for ui transforms when hovering over this component
collect: (monitor: DropTargetMonitor) => ({
isOver:
(monitor.isOver({ shallow: true }) &&
props.widgetId !== monitor.getItem().widgetId) ||
(monitor.isOver() && props.widgetId !== monitor.getItem().widgetId),
isExactlyOver: monitor.isOver({ shallow: true }),
}),
// Only allow drop if the drag object is directly over this component
// As opposed to the drag object being over a child component, or outside the component bounds
// Also only if the dropzone does not overlap any existing children
canDrop: (widget, monitor) => {
// Check if the draggable is the same as the dropTarget
if (isExactlyOver) {
const hasCollision = !noCollision(
monitor.getSourceClientOffset() as XYCoord,
props.snapColumnSpace,
props.snapRowSpace,
widget,
dropTargetOffset,
spacesOccupiedBySiblingWidgets,
rows,
props.snapColumns,
);
return !hasCollision;
}
return false;
},
});
const handleBoundsUpdate = (rect: DOMRect) => {
if (rect.x !== dropTargetOffset.x || rect.y !== dropTargetOffset.y) {
setDropTargetOffset({
x: rect.x,
y: rect.y,
});
}
};
const handleFocus = () => {
if (!props.parentId && !isResizing) {
selectWidget && selectWidget(props.widgetId);
showPropertyPane && showPropertyPane();
}
};
const width =
props.widgetId === MAIN_CONTAINER_WIDGET_ID
? `calc(100% - ${WIDGET_PADDING * 2}px)`
: "100%";
const height =
props.widgetId === MAIN_CONTAINER_WIDGET_ID
? `${rows * props.snapRowSpace}px`
: "100%";
const marginTop =
props.widgetId === MAIN_CONTAINER_WIDGET_ID ? `${theme.spaces[9]}px` : 0;
const marginBottom =
props.widgetId === MAIN_CONTAINER_WIDGET_ID ? "500px" : 0;
return (
<DropTargetContext.Provider
value={{ updateDropTargetRows, persistDropTargetRows }}
>
<StyledDropTarget
onClick={handleFocus}
ref={drop}
style={{
position: "relative",
width,
height,
marginTop,
marginBottom,
userSelect: "none",
opacity: 0.99,
}}
>
{props.children}
<DragLayerComponent
parentOffset={dropTargetOffset}
parentWidgetId={props.widgetId}
parentRowHeight={props.snapRowSpace}
parentColumnWidth={props.snapColumnSpace}
visible={isExactlyOver || isChildResizing}
isOver={isExactlyOver}
dropTargetOffset={dropTargetOffset}
occupiedSpaces={spacesOccupiedBySiblingWidgets}
onBoundsUpdate={handleBoundsUpdate}
parentRows={rows}
parentCols={props.snapColumns}
isResizing={isChildResizing}
/>
</StyledDropTarget>
</DropTargetContext.Provider>
);
};
export default DropTargetComponent;