wip
This commit is contained in:
parent
01bcdffaf8
commit
80f69bf09c
183
connections.js
Normal file
183
connections.js
Normal file
@ -0,0 +1,183 @@
|
|||||||
|
import { html, css, svg } from 'lit';
|
||||||
|
|
||||||
|
export const connectionStyles = css`
|
||||||
|
.connections {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connections path {
|
||||||
|
transition: stroke 0.3s ease, stroke-width 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connections path:hover {
|
||||||
|
stroke: #999;
|
||||||
|
stroke-width: 3;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const connections = function(superClass) {
|
||||||
|
return class extends superClass {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.connections = [];
|
||||||
|
this.draggingConnection = null;
|
||||||
|
this.mousePosition = { x: 0, y: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
firstUpdated() {
|
||||||
|
this.addEventListener('port-click', this._handlePortClick);
|
||||||
|
document.addEventListener('mousemove', this._updatePreviewConnection.bind(this));
|
||||||
|
super.firstUpdated();
|
||||||
|
}
|
||||||
|
|
||||||
|
_updatePreviewConnection(e) {
|
||||||
|
if (this.draggingConnection) {
|
||||||
|
this.mousePosition = {
|
||||||
|
x: e.clientX,
|
||||||
|
y: e.clientY
|
||||||
|
};
|
||||||
|
this.requestUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_renderConnections() {
|
||||||
|
return html`
|
||||||
|
<svg class="connections" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none;">
|
||||||
|
${this.connections.map(conn => this._renderConnection(conn))}
|
||||||
|
${this.draggingConnection ? this._renderPreviewConnection() : null}
|
||||||
|
</svg>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
_renderPreviewConnection() {
|
||||||
|
const sourceNode = this.nodes.find(node => node.id === this.draggingConnection.sourceNodeId);
|
||||||
|
if (!sourceNode) return '';
|
||||||
|
|
||||||
|
const sourcePort = sourceNode.shadowRoot.querySelector(
|
||||||
|
`.output-ports [data-port-id="${this.draggingConnection.sourcePortId}"]`
|
||||||
|
);
|
||||||
|
if (!sourcePort) return '';
|
||||||
|
|
||||||
|
const sourceRect = sourcePort.getBoundingClientRect();
|
||||||
|
const canvasRect = this.canvas.getBoundingClientRect();
|
||||||
|
|
||||||
|
const start = {
|
||||||
|
x: sourceRect.left + sourceRect.width/2 - canvasRect.left,
|
||||||
|
y: sourceRect.top + sourceRect.height/2 - canvasRect.top
|
||||||
|
};
|
||||||
|
|
||||||
|
const end = {
|
||||||
|
x: this.mousePosition.x - canvasRect.left,
|
||||||
|
y: this.mousePosition.y - canvasRect.top
|
||||||
|
};
|
||||||
|
|
||||||
|
const controlPoint1 = {
|
||||||
|
x: start.x + Math.min(100, Math.abs(end.x - start.x) / 2),
|
||||||
|
y: start.y
|
||||||
|
};
|
||||||
|
|
||||||
|
const controlPoint2 = {
|
||||||
|
x: end.x - Math.min(100, Math.abs(end.x - start.x) / 2),
|
||||||
|
y: end.y
|
||||||
|
};
|
||||||
|
|
||||||
|
const path = `M${start.x},${start.y} C${controlPoint1.x},${controlPoint1.y} ${controlPoint2.x},${controlPoint2.y} ${end.x},${end.y}`;
|
||||||
|
|
||||||
|
return svg`
|
||||||
|
<path
|
||||||
|
d="${path}"
|
||||||
|
fill="none"
|
||||||
|
stroke="#666"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-dasharray="5,5"
|
||||||
|
id="preview-connection"
|
||||||
|
/>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
_renderConnection(conn) {
|
||||||
|
const sourceNode = this.nodes.find(node => node.id === conn.sourceNodeId);
|
||||||
|
const targetNode = this.nodes.find(node => node.id === conn.targetNodeId);
|
||||||
|
|
||||||
|
if (!sourceNode || !targetNode) return '';
|
||||||
|
|
||||||
|
const sourcePort = sourceNode.shadowRoot.querySelector(`.output-ports [data-port-id="${conn.sourcePortId}"]`);
|
||||||
|
const targetPort = targetNode.shadowRoot.querySelector(`.input-ports [data-port-id="${conn.targetPortId}"]`);
|
||||||
|
|
||||||
|
if (!sourcePort || !targetPort) return '';
|
||||||
|
|
||||||
|
const sourceRect = sourcePort.getBoundingClientRect();
|
||||||
|
const targetRect = targetPort.getBoundingClientRect();
|
||||||
|
const canvasRect = this.canvas.getBoundingClientRect();
|
||||||
|
|
||||||
|
const start = {
|
||||||
|
x: sourceRect.left + sourceRect.width/2 - canvasRect.left,
|
||||||
|
y: sourceRect.top + sourceRect.height/2 - canvasRect.top
|
||||||
|
};
|
||||||
|
|
||||||
|
const end = {
|
||||||
|
x: targetRect.left + targetRect.width/2 - canvasRect.left,
|
||||||
|
y: targetRect.top + targetRect.height/2 - canvasRect.top
|
||||||
|
};
|
||||||
|
|
||||||
|
const controlPoint1 = {
|
||||||
|
x: start.x + Math.min(100, Math.abs(end.x - start.x) / 2),
|
||||||
|
y: start.y
|
||||||
|
};
|
||||||
|
|
||||||
|
const controlPoint2 = {
|
||||||
|
x: end.x - Math.min(100, Math.abs(end.x - start.x) / 2),
|
||||||
|
y: end.y
|
||||||
|
};
|
||||||
|
|
||||||
|
const path = `M${start.x},${start.y} C${controlPoint1.x},${controlPoint1.y} ${controlPoint2.x},${controlPoint2.y} ${end.x},${end.y}`;
|
||||||
|
|
||||||
|
return svg`
|
||||||
|
<path
|
||||||
|
d="${path}"
|
||||||
|
fill="none"
|
||||||
|
stroke="#666"
|
||||||
|
stroke-width="2"
|
||||||
|
id="${conn.id}"
|
||||||
|
/>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
_handlePortClick(e) {
|
||||||
|
const { nodeId, portType, portId, portName } = e.detail;
|
||||||
|
|
||||||
|
if (!this.draggingConnection) {
|
||||||
|
if (portType === 'output') {
|
||||||
|
this.draggingConnection = {
|
||||||
|
sourceNodeId: nodeId,
|
||||||
|
sourcePortId: portId,
|
||||||
|
sourcePortType: portType
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (portType === 'input' && nodeId !== this.draggingConnection.sourceNodeId) {
|
||||||
|
const connectionExists = this.connections.some(conn =>
|
||||||
|
conn.sourceNodeId === this.draggingConnection.sourceNodeId &&
|
||||||
|
conn.sourcePortId === this.draggingConnection.sourcePortId &&
|
||||||
|
conn.targetNodeId === nodeId &&
|
||||||
|
conn.targetPortId === portId
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!connectionExists) {
|
||||||
|
const connection = {
|
||||||
|
id: `conn_${Date.now()}`,
|
||||||
|
sourceNodeId: this.draggingConnection.sourceNodeId,
|
||||||
|
sourcePortId: this.draggingConnection.sourcePortId,
|
||||||
|
targetNodeId: nodeId,
|
||||||
|
targetPortId: portId
|
||||||
|
};
|
||||||
|
|
||||||
|
this.connections = [...this.connections, connection];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.draggingConnection = null;
|
||||||
|
this.requestUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
56
demo-node.js
Normal file
56
demo-node.js
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
/**
|
||||||
|
@license
|
||||||
|
Copyright (c) 2024 trading_peter
|
||||||
|
This program is available under Apache License Version 2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { html, css } from 'lit';
|
||||||
|
import { TpFlowNode } from './tp-flow-node';
|
||||||
|
|
||||||
|
class DemoNode extends TpFlowNode {
|
||||||
|
static get styles() {
|
||||||
|
return [
|
||||||
|
...super.styles,
|
||||||
|
css`
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
renderNodeContent() {
|
||||||
|
return html`
|
||||||
|
<select @change="${this._handleOperationChange}">
|
||||||
|
<option value="add">Add</option>
|
||||||
|
<option value="subtract">Subtract</option>
|
||||||
|
</select>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get properties() {
|
||||||
|
return { };
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.flowNodeType = 'MathNode';
|
||||||
|
this.inputs = [
|
||||||
|
{ name: 'value1' },
|
||||||
|
{ name: 'value2' }
|
||||||
|
];
|
||||||
|
this.outputs = [
|
||||||
|
{ name: 'result' }
|
||||||
|
];
|
||||||
|
this.data = {
|
||||||
|
operation: 'add'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleOperationChange(e) {
|
||||||
|
this.data.operation = e.target.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.customElements.define('demo-node', DemoNode);
|
@ -1,14 +1,14 @@
|
|||||||
{
|
{
|
||||||
"name": "@tp/tp-element",
|
"name": "@tp/tp-flow-nodes",
|
||||||
"version": "0.0.1",
|
"version": "1.0.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "tp-element.js",
|
"main": "tp-flow-nodes.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://gitea.codeblob.work/tp-elements/tp-element.git"
|
"url": "https://gitea.codeblob.work/tp-elements/tp-flow-nodes.git"
|
||||||
},
|
},
|
||||||
"author": "trading_peter",
|
"author": "trading_peter",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
|
98
panning.js
Normal file
98
panning.js
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
import { TpFlowNode } from './tp-flow-node.js';
|
||||||
|
|
||||||
|
export const panning = function(superClass) {
|
||||||
|
return class extends superClass {
|
||||||
|
static get properties() {
|
||||||
|
return {
|
||||||
|
isDragging: { type: Boolean },
|
||||||
|
currentX: { type: Number },
|
||||||
|
currentY: { type: Number },
|
||||||
|
initialX: { type: Number },
|
||||||
|
initialY: { type: Number },
|
||||||
|
xOffset: { type: Number },
|
||||||
|
yOffset: { type: Number },
|
||||||
|
highestZIndex: { type: Number }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.isDragging = false;
|
||||||
|
this.currentX = 0;
|
||||||
|
this.currentY = 0;
|
||||||
|
this.initialX = 0;
|
||||||
|
this.initialY = 0;
|
||||||
|
this.xOffset = 0;
|
||||||
|
this.yOffset = 0;
|
||||||
|
this.targetElement = null;
|
||||||
|
this.highestZIndex = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
firstUpdated() {
|
||||||
|
super.firstUpdated();
|
||||||
|
this.canvas = this.shadowRoot.querySelector('.canvas');
|
||||||
|
this.addEventListener('mousedown', this._startDrag.bind(this));
|
||||||
|
document.addEventListener('mousemove', this._drag.bind(this));
|
||||||
|
document.addEventListener('mouseup', this._endDrag.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
_getTopNodeAtPoint(x, y) {
|
||||||
|
const nodes = this.shadowRoot.elementsFromPoint(x, y)
|
||||||
|
.filter(el => el instanceof TpFlowNode)
|
||||||
|
.sort((a, b) => {
|
||||||
|
const aZ = parseInt(getComputedStyle(a).zIndex) || 0;
|
||||||
|
const bZ = parseInt(getComputedStyle(b).zIndex) || 0;
|
||||||
|
return bZ - aZ;
|
||||||
|
});
|
||||||
|
|
||||||
|
return nodes[0] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
_bringToFront(element) {
|
||||||
|
if (!(element instanceof TpFlowNode)) return;
|
||||||
|
|
||||||
|
this.highestZIndex++;
|
||||||
|
element.style.zIndex = this.highestZIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
_startDrag(e) {
|
||||||
|
const topNode = this._getTopNodeAtPoint(e.clientX, e.clientY);
|
||||||
|
|
||||||
|
if (topNode) {
|
||||||
|
this.targetElement = topNode;
|
||||||
|
this._bringToFront(topNode);
|
||||||
|
} else {
|
||||||
|
this.targetElement = this.canvas;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.targetElement) {
|
||||||
|
this.isDragging = true;
|
||||||
|
|
||||||
|
const transform = window.getComputedStyle(this.targetElement).transform;
|
||||||
|
const matrix = new DOMMatrix(transform);
|
||||||
|
this.xOffset = matrix.m41;
|
||||||
|
this.yOffset = matrix.m42;
|
||||||
|
|
||||||
|
this.initialX = e.clientX - this.xOffset;
|
||||||
|
this.initialY = e.clientY - this.yOffset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_drag(e) {
|
||||||
|
if (this.isDragging && this.targetElement) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
this.currentX = e.clientX - this.initialX;
|
||||||
|
this.currentY = e.clientY - this.initialY;
|
||||||
|
|
||||||
|
this.targetElement.style.transform =
|
||||||
|
`translate(${this.currentX}px, ${this.currentY}px)`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_endDrag() {
|
||||||
|
this.isDragging = false;
|
||||||
|
this.targetElement = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
@ -1,35 +0,0 @@
|
|||||||
/**
|
|
||||||
@license
|
|
||||||
Copyright (c) 2024 trading_peter
|
|
||||||
This program is available under Apache License Version 2.0
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { LitElement, html, css } from 'lit';
|
|
||||||
|
|
||||||
class TpElement extends LitElement {
|
|
||||||
static get styles() {
|
|
||||||
return [
|
|
||||||
css`
|
|
||||||
:host {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
`
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { } = this;
|
|
||||||
|
|
||||||
return html`
|
|
||||||
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
static get properties() {
|
|
||||||
return { };
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
window.customElements.define('tp-element', TpElement);
|
|
168
tp-flow-node.js
Normal file
168
tp-flow-node.js
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
/**
|
||||||
|
@license
|
||||||
|
Copyright (c) 2024 trading_peter
|
||||||
|
This program is available under Apache License Version 2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { LitElement, html, css } from 'lit';
|
||||||
|
|
||||||
|
// tp-flow-node.js
|
||||||
|
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-header {
|
||||||
|
padding: 8px;
|
||||||
|
background: #3b3b3b;
|
||||||
|
border-bottom: 1px solid #4b4b4b;
|
||||||
|
border-radius: 4px 4px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.port {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
background: #666;
|
||||||
|
border-radius: 50%;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.port:hover {
|
||||||
|
background: #888;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return html`
|
||||||
|
<div class="node-header">
|
||||||
|
<slot name="title">${this.flowNodeType}</slot>
|
||||||
|
</div>
|
||||||
|
<div class="node-body">
|
||||||
|
<div class="input-ports">
|
||||||
|
${this.inputs.map((input, idx) => html`
|
||||||
|
<div class="port"
|
||||||
|
data-port-type="input"
|
||||||
|
data-port-id="${idx}"
|
||||||
|
data-port-name="${input.name}"
|
||||||
|
@mousedown="${this._handlePortClick}">
|
||||||
|
</div>
|
||||||
|
`)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="node-content">
|
||||||
|
${this.renderNodeContent()}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="output-ports">
|
||||||
|
${this.outputs.map((output, idx) => html`
|
||||||
|
<div class="port"
|
||||||
|
data-port-type="output"
|
||||||
|
data-port-id="${idx}"
|
||||||
|
data-port-name="${output.name}"
|
||||||
|
@mousedown="${this._handlePortClick}">
|
||||||
|
</div>
|
||||||
|
`)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderNodeContent() {
|
||||||
|
console.warn('Your node should override the renderNodeContent method.');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get properties() {
|
||||||
|
return {
|
||||||
|
flowNodeType: { type: String },
|
||||||
|
inputs: { type: Array },
|
||||||
|
outputs: { type: Array },
|
||||||
|
x: { type: Number },
|
||||||
|
y: { type: Number },
|
||||||
|
data: { type: Object }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.flowNodeType = 'BaseNode';
|
||||||
|
this.inputs = [];
|
||||||
|
this.outputs = [];
|
||||||
|
this.x = 0;
|
||||||
|
this.y = 0;
|
||||||
|
this.data = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
_handlePortClick(e) {
|
||||||
|
e.stopPropagation(); // Prevents the event from getting caught by the panning action.
|
||||||
|
|
||||||
|
const portEl = e.target;
|
||||||
|
const detail = {
|
||||||
|
nodeId: this.id,
|
||||||
|
portType: portEl.dataset.portType,
|
||||||
|
portId: portEl.dataset.portId,
|
||||||
|
portName: portEl.dataset.portName
|
||||||
|
};
|
||||||
|
|
||||||
|
this.dispatchEvent(new CustomEvent('port-click', {
|
||||||
|
detail,
|
||||||
|
bubbles: true,
|
||||||
|
composed: true
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method to export node data
|
||||||
|
exportData() {
|
||||||
|
return {
|
||||||
|
id: this.id,
|
||||||
|
type: this.flowNodeType,
|
||||||
|
x: this.x,
|
||||||
|
y: this.y,
|
||||||
|
data: this.data
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method to import node data
|
||||||
|
importData(data) {
|
||||||
|
this.id = data.id;
|
||||||
|
this.x = data.x;
|
||||||
|
this.y = data.y;
|
||||||
|
this.data = data.data;
|
||||||
|
}
|
||||||
|
}
|
82
tp-flow-nodes.js
Normal file
82
tp-flow-nodes.js
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
/**
|
||||||
|
@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';
|
||||||
|
|
||||||
|
class TpFlowNodes extends panning(connections(LitElement)) {
|
||||||
|
static get styles() {
|
||||||
|
return [
|
||||||
|
connectionStyles,
|
||||||
|
css`
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.canvas {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
user-select: none;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
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.previewConnection = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export flow chart data
|
||||||
|
exportFlow() {
|
||||||
|
return {
|
||||||
|
nodes: this.nodes.map(node => node.exportData()),
|
||||||
|
connections: this.connections
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Import flow chart data
|
||||||
|
importFlow(flowData) {
|
||||||
|
// Clear existing
|
||||||
|
this.nodes = [];
|
||||||
|
this.connections = [];
|
||||||
|
|
||||||
|
// Create nodes
|
||||||
|
flowData.nodes.forEach(nodeData => {
|
||||||
|
const node = this._createNode(nodeData.type);
|
||||||
|
node.importData(nodeData);
|
||||||
|
this.nodes.push(node);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Restore connections
|
||||||
|
this.connections = flowData.connections;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.customElements.define('tp-flow-nodes', TpFlowNodes);
|
Loading…
x
Reference in New Issue
Block a user