mirror of
https://github.com/ToolJet/ToolJet
synced 2026-04-21 21:47:17 +00:00
722 lines
24 KiB
JavaScript
722 lines
24 KiB
JavaScript
import { useGridStore } from '@/_stores/gridStore';
|
|
import { isEmpty } from 'lodash';
|
|
import useStore from '@/AppBuilder/_stores/store';
|
|
import { getTabId, getSubContainerIdWithSlots } from '../appCanvasUtils';
|
|
import { NO_OF_GRIDS } from '../appCanvasConstants';
|
|
|
|
export function correctBounds(layout, bounds) {
|
|
layout = scaleLayouts(layout);
|
|
const collidesWith = [];
|
|
for (let i = 0, len = layout.length; i < len; i++) {
|
|
const l = layout[i];
|
|
// Overflows right
|
|
if (l.left + l.width > bounds.cols) l.left = bounds.cols - l.width;
|
|
// Overflows left
|
|
if (l.left < 0) {
|
|
l.left = 0;
|
|
l.width = bounds.cols;
|
|
}
|
|
if (!l.static) collidesWith.push(l);
|
|
else {
|
|
// If this is static and collides with other statics, we must move it down.
|
|
// We have to do something nicer than just letting them overlap.
|
|
while (getFirstCollision(collidesWith, l)) {
|
|
l.top++;
|
|
}
|
|
}
|
|
}
|
|
return removePaddingLeft(layout);
|
|
}
|
|
|
|
function removePaddingLeft(layouts) {
|
|
return layouts.map((layout) => {
|
|
if (layout.left == 1) {
|
|
if (!layouts.find((l) => l.top > layout.top && l.top < layout.top + layout.height && l.left < 1)) {
|
|
return { ...layout, left: 0 };
|
|
}
|
|
}
|
|
return { ...layout };
|
|
});
|
|
}
|
|
|
|
function collides(l1, l2) {
|
|
if (l1.i === l2.i) return false; // same element
|
|
if (l1.left + l1.width <= l2.left) return false; // l1 is left of l2
|
|
if (l1.left >= l2.left + l2.width) return false; // l1 is right of l2
|
|
if (l1.top + l1.height <= l2.top) return false; // l1 is above l2
|
|
if (l1.top >= l2.top + l2.height) return false; // l1 is below l2
|
|
return true; // boxes overlap
|
|
}
|
|
|
|
function getFirstCollision(layout, layoutItem) {
|
|
for (let i = 0, len = layout.length; i < len; i++) {
|
|
const isCollides = collides(layout[i], layoutItem);
|
|
if (isCollides) return layout[i];
|
|
}
|
|
return null; // Return null if there's no collision
|
|
}
|
|
|
|
export function compact(layout, compactType = 'vertical', cols = 20, allowOverlap = false) {
|
|
// Statics go in the compareWith array right away so items flow around them.
|
|
const compareWith = getStatics(layout);
|
|
// We go through the items by row and column.
|
|
const sorted = sortLayoutItems(layout, compactType);
|
|
// Holding for new items.
|
|
const out = new Array(layout.length);
|
|
|
|
for (let i = 0, len = sorted.length; i < len; i++) {
|
|
let l = cloneLayoutItem(sorted[i]);
|
|
|
|
// Don't move static elements
|
|
if (!l.static) {
|
|
l = compactItem(compareWith, l, compactType, cols, sorted, allowOverlap);
|
|
|
|
// Add to comparison array. We only collide with items before this one.
|
|
// Statics are already in this array.
|
|
compareWith.push(l);
|
|
}
|
|
|
|
// Add to output array to make sure they still come out in the right order.
|
|
out[layout.indexOf(sorted[i])] = l;
|
|
|
|
// Clear moved flag, if it exists.
|
|
l.moved = false;
|
|
}
|
|
|
|
return out;
|
|
}
|
|
|
|
export function getStatics(layout) {
|
|
return layout.filter((l) => l.static);
|
|
}
|
|
|
|
// Fast path to cloning, since this is monomorphic
|
|
export function cloneLayoutItem(layoutItem) {
|
|
return {
|
|
width: layoutItem.width,
|
|
height: layoutItem.height,
|
|
left: layoutItem.left,
|
|
top: layoutItem.top,
|
|
i: layoutItem.i,
|
|
minW: layoutItem.minW,
|
|
maxW: layoutItem.maxW,
|
|
minH: layoutItem.minH,
|
|
maxH: layoutItem.maxH,
|
|
moved: Boolean(layoutItem.moved),
|
|
static: Boolean(layoutItem.static),
|
|
// These can be null/undefined
|
|
isDraggable: layoutItem.isDraggable,
|
|
isResizable: layoutItem.isResizable,
|
|
resizeHandles: layoutItem.resizeHandles,
|
|
isBounded: layoutItem.isBounded,
|
|
};
|
|
}
|
|
|
|
function compactItem(compareWith, l, compactType, cols, fullLayout, allowOverlap) {
|
|
const compactV = compactType === 'vertical';
|
|
const compactH = compactType === 'horizontal';
|
|
if (compactV) {
|
|
// Bottom 'top' possible is the bottom of the layout.
|
|
// This allows you to do nice stuff like specify {top: Infinity}
|
|
// This is here because the layout must be sorted in order to get the correct bottom `top`.
|
|
const bottomPos = bottom(compareWith);
|
|
l.top = Math.min(bottomPos, l.top);
|
|
// Move the element up as far as it can go without colliding.
|
|
while (l.top > 0 && !getFirstCollision(compareWith, l)) {
|
|
l.top--;
|
|
}
|
|
} else if (compactH) {
|
|
// Move the element left as far as it can go without colliding.
|
|
while (l.left > 0 && !getFirstCollision(compareWith, l)) {
|
|
l.left--;
|
|
}
|
|
}
|
|
|
|
// Move it down, and keep moving it down if it's colliding.
|
|
let collides;
|
|
// Checking the compactType null value to avoid breaking the layout when overlapping is allowed.
|
|
while ((collides = getFirstCollision(compareWith, l)) && !(compactType === null && allowOverlap)) {
|
|
if (compactH) {
|
|
resolveCompactionCollision(fullLayout, l, collides.left + collides.width, 'x');
|
|
} else {
|
|
resolveCompactionCollision(fullLayout, l, collides.top + collides.height, 'top');
|
|
}
|
|
// Since we can't grow without bounds horizontally, if we've overflown, let's move it down and try again.
|
|
if (compactH && l.left + l.width > cols) {
|
|
l.left = cols - l.width;
|
|
l.top++;
|
|
// Also move the element as left as we can
|
|
while (l.left > 0 && !getFirstCollision(compareWith, l)) {
|
|
l.left--;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Ensure that there are no negative positions
|
|
l.top = Math.max(l.top, 0);
|
|
l.left = Math.max(l.left, 0);
|
|
|
|
return l;
|
|
}
|
|
|
|
export function bottom(layout) {
|
|
let max = 0,
|
|
bottomY;
|
|
for (let i = 0, len = layout.length; i < len; i++) {
|
|
bottomY = layout[i].top + layout[i].height;
|
|
if (bottomY > max) max = bottomY;
|
|
}
|
|
return max;
|
|
}
|
|
|
|
function resolveCompactionCollision(layout, item, moveToCoord, axis) {
|
|
const sizeProp = heightWidth[axis];
|
|
item[axis] += 1;
|
|
const itemIndex = layout
|
|
.map((layoutItem) => {
|
|
return layoutItem.i;
|
|
})
|
|
.indexOf(item.i);
|
|
|
|
// Go through each item we collide with.
|
|
for (let i = itemIndex + 1; i < layout.length; i++) {
|
|
const otherItem = layout[i];
|
|
// Ignore static items
|
|
if (otherItem.static) continue;
|
|
|
|
// Optimization: we can break early if we know we're past this el
|
|
// We can do this b/c it's a sorted layout
|
|
if (otherItem.top > item.top + item.height) break;
|
|
|
|
if (collides(item, otherItem)) {
|
|
resolveCompactionCollision(layout, otherItem, moveToCoord + item[sizeProp], axis);
|
|
}
|
|
}
|
|
|
|
item[axis] = moveToCoord;
|
|
}
|
|
|
|
const heightWidth = { x: 'width', y: 'height' };
|
|
|
|
function sortLayoutItems(layout, compactType) {
|
|
if (compactType === 'horizontal') return sortLayoutItemsByColRow(layout);
|
|
if (compactType === 'vertical') return sortLayoutItemsByRowCol(layout);
|
|
else return layout;
|
|
}
|
|
|
|
function sortLayoutItemsByColRow(layout) {
|
|
return layout.slice(0).sort(function (a, b) {
|
|
if (a.left > b.left || (a.left === b.left && a.top > b.top)) {
|
|
return 1;
|
|
}
|
|
return -1;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Sort layout items by top ascending and left ascending.
|
|
*
|
|
* Does not modify the original layout.
|
|
*/
|
|
function sortLayoutItemsByRowCol(layout) {
|
|
// Slice to clone the array as sort modifies the original array
|
|
return layout.slice(0).sort(function (a, b) {
|
|
if (a.top > b.top || (a.top === b.top && a.left > b.left)) {
|
|
return 1;
|
|
} else if (a.top === b.top && a.left === b.left) {
|
|
// Without this, we can get different sort results in IE vs. Chrome/FF
|
|
return 0;
|
|
}
|
|
return -1;
|
|
});
|
|
}
|
|
|
|
function scaleLayouts(layouts, cols = 6) {
|
|
return layouts.map((layout) => ({
|
|
...layout,
|
|
// width: layout.width <= 4 ? 2 : layout.width <= 8 ? 3 : layout.width,
|
|
// width: layout.width <= 10 ? 10 : layout.width <= 20 ? 24 : 43,
|
|
width: layout.width * 3 > 43 ? 43 : layout.width * 3,
|
|
}));
|
|
}
|
|
|
|
export const individualGroupableProps = (element) => {
|
|
if (element?.classList.contains('target2')) {
|
|
return {
|
|
resizable: false,
|
|
};
|
|
}
|
|
};
|
|
|
|
export const handleWidgetResize = (e, list, boxes, gridWidth) => {
|
|
const currentLayout = list.find(({ id }) => id === e.target.id);
|
|
const currentWidget = boxes.find(({ id }) => id === e.target.id);
|
|
let _gridWidth = useGridStore.getState().subContainerWidths[currentWidget.component?.parent] || gridWidth;
|
|
document.getElementById('canvas-' + currentWidget.component?.parent)?.classList.add('show-grid');
|
|
const currentWidth = currentLayout.width * _gridWidth;
|
|
const diffWidth = e.width - currentWidth;
|
|
const diffHeight = e.height - currentLayout.height;
|
|
const isLeftChanged = e.direction[0] === -1;
|
|
const isTopChanged = e.direction[1] === -1;
|
|
|
|
let transformX = currentLayout.left * _gridWidth;
|
|
let transformY = currentLayout.top;
|
|
if (isLeftChanged) {
|
|
transformX = currentLayout.left * _gridWidth - diffWidth;
|
|
}
|
|
if (isTopChanged) {
|
|
transformY = currentLayout.top - diffHeight;
|
|
}
|
|
|
|
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;
|
|
transformY = transformY < 0 ? 0 : transformY > maxY ? maxY : transformY;
|
|
transformX = transformX < 0 ? 0 : transformX > maxLeft ? maxLeft : transformX;
|
|
|
|
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)`;
|
|
};
|
|
|
|
export function getMouseDistanceFromParentDiv(event, id, parentWidgetType) {
|
|
let parentDiv = document.getElementById('canvas-' + id) || document.getElementById('real-canvas');
|
|
// Get the bounding rectangle of the parent div.
|
|
const parentDivRect = parentDiv.getBoundingClientRect();
|
|
const targetDivRect = event.target.getBoundingClientRect();
|
|
|
|
const mouseX = targetDivRect.left - parentDivRect.left;
|
|
const mouseY = targetDivRect.top - parentDivRect.top;
|
|
|
|
// Calculate the distance from the mouse pointer to the top and left edges of the parent div.
|
|
const top = mouseY;
|
|
const left = mouseX;
|
|
|
|
return { top, left };
|
|
}
|
|
|
|
export function findHighestLevelofSelection(_selectedComponents) {
|
|
const selectedComponents = _selectedComponents || useStore.getState().getSelectedComponentsDefinition();
|
|
let result = [];
|
|
if (selectedComponents.some((widget) => !widget?.component?.parent)) {
|
|
result = selectedComponents.filter((widget) => !widget?.component?.parent);
|
|
} else {
|
|
result = selectedComponents.filter(
|
|
(widget) => widget?.component?.parent === selectedComponents[0]?.component?.parent
|
|
);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
export function findChildrenAndGrandchildren(parentId, widgets) {
|
|
if (isEmpty(widgets)) {
|
|
return [];
|
|
}
|
|
const children = widgets.filter((widget) => widget?.component?.parent?.startsWith(parentId));
|
|
let result = [];
|
|
for (const child of children) {
|
|
result.push(child.id);
|
|
result = result.concat(...findChildrenAndGrandchildren(child.id, widgets));
|
|
}
|
|
return result;
|
|
}
|
|
|
|
export function adjustWidth(width, posX, gridWidth) {
|
|
posX = Math.round(posX / gridWidth);
|
|
width = Math.round(width / gridWidth);
|
|
if (posX + width > 43) {
|
|
width = 43 - posX;
|
|
}
|
|
return width * gridWidth;
|
|
}
|
|
|
|
export function getPositionForGroupDrag(events, parentWidth, parentHeight) {
|
|
return events.reduce((positions, ev) => {
|
|
const eventObj = ev.lastEvent ? ev.lastEvent : ev;
|
|
const { width, height } = eventObj;
|
|
|
|
const {
|
|
translate: [elemPosX, elemPosY],
|
|
} = eventObj.drag ? eventObj.drag : eventObj;
|
|
|
|
return {
|
|
...positions,
|
|
posRight: Math.min(
|
|
positions.posRight ?? Infinity, // Handle potential initial undefined value
|
|
parentWidth - (width + elemPosX)
|
|
),
|
|
posBottom: Math.min(positions.posBottom ?? Infinity, parentHeight - (height + elemPosY)),
|
|
posLeft: Math.min(positions.posLeft ?? Infinity, elemPosX),
|
|
posTop: Math.min(positions.posTop ?? Infinity, elemPosY),
|
|
};
|
|
}, {});
|
|
}
|
|
|
|
export function getOffset(childElement, grandparentElement) {
|
|
if (!childElement || !grandparentElement) return null;
|
|
|
|
// Get bounding rectangles for both elements
|
|
const childRect = childElement.getBoundingClientRect();
|
|
const grandparentRect = grandparentElement.getBoundingClientRect();
|
|
|
|
// Calculate offset by subtracting grandparent's position from child's position
|
|
const offsetX = childRect.left - grandparentRect.left;
|
|
const offsetY = childRect.top - grandparentRect.top;
|
|
|
|
return { x: offsetX, y: offsetY };
|
|
}
|
|
|
|
export function hasParentWithClass(child, className) {
|
|
let currentElement = child;
|
|
|
|
while (currentElement !== null && currentElement !== document.documentElement) {
|
|
if (currentElement.classList.contains(className)) {
|
|
return true;
|
|
}
|
|
currentElement = currentElement.parentElement;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
export function showGridLines() {
|
|
var canvasElms = document.getElementsByClassName('real-canvas');
|
|
// Filter out module canvas
|
|
var elementsArray = Array.from(canvasElms).filter((element) => {
|
|
if (element.classList.contains('module-container')) {
|
|
return false;
|
|
}
|
|
if (
|
|
!element.classList.contains('is-module-editor') &&
|
|
element.getAttribute('component-type') === 'ModuleContainer'
|
|
) {
|
|
return false;
|
|
}
|
|
return true;
|
|
});
|
|
elementsArray.forEach(function (element) {
|
|
element.classList.remove('hide-grid');
|
|
element.classList.add('show-grid');
|
|
});
|
|
}
|
|
|
|
export function hideGridLines() {
|
|
var canvasElms = document.getElementsByClassName('real-canvas');
|
|
var elementsArray = Array.from(canvasElms);
|
|
elementsArray.forEach(function (element) {
|
|
element.classList.remove('show-grid');
|
|
element.classList.add('hide-grid');
|
|
});
|
|
}
|
|
|
|
export function showGridLinesOnSlot(slotId) {
|
|
var canvasElm = document.getElementById(`canvas-${slotId}`);
|
|
|
|
canvasElm.classList.remove('hide-grid');
|
|
canvasElm.classList.add('show-grid');
|
|
}
|
|
|
|
export function hideGridLinesOnSlot(slotId) {
|
|
var canvasElm = document.getElementById(`canvas-${slotId}`);
|
|
|
|
canvasElm.classList.remove('show-grid');
|
|
canvasElm.classList.add('hide-grid');
|
|
}
|
|
|
|
// Track previously active elements for efficient cleanup
|
|
let previousActiveWidgets = null;
|
|
let previousActiveCanvas = null;
|
|
let processedComponents = new Set();
|
|
|
|
export const handleActivateNonDraggingComponents = () => {
|
|
// Get viewport bounds once
|
|
const viewport = {
|
|
top: 0,
|
|
left: 0,
|
|
bottom: window.innerHeight,
|
|
right: window.innerWidth,
|
|
};
|
|
|
|
const components = document.getElementsByClassName('moveable-box');
|
|
|
|
for (let i = 0; i < components.length; i++) {
|
|
const component = components[i];
|
|
|
|
// Skip if already processed or is active target
|
|
if (processedComponents.has(component) || component.classList.contains('active-target')) {
|
|
continue;
|
|
}
|
|
|
|
// Quick visibility check - only get rect if needed
|
|
const rect = component.getBoundingClientRect();
|
|
|
|
if (
|
|
rect.bottom > viewport.top &&
|
|
rect.top < viewport.bottom &&
|
|
rect.right > viewport.left &&
|
|
rect.left < viewport.right
|
|
) {
|
|
component.classList.add('non-dragging-component');
|
|
processedComponents.add(component);
|
|
}
|
|
}
|
|
};
|
|
|
|
// Clear cache when drag ends
|
|
export const clearNonDraggingComponentsCache = () => {
|
|
processedComponents.clear();
|
|
document.querySelectorAll('.non-dragging-component').forEach((component) => {
|
|
component.classList.remove('non-dragging-component');
|
|
});
|
|
};
|
|
|
|
export const handleActivateTargets = (parentId) => {
|
|
const WIDGETS_WITH_CANVAS_OUTLINE = ['Container', 'Modal', 'Form', 'Listview', 'Kanban', 'ModalV2'];
|
|
|
|
const newParentType = document.getElementById('canvas-' + parentId)?.getAttribute('component-type');
|
|
let _parentId = parentId;
|
|
if (newParentType === 'Tabs') {
|
|
_parentId = getTabId(parentId);
|
|
} else if (WIDGETS_WITH_CANVAS_OUTLINE.includes(newParentType)) {
|
|
_parentId = getSubContainerIdWithSlots(parentId);
|
|
}
|
|
|
|
// Clean up previous active elements
|
|
if (previousActiveWidgets) {
|
|
previousActiveWidgets.classList.remove('dragging-component-canvas');
|
|
previousActiveWidgets = null;
|
|
}
|
|
|
|
if (previousActiveCanvas) {
|
|
previousActiveCanvas.classList.remove('dragging-component-canvas');
|
|
previousActiveCanvas = null;
|
|
}
|
|
|
|
const parentComponent = document.getElementById(_parentId);
|
|
if (!parentComponent) return;
|
|
|
|
if (WIDGETS_WITH_CANVAS_OUTLINE?.includes(newParentType)) {
|
|
// If it's multiple canvas in single widget, highlight the specific canvas
|
|
const canvasElm = document.getElementById('canvas-' + parentId);
|
|
if (canvasElm) {
|
|
canvasElm.classList.add('dragging-component-canvas');
|
|
previousActiveCanvas = canvasElm;
|
|
}
|
|
} else {
|
|
// Otherwise highlight the component box
|
|
parentComponent.classList.remove('non-dragging-component');
|
|
parentComponent.classList.add('dragging-component-canvas');
|
|
previousActiveWidgets = parentComponent;
|
|
}
|
|
};
|
|
|
|
export const handleDeactivateTargets = () => {
|
|
if (previousActiveWidgets) {
|
|
previousActiveWidgets.classList.remove('dragging-component-canvas');
|
|
previousActiveWidgets = null;
|
|
}
|
|
|
|
if (previousActiveCanvas) {
|
|
previousActiveCanvas.classList.remove('dragging-component-canvas');
|
|
previousActiveCanvas = null;
|
|
}
|
|
clearNonDraggingComponentsCache();
|
|
document.querySelectorAll('.non-dragging-component').forEach((component) => {
|
|
component.classList.remove('non-dragging-component');
|
|
});
|
|
};
|
|
export const computeScrollDeltaOnDrag = (canvasId) => {
|
|
// Only need to calculate scroll delta when moving from a sub-container
|
|
if (canvasId !== 'real-canvas') {
|
|
const subContainerWrap = document.getElementById(`canvas-${canvasId}`);
|
|
|
|
return subContainerWrap?.scrollTop || 0;
|
|
}
|
|
|
|
// Default case: No scroll adjustment needed
|
|
return 0;
|
|
};
|
|
|
|
export const getDraggingWidgetWidth = (canvasParentId, widgetWidth) => {
|
|
const targetCanvasWidth = document.getElementById(`canvas-${canvasParentId}`)?.offsetWidth || 0;
|
|
const gridUnitWidth = targetCanvasWidth / NO_OF_GRIDS;
|
|
const gridUnits = Math.round(widgetWidth / gridUnitWidth);
|
|
const draggingWidgetWidth = gridUnits * gridUnitWidth;
|
|
return draggingWidgetWidth;
|
|
};
|
|
|
|
/**
|
|
* Positions a ghost/feedback element relative to the main canvas
|
|
* @param {HTMLElement} targetElement - The element being dragged/resized
|
|
* @param {string} ghostElementId - The ID of the ghost element to position
|
|
*/
|
|
export const positionGhostElement = (targetElement, ghostElementId) => {
|
|
const ghostElement = document.getElementById(ghostElementId);
|
|
|
|
if (!ghostElement || !targetElement) return;
|
|
|
|
const mainCanvas = document.getElementById('real-canvas');
|
|
if (!mainCanvas) return;
|
|
|
|
const mainCanvasRect = mainCanvas.getBoundingClientRect();
|
|
const targetRect = targetElement.getBoundingClientRect();
|
|
|
|
// Calculate position relative to main canvas
|
|
const relativeLeft = targetRect.left - mainCanvasRect.left;
|
|
const relativeTop = targetRect.top - mainCanvasRect.top;
|
|
|
|
// Apply the position
|
|
ghostElement.style.left = `${relativeLeft}px`;
|
|
ghostElement.style.top = `${relativeTop}px`;
|
|
ghostElement.style.width = `${targetRect.width}px`;
|
|
ghostElement.style.height = `${targetRect.height}px`;
|
|
};
|
|
|
|
/**
|
|
* Calculates the unified bounding box for a group of elements
|
|
* @param {HTMLElement[]} targetElements - Array of elements being dragged as a group
|
|
* @returns {Object} - Bounding box with left, top, width, height relative to main canvas
|
|
*/
|
|
export const calculateGroupBoundingBox = (targetElements) => {
|
|
if (!targetElements || targetElements.length === 0) return null;
|
|
|
|
const mainCanvas = document.getElementById('real-canvas');
|
|
if (!mainCanvas) return null;
|
|
|
|
const mainCanvasRect = mainCanvas.getBoundingClientRect();
|
|
|
|
// Initialize with extreme values
|
|
let minLeft = Infinity;
|
|
let minTop = Infinity;
|
|
let maxRight = -Infinity;
|
|
let maxBottom = -Infinity;
|
|
|
|
// Find the bounds of all elements
|
|
targetElements.forEach((element) => {
|
|
if (!element) return;
|
|
|
|
const rect = element.getBoundingClientRect();
|
|
const relativeLeft = rect.left - mainCanvasRect.left;
|
|
const relativeTop = rect.top - mainCanvasRect.top;
|
|
const relativeRight = relativeLeft + rect.width;
|
|
const relativeBottom = relativeTop + rect.height;
|
|
|
|
minLeft = Math.min(minLeft, relativeLeft);
|
|
minTop = Math.min(minTop, relativeTop);
|
|
maxRight = Math.max(maxRight, relativeRight);
|
|
maxBottom = Math.max(maxBottom, relativeBottom);
|
|
});
|
|
|
|
return {
|
|
left: minLeft,
|
|
top: minTop,
|
|
width: maxRight - minLeft,
|
|
height: maxBottom - minTop,
|
|
};
|
|
};
|
|
|
|
/**
|
|
* Positions a ghost element to cover the entire bounding box of a group
|
|
* @param {Object} boundingBox - Bounding box with left, top, width, height
|
|
* @param {string} ghostElementId - The ID of the ghost element to position
|
|
*/
|
|
export const positionGroupGhostElement = (events, ghostElementId, gridWidth) => {
|
|
if (!events || events.length === 0) return;
|
|
|
|
const boundingBox = calculateGroupBoundingBox(events.map((e) => e.target));
|
|
const ghostElement = document.getElementById(ghostElementId);
|
|
|
|
if (!ghostElement || !boundingBox) return;
|
|
ghostElement.style.width = `${boundingBox.width}px`;
|
|
ghostElement.style.height = `${boundingBox.height}px`;
|
|
ghostElement.style.willChange = 'transform';
|
|
|
|
ghostElement.style.transform = `translate(${boundingBox.left}px, ${boundingBox.top}px)`;
|
|
};
|
|
|
|
/**
|
|
* Finds the new parent ID based on the current mouse position during drag operations
|
|
* @param {number} clientX - The X coordinate of the mouse position
|
|
* @param {number} clientY - The Y coordinate of the mouse position
|
|
* @param {string} currentTargetId - The ID of the currently dragged element to exclude from search
|
|
* @returns {string|null} - The new parent ID or null if no valid parent is found
|
|
*/
|
|
export const findNewParentIdFromMousePosition = (clientX, clientY, currentTargetId) => {
|
|
if (!document.elementFromPoint(clientX, clientY)) {
|
|
return null;
|
|
}
|
|
|
|
const targetElems = document.elementsFromPoint(clientX, clientY);
|
|
const draggedOverElements = targetElems.filter(
|
|
(ele) => (ele.id !== currentTargetId && ele.classList.contains('target')) || ele.classList.contains('real-canvas')
|
|
);
|
|
|
|
const draggedOverElem = draggedOverElements.find((ele) => ele.classList.contains('target'));
|
|
const draggedOverContainer = draggedOverElements.find((ele) => ele.classList.contains('real-canvas'));
|
|
|
|
// Determine potential new parent
|
|
const newParentId = draggedOverContainer?.getAttribute('data-parentId') || draggedOverElem?.id;
|
|
|
|
return newParentId || null;
|
|
};
|
|
|
|
export const clearActiveTargetClassNamesAfterSnapping = (selectedComponents) => {
|
|
const components = Array.from(document.querySelectorAll('.active-target')).filter(
|
|
(component) => !selectedComponents.includes(component.getAttribute('widgetid'))
|
|
);
|
|
if (components.length > 0) {
|
|
for (const component of components) {
|
|
component?.classList?.remove('active-target');
|
|
}
|
|
}
|
|
};
|
|
|
|
export const updateDashedBordersOnHover = (targetId) => {
|
|
const dynamicHeight = useStore.getState().checkHoveredComponentDynamicHeight(targetId);
|
|
const targetMoveableBox = document.querySelector(`.moveable-control-box[target-id="${targetId}"]`);
|
|
if (targetMoveableBox && dynamicHeight && !targetMoveableBox.classList.contains('moveable-dynamic-height')) {
|
|
targetMoveableBox.classList.add('moveable-dynamic-height');
|
|
} else if (targetMoveableBox && !dynamicHeight) {
|
|
targetMoveableBox.classList.remove('moveable-dynamic-height');
|
|
}
|
|
};
|
|
|
|
// Check if dropping from modal to canvas, including nested containers within modals
|
|
export const isDraggingModalToCanvas = (source, target, boxList) => {
|
|
if (!source.isModal) return false;
|
|
|
|
// If target is the same as source, it's not modal to canvas
|
|
if (source.id === target.id) return false;
|
|
|
|
// Check if target or any of its parents is a modal
|
|
let currentTargetId = target.id;
|
|
while (currentTargetId && currentTargetId !== 'canvas') {
|
|
const targetComponent = boxList.find((b) => b.id === currentTargetId);
|
|
if (!targetComponent) break;
|
|
|
|
// If we find a modal in the parent chain, it's not modal to canvas
|
|
if (source.id === targetComponent.parent) return false;
|
|
|
|
currentTargetId = targetComponent.parent;
|
|
}
|
|
|
|
// If we've reached canvas without finding a modal parent, it's modal to canvas
|
|
return currentTargetId === 'canvas' || currentTargetId === null;
|
|
};
|
|
|
|
export const updateDashedBordersOnDragResize = (targetId, moveableControlBoxClassList) => {
|
|
const hasDynamicHeight = useStore.getState().checkHoveredComponentDynamicHeight(targetId);
|
|
if (hasDynamicHeight && !moveableControlBoxClassList?.contains('moveable-dynamic-height')) {
|
|
moveableControlBoxClassList?.add('moveable-dynamic-height');
|
|
} else if (moveableControlBoxClassList?.contains('moveable-dynamic-height') && !hasDynamicHeight) {
|
|
moveableControlBoxClassList?.remove('moveable-dynamic-height');
|
|
}
|
|
};
|