PromucFlow_constructor/app/client/src/components/editorComponents/ResizableComponent.tsx

359 lines
10 KiB
TypeScript
Raw Normal View History

2020-02-18 19:56:58 +00:00
import React, { useContext, memo } from "react";
import styled, { css } from "styled-components";
import { XYCoord } from "react-dnd";
2020-01-17 12:34:58 +00:00
import {
MAIN_CONTAINER_WIDGET_ID,
WidgetTypes,
} from "constants/WidgetConstants";
2020-02-18 19:56:58 +00:00
import { ContainerWidgetProps } from "widgets/ContainerWidget";
import {
WidgetOperations,
WidgetRowCols,
WidgetProps,
} from "widgets/BaseWidget";
import { EditorContext } from "components/editorComponents/EditorContextProvider";
import { generateClassName } from "utils/generators";
2020-01-16 11:46:21 +00:00
import { DropTargetContext } from "./DropTargetComponent";
import {
UIElementSize,
2020-02-18 19:56:58 +00:00
computeFinalRowCols,
computeRowCols,
} from "./ResizableUtils";
2020-01-20 09:00:37 +00:00
import {
useShowPropertyPane,
useWidgetSelection,
useWidgetDragResize,
} from "utils/hooks/dragResizeHooks";
2020-01-21 07:14:47 +00:00
import { useSelector } from "react-redux";
import { AppState } from "reducers";
2020-02-18 19:56:58 +00:00
import { PropertyPaneReduxState } from "reducers/uiReducers/propertyPaneReducer";
import Resizable from "resizable";
import { invisible, theme } from "constants/DefaultTheme";
2020-02-18 19:56:58 +00:00
import { isDropZoneOccupied } from "utils/WidgetPropsUtils";
export type ResizableComponentProps = ContainerWidgetProps<WidgetProps> & {
paddingOffset: number;
};
const VisibilityContainer = styled.div<{ visible: boolean; padding: number }>`
${props => (!props.visible ? invisible : "")}
height: 100%;
width: 100%;
padding: ${props => props.padding}px;
`;
2020-02-18 19:56:58 +00:00
const HandleStyles = css`
position: absolute;
z-index: 3;
width: 20px;
height: 20px;
&:before {
position: absolute;
background: ${theme.colors.widgetBorder};
content: "";
width: 2px;
height: 2px;
}
&:after {
position: absolute;
content: "";
width: 6px;
height: 6px;
border-radius: 50%;
background: ${theme.colors.widgetBorder};
top: calc(50% - 2px);
left: calc(50% - 2px);
}
`;
const VerticalHandleStyles = css`
${HandleStyles}
top:0;
height: 100%;
cursor: col-resize;
&:before {
left: 50%;
height: 100%;
top: 0;
}
&:after {
}
`;
const HorizontalHandleStyles = css`
${HandleStyles}
left: 0;
width: 100%;
cursor: row-resize;
&:before {
top: 50%;
width: 100%;
left: 0;
}
&:after {
}
`;
const LeftHandleStyles = css`
${VerticalHandleStyles}
left:-10px;
`;
const RightHandleStyles = css`
${VerticalHandleStyles};
right: -10px;
`;
const TopHandleStyles = css`
${HorizontalHandleStyles};
top: -10px;
`;
const BottomHandleStyles = css`
${HorizontalHandleStyles};
bottom: -10px;
`;
const BottomRightHandleStyles = css`
position: absolute;
z-index: 3;
bottom: -20px;
right: -20px;
width: 40px;
height: 40px;
cursor: se-resize;
`;
/* eslint-disable react/display-name */
export const ResizableComponent = memo((props: ResizableComponentProps) => {
// Fetch information from the context
const { updateWidget, occupiedSpaces } = useContext(EditorContext);
2020-01-16 11:46:21 +00:00
const { updateDropTargetRows, persistDropTargetRows } = useContext(
DropTargetContext,
);
2020-01-17 12:34:58 +00:00
2020-01-20 09:00:37 +00:00
const showPropertyPane = useShowPropertyPane();
const { selectWidget } = useWidgetSelection();
const { setIsResizing } = useWidgetDragResize();
const selectedWidget = useSelector(
(state: AppState) => state.ui.editor.selectedWidget,
);
const focusedWidget = useSelector(
(state: AppState) => state.ui.editor.focusedWidget,
);
const isDragging = useSelector(
(state: AppState) => state.ui.widgetDragResize.isDragging,
);
2020-02-18 19:56:58 +00:00
const isResizing = useSelector(
(state: AppState) => state.ui.widgetDragResize.isResizing,
);
const propertyPaneState: PropertyPaneReduxState = useSelector(
(state: AppState) => state.ui.propertyPane,
);
2020-01-20 09:00:37 +00:00
const occupiedSpacesBySiblingWidgets =
occupiedSpaces && props.parentId && occupiedSpaces[props.parentId]
? occupiedSpaces[props.parentId]
: undefined;
2020-01-17 12:34:58 +00:00
let maxBottomRowOfChildWidgets: number | undefined;
if (props.type === WidgetTypes.CONTAINER_WIDGET) {
const occupiedSpacesByChildren =
occupiedSpaces && occupiedSpaces[props.widgetId];
maxBottomRowOfChildWidgets = occupiedSpacesByChildren?.reduce(
(prev: number, next) => {
if (next.bottom > prev) return next.bottom;
return prev;
},
0,
);
}
// isFocused (string | boolean) -> isWidgetFocused (boolean)
2020-01-06 11:02:22 +00:00
const isWidgetFocused =
focusedWidget === props.widgetId || selectedWidget === props.widgetId;
2019-10-08 06:19:10 +00:00
// Calculate the dimensions of the widget,
// The ResizableContainer's size prop is controlled
const dimensions: UIElementSize = {
width: (props.rightColumn - props.leftColumn) * props.parentColumnSpace,
height: (props.bottomRow - props.topRow) * props.parentRowSpace,
};
// Resize bound's className - defaults to body
// ResizableContainer accepts the className of the element,
// whose clientRect will act as the bounds for resizing.
// Note, if there are many containers with the same className
// the bounding container becomes the nearest parent with the className
2020-02-18 19:56:58 +00:00
const boundingElementClassName = generateClassName(props.parentId);
const possibleBoundingElements = document.getElementsByClassName(
boundingElementClassName,
);
const boundingElement =
possibleBoundingElements.length > 0
? possibleBoundingElements[0]
: undefined;
const boundingElementClientRect = boundingElement
? boundingElement.getBoundingClientRect()
: undefined;
2019-10-08 06:19:10 +00:00
// onResize handler
// Checks if the current resize position has any collisions
// If yes, set isColliding flag to true.
// If no, set isColliding flag to false.
2020-02-18 19:56:58 +00:00
const isColliding = (newDimensions: UIElementSize, position: XYCoord) => {
2020-01-16 11:46:21 +00:00
const bottom =
2020-02-18 19:56:58 +00:00
props.topRow +
position.y / props.parentRowSpace +
newDimensions.height / props.parentRowSpace;
2020-01-16 11:46:21 +00:00
// Make sure to calculate collision IF we don't update the main container's rows
let updated = false;
2020-02-18 19:56:58 +00:00
if (updateDropTargetRows && props.parentId === MAIN_CONTAINER_WIDGET_ID) {
2020-01-16 11:46:21 +00:00
updated = updateDropTargetRows(bottom);
2020-02-18 19:56:58 +00:00
}
const delta: UIElementSize = {
height: newDimensions.height - dimensions.height,
width: newDimensions.width - dimensions.width,
};
const newRowCols: WidgetRowCols | false = computeRowCols(
delta,
position,
props,
);
if (
boundingElementClientRect &&
newRowCols.rightColumn * props.parentColumnSpace >
boundingElementClientRect.width
) {
return true;
}
if (newRowCols && newRowCols.leftColumn < 0) {
return true;
}
2020-01-16 11:46:21 +00:00
if (!updated) {
2020-02-18 19:56:58 +00:00
if (
// If this is a container widget, the maxBottomRow of child widgets should be one less than the max bottom row of the new row cols
maxBottomRowOfChildWidgets &&
newRowCols &&
props.type === WidgetTypes.CONTAINER_WIDGET &&
newRowCols.bottomRow - newRowCols.topRow - 1 <
maxBottomRowOfChildWidgets
) {
return true;
}
if (
boundingElementClientRect &&
newRowCols.bottomRow * props.parentRowSpace >
boundingElementClientRect.height
) {
return true;
}
if (newRowCols && newRowCols.topRow < 0) {
return true;
2020-01-16 11:46:21 +00:00
}
2019-10-08 06:19:10 +00:00
}
2020-02-18 19:56:58 +00:00
// Check if new row cols are occupied by sibling widgets
return isDropZoneOccupied(
{
left: newRowCols.leftColumn,
top: newRowCols.topRow,
bottom: newRowCols.bottomRow,
right: newRowCols.rightColumn,
},
props.widgetId,
occupiedSpacesBySiblingWidgets,
);
2019-10-08 06:19:10 +00:00
};
// onResizeStop handler
// when done resizing, check if;
// 1) There is no collision
// 2) There is a change in widget size
// Update widget, if both of the above are true.
2020-02-18 19:56:58 +00:00
const updateSize = (newDimensions: UIElementSize, position: XYCoord) => {
// Get the difference in size of the widget, before and after resizing.
const delta: UIElementSize = {
2020-02-18 19:56:58 +00:00
height: newDimensions.height - dimensions.height,
width: newDimensions.width - dimensions.width,
};
// Get the updated Widget rows and columns props
// False, if there is collision
// False, if none of the rows and cols have changed.
2020-02-18 19:56:58 +00:00
const newRowCols: WidgetRowCols | false = computeFinalRowCols(
delta,
position,
props,
);
if (newRowCols) {
2020-01-16 11:46:21 +00:00
persistDropTargetRows &&
props.parentId === MAIN_CONTAINER_WIDGET_ID &&
persistDropTargetRows(props.widgetId, newRowCols.bottomRow);
updateWidget &&
updateWidget(WidgetOperations.RESIZE, props.widgetId, newRowCols);
}
// Clear border styles
2020-02-18 19:56:58 +00:00
// setIsColliding && setIsColliding(false);
// Tell the Canvas that we've stopped resizing
2020-01-17 12:34:58 +00:00
// Put it laster in the stack so that other updates like click, are not propagated to the parent container
setTimeout(() => {
setIsResizing && setIsResizing(false);
2020-01-17 12:34:58 +00:00
}, 0);
// Tell the Canvas to put the focus back to this widget
// By setting the focus, we enable the control buttons on the widget
2020-01-06 11:02:22 +00:00
selectWidget && selectWidget(props.widgetId);
// Let the propertypane show.
// The propertypane decides whether to show itself, based on
// whether it was showing when the widget resize started.
2020-02-18 19:56:58 +00:00
showPropertyPane &&
propertyPaneState.widgetId !== props.widgetId &&
showPropertyPane(props.widgetId, true);
};
const handleResizeStart = () => {
setIsResizing && !isResizing && setIsResizing(true);
selectWidget &&
selectedWidget !== props.widgetId &&
selectWidget(props.widgetId);
showPropertyPane && showPropertyPane(props.widgetId, true);
};
2020-02-18 19:56:58 +00:00
return (
2020-02-18 19:56:58 +00:00
<Resizable
handles={{
left: LeftHandleStyles,
top: TopHandleStyles,
bottom: BottomHandleStyles,
right: RightHandleStyles,
bottomRight: BottomRightHandleStyles,
}}
2020-02-18 19:56:58 +00:00
componentHeight={dimensions.height}
componentWidth={dimensions.width}
onStart={handleResizeStart}
onStop={updateSize}
snapGrid={{ x: props.parentColumnSpace, y: props.parentRowSpace }}
enable={!isDragging && isWidgetFocused}
isColliding={isColliding}
>
<VisibilityContainer
visible={!!props.isVisible}
padding={props.paddingOffset}
>
{props.children}
</VisibilityContainer>
2020-02-18 19:56:58 +00:00
</Resizable>
);
});
export default ResizableComponent;