I can delete a connection
This commit is contained in:
@@ -4,308 +4,523 @@ function bindWorkflowDesigner(elementId) {
|
||||
}
|
||||
|
||||
function bindWorkflowDesignerToolbox(elementId) {
|
||||
// Store state for this specific designer instance
|
||||
const designer = {
|
||||
draggedType: null,
|
||||
draggedComponent: null,
|
||||
selectedComponent: null,
|
||||
connectionStart: null,
|
||||
potentialConnectionStart: null,
|
||||
// Constants for configuration
|
||||
const CONFIG = {
|
||||
COMPONENT_WIDTH: 128,
|
||||
COMPONENT_HEIGHT: 64,
|
||||
DRAG_OFFSET: { x: 64, y: 40 },
|
||||
CONNECTION_POINT_RADIUS: 6,
|
||||
INVISIBLE_DRAG_IMAGE: 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7',
|
||||
};
|
||||
|
||||
// Get the designer container and canvas
|
||||
// Designer state with better organization
|
||||
const designer = {
|
||||
// Drag state
|
||||
draggedType: null,
|
||||
draggedComponent: null,
|
||||
|
||||
// Selection state
|
||||
selectedComponent: null,
|
||||
selectedConnection: null,
|
||||
|
||||
// Connection state
|
||||
connectionStart: null,
|
||||
potentialConnectionStart: null,
|
||||
|
||||
// Performance optimization
|
||||
lastUpdateTime: 0,
|
||||
animationFrame: null,
|
||||
|
||||
// Cleanup tracking
|
||||
eventListeners: new Map(),
|
||||
|
||||
// State methods
|
||||
reset() {
|
||||
this.draggedType = null;
|
||||
this.draggedComponent = null;
|
||||
this.connectionStart = null;
|
||||
this.potentialConnectionStart = null;
|
||||
this.cancelAnimationFrame();
|
||||
},
|
||||
|
||||
cancelAnimationFrame() {
|
||||
if (this.animationFrame) {
|
||||
cancelAnimationFrame(this.animationFrame);
|
||||
this.animationFrame = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Get DOM elements with error handling
|
||||
const designerContainer = document.getElementById(elementId);
|
||||
//const canvas = designerContainer.querySelector('.wkf-canvas');
|
||||
const canvas = document.getElementById("c_" + elementId);
|
||||
const canvas = document.getElementById(`c_${elementId}`);
|
||||
|
||||
// === Event delegation for components ===
|
||||
if (!designerContainer || !canvas) {
|
||||
console.error(`Workflow designer elements not found for ID: ${elementId}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 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';
|
||||
}
|
||||
// Utility functions
|
||||
const utils = {
|
||||
|
||||
// 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';
|
||||
// Check if two rectangles overlap
|
||||
isOverlapping(rect1, circle) {
|
||||
// Find the closest point on the rectangle to the circle's center
|
||||
const closestX = Math.max(rect1.x, Math.min(circle.x, rect1.x + rect1.width));
|
||||
const closestY = Math.max(rect1.y, Math.min(circle.y, rect1.y + rect1.height));
|
||||
|
||||
// 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);
|
||||
// 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;
|
||||
|
||||
// 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');
|
||||
// Check if the distance is less than or equal to the circle's radius
|
||||
return distanceSquared <= circle.radius * circle.radius;
|
||||
},
|
||||
|
||||
// Get mouse position relative to canvas
|
||||
getCanvasPosition(event) {
|
||||
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),
|
||||
}
|
||||
});
|
||||
return {
|
||||
x: event.clientX - rect.left,
|
||||
y: event.clientY - rect.top
|
||||
};
|
||||
},
|
||||
|
||||
// Create connection if we ended the drag over a connection point
|
||||
if (designer.potentialConnectionStart) {
|
||||
// Constrain position within canvas bounds
|
||||
constrainPosition(x, y) {
|
||||
const canvasRect = canvas.getBoundingClientRect();
|
||||
return {
|
||||
x: Math.max(0, Math.min(x, canvasRect.width - CONFIG.COMPONENT_WIDTH)),
|
||||
y: Math.max(0, Math.min(y, canvasRect.height - CONFIG.COMPONENT_HEIGHT))
|
||||
};
|
||||
},
|
||||
|
||||
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
|
||||
// Create HTMX request with error handling
|
||||
async makeRequest(url, values, targetId = `#c_${elementId}`, swap="innerHTML") {
|
||||
try {
|
||||
return htmx.ajax('POST', url, {
|
||||
target: targetId,
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
swap: swap,
|
||||
values: { _id: elementId, ...values }
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('HTMX request failed:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Connection management
|
||||
const connectionManager = {
|
||||
// Update all connections with performance optimization
|
||||
updateAll() {
|
||||
designer.cancelAnimationFrame();
|
||||
designer.animationFrame = requestAnimationFrame(() => {
|
||||
const connectionLines = designerContainer.querySelectorAll('.wkf-connection-line');
|
||||
connectionLines.forEach(svg => {
|
||||
const { fromId, toId } = svg.dataset;
|
||||
if (fromId && toId) {
|
||||
this.updateLine(svg, fromId, toId);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// Remove highlighting from all connection points
|
||||
// Update a specific connection line
|
||||
updateLine(svg, fromId, toId) {
|
||||
const fromComp = designerContainer.querySelector(`[data-component-id="${fromId}"]`);
|
||||
const toComp = designerContainer.querySelector(`[data-component-id="${toId}"]`);
|
||||
|
||||
if (!fromComp || !toComp) return;
|
||||
|
||||
// Calculate connection points
|
||||
const fromX = parseInt(fromComp.style.left) + CONFIG.COMPONENT_WIDTH;
|
||||
const fromY = parseInt(fromComp.style.top) + CONFIG.COMPONENT_HEIGHT / 2;
|
||||
const toX = parseInt(toComp.style.left);
|
||||
const toY = parseInt(toComp.style.top) + CONFIG.COMPONENT_HEIGHT / 2;
|
||||
|
||||
// Create smooth curved path
|
||||
const midX = (fromX + toX) / 2;
|
||||
const path = `M ${fromX} ${fromY} C ${midX} ${fromY}, ${midX} ${toY}, ${toX} ${toY}`;
|
||||
|
||||
// Update the path element
|
||||
const pathElement = svg.querySelector('.wkf-connection-path');
|
||||
if (pathElement) {
|
||||
pathElement.setAttribute('d', path);
|
||||
}
|
||||
},
|
||||
|
||||
// Clear all connection highlighting
|
||||
clearHighlighting() {
|
||||
designerContainer.querySelectorAll('.wkf-connection-point').forEach(point => {
|
||||
point.classList.remove('potential-connection');
|
||||
point.classList.remove('potential-connection', 'potential-start');
|
||||
point.style.background = '#3b82f6';
|
||||
});
|
||||
},
|
||||
|
||||
// Select a connection
|
||||
select(connectionPath) {
|
||||
// Deselect all other connections
|
||||
designerContainer.querySelectorAll('.wkf-connection-line path').forEach(path => {
|
||||
path.classList.remove('wkf-connection-selected');
|
||||
});
|
||||
|
||||
designer.draggedComponent = null;
|
||||
designer.potentialConnectionStart = null;
|
||||
|
||||
// Update connections after drag ends
|
||||
updateConnections(designerContainer);
|
||||
// Select the clicked connection
|
||||
connectionPath.classList.add('wkf-connection-selected');
|
||||
|
||||
// Store connection data
|
||||
const connectionSvg = connectionPath.closest('.wkf-connection-line');
|
||||
designer.selectedConnection = {
|
||||
fromId: connectionSvg.dataset.fromId,
|
||||
toId: connectionSvg.dataset.toId
|
||||
};
|
||||
},
|
||||
|
||||
// Deselect all connections
|
||||
deselectAll() {
|
||||
designerContainer.querySelectorAll('.wkf-connection-line path').forEach(path => {
|
||||
path.classList.remove('wkf-connection-selected');
|
||||
});
|
||||
designer.selectedConnection = null;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 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
|
||||
// Component management
|
||||
const componentManager = {
|
||||
// Select a component
|
||||
select(component) {
|
||||
// Deselect all other components
|
||||
designerContainer.querySelectorAll('.wkf-workflow-component').forEach(comp => {
|
||||
comp.classList.remove('selected');
|
||||
});
|
||||
|
||||
// Select the clicked component
|
||||
component.classList.add('selected');
|
||||
designer.selectedComponent = component.dataset.componentId;
|
||||
|
||||
// Also trigger server-side selection
|
||||
utils.makeRequest('/workflows/select-component', {
|
||||
component_id: designer.selectedComponent
|
||||
}, `#p_${elementId}`, "outerHTML");
|
||||
},
|
||||
|
||||
// Deselect all components
|
||||
deselectAll() {
|
||||
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');
|
||||
// Update component position with constraints
|
||||
updatePosition(component, x, y) {
|
||||
const constrained = utils.constrainPosition(x, y);
|
||||
component.style.left = constrained.x + 'px';
|
||||
component.style.top = constrained.y + 'px';
|
||||
}
|
||||
};
|
||||
|
||||
// Event handlers with improved organization
|
||||
const eventHandlers = {
|
||||
// Handle drag start for both toolbox items and components
|
||||
onDragStart(event) {
|
||||
const toolboxItem = event.target.closest('.wkf-toolbox-item');
|
||||
const component = event.target.closest('.wkf-workflow-component');
|
||||
|
||||
if (toolboxItem) {
|
||||
designer.draggedType = toolboxItem.dataset.type;
|
||||
event.dataTransfer.effectAllowed = 'copy';
|
||||
return;
|
||||
}
|
||||
|
||||
if (component) {
|
||||
component.classList.add('dragging');
|
||||
designer.draggedComponent = component.dataset.componentId;
|
||||
event.dataTransfer.effectAllowed = 'move';
|
||||
|
||||
// Use invisible drag image
|
||||
const invisibleImg = new Image();
|
||||
invisibleImg.src = CONFIG.INVISIBLE_DRAG_IMAGE;
|
||||
event.dataTransfer.setDragImage(invisibleImg, 0, 0);
|
||||
|
||||
// Highlight potential connection points
|
||||
designerContainer.querySelectorAll('.wkf-connection-point').forEach(point => {
|
||||
if (point.dataset.pointType === 'output' &&
|
||||
point.dataset.componentId !== designer.draggedComponent) {
|
||||
point.classList.add('potential-connection');
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// Handle drag with immediate updates
|
||||
onDrag(event) {
|
||||
if (!event.target.closest('.wkf-workflow-component')) return;
|
||||
if (event.clientX === 0 && event.clientY === 0) return;
|
||||
|
||||
const component = event.target.closest('.wkf-workflow-component');
|
||||
const position = utils.getCanvasPosition(event);
|
||||
const x = position.x - CONFIG.DRAG_OFFSET.x;
|
||||
const y = position.y - CONFIG.DRAG_OFFSET.y;
|
||||
|
||||
// Update position immediately for responsive feel
|
||||
componentManager.updatePosition(component, x, y);
|
||||
|
||||
// Check for potential connections
|
||||
eventHandlers.checkPotentialConnections(component);
|
||||
|
||||
// Update connections with requestAnimationFrame for smooth rendering
|
||||
connectionManager.updateAll();
|
||||
},
|
||||
|
||||
// Check for potential connections during drag
|
||||
checkPotentialConnections(component) {
|
||||
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;
|
||||
|
||||
const pointRect = point.getBoundingClientRect();
|
||||
const pointCircle = {
|
||||
x: pointRect.left + pointRect.width / 2,
|
||||
y: pointRect.top + pointRect.height / 2,
|
||||
radius: CONFIG.CONNECTION_POINT_RADIUS
|
||||
};
|
||||
|
||||
if (point !== designer.potentialConnectionStart &&
|
||||
utils.isOverlapping(componentRect, pointCircle)) {
|
||||
|
||||
// Clear previous potential starts
|
||||
outputPoints.forEach(otherPoint => {
|
||||
otherPoint.classList.remove('potential-start');
|
||||
});
|
||||
|
||||
designer.potentialConnectionStart = point.dataset.componentId;
|
||||
point.classList.add('potential-start');
|
||||
}
|
||||
});
|
||||
// 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();
|
||||
// Handle drag end with cleanup
|
||||
async onDragEnd(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 position = utils.getCanvasPosition(event);
|
||||
const x = position.x - CONFIG.DRAG_OFFSET.x;
|
||||
const y = position.y - CONFIG.DRAG_OFFSET.y;
|
||||
const constrained = utils.constrainPosition(x, y);
|
||||
|
||||
try {
|
||||
// Move component
|
||||
await utils.makeRequest('/workflows/move-component', {
|
||||
component_id: designer.draggedComponent,
|
||||
x: constrained.x,
|
||||
y: constrained.y
|
||||
});
|
||||
|
||||
// Create connection if applicable
|
||||
if (designer.potentialConnectionStart) {
|
||||
await utils.makeRequest('/workflows/add-connection', {
|
||||
from_id: designer.potentialConnectionStart,
|
||||
to_id: draggedComponentId
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to update component:', error);
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
connectionManager.clearHighlighting();
|
||||
designer.reset();
|
||||
connectionManager.updateAll();
|
||||
}
|
||||
},
|
||||
|
||||
// Handle clicks with improved event delegation
|
||||
onClick(event) {
|
||||
|
||||
// Connection point handling
|
||||
const connectionPoint = event.target.closest('.wkf-connection-point');
|
||||
if (connectionPoint) {
|
||||
event.stopPropagation();
|
||||
eventHandlers.handleConnectionPoint(connectionPoint);
|
||||
}
|
||||
|
||||
// Connection selection
|
||||
const connectionPath = event.target.closest('.wkf-connection-line path');
|
||||
if (connectionPath) {
|
||||
event.stopPropagation();
|
||||
componentManager.deselectAll();
|
||||
// get the visible connection path
|
||||
const visibleConnectionPath = connectionPath.parentElement.querySelector('.wkf-connection-path');
|
||||
connectionManager.select(visibleConnectionPath);
|
||||
return;
|
||||
}
|
||||
|
||||
// Canvas click - reset everything
|
||||
if (event.target === canvas || event.target.classList.contains('wkf-canvas')) {
|
||||
designer.reset();
|
||||
connectionManager.clearHighlighting();
|
||||
connectionManager.deselectAll();
|
||||
componentManager.deselectAll();
|
||||
return;
|
||||
}
|
||||
|
||||
// Component selection
|
||||
const component = event.target.closest('.wkf-workflow-component');
|
||||
if (component) {
|
||||
event.stopPropagation();
|
||||
connectionManager.deselectAll();
|
||||
componentManager.select(component);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
},
|
||||
|
||||
// Handle connection point interactions
|
||||
async handleConnectionPoint(connectionPoint) {
|
||||
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
|
||||
};
|
||||
designer.connectionStart = { componentId, 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,
|
||||
}
|
||||
});
|
||||
try {
|
||||
await utils.makeRequest('/workflows/add-connection', {
|
||||
from_id: designer.connectionStart.componentId,
|
||||
to_id: componentId
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to create connection:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Reset connection mode
|
||||
designerContainer.querySelectorAll('.wkf-connection-point').forEach(point => {
|
||||
point.style.background = '#3b82f6';
|
||||
});
|
||||
connectionManager.clearHighlighting();
|
||||
designer.connectionStart = null;
|
||||
}
|
||||
},
|
||||
|
||||
// Handle canvas drop for new components
|
||||
async onCanvasDrop(event) {
|
||||
event.preventDefault();
|
||||
|
||||
if (designer.draggedType) {
|
||||
const position = utils.getCanvasPosition(event);
|
||||
const x = position.x - CONFIG.DRAG_OFFSET.x;
|
||||
const y = position.y - CONFIG.DRAG_OFFSET.y;
|
||||
const constrained = utils.constrainPosition(x, y);
|
||||
|
||||
try {
|
||||
await utils.makeRequest('/workflows/add-component', {
|
||||
component_type: designer.draggedType,
|
||||
x: constrained.x,
|
||||
y: constrained.y
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to add component:', error);
|
||||
}
|
||||
|
||||
designer.draggedType = null;
|
||||
}
|
||||
},
|
||||
|
||||
// Handle keyboard shortcuts
|
||||
async onKeyDown(event) {
|
||||
if (event.key === 'Delete' || event.key === 'Suppr') {
|
||||
try {
|
||||
if (designer.selectedComponent) {
|
||||
await utils.makeRequest('/workflows/delete-component', {
|
||||
component_id: designer.selectedComponent
|
||||
});
|
||||
designer.selectedComponent = null;
|
||||
} else if (designer.selectedConnection) {
|
||||
await utils.makeRequest('/workflows/delete-connection', {
|
||||
from_id: designer.selectedConnection.fromId,
|
||||
to_id: designer.selectedConnection.toId
|
||||
});
|
||||
designer.selectedConnection = null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to delete:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
else {
|
||||
const component = event.target.closest('.wkf-workflow-component');
|
||||
if (!component) return;
|
||||
// Event registration with cleanup tracking
|
||||
function registerEventListener(element, event, handler, options = {}) {
|
||||
const key = `${element.id || 'global'}-${event}`;
|
||||
element.addEventListener(event, handler, options);
|
||||
designer.eventListeners.set(key, () => element.removeEventListener(event, handler, options));
|
||||
}
|
||||
|
||||
componentId = component.dataset.componentId
|
||||
htmx.ajax('POST', '/workflows/select-component', {
|
||||
target: `#p_${elementId}`,
|
||||
headers: {"Content-Type": "application/x-www-form-urlencoded"},
|
||||
swap: "outerHTML",
|
||||
values: {
|
||||
_id: elementId,
|
||||
component_id: componentId
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Canvas drag over event (for dropping new components)
|
||||
canvas.addEventListener('dragover', (event) => {
|
||||
// Register all event listeners
|
||||
registerEventListener(designerContainer, 'dragstart', eventHandlers.onDragStart);
|
||||
registerEventListener(designerContainer, 'drag', eventHandlers.onDrag);
|
||||
registerEventListener(designerContainer, 'dragend', eventHandlers.onDragEnd);
|
||||
registerEventListener(designerContainer, 'click', eventHandlers.onClick);
|
||||
registerEventListener(canvas, 'dragover', (event) => {
|
||||
event.preventDefault();
|
||||
event.dataTransfer.dropEffect = 'copy';
|
||||
});
|
||||
registerEventListener(canvas, 'drop', eventHandlers.onCanvasDrop);
|
||||
registerEventListener(document, 'keydown', eventHandlers.onKeyDown);
|
||||
|
||||
// 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),
|
||||
}
|
||||
});
|
||||
// Public API
|
||||
const api = {
|
||||
// Cleanup function for proper disposal
|
||||
destroy() {
|
||||
designer.cancelAnimationFrame();
|
||||
designer.eventListeners.forEach(cleanup => cleanup());
|
||||
designer.eventListeners.clear();
|
||||
},
|
||||
|
||||
designer.draggedType = null;
|
||||
}
|
||||
});
|
||||
// Get current designer state
|
||||
getState() {
|
||||
return {
|
||||
selectedComponent: designer.selectedComponent,
|
||||
selectedConnection: designer.selectedConnection,
|
||||
connectionStart: designer.connectionStart
|
||||
};
|
||||
},
|
||||
|
||||
// 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,
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
// Force update all connections
|
||||
updateConnections() {
|
||||
connectionManager.updateAll();
|
||||
},
|
||||
|
||||
// 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);
|
||||
// Select component programmatically
|
||||
selectComponent(componentId) {
|
||||
const component = designerContainer.querySelector(`[data-component-id="${componentId}"]`);
|
||||
if (component) {
|
||||
componentManager.select(component);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 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;
|
||||
// Initialize connections on load
|
||||
setTimeout(() => connectionManager.updateAll(), 100);
|
||||
|
||||
return api;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -335,7 +550,6 @@ function bindWorkflowDesignerSplitter(elementId) {
|
||||
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
|
||||
@@ -357,8 +571,7 @@ function bindWorkflowDesignerSplitter(elementId) {
|
||||
const propertiesHeight = Math.max(50, containerHeight - newDesignerHeight - splitter.offsetHeight);
|
||||
properties.style.height = `${propertiesHeight}px`;
|
||||
}
|
||||
|
||||
console.debug("newDesignerHeight", newDesignerHeight);
|
||||
|
||||
});
|
||||
|
||||
// Mouse up event - stop dragging
|
||||
@@ -399,16 +612,3 @@ function bindWorkflowDesignerSplitter(elementId) {
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user