230 lines
5.8 KiB
JavaScript
230 lines
5.8 KiB
JavaScript
/**
|
|
@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`
|
|
<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 = [];
|
|
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);
|