/** @license Copyright (c) 2024 trading_peter This program is available under Apache License Version 2.0 */ import './tp-flow-node-port.js'; import { LitElement, html, css } from 'lit'; export class TpFlowNode extends LitElement { static get styles() { return [ css` :host { display: block; position: absolute; background: #2b2b2b; border-radius: 4px; color: #fff; min-width: 150px; } .node-body { display: grid; grid-template-columns: 20px 1fr 20px; } .node-content { padding: 20px; } .node-ports { justify-content: space-between; padding: 8px; } .input-ports, .output-ports { display: flex; flex-direction: column; gap: 8px; align-items: flex-start; justify-content: space-evenly; } .output-ports { align-items: flex-end; } header { display: flex; align-items: center; column-gap: 10px; padding: 2px 5px; } .delete-btn { cursor: pointer; opacity: 0.7; transition: opacity 0.2s; } .delete-btn:hover { opacity: 1; } ` ]; } render() { return html` ${this.renderNodeHeader()}
${this.inputs.map((input, idx) => html` `)}
${this.renderNodeContent()}
${this.outputs.map((output, idx) => html` `)}
`; } renderNodeHeader() { return html`
A Node ${!this.readonly ? html`
✕
` : null}
`; } renderNodeContent() { console.warn('Your node should override the renderNodeContent method.'); return null; } static get properties() { return { inputs: { type: Array }, outputs: { type: Array }, x: { type: Number }, y: { type: Number }, data: { type: Object }, readonly: { type: Boolean, reflect: true } }; } constructor() { super(); this.inputs = []; this.outputs = []; this.x = 0; this.y = 0; this.data = {}; this.readonly = false; this._handleConnectionDragStart = this._handleConnectionDragStart.bind(this); this._handleConnectionDragEnd = this._handleConnectionDragEnd.bind(this); } connectedCallback() { super.connectedCallback(); this.addEventListener('connection-drag-start', this._handleConnectionDragStart); this.addEventListener('connection-drag-end', this._handleConnectionDragEnd); } disconnectedCallback() { super.disconnectedCallback(); this.removeEventListener('connection-drag-start', this._handleConnectionDragStart); this.removeEventListener('connection-drag-end', this._handleConnectionDragEnd); } /** * Called when a connection drag starts somewhere on the canvas. * The detail contains the source node, port ID, and the source port's dataTypes. * Each node should evaluate its input ports and dim incompatible ones. * * Override `checkPortCompatibility(sourceDataTypes, targetDataTypes)` to implement * type-aware dimming. Return true if compatible, false if not. * * @param {CustomEvent} e — detail: { sourceNode, sourcePortId, sourceDataTypes } */ _handleConnectionDragStart(e) { // Don't dim ports on the source node itself. if (e.detail.sourceNode === this) { return; } const sourceDataTypes = e.detail.sourceDataTypes || []; // Evaluate each input port. const inputPorts = this.shadowRoot.querySelectorAll('.input-ports tp-flow-node-port'); inputPorts.forEach(port => { const targetDataTypes = port.dataTypes || []; const isCompatible = this.checkPortCompatibility(sourceDataTypes, targetDataTypes); if (isCompatible) { port.removeAttribute('dimmed'); port.setAttribute('compatible', ''); } else { port.setAttribute('dimmed', ''); port.removeAttribute('compatible'); } }); } _handleConnectionDragEnd() { const ports = this.shadowRoot.querySelectorAll('tp-flow-node-port'); ports.forEach(port => { port.removeAttribute('dimmed'); port.removeAttribute('compatible'); }); } /** * Override this method to implement type-aware compatibility checking. * Default: everything is compatible (no dimming). * * @param {string[]} sourceDataTypes — data types of the source port * @param {string[]} targetDataTypes — data types of the target port * @returns {boolean} true if compatible */ checkPortCompatibility(sourceDataTypes, targetDataTypes) { return true; } updated(changes) { super.updated(changes); this.dispatchEvent(new CustomEvent('update-layout', { detail: this, bubbles: true, composed: true })); } _handlePortClick(e) { e.stopPropagation(); this.dispatchEvent(new CustomEvent('port-click', { detail: { node: this, port: e.target, dataTypes: e.target.dataTypes, }, bubbles: true, composed: true })); } _handleDelete(e) { e.stopPropagation(); this.dispatchEvent(new CustomEvent('node-delete-requested', { detail: { nodeId: this.id }, bubbles: true, composed: true })); } exportData() { const transform = window.getComputedStyle(this).transform; const matrix = new DOMMatrix(transform); return { id: this.id, type: super.tagName.toLowerCase(), position: { x: matrix.m41, y: matrix.m42 }, data: this.data }; } importData(data) { this.id = data.id; this.data = data.data; if (data.position) { this.style.transform = `translate(${data.position.x}px, ${data.position.y}px)`; } } dispatchDataUpdate() { this.dispatchEvent(new CustomEvent('flow-changed', { detail: { type: 'node-data-changed', data: this.exportData() }, bubbles: true, composed: true })); } }