diff --git a/app/client/cypress/support/Objects/CommonLocators.ts b/app/client/cypress/support/Objects/CommonLocators.ts index f43e9febed..66ad6e752b 100644 --- a/app/client/cypress/support/Objects/CommonLocators.ts +++ b/app/client/cypress/support/Objects/CommonLocators.ts @@ -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"; diff --git a/app/client/cypress/support/Pages/AnvilLayout.ts b/app/client/cypress/support/Pages/AnvilLayout.ts index 9940822a8b..3666678480 100644 --- a/app/client/cypress/support/Pages/AnvilLayout.ts +++ b/app/client/cypress/support/Pages/AnvilLayout.ts @@ -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) { diff --git a/app/client/src/constants/WidgetConstants.tsx b/app/client/src/constants/WidgetConstants.tsx index 89ee7deda6..3fc5d66d48 100644 --- a/app/client/src/constants/WidgetConstants.tsx +++ b/app/client/src/constants/WidgetConstants.tsx @@ -178,6 +178,7 @@ export const WIDGET_DSL_STRUCTURE_PROPS = { topRow: true, type: true, widgetId: true, + layout: true, }; export type TextSize = keyof typeof TextSizes; diff --git a/app/client/src/layoutSystems/anvil/common/widgetComponent/AnvilErrorBoundary.tsx b/app/client/src/layoutSystems/anvil/common/widgetComponent/AnvilErrorBoundary.tsx new file mode 100644 index 0000000000..423602ecb6 --- /dev/null +++ b/app/client/src/layoutSystems/anvil/common/widgetComponent/AnvilErrorBoundary.tsx @@ -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 ? ( +

+ Something went wrong. +
+ this.setState({ hasError: false })}> + Click here to retry + +

