Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
158 changes: 154 additions & 4 deletions _extensions/editable/editable.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,16 +53,20 @@ function getEditableDivs() {
function setupDraggableElt(elt) {
let isDragging = false;
let isResizing = false;
let isRotating = false;
let startX, startY, initialX, initialY, initialWidth, initialHeight;
let initialRotation = 0;
let resizeHandle = null;

const container = createEltContainer(elt);
setupEltStyles(elt);
createResizeHandles(container);
createRotationHandle(container);
setupHoverEffects(
container,
() => isDragging,
() => isResizing
() => isResizing,
() => isRotating
);
attachEventListeners();

Expand Down Expand Up @@ -106,6 +110,78 @@ function setupDraggableElt(elt) {
handle.dataset.position = position;
container.appendChild(handle);
});
}

function createRotationHandle(container) {
const rotateHandle = document.createElement("div");
rotateHandle.className = "rotate-handle";
rotateHandle.style.position = "absolute";
rotateHandle.style.width = "12px";
rotateHandle.style.height = "12px";
rotateHandle.style.backgroundColor = "#28a745";
rotateHandle.style.border = "1px solid #fff";
rotateHandle.style.borderRadius = "50%";
rotateHandle.style.cursor = "grab";
rotateHandle.style.opacity = "0";
rotateHandle.style.transition = "opacity 0.2s";
rotateHandle.style.top = "-20px";
rotateHandle.style.left = "50%";
rotateHandle.style.transform = "translateX(-50%)";
rotateHandle.title = "Rotate element";

container.appendChild(rotateHandle);

function setupHoverEffects(container, isDraggingFn, isResizingFn) {
container.addEventListener("mouseenter", () => {
container.style.border = "2px solid #007cba";
container
.querySelectorAll(".resize-handle, .rotate-handle")
.forEach((h) => (h.style.opacity = "1"));
const fontControls = container.querySelector(".font-controls");
if (fontControls) fontControls.style.opacity = "1";
});

container.addEventListener("mouseleave", () => {
if (!isDraggingFn() && !isResizingFn()) {
container.style.border = "2px solid transparent";
container
.querySelectorAll(".resize-handle, .rotate-handle")
.forEach((h) => (h.style.opacity = "0"));
const fontControls = container.querySelector(".font-controls");
if (fontControls) fontControls.style.opacity = "0";
}
});
}
function setupHoverEffects(
container,
isDraggingFn,
isResizingFn,
isRotatingFn
) {
container.addEventListener("mouseenter", () => {
container.style.border = "2px solid #007cba";
container
.querySelectorAll(".resize-handle, .rotate-handle")
.forEach((h) => (h.style.opacity = "1"));
const rotateHandle = container.querySelector(".rotate-handle");
if (rotateHandle) rotateHandle.style.opacity = "1";
const fontControls = container.querySelector(".font-controls");
if (fontControls) fontControls.style.opacity = "1";
});

container.addEventListener("mouseleave", () => {
if (!isDraggingFn() && !isResizingFn() && !isRotatingFn()) {
container.style.border = "2px solid transparent";
container
.querySelectorAll(".resize-handle, .rotate-handle")
.forEach((h) => (h.style.opacity = "0"));
const rotateHandle = container.querySelector(".rotate-handle");
if (rotateHandle) rotateHandle.style.opacity = "0";
const fontControls = container.querySelector(".font-controls");
if (fontControls) fontControls.style.opacity = "0";
}
});
}

// Create font size controls for div elements
if (elt.tagName.toLowerCase() === "div") {
Expand Down Expand Up @@ -192,7 +268,7 @@ function setupDraggableElt(elt) {
container.addEventListener("mouseenter", () => {
container.style.border = "2px solid #007cba";
container
.querySelectorAll(".resize-handle")
.querySelectorAll(".resize-handle, .rotate-handle")
.forEach((h) => (h.style.opacity = "1"));
const fontControls = container.querySelector(".font-controls");
if (fontControls) fontControls.style.opacity = "1";
Expand All @@ -202,7 +278,7 @@ function setupDraggableElt(elt) {
if (!isDraggingFn() && !isResizingFn()) {
container.style.border = "2px solid transparent";
container
.querySelectorAll(".resize-handle")
.querySelectorAll(".resize-handle, .rotate-handle")
.forEach((h) => (h.style.opacity = "0"));
const fontControls = container.querySelector(".font-controls");
if (fontControls) fontControls.style.opacity = "0";
Expand All @@ -219,6 +295,12 @@ function setupDraggableElt(elt) {
handle.addEventListener("touchstart", startResize);
});

const rotateHandle = container.querySelector(".rotate-handle");
if (rotateHandle) {
rotateHandle.addEventListener("mousedown", startRotate);
rotateHandle.addEventListener("touchstart", startRotate);
}

document.addEventListener("mousemove", handleMouseMove);
document.addEventListener("mouseup", stopAction);
document.addEventListener("touchmove", handleTouchMove);
Expand Down Expand Up @@ -274,11 +356,33 @@ function setupDraggableElt(elt) {
e.stopPropagation();
}

function startRotate(e) {
isRotating = true;

const { clientX, clientY } = getClientCoordinates(e);

startX = clientX;
startY = clientY;

// Get current rotation from transform style
const currentTransform = elt.style.transform || "";
const rotateMatch = currentTransform.match(/rotate\(([^)]+)deg\)/);
initialRotation = rotateMatch ? parseFloat(rotateMatch[1]) : 0;

const rotateHandle = container.querySelector(".rotate-handle");
rotateHandle.style.cursor = "grabbing";

e.preventDefault();
e.stopPropagation();
}

function handleMouseMove(e) {
if (isDragging) {
drag(e);
} else if (isResizing) {
resize(e);
} else if (isRotating) {
rotate(e);
}
}

Expand All @@ -287,6 +391,8 @@ function setupDraggableElt(elt) {
drag(e);
} else if (isResizing) {
resize(e);
} else if (isRotating) {
rotate(e);
}
}

Expand Down Expand Up @@ -364,14 +470,50 @@ function setupDraggableElt(elt) {
e.preventDefault();
}

function rotate(e) {
if (!isRotating) return;

const { clientX, clientY } = getClientCoordinates(e);

// Get the center of the element
const rect = container.getBoundingClientRect();
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2;

// Calculate the angle from center to current mouse position
const angle = Math.atan2(clientY - centerY, clientX - centerX);
const startAngle = Math.atan2(startY - centerY, startX - centerX);

// Convert to degrees and calculate the rotation difference
const angleDiff = (angle - startAngle) * (180 / Math.PI);
let newRotation = initialRotation + angleDiff;

// Snap to 15-degree increments if Shift key is pressed
if (e.shiftKey) {
newRotation = Math.round(newRotation / 15) * 15;
}

// Normalize angle to 0-360 range
newRotation = ((newRotation % 360) + 360) % 360;

elt.style.transform = `rotate(${newRotation}deg)`;

e.preventDefault();
}

function stopAction() {
if (isDragging || isResizing) {
if (isDragging || isResizing || isRotating) {
setTimeout(() => {
if (!container.matches(":hover")) {
container.style.border = "2px solid transparent";
container
.querySelectorAll(".resize-handle")
.forEach((h) => (h.style.opacity = "0"));
const rotateHandle = container.querySelector(".rotate-handle");
if (rotateHandle) {
rotateHandle.style.opacity = "0";
rotateHandle.style.cursor = "grab";
}
const fontControls = container.querySelector(".font-controls");
if (fontControls) fontControls.style.opacity = "0";
}
Expand All @@ -380,6 +522,7 @@ function setupDraggableElt(elt) {

isDragging = false;
isResizing = false;
isRotating = false;
resizeHandle = null;
}

Expand Down Expand Up @@ -453,6 +596,13 @@ function extracteditableEltDimensions() {
top: top,
};

// Add rotation if it's set
const currentTransform = elt.style.transform || "";
const rotateMatch = currentTransform.match(/rotate\(([^)]+)deg\)/);
if (rotateMatch) {
dimensionData.rotation = parseFloat(rotateMatch[1]);
}

// Add font-size for div elements if it's set
if (elt.tagName.toLowerCase() === "div" && elt.style.fontSize) {
dimensionData.fontSize = parseFloat(elt.style.fontSize);
Expand Down