feat: Anvil DnD Polish(Refactor) (#32839)
[](https://workerb.linearb.io/v2/badge/collaboration-page?magicLinkId=MdeCkAG) ## Description In this PR, - we are refactoring Anvil DnD to use dom elements to render highlights instead of canvas. Why? - Doesn't have the ability to overflow a widget and still allow dnd events(section widget use case) - limitations with respect to being testable - we are adding compensators to dnd layers, so that DnD is not just for layout but for the entire widget. Widget is the appsmith entity. layout is something present in container like widgets like Section and Zone which allow you to contain children widgets based on a the layout structure. What are compensators? - additional padding for DnD layers in a widget - additional position offsets for highlights in a widget so that they don't stick to the layout. Why compensators? - Section widget can be activated to show highlights in areas overlapping with main canvas - DnD should be possible even at spacings created by parent widget like in Zone widget - we are also removing canvas activation logic and moving to using mouse move event to activate a canvas for DnD. Fixes #32016 _or_ Fixes `Issue URL` > [!WARNING] > _If no issue exists, please create an issue first, and check with the maintainers if the issue is valid._ ## Automation /ok-to-test tags="@tag.Anvil" ### 🔍 Cypress test results <!-- This is an auto-generated comment: Cypress test results --> > [!TIP] > 🟢 🟢 🟢 All cypress tests have passed! 🎉 🎉 🎉 > Workflow run: <https://github.com/appsmithorg/appsmith/actions/runs/8872919856> > Commit: 3f6603bf8480a99437552ac73764c9de1d6f7f95 > Cypress dashboard url: <a href="https://internal.appsmith.com/app/cypress-dashboard/rundetails-65890b3c81d7400d08fa9ee5?branch=master&workflowId=8872919856&attempt=1" target="_blank">Click here!</a> <!-- end of auto-generated comment: Cypress test results --> ## Communication Should the DevRel and Marketing teams inform users about this change? - [ ] Yes - [x] No <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Summary by CodeRabbit - **New Features** - Introduced a new layout property for widgets, enhancing customization options. - Added a custom error boundary component to handle and display errors elegantly. - New drag-and-drop components and utilities to improve interaction within the editor canvas. - Adjusted component hierarchies in Anvil editor and viewer for better performance and structure. - **Refactor** - Simplified rendering logic in `AnvilWidgetComponent` by removing conditional boundaries. - Updated the hierarchy within the Anvil editor, enhancing component nesting and interaction. - **Bug Fixes** - Adjusted padding values and added new constants for better UI alignment and interaction feedback. - **Chores** - Renamed and reorganized Cypress locators and methods for clearer and more efficient testing automation. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
parent
4f70c70701
commit
4b1ef98014
|
|
@ -84,7 +84,8 @@ export class CommonLocators {
|
|||
_visibleTextSpan = (spanText: string, isCss = false) =>
|
||||
isCss ? `span:contains("${spanText}")` : `//span[text()="${spanText}"]`;
|
||||
_dropHere = ".t--drop-target";
|
||||
_canvasSlider = "[data-type=canvas-slider]";
|
||||
_anvilDnDListener = "[data-type=anvil-dnd-listener]";
|
||||
_anvilDnDHighlight = "[data-type=anvil-dnd-highlight]";
|
||||
_editPage = "[data-testid=onboarding-tasks-datasource-text], .t--drop-target";
|
||||
_crossBtn = "span.cancel-icon";
|
||||
_createNew = ".t--add-item";
|
||||
|
|
|
|||
|
|
@ -32,11 +32,11 @@ export class AnvilLayout {
|
|||
}
|
||||
if (dropTarget.name) {
|
||||
return `${getWidgetSelector(dropTarget.name.toLowerCase() as any)} ${
|
||||
this.locator._canvasSlider
|
||||
this.locator._anvilDnDListener
|
||||
}`;
|
||||
}
|
||||
}
|
||||
return this.locator._canvasSlider;
|
||||
return this.locator._anvilDnDListener;
|
||||
};
|
||||
|
||||
private performDnDInAnvil(
|
||||
|
|
@ -47,33 +47,30 @@ export class AnvilLayout {
|
|||
const dropAreaSelector = this.getAnvilDropTargetSelectorFromOptions(
|
||||
options.dropTargetDetails,
|
||||
);
|
||||
cy.get(dropAreaSelector)
|
||||
.first()
|
||||
.then((dropAreaDom) => {
|
||||
const { left, top } = dropAreaDom[0].getBoundingClientRect();
|
||||
cy.document()
|
||||
// to activate ANVIL canvas
|
||||
.trigger("mousemove", left + xPos, top + yPos, {
|
||||
eventConstructor: "MouseEvent",
|
||||
force: true,
|
||||
});
|
||||
this.agHelper.Sleep(200);
|
||||
cy.get(dropAreaSelector).first().trigger("mousemove", xPos, yPos, {
|
||||
eventConstructor: "MouseEvent",
|
||||
force: true,
|
||||
});
|
||||
this.agHelper.Sleep(200);
|
||||
cy.get(dropAreaSelector).first().trigger("mousemove", xPos, yPos, {
|
||||
eventConstructor: "MouseEvent",
|
||||
force: true,
|
||||
});
|
||||
cy.get(this.locator._canvasSlider)
|
||||
.first()
|
||||
.trigger("mouseup", xPos, yPos, {
|
||||
eventConstructor: "MouseEvent",
|
||||
force: true,
|
||||
});
|
||||
cy.document()
|
||||
// to activate ANVIL canvas
|
||||
.trigger("mousemove", xPos, yPos, {
|
||||
eventConstructor: "MouseEvent",
|
||||
force: true,
|
||||
});
|
||||
this.agHelper.Sleep(200);
|
||||
cy.get(dropAreaSelector).first().trigger("mouseover", xPos, yPos, {
|
||||
eventConstructor: "MouseEvent",
|
||||
force: true,
|
||||
});
|
||||
cy.get(dropAreaSelector).first().trigger("mousemove", xPos, yPos, {
|
||||
eventConstructor: "MouseEvent",
|
||||
force: true,
|
||||
});
|
||||
cy.get(dropAreaSelector).first().trigger("mousemove", xPos, yPos, {
|
||||
eventConstructor: "MouseEvent",
|
||||
force: true,
|
||||
});
|
||||
cy.get(this.locator._anvilDnDHighlight);
|
||||
cy.get(dropAreaSelector).first().trigger("mouseup", xPos, yPos, {
|
||||
eventConstructor: "MouseEvent",
|
||||
force: true,
|
||||
});
|
||||
}
|
||||
|
||||
private startDraggingWidgetFromPane(widgetType: string) {
|
||||
|
|
|
|||
|
|
@ -178,6 +178,7 @@ export const WIDGET_DSL_STRUCTURE_PROPS = {
|
|||
topRow: true,
|
||||
type: true,
|
||||
widgetId: true,
|
||||
layout: true,
|
||||
};
|
||||
|
||||
export type TextSize = keyof typeof TextSizes;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,24 @@
|
|||
import styled from "styled-components";
|
||||
import ErrorBoundary from "components/editorComponents/ErrorBoundry";
|
||||
import React from "react";
|
||||
|
||||
const RetryLink = styled.span`
|
||||
color: ${(props) => props.theme.colors.primaryDarkest};
|
||||
cursor: pointer;
|
||||
`;
|
||||
|
||||
export class AnvilErrorBoundary extends ErrorBoundary {
|
||||
render() {
|
||||
return this.state.hasError ? (
|
||||
<p>
|
||||
Something went wrong.
|
||||
<br />
|
||||
<RetryLink onClick={() => this.setState({ hasError: false })}>
|
||||
Click here to retry
|
||||
</RetryLink>
|
||||
</p>
|
||||
) : (
|
||||
(this.props.children as any)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +1,10 @@
|
|||
import ErrorBoundary from "components/editorComponents/ErrorBoundry";
|
||||
import WidgetComponentBoundary from "layoutSystems/common/widgetComponent/WidgetComponentBoundary";
|
||||
import React from "react";
|
||||
import type { BaseWidgetProps } from "widgets/BaseWidgetHOC/withBaseWidgetHOC";
|
||||
import Skeleton from "widgets/Skeleton";
|
||||
import { AnvilErrorBoundary } from "./AnvilErrorBoundary";
|
||||
|
||||
export const AnvilWidgetComponent = (props: BaseWidgetProps) => {
|
||||
const { deferRender, detachFromLayout, type } = props;
|
||||
const { children, deferRender, type } = props;
|
||||
/**
|
||||
* The widget mount calls the withWidgetProps with the widgetId and type to fetch the
|
||||
* widget props. During the computation of the props (in withWidgetProps) if the evaluated
|
||||
|
|
@ -19,14 +18,5 @@ export const AnvilWidgetComponent = (props: BaseWidgetProps) => {
|
|||
return <Skeleton />;
|
||||
}
|
||||
|
||||
if (!detachFromLayout) return props.children;
|
||||
|
||||
return (
|
||||
// delete style as soon as we switch to Anvil layout completely
|
||||
<ErrorBoundary style={{ height: "auto", width: "auto" }}>
|
||||
<WidgetComponentBoundary widgetType={type}>
|
||||
{props.children}
|
||||
</WidgetComponentBoundary>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
return <AnvilErrorBoundary>{children}</AnvilErrorBoundary>;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,10 +1,18 @@
|
|||
import type { BaseWidgetProps } from "widgets/BaseWidgetHOC/withBaseWidgetHOC";
|
||||
import { AnvilViewerCanvas } from "layoutSystems/anvil/viewer/canvas/AnvilViewerCanvas";
|
||||
import React, { useCallback, useEffect, useRef } from "react";
|
||||
import { useCanvasActivation } from "./hooks/useCanvasActivation";
|
||||
import { useSelectWidgetListener } from "./hooks/useSelectWidgetListener";
|
||||
import { useClickToClearSelections } from "./hooks/useClickToClearSelections";
|
||||
import "./styles/anvilEditorVariables.css";
|
||||
import {
|
||||
useAnvilGlobalDnDStates,
|
||||
type AnvilGlobalDnDStates,
|
||||
} from "./hooks/useAnvilGlobalDnDStates";
|
||||
|
||||
export const AnvilDnDStatesContext = React.createContext<
|
||||
AnvilGlobalDnDStates | undefined
|
||||
>(undefined);
|
||||
|
||||
/**
|
||||
* Anvil Main Canvas is just a wrapper around AnvilCanvas.
|
||||
* Why do we need this?
|
||||
|
|
@ -45,7 +53,13 @@ export const AnvilEditorCanvas = (props: BaseWidgetProps) => {
|
|||
}, []);
|
||||
/* End of click event listener */
|
||||
|
||||
useCanvasActivation();
|
||||
useSelectWidgetListener();
|
||||
return <AnvilViewerCanvas {...props} ref={canvasRef} />;
|
||||
// Fetching all states used in Anvil DnD using the useAnvilGlobalDnDStates hook
|
||||
// using AnvilDnDStatesContext to provide the states to the child AnvilDraggingArena
|
||||
const anvilGlobalDnDStates = useAnvilGlobalDnDStates();
|
||||
return (
|
||||
<AnvilDnDStatesContext.Provider value={anvilGlobalDnDStates}>
|
||||
<AnvilViewerCanvas {...props} ref={canvasRef} />
|
||||
</AnvilDnDStatesContext.Provider>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,41 @@
|
|||
import { useEffect } from "react";
|
||||
import { useWidgetDragResize } from "utils/hooks/dragResizeHooks";
|
||||
|
||||
/**
|
||||
* This hook handles the deactivation of the DnD Listeners while dragging.
|
||||
*/
|
||||
|
||||
export const useAnvilDnDDeactivation = (
|
||||
isDragging: boolean,
|
||||
isNewWidget: boolean,
|
||||
) => {
|
||||
// Destructuring hook functions for dragging new widgets and setting dragging state
|
||||
const { setDraggingNewWidget, setDraggingState } = useWidgetDragResize();
|
||||
|
||||
// Callback function to handle mouse up events and reset dragging state
|
||||
const onMouseUp = () => {
|
||||
if (isDragging) {
|
||||
if (isNewWidget) {
|
||||
setDraggingNewWidget(false, undefined);
|
||||
} else {
|
||||
setDraggingState({
|
||||
isDragging: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isDragging) {
|
||||
// Adding event listeners for mouse move and mouse up events
|
||||
document.body.addEventListener("mouseup", onMouseUp, false);
|
||||
window.addEventListener("mouseup", onMouseUp, false);
|
||||
|
||||
// Removing event listeners when the component unmounts or when dragging ends
|
||||
return () => {
|
||||
document.body.removeEventListener("mouseup", onMouseUp);
|
||||
window.removeEventListener("mouseup", onMouseUp);
|
||||
};
|
||||
}
|
||||
}, [isDragging, onMouseUp]);
|
||||
};
|
||||
|
|
@ -12,45 +12,62 @@ import {
|
|||
getDraggedBlocks,
|
||||
getDraggedWidgetHierarchy,
|
||||
getDraggedWidgetTypes,
|
||||
} from "./utils";
|
||||
import type { AnvilDraggedWidgetTypes } from "../types";
|
||||
} from "../../canvasArenas/utils/utils";
|
||||
import type { DraggedWidget } from "layoutSystems/anvil/utils/anvilTypes";
|
||||
import type { AnvilDraggedWidgetTypesEnum } from "../../canvasArenas/types";
|
||||
import { useAnvilDnDDeactivation } from "./useAnvilDnDDeactivation";
|
||||
|
||||
export interface AnvilCanvasActivationStates {
|
||||
export interface AnvilGlobalDnDStates {
|
||||
activateOverlayWidgetDrop: boolean;
|
||||
draggedBlocks: DraggedWidget[];
|
||||
dragDetails: DragDetails;
|
||||
draggedWidgetHierarchy: number;
|
||||
draggedWidgetTypes: AnvilDraggedWidgetTypes;
|
||||
draggedWidgetTypes: AnvilDraggedWidgetTypesEnum;
|
||||
isDragging: boolean;
|
||||
isNewWidget: boolean;
|
||||
layoutElementPositions: LayoutElementPositions;
|
||||
mainCanvasLayoutId: string;
|
||||
selectedWidgets: string[];
|
||||
}
|
||||
|
||||
export const useCanvasActivationStates = (): AnvilCanvasActivationStates => {
|
||||
/**
|
||||
* This hook is used to get the global states of the canvas while dragging.
|
||||
* It also is responsible for deactivating the canvas while dragging.
|
||||
* @returns AnvilGlobalDnDStates
|
||||
*/
|
||||
export const useAnvilGlobalDnDStates = (): AnvilGlobalDnDStates => {
|
||||
const mainCanvasLayoutId: string = useSelector((state) =>
|
||||
getDropTargetLayoutId(state, MAIN_CONTAINER_WIDGET_ID),
|
||||
);
|
||||
const layoutElementPositions = useSelector(getLayoutElementPositions);
|
||||
const allWidgets = useSelector(getWidgets);
|
||||
const selectedWidgets = useSelector(getSelectedWidgets);
|
||||
// dragDetails contains of info needed for a container jump:
|
||||
// which parent the dragging widget belongs,
|
||||
// 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.
|
||||
|
||||
/**
|
||||
* dragDetails is the state that holds the details of the widget being dragged.
|
||||
*/
|
||||
const dragDetails: DragDetails = useSelector(getDragDetails);
|
||||
|
||||
/**
|
||||
* isDragging is a boolean that indicates if a widget is being dragged.
|
||||
*/
|
||||
const isDragging = useSelector(
|
||||
(state: AppState) => state.ui.widgetDragResize.isDragging,
|
||||
);
|
||||
|
||||
/**
|
||||
* dragParent is the parent of the widget being dragged.
|
||||
*/
|
||||
const { dragGroupActualParent: dragParent, newWidget } = dragDetails;
|
||||
|
||||
/**
|
||||
* boolean to indicate if the widget being dragged is a new widget
|
||||
*/
|
||||
const isNewWidget = !!newWidget && !dragParent;
|
||||
// process drag blocks only once and per first render
|
||||
// this is by taking advantage of the fact that isNewWidget and dragDetails are unchanged states during the dragging action.
|
||||
|
||||
/**
|
||||
* compute drag blocks only once and per first render
|
||||
* this is by taking advantage of the fact that isNewWidget and dragDetails are unchanged states during the dragging action.
|
||||
*/
|
||||
const draggedBlocks = useMemo(
|
||||
() =>
|
||||
isDragging
|
||||
|
|
@ -63,25 +80,40 @@ export const useCanvasActivationStates = (): AnvilCanvasActivationStates => {
|
|||
: [],
|
||||
[isDragging, selectedWidgets],
|
||||
);
|
||||
|
||||
/**
|
||||
* boolean to indicate if the widget is being dragged on this particular canvas.
|
||||
*/
|
||||
const draggedWidgetHierarchy = getDraggedWidgetHierarchy(draggedBlocks);
|
||||
|
||||
/**
|
||||
* boolean that indicates if the widget being dragged in an overlay widget like the Modal widget.
|
||||
*/
|
||||
const activateOverlayWidgetDrop = isNewWidget && !!newWidget.detachFromLayout;
|
||||
const activateOverlayWidgetDrop =
|
||||
isNewWidget && newWidget.detachFromLayout === true;
|
||||
|
||||
/**
|
||||
* get the dragged widget types to assess if the widget can be dropped on the canvas.
|
||||
*/
|
||||
const draggedWidgetTypes = useMemo(
|
||||
() => getDraggedWidgetTypes(draggedBlocks),
|
||||
[draggedBlocks],
|
||||
);
|
||||
const draggedWidgetHierarchy = getDraggedWidgetHierarchy(draggedBlocks);
|
||||
|
||||
/**
|
||||
* This hook handles the deactivation of the canvas(Drop targets) while dragging.
|
||||
*/
|
||||
useAnvilDnDDeactivation(isDragging, isNewWidget);
|
||||
|
||||
return {
|
||||
activateOverlayWidgetDrop,
|
||||
dragDetails,
|
||||
draggedBlocks,
|
||||
draggedWidgetHierarchy,
|
||||
dragDetails,
|
||||
draggedWidgetTypes,
|
||||
isDragging,
|
||||
isNewWidget,
|
||||
layoutElementPositions,
|
||||
mainCanvasLayoutId,
|
||||
selectedWidgets,
|
||||
layoutElementPositions,
|
||||
};
|
||||
};
|
||||
|
|
@ -1,237 +0,0 @@
|
|||
import { CANVAS_ART_BOARD } from "constants/componentClassNameConstants";
|
||||
import { Indices } from "constants/Layers";
|
||||
import { MAIN_CONTAINER_WIDGET_ID } from "constants/WidgetConstants";
|
||||
import type { LayoutElementPosition } from "layoutSystems/common/types";
|
||||
import { positionObserver } from "layoutSystems/common/utils/LayoutElementPositionsObserver";
|
||||
import { getAnvilLayoutDOMId } from "layoutSystems/common/utils/LayoutElementPositionsObserver/utils";
|
||||
import { debounce, uniq } from "lodash";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useWidgetDragResize } from "utils/hooks/dragResizeHooks";
|
||||
import { LayoutComponentTypes } from "layoutSystems/anvil/utils/anvilTypes";
|
||||
import type { CanvasWidgetsReduxState } from "reducers/entityReducers/canvasWidgetsReducer";
|
||||
import { useSelector } from "react-redux";
|
||||
import { getWidgets } from "sagas/selectors";
|
||||
import type { FlattenedWidgetProps } from "WidgetProvider/constants";
|
||||
import { useCanvasActivationStates } from "layoutSystems/anvil/editor/canvasArenas/hooks/useCanvasActivationStates";
|
||||
import { canActivateCanvasForDraggedWidget } from "layoutSystems/anvil/editor/canvasArenas/hooks/utils";
|
||||
|
||||
// Z-Index values for activated and deactivated states
|
||||
export const AnvilCanvasZIndex = {
|
||||
// we can decrease the z-index once we are able to provide fix for the issue #28471
|
||||
activated: Indices.Layer10.toString(),
|
||||
deactivated: "",
|
||||
};
|
||||
|
||||
// Function to check if mouse position is inside a block
|
||||
const checkIfMousePositionIsInsideBlock = (
|
||||
e: MouseEvent,
|
||||
mainCanvasRect: DOMRect,
|
||||
layoutElementPosition: LayoutElementPosition,
|
||||
) => {
|
||||
return (
|
||||
layoutElementPosition.left <= e.clientX - mainCanvasRect.left &&
|
||||
e.clientX - mainCanvasRect.left <=
|
||||
layoutElementPosition.left + layoutElementPosition.width &&
|
||||
layoutElementPosition.top <= e.clientY - mainCanvasRect.top &&
|
||||
e.clientY - mainCanvasRect.top <=
|
||||
layoutElementPosition.top + layoutElementPosition.height
|
||||
);
|
||||
};
|
||||
|
||||
// Buffer value for the main canvas
|
||||
// This buffer will make sure main canvas is not deactivated
|
||||
// until its about the below pixel distance from the main canvas border.
|
||||
const MAIN_CANVAS_BUFFER = 20;
|
||||
const SECTION_BUFFER = 20;
|
||||
|
||||
/**
|
||||
* This hook handles the activation and deactivation of the canvas(Drop targets) while dragging.
|
||||
*/
|
||||
|
||||
export const useCanvasActivation = () => {
|
||||
const {
|
||||
activateOverlayWidgetDrop,
|
||||
dragDetails,
|
||||
draggedWidgetHierarchy,
|
||||
isDragging,
|
||||
isNewWidget,
|
||||
layoutElementPositions,
|
||||
mainCanvasLayoutId,
|
||||
selectedWidgets,
|
||||
} = useCanvasActivationStates();
|
||||
const allWidgets: CanvasWidgetsReduxState = useSelector(getWidgets);
|
||||
// Getting the main canvas DOM node
|
||||
const mainContainerDOMNode = document.getElementById(CANVAS_ART_BOARD);
|
||||
|
||||
// Destructuring hook functions for drag and resize functionality
|
||||
const { setDraggingCanvas, setDraggingNewWidget, setDraggingState } =
|
||||
useWidgetDragResize();
|
||||
|
||||
// Mapping selected widget positions
|
||||
const draggedWidgetPositions = selectedWidgets.map((each) => {
|
||||
return layoutElementPositions[each];
|
||||
});
|
||||
/**
|
||||
* boolean ref that indicates if the mouse position is outside of main canvas while dragging
|
||||
* this is being tracked in order to activate/deactivate canvas.
|
||||
*/
|
||||
const isMouseOutOfMainCanvas = useRef(false);
|
||||
|
||||
// Function to handle mouse leaving the canvas while dragging
|
||||
const mouseOutOfCanvasArtBoard = () => {
|
||||
isMouseOutOfMainCanvas.current = true;
|
||||
setDraggingCanvas();
|
||||
};
|
||||
|
||||
// Debouncing functions for smoother transitions
|
||||
const debouncedSetDraggingCanvas = debounce(setDraggingCanvas);
|
||||
const debouncedMouseOutOfCanvasArtBoard = debounce(mouseOutOfCanvasArtBoard);
|
||||
|
||||
// All layouts registered on the position observer
|
||||
const allLayouts: any = isDragging
|
||||
? positionObserver.getRegisteredLayouts()
|
||||
: {};
|
||||
|
||||
// All layout IDs on the page
|
||||
const allLayoutIds = Object.keys(allLayouts);
|
||||
|
||||
// DOM ID of the main canvas layout
|
||||
const mainCanvasLayoutDomId = getAnvilLayoutDOMId(
|
||||
MAIN_CONTAINER_WIDGET_ID,
|
||||
mainCanvasLayoutId,
|
||||
);
|
||||
|
||||
/**
|
||||
* layoutIds that are supported to drop while dragging.
|
||||
* when dragging an AnvilOverlayWidgetTypes widget only the main canvas is supported for dropping.
|
||||
*/
|
||||
const filteredLayoutIds: string[] = activateOverlayWidgetDrop
|
||||
? allLayoutIds.filter((each) => each === mainCanvasLayoutDomId)
|
||||
: allLayoutIds;
|
||||
// All droppable layout IDs
|
||||
const allDroppableLayoutIds = uniq(
|
||||
filteredLayoutIds
|
||||
.filter((each) => {
|
||||
const layoutInfo = allLayouts[each];
|
||||
const currentPositions = layoutElementPositions[layoutInfo.layoutId];
|
||||
const widget: FlattenedWidgetProps = allWidgets[layoutInfo.canvasId];
|
||||
const canActivate = canActivateCanvasForDraggedWidget(
|
||||
draggedWidgetHierarchy,
|
||||
widget?.widgetId,
|
||||
widget?.type,
|
||||
);
|
||||
return canActivate && currentPositions && !!layoutInfo.isDropTarget;
|
||||
})
|
||||
.map((each) => allLayouts[each].layoutId),
|
||||
);
|
||||
/**
|
||||
* Droppable layout IDs sorted by area in ascending order
|
||||
* This is done because a point can be inside multiple canvas areas, but only the smallest of them is the immediate parent.
|
||||
*/
|
||||
const smallToLargeSortedDroppableLayoutIds = allDroppableLayoutIds.sort(
|
||||
(droppableLayout1Id: string, droppableLayout2Id: string) => {
|
||||
const droppableLayout1 = layoutElementPositions[droppableLayout1Id];
|
||||
const droppableLayout2 = layoutElementPositions[droppableLayout2Id];
|
||||
return (
|
||||
droppableLayout1.height * droppableLayout1.width -
|
||||
droppableLayout2.height * droppableLayout2.width
|
||||
);
|
||||
},
|
||||
);
|
||||
/**
|
||||
* Callback function to handle mouse move events while dragging state is set.
|
||||
* The function uses the mouse position and checks through smallToLargeSortedDroppableLayoutIds
|
||||
* to find under which layout the point is positioned and activates that layout canvas.
|
||||
*
|
||||
* Canvas activation means that the layout's canvas is raised up in z-index to register and process mouse events
|
||||
* and draw highlights appropriately.
|
||||
*/
|
||||
const onMouseMoveWhileDragging = (e: MouseEvent) => {
|
||||
if (
|
||||
isDragging &&
|
||||
mainContainerDOMNode &&
|
||||
smallToLargeSortedDroppableLayoutIds.length > 0
|
||||
) {
|
||||
// Getting the main canvas bounding rect
|
||||
const mainCanvasRect = mainContainerDOMNode.getBoundingClientRect();
|
||||
|
||||
// Checking if the mouse position is outside of dragging widgets
|
||||
const isMousePositionOutsideOfDraggingWidgets =
|
||||
!isNewWidget &&
|
||||
draggedWidgetPositions.find((each) => {
|
||||
return checkIfMousePositionIsInsideBlock(e, mainCanvasRect, each);
|
||||
});
|
||||
|
||||
// Finding the layout under the mouse position
|
||||
const hoveredCanvas = isMousePositionOutsideOfDraggingWidgets
|
||||
? dragDetails.dragGroupActualParent
|
||||
: smallToLargeSortedDroppableLayoutIds.find((each) => {
|
||||
const currentCanvasPositions = { ...layoutElementPositions[each] };
|
||||
if (each === mainCanvasLayoutId) {
|
||||
currentCanvasPositions.left -= MAIN_CANVAS_BUFFER;
|
||||
currentCanvasPositions.top -= MAIN_CANVAS_BUFFER;
|
||||
currentCanvasPositions.width += 2 * MAIN_CANVAS_BUFFER;
|
||||
currentCanvasPositions.height += 2 * MAIN_CANVAS_BUFFER;
|
||||
}
|
||||
const layoutInfo = allLayouts[each];
|
||||
if (layoutInfo.layoutType === LayoutComponentTypes.SECTION) {
|
||||
currentCanvasPositions.top += SECTION_BUFFER;
|
||||
currentCanvasPositions.height -= 2 * SECTION_BUFFER;
|
||||
currentCanvasPositions.width += 2 * SECTION_BUFFER;
|
||||
currentCanvasPositions.left -= SECTION_BUFFER;
|
||||
}
|
||||
if (currentCanvasPositions) {
|
||||
return checkIfMousePositionIsInsideBlock(
|
||||
e,
|
||||
mainCanvasRect,
|
||||
currentCanvasPositions,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Handling canvas activation and deactivation
|
||||
if (dragDetails.draggedOn !== hoveredCanvas) {
|
||||
if (hoveredCanvas) {
|
||||
isMouseOutOfMainCanvas.current = false;
|
||||
debouncedSetDraggingCanvas(hoveredCanvas);
|
||||
} else {
|
||||
debouncedMouseOutOfCanvasArtBoard();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Callback function to handle mouse up events and reset dragging state
|
||||
const onMouseUp = () => {
|
||||
if (isDragging) {
|
||||
if (isNewWidget) {
|
||||
setDraggingNewWidget(false, undefined);
|
||||
} else {
|
||||
setDraggingState({
|
||||
isDragging: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isDragging) {
|
||||
// Adding event listeners for mouse move and mouse up events
|
||||
document?.addEventListener("mousemove", onMouseMoveWhileDragging);
|
||||
document.body.addEventListener("mouseup", onMouseUp, false);
|
||||
window.addEventListener("mouseup", onMouseUp, false);
|
||||
|
||||
// Removing event listeners when the component unmounts or when dragging ends
|
||||
return () => {
|
||||
document?.removeEventListener("mousemove", onMouseMoveWhileDragging);
|
||||
document.body.removeEventListener("mouseup", onMouseUp);
|
||||
window.removeEventListener("mouseup", onMouseUp);
|
||||
};
|
||||
}
|
||||
}, [
|
||||
isDragging,
|
||||
onMouseMoveWhileDragging,
|
||||
onMouseUp,
|
||||
debouncedMouseOutOfCanvasArtBoard,
|
||||
]);
|
||||
};
|
||||
|
|
@ -1,67 +0,0 @@
|
|||
import type { LayoutElementPositions } from "layoutSystems/common/types";
|
||||
import React from "react";
|
||||
import type {
|
||||
DraggedWidget,
|
||||
HighlightPayload,
|
||||
LayoutComponentTypes,
|
||||
} from "layoutSystems/anvil/utils/anvilTypes";
|
||||
import { AnvilHighlightingCanvas } from "./AnvilHighlightingCanvas";
|
||||
import { useAnvilDnDStates } from "./hooks/useAnvilDnDStates";
|
||||
import { useAnvilWidgetDrop } from "./hooks/useAnvilWidgetDrop";
|
||||
import { DetachedWidgetsDropArena } from "./DetachedWidgetsDropArena";
|
||||
import { useSelector } from "react-redux";
|
||||
import { isEditOnlyModeSelector } from "selectors/editorSelectors";
|
||||
|
||||
// Props interface for AnvilCanvasDraggingArena component
|
||||
interface AnvilCanvasDraggingArenaProps {
|
||||
canvasId: string;
|
||||
layoutId: string;
|
||||
layoutType: LayoutComponentTypes;
|
||||
allowedWidgetTypes: string[];
|
||||
deriveAllHighlightsFn: (
|
||||
layoutElementPositions: LayoutElementPositions,
|
||||
draggedWidgets: DraggedWidget[],
|
||||
) => HighlightPayload;
|
||||
}
|
||||
|
||||
export const AnvilCanvasDraggingArena = (
|
||||
props: AnvilCanvasDraggingArenaProps,
|
||||
) => {
|
||||
const isEditOnlyMode = useSelector(isEditOnlyModeSelector);
|
||||
const {
|
||||
allowedWidgetTypes,
|
||||
canvasId,
|
||||
deriveAllHighlightsFn,
|
||||
layoutId,
|
||||
layoutType,
|
||||
} = props;
|
||||
|
||||
// Fetching all states used in Anvil DnD using the useAnvilDnDStates hook
|
||||
const anvilDragStates = useAnvilDnDStates({
|
||||
allowedWidgetTypes,
|
||||
canvasId,
|
||||
layoutId,
|
||||
layoutType,
|
||||
});
|
||||
|
||||
// Using the useAnvilWidgetDrop hook to handle widget dropping
|
||||
const onDrop = useAnvilWidgetDrop(canvasId, anvilDragStates);
|
||||
const isMainCanvasDropArena =
|
||||
anvilDragStates.mainCanvasLayoutId === props.layoutId;
|
||||
return isEditOnlyMode ? (
|
||||
<>
|
||||
<AnvilHighlightingCanvas
|
||||
anvilDragStates={anvilDragStates}
|
||||
deriveAllHighlightsFn={deriveAllHighlightsFn}
|
||||
layoutId={layoutId}
|
||||
onDrop={onDrop}
|
||||
/>
|
||||
{isMainCanvasDropArena && (
|
||||
<DetachedWidgetsDropArena
|
||||
anvilDragStates={anvilDragStates}
|
||||
onDrop={onDrop}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : null;
|
||||
};
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
import type { AnvilHighlightInfo } from "layoutSystems/anvil/utils/anvilTypes";
|
||||
import { PADDING_FOR_HORIZONTAL_HIGHLIGHT } from "layoutSystems/anvil/utils/constants";
|
||||
import React, { useMemo } from "react";
|
||||
import styled from "styled-components";
|
||||
|
||||
// Styled component for the highlight element
|
||||
const AnvilStyledHighlight = styled.div<{ zIndex: number }>`
|
||||
background-color: var(--anvil-drop-indicator);
|
||||
border-radius: 2px;
|
||||
position: absolute;
|
||||
z-index: ${(props) => props.zIndex};
|
||||
pointer-events: none;
|
||||
`;
|
||||
|
||||
export const AnvilDnDHighlight = ({
|
||||
compensatorValues = {
|
||||
left: 0,
|
||||
top: 0,
|
||||
},
|
||||
highlightShown,
|
||||
zIndex = 0,
|
||||
}: {
|
||||
compensatorValues?: {
|
||||
left: number;
|
||||
top: number;
|
||||
};
|
||||
highlightShown: AnvilHighlightInfo | null;
|
||||
zIndex?: number;
|
||||
}) => {
|
||||
// Memoized calculation of highlight dimensions styles
|
||||
const highlightDimensionStyles = useMemo(() => {
|
||||
if (!highlightShown) {
|
||||
// If no highlight info is provided, return default dimensions
|
||||
return {
|
||||
height: 0,
|
||||
left: 0,
|
||||
top: 0,
|
||||
width: 0,
|
||||
};
|
||||
}
|
||||
// Calculate padding based on highlight orientation
|
||||
const horizontalPadding = highlightShown.isVertical
|
||||
? 0
|
||||
: PADDING_FOR_HORIZONTAL_HIGHLIGHT;
|
||||
const verticalPadding = highlightShown.isVertical
|
||||
? PADDING_FOR_HORIZONTAL_HIGHLIGHT
|
||||
: 0;
|
||||
|
||||
// Calculate dimension styles based on highlight info
|
||||
return {
|
||||
height: highlightShown.height - verticalPadding * 2,
|
||||
left: highlightShown.posX + horizontalPadding - compensatorValues.left,
|
||||
top: highlightShown.posY + verticalPadding - compensatorValues.top,
|
||||
width: highlightShown.width - horizontalPadding * 2,
|
||||
};
|
||||
}, [highlightShown]);
|
||||
|
||||
// Render the highlight element if highlight info is provided
|
||||
return highlightShown ? (
|
||||
<AnvilStyledHighlight
|
||||
data-type="anvil-dnd-highlight"
|
||||
style={highlightDimensionStyles}
|
||||
zIndex={zIndex}
|
||||
/>
|
||||
) : null; // Otherwise, return null
|
||||
};
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
import type { Ref, RefObject } from "react";
|
||||
import React, { forwardRef } from "react";
|
||||
import styled from "styled-components";
|
||||
|
||||
interface AnvilDnDListenerProps {
|
||||
compensatorValues: {
|
||||
left: number;
|
||||
top: number;
|
||||
};
|
||||
ref: RefObject<HTMLDivElement>;
|
||||
zIndex: number;
|
||||
}
|
||||
|
||||
const StyledDnDListener = styled.div<{
|
||||
paddingLeft: number;
|
||||
paddingTop: number;
|
||||
zIndex: number;
|
||||
}>`
|
||||
&.disallow-dropping {
|
||||
background-color: #eb714d;
|
||||
color: white;
|
||||
text-align: center;
|
||||
opacity: 0.8;
|
||||
}
|
||||
position: absolute;
|
||||
pointer-events: all;
|
||||
top: ${(props) => -props.paddingTop}px;
|
||||
left: ${(props) => -props.paddingLeft}px;
|
||||
height: calc(100% + ${(props) => 2 * props.paddingTop}px);
|
||||
width: calc(100% + ${(props) => 2 * props.paddingLeft}px);
|
||||
padding-inline: ${(props) => props.paddingLeft}px;
|
||||
padding-block: ${(props) => props.paddingTop}px;
|
||||
z-index: ${(props) => props.zIndex};
|
||||
`;
|
||||
|
||||
export const AnvilDnDListener = forwardRef(
|
||||
(props: AnvilDnDListenerProps, ref: Ref<HTMLDivElement>) => {
|
||||
// Refer to useAnvilDnDCompensators to understand zIndex and compensatorValues
|
||||
const { compensatorValues, zIndex } = props;
|
||||
|
||||
return (
|
||||
<StyledDnDListener
|
||||
data-type="anvil-dnd-listener"
|
||||
paddingLeft={compensatorValues.left}
|
||||
paddingTop={compensatorValues.top}
|
||||
ref={ref}
|
||||
zIndex={zIndex}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
import type { LayoutElementPositions } from "layoutSystems/common/types";
|
||||
import React, { useContext } from "react";
|
||||
import type {
|
||||
DraggedWidget,
|
||||
HighlightPayload,
|
||||
LayoutComponentTypes,
|
||||
} from "layoutSystems/anvil/utils/anvilTypes";
|
||||
import { AnvilHighlightingCanvas } from "./AnvilHighlightingCanvas";
|
||||
import { useAnvilWidgetDrop } from "./hooks/useAnvilWidgetDrop";
|
||||
import { DetachedWidgetsDropArena } from "./DetachedWidgetsDropArena";
|
||||
import { useSelector } from "react-redux";
|
||||
import { isEditOnlyModeSelector } from "selectors/editorSelectors";
|
||||
import { useAnvilDnDListenerStates } from "./hooks/useAnvilDnDListenerStates";
|
||||
import { AnvilDnDStatesContext } from "../canvas/AnvilEditorCanvas";
|
||||
import type { AnvilGlobalDnDStates } from "../canvas/hooks/useAnvilGlobalDnDStates";
|
||||
|
||||
interface AnvilCanvasDraggingArenaProps {
|
||||
widgetId: string;
|
||||
layoutId: string;
|
||||
layoutType: LayoutComponentTypes;
|
||||
allowedWidgetTypes: string[];
|
||||
deriveAllHighlightsFn: (
|
||||
layoutElementPositions: LayoutElementPositions,
|
||||
draggedWidgets: DraggedWidget[],
|
||||
) => HighlightPayload;
|
||||
}
|
||||
|
||||
/**
|
||||
* AnvilDraggingArenaComponent is the main component that renders the AnvilHighlightingCanvas and DetachedWidgetsDropArena.
|
||||
* It also uses the useAnvilWidgetDrop hook to handle widget dropping.
|
||||
* It also makes sure that the DetachedWidgetsDropArena is rendered only when the main canvas is the drop arena.
|
||||
*/
|
||||
const AnvilDraggingArenaComponent = ({
|
||||
anvilGlobalDragStates,
|
||||
dragArenaProps,
|
||||
}: {
|
||||
dragArenaProps: AnvilCanvasDraggingArenaProps;
|
||||
anvilGlobalDragStates: AnvilGlobalDnDStates;
|
||||
}) => {
|
||||
const isEditOnlyMode = useSelector(isEditOnlyModeSelector);
|
||||
const {
|
||||
allowedWidgetTypes,
|
||||
deriveAllHighlightsFn,
|
||||
layoutId,
|
||||
layoutType,
|
||||
widgetId,
|
||||
} = dragArenaProps;
|
||||
// Fetching all states used in Anvil DnD Listener using the useAnvilDnDListenerStates hook
|
||||
const anvilDragStates = useAnvilDnDListenerStates({
|
||||
allowedWidgetTypes,
|
||||
anvilGlobalDragStates,
|
||||
widgetId,
|
||||
layoutId,
|
||||
layoutType,
|
||||
});
|
||||
// Using the useAnvilWidgetDrop hook to handle widget dropping
|
||||
const onDrop = useAnvilWidgetDrop(widgetId, anvilDragStates);
|
||||
const isMainCanvasDropArena =
|
||||
anvilGlobalDragStates.mainCanvasLayoutId === layoutId;
|
||||
return isEditOnlyMode ? (
|
||||
<>
|
||||
<AnvilHighlightingCanvas
|
||||
anvilDragStates={anvilDragStates}
|
||||
deriveAllHighlightsFn={deriveAllHighlightsFn}
|
||||
layoutId={layoutId}
|
||||
onDrop={onDrop}
|
||||
widgetId={widgetId}
|
||||
/>
|
||||
{isMainCanvasDropArena && (
|
||||
<DetachedWidgetsDropArena
|
||||
anvilGlobalDragStates={anvilGlobalDragStates}
|
||||
onDrop={onDrop}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : null;
|
||||
};
|
||||
|
||||
/**
|
||||
* AnvilDraggingArena is a wrapper component for AnvilHighlightingCanvas and DetachedWidgetsDropArena.
|
||||
*/
|
||||
export const AnvilDraggingArena = (props: AnvilCanvasDraggingArenaProps) => {
|
||||
const anvilGlobalDragStates = useContext(AnvilDnDStatesContext);
|
||||
return anvilGlobalDragStates ? (
|
||||
<AnvilDraggingArenaComponent
|
||||
anvilGlobalDragStates={anvilGlobalDragStates}
|
||||
dragArenaProps={props}
|
||||
/>
|
||||
) : null;
|
||||
};
|
||||
|
|
@ -1,17 +1,18 @@
|
|||
import { getNearestParentCanvas } from "utils/generators";
|
||||
import { useCanvasDragging } from "./hooks/useCanvasDragging";
|
||||
import { StickyCanvasArena } from "layoutSystems/common/canvasArenas/StickyCanvasArena";
|
||||
import { useAnvilDnDEvents } from "./hooks/useAnvilDnDEvents";
|
||||
import React from "react";
|
||||
import type {
|
||||
AnvilHighlightInfo,
|
||||
DraggedWidget,
|
||||
HighlightPayload,
|
||||
} from "layoutSystems/anvil/utils/anvilTypes";
|
||||
import type { AnvilDnDStates } from "./hooks/useAnvilDnDStates";
|
||||
import type { LayoutElementPositions } from "layoutSystems/common/types";
|
||||
import { AnvilDnDListener } from "./AnvilDnDListener";
|
||||
import { AnvilDnDHighlight } from "./AnvilDnDHighlight";
|
||||
import type { AnvilDnDListenerStates } from "./hooks/useAnvilDnDListenerStates";
|
||||
|
||||
export interface AnvilHighlightingCanvasProps {
|
||||
anvilDragStates: AnvilDnDStates;
|
||||
anvilDragStates: AnvilDnDListenerStates;
|
||||
widgetId: string;
|
||||
layoutId: string;
|
||||
deriveAllHighlightsFn: (
|
||||
layoutElementPositions: LayoutElementPositions,
|
||||
|
|
@ -25,35 +26,38 @@ export function AnvilHighlightingCanvas({
|
|||
deriveAllHighlightsFn,
|
||||
layoutId,
|
||||
onDrop,
|
||||
widgetId,
|
||||
}: AnvilHighlightingCanvasProps) {
|
||||
const slidingArenaRef = React.useRef<HTMLDivElement>(null);
|
||||
const stickyCanvasRef = React.useRef<HTMLCanvasElement>(null);
|
||||
// showDraggingCanvas indicates if the current dragging canvas i.e. the html canvas renders
|
||||
const { showCanvas: showDraggingCanvas } = useCanvasDragging(
|
||||
slidingArenaRef,
|
||||
stickyCanvasRef,
|
||||
const anvilDnDListenerRef = React.useRef<HTMLDivElement>(null);
|
||||
const [highlightShown, setHighlightShown] =
|
||||
React.useState<AnvilHighlightInfo | null>(null);
|
||||
|
||||
const { isCurrentDraggedCanvas } = anvilDragStates;
|
||||
const { showDnDListener } = useAnvilDnDEvents(
|
||||
anvilDnDListenerRef,
|
||||
{
|
||||
anvilDragStates,
|
||||
widgetId,
|
||||
deriveAllHighlightsFn,
|
||||
layoutId,
|
||||
onDrop,
|
||||
},
|
||||
setHighlightShown,
|
||||
);
|
||||
const canvasRef = React.useRef({
|
||||
stickyCanvasRef,
|
||||
slidingArenaRef,
|
||||
});
|
||||
return showDraggingCanvas ? (
|
||||
<StickyCanvasArena
|
||||
canvasId={`canvas-dragging-${layoutId}`}
|
||||
canvasPadding={0}
|
||||
getRelativeScrollingParent={getNearestParentCanvas}
|
||||
ref={canvasRef}
|
||||
// increases pixel density of the canvas
|
||||
scaleFactor={2}
|
||||
shouldObserveIntersection={anvilDragStates.isDragging}
|
||||
showCanvas={showDraggingCanvas}
|
||||
sliderId={`div-dragarena-${layoutId}`}
|
||||
/>
|
||||
return showDnDListener ? (
|
||||
<>
|
||||
{isCurrentDraggedCanvas && (
|
||||
<AnvilDnDHighlight
|
||||
compensatorValues={anvilDragStates.widgetCompensatorValues}
|
||||
highlightShown={highlightShown}
|
||||
zIndex={anvilDragStates.zIndex + 1}
|
||||
/>
|
||||
)}
|
||||
<AnvilDnDListener
|
||||
compensatorValues={anvilDragStates.widgetCompensatorValues}
|
||||
ref={anvilDnDListenerRef}
|
||||
zIndex={anvilDragStates.zIndex}
|
||||
/>
|
||||
</>
|
||||
) : null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,13 +5,15 @@ import styled from "styled-components";
|
|||
import type { DragDetails } from "reducers/uiReducers/dragResizeReducer";
|
||||
import { DropWidgetsHereMessage } from "layoutSystems/anvil/common/messages";
|
||||
|
||||
export const EMPTY_MODAL_PADDING = 4;
|
||||
|
||||
const StyledEmptyModalDropArenaWrapper = styled.div<{ isModalEmpty: boolean }>`
|
||||
position: relative;
|
||||
${(props) =>
|
||||
props.isModalEmpty &&
|
||||
`
|
||||
position: relative;
|
||||
height: 100% !important;
|
||||
padding: 4px;
|
||||
padding: ${EMPTY_MODAL_PADDING}px;
|
||||
`}
|
||||
`;
|
||||
const StyledEmptyModalDropArena = styled.div<{
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import React from "react";
|
||||
import type { AnvilDnDStates } from "./hooks/useAnvilDnDStates";
|
||||
import type { AnvilHighlightInfo } from "layoutSystems/anvil/utils/anvilTypes";
|
||||
import { FlexLayerAlignment } from "layoutSystems/common/utils/constants";
|
||||
import { MAIN_CONTAINER_WIDGET_ID } from "constants/WidgetConstants";
|
||||
|
|
@ -7,6 +6,7 @@ import styled from "styled-components";
|
|||
import { Popover, PopoverModalContent } from "@design-system/headless";
|
||||
import { DropModalHereMessage } from "layoutSystems/anvil/common/messages";
|
||||
import styles from "./styles.module.css";
|
||||
import type { AnvilGlobalDnDStates } from "../canvas/hooks/useAnvilGlobalDnDStates";
|
||||
/**
|
||||
* Default highlight passed for AnvilOverlayWidgetTypes widgets
|
||||
*/
|
||||
|
|
@ -21,6 +21,12 @@ const overlayWidgetHighlight: AnvilHighlightInfo = {
|
|||
posY: 0,
|
||||
rowIndex: 0,
|
||||
width: 0,
|
||||
edgeDetails: {
|
||||
bottom: false,
|
||||
left: false,
|
||||
right: false,
|
||||
top: false,
|
||||
},
|
||||
};
|
||||
|
||||
const DetachedWidgetsDropArenaWrapper = styled.span`
|
||||
|
|
@ -30,16 +36,16 @@ const DetachedWidgetsDropArenaWrapper = styled.span`
|
|||
`;
|
||||
|
||||
export const DetachedWidgetsDropArena = (props: {
|
||||
anvilDragStates: AnvilDnDStates;
|
||||
anvilGlobalDragStates: AnvilGlobalDnDStates;
|
||||
onDrop: (renderedBlock: AnvilHighlightInfo) => void;
|
||||
}) => {
|
||||
const onMouseUp = () => {
|
||||
props.onDrop({
|
||||
...overlayWidgetHighlight,
|
||||
layoutOrder: [props.anvilDragStates.mainCanvasLayoutId],
|
||||
layoutOrder: [props.anvilGlobalDragStates.mainCanvasLayoutId],
|
||||
});
|
||||
};
|
||||
return props.anvilDragStates.activateOverlayWidgetDrop ? (
|
||||
return props.anvilGlobalDragStates.activateOverlayWidgetDrop ? (
|
||||
<DetachedWidgetsDropArenaWrapper onMouseUp={onMouseUp}>
|
||||
<Popover isOpen modal>
|
||||
<PopoverModalContent
|
||||
|
|
|
|||
|
|
@ -0,0 +1,40 @@
|
|||
import type { FlattenedWidgetProps } from "WidgetProvider/constants";
|
||||
import { getCompensatorsForHierarchy } from "../utils/dndCompensatorUtils";
|
||||
import { useTheme } from "@design-system/theming";
|
||||
|
||||
export const useAnvilDnDCompensators = (
|
||||
canActivate: boolean,
|
||||
draggedWidgetHierarchy: number,
|
||||
currentWidgetHierarchy: number,
|
||||
isEmptyLayout: boolean,
|
||||
widgetProps: FlattenedWidgetProps,
|
||||
) => {
|
||||
const { theme } = useTheme();
|
||||
const isElevatedWidget = !!widgetProps.elevatedBackground;
|
||||
const {
|
||||
edgeCompensatorValues,
|
||||
layoutCompensatorValues,
|
||||
widgetCompensatorValues,
|
||||
} = getCompensatorsForHierarchy(
|
||||
currentWidgetHierarchy,
|
||||
isEmptyLayout,
|
||||
isElevatedWidget,
|
||||
theme.outerSpacing,
|
||||
);
|
||||
// to make sure main canvas and modal are both treated alike
|
||||
const currentHierarchy = Math.max(currentWidgetHierarchy, 1);
|
||||
|
||||
// zIndex is set in a way that drag layers with least hierarchy(as per the constant widgetHierarchy) are below so that all layers of different hierarchy are accessible for mouse events.
|
||||
// also setting zIndex only for layers below the dragged widget to restrict being dropped from lower to upper hierarchy.
|
||||
// ex: when a zone is being dragged other zones DnD is not activated,
|
||||
// because a zone cannot be dropped into another zone as they are both of same hierarchy.
|
||||
// same zIndex with an increment of 1 is set for the highlight(AnvilDnDHighlight) to make sure it is always on top of the dnd listener(AnvilDnDListener).
|
||||
const zIndex =
|
||||
canActivate && currentHierarchy < draggedWidgetHierarchy - 1 ? 0 : 1;
|
||||
return {
|
||||
edgeCompensatorValues,
|
||||
layoutCompensatorValues,
|
||||
widgetCompensatorValues,
|
||||
zIndex,
|
||||
};
|
||||
};
|
||||
|
|
@ -0,0 +1,219 @@
|
|||
import { setHighlightsDrawnAction } from "layoutSystems/anvil/integrations/actions/draggingActions";
|
||||
import type {
|
||||
AnvilHighlightInfo,
|
||||
DraggedWidget,
|
||||
HighlightPayload,
|
||||
} from "layoutSystems/anvil/utils/anvilTypes";
|
||||
import { throttle } from "lodash";
|
||||
import { useCallback, useRef } from "react";
|
||||
import { getPositionCompensatedHighlight } from "../utils/dndCompensatorUtils";
|
||||
import { useDispatch } from "react-redux";
|
||||
import {
|
||||
getClosestHighlight,
|
||||
removeDisallowDroppingsUI,
|
||||
renderDisallowDroppingUI,
|
||||
} from "../utils/utils";
|
||||
import { useWidgetDragResize } from "utils/hooks/dragResizeHooks";
|
||||
import type { AnvilDnDListenerStates } from "./useAnvilDnDListenerStates";
|
||||
import type { LayoutElementPositions } from "layoutSystems/common/types";
|
||||
|
||||
export const useAnvilDnDEventCallbacks = ({
|
||||
anvilDnDListenerRef,
|
||||
anvilDragStates,
|
||||
canvasIsDragging,
|
||||
deriveAllHighlightsFn,
|
||||
layoutId,
|
||||
onDrop,
|
||||
setHighlightShown,
|
||||
}: {
|
||||
anvilDragStates: AnvilDnDListenerStates;
|
||||
anvilDnDListenerRef: React.RefObject<HTMLDivElement>;
|
||||
canvasIsDragging: React.MutableRefObject<boolean>;
|
||||
deriveAllHighlightsFn: (
|
||||
layoutElementPositions: LayoutElementPositions,
|
||||
draggedWidgets: DraggedWidget[],
|
||||
) => HighlightPayload;
|
||||
layoutId: string;
|
||||
onDrop: (renderedBlock: AnvilHighlightInfo) => void;
|
||||
setHighlightShown: (highlight: AnvilHighlightInfo | null) => void;
|
||||
}) => {
|
||||
const {
|
||||
activateOverlayWidgetDrop,
|
||||
allowToDrop,
|
||||
canActivate,
|
||||
draggedBlocks,
|
||||
edgeCompensatorValues,
|
||||
isCurrentDraggedCanvas,
|
||||
isDragging,
|
||||
layoutCompensatorValues,
|
||||
layoutElementPositions,
|
||||
} = anvilDragStates;
|
||||
const allHighlightsRef = useRef([] as AnvilHighlightInfo[]);
|
||||
const currentSelectedHighlight = useRef<AnvilHighlightInfo | null>(null);
|
||||
const dispatch = useDispatch();
|
||||
const { setDraggingCanvas } = useWidgetDragResize();
|
||||
const calculateHighlights = useCallback(() => {
|
||||
if (activateOverlayWidgetDrop) {
|
||||
allHighlightsRef.current = [];
|
||||
} else {
|
||||
allHighlightsRef.current = deriveAllHighlightsFn(
|
||||
layoutElementPositions,
|
||||
draggedBlocks,
|
||||
)?.highlights;
|
||||
}
|
||||
}, [
|
||||
activateOverlayWidgetDrop,
|
||||
deriveAllHighlightsFn,
|
||||
draggedBlocks,
|
||||
layoutElementPositions,
|
||||
]);
|
||||
const resetCanvasState = useCallback(() => {
|
||||
// Resetting the dnd listener state when necessary
|
||||
if (anvilDnDListenerRef.current) {
|
||||
removeDisallowDroppingsUI(anvilDnDListenerRef.current);
|
||||
canvasIsDragging.current = false;
|
||||
dispatch(setHighlightsDrawnAction());
|
||||
setHighlightShown(null);
|
||||
}
|
||||
}, [dispatch, setHighlightShown]);
|
||||
const onMouseUp = useCallback(() => {
|
||||
if (
|
||||
isDragging &&
|
||||
isCurrentDraggedCanvas &&
|
||||
canvasIsDragging.current &&
|
||||
currentSelectedHighlight.current &&
|
||||
!currentSelectedHighlight.current.existingPositionHighlight &&
|
||||
allowToDrop
|
||||
) {
|
||||
// Invoke onDrop callback with the appropriate highlight info
|
||||
onDrop(currentSelectedHighlight.current);
|
||||
}
|
||||
resetCanvasState();
|
||||
}, [
|
||||
allowToDrop,
|
||||
isDragging,
|
||||
isCurrentDraggedCanvas,
|
||||
onDrop,
|
||||
resetCanvasState,
|
||||
]);
|
||||
|
||||
const getHighlightCompensator = useCallback(
|
||||
(highlight: AnvilHighlightInfo) =>
|
||||
getPositionCompensatedHighlight(
|
||||
highlight,
|
||||
layoutCompensatorValues,
|
||||
edgeCompensatorValues,
|
||||
),
|
||||
[layoutCompensatorValues, edgeCompensatorValues],
|
||||
);
|
||||
// make sure rendering highlights on dnd listener and highlighting cell happens once every 50ms
|
||||
const throttledSetHighlight = useCallback(
|
||||
throttle(
|
||||
() => {
|
||||
if (
|
||||
canvasIsDragging.current &&
|
||||
isCurrentDraggedCanvas &&
|
||||
currentSelectedHighlight.current
|
||||
) {
|
||||
const compensatedHighlight = getHighlightCompensator(
|
||||
currentSelectedHighlight.current,
|
||||
);
|
||||
dispatch(setHighlightsDrawnAction(compensatedHighlight));
|
||||
setHighlightShown(compensatedHighlight);
|
||||
}
|
||||
},
|
||||
50,
|
||||
{
|
||||
leading: true,
|
||||
trailing: true,
|
||||
},
|
||||
),
|
||||
[
|
||||
dispatch,
|
||||
getHighlightCompensator,
|
||||
isCurrentDraggedCanvas,
|
||||
setHighlightShown,
|
||||
],
|
||||
);
|
||||
|
||||
const onMouseOver = useCallback(
|
||||
(e: any) => {
|
||||
if (canActivate) {
|
||||
setDraggingCanvas(layoutId);
|
||||
e.stopPropagation();
|
||||
}
|
||||
},
|
||||
[canActivate, layoutId, setDraggingCanvas],
|
||||
);
|
||||
|
||||
const checkForHighlights = useCallback(
|
||||
(e: MouseEvent) => {
|
||||
if (canvasIsDragging.current) {
|
||||
{
|
||||
if (anvilDnDListenerRef.current && !allowToDrop) {
|
||||
// Render disallow message if dropping is not allowed
|
||||
renderDisallowDroppingUI(anvilDnDListenerRef.current);
|
||||
return;
|
||||
}
|
||||
// Get the closest highlight based on the mouse position
|
||||
const processedHighlight = getClosestHighlight(
|
||||
{
|
||||
x: e.offsetX - layoutCompensatorValues.left,
|
||||
y: e.offsetY - layoutCompensatorValues.top,
|
||||
},
|
||||
allHighlightsRef.current,
|
||||
);
|
||||
if (processedHighlight) {
|
||||
currentSelectedHighlight.current = processedHighlight;
|
||||
throttledSetHighlight();
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
[allowToDrop, layoutCompensatorValues, throttledSetHighlight],
|
||||
);
|
||||
|
||||
const onMouseMove = useCallback(
|
||||
(e: any) => {
|
||||
if (!canActivate) {
|
||||
return;
|
||||
}
|
||||
if (isCurrentDraggedCanvas) {
|
||||
// dragging state is set and the canvas is already being used to drag
|
||||
if (canvasIsDragging.current) {
|
||||
checkForHighlights(e);
|
||||
} else {
|
||||
// first move after dragging state is set
|
||||
calculateHighlights();
|
||||
canvasIsDragging.current = true;
|
||||
requestAnimationFrame(() => onMouseMove(e));
|
||||
}
|
||||
} else {
|
||||
// first move to set the dragging state
|
||||
onMouseOver(e);
|
||||
}
|
||||
},
|
||||
[
|
||||
activateOverlayWidgetDrop,
|
||||
allowToDrop,
|
||||
calculateHighlights,
|
||||
canActivate,
|
||||
isCurrentDraggedCanvas,
|
||||
isDragging,
|
||||
layoutCompensatorValues,
|
||||
onMouseOver,
|
||||
throttledSetHighlight,
|
||||
],
|
||||
);
|
||||
|
||||
const onMouseOut = useCallback(() => {
|
||||
setDraggingCanvas("");
|
||||
}, [setDraggingCanvas]);
|
||||
return {
|
||||
onMouseMove,
|
||||
onMouseOver,
|
||||
onMouseOut,
|
||||
onMouseUp,
|
||||
resetCanvasState,
|
||||
};
|
||||
};
|
||||
|
|
@ -0,0 +1,111 @@
|
|||
import type React from "react";
|
||||
import { useEffect, useRef } from "react";
|
||||
import type { AnvilHighlightingCanvasProps } from "layoutSystems/anvil/editor/canvasArenas/AnvilHighlightingCanvas";
|
||||
import type { AnvilHighlightInfo } from "layoutSystems/anvil/utils/anvilTypes";
|
||||
import { useAnvilDnDEventCallbacks } from "./useAnvilDnDEventCallbacks";
|
||||
import { removeDisallowDroppingsUI } from "../utils/utils";
|
||||
|
||||
/**
|
||||
* Hook to handle Anvil DnD events
|
||||
*/
|
||||
export const useAnvilDnDEvents = (
|
||||
anvilDnDListenerRef: React.RefObject<HTMLDivElement>,
|
||||
props: AnvilHighlightingCanvasProps,
|
||||
setHighlightShown: (highlight: AnvilHighlightInfo | null) => void,
|
||||
) => {
|
||||
const { anvilDragStates, deriveAllHighlightsFn, layoutId, onDrop } = props;
|
||||
const {
|
||||
activateOverlayWidgetDrop,
|
||||
canActivate,
|
||||
isCurrentDraggedCanvas,
|
||||
isDragging,
|
||||
} = anvilDragStates;
|
||||
|
||||
/**
|
||||
* Ref to store highlights derived in real time once dragging starts
|
||||
*/
|
||||
const canvasIsDragging = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
// Effect to handle changes in isCurrentDraggedCanvas
|
||||
if (anvilDnDListenerRef.current) {
|
||||
if (!isCurrentDraggedCanvas) {
|
||||
removeDisallowDroppingsUI(anvilDnDListenerRef.current);
|
||||
canvasIsDragging.current = false;
|
||||
setHighlightShown(null);
|
||||
}
|
||||
}
|
||||
}, [isCurrentDraggedCanvas]);
|
||||
const { onMouseMove, onMouseOut, onMouseOver, onMouseUp, resetCanvasState } =
|
||||
useAnvilDnDEventCallbacks({
|
||||
anvilDragStates,
|
||||
anvilDnDListenerRef,
|
||||
canvasIsDragging,
|
||||
deriveAllHighlightsFn,
|
||||
layoutId,
|
||||
onDrop,
|
||||
setHighlightShown,
|
||||
});
|
||||
useEffect(() => {
|
||||
if (anvilDnDListenerRef.current && isDragging) {
|
||||
// Initialize listeners
|
||||
anvilDnDListenerRef.current?.addEventListener("mouseenter", onMouseOver);
|
||||
anvilDnDListenerRef.current.addEventListener("mouseover", onMouseOver);
|
||||
anvilDnDListenerRef.current.addEventListener("mouseleave", onMouseOut);
|
||||
anvilDnDListenerRef.current.addEventListener("mouseout", onMouseOut);
|
||||
anvilDnDListenerRef.current?.addEventListener(
|
||||
"mousemove",
|
||||
onMouseMove,
|
||||
false,
|
||||
);
|
||||
anvilDnDListenerRef.current?.addEventListener(
|
||||
"mouseup",
|
||||
onMouseUp,
|
||||
false,
|
||||
);
|
||||
// To make sure drops on the main canvas boundary buffer are processed in the capturing phase.
|
||||
document.addEventListener("mouseup", onMouseUp, true);
|
||||
|
||||
return () => {
|
||||
anvilDnDListenerRef.current?.removeEventListener(
|
||||
"mouseover",
|
||||
onMouseOver,
|
||||
);
|
||||
anvilDnDListenerRef.current?.removeEventListener(
|
||||
"mouseenter",
|
||||
onMouseOver,
|
||||
);
|
||||
anvilDnDListenerRef.current?.removeEventListener(
|
||||
"mouseleave",
|
||||
onMouseOut,
|
||||
);
|
||||
anvilDnDListenerRef.current?.removeEventListener(
|
||||
"mouseout",
|
||||
onMouseOut,
|
||||
);
|
||||
// Cleanup listeners on component unmount
|
||||
anvilDnDListenerRef.current?.removeEventListener(
|
||||
"mousemove",
|
||||
onMouseMove,
|
||||
);
|
||||
anvilDnDListenerRef.current?.removeEventListener("mouseup", onMouseUp);
|
||||
document.removeEventListener("mouseup", onMouseUp, true);
|
||||
};
|
||||
} else {
|
||||
canvasIsDragging.current = false;
|
||||
// Reset canvas state if not dragging
|
||||
resetCanvasState();
|
||||
}
|
||||
}, [
|
||||
isDragging,
|
||||
onMouseMove,
|
||||
onMouseOut,
|
||||
onMouseOver,
|
||||
onMouseUp,
|
||||
resetCanvasState,
|
||||
]);
|
||||
|
||||
return {
|
||||
showDnDListener: isDragging && !activateOverlayWidgetDrop && canActivate,
|
||||
};
|
||||
};
|
||||
|
|
@ -1,32 +1,31 @@
|
|||
import { MAIN_CONTAINER_WIDGET_ID } from "constants/WidgetConstants";
|
||||
import type { AppState } from "@appsmith/reducers";
|
||||
import { getDragDetails, getWidgets } from "sagas/selectors";
|
||||
import { useSelector } from "react-redux";
|
||||
import type { DragDetails } from "reducers/uiReducers/dragResizeReducer";
|
||||
import { useMemo } from "react";
|
||||
import { getSelectedWidgets } from "selectors/ui";
|
||||
import {
|
||||
type DraggedWidget,
|
||||
LayoutComponentTypes,
|
||||
} from "layoutSystems/anvil/utils/anvilTypes";
|
||||
import { getDropTargetLayoutId } from "layoutSystems/anvil/integrations/selectors";
|
||||
import type { CanvasWidgetsReduxState } from "reducers/entityReducers/canvasWidgetsReducer";
|
||||
import { getLayoutElementPositions } from "layoutSystems/common/selectors";
|
||||
import type { LayoutElementPositions } from "layoutSystems/common/types";
|
||||
import { areWidgetsWhitelisted } from "layoutSystems/anvil/utils/layouts/whitelistUtils";
|
||||
import { AnvilDropTargetTypesEnum, type AnvilDragMeta } from "../types";
|
||||
import { getDraggedBlocks, getDraggedWidgetTypes } from "./utils";
|
||||
import { canActivateCanvasForDraggedWidget } from "../utils/utils";
|
||||
import { useAnvilDnDCompensators } from "./useAnvilDnDCompensators";
|
||||
import { getWidgetHierarchy } from "layoutSystems/anvil/utils/paste/utils";
|
||||
import type { AnvilGlobalDnDStates } from "../../canvas/hooks/useAnvilGlobalDnDStates";
|
||||
import { getWidgets } from "sagas/selectors";
|
||||
|
||||
interface AnvilDnDStatesProps {
|
||||
interface AnvilDnDListenerStatesProps {
|
||||
anvilGlobalDragStates: AnvilGlobalDnDStates;
|
||||
allowedWidgetTypes: string[];
|
||||
canvasId: string;
|
||||
widgetId: string;
|
||||
layoutId: string;
|
||||
layoutType: LayoutComponentTypes;
|
||||
}
|
||||
|
||||
export interface AnvilDnDStates {
|
||||
export interface AnvilDnDListenerStates {
|
||||
activateOverlayWidgetDrop: boolean;
|
||||
allowToDrop: boolean;
|
||||
canActivate: boolean;
|
||||
draggedBlocks: DraggedWidget[];
|
||||
dragDetails: DragDetails;
|
||||
isCurrentDraggedCanvas: boolean;
|
||||
|
|
@ -35,6 +34,19 @@ export interface AnvilDnDStates {
|
|||
layoutElementPositions: LayoutElementPositions;
|
||||
dragMeta: AnvilDragMeta;
|
||||
mainCanvasLayoutId: string;
|
||||
widgetCompensatorValues: {
|
||||
left: number;
|
||||
top: number;
|
||||
};
|
||||
edgeCompensatorValues: {
|
||||
left: number;
|
||||
top: number;
|
||||
};
|
||||
layoutCompensatorValues: {
|
||||
left: number;
|
||||
top: number;
|
||||
};
|
||||
zIndex: number;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -63,35 +75,36 @@ const checkIfWidgetTypeDraggedIsAllowedToDrop = (
|
|||
return areWidgetsWhitelisted(draggedWidgetTypes, allowedWidgetTypes);
|
||||
};
|
||||
|
||||
export const useAnvilDnDStates = ({
|
||||
export const useAnvilDnDListenerStates = ({
|
||||
allowedWidgetTypes,
|
||||
anvilGlobalDragStates,
|
||||
layoutId,
|
||||
layoutType,
|
||||
}: AnvilDnDStatesProps): AnvilDnDStates => {
|
||||
const mainCanvasLayoutId: string = useSelector((state) =>
|
||||
getDropTargetLayoutId(state, MAIN_CONTAINER_WIDGET_ID),
|
||||
);
|
||||
const layoutElementPositions = useSelector(getLayoutElementPositions);
|
||||
widgetId,
|
||||
}: AnvilDnDListenerStatesProps): AnvilDnDListenerStates => {
|
||||
const {
|
||||
activateOverlayWidgetDrop,
|
||||
dragDetails,
|
||||
draggedBlocks,
|
||||
draggedWidgetHierarchy,
|
||||
draggedWidgetTypes,
|
||||
isDragging,
|
||||
isNewWidget,
|
||||
layoutElementPositions,
|
||||
mainCanvasLayoutId,
|
||||
} = anvilGlobalDragStates;
|
||||
const allWidgets = useSelector(getWidgets);
|
||||
const widgetProps = allWidgets[widgetId];
|
||||
const selectedWidgets = useSelector(getSelectedWidgets);
|
||||
// dragDetails contains of info needed for a container jump:
|
||||
// which parent the dragging widget belongs,
|
||||
// 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: DragDetails = useSelector(getDragDetails);
|
||||
const isDragging = useSelector(
|
||||
(state: AppState) => state.ui.widgetDragResize.isDragging,
|
||||
);
|
||||
|
||||
const { dragGroupActualParent: dragParent, newWidget } = dragDetails;
|
||||
/**
|
||||
* boolean to indicate if the widget being dragged is a new widget
|
||||
*/
|
||||
const isNewWidget = !!newWidget && !dragParent;
|
||||
/**
|
||||
* boolean to indicate if the widget is being dragged on this particular canvas.
|
||||
*/
|
||||
const currentWidgetHierarchy = getWidgetHierarchy(widgetProps.type, widgetId);
|
||||
const canActivate = canActivateCanvasForDraggedWidget(
|
||||
draggedWidgetHierarchy,
|
||||
widgetProps.widgetId,
|
||||
widgetProps.type,
|
||||
);
|
||||
const isCurrentDraggedCanvas = dragDetails.draggedOn === layoutId;
|
||||
/**
|
||||
* boolean to indicate if the widgets being dragged are all allowed to drop in this particular canvas.
|
||||
|
|
@ -106,40 +119,34 @@ export const useAnvilDnDStates = ({
|
|||
selectedWidgets,
|
||||
allWidgets,
|
||||
);
|
||||
// process drag blocks only once and per first render
|
||||
// this is by taking advantage of the fact that isNewWidget and dragDetails are unchanged states during the dragging action.
|
||||
const draggedBlocks = useMemo(
|
||||
() =>
|
||||
isDragging
|
||||
? getDraggedBlocks(
|
||||
isNewWidget,
|
||||
dragDetails,
|
||||
selectedWidgets,
|
||||
allWidgets,
|
||||
)
|
||||
: [],
|
||||
[isDragging, selectedWidgets],
|
||||
);
|
||||
/**
|
||||
* boolean that indicates if the widget being dragged in an overlay widget like the Modal widget.
|
||||
*/
|
||||
const activateOverlayWidgetDrop =
|
||||
isNewWidget && newWidget.detachFromLayout === true;
|
||||
const isMainCanvas: boolean = layoutId === mainCanvasLayoutId;
|
||||
const isSection: boolean = layoutType === LayoutComponentTypes.SECTION;
|
||||
const draggedWidgetTypes = useMemo(
|
||||
() => getDraggedWidgetTypes(draggedBlocks),
|
||||
[draggedBlocks],
|
||||
);
|
||||
const draggedOn = isMainCanvas
|
||||
? AnvilDropTargetTypesEnum.MAIN_CANVAS
|
||||
: isSection
|
||||
? AnvilDropTargetTypesEnum.SECTION
|
||||
: AnvilDropTargetTypesEnum.ZONE;
|
||||
const isEmptyLayout =
|
||||
(widgetProps.children || []).filter(
|
||||
(each) => !allWidgets[each].detachFromLayout,
|
||||
).length === 0;
|
||||
const {
|
||||
edgeCompensatorValues,
|
||||
layoutCompensatorValues,
|
||||
widgetCompensatorValues,
|
||||
zIndex,
|
||||
} = useAnvilDnDCompensators(
|
||||
canActivate,
|
||||
draggedWidgetHierarchy,
|
||||
currentWidgetHierarchy,
|
||||
isEmptyLayout,
|
||||
widgetProps,
|
||||
);
|
||||
|
||||
return {
|
||||
activateOverlayWidgetDrop,
|
||||
allowToDrop,
|
||||
canActivate,
|
||||
draggedBlocks,
|
||||
dragDetails,
|
||||
dragMeta: {
|
||||
|
|
@ -151,5 +158,9 @@ export const useAnvilDnDStates = ({
|
|||
isNewWidget,
|
||||
mainCanvasLayoutId,
|
||||
layoutElementPositions,
|
||||
widgetCompensatorValues,
|
||||
edgeCompensatorValues,
|
||||
layoutCompensatorValues,
|
||||
zIndex,
|
||||
};
|
||||
};
|
||||
|
|
@ -6,16 +6,21 @@ import {
|
|||
import type { AnvilHighlightInfo } from "layoutSystems/anvil/utils/anvilTypes";
|
||||
import { useCallback } from "react";
|
||||
import { useDispatch } from "react-redux";
|
||||
import type { AnvilDnDStates } from "./useAnvilDnDStates";
|
||||
import type { AnvilDnDListenerStates } from "./useAnvilDnDListenerStates";
|
||||
import { anvilWidgets } from "widgets/anvil/constants";
|
||||
|
||||
export const useAnvilWidgetDrop = (
|
||||
canvasId: string,
|
||||
anvilDragStates: AnvilDnDStates,
|
||||
anvilDragStates: AnvilDnDListenerStates,
|
||||
) => {
|
||||
const dispatch = useDispatch();
|
||||
const { dragDetails, dragMeta, isNewWidget, layoutElementPositions } =
|
||||
anvilDragStates;
|
||||
const {
|
||||
dragDetails,
|
||||
draggedBlocks,
|
||||
dragMeta,
|
||||
isNewWidget,
|
||||
layoutElementPositions,
|
||||
} = anvilDragStates;
|
||||
const generateNewWidgetBlock = useCallback(() => {
|
||||
const { newWidget } = dragDetails;
|
||||
const isSectionWidget = newWidget.type === anvilWidgets.SECTION_WIDGET;
|
||||
|
|
@ -36,17 +41,15 @@ export const useAnvilWidgetDrop = (
|
|||
addNewAnvilWidgetAction(newWidgetBlock, renderedBlock, dragMeta),
|
||||
);
|
||||
} else {
|
||||
const sortDraggedBlocksByPosition = anvilDragStates.draggedBlocks.sort(
|
||||
(a, b) => {
|
||||
const aPos = layoutElementPositions[a.widgetId];
|
||||
const bPos = layoutElementPositions[b.widgetId];
|
||||
// sort by left then top
|
||||
if (aPos.left === bPos.left) {
|
||||
return aPos.top - bPos.top;
|
||||
}
|
||||
return aPos.left - bPos.left;
|
||||
},
|
||||
);
|
||||
const sortDraggedBlocksByPosition = draggedBlocks.sort((a, b) => {
|
||||
const aPos = layoutElementPositions[a.widgetId];
|
||||
const bPos = layoutElementPositions[b.widgetId];
|
||||
// sort by left then top
|
||||
if (aPos.left === bPos.left) {
|
||||
return aPos.top - bPos.top;
|
||||
}
|
||||
return aPos.left - bPos.left;
|
||||
});
|
||||
dispatch(
|
||||
moveAnvilWidgets(renderedBlock, sortDraggedBlocksByPosition, dragMeta),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,356 +0,0 @@
|
|||
import type React from "react";
|
||||
import { useEffect, useRef } from "react";
|
||||
import type { AnvilHighlightingCanvasProps } from "layoutSystems/anvil/editor/canvasArenas/AnvilHighlightingCanvas";
|
||||
import { useCanvasDragToScroll } from "layoutSystems/common/canvasArenas/useCanvasDragToScroll";
|
||||
import type { AnvilHighlightInfo } from "layoutSystems/anvil/utils/anvilTypes";
|
||||
import { getAbsolutePixels } from "utils/helpers";
|
||||
import { getNearestParentCanvas } from "utils/generators";
|
||||
import { getClosestHighlight } from "./utils";
|
||||
import { AnvilCanvasZIndex } from "layoutSystems/anvil/editor/canvas/hooks/useCanvasActivation";
|
||||
import { AnvilReduxActionTypes } from "layoutSystems/anvil/integrations/actions/actionTypes";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { throttle } from "lodash";
|
||||
import { PADDING_FOR_HORIZONTAL_HIGHLIGHT } from "layoutSystems/anvil/utils/constants";
|
||||
import memoize from "micro-memoize";
|
||||
|
||||
const setHighlightsDrawn = (highlight?: AnvilHighlightInfo) => {
|
||||
return {
|
||||
type: AnvilReduxActionTypes.ANVIL_SET_HIGHLIGHT_SHOWN,
|
||||
payload: {
|
||||
highlight,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Function to render UX to denote that the widget type cannot be dropped in the layout
|
||||
*/
|
||||
const renderDisallowOnCanvas = (slidingArena: HTMLDivElement) => {
|
||||
slidingArena.style.backgroundColor = "#EB714D";
|
||||
slidingArena.style.color = "white";
|
||||
slidingArena.innerText = "This Layout doesn't support the widget";
|
||||
|
||||
slidingArena.style.textAlign = "center";
|
||||
slidingArena.style.opacity = "0.8";
|
||||
};
|
||||
|
||||
const getDropIndicatorColor = memoize(() => {
|
||||
const rootStyles = getComputedStyle(document.documentElement);
|
||||
return rootStyles.getPropertyValue("--anvil-drop-indicator");
|
||||
});
|
||||
|
||||
/**
|
||||
* Function to stroke a rectangle on the canvas that looks like a highlight/drop area.
|
||||
*/
|
||||
const renderBlocksOnCanvas = (
|
||||
stickyCanvas: HTMLCanvasElement,
|
||||
blockToRender: AnvilHighlightInfo,
|
||||
shouldDraw: boolean,
|
||||
) => {
|
||||
if (!shouldDraw) {
|
||||
return;
|
||||
}
|
||||
// Calculating offset based on the position of the canvas
|
||||
const topOffset = getAbsolutePixels(stickyCanvas.style.top);
|
||||
const leftOffset = getAbsolutePixels(stickyCanvas.style.left);
|
||||
const dropIndicatorColor = getDropIndicatorColor();
|
||||
const canvasCtx = stickyCanvas.getContext("2d") as CanvasRenderingContext2D;
|
||||
|
||||
// Clearing previous drawings on the canvas
|
||||
canvasCtx.clearRect(0, 0, stickyCanvas.width, stickyCanvas.height);
|
||||
canvasCtx.beginPath();
|
||||
// Extracting dimensions of the block to render
|
||||
const { height, posX, posY, width } = blockToRender;
|
||||
// using custom function to draw a rounded rectangle to achieve more sharper rounder corners
|
||||
const horizontalPadding = blockToRender.isVertical
|
||||
? 0
|
||||
: PADDING_FOR_HORIZONTAL_HIGHLIGHT;
|
||||
const verticalPadding = blockToRender.isVertical
|
||||
? PADDING_FOR_HORIZONTAL_HIGHLIGHT / 2
|
||||
: 0;
|
||||
canvasCtx.roundRect(
|
||||
posX - leftOffset + horizontalPadding,
|
||||
posY - topOffset + verticalPadding,
|
||||
width - horizontalPadding * 2,
|
||||
height - verticalPadding * 2,
|
||||
2,
|
||||
);
|
||||
canvasCtx.fillStyle = dropIndicatorColor;
|
||||
canvasCtx.fill();
|
||||
canvasCtx.closePath();
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* This hook is written to accumulate all logic that is needed to
|
||||
* - initialize event listeners for canvas
|
||||
* - adjust z-index of canvas
|
||||
* - track mouse position on canvas
|
||||
* - render highlights on the canvas
|
||||
* - render warning to denote that a particular widget type is not allowed to drop on canvas
|
||||
* - auto scroll canvas when needed.
|
||||
* - invoke onDrop callback as per the anvilDragStates
|
||||
*/
|
||||
export const useCanvasDragging = (
|
||||
slidingArenaRef: React.RefObject<HTMLDivElement>,
|
||||
stickyCanvasRef: React.RefObject<HTMLCanvasElement>,
|
||||
props: AnvilHighlightingCanvasProps,
|
||||
) => {
|
||||
const { anvilDragStates, deriveAllHighlightsFn, onDrop } = props;
|
||||
const {
|
||||
activateOverlayWidgetDrop,
|
||||
allowToDrop,
|
||||
draggedBlocks,
|
||||
isCurrentDraggedCanvas,
|
||||
isDragging,
|
||||
layoutElementPositions,
|
||||
mainCanvasLayoutId,
|
||||
} = anvilDragStates;
|
||||
const dispatch = useDispatch();
|
||||
/**
|
||||
* Provides auto-scroll functionality
|
||||
*/
|
||||
const canScroll = useCanvasDragToScroll(
|
||||
slidingArenaRef,
|
||||
isCurrentDraggedCanvas && !activateOverlayWidgetDrop,
|
||||
isDragging,
|
||||
);
|
||||
|
||||
/**
|
||||
* Ref to store highlights derived in real time once dragging starts
|
||||
*/
|
||||
const allHighlightsRef = useRef([] as AnvilHighlightInfo[]);
|
||||
|
||||
/**
|
||||
* Function to calculate and store highlights
|
||||
*/
|
||||
const calculateHighlights = () => {
|
||||
if (activateOverlayWidgetDrop) {
|
||||
allHighlightsRef.current = [];
|
||||
} else {
|
||||
allHighlightsRef.current = deriveAllHighlightsFn(
|
||||
layoutElementPositions,
|
||||
draggedBlocks,
|
||||
)?.highlights;
|
||||
}
|
||||
};
|
||||
const canvasIsDragging = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
// Effect to handle changes in isCurrentDraggedCanvas
|
||||
if (stickyCanvasRef.current && slidingArenaRef.current) {
|
||||
if (!isCurrentDraggedCanvas) {
|
||||
// If not currently dragged, reset the canvas and styles
|
||||
const canvasCtx = stickyCanvasRef.current.getContext(
|
||||
"2d",
|
||||
) as CanvasRenderingContext2D;
|
||||
canvasCtx.clearRect(
|
||||
0,
|
||||
0,
|
||||
stickyCanvasRef.current.width,
|
||||
stickyCanvasRef.current.height,
|
||||
);
|
||||
slidingArenaRef.current.style.zIndex = AnvilCanvasZIndex.deactivated;
|
||||
stickyCanvasRef.current.style.zIndex = AnvilCanvasZIndex.deactivated;
|
||||
slidingArenaRef.current.style.backgroundColor = "unset";
|
||||
slidingArenaRef.current.style.color = "unset";
|
||||
slidingArenaRef.current.innerText = "";
|
||||
canvasIsDragging.current = false;
|
||||
} else {
|
||||
// If currently dragged, set the z-index to activate the canvas
|
||||
slidingArenaRef.current.style.zIndex = AnvilCanvasZIndex.activated;
|
||||
stickyCanvasRef.current.style.zIndex = AnvilCanvasZIndex.activated;
|
||||
}
|
||||
}
|
||||
}, [isCurrentDraggedCanvas]);
|
||||
|
||||
useEffect(() => {
|
||||
if (slidingArenaRef.current && isDragging) {
|
||||
const scrollParent: Element | null = getNearestParentCanvas(
|
||||
slidingArenaRef.current,
|
||||
);
|
||||
|
||||
let currentRectanglesToDraw: AnvilHighlightInfo;
|
||||
const scrollObj: any = {};
|
||||
const resetCanvasState = () => {
|
||||
// Resetting the canvas state when necessary
|
||||
if (stickyCanvasRef.current && slidingArenaRef.current) {
|
||||
const canvasCtx = stickyCanvasRef.current.getContext(
|
||||
"2d",
|
||||
) as CanvasRenderingContext2D;
|
||||
canvasCtx.clearRect(
|
||||
0,
|
||||
0,
|
||||
stickyCanvasRef.current.width,
|
||||
stickyCanvasRef.current.height,
|
||||
);
|
||||
slidingArenaRef.current.style.zIndex = AnvilCanvasZIndex.deactivated;
|
||||
slidingArenaRef.current.style.backgroundColor = "unset";
|
||||
slidingArenaRef.current.style.color = "unset";
|
||||
slidingArenaRef.current.innerText = "";
|
||||
canvasIsDragging.current = false;
|
||||
dispatch(setHighlightsDrawn());
|
||||
}
|
||||
};
|
||||
|
||||
if (isDragging) {
|
||||
const onMouseUp = () => {
|
||||
if (
|
||||
isDragging &&
|
||||
canvasIsDragging.current &&
|
||||
currentRectanglesToDraw &&
|
||||
!currentRectanglesToDraw.existingPositionHighlight &&
|
||||
allowToDrop
|
||||
) {
|
||||
// Invoke onDrop callback with the appropriate highlight info
|
||||
onDrop(currentRectanglesToDraw);
|
||||
}
|
||||
resetCanvasState();
|
||||
};
|
||||
|
||||
const onFirstMoveOnCanvas = (e: MouseEvent) => {
|
||||
if (
|
||||
isCurrentDraggedCanvas &&
|
||||
isDragging &&
|
||||
!canvasIsDragging.current &&
|
||||
slidingArenaRef.current
|
||||
) {
|
||||
// Calculate highlights when the mouse enters the canvas
|
||||
calculateHighlights();
|
||||
canvasIsDragging.current = true;
|
||||
onMouseMove(e);
|
||||
}
|
||||
};
|
||||
// make sure rendering highlights on canvas and highlighting cell happens once every 50ms
|
||||
const throttledRenderOnCanvas = throttle(
|
||||
() => {
|
||||
if (
|
||||
stickyCanvasRef.current &&
|
||||
canvasIsDragging.current &&
|
||||
isCurrentDraggedCanvas
|
||||
) {
|
||||
dispatch(setHighlightsDrawn(currentRectanglesToDraw));
|
||||
// Render blocks on the canvas based on the highlight
|
||||
renderBlocksOnCanvas(
|
||||
stickyCanvasRef.current,
|
||||
currentRectanglesToDraw,
|
||||
canvasIsDragging.current,
|
||||
);
|
||||
}
|
||||
},
|
||||
50,
|
||||
{
|
||||
leading: true,
|
||||
trailing: true,
|
||||
},
|
||||
);
|
||||
|
||||
const onMouseMove = (e: any) => {
|
||||
if (
|
||||
isCurrentDraggedCanvas &&
|
||||
canvasIsDragging.current &&
|
||||
slidingArenaRef.current &&
|
||||
stickyCanvasRef.current
|
||||
) {
|
||||
if (!allowToDrop) {
|
||||
// Render disallow message if dropping is not allowed
|
||||
renderDisallowOnCanvas(slidingArenaRef.current);
|
||||
return;
|
||||
}
|
||||
// Get the closest highlight based on the mouse position
|
||||
const processedHighlight = getClosestHighlight(
|
||||
e,
|
||||
allHighlightsRef.current,
|
||||
);
|
||||
if (processedHighlight) {
|
||||
currentRectanglesToDraw = processedHighlight;
|
||||
throttledRenderOnCanvas();
|
||||
// Store information for auto-scroll functionality
|
||||
scrollObj.lastMouseMoveEvent = {
|
||||
offsetX: e.offsetX,
|
||||
offsetY: e.offsetY,
|
||||
};
|
||||
scrollObj.lastScrollTop = scrollParent?.scrollTop;
|
||||
scrollObj.lastScrollHeight = scrollParent?.scrollHeight;
|
||||
}
|
||||
} else {
|
||||
// Call onFirstMoveOnCanvas for the initial move on the canvas
|
||||
onFirstMoveOnCanvas(e);
|
||||
}
|
||||
};
|
||||
|
||||
// Adding setTimeout to make sure this gets called after
|
||||
// the onscroll that resets intersectionObserver in StickyCanvasArena.tsx
|
||||
const onScroll = () =>
|
||||
setTimeout(() => {
|
||||
const { lastMouseMoveEvent, lastScrollHeight, lastScrollTop } =
|
||||
scrollObj;
|
||||
if (
|
||||
lastMouseMoveEvent &&
|
||||
lastScrollHeight &&
|
||||
lastScrollTop &&
|
||||
scrollParent &&
|
||||
canScroll.current
|
||||
) {
|
||||
// Adjusting mouse position based on scrolling for auto-scroll
|
||||
const delta =
|
||||
scrollParent?.scrollHeight +
|
||||
scrollParent?.scrollTop -
|
||||
(lastScrollHeight + lastScrollTop);
|
||||
onMouseMove({
|
||||
offsetX: lastMouseMoveEvent.offsetX,
|
||||
offsetY: lastMouseMoveEvent.offsetY + delta,
|
||||
});
|
||||
}
|
||||
}, 0);
|
||||
|
||||
if (
|
||||
slidingArenaRef.current &&
|
||||
stickyCanvasRef.current &&
|
||||
scrollParent
|
||||
) {
|
||||
// Initialize listeners
|
||||
slidingArenaRef.current?.addEventListener(
|
||||
"mousemove",
|
||||
onMouseMove,
|
||||
false,
|
||||
);
|
||||
slidingArenaRef.current?.addEventListener(
|
||||
"mouseup",
|
||||
onMouseUp,
|
||||
false,
|
||||
);
|
||||
// To make sure drops on the main canvas boundary buffer are processed in the capturing phase.
|
||||
document.addEventListener("mouseup", onMouseUp, true);
|
||||
scrollParent?.addEventListener("scroll", onScroll, false);
|
||||
}
|
||||
|
||||
return () => {
|
||||
// Cleanup listeners on component unmount
|
||||
slidingArenaRef.current?.removeEventListener(
|
||||
"mousemove",
|
||||
onMouseMove,
|
||||
);
|
||||
slidingArenaRef.current?.removeEventListener("mouseup", onMouseUp);
|
||||
document.removeEventListener("mouseup", onMouseUp, true);
|
||||
scrollParent?.removeEventListener("scroll", onScroll);
|
||||
};
|
||||
} else {
|
||||
// Reset canvas state if not dragging
|
||||
resetCanvasState();
|
||||
}
|
||||
}
|
||||
}, [
|
||||
isDragging,
|
||||
allowToDrop,
|
||||
draggedBlocks,
|
||||
isCurrentDraggedCanvas,
|
||||
isDragging,
|
||||
layoutElementPositions,
|
||||
mainCanvasLayoutId,
|
||||
]);
|
||||
|
||||
return {
|
||||
showCanvas: isDragging && !activateOverlayWidgetDrop,
|
||||
};
|
||||
};
|
||||
|
|
@ -0,0 +1,325 @@
|
|||
import type { Token } from "@design-system/theming";
|
||||
import type { AnvilHighlightInfo } from "layoutSystems/anvil/utils/anvilTypes";
|
||||
import { HIGHLIGHT_SIZE } from "layoutSystems/anvil/utils/constants";
|
||||
import { EMPTY_MODAL_PADDING } from "../AnvilModalDropArena";
|
||||
|
||||
/**
|
||||
* DnD Compensation spacing tokens
|
||||
*
|
||||
* main canvas (Aligned Column layout component) using the value spacing-4 which is set via dsl transformer
|
||||
* section widget (WDS widget) - no tokens currently, however we extend the DnD layer on both sides of the section inorder to be able to show highlights and catch mouse movements.
|
||||
* zone widget spacing when elevated (WDS widget) - uses the --outer-spacing-3 value which is set on the widget from the container component.
|
||||
* modal component body top spacing (WDS component) - uses the --outer-spacing-2 value which is set on the WDS component
|
||||
* modal component body left spacing (WDS component) - uses the --outer-spacing-4 value which is set on the WDS component
|
||||
*
|
||||
* ToDo(#32983): These values are hardcoded here for now.
|
||||
*
|
||||
* Ideally they should be coming from a constant or from the entity it-selves as a property to the drag and drop layer.
|
||||
* But we have DnD rendering on the layout component and each of these entities are defining there spacing in different places.
|
||||
*/
|
||||
const CompensationSpacingTokens = {
|
||||
MAIN_CANVAS: "4",
|
||||
ZONE: "3",
|
||||
MODAL_TOP: "2",
|
||||
MODAL_LEFT: "4",
|
||||
};
|
||||
|
||||
const extractFloatValuesOutOfToken = (token: Token) => {
|
||||
if (token) {
|
||||
return parseFloat(token.value + "");
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get widget spacing CSS variable values
|
||||
*/
|
||||
const getWidgetSpacingCSSVariableValues = (outerSpacingTokens: {
|
||||
[key: string]: Token;
|
||||
}) => {
|
||||
return {
|
||||
mainCanvasSpacing: extractFloatValuesOutOfToken(
|
||||
outerSpacingTokens[CompensationSpacingTokens.MAIN_CANVAS],
|
||||
),
|
||||
modalSpacing: {
|
||||
top: extractFloatValuesOutOfToken(
|
||||
outerSpacingTokens[CompensationSpacingTokens.MODAL_TOP],
|
||||
),
|
||||
left: extractFloatValuesOutOfToken(
|
||||
outerSpacingTokens[CompensationSpacingTokens.MODAL_LEFT],
|
||||
),
|
||||
},
|
||||
zoneSpacing: extractFloatValuesOutOfToken(
|
||||
outerSpacingTokens[CompensationSpacingTokens.ZONE],
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Get compensators for the main canvas widget
|
||||
*/
|
||||
const getMainCanvasCompensators = (
|
||||
isEmptyLayout: boolean,
|
||||
mainCanvasSpacing: number,
|
||||
) => {
|
||||
const widgetCompensatorValues = {
|
||||
left: 0,
|
||||
top: 0,
|
||||
};
|
||||
const edgeCompensatorValues = {
|
||||
left: isEmptyLayout ? -mainCanvasSpacing : 0,
|
||||
top: isEmptyLayout ? -mainCanvasSpacing : mainCanvasSpacing,
|
||||
};
|
||||
const layoutCompensatorValues = {
|
||||
left: 0,
|
||||
top: 0,
|
||||
};
|
||||
return {
|
||||
widgetCompensatorValues,
|
||||
edgeCompensatorValues,
|
||||
layoutCompensatorValues,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Get compensators for the section widget
|
||||
*/
|
||||
const getSectionCompensators = (mainCanvasSpacing: number) => {
|
||||
const widgetCompensatorValues = {
|
||||
left: mainCanvasSpacing,
|
||||
top: 0,
|
||||
};
|
||||
const edgeCompensatorValues = {
|
||||
left: HIGHLIGHT_SIZE * 2,
|
||||
top: 0,
|
||||
};
|
||||
return {
|
||||
widgetCompensatorValues,
|
||||
edgeCompensatorValues,
|
||||
layoutCompensatorValues: widgetCompensatorValues,
|
||||
};
|
||||
};
|
||||
/**
|
||||
* Get compensators for the modal widget
|
||||
*/
|
||||
const getModalCompensators = (
|
||||
isEmptyLayout: boolean,
|
||||
modalSpacing: {
|
||||
top: number;
|
||||
left: number;
|
||||
},
|
||||
) => {
|
||||
const widgetCompensatorValues = {
|
||||
left: 0,
|
||||
top: isEmptyLayout ? 0 : modalSpacing.top,
|
||||
};
|
||||
const layoutCompensatorValues = {
|
||||
left: isEmptyLayout ? EMPTY_MODAL_PADDING : 0,
|
||||
top: isEmptyLayout ? EMPTY_MODAL_PADDING : modalSpacing.top,
|
||||
};
|
||||
return {
|
||||
widgetCompensatorValues,
|
||||
edgeCompensatorValues: widgetCompensatorValues,
|
||||
layoutCompensatorValues,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Get compensators for the zone widget
|
||||
*/
|
||||
const getZoneCompensators = (
|
||||
zoneSpacing: number,
|
||||
isElevatedWidget: boolean,
|
||||
) => {
|
||||
const widgetCompensatorValues = {
|
||||
left: 0,
|
||||
top: 0,
|
||||
};
|
||||
const edgeCompensatorValues = isElevatedWidget
|
||||
? {
|
||||
left: zoneSpacing,
|
||||
top: zoneSpacing,
|
||||
}
|
||||
: {
|
||||
left: HIGHLIGHT_SIZE / 2,
|
||||
top: HIGHLIGHT_SIZE / 2,
|
||||
};
|
||||
const layoutCompensatorValues = isElevatedWidget
|
||||
? {
|
||||
left: zoneSpacing,
|
||||
top: zoneSpacing,
|
||||
}
|
||||
: {
|
||||
left: 0,
|
||||
top: 0,
|
||||
};
|
||||
return {
|
||||
widgetCompensatorValues,
|
||||
edgeCompensatorValues,
|
||||
layoutCompensatorValues,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Get compensators for the widget based on the hierarchy
|
||||
*/
|
||||
export const getCompensatorsForHierarchy = (
|
||||
hierarchy: number,
|
||||
isEmptyLayout: boolean,
|
||||
isElevatedWidget: boolean,
|
||||
outerSpacingTokens:
|
||||
| {
|
||||
[key: string]: Token;
|
||||
}
|
||||
| undefined,
|
||||
) => {
|
||||
if (!outerSpacingTokens) {
|
||||
return {
|
||||
widgetCompensatorValues: {
|
||||
left: 0,
|
||||
top: 0,
|
||||
},
|
||||
edgeCompensatorValues: {
|
||||
left: 0,
|
||||
top: 0,
|
||||
},
|
||||
layoutCompensatorValues: {
|
||||
left: 0,
|
||||
top: 0,
|
||||
},
|
||||
};
|
||||
}
|
||||
const { mainCanvasSpacing, modalSpacing, zoneSpacing } =
|
||||
getWidgetSpacingCSSVariableValues(outerSpacingTokens);
|
||||
/**
|
||||
* Get compensators based on hierarchy
|
||||
* widgetCompensatorValues - compensates for the widget's additional dragging space outside widget and its layout ( Section Widget)
|
||||
* edgeCompensatorValues - compensates for the highlights at the edges of the layout of the widget (Zone Widget)
|
||||
* layoutCompensatorValues - compensates for the layout's additional dragging space inside widget (Modal Widget)
|
||||
*/
|
||||
switch (true) {
|
||||
case hierarchy === 0:
|
||||
return getMainCanvasCompensators(isEmptyLayout, mainCanvasSpacing);
|
||||
case hierarchy === 1:
|
||||
return getModalCompensators(isEmptyLayout, modalSpacing);
|
||||
case hierarchy === 2:
|
||||
return getSectionCompensators(mainCanvasSpacing);
|
||||
case hierarchy === 3:
|
||||
return getZoneCompensators(zoneSpacing, isElevatedWidget);
|
||||
default:
|
||||
return {
|
||||
widgetCompensatorValues: {
|
||||
left: 0,
|
||||
top: 0,
|
||||
},
|
||||
edgeCompensatorValues: {
|
||||
left: 0,
|
||||
top: 0,
|
||||
},
|
||||
layoutCompensatorValues: {
|
||||
left: 0,
|
||||
top: 0,
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculate the top offset based on the edge details
|
||||
*/
|
||||
const calculateEdgeTopOffset = (
|
||||
isVertical: boolean,
|
||||
isTopEdge: boolean,
|
||||
isBottomEdge: boolean,
|
||||
topGap: number,
|
||||
) => {
|
||||
return !isVertical ? (isTopEdge ? -topGap : isBottomEdge ? topGap : 0) : 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculate the left offset based on the edge details
|
||||
*/
|
||||
const calculateEdgeLeftOffset = (
|
||||
isVertical: boolean,
|
||||
isLeftEdge: boolean,
|
||||
isRightEdge: boolean,
|
||||
leftGap: number,
|
||||
) => {
|
||||
return isVertical ? (isLeftEdge ? -leftGap : isRightEdge ? leftGap : 0) : 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the edge compensating offset values
|
||||
*/
|
||||
const getEdgeCompensatingOffsetValues = (
|
||||
highlight: AnvilHighlightInfo,
|
||||
highlightCompensatorValues: {
|
||||
top: number;
|
||||
left: number;
|
||||
},
|
||||
) => {
|
||||
const {
|
||||
edgeDetails,
|
||||
height: highlightHeight,
|
||||
isVertical,
|
||||
width: highlightWidth,
|
||||
} = highlight;
|
||||
const compensatorTop = highlightCompensatorValues.top;
|
||||
const compensatorLeft = highlightCompensatorValues.left;
|
||||
const {
|
||||
bottom: isBottomEdge,
|
||||
left: isLeftEdge,
|
||||
right: isRightEdge,
|
||||
top: isTopEdge,
|
||||
} = edgeDetails;
|
||||
const topGap = (compensatorTop + highlightHeight) * 0.5;
|
||||
const leftGap = (compensatorLeft + highlightWidth) * 0.5;
|
||||
const topOffset = calculateEdgeTopOffset(
|
||||
isVertical,
|
||||
isTopEdge,
|
||||
isBottomEdge,
|
||||
topGap,
|
||||
);
|
||||
const leftOffset = calculateEdgeLeftOffset(
|
||||
isVertical,
|
||||
isLeftEdge,
|
||||
isRightEdge,
|
||||
leftGap,
|
||||
);
|
||||
return {
|
||||
topOffset,
|
||||
leftOffset,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the position compensated highlight
|
||||
*/
|
||||
export const getPositionCompensatedHighlight = (
|
||||
highlight: AnvilHighlightInfo,
|
||||
layoutCompensatorValues: {
|
||||
top: number;
|
||||
left: number;
|
||||
},
|
||||
edgeCompensatorValues: {
|
||||
top: number;
|
||||
left: number;
|
||||
},
|
||||
): AnvilHighlightInfo => {
|
||||
const layoutCompensatedHighlight = {
|
||||
...highlight,
|
||||
posX: highlight.posX + layoutCompensatorValues.left,
|
||||
posY: highlight.posY + layoutCompensatorValues.top,
|
||||
};
|
||||
const { posX: left, posY: top } = layoutCompensatedHighlight;
|
||||
const compensatingOffsetValues = getEdgeCompensatingOffsetValues(
|
||||
highlight,
|
||||
edgeCompensatorValues,
|
||||
);
|
||||
const positionUpdatedHighlightInfo = {
|
||||
...layoutCompensatedHighlight,
|
||||
posX: left + compensatingOffsetValues.leftOffset,
|
||||
posY: top + compensatingOffsetValues.topOffset,
|
||||
};
|
||||
return positionUpdatedHighlightInfo;
|
||||
};
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
export const resetAnvilDnDListener = (
|
||||
anvilDnDListener: HTMLDivElement | null,
|
||||
) => {
|
||||
if (anvilDnDListener) {
|
||||
anvilDnDListener.style.backgroundColor = "unset";
|
||||
anvilDnDListener.style.color = "unset";
|
||||
anvilDnDListener.innerText = "";
|
||||
}
|
||||
};
|
||||
|
|
@ -17,6 +17,12 @@ describe("Highlight selection algos", () => {
|
|||
canvasId: "canvasId",
|
||||
rowIndex: 0,
|
||||
layoutOrder: [],
|
||||
edgeDetails: {
|
||||
bottom: false,
|
||||
left: false,
|
||||
right: false,
|
||||
top: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
layoutId: "",
|
||||
|
|
@ -29,6 +35,12 @@ describe("Highlight selection algos", () => {
|
|||
canvasId: "canvasId",
|
||||
rowIndex: 1,
|
||||
layoutOrder: [],
|
||||
edgeDetails: {
|
||||
bottom: false,
|
||||
left: false,
|
||||
right: false,
|
||||
top: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
layoutId: "",
|
||||
|
|
@ -41,6 +53,12 @@ describe("Highlight selection algos", () => {
|
|||
canvasId: "canvasId",
|
||||
rowIndex: 0,
|
||||
layoutOrder: [],
|
||||
edgeDetails: {
|
||||
bottom: false,
|
||||
left: false,
|
||||
right: false,
|
||||
top: false,
|
||||
},
|
||||
},
|
||||
// Add other highlights as needed...
|
||||
];
|
||||
|
|
@ -122,16 +122,11 @@ export const getDraggedBlocks = (
|
|||
};
|
||||
|
||||
export const getClosestHighlight = (
|
||||
e: MouseEvent,
|
||||
pos: XYCord,
|
||||
highlights: AnvilHighlightInfo[],
|
||||
) => {
|
||||
if (!highlights || !highlights.length) return;
|
||||
|
||||
// Current mouse coordinates.
|
||||
const pos: XYCord = {
|
||||
x: e.offsetX,
|
||||
y: e.offsetY,
|
||||
};
|
||||
/**
|
||||
* Filter highlights that span the current mouse position.
|
||||
*/
|
||||
|
|
@ -330,3 +325,16 @@ function calculateDistance(a: AnvilHighlightInfo, b: XYCord): number {
|
|||
}
|
||||
return Math.hypot(distX, distY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to render UX to denote that the widget type cannot be dropped in the layout
|
||||
*/
|
||||
export const renderDisallowDroppingUI = (slidingArena: HTMLDivElement) => {
|
||||
slidingArena.classList.add("disallow-dropping");
|
||||
slidingArena.innerText = "This Layout doesn't support the widget";
|
||||
};
|
||||
|
||||
export const removeDisallowDroppingsUI = (slidingArena: HTMLDivElement) => {
|
||||
slidingArena.classList.remove("disallow-dropping");
|
||||
slidingArena.innerText = "";
|
||||
};
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
import type { AppState } from "@appsmith/reducers";
|
||||
import { getAnvilSpaceDistributionStatus } from "layoutSystems/anvil/integrations/selectors";
|
||||
import { useCallback, useEffect } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
|
|
@ -13,7 +14,9 @@ export const useAnvilWidgetHover = (
|
|||
const isFocused = useSelector(isCurrentWidgetFocused(widgetId));
|
||||
const isPreviewMode = useSelector(combinedPreviewModeSelector);
|
||||
const isDistributingSpace = useSelector(getAnvilSpaceDistributionStatus);
|
||||
|
||||
const isDragging = useSelector(
|
||||
(state: AppState) => state.ui.widgetDragResize.isDragging,
|
||||
);
|
||||
// Access the focusWidget function from the useWidgetSelection hook
|
||||
const { focusWidget } = useWidgetSelection();
|
||||
|
||||
|
|
@ -24,13 +27,21 @@ export const useAnvilWidgetHover = (
|
|||
focusWidget &&
|
||||
!isFocused &&
|
||||
!isDistributingSpace &&
|
||||
!isDragging &&
|
||||
!isPreviewMode &&
|
||||
focusWidget(widgetId);
|
||||
|
||||
// Prevent the event from propagating further
|
||||
e.stopPropagation();
|
||||
},
|
||||
[focusWidget, isFocused, isDistributingSpace, isPreviewMode, widgetId],
|
||||
[
|
||||
focusWidget,
|
||||
isFocused,
|
||||
isDistributingSpace,
|
||||
isPreviewMode,
|
||||
widgetId,
|
||||
isDragging,
|
||||
],
|
||||
);
|
||||
|
||||
// Callback function for handling mouseleave events
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { isWidgetSelected } from "selectors/widgetSelectors";
|
|||
import { useSelector } from "react-redux";
|
||||
import { useWidgetBorderStyles } from "layoutSystems/anvil/common/hooks/useWidgetBorderStyles";
|
||||
import type { AppState } from "@appsmith/reducers";
|
||||
import { getIsNewWidgetBeingDragged } from "sagas/selectors";
|
||||
|
||||
export const useAnvilWidgetStyles = (
|
||||
widgetId: string,
|
||||
|
|
@ -35,9 +36,10 @@ export const useAnvilWidgetStyles = (
|
|||
ref.current.setAttribute("data-testid", isSelected ? "t--selected" : "");
|
||||
}
|
||||
}, [widgetName, isSelected]);
|
||||
|
||||
const isNewWidgetDrag = useSelector(getIsNewWidgetBeingDragged);
|
||||
// Calculate whether the widget should fade based on dragging, selection, and visibility
|
||||
const shouldFadeWidget = (isDragging && isSelected) || !isVisible;
|
||||
const shouldFadeWidget =
|
||||
(isDragging && !isNewWidgetDrag && isSelected) || !isVisible;
|
||||
|
||||
// Calculate opacity factor based on whether the widget should fade
|
||||
const opacityFactor = useMemo(() => {
|
||||
|
|
|
|||
|
|
@ -10,6 +10,14 @@ import type {
|
|||
} from "./actionTypes";
|
||||
import { AnvilReduxActionTypes } from "./actionTypes";
|
||||
|
||||
export const setHighlightsDrawnAction = (highlight?: AnvilHighlightInfo) => {
|
||||
return {
|
||||
type: AnvilReduxActionTypes.ANVIL_SET_HIGHLIGHT_SHOWN,
|
||||
payload: {
|
||||
highlight,
|
||||
},
|
||||
};
|
||||
};
|
||||
/**
|
||||
* Add new anvil widget to canvas.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
import type { AppState } from "@appsmith/reducers";
|
||||
import { getWidgetIdsByType, getWidgetsMeta } from "sagas/selectors";
|
||||
import { WDSModalWidget } from "widgets/wds/WDSModalWidget";
|
||||
|
||||
export const getCurrentlyOpenAnvilModal = (state: AppState) => {
|
||||
const allExistingModals = getWidgetIdsByType(state, WDSModalWidget.type);
|
||||
if (allExistingModals.length === 0) {
|
||||
return;
|
||||
}
|
||||
const metaWidgets = getWidgetsMeta(state);
|
||||
const currentlyOpenModal = allExistingModals.find((modalId) => {
|
||||
const modal = metaWidgets[modalId];
|
||||
return modal && modal.isVisible;
|
||||
});
|
||||
return currentlyOpenModal;
|
||||
};
|
||||
|
|
@ -16,7 +16,7 @@ import {
|
|||
} from "../utils/layouts/layoutUtils";
|
||||
import { RenderModes } from "constants/WidgetConstants";
|
||||
import LayoutFactory from "./LayoutFactory";
|
||||
import { AnvilCanvasDraggingArena } from "../editor/canvasArenas/AnvilCanvasDraggingArena";
|
||||
import { AnvilDraggingArena } from "../editor/canvasArenas/AnvilDraggingArena";
|
||||
import { FlexLayout, type FlexLayoutProps } from "./components/FlexLayout";
|
||||
import { defaultHighlightPayload } from "../utils/constants";
|
||||
|
||||
|
|
@ -111,9 +111,8 @@ abstract class BaseLayoutComponent extends PureComponent<
|
|||
this.props;
|
||||
if (!isDropTarget) return null;
|
||||
return (
|
||||
<AnvilCanvasDraggingArena
|
||||
<AnvilDraggingArena
|
||||
allowedWidgetTypes={this.props.allowedWidgetTypes || []}
|
||||
canvasId={canvasId}
|
||||
deriveAllHighlightsFn={LayoutFactory.getDeriveHighlightsFn(layoutType)(
|
||||
this.props,
|
||||
canvasId,
|
||||
|
|
@ -122,6 +121,7 @@ abstract class BaseLayoutComponent extends PureComponent<
|
|||
)}
|
||||
layoutId={layoutId}
|
||||
layoutType={layoutType}
|
||||
widgetId={canvasId}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -130,11 +130,7 @@ abstract class BaseLayoutComponent extends PureComponent<
|
|||
static rendersWidgets: boolean = false;
|
||||
|
||||
render(): JSX.Element | null {
|
||||
return (
|
||||
<FlexLayout {...this.getFlexLayoutProps()}>
|
||||
{this.renderContent()}
|
||||
</FlexLayout>
|
||||
);
|
||||
return <>{this.renderContent()}</>;
|
||||
}
|
||||
|
||||
protected renderContent(): React.ReactNode {
|
||||
|
|
@ -146,14 +142,18 @@ abstract class BaseLayoutComponent extends PureComponent<
|
|||
renderEditMode(): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
{this.renderViewMode()}
|
||||
{this.renderDraggingArena()}
|
||||
{this.renderChildren()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
renderViewMode(): React.ReactNode {
|
||||
return <>{this.renderChildren()}</>;
|
||||
return (
|
||||
<FlexLayout {...this.getFlexLayoutProps()}>
|
||||
{this.renderChildren()}
|
||||
</FlexLayout>
|
||||
);
|
||||
}
|
||||
|
||||
renderChildren(): React.ReactNode {
|
||||
|
|
|
|||
|
|
@ -39,19 +39,28 @@ class Section extends WidgetRow {
|
|||
/>
|
||||
);
|
||||
}
|
||||
renderDraggingArena(): React.ReactNode {
|
||||
|
||||
renderEditMode(): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
{super.renderDraggingArena()}
|
||||
{this.renderSectionSpaceDistributor()}
|
||||
{this.renderDraggingArena()}
|
||||
{this.renderSpaceDistributedSection()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
render(): JSX.Element {
|
||||
renderSpaceDistributedSection(): JSX.Element {
|
||||
return (
|
||||
<SectionRow {...this.getFlexLayoutProps()}>
|
||||
{this.renderContent()}
|
||||
{this.renderSectionSpaceDistributor()}
|
||||
{super.renderChildren()}
|
||||
</SectionRow>
|
||||
);
|
||||
}
|
||||
|
||||
renderViewMode(): JSX.Element {
|
||||
return (
|
||||
<SectionRow {...this.getFlexLayoutProps()}>
|
||||
{super.renderChildren()}
|
||||
</SectionRow>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,10 +45,10 @@ class Zone extends AlignedLayoutColumn {
|
|||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
renderViewMode() {
|
||||
return (
|
||||
<ZoneColumn {...this.getFlexLayoutProps()}>
|
||||
{this.renderContent()}
|
||||
{this.renderChildren()}
|
||||
</ZoneColumn>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,12 +2,13 @@ import { getLayoutElementPositions } from "layoutSystems/common/selectors";
|
|||
import type { LayoutElementPosition } from "layoutSystems/common/types";
|
||||
import React, { useMemo } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import { previewModeSelector } from "selectors/editorSelectors";
|
||||
import { combinedPreviewModeSelector } from "selectors/editorSelectors";
|
||||
import type { WidgetLayoutProps } from "../utils/anvilTypes";
|
||||
import { getWidgetByID } from "sagas/selectors";
|
||||
import { getDefaultSpaceDistributed } from "./utils/spaceRedistributionSagaUtils";
|
||||
import { SpaceDistributionHandle } from "./SpaceDistributionHandle";
|
||||
import { getAnvilZoneBoundaryOffset } from "./utils/spaceDistributionEditorUtils";
|
||||
import { getWidgetSelectionBlock } from "selectors/ui";
|
||||
|
||||
interface SectionSpaceDistributorProps {
|
||||
sectionWidgetId: string;
|
||||
|
|
@ -110,7 +111,8 @@ export const SectionSpaceDistributor = (
|
|||
props: SectionSpaceDistributorProps,
|
||||
) => {
|
||||
const { zones } = props;
|
||||
const isPreviewMode = useSelector(previewModeSelector);
|
||||
const isPreviewMode = useSelector(combinedPreviewModeSelector);
|
||||
const isWidgetSelectionBlocked = useSelector(getWidgetSelectionBlock);
|
||||
const isDragging = useSelector(
|
||||
(state) => state.ui.widgetDragResize.isDragging,
|
||||
);
|
||||
|
|
@ -123,6 +125,7 @@ export const SectionSpaceDistributor = (
|
|||
const canRedistributeSpace =
|
||||
!isPreviewMode &&
|
||||
!isDragging &&
|
||||
!isWidgetSelectionBlocked &&
|
||||
allZonePositionsAreAvailable &&
|
||||
zones.length > 1;
|
||||
return canRedistributeSpace ? (
|
||||
|
|
|
|||
|
|
@ -112,6 +112,12 @@ export interface HighlightRenderInfo {
|
|||
width: number; // width of the highlight.
|
||||
posX: number; // x position of the highlight.
|
||||
posY: number; // y position of the highlight.
|
||||
edgeDetails: {
|
||||
top: boolean; // Whether the highlight is at the top edge of the layout.
|
||||
bottom: boolean; // Whether the highlight is at the bottom edge of the layout.
|
||||
left: boolean; // Whether the highlight is at the left edge of the layout.
|
||||
right: boolean; // Whether the highlight is at the right edge of the layout.
|
||||
};
|
||||
}
|
||||
|
||||
export interface HighlightDropInfo {
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ import { anvilWidgets } from "widgets/anvil/constants";
|
|||
|
||||
export const MOBILE_BREAKPOINT = 480;
|
||||
|
||||
export const HIGHLIGHT_SIZE = 4;
|
||||
export const PADDING_FOR_HORIZONTAL_HIGHLIGHT = 4;
|
||||
export const HIGHLIGHT_SIZE = 2;
|
||||
export const PADDING_FOR_HORIZONTAL_HIGHLIGHT = 2;
|
||||
export const DEFAULT_VERTICAL_HIGHLIGHT_HEIGHT = 60;
|
||||
export const AlignmentIndexMap: { [key: string]: number } = {
|
||||
[FlexLayerAlignment.Start]: 0,
|
||||
|
|
@ -24,6 +24,12 @@ export const defaultHighlightRenderInfo: HighlightRenderInfo = {
|
|||
posX: 0,
|
||||
posY: 0,
|
||||
width: 0,
|
||||
edgeDetails: {
|
||||
bottom: false,
|
||||
left: false,
|
||||
right: false,
|
||||
top: false,
|
||||
},
|
||||
};
|
||||
|
||||
// Constants for the minimum and maximum zone count
|
||||
|
|
|
|||
|
|
@ -53,6 +53,12 @@ export const deriveAlignedColumnHighlights =
|
|||
posY: HIGHLIGHT_SIZE / 2,
|
||||
rowIndex: 0,
|
||||
width: 0,
|
||||
edgeDetails: {
|
||||
bottom: false,
|
||||
left: false,
|
||||
right: false,
|
||||
top: false,
|
||||
},
|
||||
};
|
||||
|
||||
const hasFillWidget: boolean = draggedWidgets.some(
|
||||
|
|
|
|||
|
|
@ -66,6 +66,12 @@ export const deriveAlignedRowHighlights =
|
|||
posY: HIGHLIGHT_SIZE / 2,
|
||||
rowIndex: 0,
|
||||
width: HIGHLIGHT_SIZE,
|
||||
edgeDetails: {
|
||||
bottom: false,
|
||||
left: false,
|
||||
right: false,
|
||||
top: false,
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -376,15 +382,24 @@ function generateHighlight(
|
|||
layoutDimension.left,
|
||||
);
|
||||
}
|
||||
|
||||
const posY = tallestWidget ? tallestWidget.top : layoutDimension.top;
|
||||
const edgeDetails = {
|
||||
top: posY === layoutDimension.top,
|
||||
bottom:
|
||||
posY + HIGHLIGHT_SIZE === layoutDimension.top + layoutDimension.height,
|
||||
left: posX === layoutDimension.left,
|
||||
right:
|
||||
posX + HIGHLIGHT_SIZE === layoutDimension.left + layoutDimension.width,
|
||||
};
|
||||
return {
|
||||
...baseHighlight,
|
||||
layoutId,
|
||||
alignment,
|
||||
height: tallestWidget?.height ?? layoutDimension.height,
|
||||
posX,
|
||||
posY: tallestWidget ? tallestWidget?.top : layoutDimension.top,
|
||||
posY,
|
||||
rowIndex: childCount,
|
||||
edgeDetails,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -50,6 +50,12 @@ export const deriveColumnHighlights =
|
|||
posY: HIGHLIGHT_SIZE / 2,
|
||||
rowIndex: 0,
|
||||
width: 0,
|
||||
edgeDetails: {
|
||||
bottom: false,
|
||||
left: false,
|
||||
right: false,
|
||||
top: false,
|
||||
},
|
||||
};
|
||||
|
||||
return deriveHighlights(
|
||||
|
|
|
|||
|
|
@ -426,7 +426,6 @@ export function generateHighlights(
|
|||
const width: number = layoutDimension.width / arr.length;
|
||||
|
||||
const isFirstHighlight: boolean = rowIndex === 0;
|
||||
|
||||
let posY = 0;
|
||||
const emptyLayout = isFirstHighlight && isLastHighlight;
|
||||
let gap = 0;
|
||||
|
|
@ -468,6 +467,12 @@ export function generateHighlights(
|
|||
posY,
|
||||
rowIndex,
|
||||
width,
|
||||
edgeDetails: {
|
||||
top: isFirstHighlight,
|
||||
bottom: isLastHighlight,
|
||||
left: width * index === 0,
|
||||
right: width * (index + 1) === layoutDimension.width,
|
||||
},
|
||||
...(isCurrentLayoutEmpty && !hasFillWidget
|
||||
? {
|
||||
isVertical: true,
|
||||
|
|
|
|||
|
|
@ -295,6 +295,12 @@ describe("rowHighlights tests", () => {
|
|||
posY: 0,
|
||||
rowIndex: 0,
|
||||
width: HIGHLIGHT_SIZE,
|
||||
edgeDetails: {
|
||||
bottom: false,
|
||||
left: false,
|
||||
right: false,
|
||||
top: false,
|
||||
},
|
||||
};
|
||||
it("should derive highlights for a row", () => {
|
||||
const data: WidgetLayoutProps[] = [
|
||||
|
|
|
|||
|
|
@ -83,6 +83,12 @@ export const deriveRowHighlights =
|
|||
posY: HIGHLIGHT_SIZE / 2,
|
||||
rowIndex: 0,
|
||||
width: HIGHLIGHT_SIZE,
|
||||
edgeDetails: {
|
||||
top: false,
|
||||
bottom: false,
|
||||
left: false,
|
||||
right: false,
|
||||
},
|
||||
};
|
||||
|
||||
// If layout is empty, add an initial highlight.
|
||||
|
|
@ -520,13 +526,20 @@ export function generateHighlights(
|
|||
layoutDimension.left,
|
||||
);
|
||||
}
|
||||
|
||||
const posY = tallestDimension?.top ?? layoutDimension.top;
|
||||
return {
|
||||
...baseHighlight,
|
||||
height: tallestDimension?.height ?? layoutDimension.height,
|
||||
posX,
|
||||
posY: tallestDimension?.top ?? layoutDimension.top,
|
||||
posY,
|
||||
rowIndex,
|
||||
edgeDetails: {
|
||||
top: posY === layoutDimension.top,
|
||||
bottom: posY === layoutDimension.top + layoutDimension.height,
|
||||
left: posX === layoutDimension.left,
|
||||
right:
|
||||
posX + HIGHLIGHT_SIZE === layoutDimension.left + layoutDimension.width,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -36,6 +36,12 @@ export function mockAnvilHighlightInfo(
|
|||
posX: 0,
|
||||
posY: 0,
|
||||
width: 4,
|
||||
edgeDetails: {
|
||||
bottom: false,
|
||||
left: false,
|
||||
right: false,
|
||||
top: false,
|
||||
},
|
||||
...data,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -191,6 +191,15 @@ export const getPluginIdOfPackageName = (
|
|||
export const getDragDetails = (state: AppState) => {
|
||||
return state.ui.widgetDragResize.dragDetails;
|
||||
};
|
||||
|
||||
export const getIsNewWidgetBeingDragged = (state: AppState) => {
|
||||
const { isDragging } = state.ui.widgetDragResize;
|
||||
if (!isDragging) return false;
|
||||
const dragDetails: DragDetails = getDragDetails(state);
|
||||
const { dragGroupActualParent: dragParent, newWidget } = dragDetails;
|
||||
return !!newWidget && !dragParent;
|
||||
};
|
||||
|
||||
export const isCurrentCanvasDragging = createSelector(
|
||||
(state: AppState) => state.ui.widgetDragResize.isDragging,
|
||||
getDragDetails,
|
||||
|
|
|
|||
|
|
@ -122,7 +122,7 @@ class WDSModalWidget extends BaseWidget<ModalWidgetProps, WidgetState> {
|
|||
? this.props.submitButtonText || "Submit"
|
||||
: undefined;
|
||||
const contentClassName = `${this.props.className} ${
|
||||
this.props.allowWidgetInteraction ? styles.disableModalInteraction : ""
|
||||
this.props.allowWidgetInteraction ? "" : styles.disableModalInteraction
|
||||
}`;
|
||||
return (
|
||||
<Modal
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
.disableModalInteraction {
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
& > * {
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user