ToolJet/frontend/src/AppBuilder/AppCanvas/Grid/Grid.jsx

1176 lines
50 KiB
JavaScript

import React, { useEffect, useState, useRef, useCallback } 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,
computeScrollDelta,
computeScrollDeltaOnDrag,
getDraggingWidgetWidth,
positionGhostElement,
clearActiveTargetClassNamesAfterSnapping,
} from './gridUtils';
import { dragContextBuilder, getAdjustedDropPosition } from './helpers/dragEnd';
import useStore from '@/AppBuilder/_stores/store';
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 { useElementGuidelines } from './hooks/useElementGuidelines';
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'],
};
const HORIZONTAL_CONFIG = {
edge: ['e', 'w'],
renderDirections: ['w', 'e'],
};
export const GRID_HEIGHT = 10;
export default function Grid({ gridWidth, currentLayout }) {
const { moduleId, isModuleEditor } = useModuleContext();
const lastDraggedEventsRef = 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 getComponentDefinition = useStore((state) => state.getComponentDefinition, shallow);
const getResolvedValue = useStore((state) => state.getResolvedValue, shallow);
const temporaryHeight = useStore((state) => state.temporaryLayouts?.[selectedComponents?.[0]]?.height, shallow);
const isGroupHandleHoverd = useIsGroupHandleHoverd();
const checkHoveredComponentDynamicHeight = useStore((state) => state.checkHoveredComponentDynamicHeight, shallow);
const openModalWidgetId = useOpenModalWidgetId();
const moveableRef = useRef(null);
const triggerCanvasUpdater = useStore((state) => state.triggerCanvasUpdater, shallow);
const toggleCanvasUpdater = useStore((state) => state.toggleCanvasUpdater, 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 getTemporaryLayouts = useStore((state) => state.getTemporaryLayouts, 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 [isGroupDragging, setIsGroupDragging] = useState(false);
const checkIfAnyWidgetVisibilityChanged = useStore((state) => state.checkIfAnyWidgetVisibilityChanged(), shallow);
const getExposedValueOfComponent = useStore((state) => state.getExposedValueOfComponent, shallow);
const setReorderContainerChildren = useStore((state) => state.setReorderContainerChildren, shallow);
const virtualTarget = useGridStore((state) => state.virtualTarget, shallow);
const currentDragCanvasId = useGridStore((state) => state.currentDragCanvasId, shallow);
const [isVerticalExpansionRestricted, setIsVerticalExpansionRestricted] = useState(false);
const groupedTargets = [...findHighestLevelofSelection().map((component) => '.ele-' + component.id)];
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;
useEffect(() => {
updateCanvasBottomHeight(boxList, moduleId);
noOfBoxs != 0;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [noOfBoxs, triggerCanvasUpdater]);
const shouldFreeze = useStore((state) => state.getShouldFreeze());
const handleResizeStop = useCallback(
(boxList) => {
const temporaryLayouts = getTemporaryLayouts();
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);
// Consider temporary layout position if it exists
const temporaryLayout = temporaryLayouts[id];
y = temporaryLayout?.top ?? 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;
}
}
setComponentLayout({
[id]: {
height: height ? height : GRID_HEIGHT,
width: newWidth ? newWidth : 1,
top: y,
left: Math.round(x / gw),
},
});
});
},
[canvasWidth, gridWidth, setComponentLayout]
);
const configHandleWhenMultipleComponentSelected = (id) => {
return (
<div
className={'multiple-components-config-handle'}
onMouseUpCapture={() => {
if (lastDraggedEventsRef.current) {
// Creatint the same event object that matches what onDragGroupEnd expects
const event = {
clientX: lastDraggedEventsRef.current.events[0].clientX,
clientY: lastDraggedEventsRef.current.events[0].clientY,
events: lastDraggedEventsRef.current.events.map((ev) => ({
target: ev.target,
lastEvent: {
translate: [ev.translate[0], ev.translate[1]],
},
})),
};
handleDragGroupEnd(event);
}
if (useGridStore.getState().isGroupHandleHoverd) {
useGridStore.getState().actions.setIsGroupHandleHoverd(false);
}
}}
onMouseDownCapture={() => {
lastDraggedEventsRef.current = null;
if (!useGridStore.getState().isGroupHandleHoverd) {
useGridStore.getState().actions.setIsGroupHandleHoverd(true);
}
}}
>
<span className="badge handle-content" id={id} style={{ background: '#4d72fa' }}>
<div style={{ display: 'flex', alignItems: 'center' }}>
<img
style={{ cursor: 'pointer', marginRight: '5px', verticalAlign: 'middle' }}
src="assets/images/icons/settings.svg"
width="12"
height="12"
draggable="false"
/>
<span>components</span>
</div>
</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');
e.controlBox.classList.remove('moveable-horizonta-only');
},
};
// 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();
}
}, [temporaryHeight, boxList]);
useEffect(() => {
reloadGrid();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedComponents, openModalWidgetId, boxList, currentLayout]);
const updateNewPosition = (events, parent = null) => {
const posWithParent = {
events,
parent,
};
lastDraggedEventsRef.current = posWithParent;
};
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 && ['Tabs', '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;
}, {});
setComponentLayout(updatedLayouts, newParent, undefined, { updateParent: true });
// const currentWidget = boxList.find((box) => box.id === id);
updateContainerAutoHeight(newParent);
updateContainerAutoHeight(oldParent);
toggleCanvasUpdater();
},
// 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);
const isHorizontallyExpandable = checkHoveredComponentDynamicHeight(targetId);
const moveableControlBox = document.querySelector(`.moveable-control-box[target-id="${targetId}"]`);
if (moveableControlBox && isHorizontallyExpandable) {
moveableControlBox.classList.add('moveable-horizontal-only');
}
setIsVerticalExpansionRestricted(!!isHorizontallyExpandable);
};
const hideConfigHandle = () => {
useStore.getState().setHoveredComponentBoundaryId('');
};
if (moveableBox) {
moveableBox.addEventListener('mouseover', showConfigHandle);
moveableBox.addEventListener('mouseout', hideConfigHandle);
}
return () => {
moveableBox.removeEventListener('mouseover', showConfigHandle);
moveableBox.removeEventListener('mouseout', hideConfigHandle);
};
}, []);
const handleDragGroupEnd = (e) => {
try {
hideGridLines();
// setIsGroupDragging(false);
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}px, ${snappedY}px)`;
return {
id: ev.target.id,
x: posX,
y: posY,
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
? isVerticalExpansionRestricted
? HORIZONTAL_CONFIG
: RESIZABLE_CONFIG
: false && mode !== 'view'
}
keepRatio={false}
individualGroupableProps={individualGroupableProps}
onResize={(e) => {
const temporaryLayouts = getTemporaryLayouts();
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);
showGridLines();
}
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;
// Calculate positions considering temporary layouts'
let transformX = currentWidget.left * _gridWidth;
let transformY = temporaryLayouts[currentWidget.id]?.top ?? 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.clientHeight;
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}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 (
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();
e.setMin([gridWidth, GRID_HEIGHT]);
}}
onResizeEnd={(e) => {
try {
useStore.getState().setResizingComponentId(null);
const currentWidget = boxList.find(({ id }) => {
return id === e.target.id;
});
hideGridLines();
if (!e.lastEvent) {
return;
}
let _gridWidth = useGridStore.getState().subContainerWidths[currentWidget.component?.parent] || gridWidth;
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: height,
width: width,
x: transformX,
y: transformY,
};
if (currentWidget.component?.parent) {
resizeData.gw = _gridWidth;
}
handleResizeStop([resizeData]);
setReorderContainerChildren(currentWidget?.parent ?? 'canvas');
} catch (error) {
console.error('ResizeEnd error ->', error);
}
handleDeactivateTargets();
toggleCanvasUpdater();
clearActiveTargetClassNamesAfterSnapping(selectedComponents);
}}
onResizeGroupStart={({ events }) => {
showGridLines();
handleActivateNonDraggingComponents();
}}
onResizeGroup={({ events }) => {
const parentElm = events[0].target.closest('.real-canvas');
const parentWidth = parentElm?.clientWidth;
const parentHeight = parentElm?.clientHeight;
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;
}
}}
onResizeGroupEnd={(e) => {
try {
const { events } = e;
const newBoxs = [];
hideGridLines();
// 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();
toggleCanvasUpdater();
clearActiveTargetClassNamesAfterSnapping(selectedComponents);
}}
checkInput
onDragStart={(e) => {
if (e.target.id === 'moveable-virtual-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;
}
newDragParentId.current = boxList.find((box) => box.id === e.target.id)?.parent;
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;
}
// 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) => {
handleDeactivateTargets();
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;
// Compute new position
let { left, top } = getAdjustedDropPosition(e, target, isParentChangeAllowed, targetGridWidth, dragged);
const isModalToCanvas = source.isModal && target.slotId === 'real-canvas';
const componentParentType = target?.widget?.componentType;
// For now, only doing it for container and form, we need to check it for other components later
let scrollDelta =
componentParentType === 'Form' || componentParentType === 'Container'
? document.getElementById(`canvas-${target.slotId}`)?.scrollTop || 0
: computeScrollDelta({ source });
if (isParentChangeAllowed && !isModalToCanvas) {
// Special case for Modal; If source widget is modal, prevent drops to canvas
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}`);
}
// 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);
}
setCanvasBounds({ ...CANVAS_BOUNDS });
hideGridLines();
clearActiveTargetClassNamesAfterSnapping(selectedComponents);
toggleCanvasUpdater();
}}
onDrag={(e) => {
if (e.target.id === 'moveable-virtual-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 });
const draggingWidgetWidth = getDraggingWidgetWidth(currentDragCanvasId, e.target.clientWidth);
e.target.style.width = `${draggingWidgetWidth}px`;
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);
showGridLines();
handleActivateNonDraggingComponents();
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;
// Snap to grid
let left = Math.round(e.translate[0] / _gridWidth) * _gridWidth;
let top = Math.round(e.translate[1] / GRID_HEIGHT) * GRID_HEIGHT;
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];
top = e.translate[1];
}
// 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);
const parentWidgetType = parentComponent?.component?.component;
const isOnHeaderOrFooter = oldParentId
? oldParentId.includes('-header') || oldParentId.includes('-footer')
: false;
const isParentModalSlot = parentWidgetType === 'ModalV2' && isOnHeaderOrFooter;
const isParentNewModal = parentComponent?.component?.component === 'ModalV2';
const isParentLegacyModal = parentComponent?.component?.component === 'Modal';
const isParentModal = isParentNewModal || isParentLegacyModal || isParentModalSlot;
if (isParentModal) {
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,
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,
bottom: modalRect.height + (modalRect.top - mainRect.top),
left: modalRect.left - mainRect.left,
};
setCanvasBounds({ ...relativePosition });
}
// 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;
if (newParentId === e.target.id) {
newParentId = boxList.find((box) => box.id === e.target.id)?.component?.parent;
} else 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);
}
}
// Build the drag context from the event
const source = { slotId: oldParentId };
let scrollDelta = computeScrollDeltaOnDrag({ source });
e.target.style.transform = `translate(${left}px, ${top - scrollDelta}px)`;
e.target.setAttribute(
'widget-pos2',
`translate: ${e.translate[0]} | Round: ${Math.round(e.translate[0] / gridWidth) * gridWidth} | ${gridWidth}`
);
positionGhostElement(e.target, 'moveable-drag-ghost');
}}
onDragGroup={(ev) => {
const { events } = ev;
const parentElm = events[0]?.target?.closest('.real-canvas');
if (parentElm && !parentElm.classList.contains('show-grid')) {
parentElm?.classList?.add('show-grid');
}
events.forEach((ev) => {
const currentWidget = boxList.find(({ id }) => id === ev.target.id);
const _gridWidth =
useGridStore.getState().subContainerWidths?.[currentWidget?.component?.parent] || gridWidth;
let left = Math.round(ev.translate[0] / _gridWidth) * _gridWidth;
let top = Math.round(ev.translate[1] / GRID_HEIGHT) * GRID_HEIGHT;
ev.target.style.transform = `translate(${left}px, ${top}px)`;
});
handleActivateTargets(parentElm?.id?.replace('canvas-', ''));
updateNewPosition(events);
}}
onDragGroupStart={({ events }) => {
showGridLines();
// setIsGroupDragging(true);
handleActivateNonDraggingComponents();
}}
onDragGroupEnd={(e) => {
handleDragGroupEnd(e);
handleDeactivateTargets();
clearActiveTargetClassNamesAfterSnapping(selectedComponents);
toggleCanvasUpdater();
}}
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={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}
/>
</>
);
}