tp-flow-nodes/tp-flow-nodes.js

248 lines
6.5 KiB
JavaScript
Raw Permalink Normal View History

2024-12-18 21:27:29 +01:00
/**
@license
Copyright (c) 2024 trading_peter
This program is available under Apache License Version 2.0
*/
import { LitElement, html, css } from 'lit';
import { connections, connectionStyles } from './connections.js';
import { panning } from './panning.js';
2024-12-18 22:27:27 +01:00
import { zoom } from './zoom.js';
2024-12-18 21:27:29 +01:00
2024-12-20 10:29:39 +01:00
export class TpFlowNodes extends zoom(panning(connections(LitElement))) {
static nodeTypes = new Map();
2024-12-18 21:27:29 +01:00
static get styles() {
return [
connectionStyles,
css`
:host {
display: block;
overflow: hidden;
position: relative;
}
.canvas {
width: 100%;
height: 100%;
user-select: none;
2024-12-18 22:27:27 +01:00
position: absolute;
min-width: 100%;
min-height: 100%;
overflow: visible;
2024-12-18 21:27:29 +01:00
transform: translate(0px, 0px);
}
`
];
}
render() {
return html`
<div class="canvas">
${this.nodes.map(node => html`${node}`)}
${this._renderConnections()}
</div>
`;
}
static get properties() {
return {
nodes: { type: Array },
connections: { type: Array }
};
}
constructor() {
super();
this.nodes = [];
2024-12-20 10:29:39 +01:00
this._boundDeleteHandler = this._handleNodeDeleteRequested.bind(this);
}
connectedCallback() {
super.connectedCallback();
this.addEventListener('node-delete-requested', this._boundDeleteHandler);
}
disconnectedCallback() {
super.disconnectedCallback();
this.removeEventListener('node-delete-requested', this._boundDeleteHandler);
}
_handleNodeDeleteRequested(e) {
const { nodeId } = e.detail;
this.removeNode(nodeId);
2024-12-18 21:27:29 +01:00
}
exportFlow() {
return {
nodes: this.nodes.map(node => node.exportData()),
2024-12-20 10:29:39 +01:00
connections: this.connections,
canvas: {
scale: this.scale,
position: {
x: this.currentX || 0,
y: this.currentY || 0
}
}
2024-12-18 21:27:29 +01:00
};
}
2024-12-20 10:29:39 +01:00
async importFlow(flowData) {
2024-12-18 21:27:29 +01:00
// Clear existing
this.nodes = [];
this.connections = [];
2024-12-20 10:29:39 +01:00
// Create all nodes first
const nodes = flowData.nodes.map(nodeData => {
2024-12-20 10:57:37 +01:00
const node = this.createNode(nodeData.type);
2024-12-18 21:27:29 +01:00
node.importData(nodeData);
2024-12-20 10:29:39 +01:00
return node;
2024-12-18 21:27:29 +01:00
});
2024-12-20 10:29:39 +01:00
// Set nodes and wait for render
this.nodes = nodes;
await this.updateComplete;
// Wait for all nodes to be ready
await Promise.all(this.nodes.map(node => node.updateComplete));
// Only after nodes are ready, restore connections
2024-12-18 21:27:29 +01:00
this.connections = flowData.connections;
2024-12-20 10:29:39 +01:00
// Restore canvas state
if (flowData.canvas) {
if (flowData.canvas.scale) {
this._applyZoom(flowData.canvas.scale);
}
if (flowData.canvas.position) {
this.currentX = flowData.canvas.position.x;
this.currentY = flowData.canvas.position.y;
this.canvas.style.transform =
`translate(${this.currentX}px, ${this.currentY}px) scale(${this.scale})`;
}
}
await this.updateComplete;
}
/**
2024-12-20 10:57:37 +01:00
* Register a node class
2024-12-20 10:29:39 +01:00
* @param {typeof TpFlowNode} nodeClass Node class to register
*/
2024-12-20 10:57:37 +01:00
static registerNode(nodeClass) {
// Get the registered tag name for this class
const tagName = (new nodeClass()).tagName?.toLowerCase();
if (!tagName) {
throw new Error(`Node class ${nodeClass.name} must be registered as a custom element first`);
}
TpFlowNodes.nodeTypes.set(nodeClass, tagName);
TpFlowNodes.nodeTypes.set(tagName, nodeClass);
2024-12-20 10:29:39 +01:00
}
/**
* Create a new node instance
2024-12-20 10:57:37 +01:00
* @param {typeof TpFlowNode|string} NodeClassOrTagName Node class or tag name to instantiate
2024-12-20 10:29:39 +01:00
* @param {Object} initialData Initial data for the node
* @param {number} x Initial x position
* @param {number} y Initial y position
* @returns {TpFlowNode} The created node instance
*/
2024-12-20 10:57:37 +01:00
createNode(NodeClassOrTagName, initialData = {}, x = 0, y = 0) {
let NodeClass;
if (typeof NodeClassOrTagName === 'string') {
NodeClass = TpFlowNodes.nodeTypes.get(NodeClassOrTagName.toLowerCase());
if (!NodeClass) {
throw new Error(`Unknown node type: ${NodeClassOrTagName}`);
}
} else {
NodeClass = NodeClassOrTagName;
if (!TpFlowNodes.nodeTypes.has(NodeClass)) {
throw new Error(`Node class not registered: ${NodeClass.name}`);
}
2024-12-20 10:29:39 +01:00
}
2024-12-20 10:57:37 +01:00
2024-12-20 14:05:35 +01:00
// Adjust position for current canvas translation and scale
const adjustedX = (x - this.currentX) / this.scale;
const adjustedY = (y - this.currentY) / this.scale;
2024-12-20 10:57:37 +01:00
const node = new NodeClass();
2024-12-20 10:29:39 +01:00
node.setAttribute('part', 'node');
node.setAttribute('exportparts', 'node-content');
node.importData({
id: `node_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`,
2024-12-20 14:05:35 +01:00
position: { x: adjustedX, y: adjustedY },
2024-12-20 10:29:39 +01:00
data: initialData
});
2024-12-20 10:57:37 +01:00
2024-12-20 10:29:39 +01:00
this.nodes = [...this.nodes, node];
2024-12-20 10:57:37 +01:00
2024-12-20 10:29:39 +01:00
this._dispatchChangeEvent({
type: 'node-added',
2024-12-20 10:57:37 +01:00
data: { nodeId: node.id, nodeType: node.tagName.toLowerCase() }
2024-12-20 10:29:39 +01:00
});
2024-12-20 10:57:37 +01:00
2024-12-20 10:29:39 +01:00
return node;
}
_createNode(type) {
const nodeClass = TpFlowNodes.nodeTypes.get(type);
if (!nodeClass) {
throw new Error(`Unknown node type: ${type}`);
}
return new nodeClass();
}
/**
* Unregister a node type
* @param {string} type Node type identifier
* @returns {boolean} True if the type was unregistered, false if it didn't exist
*/
static unregisterNode(type) {
return TpFlowNodes.nodeTypes.delete(type);
}
/**
* Remove a node by its ID
* @param {string} nodeId ID of the node to remove
* @returns {boolean} True if the node was found and removed, false otherwise
*/
removeNode(nodeId) {
const initialLength = this.nodes.length;
// Remove the node
this.nodes = this.nodes.filter(node => node.id !== nodeId);
// Remove any connections associated with this node
this.connections = this.connections.filter(conn =>
conn.sourceNodeId !== nodeId && conn.targetNodeId !== nodeId
);
// Return true if a node was actually removed
const removed = this.nodes.length < initialLength;
if (removed) {
this._dispatchChangeEvent({
type: 'node-removed',
data: { nodeId }
});
}
return removed;
}
_dispatchChangeEvent(detail = {}) {
this.dispatchEvent(new CustomEvent('flow-changed', {
detail: {
2024-12-20 14:05:35 +01:00
type: detail.type, // Type of change: 'node-added', 'node-removed', 'node-moved', 'connection-added', 'node-data-changed'.
data: detail.data,
2024-12-20 10:29:39 +01:00
},
bubbles: true,
composed: true
}));
2024-12-18 21:27:29 +01:00
}
}
window.customElements.define('tp-flow-nodes', TpFlowNodes);