Files
tp-flow-nodes/tp-flow-node.js
T
2026-09-27 18:58:32 +02:00

283 lines
7.1 KiB
JavaScript

/**
@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()}
<div class="node-body">
<div class="input-ports">
${this.inputs.map((input, idx) => html`
<tp-flow-node-port class="port" exportparts="connection-point"
portType="input"
.portId=${idx}
.portName=${input.name}
.tagContent=${input.tagContent}
.shape=${input.shape}
.color=${input.color}
.dataTypes=${input.dataTypes}
@mousedown="${this._handlePortClick}">
</tp-flow-node-port>
`)}
</div>
<div class="node-content" part="node-content">
${this.renderNodeContent()}
</div>
<div class="output-ports">
${this.outputs.map((output, idx) => html`
<tp-flow-node-port class="port" exportparts="connection-point"
portType="output"
.portId=${idx}
.portName=${output.name}
.tagContent=${output.tagContent}
.shape=${output.shape}
.color=${output.color}
.dataTypes=${output.dataTypes}
@mousedown="${this._handlePortClick}">
</tp-flow-node-port>
`)}
</div>
</div>
`;
}
renderNodeHeader() {
return html`
<header drag-node>
A Node
${!this.readonly ? html`<div class="delete-btn" @click="${this._handleDelete}">✕</div>` : null}
</header>
`;
}
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
}));
}
}