This is the code for my main page:
This is my Section Element:
// SectionElement.js
import { memo, useEffect } from "react";
import { Rnd } from "react-rnd";
const hexToRgba = (hex, opacity) => {
hex = hex.replace("#", "");
if (hex.length === 3) {
hex = hex
.split("")
.map((h) => h + h)
.join("");
}
const bigint = parseInt(hex, 16);
const r = (bigint >> 16) & 255;
const g = (bigint >> 8) & 255;
const b = bigint & 255;
return `rgba(${r}, ${g}, ${b}, ${opacity})`;
};
const SectionElement = ({
element,
updateElement,
setSelectedElements,
handleDrag,
handleDragStop,
handleResize,
handleResizeStop,
isEditor,
setSelectedCanvas,
isSelected,
selectedElements,
isGroupSelected,
isSelecting,
currentBreakpoint,
computeCanvasHeight,
canvasWidth,
children,
hoveredSectionId,
aboveSection,
belowSection
}) => {
const elementProperties = element.properties[currentBreakpoint] || {};
const styles = elementProperties.styles || {};
const isHovered = element.id === hoveredSectionId;
const isHeader = element.variant === "header";
const isFooter = element.variant === "footer";
// Remove constraints on xPosition and width
const xPosition = elementProperties.x !== undefined ? elementProperties.x : 0;
const yPosition = elementProperties.y !== undefined ? elementProperties.y : 0;
// Use elementProperties.width and elementProperties.height
// SectionElement.js
const sectionWidth = parseInt(styles.width) || 600;
const sectionHeight = parseInt(styles.height) || 600;
const onDrag = (e, d) => {
handleDrag(element.id, d.x, d.y, sectionWidth, sectionHeight);
};
const onDragStopHandler = (e, d) => {
handleDragStop(element.id, d.x, d.y, sectionWidth, sectionHeight);
};
const enableResizing =
isSelected && !isGroupSelected && !isSelecting
? {
top: true,
right: true,
bottom: true,
left: true,
topRight: true,
topLeft: true,
bottomRight: true,
bottomLeft: true,
}
: false;
const getBorderStyle = (width, color) => {
return width > 0 ? `${width}px solid ${color}` : "none";
};
// Adjust background image rendering
const backgroundImageLayer =
!isHeader && !isFooter && styles.backgroundImage ? (
<div
style={{
backgroundImage: `url(${styles.backgroundImage})`,
backgroundSize: styles.backgroundSize || "cover",
backgroundRepeat: styles.backgroundRepeat || "no-repeat",
backgroundPosition: styles.backgroundPosition || "center",
opacity:
styles.backgroundImageOpacity !== undefined
? styles.backgroundImageOpacity
: 1,
position: "absolute",
top: 0,
left: 0,
width: "100%",
height: "100%",
zIndex: 0,
}}
/>
) : null;
const canvasHeight = computeCanvasHeight();
const onResize = (e, direction, ref, delta, position) => {
let newHeight = parseInt(ref.style.height);
let newY = position.y;
if (direction.includes("bottom")) {
// Resizing from the bottom, limit height by the section below or canvas bottom
if (belowSection) {
const maxHeight = belowSection.y - yPosition;
if (newHeight > maxHeight) {
newHeight = maxHeight;
ref.style.height = `${newHeight}px`;
}
} else {
const maxHeight = canvasHeight - yPosition;
if (newHeight > maxHeight) {
newHeight = maxHeight;
ref.style.height = `${newHeight}px`;
}
}
}
if (direction.includes("top")) {
// Resizing from the top, limit by section above or canvas top (opposite logic)
let minY = aboveSection ? aboveSection.y + aboveSection.height : 0;
if (newY < minY) {
newY = minY; // Prevent dragging above the section above
}
// Adjust height based on the new top position
newHeight = yPosition + sectionHeight - newY;
if (newHeight > sectionHeight) {
// Ensure the section doesn't shrink below the initial height
ref.style.height = `${newHeight}px`;
}
// Update the position to the new Y
position.y = newY;
}
handleResize(element.id, direction, ref, delta, position);
};
const onResizeStop = (e, direction, ref, delta, position) => {
onResize(e, direction, ref, delta, position);
handleResizeStop(element.id, direction, ref, delta, position);
};
return (
<Rnd
size={{
width: sectionWidth,
height: sectionHeight,
}}
position={{
x: xPosition,
y: yPosition,
}}
onDrag={onDrag}
onDragStop={onDragStopHandler}
onResize={onResize}
onResizeStop={onResizeStop}
enableResizing={enableResizing}
disableDragging={isGroupSelected || isSelecting}
bounds=".editor-canvas-container"
style={{
zIndex: isHovered ? -1 : 0,
position: "absolute",
pointerEvents: isGroupSelected ? "none" : "auto",
}}
resizeHandleStyles={
{
// Optional: Customize resize handle styles
}
}
cancel=".section-content"
// This tells Rnd not to drag when interacting with elements inside `.section-content`
>
<div
className={`${isSelected ? "ring-2 ring-blue-500" : ""} ${
isHovered ? "border-4 border-dashed border-green-500" : ""
}`}
style={{
width: "100%",
height: "100%",
position: "relative",
overflow: "visible",
boxSizing: "border-box",
backgroundColor: styles.backgroundColor
? hexToRgba(
styles.backgroundColor,
styles.backgroundOpacity !== undefined
? styles.backgroundOpacity
: 1
)
: "transparent",
borderTop: getBorderStyle(
styles.borderTopWidth,
styles.borderTopColor
),
borderRight: getBorderStyle(
styles.borderRightWidth,
styles.borderRightColor
),
borderBottom: getBorderStyle(
styles.borderBottomWidth,
styles.borderBottomColor
),
borderLeft: getBorderStyle(
styles.borderLeftWidth,
styles.borderLeftColor
),
borderRadius: styles.borderRadius || "0px", // Add this line
zIndex: isSelected ? 20 : isHovered ? 10 : 0,
}}
onClick={(e) => {
e.stopPropagation();
setSelectedElements([element.id]);
setSelectedCanvas(false);
}}
>
{/* Background Image Layer */}
{backgroundImageLayer}
{/* Content of the section */}
<div
className="section-content"
style={{ position: "relative", zIndex: 1 }}
onClick={(e) => {
e.stopPropagation(); // Prevent clicks on children from selecting them
}}
>
{children}
</div>
{isHovered && (
<div
style={{
position: "absolute",
top: 0,
left: 0,
width: "100%",
height: "100%",
backgroundColor: "rgba(0, 255, 0, 0.1)",
zIndex: 5,
}}
/>
)}
</div>
</Rnd>
);
};
export default memo(SectionElement);
This is my EditoCanvas:
// components/EditorCanvas.js
import { useState, useRef, useEffect } from "react";
import TextElement from "./TextElement";
import ImageElement from "./ImageElement";
import ButtonElement from "./ButtonElement";
import SectionElement from "./SectionElement";
import FormElement from './FormElement';
import MenuElement from "./MenuElement";
import { Rnd } from "react-rnd";
import useElementInteractions from "../hooks/useElementInteractions";
import { toast } from "react-toastify";
import AnchorElement from './AnchorElement';
function hexToRGBA(hex, opacity) {
let r = 0,
g = 0,
b = 0;
if (hex.length === 4) {
r = "0x" + hex[1] + hex[1];
g = "0x" + hex[2] + hex[2];
b = "0x" + hex[3] + hex[3];
} else if (hex.length === 7) {
r = "0x" + hex[1] + hex[2];
g = "0x" + hex[3] + hex[4];
b = "0x" + hex[5] + hex[6];
}
return `rgba(${+r},${+g},${+b},${opacity})`;
}
const EditorCanvas = ({
elements,
setElements,
selectedElements,
updateElement,
setSelectedElements,
setSelectedCanvas,
canvasBackgroundColor,
canvasBackgroundImage,
canvasBackgroundSize,
canvasBackgroundRepeat,
canvasBackgroundPosition,
canvasBackgroundOpacity,
canvasBackgroundImageOpacity,
pushToHistory,
currentBreakpoint,
canvasHeightAdjustment
}) => {
const [isCanvasSelected, setIsCanvasSelected] = useState(false);
const canvasContainerRef = useRef(null);
const [selectionBox, setSelectionBox] = useState(null);
const [isSelecting, setIsSelecting] = useState(false);
const selectionStart = useRef(null);
const handleParentChange = (elementId, oldParentId, newParentId) => {
const oldParent = elements.find((el) => el.id === oldParentId);
const newParent = elements.find((el) => el.id === newParentId);
const getParentName = (parent) => {
if (!parent) return "canvas";
if (parent.variant === "header") return "header";
if (parent.variant === "footer") return "footer";
if (parent.type === "section") return "section";
return parent.type;
};
const oldParentName = getParentName(oldParent);
const newParentName = getParentName(newParent);
if (newParentId && !oldParentId) {
toast.success(`Element added to ${newParentName}`);
} else if (!newParentId && oldParentId) {
toast.success(`Element removed from ${oldParentName}`);
} else if (newParentId !== oldParentId) {
toast.success(`Element moved from ${oldParentName} to ${newParentName}`);
}
};
const canvasWidths = {
desktop: 1200,
tablet: 800,
mobile: 375,
};
// In EditorCanvas.js
const computeCanvasHeight = () => {
const baseHeight = 800; // Default base canvas height
const headerHeight = elements.some(el => el.variant === "header") ? 80 : 0; // Adjust header height
return baseHeight + canvasHeightAdjustment + headerHeight; // Adjusted height with header
};
const {
helperLines,
handleDrag,
handleDragStop,
handleResize,
handleDragStart,
handleResizeStop,
groupBoundingBox,
handleGroupDragStart,
handleGroupDrag,
handleGroupDragStop,
handleGroupResizeStart,
handleGroupResize,
handleGroupResizeStop,
hoveredSectionId,
isDraggingElement,
draggingElementId,
} = useElementInteractions(
elements,
setElements,
selectedElements,
setSelectedElements,
updateElement,
pushToHistory,
canvasContainerRef,
currentBreakpoint,
handleParentChange
);
const handleOuterContainerClick = (e) => {
const isInsideModal = e.target.closest("[data-modal]");
if (!canvasContainerRef.current?.contains(e.target) && !isInsideModal) {
setSelectedElements([]);
setSelectedCanvas(false);
}
};
useEffect(() => {
// Load all unique fonts used in elements
const uniqueFonts = Array.from(
new Set(elements.map((el) => el.styles?.fontFamily).filter(Boolean))
);
if (uniqueFonts.length > 0 && typeof window !== "undefined") {
import("webfontloader").then((WebFont) => {
WebFont.load({
google: {
families: uniqueFonts,
},
});
});
}
}, [elements]);
useEffect(() => {
document.addEventListener("mousedown", handleOuterContainerClick);
return () => {
document.removeEventListener("mousedown", handleOuterContainerClick);
};
}, []);
const handleCanvasClick = (e) => {
e.stopPropagation();
setSelectedCanvas(true);
setSelectedElements([]);
};
const [isDraggingSelection, setIsDraggingSelection] = useState(false);
const dragStart = useRef(null);
useEffect(() => {
const disableTextSelection = (e) => {
if (isSelecting) {
e.preventDefault();
}
};
document.addEventListener("selectstart", disableTextSelection);
return () => {
document.removeEventListener("selectstart", disableTextSelection);
};
}, [isSelecting]);
const handleMouseDown = (e) => {
if (e.button !== 0) return;
if (e.target !== canvasContainerRef.current) return;
dragStart.current = { x: e.clientX, y: e.clientY };
setIsDraggingSelection(false);
setSelectedCanvas(true);
setSelectedElements([]);
document.body.style.userSelect = "none";
};
const handleMouseMove = (e) => {
if (dragStart.current === null) return;
const dx = e.clientX - dragStart.current.x;
const dy = e.clientY - dragStart.current.y;
if (!isDraggingSelection && (Math.abs(dx) > 5 || Math.abs(dy) > 5)) {
setIsDraggingSelection(true);
setIsSelecting(true);
setSelectedCanvas(false);
const rect = canvasContainerRef.current.getBoundingClientRect();
const x = dragStart.current.x - rect.left;
const y = dragStart.current.y - rect.top;
selectionStart.current = { x, y };
setSelectionBox({ x, y, width: 0, height: 0 });
}
if (isDraggingSelection) {
e.preventDefault();
const rect = canvasContainerRef.current.getBoundingClientRect();
const currentX = e.clientX - rect.left;
const currentY = e.clientY - rect.top;
const startX = selectionStart.current.x;
const startY = selectionStart.current.y;
const x = Math.min(currentX, startX);
const y = Math.min(currentY, startY);
const width = Math.abs(currentX - startX);
const height = Math.abs(currentY - startY);
setSelectionBox({ x, y, width, height });
}
};
const handleMouseUp = (e) => {
if (isDraggingSelection) {
e.preventDefault();
setIsSelecting(false);
setIsDraggingSelection(false);
dragStart.current = null;
const selectedIds = elements
.filter((el) => {
const breakpointProps = el.properties[currentBreakpoint] || {};
const elX = breakpointProps.x || 0;
const elY = breakpointProps.y || 0;
const elWidth = parseInt(breakpointProps.styles?.width) || 0;
const elHeight = parseInt(breakpointProps.styles?.height) || 0;
return (
elX < selectionBox.x + selectionBox.width &&
elX + elWidth > selectionBox.x &&
elY < selectionBox.y + selectionBox.height &&
elY + elHeight > selectionBox.y
);
})
.map((el) => el.id);
if (e.shiftKey) {
setSelectedElements([
...new Set([...selectedElements, ...selectedIds]),
]);
} else {
setSelectedElements(selectedIds);
}
setSelectionBox(null);
} else {
dragStart.current = null;
}
document.body.style.userSelect = "";
};
useEffect(() => {
const handleWindowMouseMove = (e) => {
handleMouseMove(e);
};
const handleWindowMouseUp = (e) => {
handleMouseUp(e);
};
window.addEventListener("mousemove", handleWindowMouseMove);
window.addEventListener("mouseup", handleWindowMouseUp);
return () => {
window.removeEventListener("mousemove", handleWindowMouseMove);
window.removeEventListener("mouseup", handleWindowMouseUp);
};
}, [isDraggingSelection, isSelecting, selectionBox, selectedElements]);
const canvasWidth = canvasWidths[currentBreakpoint];
const buildElementTree = (elements) => {
const elementsById = {};
const rootElements = [];
// Initialize elementsById with an empty children array
elements.forEach((el) => {
elementsById[el.id] = { ...el, children: [] };
});
// Build the tree
elements.forEach((el) => {
if (el.parentId) {
const parent = elementsById[el.parentId];
if (parent) {
parent.children.push(elementsById[el.id]);
} else {
rootElements.push(elementsById[el.id]);
}
} else {
rootElements.push(elementsById[el.id]);
}
});
return rootElements;
};
// Compute positions of all sections
const sections = elements
.filter((el) => el.type === "section")
.map((section) => {
const properties = section.properties[currentBreakpoint] || {};
const x = properties.x || 0;
const y = properties.y || 0;
const styles = properties.styles || {};
const width = parseInt(styles.width) || 600;
const height = parseInt(styles.height) || 600;
return { id: section.id, x, y, width, height };
});
// Sort sections by their y position
sections.sort((a, b) => a.y - b.y);
// Map section IDs to their index in the sorted array
const sectionIndexMap = {};
sections.forEach((section, index) => {
sectionIndexMap[section.id] = index;
});
const renderElement = (element) => {
const isSelected = selectedElements.includes(element.id);
const boundHandleResize = (e, direction, ref, delta, position) =>
handleResize(element.id, direction, ref, delta, position);
const boundHandleResizeStop = (e, direction, ref, delta, position) =>
handleResizeStop(element.id, direction, ref, delta, position);
const commonProps = {
key: element.id,
element: element,
updateElement: updateElement,
setSelectedElements: setSelectedElements,
handleDrag: handleDrag,
handleDragStop: handleDragStop,
handleResize: boundHandleResize,
handleResizeStop: boundHandleResizeStop,
isSelected: isSelected,
setSelectedCanvas: setSelectedCanvas,
selectedElements: selectedElements,
isSelecting: isSelecting, // Pass isSelecting
isGroupSelected: groupBoundingBox !== null,
currentBreakpoint: currentBreakpoint,
canvasWidth: canvasWidth,
hoveredSectionId,
isDraggingElement,
draggingElementId,
handleDragStart,
canvasContainerRef
};
let elementComponent;
switch (element.type) {
case "text":
elementComponent = <TextElement {...commonProps} isEditor={true} />;
break;
case "image":
elementComponent = <ImageElement {...commonProps} isEditor={true} />;
break;
case "button":
elementComponent = <ButtonElement {...commonProps} isEditor={true} />;
break;
case "section":
const index = sectionIndexMap[element.id];
const aboveSection = index > 0 ? sections[index - 1] : null;
const belowSection = index < sections.length - 1 ? sections[index + 1] : null;
elementComponent = (
<SectionElement
{...commonProps}
isEditor={true}
computeCanvasHeight={computeCanvasHeight}
aboveSection={aboveSection}
belowSection={belowSection}
>
{element.children.map((child) => renderElement(child))}
</SectionElement>
);
break;
case 'menu':
elementComponent = <MenuElement {...commonProps} isEditor={true} />;
break;
case 'form':
elementComponent = (
<FormElement
{...commonProps}
isEditor={true}
canvasWidth={canvasWidth}
/>
);
break;
case 'anchor':
elementComponent = (
<AnchorElement
{...commonProps}
handleDragStart={handleDragStart}
canvasContainerRef={canvasContainerRef}
canvasWidth={canvasWidth} // Pass canvasWidth here
/>
);
break;
default:
return null;
}
return elementComponent;
};
// In the render method
const rootElements = buildElementTree(elements);
return (
<div className="flex-1 bg-white relative overflow-auto p-4">
<div
ref={canvasContainerRef}
className={`editor-canvas-container relative ${
isCanvasSelected ? "outline outline-blue-500" : "outline outline-gray-300"
} rounded-lg mx-auto`}
onClick={handleCanvasClick}
onMouseDown={handleMouseDown}
style={{
boxSizing: "border-box",
backgroundColor: "transparent",
width: `${canvasWidth}px`,
height: `${computeCanvasHeight()}px`,
zIndex: 10,
}}
>
{/* Background Color Layer */}
<div
className="absolute inset-0"
style={{
backgroundColor: hexToRGBA(
canvasBackgroundColor,
canvasBackgroundOpacity
),
zIndex: 0,
pointerEvents: "none",
}}
></div>
{/* Background Image Layer */}
{canvasBackgroundImage && (
<div
className="absolute inset-0"
style={{
backgroundImage: `url(${canvasBackgroundImage})`,
backgroundSize: canvasBackgroundSize,
backgroundRepeat: canvasBackgroundRepeat,
backgroundPosition: canvasBackgroundPosition,
opacity:
canvasBackgroundImageOpacity !== undefined
? canvasBackgroundImageOpacity
: 1,
zIndex: 0,
pointerEvents: "none",
}}
></div>
)}
{/* Render Elements */}
{rootElements.map((element) => renderElement(element))}
{/* Render Helper Lines */}
{helperLines.map((line, index) => {
if (line.type === "spacing") {
if (line.orientation === "vertical") {
return (
<div
key={`spacing-${index}`}
className="absolute flex items-center justify-center text-blue-500"
style={{
left: line.x - 1,
top: line.y1,
height: line.y2 - line.y1,
width: 2,
pointerEvents: "none",
zIndex: 1000,
}}
>
<div
style={{
position: "absolute",
top: 0,
left: -5,
width: 10,
height: 1,
backgroundColor: "blue",
}}
/>
<div
style={{
position: "absolute",
bottom: 0,
left: -5,
width: 10,
height: 1,
backgroundColor: "blue",
}}
/>
</div>
);
} else if (line.orientation === "horizontal") {
return (
<div
key={`spacing-${index}`}
className="absolute flex items-center justify-center text-blue-500"
style={{
top: line.y - 1,
left: line.x1,
width: line.x2 - line.x1,
height: 2,
pointerEvents: "none",
zIndex: 1000,
}}
>
<div
style={{
position: "absolute",
top: -5,
left: 0,
width: 1,
height: 10,
backgroundColor: "blue",
}}
/>
<div
style={{
position: "absolute",
top: -5,
right: 0,
width: 1,
height: 10,
backgroundColor: "blue",
}}
/>
</div>
);
}
} else {
// Existing alignment lines
const lineColor = line.color || 'blue'; // default to blue
const lineThickness = line.thickness || 1; // Use the thickness property, default to 1px
const isVertical = line.orientation === "vertical";
const halfThickness = lineThickness / 2;
return (
<div
key={`helper-${index}`}
className="absolute"
style={{
backgroundColor: lineColor,
[isVertical ? "left" : "top"]: line.position - halfThickness,
[isVertical ? "top" : "left"]: 0,
[isVertical ? "width" : "height"]: `${lineThickness}px`,
[isVertical ? "height" : "width"]: "100%",
pointerEvents: "none",
zIndex: 1000,
}}
/>
);
}
})}
{/* Selection Overlay */}
{isSelecting && (
<div
className="absolute top-0 left-0 w-full h-full"
style={{ zIndex: 9999 }}
>
{/* Render Selection Rectangle */}
{selectionBox && (
<div
className="absolute border border-blue-500 bg-blue-100 opacity-50"
style={{
left: selectionBox.x,
top: selectionBox.y,
width: selectionBox.width,
height: selectionBox.height,
pointerEvents: "none",
}}
/>
)}
</div>
)}
{groupBoundingBox && (
<Rnd
size={{
width: groupBoundingBox.width,
height: groupBoundingBox.height,
}}
position={{ x: groupBoundingBox.x, y: groupBoundingBox.y }}
onDragStart={handleGroupDragStart}
onDrag={(e, d) => handleGroupDrag(e, d)}
onDragStop={(e, d) => handleGroupDragStop(e, d)}
onResizeStart={handleGroupResizeStart}
onResize={(e, direction, ref, delta, position) =>
handleGroupResize(e, direction, ref, delta, position)
}
onResizeStop={(e, direction, ref, delta, position) =>
handleGroupResizeStop(e, direction, ref, delta, position)
}
bounds="parent"
style={{ zIndex: 1000 }}
>
<div
className="group-selection-box"
style={{
width: "100%",
height: "100%",
border: "2px dashed blue",
backgroundColor: "rgba(0, 0, 255, 0.1)",
}}
onClick={(e) => {
e.stopPropagation();
// Do not deselect when clicking on the group selection box
}}
></div>
</Rnd>
)}
</div>
</div>
);
};
export default EditorCanvas;
When I resize a secton from up to down, it stops (as expected) when it encounters another section. However, when resizing a section from down to up, it just continues and doesn’t stop.
What can I do to get it to stop when resizing from down to up as well?
Thank you!
You need to sign in to view this answers
Leave feedback about this