+ ) : ( + (this.props.children as any) + ); + } +} diff --git a/app/client/src/layoutSystems/anvil/common/widgetComponent/AnvilWidgetComponent.tsx b/app/client/src/layoutSystems/anvil/common/widgetComponent/AnvilWidgetComponent.tsx index 7bb702df19..281694998a 100644 --- a/app/client/src/layoutSystems/anvil/common/widgetComponent/AnvilWidgetComponent.tsx +++ b/app/client/src/layoutSystems/anvil/common/widgetComponent/AnvilWidgetComponent.tsx @@ -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 ; } - if (!detachFromLayout) return props.children; - - return ( - // delete style as soon as we switch to Anvil layout completely - - - {props.children} - - - ); + return {children}; }; diff --git a/app/client/src/layoutSystems/anvil/editor/canvas/AnvilEditorCanvas.tsx b/app/client/src/layoutSystems/anvil/editor/canvas/AnvilEditorCanvas.tsx index 4109def1ce..88d10b4f33 100644 --- a/app/client/src/layoutSystems/anvil/editor/canvas/AnvilEditorCanvas.tsx +++ b/app/client/src/layoutSystems/anvil/editor/canvas/AnvilEditorCanvas.tsx @@ -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 ; + // 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 ( + + + + ); }; diff --git a/app/client/src/layoutSystems/anvil/editor/canvas/hooks/useAnvilDnDDeactivation.ts b/app/client/src/layoutSystems/anvil/editor/canvas/hooks/useAnvilDnDDeactivation.ts new file mode 100644 index 0000000000..9c764f0eea --- /dev/null +++ b/app/client/src/layoutSystems/anvil/editor/canvas/hooks/useAnvilDnDDeactivation.ts @@ -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]); +}; diff --git a/app/client/src/layoutSystems/anvil/editor/canvasArenas/hooks/useCanvasActivationStates.ts b/app/client/src/layoutSystems/anvil/editor/canvas/hooks/useAnvilGlobalDnDStates.ts similarity index 62% rename from app/client/src/layoutSystems/anvil/editor/canvasArenas/hooks/useCanvasActivationStates.ts rename to app/client/src/layoutSystems/anvil/editor/canvas/hooks/useAnvilGlobalDnDStates.ts index d50d011d52..fa606273c5 100644 --- a/app/client/src/layoutSystems/anvil/editor/canvasArenas/hooks/useCanvasActivationStates.ts +++ b/app/client/src/layoutSystems/anvil/editor/canvas/hooks/useAnvilGlobalDnDStates.ts @@ -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, }; }; diff --git a/app/client/src/layoutSystems/anvil/editor/canvas/hooks/useCanvasActivation.ts b/app/client/src/layoutSystems/anvil/editor/canvas/hooks/useCanvasActivation.ts deleted file mode 100644 index 8983681214..0000000000 --- a/app/client/src/layoutSystems/anvil/editor/canvas/hooks/useCanvasActivation.ts +++ /dev/null @@ -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, - ]); -}; diff --git a/app/client/src/layoutSystems/anvil/editor/canvasArenas/AnvilCanvasDraggingArena.tsx b/app/client/src/layoutSystems/anvil/editor/canvasArenas/AnvilCanvasDraggingArena.tsx deleted file mode 100644 index 86a6bdfd9d..0000000000 --- a/app/client/src/layoutSystems/anvil/editor/canvasArenas/AnvilCanvasDraggingArena.tsx +++ /dev/null @@ -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 ? ( - <> - - {isMainCanvasDropArena && ( - - )} - - ) : null; -}; diff --git a/app/client/src/layoutSystems/anvil/editor/canvasArenas/AnvilDnDHighlight.tsx b/app/client/src/layoutSystems/anvil/editor/canvasArenas/AnvilDnDHighlight.tsx new file mode 100644 index 0000000000..ed5945e5af --- /dev/null +++ b/app/client/src/layoutSystems/anvil/editor/canvasArenas/AnvilDnDHighlight.tsx @@ -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 ? ( + + ) : null; // Otherwise, return null +}; diff --git a/app/client/src/layoutSystems/anvil/editor/canvasArenas/AnvilDnDListener.tsx b/app/client/src/layoutSystems/anvil/editor/canvasArenas/AnvilDnDListener.tsx new file mode 100644 index 0000000000..33fff138f6 --- /dev/null +++ b/app/client/src/layoutSystems/anvil/editor/canvasArenas/AnvilDnDListener.tsx @@ -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; + 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) => { + // Refer to useAnvilDnDCompensators to understand zIndex and compensatorValues + const { compensatorValues, zIndex } = props; + + return ( + + ); + }, +); diff --git a/app/client/src/layoutSystems/anvil/editor/canvasArenas/AnvilDraggingArena.tsx b/app/client/src/layoutSystems/anvil/editor/canvasArenas/AnvilDraggingArena.tsx new file mode 100644 index 0000000000..a6eae2e263 --- /dev/null +++ b/app/client/src/layoutSystems/anvil/editor/canvasArenas/AnvilDraggingArena.tsx @@ -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 ? ( + <> + + {isMainCanvasDropArena && ( + + )} + + ) : null; +}; + +/** + * AnvilDraggingArena is a wrapper component for AnvilHighlightingCanvas and DetachedWidgetsDropArena. + */ +export const AnvilDraggingArena = (props: AnvilCanvasDraggingArenaProps) => { + const anvilGlobalDragStates = useContext(AnvilDnDStatesContext); + return anvilGlobalDragStates ? ( + + ) : null; +}; diff --git a/app/client/src/layoutSystems/anvil/editor/canvasArenas/AnvilHighlightingCanvas.tsx b/app/client/src/layoutSystems/anvil/editor/canvasArenas/AnvilHighlightingCanvas.tsx index 12e590fbf2..53b88eff56 100644 --- a/app/client/src/layoutSystems/anvil/editor/canvasArenas/AnvilHighlightingCanvas.tsx +++ b/app/client/src/layoutSystems/anvil/editor/canvasArenas/AnvilHighlightingCanvas.tsx @@ -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(null); - const stickyCanvasRef = React.useRef(null); - // showDraggingCanvas indicates if the current dragging canvas i.e. the html canvas renders - const { showCanvas: showDraggingCanvas } = useCanvasDragging( - slidingArenaRef, - stickyCanvasRef, + const anvilDnDListenerRef = React.useRef(null); + const [highlightShown, setHighlightShown] = + React.useState(null); + + const { isCurrentDraggedCanvas } = anvilDragStates; + const { showDnDListener } = useAnvilDnDEvents( + anvilDnDListenerRef, { anvilDragStates, + widgetId, deriveAllHighlightsFn, layoutId, onDrop, }, + setHighlightShown, ); - const canvasRef = React.useRef({ - stickyCanvasRef, - slidingArenaRef, - }); - return showDraggingCanvas ? ( - + return showDnDListener ? ( + <> + {isCurrentDraggedCanvas && ( + + )} + + ) : null; } diff --git a/app/client/src/layoutSystems/anvil/editor/canvasArenas/AnvilModalDropArena.tsx b/app/client/src/layoutSystems/anvil/editor/canvasArenas/AnvilModalDropArena.tsx index 21c500a5c7..b7e8742c5a 100644 --- a/app/client/src/layoutSystems/anvil/editor/canvasArenas/AnvilModalDropArena.tsx +++ b/app/client/src/layoutSystems/anvil/editor/canvasArenas/AnvilModalDropArena.tsx @@ -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<{ diff --git a/app/client/src/layoutSystems/anvil/editor/canvasArenas/DetachedWidgetsDropArena.tsx b/app/client/src/layoutSystems/anvil/editor/canvasArenas/DetachedWidgetsDropArena.tsx index e4bc393251..e3d5031695 100644 --- a/app/client/src/layoutSystems/anvil/editor/canvasArenas/DetachedWidgetsDropArena.tsx +++ b/app/client/src/layoutSystems/anvil/editor/canvasArenas/DetachedWidgetsDropArena.tsx @@ -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 ? ( { + 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, + }; +}; diff --git a/app/client/src/layoutSystems/anvil/editor/canvasArenas/hooks/useAnvilDnDEventCallbacks.ts b/app/client/src/layoutSystems/anvil/editor/canvasArenas/hooks/useAnvilDnDEventCallbacks.ts new file mode 100644 index 0000000000..21ad6707e6 --- /dev/null +++ b/app/client/src/layoutSystems/anvil/editor/canvasArenas/hooks/useAnvilDnDEventCallbacks.ts @@ -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; + canvasIsDragging: React.MutableRefObject; + 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(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, + }; +}; diff --git a/app/client/src/layoutSystems/anvil/editor/canvasArenas/hooks/useAnvilDnDEvents.ts b/app/client/src/layoutSystems/anvil/editor/canvasArenas/hooks/useAnvilDnDEvents.ts new file mode 100644 index 0000000000..e6d47cd6bd --- /dev/null +++ b/app/client/src/layoutSystems/anvil/editor/canvasArenas/hooks/useAnvilDnDEvents.ts @@ -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, + 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, + }; +}; diff --git a/app/client/src/layoutSystems/anvil/editor/canvasArenas/hooks/useAnvilDnDStates.ts b/app/client/src/layoutSystems/anvil/editor/canvasArenas/hooks/useAnvilDnDListenerStates.ts similarity index 58% rename from app/client/src/layoutSystems/anvil/editor/canvasArenas/hooks/useAnvilDnDStates.ts rename to app/client/src/layoutSystems/anvil/editor/canvasArenas/hooks/useAnvilDnDListenerStates.ts index 4b221107da..ae6199cbfe 100644 --- a/app/client/src/layoutSystems/anvil/editor/canvasArenas/hooks/useAnvilDnDStates.ts +++ b/app/client/src/layoutSystems/anvil/editor/canvasArenas/hooks/useAnvilDnDListenerStates.ts @@ -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, }; }; diff --git a/app/client/src/layoutSystems/anvil/editor/canvasArenas/hooks/useAnvilWidgetDrop.ts b/app/client/src/layoutSystems/anvil/editor/canvasArenas/hooks/useAnvilWidgetDrop.ts index 806e29dc97..46cf0d2a6f 100644 --- a/app/client/src/layoutSystems/anvil/editor/canvasArenas/hooks/useAnvilWidgetDrop.ts +++ b/app/client/src/layoutSystems/anvil/editor/canvasArenas/hooks/useAnvilWidgetDrop.ts @@ -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), ); diff --git a/app/client/src/layoutSystems/anvil/editor/canvasArenas/hooks/useCanvasDragging.ts b/app/client/src/layoutSystems/anvil/editor/canvasArenas/hooks/useCanvasDragging.ts deleted file mode 100644 index 50f553c432..0000000000 --- a/app/client/src/layoutSystems/anvil/editor/canvasArenas/hooks/useCanvasDragging.ts +++ /dev/null @@ -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, - stickyCanvasRef: React.RefObject, - 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, - }; -}; diff --git a/app/client/src/layoutSystems/anvil/editor/canvasArenas/utils/dndCompensatorUtils.ts b/app/client/src/layoutSystems/anvil/editor/canvasArenas/utils/dndCompensatorUtils.ts new file mode 100644 index 0000000000..93df83465e --- /dev/null +++ b/app/client/src/layoutSystems/anvil/editor/canvasArenas/utils/dndCompensatorUtils.ts @@ -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; +}; diff --git a/app/client/src/layoutSystems/anvil/editor/canvasArenas/utils/dndEventUtils.ts b/app/client/src/layoutSystems/anvil/editor/canvasArenas/utils/dndEventUtils.ts new file mode 100644 index 0000000000..c2c834e13c --- /dev/null +++ b/app/client/src/layoutSystems/anvil/editor/canvasArenas/utils/dndEventUtils.ts @@ -0,0 +1,9 @@ +export const resetAnvilDnDListener = ( + anvilDnDListener: HTMLDivElement | null, +) => { + if (anvilDnDListener) { + anvilDnDListener.style.backgroundColor = "unset"; + anvilDnDListener.style.color = "unset"; + anvilDnDListener.innerText = ""; + } +}; diff --git a/app/client/src/layoutSystems/anvil/editor/canvasArenas/hooks/utils.test.ts b/app/client/src/layoutSystems/anvil/editor/canvasArenas/utils/utils.test.ts similarity index 85% rename from app/client/src/layoutSystems/anvil/editor/canvasArenas/hooks/utils.test.ts rename to app/client/src/layoutSystems/anvil/editor/canvasArenas/utils/utils.test.ts index 7595c12665..82ad152ad8 100644 --- a/app/client/src/layoutSystems/anvil/editor/canvasArenas/hooks/utils.test.ts +++ b/app/client/src/layoutSystems/anvil/editor/canvasArenas/utils/utils.test.ts @@ -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... ]; diff --git a/app/client/src/layoutSystems/anvil/editor/canvasArenas/hooks/utils.ts b/app/client/src/layoutSystems/anvil/editor/canvasArenas/utils/utils.ts similarity index 96% rename from app/client/src/layoutSystems/anvil/editor/canvasArenas/hooks/utils.ts rename to app/client/src/layoutSystems/anvil/editor/canvasArenas/utils/utils.ts index 69328834ce..6b5db412e6 100644 --- a/app/client/src/layoutSystems/anvil/editor/canvasArenas/hooks/utils.ts +++ b/app/client/src/layoutSystems/anvil/editor/canvasArenas/utils/utils.ts @@ -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 = ""; +}; diff --git a/app/client/src/layoutSystems/anvil/editor/hooks/useAnvilWidgetHover.ts b/app/client/src/layoutSystems/anvil/editor/hooks/useAnvilWidgetHover.ts index 268d4f92d1..678839987d 100644 --- a/app/client/src/layoutSystems/anvil/editor/hooks/useAnvilWidgetHover.ts +++ b/app/client/src/layoutSystems/anvil/editor/hooks/useAnvilWidgetHover.ts @@ -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 diff --git a/app/client/src/layoutSystems/anvil/editor/hooks/useAnvilWidgetStyles.ts b/app/client/src/layoutSystems/anvil/editor/hooks/useAnvilWidgetStyles.ts index 102d33bd6b..ab3b5db0be 100644 --- a/app/client/src/layoutSystems/anvil/editor/hooks/useAnvilWidgetStyles.ts +++ b/app/client/src/layoutSystems/anvil/editor/hooks/useAnvilWidgetStyles.ts @@ -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(() => { diff --git a/app/client/src/layoutSystems/anvil/integrations/actions/draggingActions.ts b/app/client/src/layoutSystems/anvil/integrations/actions/draggingActions.ts index 1547a8478f..06c172e869 100644 --- a/app/client/src/layoutSystems/anvil/integrations/actions/draggingActions.ts +++ b/app/client/src/layoutSystems/anvil/integrations/actions/draggingActions.ts @@ -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. */ diff --git a/app/client/src/layoutSystems/anvil/integrations/modalSelectors.ts b/app/client/src/layoutSystems/anvil/integrations/modalSelectors.ts new file mode 100644 index 0000000000..8b04d4c27c --- /dev/null +++ b/app/client/src/layoutSystems/anvil/integrations/modalSelectors.ts @@ -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; +}; diff --git a/app/client/src/layoutSystems/anvil/layoutComponents/BaseLayoutComponent.tsx b/app/client/src/layoutSystems/anvil/layoutComponents/BaseLayoutComponent.tsx index a21f9a3050..41097191ed 100644 --- a/app/client/src/layoutSystems/anvil/layoutComponents/BaseLayoutComponent.tsx +++ b/app/client/src/layoutSystems/anvil/layoutComponents/BaseLayoutComponent.tsx @@ -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 ( - ); } @@ -130,11 +130,7 @@ abstract class BaseLayoutComponent extends PureComponent< static rendersWidgets: boolean = false; render(): JSX.Element | null { - return ( - - {this.renderContent()} - - ); + 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 ( + + {this.renderChildren()} + + ); } renderChildren(): React.ReactNode { diff --git a/app/client/src/layoutSystems/anvil/layoutComponents/components/section/index.tsx b/app/client/src/layoutSystems/anvil/layoutComponents/components/section/index.tsx index cb95a62686..3f053c587e 100644 --- a/app/client/src/layoutSystems/anvil/layoutComponents/components/section/index.tsx +++ b/app/client/src/layoutSystems/anvil/layoutComponents/components/section/index.tsx @@ -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 ( - {this.renderContent()} + {this.renderSectionSpaceDistributor()} + {super.renderChildren()} + + ); + } + + renderViewMode(): JSX.Element { + return ( + + {super.renderChildren()} ); } diff --git a/app/client/src/layoutSystems/anvil/layoutComponents/components/zone/index.tsx b/app/client/src/layoutSystems/anvil/layoutComponents/components/zone/index.tsx index fcd01117cb..45557c2d70 100644 --- a/app/client/src/layoutSystems/anvil/layoutComponents/components/zone/index.tsx +++ b/app/client/src/layoutSystems/anvil/layoutComponents/components/zone/index.tsx @@ -45,10 +45,10 @@ class Zone extends AlignedLayoutColumn { }; } - render() { + renderViewMode() { return ( - {this.renderContent()} + {this.renderChildren()} ); } diff --git a/app/client/src/layoutSystems/anvil/sectionSpaceDistributor/SectionSpaceDistributor.tsx b/app/client/src/layoutSystems/anvil/sectionSpaceDistributor/SectionSpaceDistributor.tsx index bafe7cc511..20bf049164 100644 --- a/app/client/src/layoutSystems/anvil/sectionSpaceDistributor/SectionSpaceDistributor.tsx +++ b/app/client/src/layoutSystems/anvil/sectionSpaceDistributor/SectionSpaceDistributor.tsx @@ -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 ? ( diff --git a/app/client/src/layoutSystems/anvil/utils/anvilTypes.ts b/app/client/src/layoutSystems/anvil/utils/anvilTypes.ts index d8f5b77a0c..741fb80cf6 100644 --- a/app/client/src/layoutSystems/anvil/utils/anvilTypes.ts +++ b/app/client/src/layoutSystems/anvil/utils/anvilTypes.ts @@ -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 { diff --git a/app/client/src/layoutSystems/anvil/utils/constants.ts b/app/client/src/layoutSystems/anvil/utils/constants.ts index f5331cccda..5e0be9284b 100644 --- a/app/client/src/layoutSystems/anvil/utils/constants.ts +++ b/app/client/src/layoutSystems/anvil/utils/constants.ts @@ -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 diff --git a/app/client/src/layoutSystems/anvil/utils/layouts/highlights/alignedColumnHighlights.ts b/app/client/src/layoutSystems/anvil/utils/layouts/highlights/alignedColumnHighlights.ts index 19bcc0bd43..10063cc2f8 100644 --- a/app/client/src/layoutSystems/anvil/utils/layouts/highlights/alignedColumnHighlights.ts +++ b/app/client/src/layoutSystems/anvil/utils/layouts/highlights/alignedColumnHighlights.ts @@ -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( diff --git a/app/client/src/layoutSystems/anvil/utils/layouts/highlights/alignedRowHighlights.ts b/app/client/src/layoutSystems/anvil/utils/layouts/highlights/alignedRowHighlights.ts index 0864df4ce5..68ca5277a0 100644 --- a/app/client/src/layoutSystems/anvil/utils/layouts/highlights/alignedRowHighlights.ts +++ b/app/client/src/layoutSystems/anvil/utils/layouts/highlights/alignedRowHighlights.ts @@ -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, }; } diff --git a/app/client/src/layoutSystems/anvil/utils/layouts/highlights/columnHighlights.ts b/app/client/src/layoutSystems/anvil/utils/layouts/highlights/columnHighlights.ts index cf75a14643..cbb49a3401 100644 --- a/app/client/src/layoutSystems/anvil/utils/layouts/highlights/columnHighlights.ts +++ b/app/client/src/layoutSystems/anvil/utils/layouts/highlights/columnHighlights.ts @@ -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( diff --git a/app/client/src/layoutSystems/anvil/utils/layouts/highlights/horizontalHighlights.ts b/app/client/src/layoutSystems/anvil/utils/layouts/highlights/horizontalHighlights.ts index 46f78eabcf..98664018b4 100644 --- a/app/client/src/layoutSystems/anvil/utils/layouts/highlights/horizontalHighlights.ts +++ b/app/client/src/layoutSystems/anvil/utils/layouts/highlights/horizontalHighlights.ts @@ -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, diff --git a/app/client/src/layoutSystems/anvil/utils/layouts/highlights/rowHighlights.test.ts b/app/client/src/layoutSystems/anvil/utils/layouts/highlights/rowHighlights.test.ts index 1aee4db1c3..99d091cdb5 100644 --- a/app/client/src/layoutSystems/anvil/utils/layouts/highlights/rowHighlights.test.ts +++ b/app/client/src/layoutSystems/anvil/utils/layouts/highlights/rowHighlights.test.ts @@ -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[] = [ diff --git a/app/client/src/layoutSystems/anvil/utils/layouts/highlights/rowHighlights.ts b/app/client/src/layoutSystems/anvil/utils/layouts/highlights/rowHighlights.ts index 034f86e0f9..134d3eb8cb 100644 --- a/app/client/src/layoutSystems/anvil/utils/layouts/highlights/rowHighlights.ts +++ b/app/client/src/layoutSystems/anvil/utils/layouts/highlights/rowHighlights.ts @@ -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, + }, }; } diff --git a/app/client/src/mocks/mockHighlightInfo.ts b/app/client/src/mocks/mockHighlightInfo.ts index 9b276e3600..93149399d7 100644 --- a/app/client/src/mocks/mockHighlightInfo.ts +++ b/app/client/src/mocks/mockHighlightInfo.ts @@ -36,6 +36,12 @@ export function mockAnvilHighlightInfo( posX: 0, posY: 0, width: 4, + edgeDetails: { + bottom: false, + left: false, + right: false, + top: false, + }, ...data, }; } diff --git a/app/client/src/sagas/selectors.tsx b/app/client/src/sagas/selectors.tsx index d54ab1c0de..38caf303e2 100644 --- a/app/client/src/sagas/selectors.tsx +++ b/app/client/src/sagas/selectors.tsx @@ -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, diff --git a/app/client/src/widgets/wds/WDSModalWidget/widget/index.tsx b/app/client/src/widgets/wds/WDSModalWidget/widget/index.tsx index c08b07b66a..1705b20a01 100644 --- a/app/client/src/widgets/wds/WDSModalWidget/widget/index.tsx +++ b/app/client/src/widgets/wds/WDSModalWidget/widget/index.tsx @@ -122,7 +122,7 @@ class WDSModalWidget extends BaseWidget { ? this.props.submitButtonText || "Submit" : undefined; const contentClassName = `${this.props.className} ${ - this.props.allowWidgetInteraction ? styles.disableModalInteraction : "" + this.props.allowWidgetInteraction ? "" : styles.disableModalInteraction }`; return ( * { + pointer-events: none; + } }