feat: Anvil DnD Polish(Refactor) (#32839)

[![workerB](https://img.shields.io/endpoint?url=https%3A%2F%2Fworkerb.linearb.io%2Fv2%2Fbadge%2Fprivate%2FU2FsdGVkX18VSEOqElAtQ747ag1P1M0EAgrsJtMy4pQ%2Fcollaboration.svg%3FcacheSeconds%3D60)](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:
Ashok Kumar M 2024-04-29 11:32:08 +05:30 committed by GitHub
parent 4f70c70701
commit 4b1ef98014
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
46 changed files with 1397 additions and 865 deletions

View File

@ -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";

View File

@ -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) {

View File

@ -178,6 +178,7 @@ export const WIDGET_DSL_STRUCTURE_PROPS = {
topRow: true,
type: true,
widgetId: true,
layout: true,
};
export type TextSize = keyof typeof TextSizes;

View File

@ -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)
);
}
}

View File

@ -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>;
};

View File

@ -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>
);
};

View File

@ -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]);
};

View File

@ -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,
};
};

View File

@ -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,
]);
};

View File

@ -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;
};

View File

@ -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
};

View File

@ -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}
/>
);
},
);

View File

@ -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;
};

View File

@ -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;
}

View File

@ -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<{

View File

@ -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

View File

@ -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,
};
};

View File

@ -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,
};
};

View File

@ -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,
};
};

View File

@ -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,
};
};

View File

@ -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),
);

View File

@ -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,
};
};

View File

@ -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;
};

View File

@ -0,0 +1,9 @@
export const resetAnvilDnDListener = (
anvilDnDListener: HTMLDivElement | null,
) => {
if (anvilDnDListener) {
anvilDnDListener.style.backgroundColor = "unset";
anvilDnDListener.style.color = "unset";
anvilDnDListener.innerText = "";
}
};

View File

@ -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...
];

View File

@ -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 = "";
};

View File

@ -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

View File

@ -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(() => {

View File

@ -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.
*/

View File

@ -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;
};

View File

@ -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 {

View File

@ -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>
);
}

View File

@ -45,10 +45,10 @@ class Zone extends AlignedLayoutColumn {
};
}
render() {
renderViewMode() {
return (
<ZoneColumn {...this.getFlexLayoutProps()}>
{this.renderContent()}
{this.renderChildren()}
</ZoneColumn>
);
}

View File

@ -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 ? (

View File

@ -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 {

View File

@ -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

View File

@ -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(

View File

@ -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,
};
}

View File

@ -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(

View File

@ -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,

View File

@ -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[] = [

View File

@ -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,
},
};
}

View File

@ -36,6 +36,12 @@ export function mockAnvilHighlightInfo(
posX: 0,
posY: 0,
width: 4,
edgeDetails: {
bottom: false,
left: false,
right: false,
top: false,
},
...data,
};
}

View File

@ -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,

View File

@ -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

View File

@ -1,4 +1,6 @@
.disableModalInteraction {
user-select: none;
pointer-events: none;
& > * {
pointer-events: none;
}
}