diff --git a/.vscode/settings.json b/.vscode/settings.json index ac6e6079cc..16c9f25d4d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -10,6 +10,9 @@ ], "eslint.format.enable": true, "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "explicit" + }, "json.schemas": [ { "fileMatch": [ diff --git a/frontend/package-lock.json b/frontend/package-lock.json index b165f8fb62..e09315a4b6 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -121,7 +121,7 @@ "react-loading-skeleton": "^3.1.1", "react-markdown": "^9.0.0", "react-mentions": "^4.4.7", - "react-moveable": "^0.54.1", + "react-moveable": "^0.56.0", "react-multi-select-component": "^4.3.4", "react-pdf": "^6.2.2", "react-phone-input-2": "^2.15.1", @@ -30697,10 +30697,9 @@ } }, "node_modules/react-moveable": { - "version": "0.54.2", - "resolved": "https://registry.npmjs.org/react-moveable/-/react-moveable-0.54.2.tgz", - "integrity": "sha512-NGaVLbn0i9pb3+BWSKGWFqI/Mgm4+WMeWHxXXQ4Qi1tHxWCXrUrbGvpxEpt69G/hR7dez+/m68ex+fabjnvcUg==", - "license": "MIT", + "version": "0.56.0", + "resolved": "https://registry.npmjs.org/react-moveable/-/react-moveable-0.56.0.tgz", + "integrity": "sha512-FmJNmIOsOA36mdxbrc/huiE4wuXSRlmon/o+/OrfNhSiYYYL0AV5oObtPluEhb2Yr/7EfYWBHTxF5aWAvjg1SA==", "dependencies": { "@daybrush/utils": "^1.13.0", "@egjs/agent": "^2.2.1", @@ -30711,7 +30710,7 @@ "@scena/matrix": "^1.1.1", "css-to-mat": "^1.1.1", "framework-utils": "^1.1.0", - "gesto": "^1.19.0", + "gesto": "^1.19.3", "overlap-area": "^1.1.0", "react-css-styled": "^1.1.9", "react-selecto": "^1.25.0" diff --git a/frontend/package.json b/frontend/package.json index 6a65305bb8..ffe5da76ac 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -116,7 +116,7 @@ "react-loading-skeleton": "^3.1.1", "react-markdown": "^9.0.0", "react-mentions": "^4.4.7", - "react-moveable": "^0.54.1", + "react-moveable": "^0.56.0", "react-multi-select-component": "^4.3.4", "react-pdf": "^6.2.2", "react-phone-input-2": "^2.15.1", diff --git a/frontend/src/AppBuilder/AppCanvas/Container.jsx b/frontend/src/AppBuilder/AppCanvas/Container.jsx index 808a29f90c..1f37c2bb5b 100644 --- a/frontend/src/AppBuilder/AppCanvas/Container.jsx +++ b/frontend/src/AppBuilder/AppCanvas/Container.jsx @@ -4,32 +4,17 @@ import cx from 'classnames'; import WidgetWrapper from './WidgetWrapper'; import useStore from '@/AppBuilder/_stores/store'; import { shallow } from 'zustand/shallow'; -import { useDrop } from 'react-dnd'; -import { - addChildrenWidgetsToParent, - addNewWidgetToTheEditor, - computeViewerBackgroundColor, - getSubContainerWidthAfterPadding, - addDefaultButtonIdToForm, -} from './appCanvasUtils'; -import { - CANVAS_WIDTHS, - NO_OF_GRIDS, - WIDGETS_WITH_DEFAULT_CHILDREN, - GRID_HEIGHT, - CONTAINER_FORM_CANVAS_PADDING, - SUBCONTAINER_CANVAS_BORDER_WIDTH, - BOX_PADDING, -} from './appCanvasConstants'; +import { useDrop, useDragLayer } from 'react-dnd'; +import { computeViewerBackgroundColor, getSubContainerWidthAfterPadding } from './appCanvasUtils'; +import { CANVAS_WIDTHS, NO_OF_GRIDS, GRID_HEIGHT } from './appCanvasConstants'; import { useGridStore } from '@/_stores/gridStore'; import NoComponentCanvasContainer from './NoComponentCanvasContainer'; -import { RIGHT_SIDE_BAR_TAB } from '../RightSideBar/rightSidebarConstants'; -import { isPDFSupported } from '@/_helpers/appUtils'; -import toast from 'react-hot-toast'; import { ModuleContainerBlank } from '@/modules/Modules/components'; import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext'; import useSortedComponents from '../_hooks/useSortedComponents'; -import { noop } from 'lodash'; +import { useDropVirtualMoveableGhost } from '@/AppBuilder/_hooks/useDropVirtualMoveableGhost'; +import { useCanvasDropHandler } from './useCanvasDropHandler'; +import { findNewParentIdFromMousePosition } from './Grid/gridUtils'; //TODO: Revisit the logic of height (dropRef) @@ -51,112 +36,72 @@ export const Container = React.memo( columns, darkMode, canvasMaxWidth, - isViewerSidebarPinned, - pageSidebarStyle, - pagePositionType, componentType, appType, }) => { const { moduleId } = useModuleContext(); const realCanvasRef = useRef(null); const components = useStore((state) => state.getContainerChildrenMapping(id, moduleId), shallow); - - const addComponentToCurrentPage = useStore((state) => state.addComponentToCurrentPage, shallow); - const setActiveRightSideBarTab = useStore((state) => state.setActiveRightSideBarTab, shallow); const setLastCanvasClickPosition = useStore((state) => state.setLastCanvasClickPosition, shallow); const canvasBgColor = useStore( (state) => (id === 'canvas' ? state.getCanvasBackgroundColor('canvas', darkMode) : ''), shallow ); - const isPagesSidebarHidden = useStore((state) => state.getPagesSidebarVisibility('canvas'), shallow); const currentMode = useStore((state) => state.modeStore.modules[moduleId].currentMode, shallow); const currentLayout = useStore((state) => state.currentLayout, shallow); const setFocusedParentId = useStore((state) => state.setFocusedParentId, shallow); - const setShowModuleBorder = useStore((state) => state.setShowModuleBorder, shallow) || noop; + + // Initialize ghost moveable hook + const { activateMoveableGhost, deactivateMoveableGhost } = useDropVirtualMoveableGhost(); + + // // Monitor drag layer to update ghost position continuously + const { isDragging } = useDragLayer((monitor) => ({ + isDragging: monitor.isDragging(), + })); + + // // // Cleanup ghost when drag ends + useEffect(() => { + if (!isDragging) { + deactivateMoveableGhost(); + } + }, [id, isDragging, deactivateMoveableGhost]); const isContainerReadOnly = useMemo(() => { return (index !== 0 && (componentType === 'Listview' || componentType === 'Kanban')) || currentMode === 'view'; }, [index, componentType, currentMode]); + const setCurrentDragCanvasId = useGridStore((state) => state.actions.setCurrentDragCanvasId); + + const { handleDrop } = useCanvasDropHandler({ + appType, + }); + const [{ isOverCurrent }, drop] = useDrop({ accept: 'box', - hover: (item) => { - item.canvasRef = realCanvasRef?.current; - item.canvasId = id; - item.canvasWidth = getContainerCanvasWidth(); - }, - drop: async ({ componentType, component }, monitor) => { - setShowModuleBorder(false); - if (currentMode === 'view' || (appType === 'module' && componentType !== 'ModuleContainer')) return; + hover: (item, monitor) => { + const clientOffset = monitor.getClientOffset(); - const didDrop = monitor.didDrop(); - if (didDrop) return; + const appCanvasWidth = realCanvasRef?.current?.offsetWidth || 0; - const moduleInfo = component?.moduleId - ? { - moduleId: component.moduleId, - versionId: component.versionId, - environmentId: component.environmentId, - moduleName: component.displayName, - moduleContainer: component.moduleContainer, - } - : undefined; - - let addedComponent; - - if (WIDGETS_WITH_DEFAULT_CHILDREN.includes(componentType)) { - let parentComponent = addNewWidgetToTheEditor( - componentType, - monitor, - currentLayout, - realCanvasRef, - id, - moduleInfo - ); - const childComponents = addChildrenWidgetsToParent(componentType, parentComponent?.id, currentLayout); - if (componentType === 'Form') { - parentComponent = addDefaultButtonIdToForm(parentComponent, childComponents); + if (clientOffset) { + const canvasId = findNewParentIdFromMousePosition(clientOffset.x, clientOffset.y, id); + if (canvasId === id) { + setCurrentDragCanvasId(id); } - addedComponent = [parentComponent, ...childComponents]; - await addComponentToCurrentPage(addedComponent); - } else { - const newComponent = addNewWidgetToTheEditor( - componentType, - monitor, - currentLayout, - realCanvasRef, - id, - moduleInfo - ); - addedComponent = [newComponent]; - await addComponentToCurrentPage(addedComponent); } - - setActiveRightSideBarTab(RIGHT_SIDE_BAR_TAB.CONFIGURATION); - - const canvas = document.querySelector('.canvas-container'); - const sidebar = document.querySelector('.editor-sidebar'); - const droppedElem = document.getElementById(addedComponent?.[0]?.id); - - if (!canvas || !sidebar || !droppedElem) return; - - const droppedRect = droppedElem.getBoundingClientRect(); - const sidebarRect = sidebar.getBoundingClientRect(); - - const isOverlapping = droppedRect.right > sidebarRect.left && droppedRect.left < sidebarRect.right; - - if (isOverlapping) { - const overlap = droppedRect.right - sidebarRect.left; - canvas.scrollTo({ - left: canvas.scrollLeft + overlap, - behavior: 'smooth', - }); + // Calculate width based on the app canvas's grid + let width = (appCanvasWidth * item.component?.defaultSize?.width) / NO_OF_GRIDS; + const componentSize = { + width, + height: item.component?.defaultSize?.height, + }; + if (clientOffset && id === 'canvas') { + activateMoveableGhost(componentSize, clientOffset, realCanvasRef); } }, - - collect: (monitor) => ({ - isOverCurrent: monitor.isOver({ shallow: true }), - }), + drop: (item, monitor) => { + handleDrop(item, id); + }, }); const showEmptyContainer = @@ -175,34 +120,12 @@ export const Container = React.memo( } const gridWidth = getContainerCanvasWidth() / NO_OF_GRIDS; + useEffect(() => { useGridStore.getState().actions.setSubContainerWidths(id, gridWidth); // eslint-disable-next-line react-hooks/exhaustive-deps }, [canvasWidth, listViewMode, columns]); - const getCanvasWidth = useCallback(() => { - // if ( - // id === 'canvas' && - // !isPagesSidebarHidden && - // isViewerSidebarPinned && - // currentLayout !== 'mobile' && - // pagePositionType == 'side' && - // appType !== 'module' - // ) { - // return `calc(100% - ${pageSidebarStyle === 'icon' ? '85px' : '226px'})`; - // } - // if ( - // id === 'canvas' && - // !isPagesSidebarHidden && - // !isViewerSidebarPinned && - // currentLayout !== 'mobile' && - // pagePositionType == 'side' - // ) { - // return `calc(100% - ${'44px'})`; - // } - return '100%'; - }, [id, isPagesSidebarHidden, isViewerSidebarPinned, currentLayout, pagePositionType, pageSidebarStyle]); - const handleCanvasClick = useCallback( (e) => { const realCanvas = e.target.closest('.real-canvas'); @@ -251,8 +174,8 @@ export const Container = React.memo( currentMode === 'view' ? computeViewerBackgroundColor(darkMode, canvasBgColor) : id === 'canvas' - ? canvasBgColor - : '#f0f0f0', + ? canvasBgColor + : '#f0f0f0', width: '100%', maxWidth: (() => { // For Main Canvas diff --git a/frontend/src/AppBuilder/AppCanvas/Grid/Grid.jsx b/frontend/src/AppBuilder/AppCanvas/Grid/Grid.jsx index dbe9ed941d..8afd3c303c 100644 --- a/frontend/src/AppBuilder/AppCanvas/Grid/Grid.jsx +++ b/frontend/src/AppBuilder/AppCanvas/Grid/Grid.jsx @@ -1,5 +1,5 @@ // import '@/Editor/wdyr'; -import React, { useEffect, useState, useRef, useCallback } from 'react'; +import React, { useEffect, useState, useRef, useCallback, useMemo } from 'react'; // eslint-disable-next-line import/no-unresolved import Moveable from 'react-moveable'; import { shallow } from 'zustand/shallow'; @@ -10,10 +10,8 @@ import { useGridStore, useIsGroupHandleHoverd, useOpenModalWidgetId } from '@/_s import toast from 'react-hot-toast'; import { individualGroupableProps, - getMouseDistanceFromParentDiv, findChildrenAndGrandchildren, findHighestLevelofSelection, - getOffset, hasParentWithClass, getPositionForGroupDrag, adjustWidth, @@ -25,7 +23,8 @@ import { computeScrollDelta, computeScrollDeltaOnDrag, getDraggingWidgetWidth, - positionDragGhostWidget, + positionGhostElement, + findNewParentIdFromMousePosition, } from './gridUtils'; import { dragContextBuilder, getAdjustedDropPosition } from './helpers/dragEnd'; import useStore from '@/AppBuilder/_stores/store'; @@ -33,6 +32,8 @@ import './Grid.css'; import { useGroupedTargetsScrollHandler } from './hooks/useGroupedTargetsScrollHandler'; import { DROPPABLE_PARENTS, NO_OF_GRIDS, SUBCONTAINER_WIDGETS } from '../appCanvasConstants'; import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext'; +import { useElementGudelines } from './hooks/useElementGudelines'; + const CANVAS_BOUNDS = { left: 0, top: 0, right: 0, position: 'css' }; const RESIZABLE_CONFIG = { edge: ['nw', 'n', 'ne', 'w', 'e', 'sw', 's', 'se'], @@ -73,10 +74,15 @@ export default function Grid({ gridWidth, currentLayout }) { const getTemporaryLayouts = useStore((state) => state.getTemporaryLayouts, shallow); const updateContainerAutoHeight = useStore((state) => state.updateContainerAutoHeight, shallow); const [canvasBounds, setCanvasBounds] = useState(CANVAS_BOUNDS); - const draggingComponentId = useGridStore((state) => state.draggingComponentId, shallow); - const resizingComponentId = useGridStore((state) => state.resizingComponentId, shallow); const [dragParentId, setDragParentId] = useState(null); - const [elementGuidelines, setElementGuidelines] = useState([]); + const virtualTarget = useGridStore((state) => state.virtualTarget, shallow); + const { elementGuidelines } = useElementGudelines( + boxList, + selectedComponents, + dragParentId, + getResolvedValue, + virtualTarget + ); const componentsSnappedTo = useRef(null); const prevDragParentId = useRef(null); const newDragParentId = useRef(null); @@ -84,42 +90,39 @@ export default function Grid({ gridWidth, currentLayout }) { const checkIfAnyWidgetVisibilityChanged = useStore((state) => state.checkIfAnyWidgetVisibilityChanged(), shallow); const getExposedValueOfComponent = useStore((state) => state.getExposedValueOfComponent, shallow); const setReorderContainerChildren = useStore((state) => state.setReorderContainerChildren, shallow); + const currentDragCanvasId = useGridStore((state) => state.currentDragCanvasId, shallow); + const groupedTargets = [...findHighestLevelofSelection().map((component) => '.ele-' + component.id)]; const [isVerticalExpansionRestricted, setIsVerticalExpansionRestricted] = useState(false); const toggleRightSidebar = useStore((state) => state.toggleRightSidebar, shallow); + const draggingComponentId = useStore((state) => state.draggingComponentId, shallow); + const resizingComponentId = useStore((state) => state.resizingComponentId, shallow); + const snapContainer = useMemo(() => { + if (currentDragCanvasId) { + return `#canvas-${currentDragCanvasId}`; + } + if (dragParentId) { + return `#canvas-${dragParentId}`; + } + return '#real-canvas'; + }, [currentDragCanvasId, dragParentId]); + + const getMoveableTarget = () => { + if (virtualTarget) { + return '#moveable-ghost-element'; + } + return groupedTargets?.length > 1 ? groupedTargets : '.target'; + }; + + // Set moveable reference in grid store for access by other components useEffect(() => { - const selectedSet = new Set(selectedComponents); - const draggingOrResizingId = draggingComponentId || resizingComponentId; - const isGrouped = findHighestLevelofSelection().length > 1; - const firstSelectedParent = - selectedComponents.length > 0 ? boxList.find((b) => b.id === selectedComponents[0])?.parent : null; - const selectedParent = dragParentId || firstSelectedParent; - - const guidelines = boxList - .filter((box) => { - const isVisible = - getResolvedValue(box?.component?.definition?.properties?.visibility?.value) || - getResolvedValue(box?.component?.definition?.styles?.visibility?.value); - - // Early return for non-visible elements - if (!isVisible) return false; - - if (isGrouped) { - // If component is selected, don't show its guidelines - if (selectedSet.has(box.id)) return false; - return selectedParent ? box.parent === selectedParent : !box.parent; - } - - if (draggingOrResizingId) { - if (box.id === draggingOrResizingId) return false; - return dragParentId ? box.parent === dragParentId : !box.parent; - } - - return true; - }) - .map((box) => `.ele-${box.id}`); - setElementGuidelines(guidelines); - }, [boxList, dragParentId, draggingComponentId, resizingComponentId, selectedComponents, getResolvedValue]); + if (moveableRef.current) { + useGridStore.getState().setMoveableRef(moveableRef.current); + } + return () => { + useGridStore.getState().setMoveableRef(null); + }; + }, []); useEffect(() => { setBoxList( @@ -341,13 +344,12 @@ export default function Grid({ gridWidth, currentLayout }) { }); }, [selectedComponents]); - const groupedTargets = [...findHighestLevelofSelection().map((component) => '.ele-' + component.id)]; - useEffect(() => { if (moveableRef.current) { moveableRef.current.updateTarget(); } }, [temporaryHeight]); + useEffect(() => { reloadGrid(); // eslint-disable-next-line react-hooks/exhaustive-deps @@ -534,9 +536,8 @@ export default function Grid({ gridWidth, currentLayout }) { const _top = originalBox.top; // Apply transform to return to original position - ev.target.style.transform = `translate(${Math.round(_left / _gridWidth) * _gridWidth}px, ${ - Math.round(_top / GRID_HEIGHT) * GRID_HEIGHT - }px)`; + ev.target.style.transform = `translate(${Math.round(_left / _gridWidth) * _gridWidth}px, ${Math.round(_top / GRID_HEIGHT) * GRID_HEIGHT + }px)`; } }); @@ -599,18 +600,16 @@ export default function Grid({ gridWidth, currentLayout }) { const components = Array.from(document.querySelectorAll('.active-target')).filter( (component) => !selectedComponents.includes(component.getAttribute('widgetid')) ); - const draggingOrResizing = draggingComponentId || resizingComponentId; - if (!draggingOrResizing && components.length > 0) { + const draggingOrResizingComponentId = draggingComponentId || resizingComponentId; + if (!draggingOrResizingComponentId && components.length > 0 && !virtualTarget) { for (const component of components) { component?.classList?.remove('active-target'); } } - }, [draggingComponentId, resizingComponentId, isGroupDragging, selectedComponents]); + }, [draggingComponentId, resizingComponentId, isGroupDragging, selectedComponents, virtualTarget]); useGroupedTargetsScrollHandler(groupedTargets, boxList, moveableRef); - if (mode !== 'edit') return null; - return ( <> 1, }} flushSync={flushSync} - target={groupedTargets?.length > 1 ? groupedTargets : '.target'} + target={getMoveableTarget()} origin={false} - individualGroupable={groupedTargets.length <= 1} + individualGroupable={virtualTarget ? false : groupedTargets.length <= 1} draggable={!shouldFreeze && mode !== 'view'} resizable={ !shouldFreeze @@ -639,7 +638,7 @@ export default function Grid({ gridWidth, currentLayout }) { onResize={(e) => { const temporaryLayouts = getTemporaryLayouts(); if (resizingComponentId !== e.target.id) { - useGridStore.getState().actions.setResizingComponentId(e.target.id); + useStore.getState().setResizingComponentId(e.target.id); showGridLines(); } @@ -648,12 +647,8 @@ export default function Grid({ gridWidth, currentLayout }) { let _gridWidth = useGridStore.getState().subContainerWidths[currentWidget.component?.parent] || gridWidth; // Show grid during resize - if (currentWidget.component?.parent) { - document.getElementById('canvas-' + currentWidget.component?.parent)?.classList.add('show-grid'); - setDragParentId(currentWidget.component?.parent); - } else { - document.getElementById('real-canvas').classList.add('show-grid'); - } + showGridLines(); + handleActivateTargets(currentWidget.component?.parent); const currentWidth = currentWidget.width * _gridWidth; @@ -697,6 +692,7 @@ export default function Grid({ gridWidth, currentLayout }) { e.target.style.transform = `translate(${transformX}px, ${transformY}px)`; if (e.width > 0) e.target.style.width = `${e.width}px`; if (e.height > 0) e.target.style.height = `${e.height}px`; + positionGhostElement(e.target, 'resize-ghost-widget'); }} onResizeStart={(e) => { if ( @@ -715,7 +711,7 @@ export default function Grid({ gridWidth, currentLayout }) { }} onResizeEnd={(e) => { try { - useGridStore.getState().actions.setResizingComponentId(null); + useStore.getState().setResizingComponentId(null); const currentWidget = boxList.find(({ id }) => { return id === e.target.id; }); @@ -752,9 +748,8 @@ export default function Grid({ gridWidth, currentLayout }) { const roundedTransformY = Math.round(transformY / GRID_HEIGHT) * GRID_HEIGHT; transformY = transformY % GRID_HEIGHT === 5 ? roundedTransformY - GRID_HEIGHT : roundedTransformY; - e.target.style.transform = `translate(${Math.round(transformX / _gridWidth) * _gridWidth}px, ${ - Math.round(transformY / GRID_HEIGHT) * GRID_HEIGHT - }px)`; + e.target.style.transform = `translate(${Math.round(transformX / _gridWidth) * _gridWidth}px, ${Math.round(transformY / GRID_HEIGHT) * GRID_HEIGHT + }px)`; if (!maxWidthHit || e.width < e.target.clientWidth) { e.target.style.width = `${Math.round(e.lastEvent.width / _gridWidth) * _gridWidth}px`; } @@ -867,6 +862,9 @@ export default function Grid({ gridWidth, currentLayout }) { }} checkInput onDragStart={(e) => { + if (e.target.id === 'moveable-ghost-element') { + return true; + } // This is to prevent parent component from being dragged and the stop the propagation of the event if (getHoveredComponentForGrid() !== e.target.id) { return false; @@ -919,6 +917,9 @@ export default function Grid({ gridWidth, currentLayout }) { }} onDragEnd={(e) => { handleDeactivateTargets(); + if (e.target.id === 'moveable-ghost-element') { + return; + } try { if (isDraggingRef.current) { useStore.getState().setDraggingComponentId(null); @@ -931,7 +932,6 @@ export default function Grid({ gridWidth, currentLayout }) { setDragParentId(null); if (!e.lastEvent) return; - // Build the drag context from the event const dragContext = dragContextBuilder({ event: e, widgets: boxList, isModuleEditor }); const { target, source, dragged } = dragContext; @@ -978,6 +978,21 @@ export default function Grid({ gridWidth, currentLayout }) { toggleCanvasUpdater(); }} onDrag={(e) => { + if (e.target.id === 'moveable-ghost-element') { + showGridLines(); + const _gridWidth = useGridStore.getState().subContainerWidths[currentDragCanvasId] || gridWidth; + let left = e.translate[0]; + let top = e.translate[1]; + + if (currentDragCanvasId === 'canvas') { + left = Math.round(e.translate[0] / _gridWidth) * _gridWidth; + top = Math.round(e.translate[1] / GRID_HEIGHT) * GRID_HEIGHT; + } + + useGridStore.getState().actions.setGhostDragPosition({ left, top, e }); + e.target.style.transform = `translate(${left}px, ${top}px)`; + return false; + } // Since onDrag is called multiple times when dragging, hence we are using isDraggingRef to prevent setting state again and again if (!isDraggingRef.current) { useStore.getState().setDraggingComponentId(e.target.id); @@ -1046,16 +1061,7 @@ export default function Grid({ gridWidth, currentLayout }) { // This block is to show grid lines on the canvas when the dragged element is over a new canvas if (document.elementFromPoint(e.clientX, e.clientY)) { - const targetElems = document.elementsFromPoint(e.clientX, e.clientY); - const draggedOverElements = targetElems.filter( - (ele) => - (ele.id !== e.target.id && ele.classList.contains('target')) || ele.classList.contains('real-canvas') - ); - const draggedOverElem = draggedOverElements.find((ele) => ele.classList.contains('target')); - const draggedOverContainer = draggedOverElements.find((ele) => ele.classList.contains('real-canvas')); - - // Determine potential new parent - let newParentId = draggedOverContainer?.getAttribute('data-parentId') || draggedOverElem?.id; + let newParentId = findNewParentIdFromMousePosition(e.clientX, e.clientY, e.target.id); if (newParentId === e.target.id) { newParentId = boxList.find((box) => box.id === e.target.id)?.component?.parent; @@ -1083,7 +1089,7 @@ export default function Grid({ gridWidth, currentLayout }) { `translate: ${e.translate[0]} | Round: ${Math.round(e.translate[0] / gridWidth) * gridWidth} | ${gridWidth}` ); - positionDragGhostWidget(e.target); + positionGhostElement(e.target, 'moveable-drag-ghost'); }} onDragGroup={(ev) => { const { events } = ev; @@ -1166,8 +1172,10 @@ export default function Grid({ gridWidth, currentLayout }) { component.element.classList.add('active-target'); } }} - snapGridAll={true} + // snapGridAll={true} scrollable={true} + // snapContainer={snapContainer} + // snapGridWidth={100} /> ); diff --git a/frontend/src/AppBuilder/AppCanvas/Grid/gridUtils.js b/frontend/src/AppBuilder/AppCanvas/Grid/gridUtils.js index 07a56a3c4c..1a3bc36b64 100644 --- a/frontend/src/AppBuilder/AppCanvas/Grid/gridUtils.js +++ b/frontend/src/AppBuilder/AppCanvas/Grid/gridUtils.js @@ -530,24 +530,58 @@ export const getDraggingWidgetWidth = (canvasParentId, widgetWidth) => { return draggingWidgetWidth; }; -export const positionDragGhostWidget = (draggedElement) => { - const ghostElement = document.getElementById('moveable-drag-ghost'); +/** + * Positions a ghost/feedback element relative to the main canvas + * @param {HTMLElement} targetElement - The element being dragged/resized + * @param {string} ghostElementId - The ID of the ghost element to position + * @param {Object} options - Additional positioning options + * @param {boolean} options.includeSize - Whether to update width/height of ghost element + */ +export const positionGhostElement = (targetElement, ghostElementId) => { + const ghostElement = document.getElementById(ghostElementId); - if (!ghostElement || !draggedElement) return; + if (!ghostElement || !targetElement) return; const mainCanvas = document.getElementById('real-canvas'); if (!mainCanvas) return; const mainCanvasRect = mainCanvas.getBoundingClientRect(); - const draggedRect = draggedElement.getBoundingClientRect(); + const targetRect = targetElement.getBoundingClientRect(); // Calculate position relative to main canvas - const relativeLeft = draggedRect.left - mainCanvasRect.left; - const relativeTop = draggedRect.top - mainCanvasRect.top; + const relativeLeft = targetRect.left - mainCanvasRect.left; + const relativeTop = targetRect.top - mainCanvasRect.top; // Apply the position ghostElement.style.left = `${relativeLeft}px`; ghostElement.style.top = `${relativeTop}px`; - ghostElement.style.width = `${draggedRect.width}px`; - ghostElement.style.height = `${draggedRect.height}px`; + ghostElement.style.width = `${targetRect.width}px`; + ghostElement.style.height = `${targetRect.height}px`; +}; + + +/** + * Finds the new parent ID based on the current mouse position during drag operations + * @param {number} clientX - The X coordinate of the mouse position + * @param {number} clientY - The Y coordinate of the mouse position + * @param {string} currentTargetId - The ID of the currently dragged element to exclude from search + * @returns {string|null} - The new parent ID or null if no valid parent is found + */ +export const findNewParentIdFromMousePosition = (clientX, clientY, currentTargetId) => { + if (!document.elementFromPoint(clientX, clientY)) { + return null; + } + + const targetElems = document.elementsFromPoint(clientX, clientY); + const draggedOverElements = targetElems.filter( + (ele) => (ele.id !== currentTargetId && ele.classList.contains('target')) || ele.classList.contains('real-canvas') + ); + + const draggedOverElem = draggedOverElements.find((ele) => ele.classList.contains('target')); + const draggedOverContainer = draggedOverElements.find((ele) => ele.classList.contains('real-canvas')); + + // Determine potential new parent + const newParentId = draggedOverContainer?.getAttribute('data-parentId') || draggedOverElem?.id; + + return newParentId || null; }; diff --git a/frontend/src/AppBuilder/AppCanvas/Grid/hooks/useElementGudelines.js b/frontend/src/AppBuilder/AppCanvas/Grid/hooks/useElementGudelines.js new file mode 100644 index 0000000000..55587b33fd --- /dev/null +++ b/frontend/src/AppBuilder/AppCanvas/Grid/hooks/useElementGudelines.js @@ -0,0 +1,72 @@ +import { useEffect, useState } from 'react'; +import { findHighestLevelofSelection } from '../gridUtils'; +import { useGridStore } from '@/_stores/gridStore'; +import useStore from '@/AppBuilder/_stores/store'; + +export const useElementGudelines = (boxList, selectedComponents, dragParentId, getResolvedValue, virtualTarget) => { + const [elementGuidelines, setElementGuidelines] = useState([]); + const draggingComponentId = useStore((state) => state.draggingComponentId); + const resizingComponentId = useStore((state) => state.resizingComponentId); + const currentDragCanvasId = useGridStore((state) => state.currentDragCanvasId); + + useEffect(() => { + const selectedSet = new Set(selectedComponents); + const draggingOrResizingId = draggingComponentId || resizingComponentId; + const isGrouped = findHighestLevelofSelection().length > 1; + const firstSelectedParent = + selectedComponents.length > 0 ? boxList.find((b) => b.id === selectedComponents[0])?.parent : null; + const selectedParent = dragParentId || firstSelectedParent; + const isAnyModalOpen = document.querySelector('#modal-container') ? true : false; + + const guidelines = boxList + .filter((box) => { + const isVisible = + getResolvedValue(box?.component?.definition?.properties?.visibility?.value) || + getResolvedValue(box?.component?.definition?.styles?.visibility?.value); + + // Early return for non-visible elements + if (!isVisible) return false; + + // Don't show guidelines for components which are outside the modal specially on main canvas + if (virtualTarget && isAnyModalOpen) { + if (box.parent === 'canvas' || !box.parent) return false; + } + + // This block is for first time drop using react-dnd + if (virtualTarget && currentDragCanvasId !== null) { + if (currentDragCanvasId === 'canvas') { + if (box.parent && box.parent !== 'canvas') return false; + } else { + // For sub-containers, only show components whose parent matches the canvasId + if (box.parent !== currentDragCanvasId) return false; + } + } + + if (isGrouped) { + // If component is selected, don't show its guidelines + if (selectedSet.has(box.id)) return false; + return selectedParent ? box.parent === selectedParent : !box.parent; + } + + if (draggingOrResizingId) { + if (box.id === draggingOrResizingId) return false; + return dragParentId ? box.parent === dragParentId : !box.parent; + } + + return true; + }) + .map((box) => `.ele-${box.id}`); + setElementGuidelines(guidelines); + }, [ + boxList, + dragParentId, + draggingComponentId, + resizingComponentId, + selectedComponents, + getResolvedValue, + currentDragCanvasId, + virtualTarget, + ]); + + return { elementGuidelines }; +}; diff --git a/frontend/src/AppBuilder/AppCanvas/WidgetWrapper.jsx b/frontend/src/AppBuilder/AppCanvas/WidgetWrapper.jsx index 057781f8ec..954be5607a 100644 --- a/frontend/src/AppBuilder/AppCanvas/WidgetWrapper.jsx +++ b/frontend/src/AppBuilder/AppCanvas/WidgetWrapper.jsx @@ -1,9 +1,8 @@ import React, { memo } from 'react'; import useStore from '@/AppBuilder/_stores/store'; import { shallow } from 'zustand/shallow'; -import { DragGhostWidget, ResizeGhostWidget } from './GhostWidgets'; +import { ResizeGhostWidget } from './GhostWidgets'; import { ConfigHandle } from './ConfigHandle/ConfigHandle'; -import { useGridStore } from '@/_stores/gridStore'; import cx from 'classnames'; import RenderWidget from './RenderWidget'; import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext'; @@ -35,7 +34,7 @@ const WidgetWrapper = memo( const temporaryLayouts = useStore((state) => state.temporaryLayouts?.[id], shallow); const isWidgetActive = useStore((state) => state.selectedComponents.find((sc) => sc === id) && !readOnly, shallow); const isDragging = useStore((state) => state.draggingComponentId === id); - const isResizing = useGridStore((state) => state.resizingComponentId === id); + const isResizing = useStore((state) => state.resizingComponentId === id); const componentType = useStore( (state) => state.getComponentDefinition(id, moduleId)?.component?.component, shallow diff --git a/frontend/src/AppBuilder/AppCanvas/appCanvasUtils.js b/frontend/src/AppBuilder/AppCanvas/appCanvasUtils.js index c689adb51c..debc47c61e 100644 --- a/frontend/src/AppBuilder/AppCanvas/appCanvasUtils.js +++ b/frontend/src/AppBuilder/AppCanvas/appCanvasUtils.js @@ -5,7 +5,7 @@ import useStore from '@/AppBuilder/_stores/store'; import { toast } from 'react-hot-toast'; import _, { debounce } from 'lodash'; import { useGridStore } from '@/_stores/gridStore'; -import { findHighestLevelofSelection } from './Grid/gridUtils'; +import { findHighestLevelofSelection, getMouseDistanceFromParentDiv } from './Grid/gridUtils'; import { CANVAS_WIDTHS, NO_OF_GRIDS, @@ -29,32 +29,32 @@ export function snapToGrid(canvasWidth, x, y) { //TODO: componentTypes should be a key value pair and get the definition directly by passing the componentType export const addNewWidgetToTheEditor = ( componentType, - eventMonitorObject, currentLayout, realCanvasRef, parentId, moduleInfo = undefined ) => { - const canvasBoundingRect = realCanvasRef?.current?.getBoundingClientRect(); + const canvasBoundingRect = realCanvasRef?.getBoundingClientRect(); const componentMeta = componentTypes.find((component) => component.component === componentType); const componentName = computeComponentName(componentType, useStore.getState().getCurrentPageComponents()); - + const parentCanvasType = realCanvasRef?.getAttribute('component-type'); const componentData = deepClone(componentMeta); const defaultWidth = componentData.defaultSize.width; const defaultHeight = componentData.defaultSize.height; - const offsetFromTopOfWindow = canvasBoundingRect?.top; - const offsetFromLeftOfWindow = canvasBoundingRect?.left; - const currentOffset = eventMonitorObject?.getSourceClientOffset(); + const { e } = useGridStore.getState().getGhostDragPosition(); const subContainerWidth = canvasBoundingRect?.width; - let left = Math.round(currentOffset?.x - offsetFromLeftOfWindow); - let top = Math.round(currentOffset?.y - offsetFromTopOfWindow); - - [left, top] = snapToGrid(subContainerWidth, left, top); + const { left: _left, top: _top } = getMouseDistanceFromParentDiv( + e, + parentId === 'canvas' ? 'real-canvas' : parentId, + parentCanvasType + ); + let [left, top] = snapToGrid(subContainerWidth, _left, _top); const gridWidth = subContainerWidth / NO_OF_GRIDS; left = Math.round(left / gridWidth); + // Adjust widget width based on the dropping canvas width const mainCanvasWidth = useGridStore.getState().subContainerWidths['canvas']; let width = Math.round((defaultWidth * mainCanvasWidth) / gridWidth); @@ -85,6 +85,7 @@ export const addNewWidgetToTheEditor = ( left = Math.max(0, NO_OF_GRIDS - width); width = Math.min(width, NO_OF_GRIDS); } + if (currentLayout === 'mobile') { componentData.definition.others.showOnDesktop.value = `{{false}}`; componentData.definition.others.showOnMobile.value = `{{true}}`; diff --git a/frontend/src/AppBuilder/AppCanvas/useCanvasDropHandler.js b/frontend/src/AppBuilder/AppCanvas/useCanvasDropHandler.js new file mode 100644 index 0000000000..05a9a36502 --- /dev/null +++ b/frontend/src/AppBuilder/AppCanvas/useCanvasDropHandler.js @@ -0,0 +1,113 @@ +import useStore from '@/AppBuilder/_stores/store'; +import { useGridStore } from '@/_stores/gridStore'; +import { shallow } from 'zustand/shallow'; +import { noop } from 'lodash'; +import { + addChildrenWidgetsToParent, + addNewWidgetToTheEditor, + addDefaultButtonIdToForm, +} from '../AppCanvas/appCanvasUtils'; +import { WIDGETS_WITH_DEFAULT_CHILDREN } from '../AppCanvas/appCanvasConstants'; +import { RIGHT_SIDE_BAR_TAB } from '../RightSideBar/rightSidebarConstants'; +import { isPDFSupported } from '@/_helpers/appUtils'; +import toast from 'react-hot-toast'; +import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext'; +import { handleDeactivateTargets, hideGridLines } from '../AppCanvas/Grid/gridUtils'; + +export const useCanvasDropHandler = ({ appType }) => { + const { moduleId } = useModuleContext(); + + const addComponentToCurrentPage = useStore((state) => state.addComponentToCurrentPage, shallow); + const setActiveRightSideBarTab = useStore((state) => state.setActiveRightSideBarTab, shallow); + const setShowModuleBorder = useStore((state) => state.setShowModuleBorder, shallow) || noop; + const currentMode = useStore((state) => state.modeStore.modules[moduleId].currentMode, shallow); + const currentLayout = useStore((state) => state.currentLayout, shallow); + const setCurrentDragCanvasId = useGridStore((state) => state.actions.setCurrentDragCanvasId); + + const handleDrop = async ({ componentType: draggedComponentType, component }, canvasId) => { + const realCanvasRef = + !canvasId || canvasId === 'canvas' + ? document.getElementById(`real-canvas`) + : document.getElementById(`canvas-${canvasId}`); + + handleDeactivateTargets(); + hideGridLines(); + + setShowModuleBorder(false); // Hide the module border when dropping + + if (currentMode === 'view' || (appType === 'module' && draggedComponentType !== 'ModuleContainer')) { + return; + } + + if (draggedComponentType === 'PDF' && !isPDFSupported()) { + toast.error( + 'PDF is not supported in this version of browser. We recommend upgrading to the latest version for full support.' + ); + return; + } + + // IMPORTANT: This logic needs to be changed when we implement the module versioning + const moduleInfo = component?.moduleId + ? { + moduleId: component.moduleId, + versionId: component.versionId, + environmentId: component.environmentId, + moduleName: component.displayName, + moduleContainer: component.moduleContainer, + } + : undefined; + + let addedComponent; + + if (WIDGETS_WITH_DEFAULT_CHILDREN.includes(draggedComponentType)) { + let parentComponent = addNewWidgetToTheEditor( + draggedComponentType, + currentLayout, + realCanvasRef, + canvasId, + moduleInfo + ); + const childComponents = addChildrenWidgetsToParent(draggedComponentType, parentComponent?.id, currentLayout); + if (draggedComponentType === 'Form') { + parentComponent = addDefaultButtonIdToForm(parentComponent, childComponents); + } + addedComponent = [parentComponent, ...childComponents]; + await addComponentToCurrentPage(addedComponent); + } else { + const newComponent = addNewWidgetToTheEditor( + draggedComponentType, + currentLayout, + realCanvasRef, + canvasId, + moduleInfo + ); + addedComponent = [newComponent]; + await addComponentToCurrentPage(addedComponent); + } + + setActiveRightSideBarTab(RIGHT_SIDE_BAR_TAB.CONFIGURATION); + + const canvas = document.querySelector('.canvas-container'); + const sidebar = document.querySelector('.editor-sidebar'); + const droppedElem = document.getElementById(addedComponent?.[0]?.id); + + if (!canvas || !sidebar || !droppedElem) return; + + const droppedRect = droppedElem.getBoundingClientRect(); + const sidebarRect = sidebar.getBoundingClientRect(); + + const isOverlapping = droppedRect.right > sidebarRect.left && droppedRect.left < sidebarRect.right; + + if (isOverlapping) { + const overlap = droppedRect.right - sidebarRect.left; + canvas.scrollTo({ + left: canvas.scrollLeft + overlap, + behavior: 'smooth', + }); + } + // Reset canvas ID when dropping + setCurrentDragCanvasId(null); + }; + + return { handleDrop }; +}; diff --git a/frontend/src/AppBuilder/RightSideBar/ComponentManagerTab/DragLayer.jsx b/frontend/src/AppBuilder/RightSideBar/ComponentManagerTab/DragLayer.jsx index 16b8a74f77..73ba1cda16 100644 --- a/frontend/src/AppBuilder/RightSideBar/ComponentManagerTab/DragLayer.jsx +++ b/frontend/src/AppBuilder/RightSideBar/ComponentManagerTab/DragLayer.jsx @@ -1,4 +1,4 @@ -import React, { useEffect } from 'react'; +import React, { useEffect, useRef } from 'react'; import { WidgetBox } from '../WidgetBox'; import { ModuleWidgetBox } from '@/modules/Modules/components'; import { useDrag, useDragLayer } from 'react-dnd'; @@ -9,6 +9,8 @@ import useStore from '@/AppBuilder/_stores/store'; import { shallow } from 'zustand/shallow'; import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext'; import { noop } from 'lodash'; +import { useGridStore } from '@/_stores/gridStore'; +import { useCanvasDropHandler } from '@/AppBuilder/AppCanvas/useCanvasDropHandler'; export const DragLayer = ({ index, component, isModuleTab = false, disabled = false }) => { const [isRightSidebarOpen, toggleRightSidebar] = useStore( @@ -18,11 +20,20 @@ export const DragLayer = ({ index, component, isModuleTab = false, disabled = fa const isRightSidebarPinned = useStore((state) => state.isRightSidebarPinned); const { isModuleEditor } = useModuleContext(); const setShowModuleBorder = useStore((state) => state.setShowModuleBorder, shallow) || noop; + const { handleDrop } = useCanvasDropHandler({ appType: isModuleTab ? 'module' : 'app' }) || noop; + const [{ isDragging }, drag, preview] = useDrag( () => ({ type: 'box', item: { componentType: component.component, component }, collect: (monitor) => ({ isDragging: monitor.isDragging() }), + end: (item, monitor) => { + const clientOffset = monitor.getClientOffset(); + const currentDragCanvasId = useGridStore.getState().currentDragCanvasId; + if (clientOffset) { + handleDrop(item, currentDragCanvasId); + } + }, }), [component.component] ); @@ -46,80 +57,14 @@ export const DragLayer = ({ index, component, isModuleTab = false, disabled = fa // ? component.module_container.layouts[currentLayout] // : component.defaultSize || { width: 30, height: 40 }; - const size = component.defaultSize || { width: 30, height: 40 }; - return ( <> - {isDragging && }
- {isModuleTab ? ( - - ) : ( - - )} + style={{ height: '100%', width: isModuleTab && '100%' }}> + {isModuleTab ? : }
); -}; - -const CustomDragLayer = ({ size }) => { - const { currentOffset, item } = useDragLayer((monitor) => ({ - currentOffset: monitor.getSourceClientOffset(), - item: monitor.getItem(), - })); - console.log(currentOffset, 'currentOffset'); - if (!currentOffset) return null; - - const canvasWidth = item?.canvasWidth; - const canvasBounds = item?.canvasRef?.getBoundingClientRect(); - const height = size.height; - - const appCanvasWidth = document.getElementById('real-canvas')?.offsetWidth || 0; - - // Calculate width based on the app canvas's grid - let width = (appCanvasWidth * size.width) / NO_OF_GRIDS; - - // Calculate position relative to the current canvas (parent or child) - const left = currentOffset.x - (canvasBounds?.left || 0); - const top = currentOffset.y - (canvasBounds?.top || 0); - - // Ensure width doesn't exceed the current container's width - if (width > canvasWidth) { - width = canvasWidth; - } - - // Snap width to grid (round to nearest grid unit) - const gridUnitWidth = canvasWidth / NO_OF_GRIDS; - const gridUnits = Math.round(width / gridUnitWidth); - width = gridUnits * gridUnitWidth; - - const [x, y] = snapToGrid(canvasWidth, left, top); - return ( -
-
-
- ); -}; +}; \ No newline at end of file diff --git a/frontend/src/AppBuilder/_hooks/useDropVirtualMoveableGhost.js b/frontend/src/AppBuilder/_hooks/useDropVirtualMoveableGhost.js new file mode 100644 index 0000000000..e2795235eb --- /dev/null +++ b/frontend/src/AppBuilder/_hooks/useDropVirtualMoveableGhost.js @@ -0,0 +1,101 @@ +import { useRef } from 'react'; +import { useGridStore } from '@/_stores/gridStore'; + +export const useDropVirtualMoveableGhost = () => { + const ghostElementRef = useRef(null); + const isActiveRef = useRef(false); + + const getMoveableRef = useGridStore((state) => state.moveableRef); + const setVirtualTarget = useGridStore((state) => state.actions.setVirtualTarget); + + const createGhostMoveElement = (componentSize) => { + if (ghostElementRef.current) return; + + const ghost = document.createElement('div'); + ghost.id = 'moveable-ghost-element'; + ghost.className = 'moveable-ghost target'; + ghost.style.cssText = ` + position: absolute; + width: ${componentSize.width || 100}px; + height: ${componentSize.height || 40}px; + background: #D9E2FC; + opacity: 0.7; + pointer-events: none; + z-index: 9998; + box-sizing: border-box; + top: 0; + left: 0; + `; + + const container = document.getElementById('real-canvas'); + container.appendChild(ghost); + ghostElementRef.current = ghost; + + return ghost; + }; + + const updateMoveableGhostPosition = (mousePosition, canvasRef) => { + if (!ghostElementRef.current || !canvasRef?.current || !mousePosition) return; + + const canvasRect = canvasRef.current.getBoundingClientRect(); + const relativeX = mousePosition.x - canvasRect.left; + const relativeY = mousePosition.y - canvasRect.top; + + ghostElementRef.current.style.transform = `translate(${relativeX}px, ${relativeY}px)`; + }; + + const activateMoveableGhost = (componentSize, mousePosition, canvasRef) => { + if (isActiveRef.current) return; + + isActiveRef.current = true; + + const ghost = createGhostMoveElement(componentSize, canvasRef); + if (ghost && mousePosition) { + updateMoveableGhostPosition(mousePosition, canvasRef); + + // Trigger moveable drag on the ghost element to show guidelines + const moveableInstance = getMoveableRef; + if (moveableInstance && ghost) { + try { + const fakeEvent = new MouseEvent('mousedown', { + clientX: mousePosition.x, + clientY: mousePosition.y, + bubbles: true, + cancelable: true, + view: window, + button: 0, + buttons: 1, + }); + moveableInstance.waitToChangeTarget().then((e) => { + moveableInstance.dragStart(fakeEvent, ghost); + }); + setVirtualTarget(ghost); + } catch (error) { + console.warn('Failed to trigger moveable dragStart:', error); + } + } + } + }; + + const deactivateMoveableGhost = () => { + if (!isActiveRef.current) return; + + isActiveRef.current = false; + + const moveableInstance = getMoveableRef; + if (moveableInstance && ghostElementRef.current) { + try { + setVirtualTarget(null); + ghostElementRef.current.remove(); + ghostElementRef.current = null; + } catch (error) { + console.warn('Failed to trigger moveable dragEnd:', error); + } + } + }; + + return { + activateMoveableGhost, + deactivateMoveableGhost, + }; +}; diff --git a/frontend/src/AppBuilder/_hooks/useGhostMoveable.js b/frontend/src/AppBuilder/_hooks/useGhostMoveable.js new file mode 100644 index 0000000000..0e5adaa9c1 --- /dev/null +++ b/frontend/src/AppBuilder/_hooks/useGhostMoveable.js @@ -0,0 +1,107 @@ +import { useRef } from 'react'; +import { useGridStore } from '@/_stores/gridStore'; +import { NO_OF_GRIDS, GRID_HEIGHT } from '@/AppBuilder/AppCanvas/appCanvasConstants'; +import { snapToGrid } from '@/AppBuilder/AppCanvas/appCanvasUtils'; + +export const useGhostMoveable = () => { + const ghostElementRef = useRef(null); + const isActiveRef = useRef(false); + + const getMoveableRef = useGridStore((state) => state.moveableRef); + const setVirtualTarget = useGridStore((state) => state.actions.setVirtualTarget); + + const createGhostElement = (componentSize) => { + if (ghostElementRef.current) return; + + const ghost = document.createElement('div'); + ghost.id = 'moveable-ghost-element'; + ghost.className = 'moveable-ghost target'; + ghost.style.cssText = ` + position: absolute; + width: ${componentSize.width || 100}px; + height: ${componentSize.height || 40}px; + background: #D9E2FC; + opacity: 0.7; + pointer-events: none; + z-index: 9998; + box-sizing: border-box; + top: 0; + left: 0; + `; + + const container = document.getElementById('real-canvas'); + container.appendChild(ghost); + ghostElementRef.current = ghost; + + return ghost; + }; + + const updateGhostPosition = (mousePosition, canvasRef) => { + if (!ghostElementRef.current || !canvasRef?.current || !mousePosition) return; + + const canvasRect = canvasRef.current.getBoundingClientRect(); + const relativeX = mousePosition.x - canvasRect.left; + const relativeY = mousePosition.y - canvasRect.top; + + // Apply grid snapping + // const gridWidth = canvasRef.current.offsetWidth / NO_OF_GRIDS; + // const snappedX = Math.round(relativeX / gridWidth) * gridWidth; + // const snappedY = Math.round(relativeY / GRID_HEIGHT) * GRID_HEIGHT; + ghostElementRef.current.style.transform = `translate(${relativeX}px, ${relativeY}px)`; + }; + + const activateGhost = (componentSize, mousePosition, canvasRef) => { + if (isActiveRef.current) return; + + isActiveRef.current = true; + + const ghost = createGhostElement(componentSize, canvasRef); + if (ghost && mousePosition) { + updateGhostPosition(mousePosition, canvasRef); + + // Trigger moveable drag on the ghost element to show guidelines + const moveableInstance = getMoveableRef; + if (moveableInstance && ghost) { + try { + const fakeEvent = new MouseEvent('mousedown', { + clientX: mousePosition.x, + clientY: mousePosition.y, + bubbles: true, + cancelable: true, + view: window, + button: 0, + buttons: 1, + }); + moveableInstance.waitToChangeTarget().then((e) => { + moveableInstance.dragStart(fakeEvent, ghost); + }); + setVirtualTarget(ghost); + } catch (error) { + console.warn('Failed to trigger moveable dragStart:', error); + } + } + } + }; + + const deactivateGhost = () => { + if (!isActiveRef.current) return; + + isActiveRef.current = false; + + const moveableInstance = getMoveableRef; + if (moveableInstance && ghostElementRef.current) { + try { + setVirtualTarget(null); + ghostElementRef.current.remove(); + ghostElementRef.current = null; + } catch (error) { + console.warn('Failed to trigger moveable dragEnd:', error); + } + } + }; + + return { + activateGhost, + deactivateGhost, + }; +}; diff --git a/frontend/src/AppBuilder/_stores/slices/gridSlice.js b/frontend/src/AppBuilder/_stores/slices/gridSlice.js index 9c4d0e1203..6a74c12049 100644 --- a/frontend/src/AppBuilder/_stores/slices/gridSlice.js +++ b/frontend/src/AppBuilder/_stores/slices/gridSlice.js @@ -10,10 +10,12 @@ const initialState = { lastCanvasClickPosition: null, temporaryLayouts: {}, draggingComponentId: null, + resizingComponentId: null, reorderContainerChildren: { containerId: null, triggerUpdate: 0, }, + shouldPreventDrop: false, }; export const createGridSlice = (set, get) => ({ @@ -35,6 +37,7 @@ export const createGridSlice = (set, get) => ({ get().toggleCanvasUpdater(); }, 200), setDraggingComponentId: (id) => set(() => ({ draggingComponentId: id })), + setResizingComponentId: (id) => set(() => ({ resizingComponentId: id })), moveComponentPosition: (direction) => { const { setComponentLayout, currentLayout, getSelectedComponentsDefinition, debouncedToggleCanvasUpdater } = get(); let layouts = {}; @@ -470,4 +473,7 @@ export const createGridSlice = (set, get) => ({ reorderContainerChildren: { containerId, triggerUpdate: state.reorderContainerChildren.triggerUpdate + 1 }, })); }, + setShouldPreventDrop: (shouldPreventDrop) => { + set(() => ({ shouldPreventDrop })); + }, }); diff --git a/frontend/src/_stores/gridStore.js b/frontend/src/_stores/gridStore.js index 213f07ac16..6b9822f6dd 100644 --- a/frontend/src/_stores/gridStore.js +++ b/frontend/src/_stores/gridStore.js @@ -12,11 +12,15 @@ const initialState = { idGroupDragged: false, openModalWidgetId: null, subContainerWidths: {}, + moveableRef: null, + virtualTarget: null, + currentDragCanvasId: null, + ghostDragPosition: null, }; export const useGridStore = create( zustandDevTools( - (set) => ({ + (set, get) => ({ ...initialState, actions: { setResizingComponentId: (id) => set({ resizingComponentId: id }), @@ -26,7 +30,22 @@ export const useGridStore = create( setOpenModalWidgetId: (openModalWidgetId) => set({ openModalWidgetId }), setSubContainerWidths: (id, width) => set((state) => ({ subContainerWidths: { ...state.subContainerWidths, [id]: width } })), + setVirtualTarget: (target) => set({ virtualTarget: target }), + setCurrentDragCanvasId: (canvasId) => set({ currentDragCanvasId: canvasId }), + setGhostDragPosition: (position) => set({ ghostDragPosition: position }), }, + addToElementGuidelines: (selector) => + set((state) => ({ + dynamicElementGuidelines: [...state.dynamicElementGuidelines, selector], + })), + removeFromElementGuidelines: (selector) => + set((state) => ({ + dynamicElementGuidelines: state.dynamicElementGuidelines.filter((item) => item !== selector), + })), + clearDynamicElementGuidelines: () => set({ dynamicElementGuidelines: [] }), + setMoveableRef: (ref) => set({ moveableRef: ref }), + + getGhostDragPosition: () => get().ghostDragPosition, }), { name: 'Grid Store' } ) diff --git a/frontend/src/modules/common/components/BaseColorSwatches/BaseColorSwatches.jsx b/frontend/src/modules/common/components/BaseColorSwatches/BaseColorSwatches.jsx index 160799dc9d..0da388a0eb 100644 --- a/frontend/src/modules/common/components/BaseColorSwatches/BaseColorSwatches.jsx +++ b/frontend/src/modules/common/components/BaseColorSwatches/BaseColorSwatches.jsx @@ -116,7 +116,6 @@ const BaseColorSwatches = ({ ); }; const ColorPickerInputBox = () => { - console.log('onReset', onReset); return (