mirror of
https://github.com/ToolJet/ToolJet
synced 2026-04-21 13:37:28 +00:00
1310 lines
57 KiB
JavaScript
1310 lines
57 KiB
JavaScript
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';
|
|
import _, { isArray, isEmpty } from 'lodash';
|
|
import { flushSync } from 'react-dom';
|
|
import { RESTRICTED_WIDGETS_CONFIG } from '@/AppBuilder/WidgetManager/configs/restrictedWidgetsConfig';
|
|
import { useGridStore, useIsGroupHandleHoverd, useOpenModalWidgetId } from '@/_stores/gridStore';
|
|
import toast from 'react-hot-toast';
|
|
import {
|
|
individualGroupableProps,
|
|
findChildrenAndGrandchildren,
|
|
findHighestLevelofSelection,
|
|
hasParentWithClass,
|
|
getPositionForGroupDrag,
|
|
adjustWidth,
|
|
hideGridLines,
|
|
showGridLines,
|
|
handleActivateTargets,
|
|
handleDeactivateTargets,
|
|
handleActivateNonDraggingComponents,
|
|
computeScrollDeltaOnDrag,
|
|
getDraggingWidgetWidth,
|
|
positionGhostElement,
|
|
positionGroupGhostElement,
|
|
clearActiveTargetClassNamesAfterSnapping,
|
|
isDraggingModalToCanvas,
|
|
updateDashedBordersOnHover,
|
|
updateDashedBordersOnDragResize,
|
|
} from './gridUtils';
|
|
import { dragContextBuilder, getAdjustedDropPosition, getDroppableSlotIdOnScreen } from './helpers/dragEnd';
|
|
import useStore from '@/AppBuilder/_stores/store';
|
|
import './Grid.css';
|
|
import { useGroupedTargetsScrollHandler } from './hooks/useGroupedTargetsScrollHandler';
|
|
import { useCanvasAutoScroll } from './hooks/useCanvasAutoScroll';
|
|
import { DROPPABLE_PARENTS, NO_OF_GRIDS, SUBCONTAINER_WIDGETS } from '../appCanvasConstants';
|
|
import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext';
|
|
import { useElementGuidelines } from './hooks/useElementGuidelines';
|
|
import { RIGHT_SIDE_BAR_TAB } from '../../RightSideBar/rightSidebarConstants';
|
|
import MentionComponentInChat from '../ConfigHandle/MentionComponentInChat';
|
|
import ConfigHandleButton from '@/_components/ConfigHandleButton';
|
|
|
|
const CANVAS_BOUNDS = { left: 0, top: 0, right: 0, position: 'css' };
|
|
const RESIZABLE_CONFIG = {
|
|
edge: ['nw', 'n', 'ne', 'w', 'e', 'sw', 's', 'se'],
|
|
renderDirections: ['nw', 'n', 'ne', 'w', 'e', 'sw', 's', 'se'],
|
|
};
|
|
|
|
export const GRID_HEIGHT = 10;
|
|
|
|
export default function Grid({ gridWidth, currentLayout, mainCanvasWidth }) {
|
|
const { moduleId, isModuleEditor } = useModuleContext();
|
|
const lastGroupDragEventRef = useRef(null);
|
|
const updateCanvasBottomHeight = useStore((state) => state.updateCanvasBottomHeight, shallow);
|
|
const setComponentLayout = useStore((state) => state.setComponentLayout, shallow);
|
|
const mode = useStore((state) => state.modeStore.modules[moduleId].currentMode, shallow);
|
|
const [boxList, setBoxList] = useState([]);
|
|
const currentPageComponents = useStore((state) => state.getCurrentPageComponents(moduleId), shallow);
|
|
const selectedComponents = useStore((state) => state.selectedComponents, shallow);
|
|
const setSelectedComponents = useStore((state) => state.setSelectedComponents, shallow);
|
|
const getComponentTypeFromId = useStore((state) => state.getComponentTypeFromId, shallow);
|
|
const getResolvedValue = useStore((state) => state.getResolvedValue, shallow);
|
|
const isGroupHandleHoverd = useIsGroupHandleHoverd();
|
|
const openModalWidgetId = useOpenModalWidgetId();
|
|
const moveableRef = useRef(null);
|
|
const virtualTarget = useGridStore((state) => state.virtualTarget, shallow);
|
|
|
|
const { startAutoScroll, stopAutoScroll, updateMousePosition, getScrollDelta } = useCanvasAutoScroll(
|
|
{},
|
|
boxList,
|
|
virtualTarget,
|
|
moveableRef
|
|
);
|
|
const triggerCanvasUpdater = useStore((state) => state.triggerCanvasUpdater, shallow);
|
|
const incrementCanvasUpdater = useStore((state) => state.incrementCanvasUpdater, shallow);
|
|
const groupResizeDataRef = useRef([]);
|
|
const isDraggingRef = useRef(false);
|
|
const canvasWidth = NO_OF_GRIDS * gridWidth;
|
|
const getHoveredComponentForGrid = useStore((state) => state.getHoveredComponentForGrid, shallow);
|
|
const getResolvedComponent = useStore((state) => state.getResolvedComponent, shallow);
|
|
const updateContainerAutoHeight = useStore((state) => state.updateContainerAutoHeight, shallow);
|
|
const [canvasBounds, setCanvasBounds] = useState(CANVAS_BOUNDS);
|
|
// const [dragParentId, setDragParentId] = useState(null);
|
|
const componentsSnappedTo = useRef(null);
|
|
const prevDragParentId = useRef(null);
|
|
const newDragParentId = useRef(null);
|
|
const getExposedValueOfComponent = useStore((state) => state.getExposedValueOfComponent, shallow);
|
|
const setReorderContainerChildren = useStore((state) => state.setReorderContainerChildren, shallow);
|
|
const currentDragCanvasId = useGridStore((state) => state.currentDragCanvasId, shallow);
|
|
const checkHoveredComponentDynamicHeight = useStore((state) => state.checkHoveredComponentDynamicHeight, shallow);
|
|
const pageMenuProperties = useStore((state) => state?.pageSettings?.definition?.properties ?? {});
|
|
const isPageMenuHidden = useStore((state) => state?.getPagesSidebarVisibility(moduleId), shallow);
|
|
const groupedTargets = [...findHighestLevelofSelection().map((component) => '.ele-' + component.id)];
|
|
const isGroupResizingRef = useRef(false);
|
|
const isGroupDraggingRef = useRef(false);
|
|
const isWidgetResizable = useMemo(() => {
|
|
if (virtualTarget) {
|
|
return false;
|
|
}
|
|
return RESIZABLE_CONFIG;
|
|
}, [virtualTarget]);
|
|
|
|
const getMoveableTarget = useCallback(() => {
|
|
if (virtualTarget) {
|
|
return '#moveable-virtual-ghost-element';
|
|
}
|
|
return groupedTargets?.length > 1 ? groupedTargets : '.target';
|
|
}, [virtualTarget, groupedTargets]);
|
|
|
|
// Set moveable reference in grid store for access by other components
|
|
useEffect(() => {
|
|
if (moveableRef.current) {
|
|
useGridStore.getState().setMoveableRef(moveableRef.current);
|
|
}
|
|
return () => {
|
|
useGridStore.getState().setMoveableRef(null);
|
|
};
|
|
}, []);
|
|
|
|
const elementGuidelines = useElementGuidelines(boxList, selectedComponents, getResolvedValue, virtualTarget);
|
|
|
|
useEffect(() => {
|
|
setBoxList(
|
|
Object.keys(currentPageComponents)
|
|
.map((key) => {
|
|
const widget = currentPageComponents[key];
|
|
|
|
return {
|
|
id: key,
|
|
...widget,
|
|
height: widget?.layouts?.[currentLayout]?.height,
|
|
left: widget?.layouts?.[currentLayout]?.left,
|
|
top: widget?.layouts?.[currentLayout]?.top,
|
|
width: widget?.layouts?.[currentLayout]?.width,
|
|
parent: widget?.component?.parent,
|
|
componentType: widget?.component?.component,
|
|
component: widget?.component,
|
|
};
|
|
})
|
|
.filter((box) =>
|
|
getResolvedValue(
|
|
box?.component?.definition?.others[currentLayout === 'mobile' ? 'showOnMobile' : 'showOnDesktop'].value
|
|
)
|
|
)
|
|
);
|
|
}, [currentPageComponents, setBoxList, currentLayout]);
|
|
|
|
const safeUpdateMoveable = () => {
|
|
if (!isDraggingRef.current && moveableRef.current) {
|
|
moveableRef.current.updateTarget();
|
|
moveableRef.current.updateRect();
|
|
moveableRef.current.updateSelectors();
|
|
}
|
|
};
|
|
|
|
const noOfBoxs = Object.values(boxList || []).length;
|
|
|
|
const { position: menuPosition, hideLogo, hideHeader } = pageMenuProperties;
|
|
|
|
useEffect(() => {
|
|
updateCanvasBottomHeight(boxList, moduleId);
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [noOfBoxs, triggerCanvasUpdater, menuPosition, hideLogo, hideHeader, isPageMenuHidden]);
|
|
|
|
const shouldFreeze = useStore((state) => state.getShouldFreeze());
|
|
|
|
const handleResizeStop = useCallback(
|
|
(boxList) => {
|
|
// Batch all layout updates into a single object
|
|
const batchedLayouts = {};
|
|
|
|
boxList.forEach(({ id, height, width, x, y, gw }) => {
|
|
const _canvasWidth = gw ? gw * NO_OF_GRIDS : canvasWidth;
|
|
let newWidth = Math.round((width * NO_OF_GRIDS) / _canvasWidth);
|
|
|
|
y = Math.round(y / GRID_HEIGHT) * GRID_HEIGHT;
|
|
|
|
gw = gw ? gw : gridWidth;
|
|
|
|
const parent = boxList.find((box) => box.id === id)?.component?.parent;
|
|
if (y < 0) {
|
|
y = 0;
|
|
}
|
|
if (parent) {
|
|
const parentElem = document.getElementById(`canvas-${parent}`);
|
|
const parentId = parent.includes('-') ? parent?.split('-').slice(0, -1).join('-') : parent;
|
|
const componentType = boxList.find((box) => box.id === parentId)?.component.component;
|
|
var parentHeight = parentElem?.clientHeight || height;
|
|
if (height > parentHeight && ['Tabs', 'Listview'].includes(componentType)) {
|
|
height = parentHeight;
|
|
y = 0;
|
|
}
|
|
let posX = Math.round(x / gw);
|
|
if (posX + newWidth > 43) {
|
|
newWidth = 43 - posX;
|
|
}
|
|
}
|
|
|
|
// Add to batched layouts instead of calling setComponentLayout immediately
|
|
batchedLayouts[id] = {
|
|
height: height ? height : GRID_HEIGHT,
|
|
width: newWidth ? newWidth : 1,
|
|
top: y,
|
|
left: Math.round(x / gw),
|
|
};
|
|
});
|
|
|
|
// Call setComponentLayout once with all updates
|
|
if (Object.keys(batchedLayouts).length > 0) {
|
|
setComponentLayout(batchedLayouts);
|
|
}
|
|
},
|
|
[canvasWidth, gridWidth, setComponentLayout]
|
|
);
|
|
|
|
const configHandleWhenMultipleComponentSelected = (id) => {
|
|
return (
|
|
<div
|
|
id="multiple-components-config-handle"
|
|
className={'multiple-components-config-handle'}
|
|
// This is to handle dragging using config handle when multiple components are selected since dragGroup is not triggered
|
|
onMouseUpCapture={() => {
|
|
const lastDraggedEvents = lastGroupDragEventRef.current;
|
|
if (lastDraggedEvents?.length) {
|
|
// Creating the same event object that matches what onDragGroupEnd expects
|
|
const event = {
|
|
clientX: lastDraggedEvents?.[0].clientX,
|
|
clientY: lastDraggedEvents?.[0].clientY,
|
|
events: lastDraggedEvents?.map((ev) => ({
|
|
target: ev.target,
|
|
lastEvent: {
|
|
translate: [ev.translate[0], ev.translate[1]],
|
|
},
|
|
})),
|
|
targets: lastDraggedEvents?.map((ev) => ev.target),
|
|
};
|
|
handleDragGroupEnd(event);
|
|
}
|
|
|
|
if (useGridStore.getState().isGroupHandleHoverd) {
|
|
useGridStore.getState().actions.setIsGroupHandleHoverd(false);
|
|
}
|
|
}}
|
|
onMouseDownCapture={() => {
|
|
lastGroupDragEventRef.current = null;
|
|
if (!useGridStore.getState().isGroupHandleHoverd) {
|
|
useGridStore.getState().actions.setIsGroupHandleHoverd(true);
|
|
}
|
|
}}
|
|
>
|
|
<span id={id}>
|
|
<ConfigHandleButton className="no-hover">Components</ConfigHandleButton>
|
|
<MentionComponentInChat componentIds={selectedComponents} currentPageComponents={currentPageComponents} />
|
|
</span>
|
|
</div>
|
|
);
|
|
};
|
|
//TO-DO -> Move this to moveableExtensions.js
|
|
const MultiComponentHandle = {
|
|
name: 'multiComponentHandle',
|
|
props: [],
|
|
events: [],
|
|
render() {
|
|
return configHandleWhenMultipleComponentSelected('multiple-components-config-handle');
|
|
},
|
|
};
|
|
|
|
const CustomMouseInteraction = {
|
|
name: 'customMouseInteraction',
|
|
props: {},
|
|
events: {},
|
|
mouseEnter(e) {
|
|
const controlBoxes = document.getElementsByClassName('moveable-control-box');
|
|
for (const element of controlBoxes) {
|
|
element.classList.remove('moveable-control-box-d-block');
|
|
}
|
|
e.props.target.classList.add('hovered');
|
|
e.controlBox.classList.add('moveable-control-box-d-block');
|
|
},
|
|
mouseLeave(e) {
|
|
e.props.target.classList.remove('hovered');
|
|
e.controlBox.classList.remove('moveable-control-box-d-block');
|
|
},
|
|
};
|
|
|
|
// This is to hide the control box/selectors of the other element which are not children of the modal
|
|
useEffect(() => {
|
|
const controlBoxes = document.querySelectorAll('.moveable-control-box[target-id]');
|
|
controlBoxes.forEach((box) => {
|
|
box.style.display = '';
|
|
});
|
|
if (openModalWidgetId) {
|
|
const children = findChildrenAndGrandchildren(openModalWidgetId, boxList);
|
|
const controlBoxes = document.querySelectorAll('.moveable-control-box[target-id]');
|
|
controlBoxes.forEach((box) => {
|
|
const id = box.getAttribute('target-id');
|
|
if (!children.includes(id)) {
|
|
box.style.display = 'none';
|
|
}
|
|
});
|
|
}
|
|
}, [openModalWidgetId, boxList, selectedComponents]);
|
|
|
|
/* DON'T ADD ANY STATE UPDATE LOGIC HERE */
|
|
/* Added to avoid blocking the main thread */
|
|
const reloadGrid = useCallback(async () => {
|
|
window.requestIdleCallback(() => {
|
|
safeUpdateMoveable();
|
|
Array.isArray(moveableRef.current?.moveable?.moveables) &&
|
|
moveableRef.current?.moveable?.moveables.forEach((moveable) => {
|
|
const {
|
|
props: { target },
|
|
controlBox,
|
|
} = moveable;
|
|
controlBox.setAttribute('target-id', target.id);
|
|
});
|
|
|
|
const selectedComponentsId = new Set(
|
|
selectedComponents.map((componentId) => {
|
|
return componentId;
|
|
})
|
|
);
|
|
|
|
// Get all elements with the old class name
|
|
var elements = document.getElementsByClassName('selected-component');
|
|
// Iterate through the elements and replace the old class with the new one
|
|
for (var i = 0; i < elements.length; i++) {
|
|
elements[i].className = 'moveable-control-box modal-moveable rCS1w3zcxh';
|
|
}
|
|
|
|
const controlBoxes = moveableRef?.current?.moveable?.getMoveables();
|
|
if (controlBoxes) {
|
|
for (const element of controlBoxes) {
|
|
if (selectedComponentsId.has(element?.props?.target?.id)) {
|
|
element?.controlBox?.classList.add('selected-component', `sc-${element?.props?.target?.id}`);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}, [selectedComponents]);
|
|
|
|
useEffect(() => {
|
|
if (moveableRef.current) {
|
|
safeUpdateMoveable();
|
|
}
|
|
}, [boxList, selectedComponents, mainCanvasWidth]);
|
|
|
|
useEffect(() => {
|
|
reloadGrid();
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [selectedComponents, openModalWidgetId, boxList, currentLayout]);
|
|
|
|
const isComponentVisible = (id) => {
|
|
const component = getResolvedComponent(id, null, moduleId);
|
|
const componentExposedVisibility = getExposedValueOfComponent(id, moduleId)?.isVisible;
|
|
if (componentExposedVisibility === false) return false;
|
|
let visibility;
|
|
if (isArray(component)) {
|
|
visibility = component?.[0]?.properties?.visibility ?? component?.[0]?.styles?.visibility ?? null;
|
|
} else {
|
|
visibility = component?.properties?.visibility ?? component?.styles?.visibility ?? null;
|
|
}
|
|
return visibility;
|
|
};
|
|
|
|
const handleDragEnd = useCallback(
|
|
(boxPositions) => {
|
|
let newParent = null;
|
|
let oldParent = null;
|
|
const updatedLayouts = boxPositions.reduce((layouts, { id, x, y, parent }) => {
|
|
const currentWidget = boxList.find((box) => box.id === id);
|
|
const containerWidth = parent ? useGridStore.getState().subContainerWidths[parent] : gridWidth;
|
|
|
|
let _width = currentWidget.layouts[currentLayout].width;
|
|
let _height = currentWidget.layouts[currentLayout].height;
|
|
// Adjust width if parent changed
|
|
if (parent !== currentWidget.component?.parent) {
|
|
const oldContainerWidth = currentWidget.component?.parent
|
|
? useGridStore.getState().subContainerWidths[currentWidget.component.parent]
|
|
: gridWidth;
|
|
_width = Math.round((_width * oldContainerWidth) / containerWidth);
|
|
}
|
|
|
|
// Ensure minimum width
|
|
_width = Math.max(_width, 1);
|
|
|
|
// Calculate new left position
|
|
let _left = Math.round(x / containerWidth);
|
|
|
|
// Adjust position and width if exceeding grid bounds
|
|
if (_width + _left > NO_OF_GRIDS) {
|
|
_left = Math.max(0, NO_OF_GRIDS - _width);
|
|
_width = Math.min(_width, NO_OF_GRIDS);
|
|
}
|
|
|
|
// Round y position
|
|
y = Math.max(0, Math.round(y / GRID_HEIGHT) * GRID_HEIGHT);
|
|
// Adjust height for certain parent components
|
|
if (parent) {
|
|
const parentElem = document.getElementById(`canvas-${parent}`);
|
|
const parentId = parent.includes('-') ? parent.split('-').slice(0, -1).join('-') : parent;
|
|
const componentType = boxList.find((box) => box.id === parentId)?.component.component;
|
|
const parentHeight = parentElem?.clientHeight || _height;
|
|
if (_height > parentHeight && ['Listview'].includes(componentType)) {
|
|
_height = parentHeight;
|
|
y = 0;
|
|
}
|
|
|
|
if (componentType === 'Listview' && y > parentHeight) {
|
|
y = y % parentHeight;
|
|
}
|
|
}
|
|
newParent = parent ? parent : null;
|
|
oldParent = currentWidget.component?.parent;
|
|
layouts[id] = {
|
|
width: _width,
|
|
height: _height,
|
|
top: y,
|
|
left: _left,
|
|
};
|
|
|
|
return layouts;
|
|
}, {});
|
|
// Only set updateParent to true when the parent actually changed
|
|
// This avoids unnecessary batch updates for simple drag operations within the same parent
|
|
const hasParentChanged = newParent !== oldParent;
|
|
setComponentLayout(updatedLayouts, newParent, undefined, { updateParent: hasParentChanged });
|
|
|
|
incrementCanvasUpdater();
|
|
},
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
[boxList, currentLayout, gridWidth]
|
|
);
|
|
// Add event listeners for config handle visibility when hovering over widget boundary
|
|
// This is needed even though we have hovered widget state because when hovered on boundary,
|
|
// the hovered widget state is empty, hence created a separate state for boundary
|
|
React.useEffect(() => {
|
|
const moveableBox = document.querySelector(`.moveable-control-box`);
|
|
const showConfigHandle = (e) => {
|
|
const targetId = e.target.offsetParent.getAttribute('target-id');
|
|
const componentType = getComponentTypeFromId(targetId);
|
|
|
|
if (componentType === 'ModuleContainer') {
|
|
return;
|
|
}
|
|
useStore.getState().setHoveredComponentBoundaryId(targetId);
|
|
|
|
updateDashedBordersOnHover(targetId);
|
|
};
|
|
const hideConfigHandle = () => {
|
|
useStore.getState().setHoveredComponentBoundaryId('');
|
|
};
|
|
if (moveableBox) {
|
|
moveableBox.addEventListener('mouseover', showConfigHandle);
|
|
moveableBox.addEventListener('mouseout', hideConfigHandle);
|
|
}
|
|
return () => {
|
|
moveableBox.removeEventListener('mouseover', showConfigHandle);
|
|
moveableBox.removeEventListener('mouseout', hideConfigHandle);
|
|
};
|
|
}, [moveableRef?.current?._elementTargets?.length, checkHoveredComponentDynamicHeight, getComponentTypeFromId]);
|
|
|
|
const handleDragGroupEnd = (e) => {
|
|
try {
|
|
const scrollDelta = getScrollDelta();
|
|
// Stop autoscroll monitoring for group drag
|
|
stopAutoScroll();
|
|
hideGridLines();
|
|
handleDeactivateTargets();
|
|
clearActiveTargetClassNamesAfterSnapping(selectedComponents);
|
|
if (isGroupDraggingRef.current) {
|
|
useStore.getState().setIsGroupDragging(false);
|
|
isGroupDraggingRef.current = false;
|
|
}
|
|
e.targets.forEach((targetWidget) => {
|
|
if (!targetWidget) return;
|
|
targetWidget.classList.remove('show-ghost-group-dragging-resizing');
|
|
const moveableControlBox = document.getElementsByClassName(`sc-${targetWidget.id}`)[0];
|
|
if (moveableControlBox) {
|
|
moveableControlBox.style.setProperty('visibility', 'visible', 'important');
|
|
}
|
|
});
|
|
const { events, clientX, clientY } = e;
|
|
const initialParent = events[0].target.closest('.real-canvas');
|
|
// Get potential new parent using same logic as onDragEnd
|
|
let draggedOverElemId;
|
|
let draggedOverElem;
|
|
if (document.elementFromPoint(clientX, clientY)) {
|
|
const targetElems = document.elementsFromPoint(clientX, clientY);
|
|
draggedOverElem = targetElems.find((ele) => {
|
|
const isOwnChild = events.some((ev) => ev.target.contains(ele));
|
|
if (isOwnChild) return false;
|
|
|
|
let isDroppable =
|
|
!events.some((ev) => ev.target.id === ele.id) && ele.classList.contains('drag-container-parent');
|
|
if (isDroppable) {
|
|
let widgetId = ele?.getAttribute('component-id') || ele.id;
|
|
let widgetType = boxList.find(({ id }) => id === widgetId)?.component?.component;
|
|
if (!widgetType) {
|
|
widgetId = widgetId.split('-').slice(0, -1).join('-');
|
|
widgetType = boxList.find(({ id }) => id === widgetId)?.component?.component;
|
|
}
|
|
if (!DROPPABLE_PARENTS.has(widgetType)) {
|
|
isDroppable = false;
|
|
}
|
|
}
|
|
return isDroppable;
|
|
});
|
|
draggedOverElemId = draggedOverElem?.getAttribute('component-id') || draggedOverElem?.id;
|
|
}
|
|
|
|
const widgetsTypeToBeDropped = boxList
|
|
.filter(({ id }) => events.some((ev) => ev.target.id === id))
|
|
.map(({ component }) => component.component);
|
|
const parentId = draggedOverElemId?.length > 36 ? draggedOverElemId.slice(0, 36) : draggedOverElemId;
|
|
const parentWidgetType = getComponentTypeFromId(parentId);
|
|
let restrictedWidgetsTobeDropped =
|
|
RESTRICTED_WIDGETS_CONFIG?.[parentWidgetType]?.filter((widgetType) =>
|
|
widgetsTypeToBeDropped.includes(widgetType)
|
|
) || [];
|
|
|
|
if (isModuleEditor && parentId === undefined) {
|
|
restrictedWidgetsTobeDropped = widgetsTypeToBeDropped;
|
|
// useGridStore.getState().actions.setIsGroupHandleHoverd(false);
|
|
}
|
|
const isParentChangeAllowed = isEmpty(restrictedWidgetsTobeDropped);
|
|
|
|
if (!isParentChangeAllowed) {
|
|
// Get original positions for all dragged components
|
|
const currBoxes = boxList
|
|
.filter(({ id }) => events.some((ev) => ev.target.id === id))
|
|
.map(({ id, left, top, parent }) => ({ id, left, top, parent }));
|
|
|
|
// Return each component to its original position
|
|
events.forEach((ev) => {
|
|
const originalBox = currBoxes.find((box) => box.id === ev.target.id);
|
|
const _gridWidth = useGridStore.getState().subContainerWidths[originalBox?.parent] || gridWidth;
|
|
if (originalBox) {
|
|
const _left = originalBox.left * _gridWidth;
|
|
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)`;
|
|
}
|
|
});
|
|
|
|
// Show error message
|
|
if (isModuleEditor) {
|
|
// Added this to hide configHandle when multiple components were dragged using the configHandle and placed outside the module
|
|
setSelectedComponents([]);
|
|
} else {
|
|
toast.error(`${restrictedWidgetsTobeDropped} is not compatible as a child component of ${parentWidgetType}`);
|
|
}
|
|
}
|
|
|
|
const parentElm = draggedOverElem || document.getElementById('real-canvas');
|
|
const parentCanvas =
|
|
document.getElementById('canvas-' + draggedOverElemId) || document.getElementById('real-canvas');
|
|
parentCanvas?.classList?.remove('show-grid');
|
|
const _gridWidth = useGridStore.getState().subContainerWidths[draggedOverElemId] || gridWidth;
|
|
|
|
if (isParentChangeAllowed) {
|
|
handleDragEnd(
|
|
events.map((ev) => {
|
|
const {
|
|
translate: [rawPosX, rawPosY],
|
|
} = ev.lastEvent;
|
|
|
|
// Calculate adjusted positions when parent changes
|
|
let posX = rawPosX;
|
|
let posY = rawPosY;
|
|
|
|
if (parentElm && initialParent !== parentElm) {
|
|
const newParentRect = parentElm.getBoundingClientRect();
|
|
const initialParentRect = initialParent.getBoundingClientRect();
|
|
|
|
// Adjust coordinates based on the difference in parent positions
|
|
posX = rawPosX - (newParentRect.left - initialParentRect.left);
|
|
posY = rawPosY - (newParentRect.top - initialParentRect.top);
|
|
}
|
|
|
|
// Apply grid snapping and bounds
|
|
const snappedX = Math.round(posX / _gridWidth) * _gridWidth;
|
|
const snappedY = Math.round(posY / GRID_HEIGHT) * GRID_HEIGHT;
|
|
|
|
ev.target.style.transform = `translate(${snappedX + scrollDelta.x}px, ${snappedY + scrollDelta.y}px)`;
|
|
return {
|
|
id: ev.target.id,
|
|
x: posX + scrollDelta.x || 0,
|
|
y: posY + scrollDelta.y || 0,
|
|
parent: draggedOverElemId,
|
|
};
|
|
})
|
|
);
|
|
}
|
|
setReorderContainerChildren(draggedOverElemId ?? 'canvas');
|
|
} catch (error) {
|
|
console.error('Error dragging group', error);
|
|
}
|
|
};
|
|
|
|
useGroupedTargetsScrollHandler(groupedTargets, boxList, moveableRef);
|
|
if (mode !== 'edit') return null;
|
|
return (
|
|
<>
|
|
<Moveable
|
|
dragTargetSelf={true}
|
|
dragTarget={isGroupHandleHoverd ? document.getElementById('multiple-components-config-handle') : undefined}
|
|
ref={moveableRef}
|
|
ables={[CustomMouseInteraction, MultiComponentHandle]}
|
|
props={{
|
|
customMouseInteraction: groupedTargets.length < 2,
|
|
multiComponentHandle: groupedTargets.length > 1,
|
|
}}
|
|
flushSync={flushSync}
|
|
target={getMoveableTarget()}
|
|
origin={false}
|
|
individualGroupable={virtualTarget ? false : groupedTargets.length <= 1}
|
|
draggable={!shouldFreeze}
|
|
resizable={!shouldFreeze ? isWidgetResizable : false}
|
|
keepRatio={false}
|
|
individualGroupableProps={individualGroupableProps}
|
|
onResize={(e) => {
|
|
const currentWidget = boxList.find(({ id }) => id === e.target.id);
|
|
const resizingComponentId = useStore.getState().resizingComponentId;
|
|
if (resizingComponentId !== e.target.id) {
|
|
useStore.getState().setResizingComponentId(e.target.id);
|
|
}
|
|
|
|
let _gridWidth = useGridStore.getState().subContainerWidths[currentWidget.component?.parent] || gridWidth;
|
|
|
|
// Show grid during resize
|
|
showGridLines();
|
|
|
|
handleActivateTargets(currentWidget.component?.parent);
|
|
const currentWidth = currentWidget.width * _gridWidth;
|
|
const diffWidth = e.width - currentWidth;
|
|
const diffHeight = e.height - currentWidget.height;
|
|
const isLeftChanged = e.direction[0] === -1;
|
|
const isTopChanged = e.direction[1] === -1;
|
|
|
|
// Get scroll delta from autoscroll hook
|
|
const scrollDelta = getScrollDelta();
|
|
|
|
// Calculate positions with scroll delta adjustment
|
|
let transformX = currentWidget.left * _gridWidth;
|
|
let transformY = currentWidget.top;
|
|
|
|
if (isLeftChanged) {
|
|
// Left resize
|
|
transformX = transformX - diffWidth;
|
|
}
|
|
if (isTopChanged) {
|
|
// Top resize
|
|
transformY = transformY - diffHeight;
|
|
}
|
|
|
|
// Apply container bounds
|
|
const elemContainer = e.target.closest('.real-canvas');
|
|
const containerHeight = elemContainer.scrollHeight;
|
|
const containerWidth = elemContainer.clientWidth;
|
|
const maxY = containerHeight - e.target.clientHeight;
|
|
const maxLeft = containerWidth - e.target.clientWidth;
|
|
|
|
transformY = Math.max(0, Math.min(transformY, maxY));
|
|
transformX = Math.max(0, Math.min(transformX, maxLeft));
|
|
|
|
// Update element style
|
|
const maxWidthHit = transformX < 0 || transformX >= maxLeft;
|
|
const maxHeightHit = transformY < 0 || transformY >= maxY;
|
|
if (!maxWidthHit || e.width < e.target.clientWidth) {
|
|
e.target.style.width = `${e.width}px`;
|
|
}
|
|
if (!maxHeightHit || e.height < e.target.clientHeight) {
|
|
e.target.style.height = `${e.height}px`;
|
|
}
|
|
e.target.style.transform = `translate(${transformX + scrollDelta.x}px, ${transformY + scrollDelta.y}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, 'moveable-ghost-widget');
|
|
}}
|
|
onResizeStart={(e) => {
|
|
if (
|
|
e.target.id &&
|
|
useGridStore.getState().resizingComponentId !== e.target.id &&
|
|
!e.target.classList.contains('delete-icon')
|
|
) {
|
|
// When clicked on widget boundary/resizer, select the component
|
|
setSelectedComponents([e.target.id]);
|
|
}
|
|
if (!isComponentVisible(e.target.id)) {
|
|
return false;
|
|
}
|
|
handleActivateNonDraggingComponents();
|
|
updateDashedBordersOnDragResize(e.target.id, e?.moveable?.controlBox?.classList);
|
|
e.setMin([gridWidth, GRID_HEIGHT]);
|
|
}}
|
|
onResizeEnd={(e) => {
|
|
try {
|
|
handleDeactivateTargets();
|
|
clearActiveTargetClassNamesAfterSnapping(selectedComponents);
|
|
useStore.getState().setResizingComponentId(null);
|
|
const currentWidget = boxList.find(({ id }) => {
|
|
return id === e.target.id;
|
|
});
|
|
hideGridLines();
|
|
let _gridWidth = useGridStore.getState().subContainerWidths[currentWidget.component?.parent] || gridWidth;
|
|
const directions = e.lastEvent?.direction;
|
|
if (!e.lastEvent) {
|
|
return;
|
|
}
|
|
let width = Math.round(e?.lastEvent?.width / _gridWidth) * _gridWidth;
|
|
const height = Math.round(e?.lastEvent?.height / GRID_HEIGHT) * GRID_HEIGHT;
|
|
const currentWidth = currentWidget.width * _gridWidth;
|
|
const diffWidth = e.lastEvent?.width - currentWidth;
|
|
const diffHeight = e.lastEvent?.height - currentWidget?.height;
|
|
const isLeftChanged = e.lastEvent?.direction?.[0] === -1;
|
|
const isTopChanged = e.lastEvent?.direction?.[1] === -1;
|
|
|
|
let transformX = currentWidget.left * _gridWidth;
|
|
let transformY = currentWidget.top;
|
|
if (isLeftChanged) {
|
|
transformX = currentWidget.left * _gridWidth - diffWidth;
|
|
}
|
|
if (isTopChanged) {
|
|
transformY = currentWidget.top - diffHeight;
|
|
}
|
|
|
|
width = adjustWidth(width, transformX, _gridWidth);
|
|
const elemContainer = e.target.closest('.real-canvas');
|
|
const containerHeight = elemContainer.clientHeight;
|
|
const containerWidth = elemContainer.clientWidth;
|
|
const maxY = containerHeight - e.target.clientHeight;
|
|
const maxLeft = containerWidth - e.target.clientWidth;
|
|
const maxWidthHit = transformX < 0 || transformX >= maxLeft;
|
|
const maxHeightHit = transformY < 0 || transformY >= maxY;
|
|
|
|
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)`;
|
|
if (!maxWidthHit || e.width < e.target.clientWidth) {
|
|
e.target.style.width = `${Math.round(e.lastEvent.width / _gridWidth) * _gridWidth}px`;
|
|
}
|
|
if (!maxHeightHit || e.height < e.target.clientHeight) {
|
|
e.target.style.height = `${Math.round(e.lastEvent.height / GRID_HEIGHT) * GRID_HEIGHT}px`;
|
|
}
|
|
const resizeData = {
|
|
id: e.target.id,
|
|
height: directions[1] !== 0 ? height : currentWidget.height,
|
|
width: width,
|
|
x: transformX || 0,
|
|
y: transformY || 0,
|
|
};
|
|
if (currentWidget.component?.parent) {
|
|
resizeData.gw = _gridWidth;
|
|
}
|
|
handleResizeStop([resizeData]);
|
|
setReorderContainerChildren(currentWidget?.parent ?? 'canvas');
|
|
} catch (error) {
|
|
console.error('ResizeEnd error ->', error);
|
|
}
|
|
incrementCanvasUpdater();
|
|
}}
|
|
onResizeGroupStart={({ events }) => {
|
|
showGridLines();
|
|
handleActivateNonDraggingComponents();
|
|
events.forEach((ev) => {
|
|
ev.target.classList.add('show-ghost-group-dragging-resizing');
|
|
const moveableControlBox = document.getElementsByClassName(`sc-${ev.target.id}`)[0];
|
|
if (moveableControlBox) {
|
|
moveableControlBox.style.setProperty('visibility', 'hidden', 'important');
|
|
}
|
|
});
|
|
}}
|
|
onResizeGroup={(e) => {
|
|
const { events } = e;
|
|
if (!isGroupResizingRef.current) {
|
|
useStore.getState().setIsGroupResizing(true);
|
|
isGroupResizingRef.current = true;
|
|
}
|
|
const parentElm = events[0].target.closest('.real-canvas');
|
|
const parentWidth = parentElm?.clientWidth;
|
|
const parentHeight = parentElm?.scrollHeight;
|
|
handleActivateTargets(parentElm?.id?.replace('canvas-', ''));
|
|
const { posRight, posLeft, posTop, posBottom } = getPositionForGroupDrag(events, parentWidth, parentHeight);
|
|
events.forEach((ev) => {
|
|
ev.target.style.width = `${ev.width}px`;
|
|
ev.target.style.height = `${ev.height}px`;
|
|
ev.target.style.transform = ev.drag.transform;
|
|
});
|
|
if (!(posLeft < 0 || posTop < 0 || posRight < 0 || posBottom < 0)) {
|
|
groupResizeDataRef.current = events;
|
|
}
|
|
positionGroupGhostElement(events, 'moveable-ghost-widget');
|
|
}}
|
|
onResizeGroupEnd={(e) => {
|
|
try {
|
|
const { events } = e;
|
|
const newBoxs = [];
|
|
|
|
hideGridLines();
|
|
if (isGroupResizingRef.current) {
|
|
useStore.getState().setIsGroupResizing(false);
|
|
isGroupResizingRef.current = false;
|
|
}
|
|
events.forEach((ev) => {
|
|
ev.target.classList.remove('show-ghost-group-dragging-resizing');
|
|
const moveableControlBox = document.getElementsByClassName(`sc-${ev.target.id}`)[0];
|
|
if (moveableControlBox) {
|
|
moveableControlBox.style.setProperty('visibility', 'visible', 'important');
|
|
}
|
|
});
|
|
// TODO: Logic needs to be relooked post go live P2
|
|
groupResizeDataRef.current.forEach((ev) => {
|
|
const currentWidget = boxList.find(({ id }) => {
|
|
return id === ev.target.id;
|
|
});
|
|
let _gridWidth = useGridStore.getState().subContainerWidths[currentWidget.component?.parent] || gridWidth;
|
|
let width = Math.round(ev.width / _gridWidth) * _gridWidth;
|
|
width = width < _gridWidth ? _gridWidth : width;
|
|
let posX = Math.round(ev.drag.translate[0] / _gridWidth) * _gridWidth;
|
|
let posY = Math.round(ev.drag.translate[1] / GRID_HEIGHT) * GRID_HEIGHT;
|
|
let height = Math.round(ev.height / GRID_HEIGHT) * GRID_HEIGHT;
|
|
height = height < GRID_HEIGHT ? GRID_HEIGHT : height;
|
|
|
|
ev.target.style.width = `${width}px`;
|
|
ev.target.style.height = `${height}px`;
|
|
ev.target.style.transform = `translate(${posX}px, ${posY}px)`;
|
|
newBoxs.push({
|
|
id: ev.target.id,
|
|
height: height,
|
|
width: width,
|
|
x: posX,
|
|
y: posY,
|
|
gw: _gridWidth,
|
|
});
|
|
});
|
|
|
|
if (groupResizeDataRef.current.length) {
|
|
handleResizeStop(newBoxs);
|
|
} else {
|
|
events.forEach((ev) => {
|
|
const currentWidget = boxList.find(({ id }) => {
|
|
return id === ev.target.id;
|
|
});
|
|
let _gridWidth =
|
|
useGridStore.getState().subContainerWidths[currentWidget.component?.parent] || gridWidth;
|
|
let width = currentWidget?.layouts[currentLayout].width * _gridWidth;
|
|
let posX = currentWidget?.layouts[currentLayout].left * _gridWidth;
|
|
let posY = currentWidget?.layouts[currentLayout].top;
|
|
let height = currentWidget?.layouts[currentLayout].height;
|
|
height = height < GRID_HEIGHT ? GRID_HEIGHT : height;
|
|
ev.target.style.width = `${width}px`;
|
|
ev.target.style.height = `${height}px`;
|
|
ev.target.style.transform = `translate(${posX}px, ${posY}px)`;
|
|
});
|
|
}
|
|
|
|
const groupParentId =
|
|
boxList.find(({ id }) => id === groupResizeDataRef.current[0]?.target?.id)?.parent ?? 'canvas';
|
|
setReorderContainerChildren(groupParentId);
|
|
|
|
groupResizeDataRef.current = [];
|
|
reloadGrid();
|
|
} catch (error) {
|
|
console.error('Error resizing group', error);
|
|
}
|
|
handleDeactivateTargets();
|
|
incrementCanvasUpdater();
|
|
clearActiveTargetClassNamesAfterSnapping(selectedComponents);
|
|
}}
|
|
checkInput
|
|
onDragStart={(e) => {
|
|
if (e.target.id === 'moveable-virtual-ghost-element') {
|
|
startAutoScroll(e.clientX, e.clientY, e.target);
|
|
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;
|
|
}
|
|
|
|
newDragParentId.current = boxList.find((box) => box.id === e.target.id)?.parent;
|
|
// Reset per-drag-session flag
|
|
e?.moveable?.controlBox?.removeAttribute('data-off-screen');
|
|
|
|
const box = boxList.find((box) => box.id === e.target.id);
|
|
// Prevent drag if shift is pressed for SUBCONTAINER_WIDGETS
|
|
if (SUBCONTAINER_WIDGETS.includes(box?.component?.component) && e.inputEvent.shiftKey) {
|
|
return false;
|
|
}
|
|
|
|
const parentId = boxList.find((box) => box.id === e.target.id)?.parent?.substring(0, 36);
|
|
const parentComponent = boxList.find((box) => box.id === parentId);
|
|
const isParentLegacyModal = parentComponent?.componentType === 'Modal';
|
|
|
|
// Special case for Modal. This is added to prevent widget from being dragged out of the modal.
|
|
if (parentComponent?.componentType === 'ModalV2' || parentComponent?.componentType === 'Modal') {
|
|
const modalContainer = e.target.closest('.tj-modal--container');
|
|
const mainCanvas = document.getElementById('real-canvas');
|
|
const mainRect = mainCanvas.getBoundingClientRect();
|
|
const modalRect = modalContainer.getBoundingClientRect();
|
|
const relativePosition = {
|
|
top: modalRect.top - mainRect.top + (isParentLegacyModal ? 56 : 0), // 56 is the height of the legacy modal header
|
|
right: mainRect.right - modalRect.right + modalContainer.offsetWidth,
|
|
bottom: modalRect.height + (modalRect.top - mainRect.top),
|
|
left: modalRect.left - mainRect.left,
|
|
};
|
|
setCanvasBounds({ ...relativePosition });
|
|
} else if (isModuleEditor) {
|
|
const moduleContainer = e.target.closest('.module-container-canvas');
|
|
const mainCanvas = document.getElementById('real-canvas');
|
|
|
|
const mainRect = mainCanvas.getBoundingClientRect();
|
|
const modalRect = moduleContainer.getBoundingClientRect();
|
|
const relativePosition = {
|
|
top: modalRect.top - mainRect.top,
|
|
// right: mainRect.right - modalRect.right + moduleContainer.offsetWidth,
|
|
|
|
right: modalRect.left - mainRect.left + moduleContainer.offsetWidth,
|
|
bottom: modalRect.height + (modalRect.top - mainRect.top),
|
|
left: modalRect.left - mainRect.left,
|
|
};
|
|
|
|
setCanvasBounds({ ...relativePosition });
|
|
}
|
|
|
|
// This flag indicates whether the drag event originated on a child element within a component
|
|
// (e.g., inside a Table's columns, Calendar's dates, or Kanban's cards).
|
|
// When true, it prevents the parent component from being dragged, allowing the inner elements
|
|
// to handle their own interactions like column resizing or card dragging
|
|
let isDragOnInnerElement = false;
|
|
|
|
/* If the drag or click is on a calender popup draggable interactions are not executed so that popups and other components inside calender popup works.
|
|
Also user dont need to drag an calender from using popup */
|
|
if (hasParentWithClass(e.inputEvent.target, 'react-datepicker-popper')) {
|
|
return false;
|
|
}
|
|
|
|
/* Checking if the dragged elemenent is a table. If its a table drag is disabled since it will affect column resizing and reordering */
|
|
if (box?.component?.component === 'Table') {
|
|
const tableElem = e.target.querySelector('.jet-data-table');
|
|
isDragOnInnerElement = tableElem.contains(e.inputEvent.target);
|
|
}
|
|
if (box?.component?.component === 'Calendar') {
|
|
const calenderElem = e.target.querySelector('.rbc-month-view');
|
|
isDragOnInnerElement = calenderElem.contains(e.inputEvent.target);
|
|
}
|
|
|
|
if (box?.component?.component === 'Kanban') {
|
|
const handleContainers = e.target.querySelectorAll('.handle-container');
|
|
isDragOnInnerElement = Array.from(handleContainers).some((container) =>
|
|
container.contains(e.inputEvent.target)
|
|
);
|
|
}
|
|
if (
|
|
['RangeSlider', 'RangeSliderV2', 'BoundedBox'].includes(box?.component?.component) ||
|
|
isDragOnInnerElement
|
|
) {
|
|
const targetElems = document.elementsFromPoint(e.clientX, e.clientY);
|
|
const isHandle = targetElems.find((ele) => ele.classList.contains('handle-content'));
|
|
if (!isHandle) {
|
|
return false;
|
|
}
|
|
}
|
|
}}
|
|
onDragEnd={(e) => {
|
|
stopAutoScroll();
|
|
handleDeactivateTargets();
|
|
setCanvasBounds({ ...CANVAS_BOUNDS });
|
|
hideGridLines();
|
|
clearActiveTargetClassNamesAfterSnapping(selectedComponents);
|
|
|
|
/* if the drag end is on the virtual ghost element(component drop), return */
|
|
if (e.target.id === 'moveable-virtual-ghost-element') {
|
|
return;
|
|
}
|
|
try {
|
|
if (isDraggingRef.current) {
|
|
useStore.getState().setDraggingComponentId(null);
|
|
isDraggingRef.current = false;
|
|
}
|
|
|
|
const oldParentId = boxList.find((b) => b.id === e.target.id)?.parent ?? 'canvas';
|
|
prevDragParentId.current = null;
|
|
newDragParentId.current = 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;
|
|
const targetSlotId = target?.slotId;
|
|
const targetGridWidth = useGridStore.getState().subContainerWidths[targetSlotId] || gridWidth;
|
|
const isParentChangeAllowed = dragContext.isDroppable;
|
|
|
|
const isParentModuleContainer =
|
|
!isModuleEditor &&
|
|
document.getElementById(`canvas-${target.slotId}`)?.getAttribute('component-type') === 'ModuleContainer';
|
|
|
|
// Compute new position
|
|
let { left, top } = getAdjustedDropPosition(e, target, isParentChangeAllowed, targetGridWidth, dragged);
|
|
|
|
const isModalToCanvas = isDraggingModalToCanvas(source, target, boxList);
|
|
|
|
let scrollDelta = computeScrollDeltaOnDrag(target.slotId);
|
|
|
|
if (isParentChangeAllowed && !isModalToCanvas && !isParentModuleContainer) {
|
|
const parent = target.slotId === 'real-canvas' ? null : target.slotId;
|
|
handleDragEnd([{ id: e.target.id, x: left, y: top + scrollDelta, parent }]);
|
|
} else {
|
|
const sourcegridWidth = useGridStore.getState().subContainerWidths[source.slotId] || gridWidth;
|
|
|
|
left = dragged.left * sourcegridWidth;
|
|
top = dragged.top;
|
|
!isModalToCanvas &&
|
|
toast.error(`${dragged.widgetType} is not compatible as a child component of ${target.widgetType}`);
|
|
isParentModuleContainer ? toast.error('Modules cannot be edited inside an app') : null;
|
|
}
|
|
// Apply transform for smooth transition
|
|
e.target.style.transform = `translate(${left}px, ${top + scrollDelta}px)`;
|
|
|
|
// Force reordering of conatiner if the parent has not changed
|
|
const newParentId = target.slotId === 'real-canvas' ? 'canvas' : target.slotId;
|
|
if (oldParentId === newParentId) {
|
|
setReorderContainerChildren(newParentId);
|
|
}
|
|
|
|
// Select the dragged component after drop
|
|
setTimeout(() => setSelectedComponents([dragged.id]), 100);
|
|
} catch (error) {
|
|
console.error('Error in onDragEnd:', error);
|
|
}
|
|
|
|
incrementCanvasUpdater();
|
|
}}
|
|
onDrag={(e) => {
|
|
if (e.target.id === 'moveable-virtual-ghost-element') {
|
|
showGridLines();
|
|
const _gridWidth = useGridStore.getState().subContainerWidths[currentDragCanvasId] || gridWidth;
|
|
const scrollDelta = getScrollDelta();
|
|
let left = e.translate[0] + scrollDelta.x;
|
|
let top = e.translate[1] + scrollDelta.y;
|
|
|
|
if (currentDragCanvasId === 'canvas') {
|
|
left = Math.round(e.translate[0] / _gridWidth) * _gridWidth + scrollDelta.x || 0;
|
|
top = Math.round(e.translate[1] / GRID_HEIGHT) * GRID_HEIGHT + scrollDelta.y || 0;
|
|
|
|
const _canvasWidth = NO_OF_GRIDS * _gridWidth;
|
|
left = Math.max(0, Math.min(left, _canvasWidth - e.target.clientWidth));
|
|
top = Math.max(0, top);
|
|
}
|
|
|
|
// Apply bounds clamping to prevent widget from going out of canvas
|
|
useGridStore.getState().actions.setGhostDragPosition({ left, top, e });
|
|
const draggingWidgetWidth = getDraggingWidgetWidth(currentDragCanvasId, e.target.clientWidth);
|
|
e.target.style.width = `${draggingWidgetWidth}px`;
|
|
|
|
e.target.style.transform = `translate(${left}px, ${top}px)`;
|
|
|
|
// Update autoscroll with current mouse position and target
|
|
updateMousePosition(e.clientX, e.clientY, e.target);
|
|
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) {
|
|
// Start autoscroll monitoring
|
|
startAutoScroll(e.clientX, e.clientY, e.target);
|
|
useStore.getState().setDraggingComponentId(e.target.id);
|
|
showGridLines();
|
|
handleActivateNonDraggingComponents();
|
|
updateDashedBordersOnDragResize(e.target.id, e?.moveable?.controlBox?.classList);
|
|
isDraggingRef.current = true;
|
|
}
|
|
const currentWidget = boxList.find((box) => box.id === e.target.id);
|
|
const currentParentId =
|
|
currentWidget?.component?.parent === null ? 'canvas' : currentWidget?.component?.parent;
|
|
const _dragParentId = newDragParentId.current === null ? 'canvas' : newDragParentId.current;
|
|
const _gridWidth = useGridStore.getState().subContainerWidths[_dragParentId] || gridWidth;
|
|
|
|
// Get scroll delta from autoscroll hook
|
|
const scrollDelta = getScrollDelta();
|
|
// Snap to grid + add scroll delta to keep widget under cursor
|
|
let left = Math.round(e.translate[0] / _gridWidth) * _gridWidth + scrollDelta.x || 0;
|
|
let top = Math.round(e.translate[1] / GRID_HEIGHT) * GRID_HEIGHT + scrollDelta.y || 0;
|
|
const draggingWidgetWidth = getDraggingWidgetWidth(_dragParentId, e.target.clientWidth);
|
|
e.target.style.width = `${draggingWidgetWidth}px`;
|
|
|
|
// This logic is to handle the case when the dragged element is over a new canvas
|
|
if (_dragParentId !== currentParentId) {
|
|
left = e.translate[0] + scrollDelta.x || 0;
|
|
top = e.translate[1] + scrollDelta.y || 0;
|
|
}
|
|
|
|
// Special case for Modal
|
|
const oldParentId = boxList.find((b) => b.id === e.target.id)?.parent;
|
|
const parentId = oldParentId?.length > 36 ? oldParentId.slice(0, 36) : oldParentId;
|
|
const parentComponent = boxList.find((box) => box.id === parentId);
|
|
|
|
let newParentId = getDroppableSlotIdOnScreen(e, boxList) || 'canvas';
|
|
if (parentComponent?.component?.component === 'Modal') {
|
|
// Never update parentId for Modal
|
|
newParentId = parentComponent?.id;
|
|
e.target.style.width = `${e.target.clientWidth}px`;
|
|
}
|
|
|
|
if (newParentId !== prevDragParentId.current) {
|
|
// setDragParentId(newParentId === 'canvas' ? null : newParentId);
|
|
newDragParentId.current = newParentId === 'canvas' ? null : newParentId;
|
|
prevDragParentId.current = newParentId;
|
|
handleActivateTargets(newParentId);
|
|
}
|
|
|
|
// Apply bounds clamping to prevent widget from going out of main canvas
|
|
if (newParentId === 'canvas' && currentParentId === 'canvas') {
|
|
const _canvasWidth = NO_OF_GRIDS * _gridWidth;
|
|
left = Math.max(0, Math.min(left, _canvasWidth - e.target.clientWidth));
|
|
top = Math.max(0, top);
|
|
}
|
|
|
|
e.target.style.transform = `translate(${left}px, ${top}px)`;
|
|
|
|
e.target.setAttribute(
|
|
'widget-pos2',
|
|
`translate: ${e.translate[0]} | Round: ${Math.round(e.translate[0] / gridWidth) * gridWidth} | ${gridWidth}`
|
|
);
|
|
|
|
positionGhostElement(e.target, 'moveable-ghost-widget');
|
|
|
|
// Update autoscroll with current mouse position and target
|
|
updateMousePosition(e.clientX, e.clientY, e.target);
|
|
}}
|
|
onDragGroup={(ev) => {
|
|
const { events } = ev;
|
|
lastGroupDragEventRef.current = events;
|
|
const parentElm = events[0]?.target?.closest('.real-canvas');
|
|
if (!isGroupDraggingRef.current) {
|
|
useStore.getState().setIsGroupDragging(true);
|
|
isGroupDraggingRef.current = true;
|
|
// Add the class to the targets that are being dragged to hide the group selection
|
|
lastGroupDragEventRef?.current?.forEach((ev) => {
|
|
if (!ev?.target) return;
|
|
ev.target.classList.add('show-ghost-group-dragging-resizing');
|
|
const moveableControlBox = document.getElementsByClassName(`sc-${ev.target.id}`)[0];
|
|
if (moveableControlBox) {
|
|
moveableControlBox.style.setProperty('visibility', 'hidden', 'important');
|
|
}
|
|
});
|
|
}
|
|
|
|
// Get scroll delta from autoscroll hook for group drag
|
|
const scrollDelta = getScrollDelta();
|
|
|
|
// First pass: calculate all positions and find group bounds
|
|
const positions = [];
|
|
let groupMinLeft = Infinity;
|
|
let groupMinTop = Infinity;
|
|
let groupMaxRight = -Infinity;
|
|
|
|
events.forEach((ev) => {
|
|
const currentWidget = boxList.find(({ id }) => id === ev.target.id);
|
|
const _gridWidth =
|
|
useGridStore.getState().subContainerWidths?.[currentWidget?.component?.parent] || gridWidth;
|
|
|
|
// Add scroll delta to position for smooth scrolling during group drag
|
|
let left = Math.round(ev.translate[0] / _gridWidth) * _gridWidth + scrollDelta.x || 0;
|
|
let top = Math.round(ev.translate[1] / GRID_HEIGHT) * GRID_HEIGHT + scrollDelta.y || 0;
|
|
|
|
positions.push({ ev, left, top, currentWidget });
|
|
|
|
// Track group bounds for widgets on main canvas
|
|
if (!currentWidget?.component?.parent) {
|
|
groupMinLeft = Math.min(groupMinLeft, left);
|
|
groupMinTop = Math.min(groupMinTop, top);
|
|
groupMaxRight = Math.max(groupMaxRight, left + ev.target.clientWidth);
|
|
}
|
|
});
|
|
|
|
// Calculate offset needed to keep entire group within canvas bounds
|
|
const realCanvas = document.getElementById('real-canvas');
|
|
let offsetX = 0;
|
|
let offsetY = 0;
|
|
|
|
if (realCanvas && groupMinTop !== Infinity) {
|
|
const canvasWidth = realCanvas.clientWidth;
|
|
|
|
// Top bound: group's topmost edge should not go below 0
|
|
if (groupMinTop < 0) {
|
|
offsetY = -groupMinTop;
|
|
}
|
|
// Left bound: group's leftmost edge should not go below 0
|
|
if (groupMinLeft < 0) {
|
|
offsetX = -groupMinLeft;
|
|
}
|
|
// Right bound: group's rightmost edge should not exceed canvas width
|
|
if (groupMaxRight > canvasWidth) {
|
|
offsetX = canvasWidth - groupMaxRight;
|
|
}
|
|
}
|
|
|
|
// Second pass: apply positions with group offset
|
|
positions.forEach(({ ev, left, top, currentWidget }) => {
|
|
// Apply group offset only to widgets on main canvas
|
|
if (!currentWidget?.component?.parent) {
|
|
left += offsetX;
|
|
top += offsetY;
|
|
}
|
|
ev.target.style.transform = `translate(${left}px, ${top}px)`;
|
|
});
|
|
|
|
// Position single ghost for entire group
|
|
positionGroupGhostElement(events, 'moveable-ghost-widget');
|
|
|
|
handleActivateTargets(parentElm?.id?.replace('canvas-', ''));
|
|
|
|
// Update autoscroll with current mouse position and all targets for group drag
|
|
const targets = events.map((e) => e.target);
|
|
updateMousePosition(ev.clientX, ev.clientY, targets);
|
|
}}
|
|
onDragGroupStart={(e) => {
|
|
showGridLines();
|
|
handleActivateNonDraggingComponents();
|
|
// Don't start autoscroll if dragging via config handle
|
|
if (isGroupHandleHoverd) return;
|
|
// Start autoscroll for group drag with all target elements
|
|
const targets = e.targets || [];
|
|
if (targets.length > 0) {
|
|
startAutoScroll(e.clientX, e.clientY, targets, 'groupDrag');
|
|
}
|
|
}}
|
|
onDragGroupEnd={(e) => {
|
|
// IMP --> This function is not called when group components are dragged using config Handle, hence we have separate handler
|
|
handleDragGroupEnd(e);
|
|
incrementCanvasUpdater();
|
|
}}
|
|
onClickGroup={(e) => {
|
|
const targetId =
|
|
e.inputEvent.target.id || e.inputEvent.target.closest('.moveable-box')?.getAttribute('widgetid');
|
|
if (e.inputEvent.shiftKey && targetId) {
|
|
const currentSelectedComponents = selectedComponents;
|
|
if (currentSelectedComponents.includes(targetId)) {
|
|
// If component is already selected and shift is pressed, unselect it
|
|
const filteredComponents = currentSelectedComponents.filter((id) => id !== targetId);
|
|
setSelectedComponents(filteredComponents);
|
|
} else {
|
|
// If component is not selected and shift is pressed, add it to selection
|
|
setSelectedComponents([...currentSelectedComponents, targetId]);
|
|
}
|
|
}
|
|
}}
|
|
//snap settgins
|
|
snappable={true}
|
|
snapGap={false}
|
|
isDisplaySnapDigit={false}
|
|
// snapThreshold={GRID_HEIGHT}
|
|
bounds={virtualTarget ? CANVAS_BOUNDS : canvasBounds}
|
|
// Guidelines configuration
|
|
elementGuidelines={elementGuidelines}
|
|
snapDirections={{
|
|
top: true,
|
|
right: true,
|
|
bottom: true,
|
|
left: true,
|
|
center: false,
|
|
middle: false,
|
|
}}
|
|
elementSnapDirections={{
|
|
top: true,
|
|
left: true,
|
|
bottom: true,
|
|
right: true,
|
|
center: false,
|
|
middle: false,
|
|
}}
|
|
onSnap={(e) => {
|
|
const components = e.elements;
|
|
if (isArray(componentsSnappedTo.current)) {
|
|
for (const component of componentsSnappedTo.current) {
|
|
component?.element?.classList?.remove('active-target');
|
|
}
|
|
}
|
|
componentsSnappedTo.current = components;
|
|
for (const component of components) {
|
|
component.element.classList.add('active-target');
|
|
}
|
|
}}
|
|
snapGridAll={true}
|
|
onClick={(e) => {
|
|
// Check if the click is on a config handle button
|
|
const configHandleButton = e.inputEvent?.target?.closest('.config-handle-button');
|
|
|
|
// Only execute if clicked on the first child (component-name-btn) span, not on inspect, properties, or delete buttons
|
|
if (configHandleButton && !configHandleButton.classList.contains('component-name-btn')) {
|
|
// Clicked on inspect, properties, or delete buttons - don't execute
|
|
return;
|
|
}
|
|
|
|
useStore.getState().setActiveRightSideBarTab(RIGHT_SIDE_BAR_TAB.CONFIGURATION);
|
|
useStore.getState().setRightSidebarOpen(true);
|
|
}}
|
|
/>
|
|
</>
|
|
);
|
|
}
|