/** @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'; import { zoom } from './zoom.js'; export class TpFlowNodes extends zoom(panning(connections(LitElement))) { static nodeTypes = new Map(); static get styles() { return [ connectionStyles, css` :host { display: block; overflow: hidden; position: relative; } .canvas { width: 100%; height: 100%; user-select: none; position: absolute; min-width: 100%; min-height: 100%; overflow: visible; transform: translate(0px, 0px); } ` ]; } render() { return html`
${this.nodes.map(node => html`${node}`)} ${this._renderConnections()}
`; } static get properties() { return { nodes: { type: Array }, connections: { type: Array } }; } constructor() { super(); this.nodes = []; 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); } exportFlow() { return { nodes: this.nodes.map(node => node.exportData()), connections: this.connections, canvas: { scale: this.scale, position: { x: this.currentX || 0, y: this.currentY || 0 } } }; } async importFlow(flowData) { // Clear existing this.nodes = []; this.connections = []; // Create all nodes first const nodes = flowData.nodes.map(nodeData => { const node = this._createNode(nodeData.type); node.importData(nodeData); return node; }); // 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 this.connections = flowData.connections; // 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; } /** * Register a node type * @param {string} type Node type identifier * @param {typeof TpFlowNode} nodeClass Node class to register */ static registerNode(type, nodeClass) { TpFlowNodes.nodeTypes.set(type, nodeClass); } /** * Create a new node instance * @param {string} type Node type identifier * @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 */ createNode(type, initialData = {}, x = 0, y = 0) { const nodeClass = TpFlowNodes.nodeTypes.get(type); if (!nodeClass) { throw new Error(`Unknown node type: ${type}`); } const node = new nodeClass(); node.setAttribute('part', 'node'); node.setAttribute('exportparts', 'node-content'); node.importData({ id: `node_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`, position: { x, y }, data: initialData }); this.nodes = [...this.nodes, node]; this._dispatchChangeEvent({ type: 'node-added', data: { nodeId: node.id, nodeType: type } }); 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: { type: detail.type, // Type of change: 'node-added', 'node-removed', 'node-moved', 'connection-added', etc. data: detail.data, // Additional data specific to the change flow: this.exportFlow() // Current state of the entire flow }, bubbles: true, composed: true })); } } window.customElements.define('tp-flow-nodes', TpFlowNodes);