414 lines
16 KiB
JavaScript
414 lines
16 KiB
JavaScript
function bindWorkflowDesigner(elementId) {
|
|
bindWorkflowDesignerToolbox(elementId)
|
|
bindWorkflowDesignerSplitter(elementId)
|
|
}
|
|
|
|
function bindWorkflowDesignerToolbox(elementId) {
|
|
// Store state for this specific designer instance
|
|
const designer = {
|
|
draggedType: null,
|
|
draggedComponent: null,
|
|
selectedComponent: null,
|
|
connectionStart: null,
|
|
potentialConnectionStart: null,
|
|
};
|
|
|
|
// Get the designer container and canvas
|
|
const designerContainer = document.getElementById(elementId);
|
|
//const canvas = designerContainer.querySelector('.wkf-canvas');
|
|
const canvas = document.getElementById("c_" + elementId);
|
|
|
|
// === Event delegation for components ===
|
|
|
|
// Use event delegation for all component-related events
|
|
designerContainer.addEventListener('dragstart', (event) => {
|
|
// Handle toolbox items
|
|
if (event.target.closest('.wkf-toolbox-item')) {
|
|
designer.draggedType = event.target.closest('.wkf-toolbox-item').dataset.type;
|
|
event.dataTransfer.effectAllowed = 'copy';
|
|
}
|
|
|
|
// Handle components
|
|
if (event.target.closest('.wkf-workflow-component')) {
|
|
const component = event.target.closest('.wkf-workflow-component');
|
|
component.classList.add('dragging');
|
|
designer.draggedComponent = component.dataset.componentId;
|
|
event.dataTransfer.effectAllowed = 'move';
|
|
|
|
// Create an invisible image to use as the drag image
|
|
const invisibleImg = new Image();
|
|
invisibleImg.src = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'; // 1px transparent GIF
|
|
event.dataTransfer.setDragImage(invisibleImg, 0, 0);
|
|
|
|
// Highlight all valid output points on other components
|
|
designerContainer.querySelectorAll('.wkf-connection-point').forEach(point => {
|
|
if (point.dataset.pointType === 'output' &&
|
|
point.dataset.componentId !== designer.draggedComponent) {
|
|
point.classList.add('potential-connection');
|
|
}
|
|
});
|
|
|
|
}
|
|
});
|
|
|
|
designerContainer.addEventListener('drag', (event) => {
|
|
if (!event.target.closest('.wkf-workflow-component')) return;
|
|
if (event.clientX === 0 && event.clientY === 0) return; // Ignore invalid drag events
|
|
|
|
const component = event.target.closest('.wkf-workflow-component');
|
|
const rect = canvas.getBoundingClientRect();
|
|
const x = event.clientX - rect.left - 64;
|
|
const y = event.clientY - rect.top - 40;
|
|
component.style.left = Math.max(0, x) + 'px';
|
|
component.style.top = Math.max(0, y) + 'px';
|
|
|
|
const componentRect = component.getBoundingClientRect();
|
|
const componentId = component.dataset.componentId;
|
|
const outputPoints = designerContainer.querySelectorAll('.wkf-connection-point[data-point-type="output"]');
|
|
|
|
outputPoints.forEach(point => {
|
|
if (point.dataset.componentId === componentId) return; // Skip points from the same component
|
|
|
|
const pointRect = point.getBoundingClientRect();
|
|
const pointCircle = {
|
|
x: pointRect.left + pointRect.width / 2,
|
|
y: pointRect.top + pointRect.height / 2,
|
|
radius: 6
|
|
};
|
|
|
|
if (point != designer.potentialConnectionStart && _isOverlapping(componentRect, pointCircle)) {
|
|
console.debug("overlapping !")
|
|
outputPoints.forEach(other_point => {
|
|
other_point.classList.remove("potential-start")
|
|
});
|
|
designer.potentialConnectionStart = point.dataset.componentId;
|
|
point.classList.add('potential-start');
|
|
}
|
|
|
|
});
|
|
|
|
// Update connections in real-time
|
|
updateConnections(designerContainer);
|
|
});
|
|
|
|
designerContainer.addEventListener('dragend', (event) => {
|
|
if (!event.target.closest('.wkf-workflow-component')) return;
|
|
|
|
if (designer.draggedComponent) {
|
|
const component = event.target.closest('.wkf-workflow-component');
|
|
const draggedComponentId = component.dataset.componentId;
|
|
component.classList.remove('dragging');
|
|
|
|
const rect = canvas.getBoundingClientRect();
|
|
const x = event.clientX - rect.left - 64;
|
|
const y = event.clientY - rect.top - 40;
|
|
htmx.ajax('POST', '/workflows/move-component', {
|
|
target: `#c_${elementId}`,
|
|
headers: {"Content-Type": "application/x-www-form-urlencoded"},
|
|
swap: "innerHTML",
|
|
values: {
|
|
_id: elementId,
|
|
component_id: designer.draggedComponent,
|
|
x: Math.max(0, x),
|
|
y: Math.max(0, y),
|
|
}
|
|
});
|
|
|
|
// Create connection if we ended the drag over a connection point
|
|
if (designer.potentialConnectionStart) {
|
|
|
|
htmx.ajax('POST', '/workflows/add-connection', {
|
|
target: `#c_${elementId}`,
|
|
headers: {"Content-Type": "application/x-www-form-urlencoded"},
|
|
swap: "innerHTML",
|
|
values: {
|
|
_id: elementId,
|
|
from_id: designer.potentialConnectionStart, // The output point we're hovering over
|
|
to_id: draggedComponentId, // The component we're dragging
|
|
}
|
|
});
|
|
}
|
|
|
|
// Remove highlighting from all connection points
|
|
designerContainer.querySelectorAll('.wkf-connection-point').forEach(point => {
|
|
point.classList.remove('potential-connection');
|
|
});
|
|
|
|
designer.draggedComponent = null;
|
|
designer.potentialConnectionStart = null;
|
|
|
|
// Update connections after drag ends
|
|
updateConnections(designerContainer);
|
|
}
|
|
});
|
|
|
|
// Handle component clicks using event delegation
|
|
designerContainer.addEventListener('click', (event) => {
|
|
// Handle canvas clicks to reset connection mode and deselect components
|
|
if (event.target === canvas || event.target.classList.contains('wkf-canvas')) {
|
|
if (designer.connectionStart) {
|
|
designerContainer.querySelectorAll('.wkf-connection-point').forEach(point => {
|
|
point.style.background = '#3b82f6';
|
|
});
|
|
designer.connectionStart = null;
|
|
}
|
|
// Deselect components
|
|
designerContainer.querySelectorAll('.wkf-workflow-component').forEach(comp => {
|
|
comp.classList.remove('selected');
|
|
});
|
|
designer.selectedComponent = null;
|
|
return;
|
|
}
|
|
|
|
// Handle component selection
|
|
const component = event.target.closest('.wkf-workflow-component');
|
|
if (component) {
|
|
event.stopPropagation();
|
|
// Remove previous selection
|
|
designerContainer.querySelectorAll('.wkf-workflow-component').forEach(comp => {
|
|
comp.classList.remove('selected');
|
|
});
|
|
// Select current component
|
|
component.classList.add('selected');
|
|
designer.selectedComponent = component.dataset.componentId;
|
|
}
|
|
|
|
// Handle connection points
|
|
const connectionPoint = event.target.closest('.wkf-connection-point');
|
|
if (connectionPoint) {
|
|
event.stopPropagation();
|
|
const componentId = connectionPoint.dataset.componentId;
|
|
const pointType = connectionPoint.dataset.pointType;
|
|
|
|
if (!designer.connectionStart) {
|
|
// Start connection from output point
|
|
if (pointType === 'output') {
|
|
designer.connectionStart = {
|
|
componentId: componentId,
|
|
pointType: pointType
|
|
};
|
|
connectionPoint.style.background = '#ef4444';
|
|
console.log('Connection started from:', componentId);
|
|
}
|
|
} else {
|
|
// Complete connection to input point
|
|
if (pointType === 'input' && componentId !== designer.connectionStart.componentId) {
|
|
htmx.ajax('POST', '/workflows/add-connection', {
|
|
target: `#c_${elementId}`,
|
|
headers: {"Content-Type": "application/x-www-form-urlencoded"},
|
|
swap: "innerHTML",
|
|
values: {
|
|
_id: elementId,
|
|
from_id: designer.connectionStart.componentId,
|
|
to_id: componentId,
|
|
}
|
|
});
|
|
}
|
|
// Reset connection mode
|
|
designerContainer.querySelectorAll('.wkf-connection-point').forEach(point => {
|
|
point.style.background = '#3b82f6';
|
|
});
|
|
designer.connectionStart = null;
|
|
}
|
|
}
|
|
|
|
else {
|
|
const component = event.target.closest('.wkf-workflow-component');
|
|
if (!component) return;
|
|
|
|
componentId = component.dataset.componentId
|
|
htmx.ajax('POST', '/workflows/select-connection', {
|
|
target: `#c_${elementId}`,
|
|
headers: {"Content-Type": "application/x-www-form-urlencoded"},
|
|
swap: "innerHTML",
|
|
values: {
|
|
_id: elementId,
|
|
component_id: componentId
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
// Canvas drag over event (for dropping new components)
|
|
canvas.addEventListener('dragover', (event) => {
|
|
event.preventDefault();
|
|
event.dataTransfer.dropEffect = 'copy';
|
|
});
|
|
|
|
// Canvas drop event (for creating new components)
|
|
canvas.addEventListener('drop', (event) => {
|
|
event.preventDefault();
|
|
if (designer.draggedType) {
|
|
const rect = event.currentTarget.getBoundingClientRect();
|
|
const x = event.clientX - rect.left - 64; // Center the component
|
|
const y = event.clientY - rect.top - 40;
|
|
htmx.ajax('POST', '/workflows/add-component', {
|
|
target: `#c_${elementId}`,
|
|
headers: {"Content-Type": "application/x-www-form-urlencoded"},
|
|
swap: "innerHTML",
|
|
values: {
|
|
_id: elementId,
|
|
component_type: designer.draggedType,
|
|
x: Math.max(0, x),
|
|
y: Math.max(0, y),
|
|
}
|
|
});
|
|
|
|
designer.draggedType = null;
|
|
}
|
|
});
|
|
|
|
// Delete selected component with Delete key
|
|
document.addEventListener('keydown', (event) => {
|
|
if (event.key === 'Delete' && designer.selectedComponent) {
|
|
htmx.ajax('POST', '/workflows/delete-component', {
|
|
target: `#c_${elementId}`,
|
|
headers: {"Content-Type": "application/x-www-form-urlencoded"},
|
|
swap: "innerHTML",
|
|
values: {
|
|
_id: elementId,
|
|
component_id: designer.selectedComponent,
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
// Function to update all connection lines for this designer
|
|
function updateConnections(container) {
|
|
const connectionLines = container.querySelectorAll('.wkf-connection-line');
|
|
connectionLines.forEach(svg => {
|
|
const connectionData = svg.dataset;
|
|
if (connectionData.fromId && connectionData.toId) {
|
|
updateConnectionLine(svg, connectionData.fromId, connectionData.toId, container);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Function to update a single connection line
|
|
function updateConnectionLine(svg, fromId, toId, container) {
|
|
const fromComp = container.querySelector(`[data-component-id="${fromId}"]`);
|
|
const toComp = container.querySelector(`[data-component-id="${toId}"]`);
|
|
if (!fromComp || !toComp) return;
|
|
// Get current positions
|
|
const fromX = parseInt(fromComp.style.left) + 128; // component width + output point
|
|
const fromY = parseInt(fromComp.style.top) + 32; // component height / 2
|
|
const toX = parseInt(toComp.style.left);
|
|
const toY = parseInt(toComp.style.top) + 32;
|
|
// Create curved path
|
|
const midX = (fromX + toX) / 2;
|
|
const path = `M ${fromX} ${fromY} C ${midX} ${fromY}, ${midX} ${toY}, ${toX} ${toY}`;
|
|
// Update the path
|
|
const pathElement = svg.querySelector('path');
|
|
if (pathElement) {
|
|
pathElement.setAttribute('d', path);
|
|
}
|
|
}
|
|
|
|
// Return the designer object for possible external use
|
|
return designer;
|
|
}
|
|
|
|
/**
|
|
* Binds drag resize functionality to a workflow designer splitter
|
|
* @param {string} elementId - The base ID of the workflow designer element
|
|
*/
|
|
function bindWorkflowDesignerSplitter(elementId) {
|
|
// Get the elements
|
|
const designer = document.getElementById(`d_${elementId}`);
|
|
const splitter = document.getElementById(`s_${elementId}`);
|
|
const properties = document.getElementById(`p_${elementId}`);
|
|
const designerMinHeight = parseInt(designer.style.minHeight, 10) || 230;
|
|
|
|
if (!designer || !splitter) {
|
|
console.error("Cannot find all required elements for workflow designer splitter");
|
|
return;
|
|
}
|
|
|
|
// Initialize drag state
|
|
let isResizing = false;
|
|
let startY = 0;
|
|
let startDesignerHeight = 0;
|
|
|
|
// Mouse down event - start dragging
|
|
splitter.addEventListener('mousedown', (e) => {
|
|
e.preventDefault();
|
|
isResizing = true;
|
|
startY = e.clientY;
|
|
startDesignerHeight = parseInt(designer.style.height, 10) || designer.parentNode.getBoundingClientRect().height;
|
|
console.debug("startDesignerHeight", startDesignerHeight);
|
|
|
|
document.body.style.userSelect = 'none'; // Disable text selection
|
|
document.body.style.cursor = "row-resize"; // Change cursor style globally for horizontal splitter
|
|
splitter.classList.add('wkf-splitter-active'); // Add class for visual feedback
|
|
});
|
|
|
|
// Mouse move event - update heights while dragging
|
|
document.addEventListener('mousemove', (e) => {
|
|
if (!isResizing) return;
|
|
|
|
// Calculate new height
|
|
const deltaY = e.clientY - startY;
|
|
const newDesignerHeight = Math.max(designerMinHeight, startDesignerHeight + deltaY); // Enforce minimum height
|
|
designer.style.height = `${newDesignerHeight}px`;
|
|
|
|
// Update properties panel height if it exists
|
|
if (properties) {
|
|
const containerHeight = designer.parentNode.getBoundingClientRect().height;
|
|
const propertiesHeight = Math.max(50, containerHeight - newDesignerHeight - splitter.offsetHeight);
|
|
properties.style.height = `${propertiesHeight}px`;
|
|
}
|
|
|
|
console.debug("newDesignerHeight", newDesignerHeight);
|
|
});
|
|
|
|
// Mouse up event - stop dragging
|
|
document.addEventListener('mouseup', () => {
|
|
if (!isResizing) return;
|
|
|
|
isResizing = false;
|
|
document.body.style.cursor = ""; // Reset cursor
|
|
document.body.style.userSelect = ""; // Re-enable text selection
|
|
splitter.classList.remove('wkf-splitter-active');
|
|
|
|
// Store the current state
|
|
const designerHeight = parseInt(designer.style.height, 10);
|
|
saveDesignerHeight(elementId, designerHeight);
|
|
});
|
|
|
|
// Handle case when mouse leaves the window
|
|
document.addEventListener('mouseleave', () => {
|
|
if (isResizing) {
|
|
isResizing = false;
|
|
document.body.style.cursor = ""; // Reset cursor
|
|
document.body.style.userSelect = ""; // Re-enable text selection
|
|
splitter.classList.remove('wkf-splitter-active');
|
|
}
|
|
});
|
|
|
|
// Function to save the designer height
|
|
function saveDesignerHeight(id, height) {
|
|
htmx.ajax('POST', '/workflows/resize-designer', {
|
|
target: `#${elementId}`,
|
|
headers: {"Content-Type": "application/x-www-form-urlencoded"},
|
|
swap: "outerHTML",
|
|
values: {
|
|
_id: elementId,
|
|
designer_height: height,
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
function _isOverlapping(rect, circle) {
|
|
// Find the closest point on the rectangle to the circle's center
|
|
const closestX = Math.max(rect.x, Math.min(circle.x, rect.x + rect.width));
|
|
const closestY = Math.max(rect.y, Math.min(circle.y, rect.y + rect.height));
|
|
|
|
// Calculate the distance between the circle's center and the closest point
|
|
const deltaX = circle.x - closestX;
|
|
const deltaY = circle.y - closestY;
|
|
const distanceSquared = deltaX * deltaX + deltaY * deltaY;
|
|
|
|
// Check if the distance is less than or equal to the circle's radius
|
|
return distanceSquared <= circle.radius * circle.radius;
|
|
} |