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>
This commit is contained in:
parent
cf59d4d10e
commit
0149085bf8
|
|
@ -32,7 +32,6 @@ const jsEditorLocators = require("../locators/JSEditor.json");
|
|||
const queryLocators = require("../locators/QueryEditor.json");
|
||||
const welcomePage = require("../locators/welcomePage.json");
|
||||
const publishWidgetspage = require("../locators/publishWidgetspage.json");
|
||||
|
||||
let pageidcopy = " ";
|
||||
const chainStart = Symbol();
|
||||
|
||||
|
|
@ -833,7 +832,7 @@ Cypress.Commands.add("SearchEntityandDblClick", (apiname1) => {
|
|||
return cy
|
||||
.get(commonlocators.entitySearchResult.concat(apiname1).concat("')"))
|
||||
.dblclick()
|
||||
.get("input")
|
||||
.get("input[type=text]")
|
||||
.last();
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -253,6 +253,8 @@ export type WidgetResize = {
|
|||
rightColumn: number;
|
||||
topRow: number;
|
||||
bottomRow: number;
|
||||
snapColumnSpace: number;
|
||||
snapRowSpace: number;
|
||||
};
|
||||
|
||||
export type ModalWidgetResize = {
|
||||
|
|
|
|||
27
app/client/src/actions/reflowActions.ts
Normal file
27
app/client/src/actions/reflowActions.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import {
|
||||
ReduxAction,
|
||||
ReflowReduxActionTypes,
|
||||
} from "constants/ReduxActionConstants";
|
||||
import { ReflowedSpaceMap } from "reflow/reflowTypes";
|
||||
|
||||
export const reflowMoveAction = (
|
||||
payload: ReflowedSpaceMap,
|
||||
): ReduxAction<ReflowedSpaceMap> => {
|
||||
return {
|
||||
type: ReflowReduxActionTypes.REFLOW_MOVE,
|
||||
payload: payload,
|
||||
};
|
||||
};
|
||||
|
||||
export const stopReflowAction = () => {
|
||||
return {
|
||||
type: ReflowReduxActionTypes.STOP_REFLOW,
|
||||
};
|
||||
};
|
||||
|
||||
export const setEnableReflowAction = (payload: boolean) => {
|
||||
return {
|
||||
type: ReflowReduxActionTypes.ENABLE_REFLOW,
|
||||
payload,
|
||||
};
|
||||
};
|
||||
|
|
@ -1 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="36" height="20" fill="none" viewBox="0 0 36 20"><path fill="#716E6E" d="M8.38184 14C9.88574 14 10.8037 13.2334 10.8037 11.998C10.8037 11.0801 10.1494 10.3965 9.20215 10.3086V10.2207C9.91016 10.1084 10.457 9.4541 10.457 8.71191C10.457 7.62793 9.65137 6.9541 8.31348 6.9541H5.36914V14H8.38184ZM6.62891 7.95508H8.00586C8.76758 7.95508 9.21191 8.31641 9.21191 8.93652C9.21191 9.57129 8.73828 9.91797 7.84473 9.91797H6.62891V7.95508ZM6.62891 12.999V10.8262H8.04492C9.00684 10.8262 9.51465 11.1973 9.51465 11.9004C9.51465 12.6182 9.02148 12.999 8.09375 12.999H6.62891ZM17.1363 12.9111H13.8355V10.9385H16.9557V9.91309H13.8355V8.04297H17.1363V6.9541H12.5758V14H17.1363V12.9111ZM22.1115 14V8.04297H24.2844V6.9541H18.6838V8.04297H20.8518V14H22.1115ZM29.7674 14H31.1199L28.615 6.9541H27.199L24.699 14H25.9734L26.5789 12.1982H29.1717L29.7674 14ZM27.8387 8.29688H27.9217L28.8787 11.2021H26.8719L27.8387 8.29688Z"/><rect width="34" height="19" x="1" y=".5" stroke="#716E6E" rx="1.5"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="36" height="20" fill="none" viewBox="0 0 36 20"><rect width="34" height="19" x="1" y=".5" stroke="#716E6E" rx="1.5"/><path fill="#716E6E" d="M8.38184 14C9.88574 14 10.8037 13.2334 10.8037 11.998C10.8037 11.0801 10.1494 10.3965 9.20215 10.3086V10.2207C9.91016 10.1084 10.457 9.4541 10.457 8.71191C10.457 7.62793 9.65137 6.9541 8.31348 6.9541H5.36914V14H8.38184ZM6.62891 7.95508H8.00586C8.76758 7.95508 9.21191 8.31641 9.21191 8.93652C9.21191 9.57129 8.73828 9.91797 7.84473 9.91797H6.62891V7.95508ZM6.62891 12.999V10.8262H8.04492C9.00684 10.8262 9.51465 11.1973 9.51465 11.9004C9.51465 12.6182 9.02148 12.999 8.09375 12.999H6.62891ZM17.1363 12.9111H13.8355V10.9385H16.9557V9.91309H13.8355V8.04297H17.1363V6.9541H12.5758V14H17.1363V12.9111ZM22.1115 14V8.04297H24.2844V6.9541H18.6838V8.04297H20.8518V14H22.1115ZM29.7674 14H31.1199L28.615 6.9541H27.199L24.699 14H25.9734L26.5789 12.1982H29.1717L29.7674 14ZM27.8387 8.29688H27.9217L28.8787 11.2021H26.8719L27.8387 8.29688Z"/></svg>
|
||||
|
Before Width: | Height: | Size: 1023 B After Width: | Height: | Size: 1023 B |
|
|
@ -21,8 +21,8 @@ function ShowcaseCarouselModal({ children }: { children: React.ReactNode }) {
|
|||
dispatch(hideCommentsIntroCarousel());
|
||||
AnalyticsUtil.logEvent("COMMENTS_ONBOARDING_MODAL_DISMISSED");
|
||||
}}
|
||||
overlayClassName="comments-onboarding-carousel"
|
||||
portalClassName="comments-onboarding-carousel-portal"
|
||||
overlayClassName="onboarding-carousel"
|
||||
portalClassName="onboarding-carousel-portal"
|
||||
scrollContents
|
||||
width={325}
|
||||
zIndex={Layers.appComments}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,9 @@ import { usePositionedContainerZIndex } from "utils/hooks/usePositionedContainer
|
|||
import { useSelector } from "react-redux";
|
||||
import { snipingModeSelector } from "selectors/editorSelectors";
|
||||
import WidgetFactory from "utils/WidgetFactory";
|
||||
import { memoize } from "lodash";
|
||||
import { isEqual, memoize } from "lodash";
|
||||
import { getReflowSelector } from "selectors/widgetReflowSelectors";
|
||||
import { AppState } from "reducers";
|
||||
|
||||
const PositionedWidget = styled.div<{ zIndexOnHover: number }>`
|
||||
&:hover {
|
||||
|
|
@ -18,6 +20,7 @@ const PositionedWidget = styled.div<{ zIndexOnHover: number }>`
|
|||
export type PositionedContainerProps = {
|
||||
style: BaseStyle;
|
||||
children: ReactNode;
|
||||
parentId?: string;
|
||||
widgetId: string;
|
||||
widgetType: WidgetType;
|
||||
selected?: boolean;
|
||||
|
|
@ -54,18 +57,70 @@ export function PositionedContainer(props: PositionedContainerProps) {
|
|||
isDropTarget,
|
||||
);
|
||||
|
||||
const reflowSelector = getReflowSelector(props.widgetId);
|
||||
|
||||
const reflowedPosition = useSelector(reflowSelector, isEqual);
|
||||
const dragDetails = useSelector(
|
||||
(state: AppState) => state.ui.widgetDragResize.dragDetails,
|
||||
);
|
||||
const isResizing = useSelector(
|
||||
(state: AppState) => state.ui.widgetDragResize.isResizing,
|
||||
);
|
||||
const isCurrentCanvasReflowing =
|
||||
(dragDetails && dragDetails.draggedOn === props.parentId) || isResizing;
|
||||
const containerStyle: CSSProperties = useMemo(() => {
|
||||
return {
|
||||
const reflowX = reflowedPosition?.X || 0;
|
||||
const reflowY = reflowedPosition?.Y || 0;
|
||||
const reflowWidth = reflowedPosition?.width;
|
||||
const reflowHeight = reflowedPosition?.height;
|
||||
const reflowEffected = isCurrentCanvasReflowing && reflowedPosition;
|
||||
const hasReflowedPosition = reflowEffected && reflowX + reflowY !== 0;
|
||||
const hasReflowedDimensions =
|
||||
reflowEffected &&
|
||||
((reflowHeight && reflowHeight !== props.style.componentHeight) ||
|
||||
(reflowWidth && reflowWidth !== props.style.componentWidth));
|
||||
const effectedByReflow = hasReflowedPosition || hasReflowedDimensions;
|
||||
const dropTargetStyles: CSSProperties =
|
||||
isDropTarget && effectedByReflow ? { pointerEvents: "none" } : {};
|
||||
const reflowedPositionStyles: CSSProperties = hasReflowedPosition
|
||||
? {
|
||||
transform: `translate(${reflowX}px,${reflowY}px)`,
|
||||
transition: `transform 100ms linear`,
|
||||
boxShadow: `0 0 0 1px rgba(104,113,239,0.5)`,
|
||||
}
|
||||
: {};
|
||||
const reflowDimensionsStyles = hasReflowedDimensions
|
||||
? {
|
||||
transition: `width 0.1s, height 0.1s`,
|
||||
boxShadow: `0 0 0 1px rgba(104,113,239,0.5)`,
|
||||
}
|
||||
: {};
|
||||
const styles: CSSProperties = {
|
||||
position: "absolute",
|
||||
left: x,
|
||||
top: y,
|
||||
height: props.style.componentHeight + (props.style.heightUnit || "px"),
|
||||
width: props.style.componentWidth + (props.style.widthUnit || "px"),
|
||||
height:
|
||||
reflowHeight ||
|
||||
props.style.componentHeight + (props.style.heightUnit || "px"),
|
||||
width:
|
||||
reflowWidth ||
|
||||
props.style.componentWidth + (props.style.widthUnit || "px"),
|
||||
padding: padding + "px",
|
||||
zIndex,
|
||||
backgroundColor: "inherit",
|
||||
...reflowedPositionStyles,
|
||||
...reflowDimensionsStyles,
|
||||
...dropTargetStyles,
|
||||
};
|
||||
}, [props.style, onHoverZIndex, zIndex]);
|
||||
return styles;
|
||||
}, [
|
||||
props.style,
|
||||
isCurrentCanvasReflowing,
|
||||
onHoverZIndex,
|
||||
zIndex,
|
||||
reflowSelector,
|
||||
reflowedPosition,
|
||||
]);
|
||||
|
||||
const onClickFn = useCallback(
|
||||
(e) => {
|
||||
|
|
|
|||
|
|
@ -100,6 +100,8 @@ export function DropTargetComponent(props: DropTargetComponentProps) {
|
|||
const showPropertyPane = useShowPropertyPane();
|
||||
const { deselectAll, focusWidget } = useWidgetSelection();
|
||||
const updateCanvasSnapRows = useCanvasSnapRowsUpdateHook();
|
||||
const showDragLayer =
|
||||
(isDragging && draggedOn === props.widgetId) || isResizing;
|
||||
|
||||
useEffect(() => {
|
||||
const snapRows = getCanvasSnapRows(props.bottomRow, props.canExtend);
|
||||
|
|
@ -111,6 +113,16 @@ export function DropTargetComponent(props: DropTargetComponentProps) {
|
|||
}
|
||||
}
|
||||
}, [props.bottomRow, props.canExtend]);
|
||||
useEffect(() => {
|
||||
if (!isDragging || !isResizing) {
|
||||
// bottom row of canvas can increase by any number as user moves/resizes any widget towards the bottom of the canvas
|
||||
// but canvas height is not lost when user moves/resizes back top.
|
||||
// it is done that way to have a pleasant building experience.
|
||||
// post drop the bottom most row is used to appropriately calculate the canvas height and lose unwanted height.
|
||||
rowRef.current = snapRows;
|
||||
updateHeight();
|
||||
}
|
||||
}, [isDragging, isResizing]);
|
||||
|
||||
const updateHeight = () => {
|
||||
if (dropTargetRef.current) {
|
||||
|
|
@ -165,7 +177,6 @@ export function DropTargetComponent(props: DropTargetComponentProps) {
|
|||
updateDropTargetRows,
|
||||
};
|
||||
}, [updateDropTargetRows, occupiedSpacesByChildren]);
|
||||
|
||||
return (
|
||||
<DropTargetContext.Provider value={contextValue}>
|
||||
<StyledDropTarget
|
||||
|
|
@ -181,7 +192,7 @@ export function DropTargetComponent(props: DropTargetComponentProps) {
|
|||
{!(childWidgets && childWidgets.length) &&
|
||||
!isDragging &&
|
||||
!props.parentId && <Onboarding />}
|
||||
{((isDragging && draggedOn === props.widgetId) || isResizing) && (
|
||||
{showDragLayer && (
|
||||
<DragLayerComponent
|
||||
noPad={props.noPad || false}
|
||||
parentColumnWidth={props.snapColumnSpace}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useContext, useRef, memo, useMemo } from "react";
|
||||
import React, { useContext, memo, useMemo } from "react";
|
||||
import { XYCord } from "utils/hooks/useCanvasDragging";
|
||||
|
||||
import {
|
||||
|
|
@ -7,8 +7,6 @@ import {
|
|||
WidgetProps,
|
||||
} from "widgets/BaseWidget";
|
||||
import { EditorContext } from "components/editorComponents/EditorContextProvider";
|
||||
import { generateClassName } from "utils/generators";
|
||||
import { DropTargetContext } from "./DropTargetComponent";
|
||||
import {
|
||||
UIElementSize,
|
||||
computeFinalRowCols,
|
||||
|
|
@ -21,9 +19,9 @@ import {
|
|||
} from "utils/hooks/dragResizeHooks";
|
||||
import { useSelector } from "react-redux";
|
||||
import { AppState } from "reducers";
|
||||
import Resizable from "resizable";
|
||||
import { omit, get, ceil } from "lodash";
|
||||
import { getSnapColumns, isDropZoneOccupied } from "utils/WidgetPropsUtils";
|
||||
import Resizable from "resizable/resizenreflow";
|
||||
import { omit, get } from "lodash";
|
||||
import { getSnapColumns } from "utils/WidgetPropsUtils";
|
||||
import {
|
||||
VisibilityContainer,
|
||||
LeftHandleStyles,
|
||||
|
|
@ -36,18 +34,17 @@ import {
|
|||
BottomRightHandleStyles,
|
||||
} from "./ResizeStyledComponents";
|
||||
import AnalyticsUtil from "utils/AnalyticsUtil";
|
||||
import { scrollElementIntoParentCanvasView } from "utils/helpers";
|
||||
import { getNearestParentCanvas } from "utils/generators";
|
||||
import {
|
||||
getOccupiedSpaces,
|
||||
previewModeSelector,
|
||||
} from "selectors/editorSelectors";
|
||||
import { commentModeSelector } from "selectors/commentsSelectors";
|
||||
import { snipingModeSelector } from "selectors/editorSelectors";
|
||||
import {
|
||||
previewModeSelector,
|
||||
snipingModeSelector,
|
||||
} from "selectors/editorSelectors";
|
||||
import { useWidgetSelection } from "utils/hooks/useWidgetSelection";
|
||||
import { getCanvasWidgets } from "selectors/entitiesSelector";
|
||||
import { focusWidget } from "actions/widgetActions";
|
||||
import { getParentToOpenIfAny } from "utils/hooks/useClickToSelectWidget";
|
||||
import { GridDefaults } from "constants/WidgetConstants";
|
||||
import { DropTargetContext } from "./DropTargetComponent";
|
||||
|
||||
export type ResizableComponentProps = WidgetProps & {
|
||||
paddingOffset: number;
|
||||
|
|
@ -56,14 +53,10 @@ export type ResizableComponentProps = WidgetProps & {
|
|||
export const ResizableComponent = memo(function ResizableComponent(
|
||||
props: ResizableComponentProps,
|
||||
) {
|
||||
const resizableRef = useRef<HTMLDivElement>(null);
|
||||
// Fetch information from the context
|
||||
const { updateWidget } = useContext(EditorContext);
|
||||
const occupiedSpaces = useSelector(getOccupiedSpaces);
|
||||
const canvasWidgets = useSelector(getCanvasWidgets);
|
||||
|
||||
const { updateDropTargetRows } = useContext(DropTargetContext);
|
||||
|
||||
const isCommentMode = useSelector(commentModeSelector);
|
||||
const isSnipingMode = useSelector(snipingModeSelector);
|
||||
const isPreviewMode = useSelector(previewModeSelector);
|
||||
|
|
@ -93,13 +86,6 @@ export const ResizableComponent = memo(function ResizableComponent(
|
|||
canvasWidgets,
|
||||
);
|
||||
|
||||
const occupiedSpacesBySiblingWidgets = useMemo(() => {
|
||||
return occupiedSpaces && props.parentId && occupiedSpaces[props.parentId]
|
||||
? occupiedSpaces[props.parentId]
|
||||
: undefined;
|
||||
}, [occupiedSpaces, props.parentId]);
|
||||
|
||||
// isFocused (string | boolean) -> isWidgetFocused (boolean)
|
||||
const isWidgetFocused =
|
||||
focusedWidget === props.widgetId ||
|
||||
selectedWidget === props.widgetId ||
|
||||
|
|
@ -116,116 +102,56 @@ export const ResizableComponent = memo(function ResizableComponent(
|
|||
2 * props.paddingOffset,
|
||||
};
|
||||
|
||||
// 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
|
||||
const boundingElementClassName = generateClassName(props.parentId);
|
||||
const possibleBoundingElements = document.getElementsByClassName(
|
||||
boundingElementClassName,
|
||||
);
|
||||
const boundingElement =
|
||||
possibleBoundingElements.length > 0
|
||||
? possibleBoundingElements[0]
|
||||
: undefined;
|
||||
|
||||
// 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.
|
||||
const isColliding = (newDimensions: UIElementSize, position: XYCord) => {
|
||||
// Moving the bounding element calculations inside
|
||||
// to make this expensive operation only whne
|
||||
const boundingElementClientRect = boundingElement
|
||||
? boundingElement.getBoundingClientRect()
|
||||
: undefined;
|
||||
|
||||
const bottom =
|
||||
props.topRow +
|
||||
position.y / props.parentRowSpace +
|
||||
newDimensions.height / props.parentRowSpace;
|
||||
// Make sure to calculate collision IF we don't update the main container's rows
|
||||
let updated = false;
|
||||
if (updateDropTargetRows) {
|
||||
updated = !!updateDropTargetRows(props.widgetId, bottom);
|
||||
const el = resizableRef.current;
|
||||
if (el) {
|
||||
const { height } = el?.getBoundingClientRect();
|
||||
const scrollParent = getNearestParentCanvas(resizableRef.current);
|
||||
scrollElementIntoParentCanvasView(
|
||||
{
|
||||
top: 40,
|
||||
height,
|
||||
},
|
||||
scrollParent,
|
||||
el,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const getResizedPositions = (
|
||||
newDimensions: UIElementSize,
|
||||
position: XYCord,
|
||||
) => {
|
||||
const delta: UIElementSize = {
|
||||
height: newDimensions.height - dimensions.height,
|
||||
width: newDimensions.width - dimensions.width,
|
||||
};
|
||||
const newRowCols: WidgetRowCols | false = computeRowCols(
|
||||
delta,
|
||||
position,
|
||||
props,
|
||||
);
|
||||
|
||||
if (newRowCols.rightColumn > getSnapColumns()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Minimum row and columns to be set to a widget.
|
||||
if (
|
||||
newRowCols.rightColumn - newRowCols.leftColumn < 2 ||
|
||||
newRowCols.bottomRow - newRowCols.topRow < 4
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
boundingElementClientRect &&
|
||||
newRowCols.rightColumn * props.parentColumnSpace >
|
||||
ceil(boundingElementClientRect.width)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (newRowCols && newRowCols.leftColumn < 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!updated) {
|
||||
if (
|
||||
boundingElementClientRect &&
|
||||
newRowCols.bottomRow * props.parentRowSpace >
|
||||
ceil(boundingElementClientRect.height)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (newRowCols && newRowCols.topRow < 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
const newRowCols: WidgetRowCols = computeRowCols(delta, position, props);
|
||||
let canResizeHorizontally = true,
|
||||
canResizeVertically = true;
|
||||
|
||||
// this is required for list widget so that template have no collision
|
||||
if (props.ignoreCollision) return false;
|
||||
if (props.ignoreCollision)
|
||||
return {
|
||||
canResizeHorizontally,
|
||||
canResizeVertically,
|
||||
};
|
||||
|
||||
if (
|
||||
newRowCols &&
|
||||
(newRowCols.rightColumn > getSnapColumns() ||
|
||||
newRowCols.leftColumn < 0 ||
|
||||
newRowCols.rightColumn - newRowCols.leftColumn < 2)
|
||||
) {
|
||||
canResizeHorizontally = false;
|
||||
}
|
||||
|
||||
if (
|
||||
newRowCols &&
|
||||
(newRowCols.topRow < 0 || newRowCols.bottomRow - newRowCols.topRow < 4)
|
||||
) {
|
||||
canResizeVertically = false;
|
||||
}
|
||||
|
||||
const resizedPositions = {
|
||||
id: props.widgetId,
|
||||
left: newRowCols.leftColumn,
|
||||
top: newRowCols.topRow,
|
||||
bottom: newRowCols.bottomRow,
|
||||
right: newRowCols.rightColumn,
|
||||
};
|
||||
|
||||
// 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,
|
||||
);
|
||||
return {
|
||||
canResizeHorizontally,
|
||||
canResizeVertically,
|
||||
resizedPositions,
|
||||
};
|
||||
};
|
||||
|
||||
// onResizeStop handler
|
||||
|
|
@ -254,6 +180,8 @@ export const ResizableComponent = memo(function ResizableComponent(
|
|||
updateWidget(WidgetOperations.RESIZE, props.widgetId, {
|
||||
...newRowCols,
|
||||
parentId: props.parentId,
|
||||
snapColumnSpace: props.parentColumnSpace,
|
||||
snapRowSpace: props.parentRowSpace,
|
||||
});
|
||||
}
|
||||
// Tell the Canvas that we've stopped resizing
|
||||
|
|
@ -328,6 +256,27 @@ export const ResizableComponent = memo(function ResizableComponent(
|
|||
selectedWidgets &&
|
||||
selectedWidgets.length > 1 &&
|
||||
selectedWidgets.includes(props.widgetId);
|
||||
const { updateDropTargetRows } = useContext(DropTargetContext);
|
||||
|
||||
const gridProps = {
|
||||
parentColumnSpace: props.parentColumnSpace,
|
||||
parentRowSpace: props.parentRowSpace,
|
||||
paddingOffset: props.paddingOffset,
|
||||
maxGridColumns: GridDefaults.DEFAULT_GRID_COLUMNS,
|
||||
};
|
||||
|
||||
const originalPositions = {
|
||||
id: props.widgetId,
|
||||
left: props.leftColumn,
|
||||
top: props.topRow,
|
||||
bottom: props.bottomRow,
|
||||
right: props.rightColumn,
|
||||
};
|
||||
const updateBottomRow = (bottom: number) => {
|
||||
if (props.parentId) {
|
||||
updateDropTargetRows && updateDropTargetRows(props.parentId, bottom);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Resizable
|
||||
|
|
@ -335,12 +284,16 @@ export const ResizableComponent = memo(function ResizableComponent(
|
|||
componentHeight={dimensions.height}
|
||||
componentWidth={dimensions.width}
|
||||
enable={isEnabled}
|
||||
getResizedPositions={getResizedPositions}
|
||||
gridProps={gridProps}
|
||||
handles={handles}
|
||||
isColliding={isColliding}
|
||||
onStart={handleResizeStart}
|
||||
onStop={updateSize}
|
||||
ref={resizableRef}
|
||||
originalPositions={originalPositions}
|
||||
parentId={props.parentId}
|
||||
snapGrid={{ x: props.parentColumnSpace, y: props.parentRowSpace }}
|
||||
updateBottomRow={updateBottomRow}
|
||||
widgetId={props.widgetId}
|
||||
// Used only for performance tracking, can be removed after optimization.
|
||||
zWidgetId={props.widgetId}
|
||||
zWidgetType={props.type}
|
||||
|
|
|
|||
|
|
@ -6,10 +6,23 @@ export type UIElementSize = { height: number; width: number };
|
|||
|
||||
export const RESIZABLE_CONTAINER_BORDER_THEME_INDEX = 1;
|
||||
|
||||
export type WidgetPosition = {
|
||||
rightColumn: number;
|
||||
leftColumn: number;
|
||||
bottomRow: number;
|
||||
topRow: number;
|
||||
parentRowSpace: number;
|
||||
parentColumnSpace: number;
|
||||
};
|
||||
|
||||
export type WidgetExtendedPosition = WidgetPosition & {
|
||||
paddingOffset: number;
|
||||
};
|
||||
|
||||
export const computeRowCols = (
|
||||
delta: UIElementSize,
|
||||
position: XYCord,
|
||||
props: WidgetProps,
|
||||
props: WidgetPosition,
|
||||
) => {
|
||||
return {
|
||||
leftColumn: Math.round(
|
||||
|
|
|
|||
|
|
@ -7,6 +7,16 @@ export type OccupiedSpace = {
|
|||
parentId?: string;
|
||||
};
|
||||
|
||||
export type WidgetSpace = {
|
||||
left: number;
|
||||
right: number;
|
||||
top: number;
|
||||
bottom: number;
|
||||
id: string;
|
||||
type: string;
|
||||
parentId?: string;
|
||||
};
|
||||
|
||||
export const zIndexLayers = {
|
||||
PROPERTY_PANE: "z-3",
|
||||
ENTITY_EXPLORER: "z-3",
|
||||
|
|
|
|||
|
|
@ -231,6 +231,7 @@ export const ReduxActionTypes = {
|
|||
WIDGET_CHILD_ADDED: "WIDGET_CHILD_ADDED",
|
||||
WIDGET_REMOVE_CHILD: "WIDGET_REMOVE_CHILD",
|
||||
WIDGETS_MOVE: "WIDGETS_MOVE",
|
||||
WIDGETS_ADD_CHILD_AND_MOVE: "WIDGETS_ADD_CHILD_AND_MOVE",
|
||||
WIDGET_RESIZE: "WIDGET_RESIZE",
|
||||
WIDGET_DELETE: "WIDGET_DELETE",
|
||||
WIDGET_BULK_DELETE: "WIDGET_BULK_DELETE",
|
||||
|
|
@ -810,6 +811,7 @@ export const ReduxActionErrorTypes = {
|
|||
RESTART_SERVER_ERROR: "RESTART_SERVER_ERROR",
|
||||
UPDATE_JS_ACTION_BODY_ERROR: "UPDATE_JS_ACTION_BODY_ERROR",
|
||||
DELETE_ORG_ERROR: "DELETE_ORG_ERROR",
|
||||
REFLOW_BETA_FLAGS_INIT_ERROR: "REFLOW_BETA_FLAGS_INIT_ERROR",
|
||||
};
|
||||
|
||||
export const ReduxFormActionTypes = {
|
||||
|
|
@ -824,6 +826,14 @@ export enum ReplayReduxActionTypes {
|
|||
REDO = "redo",
|
||||
}
|
||||
|
||||
export const ReflowReduxActionTypes = {
|
||||
STOP_REFLOW: "STOP_REFLOW",
|
||||
REFLOW_MOVE: "REFLOW_MOVE",
|
||||
ENABLE_REFLOW: "ENABLE_REFLOW",
|
||||
ONBOARDING_UPDATE: "ONBOARDING_UPDATE",
|
||||
FORCE_STOP_ON_BOARDING: "FORCE_STOP_ON_BOARDING",
|
||||
};
|
||||
|
||||
export const WidgetReduxActionTypes: { [key: string]: string } = {
|
||||
WIDGET_ADD_CHILD: "WIDGET_ADD_CHILD",
|
||||
WIDGET_CHILD_ADDED: "WIDGET_CHILD_ADDED",
|
||||
|
|
|
|||
|
|
@ -19,10 +19,10 @@ export const PopoverStyles = createGlobalStyle`
|
|||
min-width: 233px !important ;
|
||||
}
|
||||
}
|
||||
.comments-onboarding-carousel .${Classes.OVERLAY_CONTENT} {
|
||||
.onboarding-carousel .${Classes.OVERLAY_CONTENT} {
|
||||
filter: drop-shadow(0px 6px 20px rgba(0, 0, 0, 0.15));
|
||||
}
|
||||
.bp3-modal-widget.comments-onboarding-carousel-portal {
|
||||
.bp3-modal-widget.onboarding-carousel-portal {
|
||||
z-index: ${Layers.help} !important;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import lodash from "lodash";
|
|||
import { getAbsolutePixels } from "utils/helpers";
|
||||
import { UpdatedMainContainer } from "test/testMockedWidgets";
|
||||
import { AppState } from "reducers";
|
||||
import { generateReactKey } from "utils/generators";
|
||||
|
||||
const renderNestedComponent = () => {
|
||||
const initialState = (store.getState() as unknown) as Partial<AppState>;
|
||||
|
|
@ -626,16 +627,29 @@ describe("Drag and Drop widgets into Main container", () => {
|
|||
|
||||
it("Disallow drag if widget not focused", () => {
|
||||
const initialState = (store.getState() as unknown) as Partial<AppState>;
|
||||
const containerId = generateReactKey();
|
||||
const canvasId = generateReactKey();
|
||||
|
||||
const children: any = buildChildren([
|
||||
const canvasWidget = buildChildren([
|
||||
{
|
||||
type: "CANVAS_WIDGET",
|
||||
parentId: containerId,
|
||||
children: [],
|
||||
widgetId: canvasId,
|
||||
dropDisabled: true,
|
||||
},
|
||||
]);
|
||||
const containerChildren: any = buildChildren([
|
||||
{
|
||||
type: "CONTAINER_WIDGET",
|
||||
children: canvasWidget,
|
||||
widgetId: containerId,
|
||||
parentId: "0",
|
||||
},
|
||||
]);
|
||||
|
||||
const dsl: any = widgetCanvasFactory.build({
|
||||
children,
|
||||
children: containerChildren,
|
||||
});
|
||||
|
||||
spyGetCanvasWidgetDsl.mockImplementation(mockGetCanvasWidgetDsl);
|
||||
|
|
|
|||
|
|
@ -16,6 +16,16 @@ import TooltipComponent from "components/ads/Tooltip";
|
|||
import Icon, { IconName, IconSize } from "components/ads/Icon";
|
||||
import { updateApplicationLayout } from "actions/applicationActions";
|
||||
|
||||
import { setEnableReflowAction } from "actions/reflowActions";
|
||||
import Checkbox from "components/ads/Checkbox";
|
||||
import { ReactComponent as BetaIcon } from "assets/icons/menu/beta.svg";
|
||||
import styled from "styled-components";
|
||||
import { isReflowEnabled } from "selectors/widgetReflowSelectors";
|
||||
import { setReflowBetaFlag } from "utils/storage";
|
||||
import AnalyticsUtil from "utils/AnalyticsUtil";
|
||||
import { getCurrentUser } from "selectors/usersSelectors";
|
||||
import { User } from "constants/userConstants";
|
||||
|
||||
interface AppsmithLayoutConfigOption {
|
||||
name: string;
|
||||
type: SupportedLayouts;
|
||||
|
|
@ -53,12 +63,26 @@ const AppsmithLayouts: AppsmithLayoutConfigOption[] = [
|
|||
icon: "mobile",
|
||||
},
|
||||
];
|
||||
const ReflowBetaWrapper = styled.div`
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
.beta-icon {
|
||||
fill: #feb811;
|
||||
rect {
|
||||
stroke: #feb811;
|
||||
}
|
||||
path {
|
||||
fill: #fff;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export function MainContainerLayoutControl() {
|
||||
const dispatch = useDispatch();
|
||||
const appId = useSelector(getCurrentApplicationId);
|
||||
const appLayout = useSelector(getCurrentApplicationLayout);
|
||||
|
||||
const shouldResize = useSelector(isReflowEnabled);
|
||||
const user: User | undefined = useSelector(getCurrentUser);
|
||||
/**
|
||||
* return selected layout. if there is no app
|
||||
* layout, use the default one ( fluid )
|
||||
|
|
@ -89,6 +113,18 @@ export function MainContainerLayoutControl() {
|
|||
[dispatch, appLayout],
|
||||
);
|
||||
|
||||
const reflowBetaToggle = (isChecked: boolean) => {
|
||||
if (user?.email) {
|
||||
setReflowBetaFlag(user.email, isChecked);
|
||||
}
|
||||
dispatch(setEnableReflowAction(isChecked));
|
||||
AnalyticsUtil.logEvent("REFLOW_BETA_FLAG", {
|
||||
enabled: isChecked,
|
||||
});
|
||||
};
|
||||
const appsmithEmailRegex = /@appsmith.com/g;
|
||||
const canReflow = user && user.email && appsmithEmailRegex.test(user.email);
|
||||
|
||||
return (
|
||||
<div className="px-3 space-y-2 t--layout-control-wrapper">
|
||||
<p className="text-sm text-gray-700">Canvas Size</p>
|
||||
|
|
@ -123,6 +159,16 @@ export function MainContainerLayoutControl() {
|
|||
);
|
||||
})}
|
||||
</div>
|
||||
{canReflow && (
|
||||
<ReflowBetaWrapper>
|
||||
<Checkbox
|
||||
isDefaultChecked={shouldResize}
|
||||
label="New Reflow & Resize"
|
||||
onCheckChange={reflowBetaToggle}
|
||||
/>
|
||||
<BetaIcon className="beta-icon" />
|
||||
</ReflowBetaWrapper>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ export interface CanvasDraggingArenaProps {
|
|||
snapColumnSpace: number;
|
||||
snapRows: number;
|
||||
snapRowSpace: number;
|
||||
parentId?: string;
|
||||
widgetId: string;
|
||||
}
|
||||
|
||||
|
|
@ -39,6 +40,7 @@ export function CanvasDraggingArena({
|
|||
canExtend,
|
||||
dropDisabled = false,
|
||||
noPad,
|
||||
parentId = "",
|
||||
snapColumnSpace,
|
||||
snapRows,
|
||||
snapRowSpace,
|
||||
|
|
@ -54,6 +56,7 @@ export function CanvasDraggingArena({
|
|||
canExtend,
|
||||
dropDisabled,
|
||||
noPad,
|
||||
parentId,
|
||||
snapColumnSpace,
|
||||
snapRows,
|
||||
snapRowSpace,
|
||||
|
|
|
|||
|
|
@ -150,8 +150,8 @@ export function CanvasSelectionArena({
|
|||
|
||||
useCanvasDragToScroll(
|
||||
canvasRef,
|
||||
isCurrentWidgetDrawing,
|
||||
isDraggingForSelection,
|
||||
isCurrentWidgetDrawing || isResizing,
|
||||
isDraggingForSelection || isResizing,
|
||||
snapRows,
|
||||
canExtend,
|
||||
);
|
||||
|
|
@ -446,15 +446,11 @@ export function CanvasSelectionArena({
|
|||
snapRowSpace,
|
||||
]);
|
||||
|
||||
// Resizing state still shows selection arena to aid with scroll behavior
|
||||
|
||||
const shouldShow =
|
||||
appMode === APP_MODE.EDIT &&
|
||||
!(
|
||||
isDragging ||
|
||||
isResizing ||
|
||||
isCommentMode ||
|
||||
isPreviewMode ||
|
||||
dropDisabled
|
||||
);
|
||||
!(isDragging || isCommentMode || isPreviewMode || dropDisabled);
|
||||
|
||||
return shouldShow ? (
|
||||
<StyledSelectionCanvas
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ import { GitSyncReducerState } from "./uiReducers/gitSyncReducer";
|
|||
import { AppCollabReducerState } from "./uiReducers/appCollabReducer";
|
||||
import { CrudInfoModalReduxState } from "./uiReducers/crudInfoModalReducer";
|
||||
import { FormEvaluationState } from "./evaluationReducers/formEvaluationReducer";
|
||||
import { widgetReflowState } from "./uiReducers/reflowReducer";
|
||||
import SettingsReducer, { SettingsReduxState } from "./settingsReducer";
|
||||
|
||||
const appReducer = combineReducers({
|
||||
|
|
@ -106,6 +107,7 @@ export interface AppState {
|
|||
gitSync: GitSyncReducerState;
|
||||
appCollab: AppCollabReducerState;
|
||||
crudInfoModal: CrudInfoModalReduxState;
|
||||
widgetReflow: widgetReflowState;
|
||||
};
|
||||
entities: {
|
||||
canvasWidgets: CanvasWidgetsReduxState;
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ import appCollabReducer from "./appCollabReducer";
|
|||
import canvasSelectionReducer from "./canvasSelectionReducer";
|
||||
import gitSyncReducer from "./gitSyncReducer";
|
||||
import crudInfoModalReducer from "./crudInfoModalReducer";
|
||||
import { widgetReflowReducer } from "./reflowReducer";
|
||||
import jsObjectNameReducer from "./jsObjectNameReducer";
|
||||
|
||||
const uiReducer = combineReducers({
|
||||
|
|
@ -77,6 +78,7 @@ const uiReducer = combineReducers({
|
|||
gitSync: gitSyncReducer,
|
||||
appCollab: appCollabReducer,
|
||||
crudInfoModal: crudInfoModalReducer,
|
||||
widgetReflow: widgetReflowReducer,
|
||||
});
|
||||
|
||||
export default uiReducer;
|
||||
|
|
|
|||
52
app/client/src/reducers/uiReducers/reflowReducer.ts
Normal file
52
app/client/src/reducers/uiReducers/reflowReducer.ts
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
import { createReducer } from "utils/AppsmithUtils";
|
||||
import {
|
||||
ReduxAction,
|
||||
ReflowReduxActionTypes,
|
||||
} from "constants/ReduxActionConstants";
|
||||
import { ReflowedSpaceMap } from "reflow/reflowTypes";
|
||||
|
||||
const initialState: widgetReflowState = {
|
||||
isReflowing: false,
|
||||
reflowingWidgets: {},
|
||||
enableReflow: true,
|
||||
};
|
||||
|
||||
export const widgetReflowReducer = createReducer(initialState, {
|
||||
[ReflowReduxActionTypes.STOP_REFLOW]: ({
|
||||
enableReflow,
|
||||
}: widgetReflowState) => {
|
||||
return {
|
||||
isReflowing: false,
|
||||
enableReflow,
|
||||
};
|
||||
},
|
||||
[ReflowReduxActionTypes.REFLOW_MOVE]: (
|
||||
{ enableReflow }: widgetReflowState,
|
||||
action: ReduxAction<{ reflowingWidgets: ReflowedSpaceMap }>,
|
||||
) => {
|
||||
return {
|
||||
isReflowing: true,
|
||||
reflowingWidgets: { ...action.payload },
|
||||
enableReflow,
|
||||
};
|
||||
},
|
||||
[ReflowReduxActionTypes.ENABLE_REFLOW]: (
|
||||
state: widgetReflowState,
|
||||
action: ReduxAction<boolean>,
|
||||
) => {
|
||||
return { ...state, enableReflow: action.payload };
|
||||
},
|
||||
});
|
||||
|
||||
export type widgetReflow = {
|
||||
isReflowing: boolean;
|
||||
reflowingWidgets: ReflowedSpaceMap;
|
||||
};
|
||||
|
||||
export type widgetReflowState = widgetReflow & {
|
||||
enableReflow: boolean;
|
||||
};
|
||||
|
||||
export type Reflow = {
|
||||
reflowingWidgets?: ReflowedSpaceMap;
|
||||
};
|
||||
90
app/client/src/reflow/index.ts
Normal file
90
app/client/src/reflow/index.ts
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
import { OccupiedSpace } from "constants/CanvasEditorConstants";
|
||||
import { getMovementMap } from "./reflowHelpers";
|
||||
import { CollidingSpaceMap, GridProps, ReflowDirection } from "./reflowTypes";
|
||||
import {
|
||||
changeExitContainerDirection,
|
||||
filterSpaceById,
|
||||
getCollidingSpaces,
|
||||
getDelta,
|
||||
getIsHorizontalMove,
|
||||
getShouldReflow,
|
||||
} from "./reflowUtils";
|
||||
|
||||
/**
|
||||
* Reflow method that returns the displacement metrics of all other colliding spaces
|
||||
*
|
||||
* @param newPositions new/current positions of the space/block
|
||||
* @param OGPositions original positions of the space before movement
|
||||
* @param occupiedSpaces array of all the occupied spaces on the canvas
|
||||
* @param direction direction of movement of the moving space
|
||||
* @param gridProps properties of the canvas's grid
|
||||
* @param forceDirection boolean to force the direction on certain scenarioes
|
||||
* @param shouldResize boolean to indicate if colliding spaces should resize
|
||||
* @param immediateExitContainer boolean to indicate if the space exitted a nested canvas
|
||||
* @param prevPositions last known position of the space
|
||||
* @param prevCollidingSpaces last known colliding spaces of the dragging/resising space
|
||||
* @returns movement information of the dragging/resizing space and other colliding spaces
|
||||
*/
|
||||
export function reflow(
|
||||
newPositions: OccupiedSpace,
|
||||
OGPositions: OccupiedSpace,
|
||||
occupiedSpaces: OccupiedSpace[],
|
||||
direction: ReflowDirection,
|
||||
gridProps: GridProps,
|
||||
forceDirection = false,
|
||||
shouldResize = true,
|
||||
immediateExitContainer?: string,
|
||||
prevPositions?: OccupiedSpace,
|
||||
prevCollidingSpaces?: CollidingSpaceMap,
|
||||
) {
|
||||
const isHorizontalMove = getIsHorizontalMove(newPositions, prevPositions);
|
||||
const filteredOccupiedSpace = filterSpaceById(
|
||||
newPositions.id,
|
||||
occupiedSpaces,
|
||||
);
|
||||
|
||||
const { collidingSpaceMap, isColliding } = getCollidingSpaces(
|
||||
newPositions,
|
||||
direction,
|
||||
filteredOccupiedSpace,
|
||||
isHorizontalMove,
|
||||
prevPositions,
|
||||
prevCollidingSpaces,
|
||||
forceDirection,
|
||||
);
|
||||
|
||||
if (!isColliding || !OGPositions || direction === ReflowDirection.UNSET) {
|
||||
return {
|
||||
movementLimit: {
|
||||
canHorizontalMove: true,
|
||||
canVerticalMove: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
changeExitContainerDirection(
|
||||
collidingSpaceMap,
|
||||
immediateExitContainer,
|
||||
direction,
|
||||
);
|
||||
|
||||
const delta = getDelta(OGPositions, newPositions, direction);
|
||||
|
||||
const { movementMap, newPositionsMovement } = getMovementMap(
|
||||
filteredOccupiedSpace,
|
||||
newPositions,
|
||||
collidingSpaceMap,
|
||||
gridProps,
|
||||
delta,
|
||||
shouldResize,
|
||||
);
|
||||
|
||||
const movementLimit = getShouldReflow(newPositionsMovement, delta);
|
||||
|
||||
return {
|
||||
movementLimit,
|
||||
movementMap,
|
||||
newPositionsMovement,
|
||||
collidingSpaceMap,
|
||||
};
|
||||
}
|
||||
466
app/client/src/reflow/reflowHelpers.ts
Normal file
466
app/client/src/reflow/reflowHelpers.ts
Normal file
|
|
@ -0,0 +1,466 @@
|
|||
import { OccupiedSpace } from "constants/CanvasEditorConstants";
|
||||
import { GridDefaults } from "constants/WidgetConstants";
|
||||
import { isEmpty } from "lodash";
|
||||
import {
|
||||
CollidingSpace,
|
||||
CollidingSpaceMap,
|
||||
CollisionAccessors,
|
||||
CollisionTree,
|
||||
Delta,
|
||||
DirectionalMovement,
|
||||
GridProps,
|
||||
HORIZONTAL_RESIZE_LIMIT,
|
||||
ReflowDirection,
|
||||
ReflowedSpaceMap,
|
||||
VERTICAL_RESIZE_LIMIT,
|
||||
} from "./reflowTypes";
|
||||
import {
|
||||
getAccessor,
|
||||
getCollidingSpacesInDirection,
|
||||
getMaxX,
|
||||
getMaxY,
|
||||
getReflowDistance,
|
||||
getResizedDimension,
|
||||
getResizedDimensions,
|
||||
shouldReplaceOldMovement,
|
||||
sortCollidingSpacesByDistance,
|
||||
} from "./reflowUtils";
|
||||
|
||||
/**
|
||||
* returns movement map of all the cascading colliding spaces
|
||||
* @param occupiedSpaces array of all the occupied spaces on the canvas
|
||||
* @param newPositions new/current positions of the space/block
|
||||
* @param collidingSpaceMap Map of Colliding spaces of the dragging/resizing space
|
||||
* @param gridProps properties of the canvas's grid
|
||||
* @param delta X and Y coordinate displacement of the newPosition from the original position
|
||||
* @param shouldResize boolean to indicate if colliding spaces should resize
|
||||
* @returns movement map of all the cascading colliding spaces
|
||||
*/
|
||||
export function getMovementMap(
|
||||
occupiedSpaces: OccupiedSpace[],
|
||||
newPositions: OccupiedSpace,
|
||||
collidingSpaceMap: CollidingSpaceMap,
|
||||
gridProps: GridProps,
|
||||
delta = { X: 0, Y: 0 },
|
||||
shouldResize = true,
|
||||
) {
|
||||
const movementMap: ReflowedSpaceMap = {};
|
||||
const collisionTree = getCollisionTree(
|
||||
occupiedSpaces,
|
||||
newPositions,
|
||||
collidingSpaceMap,
|
||||
);
|
||||
|
||||
if (
|
||||
!collisionTree ||
|
||||
!collisionTree.children ||
|
||||
Object.keys(collisionTree.children).length <= 0
|
||||
) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const childrenKeys = Object.keys(collisionTree.children);
|
||||
|
||||
const directionalVariables: {
|
||||
[direction: string]: [number, number, CollisionAccessors, ReflowDirection];
|
||||
} = {};
|
||||
|
||||
for (const childKey of childrenKeys) {
|
||||
const childNode = collisionTree.children[childKey];
|
||||
const childDirection = collidingSpaceMap[childNode.id].direction;
|
||||
const directionalAccessors = getAccessor(childDirection);
|
||||
|
||||
const distanceBeforeCollision =
|
||||
childNode[directionalAccessors.oppositeDirection] -
|
||||
newPositions[directionalAccessors.direction];
|
||||
|
||||
const { depth, occupiedSpace } = getMovementMapHelper(
|
||||
childNode,
|
||||
movementMap,
|
||||
delta,
|
||||
gridProps,
|
||||
directionalAccessors,
|
||||
childDirection,
|
||||
0,
|
||||
childNode[directionalAccessors.direction],
|
||||
distanceBeforeCollision,
|
||||
shouldResize,
|
||||
);
|
||||
|
||||
let staticDepth = 0,
|
||||
maxOccupiedSpace = 0;
|
||||
if (directionalVariables[childDirection]) {
|
||||
[staticDepth, maxOccupiedSpace] = directionalVariables[childDirection];
|
||||
}
|
||||
staticDepth = Math.max(staticDepth, depth);
|
||||
maxOccupiedSpace = Math.max(maxOccupiedSpace, occupiedSpace);
|
||||
directionalVariables[childDirection] = [
|
||||
staticDepth,
|
||||
maxOccupiedSpace,
|
||||
directionalAccessors,
|
||||
childDirection,
|
||||
];
|
||||
}
|
||||
|
||||
const directionalKeys = Object.keys(directionalVariables);
|
||||
|
||||
const directionalMovements: DirectionalMovement[] = [];
|
||||
|
||||
for (const directionKey of directionalKeys) {
|
||||
const [
|
||||
staticDepth,
|
||||
maxOccupiedSpace,
|
||||
accessors,
|
||||
reflowDirection,
|
||||
] = directionalVariables[directionKey];
|
||||
const maxMethod = accessors.isHorizontal ? getMaxX : getMaxY;
|
||||
const gridDistance = accessors.isHorizontal
|
||||
? gridProps.parentColumnSpace
|
||||
: gridProps.parentRowSpace;
|
||||
const coordinateKey = accessors.isHorizontal ? "X" : "Y";
|
||||
const maxMovement =
|
||||
maxMethod(
|
||||
collisionTree,
|
||||
gridProps,
|
||||
reflowDirection,
|
||||
staticDepth,
|
||||
maxOccupiedSpace,
|
||||
shouldResize,
|
||||
) +
|
||||
delta[coordinateKey] +
|
||||
accessors.directionIndicator * gridDistance;
|
||||
directionalMovements.push({
|
||||
maxMovement,
|
||||
directionalIndicator: accessors.directionIndicator,
|
||||
coordinateKey,
|
||||
isHorizontal: accessors.isHorizontal,
|
||||
});
|
||||
}
|
||||
const newPositionsMovement = {
|
||||
id: collisionTree.id,
|
||||
directionalMovements,
|
||||
};
|
||||
|
||||
return {
|
||||
newPositionsMovement,
|
||||
movementMap,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively create a tree of Space collisions
|
||||
* @param occupiedSpaces array of all the occupied spaces on the canvas
|
||||
* @param newPositions new/current positions of the space/block
|
||||
* @param collidingSpaceMap Map of Colliding spaces of the dragging/resizing space
|
||||
* @returns Collisions in a tree structure
|
||||
*/
|
||||
function getCollisionTree(
|
||||
occupiedSpaces: OccupiedSpace[],
|
||||
newPositions: OccupiedSpace,
|
||||
collidingSpaceMap: CollidingSpaceMap,
|
||||
) {
|
||||
const collisionTree: CollisionTree = {
|
||||
...newPositions,
|
||||
children: {},
|
||||
};
|
||||
|
||||
const collidingSpaces = Object.values(collidingSpaceMap);
|
||||
sortCollidingSpacesByDistance(collidingSpaces, newPositions, false);
|
||||
|
||||
const globalCompletedTree: { [key: string]: boolean } = {
|
||||
[newPositions.id]: true,
|
||||
};
|
||||
for (const collidingSpace of collidingSpaces) {
|
||||
const directionalAccessors = getAccessor(collidingSpace.direction);
|
||||
|
||||
if (!globalCompletedTree[collidingSpace.id]) {
|
||||
const currentCollisionTree = getCollisionTreeHelper(
|
||||
occupiedSpaces,
|
||||
collidingSpace,
|
||||
directionalAccessors,
|
||||
collidingSpace[directionalAccessors.oppositeDirection] -
|
||||
collisionTree[directionalAccessors.direction],
|
||||
collidingSpace.direction,
|
||||
globalCompletedTree,
|
||||
);
|
||||
|
||||
if (currentCollisionTree && collisionTree.children) {
|
||||
collisionTree.children[collidingSpace.id] = { ...currentCollisionTree };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return collisionTree;
|
||||
}
|
||||
|
||||
function getCollisionTreeHelper(
|
||||
occupiedSpaces: OccupiedSpace[],
|
||||
collidingSpace: CollidingSpace,
|
||||
accessors: CollisionAccessors,
|
||||
distanceBeforeCollision: number,
|
||||
direction: ReflowDirection,
|
||||
globalProcessedNodes: { [key: string]: boolean },
|
||||
emptySpaces = 0,
|
||||
processedNodes: { [key: string]: boolean } = {},
|
||||
) {
|
||||
if (!collidingSpace) return;
|
||||
const collisionTree: CollisionTree = { ...collidingSpace, children: {} };
|
||||
|
||||
const resizedDimensions = getResizedDimensions(
|
||||
collisionTree,
|
||||
distanceBeforeCollision,
|
||||
emptySpaces,
|
||||
accessors,
|
||||
);
|
||||
|
||||
const {
|
||||
collidingSpaces,
|
||||
occupiedSpacesInDirection,
|
||||
} = getCollidingSpacesInDirection(
|
||||
resizedDimensions,
|
||||
direction,
|
||||
occupiedSpaces,
|
||||
);
|
||||
|
||||
sortCollidingSpacesByDistance(collidingSpaces, collisionTree);
|
||||
|
||||
const currentProcessedNodes: { [key: string]: boolean } = {};
|
||||
for (const currentCollidingSpace of collidingSpaces) {
|
||||
const nextEmptySpaces =
|
||||
emptySpaces +
|
||||
currentCollidingSpace[accessors.oppositeDirection] -
|
||||
collisionTree[accessors.direction];
|
||||
|
||||
if (!currentProcessedNodes[currentCollidingSpace.id]) {
|
||||
const currentCollisionTree = getCollisionTreeHelper(
|
||||
occupiedSpacesInDirection,
|
||||
currentCollidingSpace,
|
||||
accessors,
|
||||
distanceBeforeCollision,
|
||||
direction,
|
||||
globalProcessedNodes,
|
||||
nextEmptySpaces,
|
||||
currentProcessedNodes,
|
||||
);
|
||||
|
||||
currentProcessedNodes[currentCollidingSpace.id] = true;
|
||||
|
||||
if (currentCollisionTree && collisionTree.children) {
|
||||
collisionTree.children[currentCollidingSpace.id] = {
|
||||
...currentCollisionTree,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const currentProcessedNodesKeys = Object.keys(currentProcessedNodes);
|
||||
for (const key of currentProcessedNodesKeys) processedNodes[key] = true;
|
||||
globalProcessedNodes[collisionTree.id] = true;
|
||||
return collisionTree;
|
||||
}
|
||||
|
||||
function getMovementMapHelper(
|
||||
collisionTree: CollisionTree,
|
||||
movementMap: ReflowedSpaceMap,
|
||||
dimensions = { X: 0, Y: 0 },
|
||||
gridProps: GridProps,
|
||||
accessors: CollisionAccessors,
|
||||
direction: ReflowDirection,
|
||||
emptySpaces = 0,
|
||||
prevWidgetdistance: number,
|
||||
distanceBeforeCollision = 0,
|
||||
shouldResize: boolean,
|
||||
) {
|
||||
let maxOccupiedSpace = 0,
|
||||
depth = 0;
|
||||
let currentEmptySpaces = emptySpaces;
|
||||
|
||||
if (collisionTree.children && !isEmpty(collisionTree.children)) {
|
||||
const childNodes = Object.values(collisionTree.children);
|
||||
|
||||
for (const childNode of childNodes) {
|
||||
const nextEmptySpaces =
|
||||
emptySpaces +
|
||||
Math.abs(prevWidgetdistance - childNode[accessors.oppositeDirection]);
|
||||
|
||||
const {
|
||||
currentEmptySpaces: childEmptySpaces,
|
||||
depth: currentDepth,
|
||||
occupiedSpace,
|
||||
} = getMovementMapHelper(
|
||||
childNode,
|
||||
movementMap,
|
||||
dimensions,
|
||||
gridProps,
|
||||
accessors,
|
||||
direction,
|
||||
nextEmptySpaces,
|
||||
childNode[accessors.direction],
|
||||
distanceBeforeCollision,
|
||||
shouldResize,
|
||||
);
|
||||
|
||||
if (maxOccupiedSpace < occupiedSpace) {
|
||||
currentEmptySpaces = childEmptySpaces;
|
||||
}
|
||||
|
||||
maxOccupiedSpace = Math.max(maxOccupiedSpace, occupiedSpace || 0);
|
||||
depth = Math.max(depth, currentDepth);
|
||||
}
|
||||
} else {
|
||||
if (direction === ReflowDirection.RIGHT) {
|
||||
currentEmptySpaces +=
|
||||
GridDefaults.DEFAULT_GRID_COLUMNS - collisionTree.right;
|
||||
} else if (direction !== ReflowDirection.BOTTOM) {
|
||||
currentEmptySpaces += collisionTree[accessors.direction];
|
||||
}
|
||||
}
|
||||
|
||||
const getSpaceMovement = accessors.isHorizontal
|
||||
? getHorizontalSpaceMovement
|
||||
: getVerticalSpaceMovement;
|
||||
const movementObj = getSpaceMovement(
|
||||
collisionTree,
|
||||
gridProps,
|
||||
direction,
|
||||
maxOccupiedSpace,
|
||||
depth,
|
||||
distanceBeforeCollision,
|
||||
emptySpaces,
|
||||
currentEmptySpaces,
|
||||
dimensions,
|
||||
shouldResize,
|
||||
);
|
||||
|
||||
if (
|
||||
!shouldReplaceOldMovement(
|
||||
movementMap[collisionTree.id],
|
||||
movementObj,
|
||||
direction,
|
||||
)
|
||||
) {
|
||||
return {
|
||||
occupiedSpace:
|
||||
(movementMap[collisionTree.id].maxOccupiedSpace || 0) +
|
||||
collisionTree[accessors.parallelMax] -
|
||||
collisionTree[accessors.parallelMin],
|
||||
depth: (movementMap[collisionTree.id].depth || 0) + 1,
|
||||
currentEmptySpaces: movementMap[collisionTree.id].emptySpaces || 0,
|
||||
};
|
||||
}
|
||||
|
||||
movementMap[collisionTree.id] = { ...movementObj };
|
||||
return {
|
||||
occupiedSpace:
|
||||
maxOccupiedSpace +
|
||||
collisionTree[accessors.parallelMax] -
|
||||
collisionTree[accessors.parallelMin],
|
||||
depth: depth + 1,
|
||||
currentEmptySpaces,
|
||||
};
|
||||
}
|
||||
|
||||
function getHorizontalSpaceMovement(
|
||||
collisionTree: CollisionTree,
|
||||
gridProps: GridProps,
|
||||
direction: ReflowDirection,
|
||||
maxOccupiedSpace: number,
|
||||
depth: number,
|
||||
distanceBeforeCollision: number,
|
||||
emptySpaces: number,
|
||||
currentEmptySpaces: number,
|
||||
{ X }: Delta,
|
||||
shouldResize: boolean,
|
||||
) {
|
||||
const maxX = getMaxX(
|
||||
collisionTree,
|
||||
gridProps,
|
||||
direction,
|
||||
depth,
|
||||
maxOccupiedSpace,
|
||||
shouldResize,
|
||||
);
|
||||
const width = getResizedDimension(
|
||||
collisionTree,
|
||||
direction,
|
||||
X,
|
||||
maxX,
|
||||
distanceBeforeCollision,
|
||||
gridProps.parentColumnSpace,
|
||||
emptySpaces,
|
||||
HORIZONTAL_RESIZE_LIMIT,
|
||||
shouldResize,
|
||||
);
|
||||
const spaceMovement = {
|
||||
X: getReflowDistance(
|
||||
collisionTree,
|
||||
direction,
|
||||
maxX,
|
||||
distanceBeforeCollision,
|
||||
width,
|
||||
emptySpaces,
|
||||
gridProps.parentColumnSpace,
|
||||
),
|
||||
distanceBeforeCollision,
|
||||
maxX,
|
||||
width,
|
||||
emptySpaces: currentEmptySpaces,
|
||||
depth,
|
||||
maxOccupiedSpace,
|
||||
};
|
||||
|
||||
return spaceMovement;
|
||||
}
|
||||
|
||||
function getVerticalSpaceMovement(
|
||||
collisionTree: CollisionTree,
|
||||
gridProps: GridProps,
|
||||
direction: ReflowDirection,
|
||||
maxOccupiedSpace: number,
|
||||
depth: number,
|
||||
distanceBeforeCollision: number,
|
||||
emptySpaces: number,
|
||||
currentEmptySpaces: number,
|
||||
{ Y }: Delta,
|
||||
shouldResize: boolean,
|
||||
) {
|
||||
const maxY = getMaxY(
|
||||
collisionTree,
|
||||
gridProps,
|
||||
direction,
|
||||
depth,
|
||||
maxOccupiedSpace,
|
||||
shouldResize,
|
||||
);
|
||||
const height = getResizedDimension(
|
||||
collisionTree,
|
||||
direction,
|
||||
Y,
|
||||
maxY,
|
||||
distanceBeforeCollision,
|
||||
gridProps.parentRowSpace,
|
||||
emptySpaces,
|
||||
VERTICAL_RESIZE_LIMIT,
|
||||
shouldResize,
|
||||
);
|
||||
const spaceMovement = {
|
||||
Y: getReflowDistance(
|
||||
collisionTree,
|
||||
direction,
|
||||
maxY,
|
||||
distanceBeforeCollision,
|
||||
height,
|
||||
emptySpaces,
|
||||
gridProps.parentRowSpace,
|
||||
true,
|
||||
),
|
||||
distanceBeforeCollision,
|
||||
maxY,
|
||||
height,
|
||||
emptySpaces: currentEmptySpaces,
|
||||
depth,
|
||||
maxOccupiedSpace,
|
||||
};
|
||||
|
||||
return spaceMovement;
|
||||
}
|
||||
98
app/client/src/reflow/reflowTypes.ts
Normal file
98
app/client/src/reflow/reflowTypes.ts
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
import { OccupiedSpace } from "constants/CanvasEditorConstants";
|
||||
|
||||
export const HORIZONTAL_RESIZE_LIMIT = 2;
|
||||
export const VERTICAL_RESIZE_LIMIT = 4;
|
||||
|
||||
export enum ReflowDirection {
|
||||
LEFT = "LEFT",
|
||||
RIGHT = "RIGHT",
|
||||
TOP = "TOP",
|
||||
BOTTOM = "BOTTOM",
|
||||
TOPLEFT = "TOP|LEFT",
|
||||
TOPRIGHT = "TOP|RIGHT",
|
||||
BOTTOMLEFT = "BOTTOM|LEFT",
|
||||
BOTTOMRIGHT = "BOTTOM|RIGHT",
|
||||
UNSET = "UNSET",
|
||||
}
|
||||
|
||||
export enum SpaceAttributes {
|
||||
top = "top",
|
||||
bottom = "bottom",
|
||||
left = "left",
|
||||
right = "right",
|
||||
}
|
||||
|
||||
export enum MathComparators {
|
||||
min = "min",
|
||||
max = "max",
|
||||
}
|
||||
|
||||
export type GridProps = {
|
||||
parentColumnSpace: number;
|
||||
parentRowSpace: number;
|
||||
maxGridColumns: number;
|
||||
paddingOffset?: number;
|
||||
};
|
||||
|
||||
export type CollisionAccessors = {
|
||||
direction: SpaceAttributes;
|
||||
oppositeDirection: SpaceAttributes;
|
||||
perpendicularMax: SpaceAttributes;
|
||||
perpendicularMin: SpaceAttributes;
|
||||
parallelMax: SpaceAttributes;
|
||||
parallelMin: SpaceAttributes;
|
||||
mathComparator: MathComparators;
|
||||
directionIndicator: 1 | -1;
|
||||
isHorizontal: boolean;
|
||||
};
|
||||
|
||||
export type Delta = {
|
||||
X: number;
|
||||
Y: number;
|
||||
};
|
||||
|
||||
export type CollidingSpace = OccupiedSpace & {
|
||||
direction: ReflowDirection;
|
||||
};
|
||||
|
||||
export type CollidingSpaceMap = {
|
||||
[key: string]: CollidingSpace;
|
||||
};
|
||||
|
||||
export type CollisionTree = OccupiedSpace & {
|
||||
children?: {
|
||||
[key: string]: CollisionTree;
|
||||
};
|
||||
};
|
||||
|
||||
export type SpaceMovement = {
|
||||
id?: string;
|
||||
directionalMovements: DirectionalMovement[];
|
||||
};
|
||||
|
||||
export type DirectionalMovement = {
|
||||
maxMovement: number;
|
||||
directionalIndicator: 1 | -1;
|
||||
coordinateKey: "X" | "Y";
|
||||
isHorizontal: boolean;
|
||||
};
|
||||
|
||||
export type ReflowedSpace = {
|
||||
X?: number;
|
||||
Y?: number;
|
||||
width?: number;
|
||||
height?: number;
|
||||
depth?: number;
|
||||
x?: number;
|
||||
y?: number;
|
||||
maxX?: number;
|
||||
maxY?: number;
|
||||
dimensionXBeforeCollision?: number;
|
||||
dimensionYBeforeCollision?: number;
|
||||
maxOccupiedSpace?: number;
|
||||
emptySpaces?: number;
|
||||
};
|
||||
|
||||
export type ReflowedSpaceMap = {
|
||||
[key: string]: ReflowedSpace;
|
||||
};
|
||||
836
app/client/src/reflow/reflowUtils.ts
Normal file
836
app/client/src/reflow/reflowUtils.ts
Normal file
|
|
@ -0,0 +1,836 @@
|
|||
import { OccupiedSpace } from "constants/CanvasEditorConstants";
|
||||
import { Rect } from "utils/WidgetPropsUtils";
|
||||
import {
|
||||
CollidingSpace,
|
||||
CollidingSpaceMap,
|
||||
CollisionAccessors,
|
||||
CollisionTree,
|
||||
GridProps,
|
||||
HORIZONTAL_RESIZE_LIMIT,
|
||||
MathComparators,
|
||||
ReflowDirection,
|
||||
ReflowedSpace,
|
||||
ReflowedSpaceMap,
|
||||
SpaceAttributes,
|
||||
SpaceMovement,
|
||||
VERTICAL_RESIZE_LIMIT,
|
||||
} from "./reflowTypes";
|
||||
|
||||
/**
|
||||
* Get if the space moved horizontally
|
||||
*
|
||||
* @param newPositions
|
||||
* @param prevPositions
|
||||
* @returns boolean
|
||||
*/
|
||||
export function getIsHorizontalMove(
|
||||
newPositions: OccupiedSpace,
|
||||
prevPositions?: OccupiedSpace,
|
||||
) {
|
||||
if (
|
||||
prevPositions?.left !== newPositions.left ||
|
||||
prevPositions?.right !== newPositions.right
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* method to determine if the newly calculated MovementValue should replace an old value of the same Space Id
|
||||
*
|
||||
* @param oldMovement
|
||||
* @param newMovement
|
||||
* @param direction
|
||||
* @returns boolean
|
||||
*/
|
||||
export function shouldReplaceOldMovement(
|
||||
oldMovement: ReflowedSpace,
|
||||
newMovement: ReflowedSpace,
|
||||
direction: ReflowDirection,
|
||||
) {
|
||||
if (!oldMovement) return true;
|
||||
|
||||
const { directionIndicator, isHorizontal } = getAccessor(direction);
|
||||
|
||||
const distanceKey = isHorizontal ? "X" : "Y";
|
||||
|
||||
const oldDistance = oldMovement[distanceKey],
|
||||
newDistance = newMovement[distanceKey];
|
||||
|
||||
if (oldDistance === undefined || newDistance === undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return compareNumbers(oldDistance, newDistance, directionIndicator < 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* method to get resized dimensions of the Space to determine the Spaces colliding with this Space
|
||||
*
|
||||
* @param collisionTree
|
||||
* @param distanceBeforeCollision
|
||||
* @param emptySpaces
|
||||
* @param accessors
|
||||
* @returns resized Direction
|
||||
*/
|
||||
export function getResizedDimensions(
|
||||
collisionTree: CollisionTree,
|
||||
distanceBeforeCollision: number,
|
||||
emptySpaces: number,
|
||||
{ direction }: CollisionAccessors,
|
||||
) {
|
||||
const reflowedPosition = { ...collisionTree, children: [] };
|
||||
|
||||
const newDimension = distanceBeforeCollision + emptySpaces;
|
||||
reflowedPosition[direction] -= newDimension;
|
||||
|
||||
return reflowedPosition;
|
||||
}
|
||||
|
||||
/**
|
||||
* sort the collidingSpaces with respect to the distance from the staticPosition
|
||||
*
|
||||
* @param collidingSpaces
|
||||
* @param staticPosition
|
||||
* @param isAscending
|
||||
*/
|
||||
export function sortCollidingSpacesByDistance(
|
||||
collidingSpaces: CollidingSpace[],
|
||||
staticPosition: OccupiedSpace,
|
||||
isAscending = true,
|
||||
) {
|
||||
const distanceComparator = getDistanceComparator(staticPosition, isAscending);
|
||||
collidingSpaces.sort(distanceComparator);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a comparator bound to the
|
||||
*
|
||||
* @param staticPosition
|
||||
* @param isAscending
|
||||
* @returns negative or positive indicator
|
||||
*/
|
||||
function getDistanceComparator(
|
||||
staticPosition: OccupiedSpace,
|
||||
isAscending = true,
|
||||
) {
|
||||
return function(spaceA: CollidingSpace, spaceB: CollidingSpace) {
|
||||
const accessorA = getAccessor(spaceA.direction);
|
||||
const accessorB = getAccessor(spaceB.direction);
|
||||
|
||||
const distanceA = Math.abs(
|
||||
staticPosition[accessorA.direction] - spaceA[accessorA.oppositeDirection],
|
||||
);
|
||||
const distanceB = Math.abs(
|
||||
staticPosition[accessorB.direction] - spaceB[accessorB.oppositeDirection],
|
||||
);
|
||||
return isAscending ? distanceA - distanceB : distanceB - distanceA;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* To Get Indicators if the static widget can continue to reflow without Overlapping
|
||||
*
|
||||
* @param staticPosition
|
||||
* @param delta
|
||||
* @param beforeLimit
|
||||
* @returns object with a boolean each for vertical and horizontal direction
|
||||
*/
|
||||
export function getShouldReflow(
|
||||
staticPosition: SpaceMovement | undefined,
|
||||
delta = { X: 0, Y: 0 },
|
||||
beforeLimit = false,
|
||||
): { canVerticalMove: boolean; canHorizontalMove: boolean } {
|
||||
if (!staticPosition) {
|
||||
return {
|
||||
canHorizontalMove: false,
|
||||
canVerticalMove: false,
|
||||
};
|
||||
}
|
||||
|
||||
let canHorizontalMove = true,
|
||||
canVerticalMove = true;
|
||||
const { directionalMovements } = staticPosition;
|
||||
for (const movementLimit of directionalMovements) {
|
||||
const {
|
||||
coordinateKey,
|
||||
directionalIndicator,
|
||||
isHorizontal,
|
||||
maxMovement,
|
||||
} = movementLimit;
|
||||
|
||||
const canMove = compareNumbers(
|
||||
delta[coordinateKey],
|
||||
maxMovement,
|
||||
directionalIndicator < 0,
|
||||
beforeLimit,
|
||||
);
|
||||
|
||||
if (!canMove) {
|
||||
if (isHorizontal) canHorizontalMove = false;
|
||||
else canVerticalMove = false;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
canHorizontalMove,
|
||||
canVerticalMove,
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Should return X and Y coordinates of movement from OGPositions to newPositions
|
||||
* in a particular direction
|
||||
*
|
||||
* @param OGPositions
|
||||
* @param newPositions
|
||||
* @param direction
|
||||
* @returns
|
||||
*/
|
||||
export function getDelta(
|
||||
OGPositions: OccupiedSpace,
|
||||
newPositions: OccupiedSpace,
|
||||
direction: ReflowDirection,
|
||||
) {
|
||||
let X = OGPositions.left - newPositions.left,
|
||||
Y = OGPositions.top - newPositions.top;
|
||||
if (direction.indexOf("|") > 0) {
|
||||
const [verticalDirection, horizontalDirection] = direction.split("|");
|
||||
const { direction: xDirection } = getAccessor(
|
||||
horizontalDirection as ReflowDirection,
|
||||
);
|
||||
const { direction: yDirection } = getAccessor(
|
||||
verticalDirection as ReflowDirection,
|
||||
);
|
||||
X = OGPositions[xDirection] - newPositions[xDirection];
|
||||
Y = OGPositions[yDirection] - newPositions[yDirection];
|
||||
return { X, Y };
|
||||
}
|
||||
|
||||
const { direction: directionalAccessor, isHorizontal } = getAccessor(
|
||||
direction,
|
||||
);
|
||||
const diff =
|
||||
OGPositions[directionalAccessor] - newPositions[directionalAccessor];
|
||||
|
||||
if (isHorizontal) X = diff;
|
||||
else Y = diff;
|
||||
|
||||
return { X, Y };
|
||||
}
|
||||
|
||||
/**
|
||||
* returns Collising Spaces map with the direction of collision
|
||||
*
|
||||
* @param staticPosition
|
||||
* @param direction
|
||||
* @param occupiedSpaces
|
||||
* @param isHorizontalMove
|
||||
* @param prevPositions
|
||||
* @param prevCollidingSpaces
|
||||
* @param forceDirection
|
||||
* @returns collision spaces Map
|
||||
*/
|
||||
export function getCollidingSpaces(
|
||||
staticPosition: OccupiedSpace,
|
||||
direction: ReflowDirection,
|
||||
occupiedSpaces?: OccupiedSpace[],
|
||||
isHorizontalMove?: boolean,
|
||||
prevPositions?: OccupiedSpace,
|
||||
prevCollidingSpaces?: CollidingSpaceMap,
|
||||
forceDirection = false,
|
||||
) {
|
||||
let isColliding = false;
|
||||
const collidingSpaceMap: CollidingSpaceMap = {};
|
||||
const filteredOccupiedSpaces = filterSpaceById(
|
||||
staticPosition.id,
|
||||
occupiedSpaces,
|
||||
);
|
||||
|
||||
for (const occupiedSpace of filteredOccupiedSpaces) {
|
||||
if (areIntersecting(occupiedSpace, staticPosition)) {
|
||||
isColliding = true;
|
||||
const currentSpaceId = occupiedSpace.id;
|
||||
|
||||
const movementDirection = getCorrectedDirection(
|
||||
occupiedSpace,
|
||||
prevPositions,
|
||||
direction,
|
||||
forceDirection,
|
||||
isHorizontalMove,
|
||||
prevCollidingSpaces,
|
||||
);
|
||||
|
||||
collidingSpaceMap[currentSpaceId] = {
|
||||
...occupiedSpace,
|
||||
direction: movementDirection,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
isColliding,
|
||||
collidingSpaceMap,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* returns Collising Spaces map in a particular direction
|
||||
* @param staticPosition
|
||||
* @param direction
|
||||
* @param occupiedSpaces
|
||||
* @returns Colliding Spaces Array
|
||||
*/
|
||||
export function getCollidingSpacesInDirection(
|
||||
staticPosition: OccupiedSpace,
|
||||
direction: ReflowDirection,
|
||||
occupiedSpaces?: OccupiedSpace[],
|
||||
) {
|
||||
const collidingSpaces: CollidingSpace[] = [];
|
||||
const occupiedSpacesInDirection = filterSpaceByDirection(
|
||||
staticPosition,
|
||||
occupiedSpaces,
|
||||
direction,
|
||||
);
|
||||
|
||||
for (const occupiedSpace of occupiedSpacesInDirection) {
|
||||
if (areIntersecting(occupiedSpace, staticPosition)) {
|
||||
collidingSpaces.push({
|
||||
...occupiedSpace,
|
||||
direction,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return { collidingSpaces, occupiedSpacesInDirection };
|
||||
}
|
||||
|
||||
function filterSpaceByDirection(
|
||||
staticPosition: OccupiedSpace,
|
||||
occupiedSpaces: OccupiedSpace[] | undefined,
|
||||
direction: ReflowDirection,
|
||||
): OccupiedSpace[] {
|
||||
let filteredSpaces: OccupiedSpace[] = [];
|
||||
|
||||
const { directionIndicator, oppositeDirection } = getAccessor(direction);
|
||||
if (occupiedSpaces) {
|
||||
filteredSpaces = occupiedSpaces.filter((occupiedSpace) => {
|
||||
if (
|
||||
occupiedSpace.id === staticPosition.id ||
|
||||
occupiedSpace.parentId === staticPosition.id
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return compareNumbers(
|
||||
occupiedSpace[oppositeDirection],
|
||||
staticPosition[oppositeDirection],
|
||||
directionIndicator > 0,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return filteredSpaces;
|
||||
}
|
||||
|
||||
/**
|
||||
* filters out a space with an id and returns the filtered Spaces
|
||||
* @param id
|
||||
* @param occupiedSpaces
|
||||
* @returns filtered occupied spaces
|
||||
*/
|
||||
export function filterSpaceById(
|
||||
id: string,
|
||||
occupiedSpaces: OccupiedSpace[] | undefined,
|
||||
): OccupiedSpace[] {
|
||||
let filteredSpaces: OccupiedSpace[] = [];
|
||||
if (occupiedSpaces) {
|
||||
filteredSpaces = occupiedSpaces.filter((occupiedSpace) => {
|
||||
return occupiedSpace.id !== id && occupiedSpace.parentId !== id;
|
||||
});
|
||||
}
|
||||
return filteredSpaces;
|
||||
}
|
||||
|
||||
function areIntersecting(r1: Rect, r2: Rect) {
|
||||
return !(
|
||||
r2.left >= r1.right ||
|
||||
r2.right <= r1.left ||
|
||||
r2.top >= r1.bottom ||
|
||||
r2.bottom <= r1.top
|
||||
);
|
||||
}
|
||||
|
||||
function getCorrectedDirection(
|
||||
collidingSpace: OccupiedSpace,
|
||||
prevPositions: OccupiedSpace | undefined,
|
||||
direction: ReflowDirection,
|
||||
forceDirection = false,
|
||||
isHorizontalMove?: boolean,
|
||||
prevCollidingSpaces?: CollidingSpaceMap,
|
||||
): ReflowDirection {
|
||||
if (forceDirection) return direction;
|
||||
|
||||
if (prevCollidingSpaces && prevCollidingSpaces[collidingSpace.id]) {
|
||||
return prevCollidingSpaces[collidingSpace.id].direction;
|
||||
}
|
||||
|
||||
let primaryDirection: ReflowDirection = direction,
|
||||
secondaryDirection: ReflowDirection | undefined = undefined;
|
||||
|
||||
if (direction.indexOf("|") >= 0) {
|
||||
const directions = direction.split("|");
|
||||
|
||||
if (isHorizontalMove) {
|
||||
primaryDirection = directions[1] as ReflowDirection;
|
||||
secondaryDirection = directions[0] as ReflowDirection;
|
||||
} else {
|
||||
primaryDirection = directions[0] as ReflowDirection;
|
||||
secondaryDirection = directions[1] as ReflowDirection;
|
||||
}
|
||||
}
|
||||
|
||||
if (!prevPositions) return primaryDirection;
|
||||
|
||||
const primaryAccessors = getAccessor(primaryDirection);
|
||||
|
||||
const isCorrectDirection = compareNumbers(
|
||||
collidingSpace[primaryAccessors.oppositeDirection],
|
||||
prevPositions[primaryAccessors.direction],
|
||||
primaryAccessors.directionIndicator > 0,
|
||||
true,
|
||||
);
|
||||
|
||||
if (isCorrectDirection) {
|
||||
return primaryDirection;
|
||||
} else if (secondaryDirection) {
|
||||
return secondaryDirection;
|
||||
}
|
||||
|
||||
return getVerifiedDirection(
|
||||
collidingSpace,
|
||||
prevPositions,
|
||||
primaryDirection,
|
||||
primaryAccessors.isHorizontal,
|
||||
);
|
||||
}
|
||||
|
||||
function getVerifiedDirection(
|
||||
collidingSpace: OccupiedSpace,
|
||||
prevPositions: OccupiedSpace,
|
||||
direction: ReflowDirection,
|
||||
isHorizontalMove: boolean,
|
||||
) {
|
||||
if (isHorizontalMove) {
|
||||
if (collidingSpace.bottom <= prevPositions.top) {
|
||||
return ReflowDirection.TOP;
|
||||
} else if (collidingSpace.top >= prevPositions.bottom) {
|
||||
return ReflowDirection.BOTTOM;
|
||||
} else if (
|
||||
direction !== ReflowDirection.RIGHT &&
|
||||
collidingSpace.left >= prevPositions.right
|
||||
) {
|
||||
return ReflowDirection.RIGHT;
|
||||
} else if (
|
||||
direction !== ReflowDirection.LEFT &&
|
||||
collidingSpace.right <= prevPositions.left
|
||||
) {
|
||||
return ReflowDirection.LEFT;
|
||||
}
|
||||
} else {
|
||||
if (collidingSpace.right <= prevPositions.left) {
|
||||
return ReflowDirection.LEFT;
|
||||
} else if (collidingSpace.left >= prevPositions.right) {
|
||||
return ReflowDirection.RIGHT;
|
||||
} else if (
|
||||
direction !== ReflowDirection.TOP &&
|
||||
collidingSpace.bottom <= prevPositions.top
|
||||
) {
|
||||
return ReflowDirection.TOP;
|
||||
} else if (
|
||||
direction !== ReflowDirection.BOTTOM &&
|
||||
collidingSpace.top >= prevPositions.bottom
|
||||
) {
|
||||
return ReflowDirection.BOTTOM;
|
||||
}
|
||||
}
|
||||
|
||||
return direction;
|
||||
}
|
||||
/**
|
||||
* compares numbers and returns boolean
|
||||
* @param numberA
|
||||
* @param numberB
|
||||
* @param isGreaterThan
|
||||
* @param isEqual
|
||||
* @returns boolean
|
||||
*/
|
||||
export function compareNumbers(
|
||||
numberA: number,
|
||||
numberB: number,
|
||||
isGreaterThan: boolean,
|
||||
isEqual = false,
|
||||
): boolean {
|
||||
if (isGreaterThan) {
|
||||
if (isEqual) {
|
||||
return numberA >= numberB;
|
||||
}
|
||||
return numberA > numberB;
|
||||
}
|
||||
|
||||
if (isEqual) {
|
||||
return numberA <= numberB;
|
||||
}
|
||||
|
||||
return numberA < numberB;
|
||||
}
|
||||
|
||||
/**
|
||||
* gets opposite direction
|
||||
* @param direction
|
||||
* @returns ReflowDirection
|
||||
*/
|
||||
export function getOppositeDirection(
|
||||
direction: ReflowDirection,
|
||||
): ReflowDirection {
|
||||
const directionalAccessors = getAccessor(direction);
|
||||
return directionalAccessors.oppositeDirection.toUpperCase() as ReflowDirection;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param direction
|
||||
* @returns accessors
|
||||
*/
|
||||
export function getAccessor(direction: ReflowDirection): CollisionAccessors {
|
||||
switch (direction) {
|
||||
case ReflowDirection.LEFT:
|
||||
return {
|
||||
direction: SpaceAttributes.left,
|
||||
oppositeDirection: SpaceAttributes.right,
|
||||
perpendicularMax: SpaceAttributes.bottom,
|
||||
perpendicularMin: SpaceAttributes.top,
|
||||
parallelMax: SpaceAttributes.right,
|
||||
parallelMin: SpaceAttributes.left,
|
||||
mathComparator: MathComparators.max,
|
||||
directionIndicator: -1,
|
||||
isHorizontal: true,
|
||||
};
|
||||
case ReflowDirection.RIGHT:
|
||||
return {
|
||||
direction: SpaceAttributes.right,
|
||||
oppositeDirection: SpaceAttributes.left,
|
||||
perpendicularMax: SpaceAttributes.bottom,
|
||||
perpendicularMin: SpaceAttributes.top,
|
||||
parallelMax: SpaceAttributes.right,
|
||||
parallelMin: SpaceAttributes.left,
|
||||
mathComparator: MathComparators.min,
|
||||
directionIndicator: 1,
|
||||
isHorizontal: true,
|
||||
};
|
||||
case ReflowDirection.TOP:
|
||||
return {
|
||||
direction: SpaceAttributes.top,
|
||||
oppositeDirection: SpaceAttributes.bottom,
|
||||
perpendicularMax: SpaceAttributes.right,
|
||||
perpendicularMin: SpaceAttributes.left,
|
||||
parallelMax: SpaceAttributes.bottom,
|
||||
parallelMin: SpaceAttributes.top,
|
||||
mathComparator: MathComparators.max,
|
||||
directionIndicator: -1,
|
||||
isHorizontal: false,
|
||||
};
|
||||
case ReflowDirection.BOTTOM:
|
||||
return {
|
||||
direction: SpaceAttributes.bottom,
|
||||
oppositeDirection: SpaceAttributes.top,
|
||||
perpendicularMax: SpaceAttributes.right,
|
||||
perpendicularMin: SpaceAttributes.left,
|
||||
parallelMax: SpaceAttributes.bottom,
|
||||
parallelMin: SpaceAttributes.top,
|
||||
mathComparator: MathComparators.min,
|
||||
directionIndicator: 1,
|
||||
isHorizontal: false,
|
||||
};
|
||||
}
|
||||
return {
|
||||
direction: SpaceAttributes.bottom,
|
||||
oppositeDirection: SpaceAttributes.top,
|
||||
perpendicularMax: SpaceAttributes.right,
|
||||
perpendicularMin: SpaceAttributes.left,
|
||||
parallelMax: SpaceAttributes.bottom,
|
||||
parallelMin: SpaceAttributes.top,
|
||||
mathComparator: MathComparators.min,
|
||||
directionIndicator: 1,
|
||||
isHorizontal: false,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* get Max X coordinate of the the space
|
||||
*
|
||||
* @param collisionTree
|
||||
* @param gridProps
|
||||
* @param direction
|
||||
* @param depth
|
||||
* @param maxOccupiedSpace
|
||||
* @param shouldResize
|
||||
* @returns number
|
||||
*/
|
||||
export function getMaxX(
|
||||
collisionTree: CollisionTree,
|
||||
gridProps: GridProps,
|
||||
direction: ReflowDirection,
|
||||
depth: number,
|
||||
maxOccupiedSpace: number,
|
||||
shouldResize: boolean,
|
||||
) {
|
||||
const accessors = getAccessor(direction);
|
||||
const movementLimit = shouldResize
|
||||
? depth * HORIZONTAL_RESIZE_LIMIT
|
||||
: maxOccupiedSpace;
|
||||
|
||||
let maxX = collisionTree[accessors.direction] - movementLimit;
|
||||
|
||||
if (direction === ReflowDirection.RIGHT) {
|
||||
maxX =
|
||||
gridProps.maxGridColumns -
|
||||
collisionTree[accessors.direction] -
|
||||
movementLimit;
|
||||
}
|
||||
|
||||
return accessors.directionIndicator * maxX * gridProps.parentColumnSpace;
|
||||
}
|
||||
|
||||
/**
|
||||
* get Max Y coordinate of the the space
|
||||
*
|
||||
* @param collisionTree
|
||||
* @param gridProps
|
||||
* @param direction
|
||||
* @param depth
|
||||
* @param maxOccupiedSpace
|
||||
* @param shouldResize
|
||||
* @returns number
|
||||
*/
|
||||
export function getMaxY(
|
||||
collisionTree: CollisionTree,
|
||||
gridProps: GridProps,
|
||||
direction: ReflowDirection,
|
||||
depth: number,
|
||||
maxOccupiedSpace: number,
|
||||
shouldResize: boolean,
|
||||
) {
|
||||
const accessors = getAccessor(direction);
|
||||
const movementLimit = shouldResize
|
||||
? depth * VERTICAL_RESIZE_LIMIT
|
||||
: maxOccupiedSpace;
|
||||
|
||||
let maxY =
|
||||
(collisionTree[accessors.direction] - movementLimit) *
|
||||
gridProps.parentRowSpace;
|
||||
|
||||
if (direction === ReflowDirection.BOTTOM) {
|
||||
maxY = Infinity;
|
||||
}
|
||||
|
||||
return accessors.directionIndicator * maxY;
|
||||
}
|
||||
|
||||
/**
|
||||
* get X or Y coordinate distance for space
|
||||
*
|
||||
* @param collisionTree
|
||||
* @param direction
|
||||
* @param maxDistance
|
||||
* @param distanceBeforeCollision
|
||||
* @param actualDimension
|
||||
* @param emptySpaces
|
||||
* @param snapGridSpace
|
||||
* @param expandableCanvas
|
||||
* @returns distance in number
|
||||
*/
|
||||
export function getReflowDistance(
|
||||
collisionTree: CollisionTree,
|
||||
direction: ReflowDirection,
|
||||
maxDistance: number,
|
||||
distanceBeforeCollision: number,
|
||||
actualDimension: number,
|
||||
emptySpaces: number,
|
||||
snapGridSpace: number,
|
||||
expandableCanvas = false,
|
||||
) {
|
||||
const accessors = getAccessor(direction);
|
||||
|
||||
const originalDimension =
|
||||
(collisionTree[accessors.parallelMax] -
|
||||
collisionTree[accessors.parallelMin]) *
|
||||
snapGridSpace;
|
||||
|
||||
const value =
|
||||
(distanceBeforeCollision + emptySpaces * accessors.directionIndicator) *
|
||||
snapGridSpace *
|
||||
-1;
|
||||
const maxValue = Math[accessors.mathComparator](value, maxDistance);
|
||||
|
||||
if (expandableCanvas) {
|
||||
return maxValue;
|
||||
}
|
||||
|
||||
return accessors.directionIndicator < 0
|
||||
? maxValue
|
||||
: maxValue + originalDimension - actualDimension;
|
||||
}
|
||||
|
||||
/**
|
||||
* gets the resized dimension of the space along a direction
|
||||
*
|
||||
* @param collisionTree
|
||||
* @param direction
|
||||
* @param travelDistance
|
||||
* @param maxDistance
|
||||
* @param distanceBeforeCollision
|
||||
* @param snapGridSpace
|
||||
* @param emptySpaces
|
||||
* @param minDimension
|
||||
* @param shouldResize
|
||||
* @returns resized width or height of space
|
||||
*/
|
||||
export function getResizedDimension(
|
||||
collisionTree: CollisionTree,
|
||||
direction: ReflowDirection,
|
||||
travelDistance: number,
|
||||
maxDistance: number,
|
||||
distanceBeforeCollision: number,
|
||||
snapGridSpace: number,
|
||||
emptySpaces: number,
|
||||
minDimension: number,
|
||||
shouldResize: boolean,
|
||||
) {
|
||||
const accessors = getAccessor(direction);
|
||||
|
||||
const currentDistanceBeforeCollision =
|
||||
travelDistance +
|
||||
(distanceBeforeCollision + emptySpaces * accessors.directionIndicator) *
|
||||
snapGridSpace;
|
||||
|
||||
const originalDimension =
|
||||
collisionTree[accessors.parallelMax] - collisionTree[accessors.parallelMin];
|
||||
|
||||
if (!shouldResize) {
|
||||
return originalDimension * snapGridSpace;
|
||||
}
|
||||
const resizeTreshold = maxDistance + currentDistanceBeforeCollision;
|
||||
const resizeLimit =
|
||||
resizeTreshold +
|
||||
(originalDimension - minDimension) *
|
||||
snapGridSpace *
|
||||
accessors.directionIndicator;
|
||||
|
||||
let shrink = 0;
|
||||
const canResize = compareNumbers(
|
||||
travelDistance,
|
||||
resizeTreshold,
|
||||
accessors.directionIndicator > 0,
|
||||
true,
|
||||
);
|
||||
|
||||
if (canResize) {
|
||||
shrink = Math[accessors.mathComparator](travelDistance, resizeLimit);
|
||||
shrink = shrink - resizeTreshold;
|
||||
}
|
||||
|
||||
return originalDimension * snapGridSpace - Math.abs(shrink);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param movementMap
|
||||
* @param prevMovementMap
|
||||
* @param movementLimit
|
||||
* @returns
|
||||
*/
|
||||
export function getLimitedMovementMap(
|
||||
movementMap: ReflowedSpaceMap | undefined,
|
||||
prevMovementMap: ReflowedSpaceMap,
|
||||
movementLimit: { canVerticalMove: boolean; canHorizontalMove: boolean },
|
||||
): ReflowedSpaceMap {
|
||||
if (!movementMap) return {};
|
||||
const { canHorizontalMove, canVerticalMove } = movementLimit;
|
||||
|
||||
if (!canVerticalMove && !canHorizontalMove) {
|
||||
return prevMovementMap;
|
||||
}
|
||||
|
||||
if (!canVerticalMove) {
|
||||
return replaceMovementMapByDirection(movementMap, prevMovementMap, false);
|
||||
}
|
||||
|
||||
if (!canHorizontalMove) {
|
||||
return replaceMovementMapByDirection(movementMap, prevMovementMap, true);
|
||||
}
|
||||
|
||||
return movementMap;
|
||||
}
|
||||
|
||||
function replaceMovementMapByDirection(
|
||||
movementMap: ReflowedSpaceMap,
|
||||
prevMovementMap: ReflowedSpaceMap,
|
||||
replaceHorizontal: boolean,
|
||||
): ReflowedSpaceMap {
|
||||
const checkKey = replaceHorizontal ? "X" : "Y";
|
||||
const currentMovementMap = { ...movementMap };
|
||||
const movementMapIds = Object.keys(movementMap);
|
||||
|
||||
for (const spaceId of movementMapIds) {
|
||||
if (currentMovementMap[spaceId][checkKey] !== undefined) {
|
||||
delete currentMovementMap[spaceId];
|
||||
|
||||
if (prevMovementMap[spaceId]) {
|
||||
currentMovementMap[spaceId] = { ...prevMovementMap[spaceId] };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return currentMovementMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* on Container exit, the exitted container and the widgets behind it should reflow in opposite direction
|
||||
* @param collidingSpaceMap
|
||||
* @param immediateExitContainer
|
||||
* @param direction
|
||||
* changes reference of collidingSpaceMap
|
||||
*/
|
||||
export function changeExitContainerDirection(
|
||||
collidingSpaceMap: CollidingSpaceMap,
|
||||
immediateExitContainer: string | undefined,
|
||||
direction: ReflowDirection,
|
||||
) {
|
||||
if (!immediateExitContainer || !collidingSpaceMap[immediateExitContainer]) {
|
||||
return;
|
||||
}
|
||||
|
||||
const oppDirection = getOppositeDirection(direction);
|
||||
const { directionIndicator, oppositeDirection } = getAccessor(oppDirection);
|
||||
|
||||
const collidingSpaces: CollidingSpace[] = Object.values(collidingSpaceMap);
|
||||
const oppositeFrom =
|
||||
collidingSpaceMap[immediateExitContainer][oppositeDirection];
|
||||
|
||||
const oppositeSpaceIds = collidingSpaces
|
||||
.filter((collidingSpace: CollidingSpace) => {
|
||||
return compareNumbers(
|
||||
collidingSpace[oppositeDirection],
|
||||
oppositeFrom,
|
||||
directionIndicator > 0,
|
||||
true,
|
||||
);
|
||||
})
|
||||
.map((collidingSpace: CollidingSpace) => collidingSpace.id);
|
||||
|
||||
for (const spaceId of oppositeSpaceIds) {
|
||||
collidingSpaceMap[spaceId].direction = oppDirection;
|
||||
}
|
||||
}
|
||||
951
app/client/src/reflow/tests/reflowUtils.test.js
Normal file
951
app/client/src/reflow/tests/reflowUtils.test.js
Normal file
|
|
@ -0,0 +1,951 @@
|
|||
import { ReflowDirection } from "reflow/reflowTypes";
|
||||
import {
|
||||
getAccessor,
|
||||
getIsHorizontalMove,
|
||||
shouldReplaceOldMovement,
|
||||
getResizedDimensions,
|
||||
sortCollidingSpacesByDistance,
|
||||
getShouldReflow,
|
||||
getDelta,
|
||||
getCollidingSpaces,
|
||||
getCollidingSpacesInDirection,
|
||||
filterSpaceById,
|
||||
getMaxX,
|
||||
getMaxY,
|
||||
getReflowDistance,
|
||||
getResizedDimension,
|
||||
} from "../reflowUtils";
|
||||
import { HORIZONTAL_RESIZE_LIMIT, VERTICAL_RESIZE_LIMIT } from "../reflowTypes";
|
||||
|
||||
const gridProps = {
|
||||
parentColumnSpace: 20,
|
||||
parentRowSpace: 10,
|
||||
maxGridColumns: 100,
|
||||
};
|
||||
|
||||
describe("Test reflow util methods", () => {
|
||||
describe("Test getIsHorizontalMove method", () => {
|
||||
it("should return true when there is a difference in horizonatal direction coordinates", () => {
|
||||
const newPositions = {
|
||||
left: 20,
|
||||
right: 40,
|
||||
},
|
||||
oldPositions = {
|
||||
left: 10,
|
||||
right: 30,
|
||||
};
|
||||
expect(getIsHorizontalMove(newPositions, oldPositions)).toBe(true);
|
||||
});
|
||||
it("should return false when there is no difference horizontal direction coordinates", () => {
|
||||
const newPositions = {
|
||||
left: 20,
|
||||
right: 40,
|
||||
},
|
||||
oldPositions = {
|
||||
left: 20,
|
||||
right: 40,
|
||||
};
|
||||
expect(getIsHorizontalMove(newPositions, oldPositions)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Test shouldReplaceOldMovement method", () => {
|
||||
it("should return true when Space's new movement coordinate is farther than the olds'", () => {
|
||||
const newMovement = {
|
||||
X: 20,
|
||||
},
|
||||
oldMovement = {
|
||||
X: 10,
|
||||
};
|
||||
expect(
|
||||
shouldReplaceOldMovement(
|
||||
oldMovement,
|
||||
newMovement,
|
||||
ReflowDirection.RIGHT,
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
it("should return false when Space's new movement coordinate is not farther than the olds'", () => {
|
||||
const newMovement = {
|
||||
X: 10,
|
||||
},
|
||||
oldMovement = {
|
||||
X: 20,
|
||||
};
|
||||
expect(
|
||||
shouldReplaceOldMovement(oldMovement, newMovement, ReflowDirection.TOP),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Test getResizedDimensions method", () => {
|
||||
it("should return Resized Dimensions based on dimensionbeforeCollision and the emptySpaces in between", () => {
|
||||
const collisionTree = {
|
||||
right: 20,
|
||||
},
|
||||
distanceBeforeCollision = -20,
|
||||
emptySpaces = 10,
|
||||
accesssors = getAccessor(ReflowDirection.RIGHT);
|
||||
const resizedDimension = {
|
||||
right: 30,
|
||||
};
|
||||
|
||||
expect(
|
||||
getResizedDimensions(
|
||||
collisionTree,
|
||||
distanceBeforeCollision,
|
||||
emptySpaces,
|
||||
accesssors,
|
||||
).right,
|
||||
).toBe(resizedDimension.right);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Test sortCollidingSpacesByDistance method", () => {
|
||||
const collisionSpaces = [
|
||||
{
|
||||
direction: ReflowDirection.RIGHT,
|
||||
left: 100,
|
||||
},
|
||||
{
|
||||
direction: ReflowDirection.LEFT,
|
||||
right: 30,
|
||||
},
|
||||
{
|
||||
direction: ReflowDirection.BOTTOM,
|
||||
top: 110,
|
||||
},
|
||||
{
|
||||
direction: ReflowDirection.TOP,
|
||||
bottom: 0,
|
||||
},
|
||||
],
|
||||
staticPosition = {
|
||||
right: 80,
|
||||
left: 40,
|
||||
bottom: 70,
|
||||
top: 30,
|
||||
};
|
||||
|
||||
it("should sort the collidingSpaces with respect to the distance from the staticPosition in a Ascending manner", () => {
|
||||
const sortedCollisionSpaces = [
|
||||
{
|
||||
direction: ReflowDirection.LEFT,
|
||||
right: 30,
|
||||
},
|
||||
{
|
||||
direction: ReflowDirection.RIGHT,
|
||||
left: 100,
|
||||
},
|
||||
{
|
||||
direction: ReflowDirection.TOP,
|
||||
bottom: 0,
|
||||
},
|
||||
{
|
||||
direction: ReflowDirection.BOTTOM,
|
||||
top: 110,
|
||||
},
|
||||
];
|
||||
sortCollidingSpacesByDistance(collisionSpaces, staticPosition, true);
|
||||
expect(collisionSpaces).toEqual(sortedCollisionSpaces);
|
||||
});
|
||||
it("should sort the collidingSpaces with respect to the distance from the staticPosition in a descending manner", () => {
|
||||
const sortedCollisionSpaces = [
|
||||
{
|
||||
direction: ReflowDirection.BOTTOM,
|
||||
top: 110,
|
||||
},
|
||||
{
|
||||
direction: ReflowDirection.TOP,
|
||||
bottom: 0,
|
||||
},
|
||||
{
|
||||
direction: ReflowDirection.RIGHT,
|
||||
left: 100,
|
||||
},
|
||||
{
|
||||
direction: ReflowDirection.LEFT,
|
||||
right: 30,
|
||||
},
|
||||
];
|
||||
sortCollidingSpacesByDistance(collisionSpaces, staticPosition, false);
|
||||
expect(collisionSpaces).toEqual(sortedCollisionSpaces);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Test getShouldReflow method", () => {
|
||||
const staticPosition = {
|
||||
directionalMovements: [
|
||||
{
|
||||
maxMovement: 30,
|
||||
directionalIndicator: 1,
|
||||
coordinateKey: "X",
|
||||
isHorizontal: true,
|
||||
},
|
||||
{
|
||||
maxMovement: 30,
|
||||
directionalIndicator: 1,
|
||||
coordinateKey: "Y",
|
||||
isHorizontal: false,
|
||||
},
|
||||
{
|
||||
maxMovement: -30,
|
||||
directionalIndicator: -1,
|
||||
coordinateKey: "X",
|
||||
isHorizontal: true,
|
||||
},
|
||||
{
|
||||
maxMovement: -30,
|
||||
directionalIndicator: -1,
|
||||
coordinateKey: "Y",
|
||||
isHorizontal: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
it("should check canHorizontalMove or canVerticalMove when either direction movement has reached limit", () => {
|
||||
expect(getShouldReflow(staticPosition, { X: 25, Y: 0 })).toEqual({
|
||||
canHorizontalMove: true,
|
||||
canVerticalMove: true,
|
||||
});
|
||||
expect(getShouldReflow(staticPosition, { X: 35, Y: 0 })).toEqual({
|
||||
canHorizontalMove: false,
|
||||
canVerticalMove: true,
|
||||
});
|
||||
expect(getShouldReflow(staticPosition, { X: -25, Y: 0 })).toEqual({
|
||||
canHorizontalMove: true,
|
||||
canVerticalMove: true,
|
||||
});
|
||||
expect(getShouldReflow(staticPosition, { X: -35, Y: 0 })).toEqual({
|
||||
canHorizontalMove: false,
|
||||
canVerticalMove: true,
|
||||
});
|
||||
expect(getShouldReflow(staticPosition, { X: 0, Y: 25 })).toEqual({
|
||||
canHorizontalMove: true,
|
||||
canVerticalMove: true,
|
||||
});
|
||||
expect(getShouldReflow(staticPosition, { X: 0, Y: 35 })).toEqual({
|
||||
canHorizontalMove: true,
|
||||
canVerticalMove: false,
|
||||
});
|
||||
expect(getShouldReflow(staticPosition, { X: 0, Y: -25 })).toEqual({
|
||||
canHorizontalMove: true,
|
||||
canVerticalMove: true,
|
||||
});
|
||||
expect(getShouldReflow(staticPosition, { X: 0, Y: -35 })).toEqual({
|
||||
canHorizontalMove: true,
|
||||
canVerticalMove: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Test getDelta method", () => {
|
||||
const OGPositions = {
|
||||
id: "1234",
|
||||
left: 50,
|
||||
top: 50,
|
||||
right: 110,
|
||||
bottom: 110,
|
||||
},
|
||||
newPositions = {
|
||||
id: "1234",
|
||||
left: 40,
|
||||
top: 30,
|
||||
right: 80,
|
||||
bottom: 70,
|
||||
};
|
||||
|
||||
it("should check X and Y Coordinates for constant direction", () => {
|
||||
expect(getDelta(OGPositions, newPositions, ReflowDirection.LEFT)).toEqual(
|
||||
{
|
||||
X: 10,
|
||||
Y: 20,
|
||||
},
|
||||
);
|
||||
expect(getDelta(OGPositions, newPositions, ReflowDirection.TOP)).toEqual({
|
||||
X: 10,
|
||||
Y: 20,
|
||||
});
|
||||
expect(
|
||||
getDelta(OGPositions, newPositions, ReflowDirection.RIGHT),
|
||||
).toEqual({
|
||||
X: 30,
|
||||
Y: 20,
|
||||
});
|
||||
expect(
|
||||
getDelta(OGPositions, newPositions, ReflowDirection.BOTTOM),
|
||||
).toEqual({
|
||||
X: 10,
|
||||
Y: 40,
|
||||
});
|
||||
});
|
||||
|
||||
it("should check X and Y Coordinates for composite direction", () => {
|
||||
expect(
|
||||
getDelta(OGPositions, newPositions, ReflowDirection.TOPLEFT),
|
||||
).toEqual({
|
||||
X: 10,
|
||||
Y: 20,
|
||||
});
|
||||
expect(
|
||||
getDelta(OGPositions, newPositions, ReflowDirection.TOPRIGHT),
|
||||
).toEqual({
|
||||
X: 30,
|
||||
Y: 20,
|
||||
});
|
||||
expect(
|
||||
getDelta(OGPositions, newPositions, ReflowDirection.BOTTOMLEFT),
|
||||
).toEqual({
|
||||
X: 10,
|
||||
Y: 40,
|
||||
});
|
||||
expect(
|
||||
getDelta(OGPositions, newPositions, ReflowDirection.BOTTOMRIGHT),
|
||||
).toEqual({
|
||||
X: 30,
|
||||
Y: 40,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Test getCollidingSpaces and getCollidingSpacesInDirection method", () => {
|
||||
const newPositions = {
|
||||
id: "1234",
|
||||
left: 40,
|
||||
top: 30,
|
||||
right: 80,
|
||||
bottom: 70,
|
||||
},
|
||||
occupiedSpaces = [
|
||||
{
|
||||
id: "1234",
|
||||
left: 40,
|
||||
top: 30,
|
||||
right: 80,
|
||||
bottom: 70,
|
||||
},
|
||||
{
|
||||
id: "1235",
|
||||
left: 10,
|
||||
top: 10,
|
||||
right: 35,
|
||||
bottom: 25,
|
||||
},
|
||||
{
|
||||
id: "1236",
|
||||
left: 30,
|
||||
top: 20,
|
||||
right: 50,
|
||||
bottom: 35,
|
||||
},
|
||||
{
|
||||
id: "1237",
|
||||
left: 10,
|
||||
top: 10,
|
||||
right: 90,
|
||||
bottom: 80,
|
||||
},
|
||||
];
|
||||
|
||||
it("should return collidingSpaces with direction", () => {
|
||||
const collidingSpaces = {
|
||||
"1236": {
|
||||
id: "1236",
|
||||
left: 30,
|
||||
top: 20,
|
||||
right: 50,
|
||||
bottom: 35,
|
||||
direction: "BOTTOM",
|
||||
},
|
||||
"1237": {
|
||||
id: "1237",
|
||||
left: 10,
|
||||
top: 10,
|
||||
right: 90,
|
||||
bottom: 80,
|
||||
direction: "BOTTOM",
|
||||
},
|
||||
};
|
||||
expect(
|
||||
getCollidingSpaces(newPositions, ReflowDirection.BOTTOM, occupiedSpaces)
|
||||
.collidingSpaceMap,
|
||||
).toEqual(collidingSpaces);
|
||||
});
|
||||
|
||||
it("should return collidingSpaces with predicted direction based on Previous positions", () => {
|
||||
const collidingSpaces = {
|
||||
"1236": {
|
||||
id: "1236",
|
||||
left: 30,
|
||||
top: 20,
|
||||
right: 50,
|
||||
bottom: 35,
|
||||
direction: "LEFT",
|
||||
},
|
||||
"1237": {
|
||||
id: "1237",
|
||||
left: 10,
|
||||
top: 10,
|
||||
right: 90,
|
||||
bottom: 80,
|
||||
direction: "BOTTOM",
|
||||
},
|
||||
},
|
||||
prevPositions = {
|
||||
id: "1234",
|
||||
left: 50,
|
||||
top: 30,
|
||||
right: 90,
|
||||
bottom: 70,
|
||||
};
|
||||
expect(
|
||||
getCollidingSpaces(
|
||||
newPositions,
|
||||
ReflowDirection.BOTTOM,
|
||||
occupiedSpaces,
|
||||
true,
|
||||
prevPositions,
|
||||
).collidingSpaceMap,
|
||||
).toEqual(collidingSpaces);
|
||||
});
|
||||
|
||||
it("should return collidingSpaces In a particular direction", () => {
|
||||
const collidingSpaces = [
|
||||
{
|
||||
id: "1236",
|
||||
left: 30,
|
||||
top: 20,
|
||||
right: 50,
|
||||
bottom: 35,
|
||||
direction: "LEFT",
|
||||
},
|
||||
];
|
||||
expect(
|
||||
getCollidingSpacesInDirection(
|
||||
newPositions,
|
||||
ReflowDirection.LEFT,
|
||||
occupiedSpaces,
|
||||
).collidingSpaces,
|
||||
).toEqual(collidingSpaces);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Test filterSpaceById method", () => {
|
||||
const occupiedSpaces = [
|
||||
{
|
||||
id: "1234",
|
||||
left: 40,
|
||||
top: 30,
|
||||
right: 80,
|
||||
bottom: 70,
|
||||
},
|
||||
{
|
||||
id: "1235",
|
||||
left: 10,
|
||||
top: 10,
|
||||
right: 35,
|
||||
bottom: 25,
|
||||
},
|
||||
{
|
||||
id: "1236",
|
||||
left: 30,
|
||||
top: 20,
|
||||
right: 50,
|
||||
bottom: 35,
|
||||
},
|
||||
];
|
||||
|
||||
it("should return filtered Spaces", () => {
|
||||
const filteredSpaces = occupiedSpaces.slice(1);
|
||||
expect(filterSpaceById("1234", occupiedSpaces)).toEqual(filteredSpaces);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Test getMaxX method", () => {
|
||||
const collisionTree = {
|
||||
id: "1234",
|
||||
left: 40,
|
||||
top: 30,
|
||||
right: 60,
|
||||
bottom: 70,
|
||||
children: [],
|
||||
};
|
||||
|
||||
it("should return max number for LEFT Direction when not Resizing", () => {
|
||||
const depth = 2;
|
||||
expect(
|
||||
getMaxX(
|
||||
collisionTree,
|
||||
gridProps,
|
||||
ReflowDirection.LEFT,
|
||||
depth,
|
||||
30,
|
||||
false,
|
||||
),
|
||||
).toBe(-1 * 10 * gridProps.parentColumnSpace);
|
||||
});
|
||||
it("should return max number for LEFT Direction when Resizing", () => {
|
||||
const depth = 2;
|
||||
expect(
|
||||
getMaxX(
|
||||
collisionTree,
|
||||
gridProps,
|
||||
ReflowDirection.LEFT,
|
||||
depth,
|
||||
30,
|
||||
true,
|
||||
),
|
||||
).toBe(
|
||||
-1 *
|
||||
(collisionTree.left - depth * HORIZONTAL_RESIZE_LIMIT) *
|
||||
gridProps.parentColumnSpace,
|
||||
);
|
||||
});
|
||||
it("should return max number for RIGHT Direction when not Resizing", () => {
|
||||
const depth = 2;
|
||||
expect(
|
||||
getMaxX(
|
||||
collisionTree,
|
||||
gridProps,
|
||||
ReflowDirection.RIGHT,
|
||||
depth,
|
||||
30,
|
||||
false,
|
||||
),
|
||||
).toBe(
|
||||
(gridProps.maxGridColumns - collisionTree.right - 30) *
|
||||
gridProps.parentColumnSpace,
|
||||
);
|
||||
});
|
||||
it("should return max number for RIGHT Direction when Resizing", () => {
|
||||
const depth = 2;
|
||||
expect(
|
||||
getMaxX(
|
||||
collisionTree,
|
||||
gridProps,
|
||||
ReflowDirection.RIGHT,
|
||||
depth,
|
||||
30,
|
||||
true,
|
||||
),
|
||||
).toBe(
|
||||
(gridProps.maxGridColumns -
|
||||
collisionTree.right -
|
||||
depth * HORIZONTAL_RESIZE_LIMIT) *
|
||||
gridProps.parentColumnSpace,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Test getMaxY method", () => {
|
||||
const collisionTree = {
|
||||
id: "1234",
|
||||
left: 40,
|
||||
top: 30,
|
||||
right: 60,
|
||||
bottom: 70,
|
||||
children: [],
|
||||
};
|
||||
|
||||
it("should return max number for TOP Direction when not Resizing", () => {
|
||||
const depth = 2;
|
||||
expect(
|
||||
getMaxY(
|
||||
collisionTree,
|
||||
gridProps,
|
||||
ReflowDirection.TOP,
|
||||
depth,
|
||||
20,
|
||||
false,
|
||||
),
|
||||
).toBe(-1 * 10 * gridProps.parentRowSpace);
|
||||
});
|
||||
it("should return max number for TOP Direction when Resizing", () => {
|
||||
const depth = 2;
|
||||
expect(
|
||||
getMaxY(collisionTree, gridProps, ReflowDirection.TOP, depth, 20, true),
|
||||
).toBe(
|
||||
-1 *
|
||||
(collisionTree.top - depth * VERTICAL_RESIZE_LIMIT) *
|
||||
gridProps.parentRowSpace,
|
||||
);
|
||||
});
|
||||
it("should return max number for BOTTOM Direction with or without Resizing", () => {
|
||||
const depth = 2;
|
||||
expect(
|
||||
getMaxY(
|
||||
collisionTree,
|
||||
gridProps,
|
||||
ReflowDirection.BOTTOM,
|
||||
depth,
|
||||
230,
|
||||
false,
|
||||
),
|
||||
).toBe(Infinity);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Test getReflowDistance method", () => {
|
||||
const collisionTree = {
|
||||
id: "1234",
|
||||
left: 90,
|
||||
top: 100,
|
||||
right: 150,
|
||||
bottom: 140,
|
||||
children: [],
|
||||
},
|
||||
width =
|
||||
(collisionTree.right - collisionTree.left) *
|
||||
gridProps.parentColumnSpace,
|
||||
height =
|
||||
(collisionTree.bottom - collisionTree.top) * gridProps.parentRowSpace;
|
||||
|
||||
it("should return distance for space in TOP direction", () => {
|
||||
//before it reaches max limit
|
||||
let dimensionBeforeCollision = 40,
|
||||
emptySpaces = 20,
|
||||
maxDistance = -60 * gridProps.parentRowSpace;
|
||||
expect(
|
||||
getReflowDistance(
|
||||
collisionTree,
|
||||
ReflowDirection.TOP,
|
||||
maxDistance,
|
||||
dimensionBeforeCollision,
|
||||
height,
|
||||
emptySpaces,
|
||||
gridProps.parentRowSpace,
|
||||
),
|
||||
).toBe(
|
||||
-1 *
|
||||
(dimensionBeforeCollision - emptySpaces) *
|
||||
gridProps.parentRowSpace,
|
||||
);
|
||||
|
||||
//reflowing past max limit
|
||||
dimensionBeforeCollision = 90;
|
||||
expect(
|
||||
getReflowDistance(
|
||||
collisionTree,
|
||||
ReflowDirection.TOP,
|
||||
maxDistance,
|
||||
dimensionBeforeCollision,
|
||||
height,
|
||||
emptySpaces,
|
||||
gridProps.parentRowSpace,
|
||||
),
|
||||
).toBe(maxDistance);
|
||||
});
|
||||
|
||||
it("should return distance for space in LEFT direction", () => {
|
||||
//before it reaches max limit
|
||||
let dimensionBeforeCollision = 40,
|
||||
emptySpaces = 20,
|
||||
maxDistance = -60 * gridProps.parentRowSpace;
|
||||
expect(
|
||||
getReflowDistance(
|
||||
collisionTree,
|
||||
ReflowDirection.LEFT,
|
||||
maxDistance,
|
||||
dimensionBeforeCollision,
|
||||
height,
|
||||
emptySpaces,
|
||||
gridProps.parentRowSpace,
|
||||
),
|
||||
).toBe(
|
||||
-1 *
|
||||
(dimensionBeforeCollision - emptySpaces) *
|
||||
gridProps.parentRowSpace,
|
||||
);
|
||||
|
||||
//reflowing past max limit
|
||||
dimensionBeforeCollision = 90;
|
||||
expect(
|
||||
getReflowDistance(
|
||||
collisionTree,
|
||||
ReflowDirection.LEFT,
|
||||
maxDistance,
|
||||
dimensionBeforeCollision,
|
||||
height,
|
||||
emptySpaces,
|
||||
gridProps.parentRowSpace,
|
||||
),
|
||||
).toBe(maxDistance);
|
||||
});
|
||||
|
||||
it("should return distance for space in RIGHT direction", () => {
|
||||
//before it reaches max limit
|
||||
let dimensionBeforeCollision = -40,
|
||||
emptySpaces = 20,
|
||||
maxDistance = 60 * gridProps.parentRowSpace;
|
||||
expect(
|
||||
getReflowDistance(
|
||||
collisionTree,
|
||||
ReflowDirection.RIGHT,
|
||||
maxDistance,
|
||||
dimensionBeforeCollision,
|
||||
width,
|
||||
emptySpaces,
|
||||
gridProps.parentColumnSpace,
|
||||
),
|
||||
).toBe(
|
||||
-1 *
|
||||
(dimensionBeforeCollision + emptySpaces) *
|
||||
gridProps.parentColumnSpace,
|
||||
);
|
||||
|
||||
//reflowing past max limit
|
||||
dimensionBeforeCollision = -90;
|
||||
expect(
|
||||
getReflowDistance(
|
||||
collisionTree,
|
||||
ReflowDirection.RIGHT,
|
||||
maxDistance,
|
||||
dimensionBeforeCollision,
|
||||
width,
|
||||
emptySpaces,
|
||||
gridProps.parentColumnSpace,
|
||||
),
|
||||
).toBe(maxDistance);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Test getResizedDimension method", () => {
|
||||
const collisionTree = {
|
||||
id: "1234",
|
||||
left: 90,
|
||||
top: 100,
|
||||
right: 150,
|
||||
bottom: 140,
|
||||
children: [],
|
||||
},
|
||||
width =
|
||||
(collisionTree.right - collisionTree.left) *
|
||||
gridProps.parentColumnSpace,
|
||||
height =
|
||||
(collisionTree.bottom - collisionTree.top) * gridProps.parentRowSpace;
|
||||
|
||||
describe("should return resized height in TOP direction", () => {
|
||||
let dimensionBeforeCollision = 40,
|
||||
emptySpaces = 20,
|
||||
maxDistance = -60 * gridProps.parentRowSpace,
|
||||
travelDistance = -50;
|
||||
|
||||
it("should return height when resize is off", () => {
|
||||
expect(
|
||||
getResizedDimension(
|
||||
collisionTree,
|
||||
ReflowDirection.TOP,
|
||||
travelDistance * gridProps.parentRowSpace,
|
||||
maxDistance,
|
||||
dimensionBeforeCollision,
|
||||
gridProps.parentRowSpace,
|
||||
emptySpaces,
|
||||
VERTICAL_RESIZE_LIMIT,
|
||||
),
|
||||
).toBe(height);
|
||||
});
|
||||
|
||||
it("should return height before resize threshold", () => {
|
||||
expect(
|
||||
getResizedDimension(
|
||||
collisionTree,
|
||||
ReflowDirection.TOP,
|
||||
travelDistance * gridProps.parentRowSpace,
|
||||
maxDistance,
|
||||
dimensionBeforeCollision,
|
||||
gridProps.parentRowSpace,
|
||||
emptySpaces,
|
||||
VERTICAL_RESIZE_LIMIT,
|
||||
true,
|
||||
),
|
||||
).toBe(height);
|
||||
});
|
||||
it("should return resized height after resize threshold before min height", () => {
|
||||
dimensionBeforeCollision = 100;
|
||||
const resizedHeight =
|
||||
maxDistance +
|
||||
(dimensionBeforeCollision - emptySpaces) * gridProps.parentRowSpace;
|
||||
expect(
|
||||
getResizedDimension(
|
||||
collisionTree,
|
||||
ReflowDirection.TOP,
|
||||
travelDistance * gridProps.parentRowSpace,
|
||||
maxDistance,
|
||||
dimensionBeforeCollision,
|
||||
gridProps.parentRowSpace,
|
||||
emptySpaces,
|
||||
VERTICAL_RESIZE_LIMIT,
|
||||
true,
|
||||
),
|
||||
).toBe(resizedHeight);
|
||||
});
|
||||
it("should return min height after resize threshold after reaching min height", () => {
|
||||
dimensionBeforeCollision = 130;
|
||||
expect(
|
||||
getResizedDimension(
|
||||
collisionTree,
|
||||
ReflowDirection.TOP,
|
||||
travelDistance * gridProps.parentRowSpace,
|
||||
maxDistance,
|
||||
dimensionBeforeCollision,
|
||||
gridProps.parentRowSpace,
|
||||
emptySpaces,
|
||||
VERTICAL_RESIZE_LIMIT,
|
||||
true,
|
||||
),
|
||||
).toBe(VERTICAL_RESIZE_LIMIT * gridProps.parentRowSpace);
|
||||
});
|
||||
});
|
||||
|
||||
describe("should return resized width in LEFT direction", () => {
|
||||
let dimensionBeforeCollision = 40,
|
||||
emptySpaces = 20,
|
||||
maxDistance = -60 * gridProps.parentColumnSpace,
|
||||
travelDistance = -50;
|
||||
|
||||
it("should return width when resize is off", () => {
|
||||
expect(
|
||||
getResizedDimension(
|
||||
collisionTree,
|
||||
ReflowDirection.LEFT,
|
||||
travelDistance * gridProps.parentColumnSpace,
|
||||
maxDistance,
|
||||
dimensionBeforeCollision,
|
||||
gridProps.parentColumnSpace,
|
||||
emptySpaces,
|
||||
HORIZONTAL_RESIZE_LIMIT,
|
||||
),
|
||||
).toBe(width);
|
||||
});
|
||||
|
||||
it("should return width before resize threshold", () => {
|
||||
expect(
|
||||
getResizedDimension(
|
||||
collisionTree,
|
||||
ReflowDirection.LEFT,
|
||||
travelDistance * gridProps.parentColumnSpace,
|
||||
maxDistance,
|
||||
dimensionBeforeCollision,
|
||||
gridProps.parentColumnSpace,
|
||||
emptySpaces,
|
||||
HORIZONTAL_RESIZE_LIMIT,
|
||||
true,
|
||||
),
|
||||
).toBe(width);
|
||||
});
|
||||
|
||||
it("should return resized width after resize threshold before min width", () => {
|
||||
dimensionBeforeCollision = 110;
|
||||
const resizedWidth =
|
||||
maxDistance +
|
||||
(dimensionBeforeCollision - emptySpaces) *
|
||||
gridProps.parentColumnSpace;
|
||||
expect(
|
||||
getResizedDimension(
|
||||
collisionTree,
|
||||
ReflowDirection.LEFT,
|
||||
travelDistance * gridProps.parentColumnSpace,
|
||||
maxDistance,
|
||||
dimensionBeforeCollision,
|
||||
gridProps.parentColumnSpace,
|
||||
emptySpaces,
|
||||
HORIZONTAL_RESIZE_LIMIT,
|
||||
true,
|
||||
),
|
||||
).toBe(resizedWidth);
|
||||
});
|
||||
it("should return min width after resize threshold after reaching min width", () => {
|
||||
dimensionBeforeCollision = 150;
|
||||
expect(
|
||||
getResizedDimension(
|
||||
collisionTree,
|
||||
ReflowDirection.LEFT,
|
||||
travelDistance * gridProps.parentColumnSpace,
|
||||
maxDistance,
|
||||
dimensionBeforeCollision,
|
||||
gridProps.parentColumnSpace,
|
||||
emptySpaces,
|
||||
HORIZONTAL_RESIZE_LIMIT,
|
||||
true,
|
||||
),
|
||||
).toBe(HORIZONTAL_RESIZE_LIMIT * gridProps.parentColumnSpace);
|
||||
});
|
||||
});
|
||||
|
||||
describe("should return resized width in RIGHT direction", () => {
|
||||
let dimensionBeforeCollision = -40,
|
||||
emptySpaces = 20,
|
||||
maxDistance = 60 * gridProps.parentColumnSpace,
|
||||
travelDistance = 50;
|
||||
|
||||
it("should return width when resize is off", () => {
|
||||
expect(
|
||||
getResizedDimension(
|
||||
collisionTree,
|
||||
ReflowDirection.RIGHT,
|
||||
travelDistance * gridProps.parentColumnSpace,
|
||||
maxDistance,
|
||||
dimensionBeforeCollision,
|
||||
gridProps.parentColumnSpace,
|
||||
emptySpaces,
|
||||
HORIZONTAL_RESIZE_LIMIT,
|
||||
),
|
||||
).toBe(width);
|
||||
});
|
||||
|
||||
it("should return width before resize threshold", () => {
|
||||
expect(
|
||||
getResizedDimension(
|
||||
collisionTree,
|
||||
ReflowDirection.RIGHT,
|
||||
travelDistance * gridProps.parentColumnSpace,
|
||||
maxDistance,
|
||||
dimensionBeforeCollision,
|
||||
gridProps.parentColumnSpace,
|
||||
emptySpaces,
|
||||
HORIZONTAL_RESIZE_LIMIT,
|
||||
true,
|
||||
),
|
||||
).toBe(width);
|
||||
});
|
||||
|
||||
it("should return resized width after resize threshold before min width", () => {
|
||||
dimensionBeforeCollision = -110;
|
||||
const resizedWidth =
|
||||
-1 *
|
||||
(dimensionBeforeCollision + emptySpaces) *
|
||||
gridProps.parentColumnSpace -
|
||||
maxDistance;
|
||||
expect(
|
||||
getResizedDimension(
|
||||
collisionTree,
|
||||
ReflowDirection.RIGHT,
|
||||
travelDistance * gridProps.parentColumnSpace,
|
||||
maxDistance,
|
||||
dimensionBeforeCollision,
|
||||
gridProps.parentColumnSpace,
|
||||
emptySpaces,
|
||||
HORIZONTAL_RESIZE_LIMIT,
|
||||
true,
|
||||
),
|
||||
).toBe(resizedWidth);
|
||||
});
|
||||
it("should return min width after resize threshold after reaching min width", () => {
|
||||
dimensionBeforeCollision = -150;
|
||||
expect(
|
||||
getResizedDimension(
|
||||
collisionTree,
|
||||
ReflowDirection.RIGHT,
|
||||
travelDistance * gridProps.parentColumnSpace,
|
||||
maxDistance,
|
||||
dimensionBeforeCollision,
|
||||
gridProps.parentColumnSpace,
|
||||
emptySpaces,
|
||||
HORIZONTAL_RESIZE_LIMIT,
|
||||
true,
|
||||
),
|
||||
).toBe(HORIZONTAL_RESIZE_LIMIT * gridProps.parentColumnSpace);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
507
app/client/src/resizable/resizenreflow/index.tsx
Normal file
507
app/client/src/resizable/resizenreflow/index.tsx
Normal file
|
|
@ -0,0 +1,507 @@
|
|||
import React, { ReactNode, useState, useEffect, useRef, useMemo } from "react";
|
||||
import styled, { StyledComponent } from "styled-components";
|
||||
import { WIDGET_PADDING } from "constants/WidgetConstants";
|
||||
import { useDrag } from "react-use-gesture";
|
||||
import { Spring } from "react-spring/renderprops";
|
||||
import PerformanceTracker, {
|
||||
PerformanceTransactionName,
|
||||
} from "utils/PerformanceTracker";
|
||||
import { useReflow } from "utils/hooks/useReflow";
|
||||
import {
|
||||
getReflowSelector,
|
||||
isReflowEnabled,
|
||||
} from "selectors/widgetReflowSelectors";
|
||||
import { useSelector } from "react-redux";
|
||||
import { OccupiedSpace } from "constants/CanvasEditorConstants";
|
||||
import { GridProps, ReflowDirection, ReflowedSpace } from "reflow/reflowTypes";
|
||||
import { getNearestParentCanvas } from "utils/generators";
|
||||
import { getOccupiedSpaces } from "selectors/editorSelectors";
|
||||
import { isDropZoneOccupied } from "utils/WidgetPropsUtils";
|
||||
|
||||
const ResizeWrapper = styled.div<{ prevents: boolean }>`
|
||||
display: block;
|
||||
& {
|
||||
* {
|
||||
pointer-events: ${(props) => !props.prevents && "none"};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const getSnappedValues = (
|
||||
x: number,
|
||||
y: number,
|
||||
snapGrid: { x: number; y: number },
|
||||
) => {
|
||||
return {
|
||||
x: Math.round(x / snapGrid.x) * snapGrid.x,
|
||||
y: Math.round(y / snapGrid.y) * snapGrid.y,
|
||||
};
|
||||
};
|
||||
|
||||
export type DimensionProps = {
|
||||
width: number;
|
||||
height: number;
|
||||
x: number;
|
||||
y: number;
|
||||
reset?: boolean;
|
||||
direction: ReflowDirection;
|
||||
X?: number;
|
||||
Y?: number;
|
||||
};
|
||||
|
||||
type ResizableHandleProps = {
|
||||
allowResize: boolean;
|
||||
scrollParent: HTMLDivElement | null;
|
||||
checkForCollision: (widgetNewSize: {
|
||||
left: number;
|
||||
top: number;
|
||||
bottom: number;
|
||||
right: number;
|
||||
}) => boolean;
|
||||
dragCallback: (x: number, y: number) => void;
|
||||
component: StyledComponent<"div", Record<string, unknown>>;
|
||||
onStart: () => void;
|
||||
onStop: () => void;
|
||||
snapGrid: {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
};
|
||||
|
||||
function ResizableHandle(props: ResizableHandleProps) {
|
||||
const bind = useDrag((state) => {
|
||||
const {
|
||||
first,
|
||||
last,
|
||||
dragging,
|
||||
memo,
|
||||
movement: [mx, my],
|
||||
} = state;
|
||||
if (!props.allowResize) {
|
||||
return;
|
||||
}
|
||||
const scrollParent = getNearestParentCanvas(props.scrollParent);
|
||||
|
||||
const initialScrollTop = memo ? memo.scrollTop : 0;
|
||||
const currentScrollTop = scrollParent?.scrollTop || 0;
|
||||
|
||||
const deltaScrolledHeight = currentScrollTop - initialScrollTop;
|
||||
const deltaY = my + deltaScrolledHeight;
|
||||
const snapped = getSnappedValues(mx, deltaY, props.snapGrid);
|
||||
if (first) {
|
||||
props.onStart();
|
||||
return { scrollTop: currentScrollTop, snapped };
|
||||
}
|
||||
const { snapped: snappedMemo } = memo;
|
||||
|
||||
if (
|
||||
dragging &&
|
||||
snappedMemo &&
|
||||
(snapped.x !== snappedMemo.x || snapped.y !== snappedMemo.y)
|
||||
) {
|
||||
props.dragCallback(snapped.x, snapped.y);
|
||||
}
|
||||
if (last) {
|
||||
props.onStop();
|
||||
}
|
||||
|
||||
return { ...memo, snapped };
|
||||
});
|
||||
const propsToPass = {
|
||||
...bind(),
|
||||
showAsBorder: !props.allowResize,
|
||||
};
|
||||
|
||||
return <props.component {...propsToPass} />;
|
||||
}
|
||||
|
||||
type ResizableProps = {
|
||||
allowResize: boolean;
|
||||
handles: {
|
||||
left?: StyledComponent<"div", Record<string, unknown>>;
|
||||
top?: StyledComponent<"div", Record<string, unknown>>;
|
||||
bottom?: StyledComponent<"div", Record<string, unknown>>;
|
||||
right?: StyledComponent<"div", Record<string, unknown>>;
|
||||
bottomRight?: StyledComponent<"div", Record<string, unknown>>;
|
||||
topLeft?: StyledComponent<"div", Record<string, unknown>>;
|
||||
topRight?: StyledComponent<"div", Record<string, unknown>>;
|
||||
bottomLeft?: StyledComponent<"div", Record<string, unknown>>;
|
||||
};
|
||||
componentWidth: number;
|
||||
componentHeight: number;
|
||||
children: ReactNode;
|
||||
updateBottomRow: (bottomRow: number) => void;
|
||||
getResizedPositions: (
|
||||
size: { width: number; height: number },
|
||||
position: { x: number; y: number },
|
||||
) => {
|
||||
canResizeHorizontally: boolean;
|
||||
canResizeVertically: boolean;
|
||||
resizedPositions?: OccupiedSpace;
|
||||
};
|
||||
originalPositions: OccupiedSpace;
|
||||
onStart: () => void;
|
||||
onStop: (
|
||||
size: { width: number; height: number },
|
||||
position: { x: number; y: number },
|
||||
) => void;
|
||||
snapGrid: { x: number; y: number };
|
||||
enable: boolean;
|
||||
className?: string;
|
||||
parentId?: string;
|
||||
widgetId: string;
|
||||
gridProps: GridProps;
|
||||
zWidgetType?: string;
|
||||
zWidgetId?: string;
|
||||
};
|
||||
|
||||
export function ReflowResizable(props: ResizableProps) {
|
||||
const resizableRef = useRef<HTMLDivElement>(null);
|
||||
const [isResizing, setResizing] = useState(false);
|
||||
const reflowEnabled = useSelector(isReflowEnabled);
|
||||
|
||||
const occupiedSpaces = useSelector(getOccupiedSpaces);
|
||||
const occupiedSpacesBySiblingWidgets = useMemo(() => {
|
||||
return occupiedSpaces && props.parentId && occupiedSpaces[props.parentId]
|
||||
? occupiedSpaces[props.parentId]
|
||||
: undefined;
|
||||
}, [occupiedSpaces, props.parentId]);
|
||||
const checkForCollision = (widgetNewSize: {
|
||||
left: number;
|
||||
top: number;
|
||||
bottom: number;
|
||||
right: number;
|
||||
}) => {
|
||||
return isDropZoneOccupied(
|
||||
widgetNewSize,
|
||||
props.widgetId,
|
||||
occupiedSpacesBySiblingWidgets,
|
||||
);
|
||||
};
|
||||
// Performance tracking start
|
||||
const sentryPerfTags = props.zWidgetType
|
||||
? [{ name: "widget_type", value: props.zWidgetType }]
|
||||
: [];
|
||||
PerformanceTracker.startTracking(
|
||||
PerformanceTransactionName.SHOW_RESIZE_HANDLES,
|
||||
{ widgetId: props.zWidgetId },
|
||||
true,
|
||||
sentryPerfTags,
|
||||
);
|
||||
const reflowSelector = getReflowSelector(props.widgetId);
|
||||
|
||||
const equal = (
|
||||
reflowA: ReflowedSpace | undefined,
|
||||
reflowB: ReflowedSpace | undefined,
|
||||
) => {
|
||||
if (
|
||||
reflowA?.width !== reflowB?.width ||
|
||||
reflowA?.height !== reflowB?.height
|
||||
)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const reflowedPosition = useSelector(reflowSelector, equal);
|
||||
|
||||
const reflow = useReflow(
|
||||
props.widgetId,
|
||||
props.parentId || "",
|
||||
props.gridProps,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
PerformanceTracker.stopTracking(
|
||||
PerformanceTransactionName.SHOW_RESIZE_HANDLES,
|
||||
);
|
||||
}, []);
|
||||
//end
|
||||
const [pointerEvents, togglePointerEvents] = useState(true);
|
||||
const [newDimensions, set] = useState<DimensionProps>({
|
||||
width: props.componentWidth,
|
||||
height: props.componentHeight,
|
||||
x: 0,
|
||||
y: 0,
|
||||
reset: false,
|
||||
direction: ReflowDirection.UNSET,
|
||||
});
|
||||
|
||||
const setNewDimensions = (rect: DimensionProps) => {
|
||||
const { direction, height, width, x, y } = rect;
|
||||
const {
|
||||
canResizeHorizontally,
|
||||
canResizeVertically,
|
||||
resizedPositions,
|
||||
} = props.getResizedPositions({ width, height }, { x, y });
|
||||
const canResize = canResizeHorizontally || canResizeVertically;
|
||||
|
||||
if (canResize) {
|
||||
set((prevState) => {
|
||||
let newRect = { ...rect };
|
||||
|
||||
let canVerticalMove = true,
|
||||
canHorizontalMove = true,
|
||||
bottomMostRow = 0;
|
||||
if (!reflowEnabled && resizedPositions) {
|
||||
const isColliding = checkForCollision(resizedPositions);
|
||||
if (isColliding) {
|
||||
return prevState;
|
||||
}
|
||||
}
|
||||
if (resizedPositions) {
|
||||
({ bottomMostRow, canHorizontalMove, canVerticalMove } = reflow(
|
||||
resizedPositions,
|
||||
props.originalPositions,
|
||||
direction,
|
||||
true,
|
||||
));
|
||||
}
|
||||
|
||||
if (!canHorizontalMove || !canResizeHorizontally) {
|
||||
newRect = {
|
||||
...newRect,
|
||||
width: prevState.width,
|
||||
x: prevState.x,
|
||||
X: prevState.X,
|
||||
};
|
||||
}
|
||||
|
||||
if (!canVerticalMove || !canResizeVertically) {
|
||||
newRect = {
|
||||
...newRect,
|
||||
height: prevState.height,
|
||||
y: prevState.y,
|
||||
Y: prevState.Y,
|
||||
};
|
||||
}
|
||||
|
||||
if (bottomMostRow) {
|
||||
props.updateBottomRow(bottomMostRow);
|
||||
}
|
||||
|
||||
return newRect;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
set((prevDimensions) => {
|
||||
return {
|
||||
...prevDimensions,
|
||||
width: props.componentWidth,
|
||||
height: props.componentHeight,
|
||||
x: 0,
|
||||
y: 0,
|
||||
reset: true,
|
||||
};
|
||||
});
|
||||
}, [props.componentHeight, props.componentWidth, isResizing]);
|
||||
|
||||
const handles = [];
|
||||
|
||||
if (props.handles.left) {
|
||||
handles.push({
|
||||
dragCallback: (x: number) => {
|
||||
setNewDimensions({
|
||||
width: props.componentWidth - x,
|
||||
height: newDimensions.height,
|
||||
x: x,
|
||||
y: newDimensions.y,
|
||||
direction: ReflowDirection.LEFT,
|
||||
X: x,
|
||||
});
|
||||
},
|
||||
component: props.handles.left,
|
||||
});
|
||||
}
|
||||
|
||||
if (props.handles.top) {
|
||||
handles.push({
|
||||
dragCallback: (x: number, y: number) => {
|
||||
setNewDimensions({
|
||||
width: newDimensions.width,
|
||||
height: props.componentHeight - y,
|
||||
y: y,
|
||||
x: newDimensions.x,
|
||||
direction: ReflowDirection.TOP,
|
||||
Y: y,
|
||||
});
|
||||
},
|
||||
component: props.handles.top,
|
||||
});
|
||||
}
|
||||
|
||||
if (props.handles.right) {
|
||||
handles.push({
|
||||
dragCallback: (x: number) => {
|
||||
setNewDimensions({
|
||||
width: props.componentWidth + x,
|
||||
height: newDimensions.height,
|
||||
x: newDimensions.x,
|
||||
y: newDimensions.y,
|
||||
direction: ReflowDirection.RIGHT,
|
||||
X: x,
|
||||
});
|
||||
},
|
||||
component: props.handles.right,
|
||||
});
|
||||
}
|
||||
|
||||
if (props.handles.bottom) {
|
||||
handles.push({
|
||||
dragCallback: (x: number, y: number) => {
|
||||
setNewDimensions({
|
||||
width: newDimensions.width,
|
||||
height: props.componentHeight + y,
|
||||
x: newDimensions.x,
|
||||
y: newDimensions.y,
|
||||
direction: ReflowDirection.BOTTOM,
|
||||
Y: y,
|
||||
});
|
||||
},
|
||||
component: props.handles.bottom,
|
||||
});
|
||||
}
|
||||
|
||||
if (props.handles.topLeft) {
|
||||
handles.push({
|
||||
dragCallback: (x: number, y: number) => {
|
||||
setNewDimensions({
|
||||
width: props.componentWidth - x,
|
||||
height: props.componentHeight - y,
|
||||
x: x,
|
||||
y: y,
|
||||
direction: ReflowDirection.TOPLEFT,
|
||||
X: x,
|
||||
Y: y,
|
||||
});
|
||||
},
|
||||
component: props.handles.topLeft,
|
||||
});
|
||||
}
|
||||
|
||||
if (props.handles.topRight) {
|
||||
handles.push({
|
||||
dragCallback: (x: number, y: number) => {
|
||||
setNewDimensions({
|
||||
width: props.componentWidth + x,
|
||||
height: props.componentHeight - y,
|
||||
x: newDimensions.x,
|
||||
y: y,
|
||||
direction: ReflowDirection.TOPRIGHT,
|
||||
X: x,
|
||||
Y: y,
|
||||
});
|
||||
},
|
||||
component: props.handles.topRight,
|
||||
});
|
||||
}
|
||||
|
||||
if (props.handles.bottomRight) {
|
||||
handles.push({
|
||||
dragCallback: (x: number, y: number) => {
|
||||
setNewDimensions({
|
||||
width: props.componentWidth + x,
|
||||
height: props.componentHeight + y,
|
||||
x: newDimensions.x,
|
||||
y: newDimensions.y,
|
||||
direction: ReflowDirection.BOTTOMRIGHT,
|
||||
X: x,
|
||||
Y: y,
|
||||
});
|
||||
},
|
||||
component: props.handles.bottomRight,
|
||||
});
|
||||
}
|
||||
|
||||
if (props.handles.bottomLeft) {
|
||||
handles.push({
|
||||
dragCallback: (x: number, y: number) => {
|
||||
setNewDimensions({
|
||||
width: props.componentWidth - x,
|
||||
height: props.componentHeight + y,
|
||||
x,
|
||||
y: newDimensions.y,
|
||||
direction: ReflowDirection.BOTTOMLEFT,
|
||||
X: x,
|
||||
Y: y,
|
||||
});
|
||||
},
|
||||
component: props.handles.bottomLeft,
|
||||
});
|
||||
}
|
||||
const onResizeStop = () => {
|
||||
togglePointerEvents(true);
|
||||
props.onStop(
|
||||
{
|
||||
width: newDimensions.width,
|
||||
height: newDimensions.height,
|
||||
},
|
||||
{
|
||||
x: newDimensions.x,
|
||||
y: newDimensions.y,
|
||||
},
|
||||
);
|
||||
setResizing(false);
|
||||
};
|
||||
|
||||
const renderHandles = handles.map((handle, index) => (
|
||||
<ResizableHandle
|
||||
{...handle}
|
||||
allowResize={props.allowResize}
|
||||
checkForCollision={checkForCollision}
|
||||
key={index}
|
||||
onStart={() => {
|
||||
togglePointerEvents(false);
|
||||
props.onStart();
|
||||
setResizing(true);
|
||||
}}
|
||||
onStop={onResizeStop}
|
||||
scrollParent={resizableRef.current}
|
||||
snapGrid={props.snapGrid}
|
||||
/>
|
||||
));
|
||||
|
||||
const widgetWidth =
|
||||
reflowedPosition?.width === undefined
|
||||
? newDimensions.width
|
||||
: reflowedPosition.width - 2 * WIDGET_PADDING;
|
||||
const widgetHeight =
|
||||
reflowedPosition?.height === undefined
|
||||
? newDimensions.height
|
||||
: reflowedPosition.height - 2 * WIDGET_PADDING;
|
||||
return (
|
||||
<Spring
|
||||
config={{
|
||||
clamp: true,
|
||||
friction: 0,
|
||||
tension: 999,
|
||||
}}
|
||||
from={{
|
||||
width: props.componentWidth,
|
||||
height: props.componentHeight,
|
||||
}}
|
||||
immediate={newDimensions.reset ? true : false}
|
||||
to={{
|
||||
width: widgetWidth,
|
||||
height: widgetHeight,
|
||||
transform: `translate3d(${newDimensions.x}px,${newDimensions.y}px,0)`,
|
||||
}}
|
||||
>
|
||||
{(_props) => (
|
||||
<ResizeWrapper
|
||||
className={props.className}
|
||||
prevents={pointerEvents}
|
||||
ref={resizableRef}
|
||||
style={_props}
|
||||
>
|
||||
{props.children}
|
||||
{props.enable && renderHandles}
|
||||
</ResizeWrapper>
|
||||
)}
|
||||
</Spring>
|
||||
);
|
||||
}
|
||||
|
||||
export default ReflowResizable;
|
||||
|
|
@ -10,21 +10,24 @@ import {
|
|||
} from "reducers/entityReducers/canvasWidgetsReducer";
|
||||
import { all, call, put, select, takeLatest } from "redux-saga/effects";
|
||||
import { WidgetDraggingUpdateParams } from "utils/hooks/useBlocksToBeDraggedOnCanvas";
|
||||
import { updateWidgetPosition } from "utils/WidgetPropsUtils";
|
||||
import { getWidget, getWidgets } from "./selectors";
|
||||
import log from "loglevel";
|
||||
import { cloneDeep } from "lodash";
|
||||
import { updateAndSaveLayout } from "actions/pageActions";
|
||||
import { updateAndSaveLayout, WidgetAddChild } from "actions/pageActions";
|
||||
import { calculateDropTargetRows } from "components/editorComponents/DropTargetUtils";
|
||||
import { GridDefaults } from "constants/WidgetConstants";
|
||||
import { WidgetProps } from "widgets/BaseWidget";
|
||||
import { getOccupiedSpacesSelectorForContainer } from "selectors/editorSelectors";
|
||||
import { OccupiedSpace } from "constants/CanvasEditorConstants";
|
||||
import { collisionCheckPostReflow } from "utils/reflowHookUtils";
|
||||
import { getUpdateDslAfterCreatingChild } from "./WidgetAdditionSagas";
|
||||
|
||||
export type WidgetMoveParams = {
|
||||
widgetId: string;
|
||||
leftColumn: number;
|
||||
topRow: number;
|
||||
bottomRow: number;
|
||||
rightColumn: number;
|
||||
parentId: string;
|
||||
/*
|
||||
If newParentId is different from what we have in redux store,
|
||||
|
|
@ -86,6 +89,117 @@ const getBottomMostRowAfterMove = (
|
|||
return widgetBottomRow;
|
||||
};
|
||||
|
||||
function* addWidgetAndMoveWidgetsSaga(
|
||||
actionPayload: ReduxAction<{
|
||||
newWidget: WidgetAddChild;
|
||||
draggedBlocksToUpdate: WidgetDraggingUpdateParams[];
|
||||
canvasId: string;
|
||||
}>,
|
||||
) {
|
||||
const start = performance.now();
|
||||
|
||||
const { canvasId, draggedBlocksToUpdate, newWidget } = actionPayload.payload;
|
||||
try {
|
||||
const updatedWidgetsOnAddAndMove: CanvasWidgetsReduxState = yield call(
|
||||
addWidgetAndMoveWidgets,
|
||||
newWidget,
|
||||
draggedBlocksToUpdate,
|
||||
canvasId,
|
||||
);
|
||||
if (
|
||||
!collisionCheckPostReflow(
|
||||
updatedWidgetsOnAddAndMove,
|
||||
draggedBlocksToUpdate.map((block) => block.widgetId),
|
||||
canvasId,
|
||||
)
|
||||
) {
|
||||
throw Error;
|
||||
}
|
||||
yield put(updateAndSaveLayout(updatedWidgetsOnAddAndMove));
|
||||
log.debug("move computations took", performance.now() - start, "ms");
|
||||
} catch (error) {
|
||||
yield put({
|
||||
type: ReduxActionErrorTypes.WIDGET_OPERATION_ERROR,
|
||||
payload: {
|
||||
action: ReduxActionTypes.WIDGETS_ADD_CHILD_AND_MOVE,
|
||||
error,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function* addWidgetAndMoveWidgets(
|
||||
newWidget: WidgetAddChild,
|
||||
draggedBlocksToUpdate: WidgetDraggingUpdateParams[],
|
||||
canvasId: string,
|
||||
) {
|
||||
const allWidgets: CanvasWidgetsReduxState = yield select(getWidgets);
|
||||
const updatedWidgetsOnAddition: CanvasWidgetsReduxState = yield call(
|
||||
getUpdateDslAfterCreatingChild,
|
||||
{ ...newWidget, widgetId: canvasId },
|
||||
);
|
||||
const bottomMostRowOnAddition = updatedWidgetsOnAddition[canvasId]
|
||||
? updatedWidgetsOnAddition[canvasId].bottomRow
|
||||
: 0;
|
||||
const allWidgetsAfterAddition = {
|
||||
...allWidgets,
|
||||
...updatedWidgetsOnAddition,
|
||||
};
|
||||
const updatedWidgetsOnMove: CanvasWidgetsReduxState = yield call(
|
||||
moveAndUpdateWidgets,
|
||||
allWidgetsAfterAddition,
|
||||
draggedBlocksToUpdate,
|
||||
canvasId,
|
||||
);
|
||||
const bottomMostRowOnMove = updatedWidgetsOnMove[canvasId]
|
||||
? updatedWidgetsOnMove[canvasId].bottomRow
|
||||
: 0;
|
||||
|
||||
const bottomMostRow =
|
||||
bottomMostRowOnAddition > bottomMostRowOnMove
|
||||
? bottomMostRowOnAddition
|
||||
: bottomMostRowOnMove;
|
||||
const updatedWidgets = {
|
||||
...updatedWidgetsOnMove,
|
||||
};
|
||||
updatedWidgets[canvasId].bottomRow = bottomMostRow;
|
||||
return updatedWidgets;
|
||||
}
|
||||
|
||||
function* moveAndUpdateWidgets(
|
||||
allWidgets: CanvasWidgetsReduxState,
|
||||
draggedBlocksToUpdate: WidgetDraggingUpdateParams[],
|
||||
canvasId: string,
|
||||
) {
|
||||
const widgets = cloneDeep(allWidgets);
|
||||
const bottomMostRowAfterMove = getBottomMostRowAfterMove(
|
||||
draggedBlocksToUpdate,
|
||||
allWidgets,
|
||||
);
|
||||
// draggedBlocksToUpdate is already sorted based on bottomRow
|
||||
const updatedWidgets = draggedBlocksToUpdate.reduce((widgetsObj, each) => {
|
||||
return moveWidget({
|
||||
...each.updateWidgetParams.payload,
|
||||
widgetId: each.widgetId,
|
||||
allWidgets: widgetsObj,
|
||||
});
|
||||
}, widgets);
|
||||
|
||||
const updatedCanvasBottomRow: number = yield call(
|
||||
getCanvasSizeAfterWidgetMove,
|
||||
canvasId,
|
||||
bottomMostRowAfterMove,
|
||||
);
|
||||
if (updatedCanvasBottomRow) {
|
||||
const canvasWidget = updatedWidgets[canvasId];
|
||||
updatedWidgets[canvasId] = {
|
||||
...canvasWidget,
|
||||
bottomRow: updatedCanvasBottomRow,
|
||||
};
|
||||
}
|
||||
return updatedWidgets;
|
||||
}
|
||||
|
||||
function* moveWidgetsSaga(
|
||||
actionPayload: ReduxAction<{
|
||||
draggedBlocksToUpdate: WidgetDraggingUpdateParams[];
|
||||
|
|
@ -95,36 +209,25 @@ function* moveWidgetsSaga(
|
|||
const start = performance.now();
|
||||
|
||||
const { canvasId, draggedBlocksToUpdate } = actionPayload.payload;
|
||||
const allWidgets: CanvasWidgetsReduxState = yield select(getWidgets);
|
||||
const widgets = cloneDeep(allWidgets);
|
||||
const bottomMostRowAfterMove = getBottomMostRowAfterMove(
|
||||
draggedBlocksToUpdate,
|
||||
allWidgets,
|
||||
);
|
||||
// draggedBlocksToUpdate is already sorted based on bottomRow
|
||||
|
||||
try {
|
||||
const updatedWidgets = draggedBlocksToUpdate.reduce((widgetsObj, each) => {
|
||||
return moveWidget({
|
||||
...each.updateWidgetParams.payload,
|
||||
widgetId: each.widgetId,
|
||||
allWidgets: widgetsObj,
|
||||
});
|
||||
}, widgets);
|
||||
const allWidgets: CanvasWidgetsReduxState = yield select(getWidgets);
|
||||
|
||||
const updatedCanvasBottomRow: number = yield call(
|
||||
getCanvasSizeAfterWidgetMove,
|
||||
const updatedWidgetsOnMove: CanvasWidgetsReduxState = yield call(
|
||||
moveAndUpdateWidgets,
|
||||
allWidgets,
|
||||
draggedBlocksToUpdate,
|
||||
canvasId,
|
||||
bottomMostRowAfterMove,
|
||||
);
|
||||
if (updatedCanvasBottomRow) {
|
||||
const canvasWidget = updatedWidgets[canvasId];
|
||||
updatedWidgets[canvasId] = {
|
||||
...canvasWidget,
|
||||
bottomRow: updatedCanvasBottomRow,
|
||||
};
|
||||
if (
|
||||
!collisionCheckPostReflow(
|
||||
updatedWidgetsOnMove,
|
||||
draggedBlocksToUpdate.map((block) => block.widgetId),
|
||||
canvasId,
|
||||
)
|
||||
) {
|
||||
throw Error;
|
||||
}
|
||||
yield put(updateAndSaveLayout(updatedWidgets));
|
||||
yield put(updateAndSaveLayout(updatedWidgetsOnMove));
|
||||
log.debug("move computations took", performance.now() - start, "ms");
|
||||
} catch (error) {
|
||||
yield put({
|
||||
|
|
@ -141,9 +244,11 @@ function moveWidget(widgetMoveParams: WidgetMoveParams) {
|
|||
Toaster.clear();
|
||||
const {
|
||||
allWidgets,
|
||||
bottomRow,
|
||||
leftColumn,
|
||||
newParentId,
|
||||
parentId,
|
||||
rightColumn,
|
||||
topRow,
|
||||
widgetId,
|
||||
} = widgetMoveParams;
|
||||
|
|
@ -158,7 +263,12 @@ function moveWidget(widgetMoveParams: WidgetMoveParams) {
|
|||
children: [...(stateParent.children || [])],
|
||||
};
|
||||
// Update position of widget
|
||||
const updatedPosition = updateWidgetPosition(widget, leftColumn, topRow);
|
||||
const updatedPosition = {
|
||||
topRow,
|
||||
bottomRow,
|
||||
leftColumn,
|
||||
rightColumn,
|
||||
};
|
||||
widget = { ...widget, ...updatedPosition };
|
||||
|
||||
// Replace widget with update widget props
|
||||
|
|
@ -189,5 +299,11 @@ function moveWidget(widgetMoveParams: WidgetMoveParams) {
|
|||
}
|
||||
|
||||
export default function* draggingCanvasSagas() {
|
||||
yield all([takeLatest(ReduxActionTypes.WIDGETS_MOVE, moveWidgetsSaga)]);
|
||||
yield all([
|
||||
takeLatest(ReduxActionTypes.WIDGETS_MOVE, moveWidgetsSaga),
|
||||
takeLatest(
|
||||
ReduxActionTypes.WIDGETS_ADD_CHILD_AND_MOVE,
|
||||
addWidgetAndMoveWidgetsSaga,
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
|
|
|||
40
app/client/src/sagas/ReflowSagas.ts
Normal file
40
app/client/src/sagas/ReflowSagas.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import { setEnableReflowAction } from "actions/reflowActions";
|
||||
import {
|
||||
ReduxActionErrorTypes,
|
||||
ReduxActionTypes,
|
||||
} from "constants/ReduxActionConstants";
|
||||
import { User } from "constants/userConstants";
|
||||
import { isBoolean } from "lodash";
|
||||
import { all, put, select, takeLatest } from "redux-saga/effects";
|
||||
import { getCurrentUser } from "selectors/usersSelectors";
|
||||
import { getReflowBetaFlag, setReflowBetaFlag } from "utils/storage";
|
||||
|
||||
function* initReflowStates() {
|
||||
try {
|
||||
const user: User = yield select(getCurrentUser);
|
||||
const { email } = user;
|
||||
if (email) {
|
||||
const enableReflow: boolean = yield getReflowBetaFlag(email);
|
||||
const enableReflowHasBeenSet = isBoolean(enableReflow);
|
||||
const appsmithEmailRegex = /@appsmith.com/g;
|
||||
const canReflow = appsmithEmailRegex.test(email);
|
||||
const enableReflowState = enableReflowHasBeenSet
|
||||
? enableReflow
|
||||
: canReflow;
|
||||
yield put(setEnableReflowAction(enableReflowState));
|
||||
if (canReflow && !enableReflowHasBeenSet) {
|
||||
setReflowBetaFlag(email, true);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
yield put({
|
||||
type: ReduxActionErrorTypes.REFLOW_BETA_FLAGS_INIT_ERROR,
|
||||
payload: {
|
||||
error,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
export default function* reflowSagas() {
|
||||
yield all([takeLatest(ReduxActionTypes.INITIALIZE_EDITOR, initReflowStates)]);
|
||||
}
|
||||
|
|
@ -213,7 +213,9 @@ function* generateChildWidgets(
|
|||
return { widgetId: widget.widgetId, widgets };
|
||||
}
|
||||
|
||||
function* getUpdateDslAfterCreatingChild(addChildPayload: WidgetAddChild) {
|
||||
export function* getUpdateDslAfterCreatingChild(
|
||||
addChildPayload: WidgetAddChild,
|
||||
) {
|
||||
// NOTE: widgetId here is the parentId of the dropped widget ( we should rename it to avoid confusion )
|
||||
const { widgetId } = addChildPayload;
|
||||
// Get the current parent widget whose child will be the new widget.
|
||||
|
|
|
|||
|
|
@ -101,6 +101,10 @@ import { DataTree } from "entities/DataTree/dataTreeFactory";
|
|||
import { getCanvasSizeAfterWidgetMove } from "./DraggingCanvasSagas";
|
||||
import widgetAdditionSagas from "./WidgetAdditionSagas";
|
||||
import widgetDeletionSagas from "./WidgetDeletionSagas";
|
||||
import { getReflow } from "selectors/widgetReflowSelectors";
|
||||
import { widgetReflowState } from "reducers/uiReducers/reflowReducer";
|
||||
import { stopReflowAction } from "actions/reflowActions";
|
||||
import { collisionCheckPostReflow } from "utils/reflowHookUtils";
|
||||
|
||||
export function* resizeSaga(resizeAction: ReduxAction<WidgetResize>) {
|
||||
try {
|
||||
|
|
@ -111,6 +115,8 @@ export function* resizeSaga(resizeAction: ReduxAction<WidgetResize>) {
|
|||
leftColumn,
|
||||
parentId,
|
||||
rightColumn,
|
||||
snapColumnSpace,
|
||||
snapRowSpace,
|
||||
topRow,
|
||||
widgetId,
|
||||
} = resizeAction.payload;
|
||||
|
|
@ -121,21 +127,31 @@ export function* resizeSaga(resizeAction: ReduxAction<WidgetResize>) {
|
|||
const widgets = { ...stateWidgets };
|
||||
|
||||
widget = { ...widget, leftColumn, rightColumn, topRow, bottomRow };
|
||||
widgets[widgetId] = widget;
|
||||
const movedWidgets: {
|
||||
[widgetId: string]: FlattenedWidgetProps;
|
||||
} = yield call(
|
||||
reflowWidgets,
|
||||
widgets,
|
||||
widget,
|
||||
snapColumnSpace,
|
||||
snapRowSpace,
|
||||
);
|
||||
|
||||
const updatedCanvasBottomRow: number = yield call(
|
||||
getCanvasSizeAfterWidgetMove,
|
||||
parentId,
|
||||
bottomRow,
|
||||
);
|
||||
if (updatedCanvasBottomRow) {
|
||||
const canvasWidget = widgets[parentId];
|
||||
widgets[parentId] = {
|
||||
const canvasWidget = movedWidgets[parentId];
|
||||
movedWidgets[parentId] = {
|
||||
...canvasWidget,
|
||||
bottomRow: updatedCanvasBottomRow,
|
||||
};
|
||||
}
|
||||
log.debug("resize computations took", performance.now() - start, "ms");
|
||||
yield put(updateAndSaveLayout(widgets));
|
||||
yield put(stopReflowAction());
|
||||
yield put(updateAndSaveLayout(movedWidgets));
|
||||
} catch (error) {
|
||||
yield put({
|
||||
type: ReduxActionErrorTypes.WIDGET_OPERATION_ERROR,
|
||||
|
|
@ -147,6 +163,60 @@ export function* resizeSaga(resizeAction: ReduxAction<WidgetResize>) {
|
|||
}
|
||||
}
|
||||
|
||||
export function* reflowWidgets(
|
||||
widgets: {
|
||||
[widgetId: string]: FlattenedWidgetProps;
|
||||
},
|
||||
widget: FlattenedWidgetProps,
|
||||
snapColumnSpace: number,
|
||||
snapRowSpace: number,
|
||||
) {
|
||||
const reflowState: widgetReflowState = yield select(getReflow);
|
||||
|
||||
const currentWidgets: {
|
||||
[widgetId: string]: FlattenedWidgetProps;
|
||||
} = { ...widgets, [widget.widgetId]: { ...widget } };
|
||||
|
||||
if (!reflowState || !reflowState.isReflowing || !reflowState.reflowingWidgets)
|
||||
return currentWidgets;
|
||||
|
||||
const reflowingWidgets = reflowState.reflowingWidgets;
|
||||
|
||||
const reflowWidgetKeys = Object.keys(reflowingWidgets || {});
|
||||
|
||||
if (reflowWidgetKeys.length <= 0) return widgets;
|
||||
|
||||
for (const reflowedWidgetId of reflowWidgetKeys) {
|
||||
const reflowWidget = reflowingWidgets[reflowedWidgetId];
|
||||
const canvasWidget = { ...currentWidgets[reflowedWidgetId] };
|
||||
if (reflowWidget.X !== undefined && reflowWidget.width !== undefined) {
|
||||
const leftColumn =
|
||||
canvasWidget.leftColumn + reflowWidget.X / snapColumnSpace;
|
||||
const rightColumn = leftColumn + reflowWidget.width / snapColumnSpace;
|
||||
currentWidgets[reflowedWidgetId] = {
|
||||
...canvasWidget,
|
||||
leftColumn,
|
||||
rightColumn,
|
||||
};
|
||||
} else if (
|
||||
reflowWidget.Y !== undefined &&
|
||||
reflowWidget.height !== undefined
|
||||
) {
|
||||
const topRow = canvasWidget.topRow + reflowWidget.Y / snapRowSpace;
|
||||
const bottomRow = topRow + reflowWidget.height / snapRowSpace;
|
||||
currentWidgets[reflowedWidgetId] = { ...canvasWidget, topRow, bottomRow };
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
collisionCheckPostReflow(currentWidgets, reflowWidgetKeys, widget.parentId)
|
||||
) {
|
||||
return currentWidgets;
|
||||
}
|
||||
|
||||
return widgets;
|
||||
}
|
||||
|
||||
enum DynamicPathUpdateEffectEnum {
|
||||
ADD = "ADD",
|
||||
REMOVE = "REMOVE",
|
||||
|
|
|
|||
|
|
@ -43,6 +43,8 @@ import log from "loglevel";
|
|||
import * as sentry from "@sentry/react";
|
||||
import formEvaluationChangeListener from "./FormEvaluationSaga";
|
||||
import SuperUserSagas from "./SuperUserSagas";
|
||||
import reflowSagas from "./ReflowSagas";
|
||||
|
||||
const sagas = [
|
||||
initSagas,
|
||||
pageSagas,
|
||||
|
|
@ -85,6 +87,7 @@ const sagas = [
|
|||
draggingCanvasSagas,
|
||||
gitSyncSagas,
|
||||
SuperUserSagas,
|
||||
reflowSagas,
|
||||
];
|
||||
|
||||
export function* rootSaga(sagasToRun = sagas) {
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import {
|
|||
} from "reducers/entityReducers/canvasWidgetsReducer";
|
||||
import { PageListReduxState } from "reducers/entityReducers/pageListReducer";
|
||||
|
||||
import { OccupiedSpace } from "constants/CanvasEditorConstants";
|
||||
import { OccupiedSpace, WidgetSpace } from "constants/CanvasEditorConstants";
|
||||
import {
|
||||
getActions,
|
||||
getCanvasWidgets,
|
||||
|
|
@ -246,6 +246,24 @@ const getOccupiedSpacesForContainer = (
|
|||
});
|
||||
};
|
||||
|
||||
const getWidgetSpacesForContainer = (
|
||||
containerWidgetId: string,
|
||||
widgets: FlattenedWidgetProps[],
|
||||
): WidgetSpace[] => {
|
||||
return widgets.map((widget) => {
|
||||
const occupiedSpace: WidgetSpace = {
|
||||
id: widget.widgetId,
|
||||
parentId: containerWidgetId,
|
||||
left: widget.leftColumn,
|
||||
top: widget.topRow,
|
||||
bottom: widget.bottomRow,
|
||||
right: widget.rightColumn,
|
||||
type: widget.type,
|
||||
};
|
||||
return occupiedSpace;
|
||||
});
|
||||
};
|
||||
|
||||
export const getOccupiedSpaces = createSelector(
|
||||
getWidgets,
|
||||
(
|
||||
|
|
@ -312,6 +330,35 @@ export function getOccupiedSpacesSelectorForContainer(
|
|||
});
|
||||
}
|
||||
|
||||
// same as getOccupiedSpaces but gets only the container specific ocupied Spaces
|
||||
export function getWidgetSpacesSelectorForContainer(
|
||||
containerId: string | undefined,
|
||||
) {
|
||||
return createSelector(getWidgets, (widgets: CanvasWidgetsReduxState):
|
||||
| WidgetSpace[]
|
||||
| undefined => {
|
||||
if (containerId === null || containerId === undefined) return undefined;
|
||||
|
||||
const containerWidget: FlattenedWidgetProps = widgets[containerId];
|
||||
|
||||
if (!containerWidget || !containerWidget.children) return undefined;
|
||||
|
||||
// Get child widgets for the container
|
||||
const childWidgets = Object.keys(widgets).filter(
|
||||
(widgetId) =>
|
||||
containerWidget.children &&
|
||||
containerWidget.children.indexOf(widgetId) > -1 &&
|
||||
!widgets[widgetId].detachFromLayout,
|
||||
);
|
||||
|
||||
const occupiedSpaces = getWidgetSpacesForContainer(
|
||||
containerId,
|
||||
childWidgets.map((widgetId) => widgets[widgetId]),
|
||||
);
|
||||
return occupiedSpaces;
|
||||
});
|
||||
}
|
||||
|
||||
export const getActionById = createSelector(
|
||||
[getActions, (state: any, props: any) => props.match.params.apiId],
|
||||
(actions, id) => {
|
||||
|
|
|
|||
21
app/client/src/selectors/widgetReflowSelectors.tsx
Normal file
21
app/client/src/selectors/widgetReflowSelectors.tsx
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import { AppState } from "reducers";
|
||||
import { widgetReflowState } from "reducers/uiReducers/reflowReducer";
|
||||
import { createSelector } from "reselect";
|
||||
|
||||
export const getReflow = (state: AppState): widgetReflowState =>
|
||||
state.ui.widgetReflow;
|
||||
|
||||
export const isReflowEnabled = (state: any): boolean =>
|
||||
state.ui.widgetReflow.enableReflow;
|
||||
|
||||
export const getIsReflowing = (state: AppState): boolean =>
|
||||
state.ui.widgetReflow.isReflowing;
|
||||
|
||||
export const getReflowSelector = (widgetId: string) => {
|
||||
return createSelector(getReflow, (reflowState: widgetReflowState) => {
|
||||
if (reflowState?.reflowingWidgets) {
|
||||
return reflowState?.reflowingWidgets[widgetId];
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
};
|
||||
|
|
@ -198,7 +198,8 @@ export type EventName =
|
|||
| "GS_DEFAULT_CONFIGURATION_CHECKBOX_TOGGLED"
|
||||
| "GS_CONNECT_BUTTON_ON_GIT_SYNC_MODAL_CLICK"
|
||||
| "GS_IMPORT_VIA_GIT_CLICK"
|
||||
| "GS_CONTACT_SALES_CLICK";
|
||||
| "GS_CONTACT_SALES_CLICK"
|
||||
| "REFLOW_BETA_FLAG";
|
||||
|
||||
function getApplicationId(location: Location) {
|
||||
const pathSplit = location.pathname.split("/");
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ export type WidgetOperationParams = {
|
|||
payload: any;
|
||||
};
|
||||
|
||||
type Rect = {
|
||||
export type Rect = {
|
||||
top: number;
|
||||
left: number;
|
||||
right: number;
|
||||
|
|
@ -161,6 +161,10 @@ export const widgetOperationParams = (
|
|||
parentColumnSpace: number,
|
||||
parentRowSpace: number,
|
||||
parentWidgetId: string, // parentWidget
|
||||
widgetSizeUpdates: {
|
||||
width: number;
|
||||
height: number;
|
||||
},
|
||||
): WidgetOperationParams => {
|
||||
const [leftColumn, topRow] = getDropZoneOffsets(
|
||||
parentColumnSpace,
|
||||
|
|
@ -177,6 +181,12 @@ export const widgetOperationParams = (
|
|||
payload: {
|
||||
leftColumn,
|
||||
topRow,
|
||||
bottomRow: Math.round(
|
||||
topRow + widgetSizeUpdates.height / parentRowSpace,
|
||||
),
|
||||
rightColumn: Math.round(
|
||||
leftColumn + widgetSizeUpdates.width / parentColumnSpace,
|
||||
),
|
||||
parentId: widget.parentId,
|
||||
newParentId: parentWidgetId,
|
||||
},
|
||||
|
|
@ -204,23 +214,6 @@ export const widgetOperationParams = (
|
|||
};
|
||||
};
|
||||
|
||||
export const updateWidgetPosition = (
|
||||
widget: WidgetProps,
|
||||
leftColumn: number,
|
||||
topRow: number,
|
||||
) => {
|
||||
const newPositions = {
|
||||
leftColumn,
|
||||
topRow,
|
||||
rightColumn: leftColumn + (widget.rightColumn - widget.leftColumn),
|
||||
bottomRow: topRow + (widget.bottomRow - widget.topRow),
|
||||
};
|
||||
|
||||
return {
|
||||
...newPositions,
|
||||
};
|
||||
};
|
||||
|
||||
export const getCanvasSnapRows = (
|
||||
bottomRow: number,
|
||||
canExtend: boolean,
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import { getSelectedWidgets } from "selectors/ui";
|
|||
import { getOccupiedSpaces } from "selectors/editorSelectors";
|
||||
import { getTableFilterState } from "selectors/tableFilterSelectors";
|
||||
import { OccupiedSpace } from "constants/CanvasEditorConstants";
|
||||
import { getDragDetails, getWidgets } from "sagas/selectors";
|
||||
import { getDragDetails, getWidgetByID, getWidgets } from "sagas/selectors";
|
||||
import {
|
||||
getDropZoneOffsets,
|
||||
WidgetOperationParams,
|
||||
|
|
@ -18,7 +18,7 @@ import {
|
|||
} from "utils/WidgetPropsUtils";
|
||||
import { DropTargetContext } from "components/editorComponents/DropTargetComponent";
|
||||
import { XYCord } from "utils/hooks/useCanvasDragging";
|
||||
import { isEmpty } from "lodash";
|
||||
import { isEmpty, isEqual } from "lodash";
|
||||
import { CanvasDraggingArenaProps } from "pages/common/CanvasDraggingArena";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { ReduxActionTypes } from "constants/ReduxActionConstants";
|
||||
|
|
@ -26,6 +26,9 @@ 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;
|
||||
|
|
@ -53,6 +56,7 @@ export const useBlocksToBeDraggedOnCanvas = ({
|
|||
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
|
||||
|
|
@ -62,7 +66,21 @@ export const useBlocksToBeDraggedOnCanvas = ({
|
|||
// 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 = useSelector(getDragDetails);
|
||||
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,
|
||||
|
|
@ -76,7 +94,7 @@ export const useBlocksToBeDraggedOnCanvas = ({
|
|||
(state: AppState) => state.ui.widgetDragResize.isResizing,
|
||||
);
|
||||
const selectedWidgets = useSelector(getSelectedWidgets);
|
||||
const occupiedSpaces = useSelector(getOccupiedSpaces) || {};
|
||||
const occupiedSpaces = useSelector(getOccupiedSpaces, isEqual) || {};
|
||||
const isNewWidget = !!newWidget && !dragParent;
|
||||
const childrenOccupiedSpaces: OccupiedSpace[] =
|
||||
(dragParent && occupiedSpaces[dragParent]) || [];
|
||||
|
|
@ -160,19 +178,45 @@ export const useBlocksToBeDraggedOnCanvas = ({
|
|||
(each) => !selectedWidgets.includes(each.id),
|
||||
);
|
||||
const { updateDropTargetRows } = useContext(DropTargetContext);
|
||||
|
||||
const onDrop = (drawingBlocks: WidgetDraggingBlock[]) => {
|
||||
const cannotDrop = drawingBlocks.some((each) => {
|
||||
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 = drawingBlocks
|
||||
const draggedBlocksToUpdate = allUpdatedBlocks
|
||||
.sort(
|
||||
(each1, each2) =>
|
||||
each1.top + each1.height - (each2.top + each2.height),
|
||||
)
|
||||
.map((each) => {
|
||||
const widget = newWidget ? newWidget : allWidgets[each.widgetId];
|
||||
const widget =
|
||||
newWidget && !reflowedIds.includes(each.widgetId)
|
||||
? newWidget
|
||||
: allWidgets[each.widgetId];
|
||||
const updateWidgetParams = widgetOperationParams(
|
||||
widget,
|
||||
{ x: each.left, y: each.top },
|
||||
|
|
@ -180,6 +224,7 @@ export const useBlocksToBeDraggedOnCanvas = ({
|
|||
snapColumnSpace,
|
||||
snapRowSpace,
|
||||
widget.detachFromLayout ? MAIN_CONTAINER_WIDGET_ID : widgetId,
|
||||
{ width: each.width, height: each.height },
|
||||
);
|
||||
|
||||
return {
|
||||
|
|
@ -195,7 +240,15 @@ export const useBlocksToBeDraggedOnCanvas = ({
|
|||
draggedBlocksToUpdate: WidgetDraggingUpdateParams[],
|
||||
) => {
|
||||
if (isNewWidget) {
|
||||
addNewWidget(draggedBlocksToUpdate[0]);
|
||||
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);
|
||||
}
|
||||
|
|
@ -213,14 +266,29 @@ export const useBlocksToBeDraggedOnCanvas = ({
|
|||
});
|
||||
};
|
||||
|
||||
const addNewWidget = (newWidget: WidgetDraggingUpdateParams) => {
|
||||
const addNewWidget = (
|
||||
newWidget: WidgetDraggingUpdateParams,
|
||||
movedWidgets: WidgetDraggingUpdateParams[],
|
||||
) => {
|
||||
const { updateWidgetParams } = newWidget;
|
||||
updateWidget &&
|
||||
updateWidget(
|
||||
updateWidgetParams.operation,
|
||||
updateWidgetParams.widgetId,
|
||||
updateWidgetParams.payload,
|
||||
);
|
||||
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({
|
||||
|
|
@ -238,7 +306,10 @@ export const useBlocksToBeDraggedOnCanvas = ({
|
|||
didDrop: true,
|
||||
});
|
||||
};
|
||||
const updateRows = (drawingBlocks: WidgetDraggingBlock[], rows: number) => {
|
||||
const updateRelativeRows = (
|
||||
drawingBlocks: WidgetDraggingBlock[],
|
||||
rows: number,
|
||||
) => {
|
||||
if (drawingBlocks.length) {
|
||||
const sortedByTopBlocks = drawingBlocks.sort(
|
||||
(each1, each2) => each2.top + each2.height - (each1.top + each1.height),
|
||||
|
|
@ -253,9 +324,12 @@ export const useBlocksToBeDraggedOnCanvas = ({
|
|||
} as XYCord,
|
||||
{ x: 0, y: 0 },
|
||||
);
|
||||
if (top > rows - GridDefaults.CANVAS_EXTENSION_OFFSET) {
|
||||
return updateDropTargetRows && updateDropTargetRows(widgetId, top);
|
||||
}
|
||||
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);
|
||||
|
|
@ -312,11 +386,17 @@ export const useBlocksToBeDraggedOnCanvas = ({
|
|||
isNewWidget,
|
||||
isNewWidgetInitialTargetCanvas,
|
||||
isResizing,
|
||||
lastDraggedCanvas,
|
||||
occSpaces,
|
||||
onDrop,
|
||||
parentDiff,
|
||||
relativeStartPoints,
|
||||
rowRef,
|
||||
updateRows,
|
||||
stopReflowing,
|
||||
updateBottomRow,
|
||||
updateRelativeRows,
|
||||
widgetOccupiedSpace: childrenOccupiedSpaces.filter(
|
||||
(each) => each.id === dragCenter?.widgetId,
|
||||
)[0],
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,25 +2,28 @@ import {
|
|||
CONTAINER_GRID_PADDING,
|
||||
GridDefaults,
|
||||
} from "constants/WidgetConstants";
|
||||
import { debounce, throttle } from "lodash";
|
||||
import { debounce, isEmpty, throttle } from "lodash";
|
||||
import { CanvasDraggingArenaProps } from "pages/common/CanvasDraggingArena";
|
||||
import { useEffect } from "react";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { ReflowDirection, ReflowedSpaceMap } from "reflow/reflowTypes";
|
||||
import { useReflow, ReflowInterface } from "utils/hooks/useReflow";
|
||||
import { useSelector } from "react-redux";
|
||||
import { getZoomLevel } from "selectors/editorSelectors";
|
||||
import { getNearestParentCanvas } from "utils/generators";
|
||||
import { noCollision } from "utils/WidgetPropsUtils";
|
||||
import { getDropZoneOffsets, noCollision } from "utils/WidgetPropsUtils";
|
||||
import { useWidgetDragResize } from "./dragResizeHooks";
|
||||
import {
|
||||
useBlocksToBeDraggedOnCanvas,
|
||||
WidgetDraggingBlock,
|
||||
} from "./useBlocksToBeDraggedOnCanvas";
|
||||
import { useCanvasDragToScroll } from "./useCanvasDragToScroll";
|
||||
import { OccupiedSpace } from "constants/CanvasEditorConstants";
|
||||
import { isReflowEnabled } from "selectors/widgetReflowSelectors";
|
||||
|
||||
export interface XYCord {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
export const useCanvasDragging = (
|
||||
canvasRef: React.RefObject<HTMLDivElement>,
|
||||
canvasDrawRef: React.RefObject<HTMLCanvasElement>,
|
||||
|
|
@ -35,8 +38,9 @@ export const useCanvasDragging = (
|
|||
}: CanvasDraggingArenaProps,
|
||||
) => {
|
||||
const canvasZoomLevel = useSelector(getZoomLevel);
|
||||
const currentDirection = useRef<ReflowDirection>(ReflowDirection.UNSET);
|
||||
const { devicePixelRatio: scale = 1 } = window;
|
||||
|
||||
const reflowEnabled = useSelector(isReflowEnabled);
|
||||
const {
|
||||
blocksToDraw,
|
||||
defaultHandlePositions,
|
||||
|
|
@ -47,12 +51,16 @@ export const useCanvasDragging = (
|
|||
isNewWidget,
|
||||
isNewWidgetInitialTargetCanvas,
|
||||
isResizing,
|
||||
lastDraggedCanvas,
|
||||
occSpaces,
|
||||
onDrop,
|
||||
parentDiff,
|
||||
relativeStartPoints,
|
||||
rowRef,
|
||||
updateRows,
|
||||
stopReflowing,
|
||||
updateBottomRow,
|
||||
updateRelativeRows,
|
||||
widgetOccupiedSpace,
|
||||
} = useBlocksToBeDraggedOnCanvas({
|
||||
canExtend,
|
||||
noPad,
|
||||
|
|
@ -61,6 +69,19 @@ export const useCanvasDragging = (
|
|||
snapRowSpace,
|
||||
widgetId,
|
||||
});
|
||||
const gridProps = {
|
||||
parentColumnSpace: snapColumnSpace,
|
||||
parentRowSpace: snapRowSpace,
|
||||
maxGridColumns: GridDefaults.DEFAULT_GRID_COLUMNS,
|
||||
paddingOffset: 0,
|
||||
};
|
||||
|
||||
const reflow = useRef<ReflowInterface>();
|
||||
reflow.current = useReflow(
|
||||
widgetOccupiedSpace ? widgetOccupiedSpace.id : "",
|
||||
widgetId || "",
|
||||
gridProps,
|
||||
);
|
||||
|
||||
const {
|
||||
setDraggingCanvas,
|
||||
|
|
@ -128,6 +149,32 @@ export const useCanvasDragging = (
|
|||
return 0;
|
||||
};
|
||||
|
||||
const mouseAttributesRef = useRef<{
|
||||
prevEvent: any;
|
||||
currentEvent: any;
|
||||
prevSpeed: number;
|
||||
prevAcceleration: number;
|
||||
maxPositiveAcc: number;
|
||||
maxNegativeAcc: number;
|
||||
maxSpeed: number;
|
||||
lastMousePositionOutsideCanvas: {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
}>({
|
||||
prevSpeed: 0,
|
||||
prevAcceleration: 0,
|
||||
maxPositiveAcc: 0,
|
||||
maxNegativeAcc: 0,
|
||||
maxSpeed: 0,
|
||||
prevEvent: null,
|
||||
currentEvent: null,
|
||||
lastMousePositionOutsideCanvas: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
});
|
||||
|
||||
const canScroll = useCanvasDragToScroll(
|
||||
canvasRef,
|
||||
isCurrentDraggedCanvas,
|
||||
|
|
@ -135,6 +182,57 @@ export const useCanvasDragging = (
|
|||
snapRows,
|
||||
canExtend,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const speedCalculationInterval = setInterval(function() {
|
||||
const {
|
||||
currentEvent,
|
||||
maxNegativeAcc,
|
||||
maxPositiveAcc,
|
||||
maxSpeed,
|
||||
prevEvent,
|
||||
prevSpeed,
|
||||
} = mouseAttributesRef.current;
|
||||
if (prevEvent && currentEvent) {
|
||||
const movementX = Math.abs(currentEvent.screenX - prevEvent.screenX);
|
||||
const movementY = Math.abs(currentEvent.screenY - prevEvent.screenY);
|
||||
const movement = Math.sqrt(
|
||||
movementX * movementX + movementY * movementY,
|
||||
);
|
||||
|
||||
const speed = 10 * movement; //current speed
|
||||
|
||||
const acceleration = 10 * (speed - prevSpeed);
|
||||
mouseAttributesRef.current.prevAcceleration = acceleration;
|
||||
mouseAttributesRef.current.prevSpeed = speed;
|
||||
if (speed > maxSpeed) {
|
||||
mouseAttributesRef.current.maxSpeed = speed;
|
||||
}
|
||||
if (acceleration > 0 && acceleration > maxPositiveAcc) {
|
||||
mouseAttributesRef.current.maxPositiveAcc = acceleration;
|
||||
} else if (acceleration < 0 && acceleration < maxNegativeAcc) {
|
||||
mouseAttributesRef.current.maxNegativeAcc = acceleration;
|
||||
}
|
||||
}
|
||||
mouseAttributesRef.current.prevEvent = currentEvent;
|
||||
}, 100);
|
||||
const stopSpeedCalculation = () => {
|
||||
clearInterval(speedCalculationInterval);
|
||||
};
|
||||
const registerMouseMoveEvent = (e: any) => {
|
||||
mouseAttributesRef.current.currentEvent = e;
|
||||
mouseAttributesRef.current.lastMousePositionOutsideCanvas = {
|
||||
x: e.clientX,
|
||||
y: e.clientY,
|
||||
};
|
||||
};
|
||||
window.addEventListener("mousemove", registerMouseMoveEvent);
|
||||
return () => {
|
||||
stopSpeedCalculation();
|
||||
window.removeEventListener("mousemove", registerMouseMoveEvent);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
canvasRef.current &&
|
||||
|
|
@ -142,6 +240,8 @@ export const useCanvasDragging = (
|
|||
isDragging &&
|
||||
blocksToDraw.length > 0
|
||||
) {
|
||||
// doing throttling coz reflow moves are also throttled and resetCanvas can be called multiple times
|
||||
const throttledStopReflowing = throttle(stopReflowing, 50);
|
||||
const scrollParent: Element | null = getNearestParentCanvas(
|
||||
canvasRef.current,
|
||||
);
|
||||
|
|
@ -150,7 +250,28 @@ export const useCanvasDragging = (
|
|||
let currentRectanglesToDraw: WidgetDraggingBlock[] = [];
|
||||
const scrollObj: any = {};
|
||||
|
||||
let currentReflowParams: {
|
||||
canVerticalMove: boolean;
|
||||
canHorizontalMove: boolean;
|
||||
bottomMostRow: number;
|
||||
movementMap: ReflowedSpaceMap;
|
||||
} = {
|
||||
canVerticalMove: false,
|
||||
canHorizontalMove: false,
|
||||
bottomMostRow: 0,
|
||||
movementMap: {},
|
||||
};
|
||||
let lastMousePosition = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
};
|
||||
let lastSnappedPosition = {
|
||||
leftColumn: 0,
|
||||
topRow: 0,
|
||||
};
|
||||
|
||||
const resetCanvasState = () => {
|
||||
throttledStopReflowing();
|
||||
if (canvasDrawRef.current && canvasRef.current) {
|
||||
const canvasCtx: any = canvasDrawRef.current.getContext("2d");
|
||||
canvasCtx.clearRect(
|
||||
|
|
@ -167,7 +288,43 @@ export const useCanvasDragging = (
|
|||
const startPoints = defaultHandlePositions;
|
||||
const onMouseUp = () => {
|
||||
if (isDragging && canvasIsDragging) {
|
||||
onDrop(currentRectanglesToDraw);
|
||||
const { movementMap: reflowingWidgets } = currentReflowParams;
|
||||
const reflowedPositionsUpdatesWidgets: OccupiedSpace[] = occSpaces
|
||||
.filter((each) => !!reflowingWidgets[each.id])
|
||||
.map((each) => {
|
||||
const reflowedWidget = reflowingWidgets[each.id];
|
||||
if (
|
||||
reflowedWidget.X !== undefined &&
|
||||
(Math.abs(reflowedWidget.X) || reflowedWidget.width)
|
||||
) {
|
||||
const movement = reflowedWidget.X / snapColumnSpace;
|
||||
const newWidth = reflowedWidget.width
|
||||
? reflowedWidget.width / snapColumnSpace
|
||||
: each.right - each.left;
|
||||
each = {
|
||||
...each,
|
||||
left: each.left + movement,
|
||||
right: each.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
|
||||
: each.bottom - each.top;
|
||||
each = {
|
||||
...each,
|
||||
top: each.top + movement,
|
||||
bottom: each.top + movement + newHeight,
|
||||
};
|
||||
}
|
||||
return each;
|
||||
});
|
||||
|
||||
onDrop(currentRectanglesToDraw, reflowedPositionsUpdatesWidgets);
|
||||
}
|
||||
startPoints.top = defaultHandlePositions.top;
|
||||
startPoints.left = defaultHandlePositions.left;
|
||||
|
|
@ -185,7 +342,7 @@ export const useCanvasDragging = (
|
|||
}
|
||||
};
|
||||
|
||||
const onFirstMoveOnCanvas = (e: any) => {
|
||||
const onFirstMoveOnCanvas = (e: any, over = false) => {
|
||||
if (
|
||||
!isResizing &&
|
||||
isDragging &&
|
||||
|
|
@ -204,10 +361,158 @@ export const useCanvasDragging = (
|
|||
}
|
||||
canvasIsDragging = true;
|
||||
canvasRef.current.style.zIndex = "2";
|
||||
onMouseMove(e);
|
||||
if (over) {
|
||||
lastMousePosition = {
|
||||
...mouseAttributesRef.current.lastMousePositionOutsideCanvas,
|
||||
};
|
||||
} else {
|
||||
lastMousePosition = {
|
||||
x: e.clientX,
|
||||
y: e.clientY,
|
||||
};
|
||||
}
|
||||
|
||||
onMouseMove(e, over);
|
||||
}
|
||||
};
|
||||
const onMouseMove = (e: any) => {
|
||||
|
||||
const canReflowForCurrentMouseMove = () => {
|
||||
const {
|
||||
maxNegativeAcc,
|
||||
maxPositiveAcc,
|
||||
maxSpeed,
|
||||
prevAcceleration,
|
||||
prevSpeed,
|
||||
} = mouseAttributesRef.current;
|
||||
const limit = Math.abs(
|
||||
prevAcceleration < 0 ? maxNegativeAcc : maxPositiveAcc,
|
||||
);
|
||||
const acceleration = Math.abs(prevAcceleration);
|
||||
return acceleration < limit / 5 || prevSpeed < maxSpeed / 5;
|
||||
};
|
||||
const getMouseMoveDirection = (event: any) => {
|
||||
if (lastMousePosition) {
|
||||
const deltaX = lastMousePosition.x - event.clientX,
|
||||
deltaY = lastMousePosition.y - event.clientY;
|
||||
lastMousePosition = {
|
||||
x: event.clientX,
|
||||
y: event.clientY,
|
||||
};
|
||||
if (
|
||||
deltaX === 0 &&
|
||||
["TOP", "BOTTOM"].includes(currentDirection.current)
|
||||
) {
|
||||
return currentDirection.current;
|
||||
}
|
||||
if (Math.abs(deltaY) > Math.abs(deltaX) && deltaY > 0) {
|
||||
return ReflowDirection.TOP;
|
||||
} else if (Math.abs(deltaY) > Math.abs(deltaX) && deltaY < 0) {
|
||||
return ReflowDirection.BOTTOM;
|
||||
}
|
||||
if (
|
||||
deltaY === 0 &&
|
||||
["LEFT", "RIGHT"].includes(currentDirection.current)
|
||||
) {
|
||||
return currentDirection.current;
|
||||
}
|
||||
if (Math.abs(deltaX) > Math.abs(deltaY) && deltaX > 0) {
|
||||
return ReflowDirection.LEFT;
|
||||
} else if (Math.abs(deltaX) > Math.abs(deltaY) && deltaX < 0) {
|
||||
return ReflowDirection.RIGHT;
|
||||
}
|
||||
}
|
||||
return currentDirection.current;
|
||||
};
|
||||
const triggerReflow = (e: any, firstMove: boolean) => {
|
||||
const canReflowBasedOnMouseSpeed = canReflowForCurrentMouseMove();
|
||||
const isReflowing = !isEmpty(currentReflowParams.movementMap);
|
||||
const canReflow =
|
||||
reflowEnabled &&
|
||||
currentRectanglesToDraw.length === 1 &&
|
||||
!currentRectanglesToDraw[0].detachFromLayout;
|
||||
const currentBlock = currentRectanglesToDraw[0];
|
||||
const [leftColumn, topRow] = getDropZoneOffsets(
|
||||
snapColumnSpace,
|
||||
snapRowSpace,
|
||||
{
|
||||
x: currentBlock.left,
|
||||
y: currentBlock.top,
|
||||
},
|
||||
{
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
);
|
||||
const needsReflow = !(
|
||||
lastSnappedPosition.leftColumn === leftColumn &&
|
||||
lastSnappedPosition.topRow === topRow
|
||||
);
|
||||
lastSnappedPosition = {
|
||||
leftColumn,
|
||||
topRow,
|
||||
};
|
||||
if (canReflow && reflow.current) {
|
||||
if (needsReflow) {
|
||||
const resizedPositions = {
|
||||
left: leftColumn,
|
||||
top: topRow,
|
||||
right: leftColumn + currentBlock.width / snapColumnSpace,
|
||||
bottom: topRow + currentBlock.height / snapRowSpace,
|
||||
id: currentBlock.widgetId,
|
||||
};
|
||||
const originalPositions = widgetOccupiedSpace
|
||||
? { ...widgetOccupiedSpace }
|
||||
: {
|
||||
left: 0,
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
id: currentBlock.widgetId,
|
||||
};
|
||||
currentDirection.current = getMouseMoveDirection(e);
|
||||
const immediateExitContainer = lastDraggedCanvas.current;
|
||||
if (lastDraggedCanvas.current) {
|
||||
lastDraggedCanvas.current = undefined;
|
||||
}
|
||||
currentReflowParams = reflow.current(
|
||||
resizedPositions,
|
||||
originalPositions,
|
||||
currentDirection.current,
|
||||
false,
|
||||
!canReflowBasedOnMouseSpeed,
|
||||
firstMove,
|
||||
immediateExitContainer,
|
||||
);
|
||||
}
|
||||
|
||||
if (isReflowing) {
|
||||
const block = currentRectanglesToDraw[0];
|
||||
const isNotInParentBoundaries = noCollision(
|
||||
{ x: block.left, y: block.top },
|
||||
snapColumnSpace,
|
||||
snapRowSpace,
|
||||
{ x: 0, y: 0 },
|
||||
block.columnWidth,
|
||||
block.rowHeight,
|
||||
block.widgetId,
|
||||
[],
|
||||
rowRef.current,
|
||||
GridDefaults.DEFAULT_GRID_COLUMNS,
|
||||
block.detachFromLayout,
|
||||
);
|
||||
const newRows = updateBottomRow(
|
||||
currentReflowParams.bottomMostRow,
|
||||
rowRef.current,
|
||||
);
|
||||
rowRef.current = newRows ? newRows : rowRef.current;
|
||||
currentRectanglesToDraw[0].isNotColliding =
|
||||
isNotInParentBoundaries &&
|
||||
currentReflowParams.canHorizontalMove &&
|
||||
currentReflowParams.canVerticalMove;
|
||||
}
|
||||
}
|
||||
};
|
||||
const onMouseMove = (e: any, firstMove = false) => {
|
||||
if (isDragging && canvasIsDragging && canvasRef.current) {
|
||||
const delta = {
|
||||
left: e.offsetX - startPoints.left - parentDiff.left,
|
||||
|
|
@ -219,7 +524,7 @@ export const useCanvasDragging = (
|
|||
left: each.left + delta.left,
|
||||
top: each.top + delta.top,
|
||||
}));
|
||||
const newRows = updateRows(drawingBlocks, rowRef.current);
|
||||
const newRows = updateRelativeRows(drawingBlocks, rowRef.current);
|
||||
const rowDelta = newRows ? newRows - rowRef.current : 0;
|
||||
rowRef.current = newRows ? newRows : rowRef.current;
|
||||
currentRectanglesToDraw = drawingBlocks.map((each) => ({
|
||||
|
|
@ -245,6 +550,7 @@ export const useCanvasDragging = (
|
|||
canScroll.current = false;
|
||||
renderNewRows(delta);
|
||||
} else if (!isUpdatingRows) {
|
||||
triggerReflow(e, firstMove);
|
||||
renderBlocks();
|
||||
}
|
||||
scrollObj.lastMouseMoveEvent = {
|
||||
|
|
@ -411,17 +717,19 @@ export const useCanvasDragging = (
|
|||
});
|
||||
}
|
||||
};
|
||||
const captureMousePosition = (e: any) => {
|
||||
if (isDragging && !canvasIsDragging) {
|
||||
currentDirection.current = getMouseMoveDirection(e);
|
||||
}
|
||||
};
|
||||
const onMouseOver = (e: any) => onFirstMoveOnCanvas(e, true);
|
||||
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("mouseover", onMouseOver, false);
|
||||
canvasRef.current?.addEventListener(
|
||||
"mouseout",
|
||||
resetCanvasState,
|
||||
|
|
@ -434,6 +742,7 @@ export const useCanvasDragging = (
|
|||
);
|
||||
document.body.addEventListener("mouseup", onMouseUp, false);
|
||||
window.addEventListener("mouseup", onMouseUp, false);
|
||||
window.addEventListener("mousemove", captureMousePosition);
|
||||
};
|
||||
const startDragging = () => {
|
||||
if (canvasRef.current && canvasDrawRef.current && scrollParent) {
|
||||
|
|
@ -460,10 +769,7 @@ export const useCanvasDragging = (
|
|||
canvasRef.current?.removeEventListener("mouseup", onMouseUp);
|
||||
scrollParent?.removeEventListener("scroll", updateCanvasStyles);
|
||||
scrollParent?.removeEventListener("scroll", onScroll);
|
||||
canvasRef.current?.removeEventListener(
|
||||
"mouseover",
|
||||
onFirstMoveOnCanvas,
|
||||
);
|
||||
canvasRef.current?.removeEventListener("mouseover", onMouseOver);
|
||||
canvasRef.current?.removeEventListener("mouseout", resetCanvasState);
|
||||
canvasRef.current?.removeEventListener(
|
||||
"mouseleave",
|
||||
|
|
@ -471,12 +777,20 @@ export const useCanvasDragging = (
|
|||
);
|
||||
document.body.removeEventListener("mouseup", onMouseUp);
|
||||
window.removeEventListener("mouseup", onMouseUp);
|
||||
window.removeEventListener("mousemove", captureMousePosition);
|
||||
};
|
||||
} else {
|
||||
resetCanvasState();
|
||||
}
|
||||
}
|
||||
}, [isDragging, isResizing, blocksToDraw, snapRows, canExtend]);
|
||||
}, [
|
||||
isDragging,
|
||||
isResizing,
|
||||
blocksToDraw,
|
||||
snapRows,
|
||||
canExtend,
|
||||
reflowEnabled,
|
||||
]);
|
||||
return {
|
||||
showCanvas: isDragging && !isResizing,
|
||||
};
|
||||
|
|
|
|||
138
app/client/src/utils/hooks/useReflow.ts
Normal file
138
app/client/src/utils/hooks/useReflow.ts
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
import { reflowMoveAction, stopReflowAction } from "actions/reflowActions";
|
||||
import { OccupiedSpace, WidgetSpace } from "constants/CanvasEditorConstants";
|
||||
import { isEmpty, throttle } from "lodash";
|
||||
import { useRef } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { getWidgetSpacesSelectorForContainer } from "selectors/editorSelectors";
|
||||
import { reflow } from "reflow";
|
||||
import {
|
||||
CollidingSpace,
|
||||
GridProps,
|
||||
ReflowDirection,
|
||||
ReflowedSpaceMap,
|
||||
} from "reflow/reflowTypes";
|
||||
import { getLimitedMovementMap } from "reflow/reflowUtils";
|
||||
import { getBottomRowAfterReflow } from "utils/reflowHookUtils";
|
||||
import { checkIsDropTarget } from "components/designSystems/appsmith/PositionedContainer";
|
||||
|
||||
type WidgetCollidingSpace = CollidingSpace & {
|
||||
type: string;
|
||||
};
|
||||
|
||||
type WidgetCollidingSpaceMap = {
|
||||
[key: string]: WidgetCollidingSpace;
|
||||
};
|
||||
|
||||
export interface ReflowInterface {
|
||||
(
|
||||
newPositions: OccupiedSpace,
|
||||
OGPositions: OccupiedSpace,
|
||||
direction: ReflowDirection,
|
||||
stopMoveAfterLimit?: boolean,
|
||||
shouldSkipContainerReflow?: boolean,
|
||||
forceDirection?: boolean,
|
||||
immediateExitContainer?: string,
|
||||
): {
|
||||
canHorizontalMove: boolean;
|
||||
canVerticalMove: boolean;
|
||||
movementMap: ReflowedSpaceMap;
|
||||
bottomMostRow: number;
|
||||
};
|
||||
}
|
||||
|
||||
export const useReflow = (
|
||||
widgetId: string,
|
||||
parentId: string,
|
||||
gridProps: GridProps,
|
||||
): ReflowInterface => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const throttledDispatch = throttle(dispatch, 50);
|
||||
|
||||
const isReflowing = useRef<boolean>(false);
|
||||
|
||||
const reflowSpacesSelector = getWidgetSpacesSelectorForContainer(parentId);
|
||||
const widgetSpaces: WidgetSpace[] = useSelector(reflowSpacesSelector) || [];
|
||||
|
||||
const originalSpacePosition = widgetSpaces?.find(
|
||||
(space) => space.id === widgetId,
|
||||
);
|
||||
|
||||
const prevPositions = useRef<OccupiedSpace | undefined>(
|
||||
originalSpacePosition,
|
||||
);
|
||||
const prevCollidingSpaces = useRef<WidgetCollidingSpaceMap>();
|
||||
const prevMovementMap = useRef<ReflowedSpaceMap>({});
|
||||
// will become a state if we decide that resize should be a "toggle on-demand" feature
|
||||
const shouldResize = true;
|
||||
return function reflowSpaces(
|
||||
newPositions: OccupiedSpace,
|
||||
OGPositions: OccupiedSpace,
|
||||
direction: ReflowDirection,
|
||||
stopMoveAfterLimit = false,
|
||||
shouldSkipContainerReflow = false,
|
||||
forceDirection = false,
|
||||
immediateExitContainer?: string,
|
||||
) {
|
||||
const { collidingSpaceMap, movementLimit, movementMap } = reflow(
|
||||
newPositions,
|
||||
OGPositions,
|
||||
widgetSpaces,
|
||||
direction,
|
||||
gridProps,
|
||||
forceDirection,
|
||||
shouldResize,
|
||||
immediateExitContainer,
|
||||
prevPositions.current,
|
||||
prevCollidingSpaces.current,
|
||||
);
|
||||
|
||||
prevPositions.current = newPositions;
|
||||
prevCollidingSpaces.current = collidingSpaceMap as WidgetCollidingSpaceMap;
|
||||
|
||||
let correctedMovementMap = movementMap || {};
|
||||
|
||||
if (stopMoveAfterLimit)
|
||||
correctedMovementMap = getLimitedMovementMap(
|
||||
movementMap,
|
||||
prevMovementMap.current,
|
||||
movementLimit,
|
||||
);
|
||||
|
||||
if (shouldSkipContainerReflow) {
|
||||
const collidingSpaces = Object.values(
|
||||
(collidingSpaceMap as WidgetCollidingSpaceMap) || {},
|
||||
);
|
||||
for (const collidingSpace of collidingSpaces) {
|
||||
if (checkIsDropTarget(collidingSpace.type)) {
|
||||
correctedMovementMap = {};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
prevMovementMap.current = correctedMovementMap;
|
||||
|
||||
if (!isEmpty(correctedMovementMap)) {
|
||||
isReflowing.current = true;
|
||||
if (forceDirection) dispatch(reflowMoveAction(correctedMovementMap));
|
||||
else throttledDispatch(reflowMoveAction(correctedMovementMap));
|
||||
} else if (isReflowing.current) {
|
||||
isReflowing.current = false;
|
||||
throttledDispatch.cancel();
|
||||
dispatch(stopReflowAction());
|
||||
}
|
||||
|
||||
const bottomMostRow = getBottomRowAfterReflow(
|
||||
movementMap,
|
||||
newPositions.bottom,
|
||||
widgetSpaces,
|
||||
gridProps,
|
||||
);
|
||||
|
||||
return {
|
||||
...movementLimit,
|
||||
movementMap: correctedMovementMap,
|
||||
bottomMostRow,
|
||||
};
|
||||
};
|
||||
};
|
||||
90
app/client/src/utils/reflowHookUtils.ts
Normal file
90
app/client/src/utils/reflowHookUtils.ts
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
import { OccupiedSpace } from "constants/CanvasEditorConstants";
|
||||
import { GridDefaults } from "constants/WidgetConstants";
|
||||
import { FlattenedWidgetProps } from "reducers/entityReducers/canvasWidgetsReducer";
|
||||
import { GridProps, ReflowedSpace, ReflowedSpaceMap } from "reflow/reflowTypes";
|
||||
|
||||
export function collisionCheckPostReflow(
|
||||
widgets: {
|
||||
[widgetId: string]: FlattenedWidgetProps;
|
||||
},
|
||||
reflowWidgetKeys: string[],
|
||||
parentId?: string,
|
||||
) {
|
||||
const widgetKeys = Object.keys(widgets).filter((widgetId) => {
|
||||
if (!widgets[widgetId].parentId) return false;
|
||||
|
||||
if (widgets[widgetId].parentId !== parentId) return false;
|
||||
|
||||
if (widgets[widgetId].type === "MODAL_WIDGET") return false;
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
//boundary Check
|
||||
for (const reflowedKey of reflowWidgetKeys) {
|
||||
if (isOutOfCanvas(widgets[reflowedKey])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
//overlapping check
|
||||
for (const reflowedKey of reflowWidgetKeys) {
|
||||
for (const widgetId of widgetKeys) {
|
||||
if (areIntersecting(widgets[reflowedKey], widgets[widgetId])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function areIntersecting(r1: FlattenedWidgetProps, r2: FlattenedWidgetProps) {
|
||||
if (r1.widgetId === r2.widgetId) return false;
|
||||
|
||||
return !(
|
||||
r2.leftColumn >= r1.rightColumn ||
|
||||
r2.rightColumn <= r1.leftColumn ||
|
||||
r2.topRow >= r1.bottomRow ||
|
||||
r2.bottomRow <= r1.topRow
|
||||
);
|
||||
}
|
||||
|
||||
function isOutOfCanvas(widget: FlattenedWidgetProps) {
|
||||
return (
|
||||
widget.leftColumn < 0 ||
|
||||
widget.topRow < 0 ||
|
||||
widget.rightColumn > GridDefaults.DEFAULT_GRID_COLUMNS
|
||||
);
|
||||
}
|
||||
|
||||
export function getBottomRowAfterReflow(
|
||||
movementMap: ReflowedSpaceMap | undefined,
|
||||
widgetBottom: number,
|
||||
occupiedSpaces: OccupiedSpace[],
|
||||
gridProps: GridProps,
|
||||
) {
|
||||
const reflowedWidgets: [string, ReflowedSpace][] = Object.entries(
|
||||
movementMap || {},
|
||||
);
|
||||
const bottomReflowedWidgets = reflowedWidgets.filter((each) => !!each[1].Y);
|
||||
|
||||
const reflowedWidgetsBottomMostRow = bottomReflowedWidgets.reduce(
|
||||
(bottomMostRow, each) => {
|
||||
const [id, reflowedParams] = each;
|
||||
const widget = occupiedSpaces.find((eachSpace) => eachSpace.id === id);
|
||||
if (widget) {
|
||||
const bottomMovement =
|
||||
(reflowedParams.Y || 0) / gridProps.parentRowSpace;
|
||||
const bottomRow = widget.bottom + bottomMovement;
|
||||
if (bottomRow > bottomMostRow) {
|
||||
return bottomRow;
|
||||
}
|
||||
}
|
||||
return bottomMostRow;
|
||||
},
|
||||
0,
|
||||
);
|
||||
|
||||
return Math.max(reflowedWidgetsBottomMostRow, widgetBottom);
|
||||
}
|
||||
|
|
@ -18,6 +18,8 @@ const STORAGE_KEYS: { [id: string]: string } = {
|
|||
FIRST_TIME_USER_ONBOARDING_INTRO_MODAL_VISIBILITY:
|
||||
"FIRST_TIME_USER_ONBOARDING_INTRO_MODAL_VISIBILITY",
|
||||
HIDE_CONCURRENT_EDITOR_WARNING_TOAST: "HIDE_CONCURRENT_EDITOR_WARNING_TOAST",
|
||||
REFLOW_BETA_FLAG: "REFLOW_BETA_FLAG",
|
||||
REFLOW_ONBOARDING_FLAG: "REFLOW_ONBOARDING_FLAG",
|
||||
};
|
||||
|
||||
const store = localforage.createInstance({
|
||||
|
|
@ -54,6 +56,28 @@ export const saveCopiedWidgets = async (widgetJSON: string) => {
|
|||
}
|
||||
};
|
||||
|
||||
const getStoredUsersBetaFlags = (email: any) => {
|
||||
return store.getItem(email);
|
||||
};
|
||||
|
||||
const setStoredUsersBetaFlags = (email: any, userBetaFlagsObj: any) => {
|
||||
return store.setItem(email, userBetaFlagsObj);
|
||||
};
|
||||
|
||||
export const setReflowBetaFlag = async (email: any, enable: boolean) => {
|
||||
const userBetaFlagsObj: any = await getStoredUsersBetaFlags(email);
|
||||
const updatedObj = {
|
||||
...userBetaFlagsObj,
|
||||
[STORAGE_KEYS.REFLOW_BETA_FLAG]: enable,
|
||||
};
|
||||
setStoredUsersBetaFlags(email, updatedObj);
|
||||
};
|
||||
|
||||
export const getReflowBetaFlag = async (email: any) => {
|
||||
const userBetaFlagsObj: any = await getStoredUsersBetaFlags(email);
|
||||
return userBetaFlagsObj && userBetaFlagsObj[STORAGE_KEYS.REFLOW_BETA_FLAG];
|
||||
};
|
||||
|
||||
export const getCopiedWidgets = async () => {
|
||||
try {
|
||||
const widget: string | null = await store.getItem(
|
||||
|
|
|
|||
|
|
@ -271,6 +271,7 @@ abstract class BaseWidget<
|
|||
return (
|
||||
<PositionedContainer
|
||||
focused={this.props.focused}
|
||||
parentId={this.props.parentId}
|
||||
resizeDisabled={this.props.resizeDisabled}
|
||||
selected={this.props.selected}
|
||||
style={style}
|
||||
|
|
|
|||
|
|
@ -221,6 +221,7 @@ class ContainerWidget extends BaseWidget<
|
|||
canExtend={props.canExtend}
|
||||
dropDisabled={!!props.dropDisabled}
|
||||
noPad={this.props.noPad}
|
||||
parentId={props.parentId}
|
||||
snapRows={snapRows}
|
||||
widgetId={props.widgetId}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -29,6 +29,9 @@ describe("<DividerWidget />", () => {
|
|||
editor: {
|
||||
isPreviewMode: false,
|
||||
},
|
||||
widgetReflow: {
|
||||
enableReflow: true,
|
||||
},
|
||||
},
|
||||
entities: { canvasWidgets: {}, app: { mode: "canvas" } },
|
||||
};
|
||||
|
|
|
|||
|
|
@ -33,6 +33,9 @@ describe("<DropdownWidget />", () => {
|
|||
editor: {
|
||||
isPreviewMode: false,
|
||||
},
|
||||
widgetReflow: {
|
||||
enableReflow: true,
|
||||
},
|
||||
},
|
||||
entities: { canvasWidgets: {}, app: { mode: "canvas" } },
|
||||
};
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ import {
|
|||
BottomHandleStyles,
|
||||
} from "components/editorComponents/ResizeStyledComponents";
|
||||
import { Layers } from "constants/Layers";
|
||||
import Resizable from "resizable";
|
||||
import Resizable from "resizable/resize";
|
||||
import { getCanvasClassName } from "utils/generators";
|
||||
import { AppState } from "reducers";
|
||||
import { useWidgetDragResize } from "utils/hooks/dragResizeHooks";
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user