From ef49374334ecd6d39500a4f2b93cc4e8b9cf3493 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Mon, 9 Jun 2025 10:46:32 +0100 Subject: [PATCH] Allow port appearance to vary if connected --- .../@node-red/editor-client/src/js/nodes.js | 30 +++- .../@node-red/editor-client/src/js/ui/view.js | 142 +++++++++++++----- .../editor-client/src/sass/flow.scss | 4 + 3 files changed, 133 insertions(+), 43 deletions(-) diff --git a/packages/node_modules/@node-red/editor-client/src/js/nodes.js b/packages/node_modules/@node-red/editor-client/src/js/nodes.js index a4cb79698..fc3347eeb 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/nodes.js +++ b/packages/node_modules/@node-red/editor-client/src/js/nodes.js @@ -725,7 +725,9 @@ RED.nodes = (function() { } allNodes.addNode(newNode); if (!nodeLinks[n.id]) { - nodeLinks[n.id] = {in:[],out:[]}; + nodeLinks[n.id] = { + inCount:[],outCount:[],in:[],out:[] + } } } RED.events.emit('nodes:add',newNode, opt); @@ -744,15 +746,19 @@ RED.nodes = (function() { if (l.source) { // Possible the node hasn't been added yet if (!nodeLinks[l.source.id]) { - nodeLinks[l.source.id] = {in:[],out:[]}; + nodeLinks[l.source.id] = {inCount:[],outCount:[],in:[],out:[]}; } nodeLinks[l.source.id].out.push(l); + nodeLinks[l.source.id].outCount[l.sourcePort] = (nodeLinks[l.source.id].outCount[l.sourcePort] || 0) + 1 + l.source.dirty = true; } if (l.target) { if (!nodeLinks[l.target.id]) { - nodeLinks[l.target.id] = {in:[],out:[]}; + nodeLinks[l.target.id] = {inCount:[],outCount:[],in:[],out:[]}; } nodeLinks[l.target.id].in.push(l); + nodeLinks[l.target.id].inCount[0] = (nodeLinks[l.target.id].inCount[0] || 0) + 1 + l.target.dirty = true; } if (l.source.z === l.target.z && linkTabMap[l.source.z]) { linkTabMap[l.source.z].push(l); @@ -945,15 +951,19 @@ RED.nodes = (function() { if (index != -1) { links.splice(index,1); if (l.source && nodeLinks[l.source.id]) { + l.source.dirty = true; var sIndex = nodeLinks[l.source.id].out.indexOf(l) if (sIndex !== -1) { nodeLinks[l.source.id].out.splice(sIndex,1) + nodeLinks[l.source.id].outCount[l.sourcePort]-- } } if (l.target && nodeLinks[l.target.id]) { + l.target.dirty = true; var tIndex = nodeLinks[l.target.id].in.indexOf(l) if (tIndex !== -1) { nodeLinks[l.target.id].in.splice(tIndex,1) + nodeLinks[l.target.id].inCount[0]-- } } if (l.source.z === l.target.z && linkTabMap[l.source.z]) { @@ -3386,6 +3396,20 @@ RED.nodes = (function() { } return []; }, + getNodeLinkCount: function(id, portType, index) { + // We *could* just let callers use `getNodeLinks` and get the + // the length for themselves. However, that function creates + // a clone of the array - which is needless work if all you + // want is the length + if (nodeLinks[id]) { + if (portType === 1) { + return nodeLinks[id].inCount[index] || 0 + } else { + return nodeLinks[id].outCount[index] || 0 + } + } + return 0; + }, addWorkspace: addWorkspace, removeWorkspace: removeWorkspace, getWorkspaceOrder: function() { return [...workspacesOrder] }, diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/view.js b/packages/node_modules/@node-red/editor-client/src/js/ui/view.js index 08ab0ec0a..903633024 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/view.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/view.js @@ -4967,45 +4967,106 @@ RED.view = (function() { this.__textGroup__.setAttribute("transform", "translate(38,"+yp+")"); } - var inputPorts = thisNode.selectAll(".red-ui-flow-port-input"); - if ((!isLink || (showAllLinkPorts === -1 && !activeLinkNodes[d.id])) && d.inputs === 0 && !inputPorts.empty()) { - inputPorts.each(function(d,i) { - if (!d.__ghost) { - RED.hooks.trigger("viewRemovePort",{ - node:d, - el:self, - port:d3.select(this)[0][0], - portType: "input", - portIndex: 0 - }) - } - }).remove(); - } else if (((isLink && (showAllLinkPorts===PORT_TYPE_INPUT||activeLinkNodes[d.id]))|| d.inputs === 1) && inputPorts.empty()) { - var inputGroup = thisNode.append("g").attr("class","red-ui-flow-port-input"); - var inputGroupPorts; - - if (d.type === "link in") { - inputGroupPorts = inputGroup.append("circle") - .attr("cx",-1).attr("cy",5) - .attr("r",5) - .attr("class","red-ui-flow-port red-ui-flow-link-port") + let numInputs = d.inputs; + if (isLink && d.type === "link in") { + if (showAllLinkPorts===PORT_TYPE_INPUT || activeLinkNodes[d.id]) { + numInputs = 1; } else { - inputGroupPorts = inputGroup.append("rect").attr("class","red-ui-flow-port").attr("rx",3).attr("ry",3).attr("width",10).attr("height",10) - } - inputGroup[0][0].__port__ = inputGroupPorts[0][0]; - inputGroupPorts[0][0].__data__ = this.__data__; - inputGroupPorts[0][0].__portType__ = PORT_TYPE_INPUT; - inputGroupPorts[0][0].__portIndex__ = 0; - if (!d.__ghost) { - inputGroupPorts.on("mousedown",function(d){portMouseDown(d,PORT_TYPE_INPUT,0);}) - .on("touchstart",function(d){portMouseDown(d,PORT_TYPE_INPUT,0);d3.event.preventDefault();}) - .on("mouseup",function(d){portMouseUp(d,PORT_TYPE_INPUT,0);} ) - .on("touchend",function(d){portMouseUp(d,PORT_TYPE_INPUT,0);d3.event.preventDefault();} ) - .on("mouseover",function(d){portMouseOver(d3.select(this),d,PORT_TYPE_INPUT,0);}) - .on("mouseout",function(d) {portMouseOut(d3.select(this),d,PORT_TYPE_INPUT,0);}); - RED.hooks.trigger("viewAddPort",{node:d,el: this, port: inputGroup[0][0], portType: "input", portIndex: 0}) + numInputs = 0; } } + // Remove extra ports + while (this.__inputs__.length > numInputs) { + const port = this.__inputs__.pop(); + if (!d.__ghost) { + RED.hooks.trigger("viewRemovePort",{ + node: d, + el: this, + port: port, + portType: "input", + portIndex: this.__inputs__.length + }) + } + port.remove(); + } + for (let portIndex = 0; portIndex < numInputs; portIndex++ ) { + let portGroup; + if (portIndex === this.__inputs__.length) { + portGroup = document.createElementNS("http://www.w3.org/2000/svg","g"); + portGroup.setAttribute("class","red-ui-flow-port-input"); + var portPort; + if (d.type === "link in") { + portPort = document.createElementNS("http://www.w3.org/2000/svg","circle"); + portPort.setAttribute("cx",-1); + portPort.setAttribute("cy",5); + portPort.setAttribute("r",5); + portPort.setAttribute("class","red-ui-flow-port red-ui-flow-link-port"); + } else { + portPort = document.createElementNS("http://www.w3.org/2000/svg","rect"); + portPort.setAttribute("rx",3); + portPort.setAttribute("ry",3); + portPort.setAttribute("width",10); + portPort.setAttribute("height",10); + portPort.setAttribute("class","red-ui-flow-port"); + } + portGroup.appendChild(portPort); + portGroup.__port__ = portPort; + portPort.__data__ = this.__data__; + portPort.__portType__ = PORT_TYPE_INPUT; + portPort.__portIndex__ = portIndex; + if (!d.__ghost) { + portPort.addEventListener("mousedown", portMouseDownProxy); + portPort.addEventListener("touchstart", portTouchStartProxy); + portPort.addEventListener("mouseup", portMouseUpProxy); + portPort.addEventListener("touchend", portTouchEndProxy); + portPort.addEventListener("mouseover", portMouseOverProxy); + portPort.addEventListener("mouseout", portMouseOutProxy); + } + + this.appendChild(portGroup); + this.__inputs__.push(portGroup); + if (!d.__ghost) { + RED.hooks.trigger("viewAddPort",{node:d,el: this, port: portGroup, portType: "input", portIndex: portIndex}) + } + } else { + portGroup = this.__inputs__[portIndex]; + } + const y = (d.h/2)-((numInputs-1)/2)*13; + portGroup.setAttribute("transform","translate(-5,"+((y+13*portIndex)-5)+")") + portGroup.classList.toggle("red-ui-flow-port-connected",RED.nodes.getNodeLinkCount(d.id,PORT_TYPE_INPUT,portIndex) > 0 ) + } + + + + // } else if (((isLink && (showAllLinkPorts===PORT_TYPE_INPUT||activeLinkNodes[d.id]))|| d.inputs === 1) && inputPorts.empty()) { + // var inputGroup = thisNode.append("g").attr("class","red-ui-flow-port-input"); + // var inputGroupPorts; + + // if (d.type === "link in") { + // inputGroupPorts = inputGroup.append("circle") + // .attr("cx",-1).attr("cy",5) + // .attr("r",5) + // .attr("class","red-ui-flow-port red-ui-flow-link-port") + // } else { + // inputGroupPorts = inputGroup.append("rect").attr("class","red-ui-flow-port").attr("rx",3).attr("ry",3).attr("width",10).attr("height",10) + // } + // inputGroup[0][0].__port__ = inputGroupPorts[0][0]; + // inputGroupPorts[0][0].__data__ = this.__data__; + // inputGroupPorts[0][0].__portType__ = PORT_TYPE_INPUT; + // inputGroupPorts[0][0].__portIndex__ = 0; + // if (!d.__ghost) { + // inputGroupPorts.on("mousedown",function(d){portMouseDown(d,PORT_TYPE_INPUT,0);}) + // .on("touchstart",function(d){portMouseDown(d,PORT_TYPE_INPUT,0);d3.event.preventDefault();}) + // .on("mouseup",function(d){portMouseUp(d,PORT_TYPE_INPUT,0);} ) + // .on("touchend",function(d){portMouseUp(d,PORT_TYPE_INPUT,0);d3.event.preventDefault();} ) + // .on("mouseover",function(d){portMouseOver(d3.select(this),d,PORT_TYPE_INPUT,0);}) + // .on("mouseout",function(d) {portMouseOut(d3.select(this),d,PORT_TYPE_INPUT,0);}); + // RED.hooks.trigger("viewAddPort",{node:d,el: this, port: inputGroup[0][0], portType: "input", portIndex: 0}) + // } + // } + + // inputPorts.classList.toggle("red-ui-flow-port-connected",RED.nodes.getNodeLinkCount(d.id,PORT_TYPE_INPUT,0) > 0 ) + var numOutputs = d.outputs; if (isLink && d.type === "link out") { if (d.mode !== "return" && (showAllLinkPorts===PORT_TYPE_OUTPUT || activeLinkNodes[d.id])) { @@ -5075,6 +5136,7 @@ RED.view = (function() { var x = d.w - 5; var y = (d.h/2)-((numOutputs-1)/2)*13; portGroup.setAttribute("transform","translate("+x+","+((y+13*portIndex)-5)+")") + portGroup.classList.toggle("red-ui-flow-port-connected",RED.nodes.getNodeLinkCount(d.id,PORT_TYPE_OUTPUT,portIndex) > 0 ) } if (d._def.icon) { var icon = thisNode.select(".red-ui-flow-node-icon"); @@ -5121,10 +5183,10 @@ RED.view = (function() { // this.__errorBadge__.setAttribute("transform", "translate("+(d.w-10-((d.changed||d.moved)?14:0))+", -2)"); // this.__errorBadge__.classList.toggle("hide", d.valid); - thisNode.selectAll(".red-ui-flow-port-input").each(function(d,i) { - var port = d3.select(this); - port.attr("transform",function(d){return "translate(-5,"+((d.h/2)-5)+")";}) - }); + // thisNode.selectAll(".red-ui-flow-port-input").each(function(d,i) { + // var port = d3.select(this); + // port.attr("transform",function(d){return "translate(-5,"+((d.h/2)-5)+")";}) + // }); if (d._def.button) { var buttonEnabled = isButtonEnabled(d); diff --git a/packages/node_modules/@node-red/editor-client/src/sass/flow.scss b/packages/node_modules/@node-red/editor-client/src/sass/flow.scss index 944536845..024b499c0 100644 --- a/packages/node_modules/@node-red/editor-client/src/sass/flow.scss +++ b/packages/node_modules/@node-red/editor-client/src/sass/flow.scss @@ -234,6 +234,10 @@ svg:not(.red-ui-workspace-lasso-active) { fill: var(--red-ui-node-port-background); cursor: crosshair; } +.red-ui-flow-port-connected .red-ui-flow-port { + // TODO: what should a connected port look like? + fill: var(--red-ui-node-port-background); +} .red-ui-flow-node-error { fill: var(--red-ui-node-status-error-background);