Files
MyManagingTools/src/components/workflows/assets/Workflows.js

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;
}