From 5ea3329b36b4cb40ccc6ef2459e26a7d38d0c892 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Tue, 7 Dec 2021 09:19:27 +0000 Subject: [PATCH 1/3] Allow multiple links to be selected by ctrl-click --- .../@node-red/editor-client/src/js/ui/view.js | 249 ++++++++++-------- 1 file changed, 145 insertions(+), 104 deletions(-) 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 166cb2901..227a217aa 100755 --- 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 @@ -63,7 +63,8 @@ RED.view = (function() { var activeGroups = []; var dirtyGroups = {}; - var selected_link = null; + var selectedLinks = []; + var mousedown_link = null; var mousedown_node = null; var mousedown_group = null; @@ -909,7 +910,7 @@ RED.view = (function() { return; } if (!mousedown_node && !mousedown_link && !mousedown_group) { - selected_link = null; + selectedLinks = []; updateSelection(); } if (mouse_mode === 0) { @@ -1348,7 +1349,7 @@ RED.view = (function() { return; } - if (mouse_mode != RED.state.QUICK_JOINING && mouse_mode != RED.state.IMPORT_DRAGGING && !mousedown_node && !mousedown_group && selected_link == null) { + if (mouse_mode != RED.state.QUICK_JOINING && mouse_mode != RED.state.IMPORT_DRAGGING && !mousedown_node && !mousedown_group && selectedLinks.length === 0) { return; } @@ -1372,16 +1373,18 @@ RED.view = (function() { // Get all the wires we need to detach. var links = []; var existingLinks = []; - if (selected_link && - ((mousedown_port_type === PORT_TYPE_OUTPUT && - selected_link.source === mousedown_node && - selected_link.sourcePort === mousedown_port_index - ) || - (mousedown_port_type === PORT_TYPE_INPUT && - selected_link.target === mousedown_node - )) - ) { - existingLinks = [selected_link]; + if (selectedLinks.length > 0) { + selectedLinks.forEach(function(link) { + if (((mousedown_port_type === PORT_TYPE_OUTPUT && + link.source === mousedown_node && + link.sourcePort === mousedown_port_index + ) || + (mousedown_port_type === PORT_TYPE_INPUT && + link.target === mousedown_node + ))) { + existingLinks.push(link); + } + }) } else { var filter; if (mousedown_port_type === PORT_TYPE_OUTPUT) { @@ -1419,7 +1422,7 @@ RED.view = (function() { } else if (mousedown_node && !quickAddLink) { showDragLines([{node:mousedown_node,port:mousedown_port_index,portType:mousedown_port_type}]); } - selected_link = null; + selectedLinks = []; } mousePos = mouse_position; for (i=0;i 0) { + selectedLinks.forEach(function(link) { + if (link.link) { + activeLinks.push(link); + activeLinkNodes[link.source.id] = link.source; + link.source.dirty = true; + activeLinkNodes[link.target.id] = link.target; + link.target.dirty = true; + } + }) } } else { selection.flows = workspaceSelection; @@ -2066,6 +2073,10 @@ RED.view = (function() { return value.map(function(n) { return n.id }) } else if (key === 'link') { return value.source.id+":"+value.sourcePort+":"+value.target.id; + } else if (key === 'links') { + return value.map(function(link) { + link.source.id+":"+link.sourcePort+":"+link.target.id; + }); } return value; }); @@ -2135,7 +2146,7 @@ RED.view = (function() { updateActiveNodes(); updateSelection(); redraw(); - } else if (movingSet.length() > 0 || selected_link != null) { + } else if (movingSet.length() > 0 || selectedLinks.length > 0) { var result; var node; var removedNodes = []; @@ -2234,71 +2245,81 @@ RED.view = (function() { RED.nodes.dirty(true); } } - var historyEvent; - if (selected_link && selected_link.link) { - var sourceId = selected_link.source.id; - var targetId = selected_link.target.id; - var sourceIdIndex = selected_link.target.links.indexOf(sourceId); - var targetIdIndex = selected_link.source.links.indexOf(targetId); + var linkRemoveHistoryEvents = []; + if (selectedLinks.length > 0) { + selectedLinks.forEach(function(link) { + if (link.link) { + var sourceId = link.source.id; + var targetId = link.target.id; + var sourceIdIndex = link.target.links.indexOf(sourceId); + var targetIdIndex = link.source.links.indexOf(targetId); - historyEvent = { - t:"multi", - events: [ - { - t: "edit", - node: selected_link.source, - changed: selected_link.source.changed, - changes: { - links: $.extend(true,{},{v:selected_link.source.links}).v - } - }, - { - t: "edit", - node: selected_link.target, - changed: selected_link.target.changed, - changes: { - links: $.extend(true,{},{v:selected_link.target.links}).v - } - } + linkRemoveHistoryEvents.push({ + t:"multi", + events: [ + { + t: "edit", + node: link.source, + changed: link.source.changed, + changes: { + links: $.extend(true,{},{v:link.source.links}).v + } + }, + { + t: "edit", + node: link.target, + changed: link.target.changed, + changes: { + links: $.extend(true,{},{v:link.target.links}).v + } + } - ], - dirty:RED.nodes.dirty() - } - RED.nodes.dirty(true); - selected_link.source.changed = true; - selected_link.target.changed = true; - selected_link.target.links.splice(sourceIdIndex,1); - selected_link.source.links.splice(targetIdIndex,1); - selected_link.source.dirty = true; - selected_link.target.dirty = true; + ], + dirty:startDirty + }) - } else { - if (selected_link) { - RED.nodes.removeLink(selected_link); - removedLinks.push(selected_link); - } - RED.nodes.dirty(true); - historyEvent = { - t:"delete", - nodes:removedNodes, - links:removedLinks, - groups: removedGroups, - subflowOutputs:removedSubflowOutputs, - subflowInputs:removedSubflowInputs, - subflow: { - id: activeSubflow?activeSubflow.id:undefined, - instances: subflowInstances - }, - dirty:startDirty - }; - if (removedSubflowStatus) { - historyEvent.subflow.status = removedSubflowStatus; - } + link.source.changed = true; + link.target.changed = true; + link.target.links.splice(sourceIdIndex,1); + link.source.links.splice(targetIdIndex,1); + link.source.dirty = true; + link.target.dirty = true; + + } else { + RED.nodes.removeLink(link); + removedLinks.push(link); + } + }) + } + RED.nodes.dirty(true); + var historyEvent = { + t:"delete", + nodes:removedNodes, + links:removedLinks, + groups: removedGroups, + subflowOutputs:removedSubflowOutputs, + subflowInputs:removedSubflowInputs, + subflow: { + id: activeSubflow?activeSubflow.id:undefined, + instances: subflowInstances + }, + dirty:startDirty + }; + if (removedSubflowStatus) { + historyEvent.subflow.status = removedSubflowStatus; + } + if (linkRemoveHistoryEvents.length > 0) { + linkRemoveHistoryEvents.unshift(historyEvent); + RED.history.push({ + t:"multi", + events: linkRemoveHistoryEvents + }) + } else { + RED.history.push(historyEvent); } - RED.history.push(historyEvent); - selected_link = null; + selectedLinks = []; updateActiveNodes(); updateSelection(); redraw(); @@ -2709,7 +2730,9 @@ RED.view = (function() { } else { resetMouseVars(); } - selected_link = select_link; + //TODO: selectedLinks + console.log("Wanted to set selected_link (1)",select_link) + // selected_link = select_link; mousedown_link = select_link; if (select_link) { updateSelection(); @@ -2721,7 +2744,9 @@ RED.view = (function() { resetMouseVars(); hideDragLines(); - selected_link = select_link; + if (select_link) { + selectedLinks = [select_link]; + } mousedown_link = select_link; if (select_link) { updateSelection(); @@ -3212,7 +3237,7 @@ RED.view = (function() { mousedown_node.selected = true; movingSet.add(mousedown_node); } - selected_link = null; + selectedLinks = []; if (d3.event.button != 2) { mouse_mode = RED.state.MOVING; var mouse = d3.touches(this)[0]||d3.mouse(this); @@ -3338,19 +3363,33 @@ RED.view = (function() { d3.event.stopPropagation(); return; } - mousedown_link = d; - clearSelection(); - selected_link = mousedown_link; - updateSelection(); - redraw(); - focusView(); - d3.event.stopPropagation(); - if (d3.event.metaKey || d3.event.ctrlKey) { - d3.select(this).classed("red-ui-flow-link-splice",true); - var point = d3.mouse(this); - var clickedGroup = getGroupAt(point[0],point[1]); - showQuickAddDialog({position:point, splice:selected_link, group:clickedGroup}); - } + mousedown_link = d; + + if (selectedLinks.length === 0 || !(d3.event.metaKey || d3.event.ctrlKey)) { + clearSelection(); + } + if (d3.event.metaKey || d3.event.ctrlKey) { + var linkIndex = selectedLinks.indexOf(mousedown_link); + if (linkIndex === -1) { + selectedLinks.push(mousedown_link); + } else { + if (selectedLinks.length !== 1) { + selectedLinks.splice(linkIndex,1); + } + } + } else { + selectedLinks.push(mousedown_link); + } + updateSelection(); + redraw(); + focusView(); + d3.event.stopPropagation(); + if (selectedLinks.length === 1 && selectedLinks[0] === mousedown_link && (d3.event.metaKey || d3.event.ctrlKey)) { + d3.select(this).classed("red-ui-flow-link-splice",true); + var point = d3.mouse(this); + var clickedGroup = getGroupAt(point[0],point[1]); + showQuickAddDialog({position:point, splice:mousedown_link, group:clickedGroup}); + } } function linkTouchStart(d) { if (mouse_mode === RED.state.SELECTING_NODE) { @@ -3359,7 +3398,7 @@ RED.view = (function() { } mousedown_link = d; clearSelection(); - selected_link = mousedown_link; + selectedLinks = [mousedown_link]; updateSelection(); redraw(); focusView(); @@ -3595,7 +3634,7 @@ RED.view = (function() { function showTouchMenu(obj,pos) { var mdn = mousedown_node; var options = []; - options.push({name:"delete",disabled:(movingSet.length()===0 && selected_link === null),onselect:function() {deleteSelection();}}); + options.push({name:"delete",disabled:(movingSet.length()===0 && selectedLinks.length === 0),onselect:function() {deleteSelection();}}); options.push({name:"cut",disabled:(movingSet.length()===0),onselect:function() {copySelection();deleteSelection();}}); options.push({name:"copy",disabled:(movingSet.length()===0),onselect:function() {copySelection();}}); options.push({name:"paste",disabled:(clipboard.length===0),onselect:function() {importNodes(clipboard, {generateIds: true, touchImport: true});}}); @@ -4352,7 +4391,7 @@ RED.view = (function() { link.exit().remove(); link.each(function(d) { var link = d3.select(this); - if (d.added || d===selected_link || d.selected || dirtyNodes[d.source.id] || dirtyNodes[d.target.id]) { + if (d.added || selectedLinks.includes(d) || d.selected || dirtyNodes[d.source.id] || dirtyNodes[d.target.id]) { var numOutputs = d.source.outputs || 1; var sourcePort = d.sourcePort || 0; var y = -((numOutputs-1)/2)*13 +13*sourcePort; @@ -4375,7 +4414,7 @@ RED.view = (function() { this.__pathLine__.classList.toggle("red-ui-flow-node-disabled",!!(d.source.d || d.target.d)); this.__pathLine__.classList.toggle("red-ui-flow-subflow-link", !d.link && activeSubflow); } - this.classList.toggle("red-ui-flow-link-selected", !!(d===selected_link||d.selected)); + this.classList.toggle("red-ui-flow-link-selected", !!(selectedLinks.includes(d)||d.selected)); var connectedToUnknown = !!(d.target.type == "unknown" || d.source.type == "unknown"); this.classList.toggle("red-ui-flow-link-unknown",!!(d.target.type == "unknown" || d.source.type == "unknown")) @@ -5083,8 +5122,10 @@ RED.view = (function() { if (allNodes.size > 0) { selection.nodes = Array.from(allNodes); } - if (selected_link != null) { - selection.link = selected_link; + if (selectedLinks.length > 0) { + selection.link = selectedLinks[0]; + // TODO: clone the array + selection.links = selectedLinks; } return selection; } From ecaf866613665c58e55bf396938831da10581ea6 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Tue, 7 Dec 2021 09:29:11 +0000 Subject: [PATCH 2/3] Fix serialisation of selection in view --- packages/node_modules/@node-red/editor-client/src/js/ui/view.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 227a217aa..10347b76a 100755 --- 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 @@ -2075,7 +2075,7 @@ RED.view = (function() { return value.source.id+":"+value.sourcePort+":"+value.target.id; } else if (key === 'links') { return value.map(function(link) { - link.source.id+":"+link.sourcePort+":"+link.target.id; + return link.source.id+":"+link.sourcePort+":"+link.target.id; }); } return value; From 43651135f3d891c287f43bd07cfdedc2d13af74c Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Mon, 13 Dec 2021 21:02:55 +0000 Subject: [PATCH 3/3] Replace selectedLinks array with wrapped Set object --- .../@node-red/editor-client/src/js/ui/view.js | 91 ++++++++++++------- 1 file changed, 58 insertions(+), 33 deletions(-) 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 10347b76a..776bb0962 100755 --- 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 @@ -63,8 +63,6 @@ RED.view = (function() { var activeGroups = []; var dirtyGroups = {}; - var selectedLinks = []; - var mousedown_link = null; var mousedown_node = null; var mousedown_group = null; @@ -160,6 +158,31 @@ RED.view = (function() { return api; })(); + var selectedLinks = (function() { + var links = new Set(); + return { + add: function(link) { + links.add(link); + link.selected = true; + }, + remove: function(link) { + links.delete(link); + link.selected = false; + }, + clear: function() { + links.forEach(function(link) { link.selected = false }) + links.clear(); + }, + length: function() { + return links.size; + }, + forEach: function(func) { links.forEach(func) }, + has: function(link) { return links.has(link) }, + toArray: function() { return Array.from(links) } + } + })(); + + function init() { chart = $("#red-ui-workspace-chart"); @@ -910,7 +933,7 @@ RED.view = (function() { return; } if (!mousedown_node && !mousedown_link && !mousedown_group) { - selectedLinks = []; + selectedLinks.clear(); updateSelection(); } if (mouse_mode === 0) { @@ -1349,7 +1372,7 @@ RED.view = (function() { return; } - if (mouse_mode != RED.state.QUICK_JOINING && mouse_mode != RED.state.IMPORT_DRAGGING && !mousedown_node && !mousedown_group && selectedLinks.length === 0) { + if (mouse_mode != RED.state.QUICK_JOINING && mouse_mode != RED.state.IMPORT_DRAGGING && !mousedown_node && !mousedown_group && selectedLinks.length() === 0) { return; } @@ -1373,7 +1396,7 @@ RED.view = (function() { // Get all the wires we need to detach. var links = []; var existingLinks = []; - if (selectedLinks.length > 0) { + if (selectedLinks.length() > 0) { selectedLinks.forEach(function(link) { if (((mousedown_port_type === PORT_TYPE_OUTPUT && link.source === mousedown_node && @@ -1422,7 +1445,7 @@ RED.view = (function() { } else if (mousedown_node && !quickAddLink) { showDragLines([{node:mousedown_node,port:mousedown_port_index,portType:mousedown_port_type}]); } - selectedLinks = []; + selectedLinks.clear(); } mousePos = mouse_position; for (i=0;i 0) { + if (activeFlowLinks.length === 0 && selectedLinks.length() > 0) { selectedLinks.forEach(function(link) { if (link.link) { activeLinks.push(link); @@ -2146,7 +2169,7 @@ RED.view = (function() { updateActiveNodes(); updateSelection(); redraw(); - } else if (movingSet.length() > 0 || selectedLinks.length > 0) { + } else if (movingSet.length() > 0 || selectedLinks.length() > 0) { var result; var node; var removedNodes = []; @@ -2247,7 +2270,7 @@ RED.view = (function() { } var linkRemoveHistoryEvents = []; - if (selectedLinks.length > 0) { + if (selectedLinks.length() > 0) { selectedLinks.forEach(function(link) { if (link.link) { var sourceId = link.source.id; @@ -2319,7 +2342,7 @@ RED.view = (function() { RED.history.push(historyEvent); } - selectedLinks = []; + selectedLinks.clear(); updateActiveNodes(); updateSelection(); redraw(); @@ -2730,12 +2753,13 @@ RED.view = (function() { } else { resetMouseVars(); } - //TODO: selectedLinks - console.log("Wanted to set selected_link (1)",select_link) - // selected_link = select_link; mousedown_link = select_link; if (select_link) { + selectedLinks.clear(); + selectedLinks.add(select_link); updateSelection(); + } else { + selectedLinks.clear(); } } redraw(); @@ -2745,7 +2769,8 @@ RED.view = (function() { resetMouseVars(); hideDragLines(); if (select_link) { - selectedLinks = [select_link]; + selectedLinks.clear(); + selectedLinks.add(select_link); } mousedown_link = select_link; if (select_link) { @@ -3237,7 +3262,7 @@ RED.view = (function() { mousedown_node.selected = true; movingSet.add(mousedown_node); } - selectedLinks = []; + selectedLinks.clear(); if (d3.event.button != 2) { mouse_mode = RED.state.MOVING; var mouse = d3.touches(this)[0]||d3.mouse(this); @@ -3365,26 +3390,25 @@ RED.view = (function() { } mousedown_link = d; - if (selectedLinks.length === 0 || !(d3.event.metaKey || d3.event.ctrlKey)) { + if (selectedLinks.length() === 0 || !(d3.event.metaKey || d3.event.ctrlKey)) { clearSelection(); } if (d3.event.metaKey || d3.event.ctrlKey) { - var linkIndex = selectedLinks.indexOf(mousedown_link); - if (linkIndex === -1) { - selectedLinks.push(mousedown_link); + if (!selectedLinks.has(mousedown_link)) { + selectedLinks.add(mousedown_link); } else { - if (selectedLinks.length !== 1) { - selectedLinks.splice(linkIndex,1); + if (selectedLinks.length() !== 1) { + selectedLinks.remove(mousedown_link); } } } else { - selectedLinks.push(mousedown_link); + selectedLinks.add(mousedown_link); } updateSelection(); redraw(); focusView(); d3.event.stopPropagation(); - if (selectedLinks.length === 1 && selectedLinks[0] === mousedown_link && (d3.event.metaKey || d3.event.ctrlKey)) { + if (!mousedown_link.link && selectedLinks.length() === 1 && selectedLinks.has(mousedown_link) && (d3.event.metaKey || d3.event.ctrlKey)) { d3.select(this).classed("red-ui-flow-link-splice",true); var point = d3.mouse(this); var clickedGroup = getGroupAt(point[0],point[1]); @@ -3398,7 +3422,8 @@ RED.view = (function() { } mousedown_link = d; clearSelection(); - selectedLinks = [mousedown_link]; + selectedLinks.clear(); + selectedLinks.add(mousedown_link); updateSelection(); redraw(); focusView(); @@ -3634,7 +3659,7 @@ RED.view = (function() { function showTouchMenu(obj,pos) { var mdn = mousedown_node; var options = []; - options.push({name:"delete",disabled:(movingSet.length()===0 && selectedLinks.length === 0),onselect:function() {deleteSelection();}}); + options.push({name:"delete",disabled:(movingSet.length()===0 && selectedLinks.length() === 0),onselect:function() {deleteSelection();}}); options.push({name:"cut",disabled:(movingSet.length()===0),onselect:function() {copySelection();deleteSelection();}}); options.push({name:"copy",disabled:(movingSet.length()===0),onselect:function() {copySelection();}}); options.push({name:"paste",disabled:(clipboard.length===0),onselect:function() {importNodes(clipboard, {generateIds: true, touchImport: true});}}); @@ -4391,7 +4416,7 @@ RED.view = (function() { link.exit().remove(); link.each(function(d) { var link = d3.select(this); - if (d.added || selectedLinks.includes(d) || d.selected || dirtyNodes[d.source.id] || dirtyNodes[d.target.id]) { + if (d.added || d.selected || dirtyNodes[d.source.id] || dirtyNodes[d.target.id]) { var numOutputs = d.source.outputs || 1; var sourcePort = d.sourcePort || 0; var y = -((numOutputs-1)/2)*13 +13*sourcePort; @@ -4414,7 +4439,8 @@ RED.view = (function() { this.__pathLine__.classList.toggle("red-ui-flow-node-disabled",!!(d.source.d || d.target.d)); this.__pathLine__.classList.toggle("red-ui-flow-subflow-link", !d.link && activeSubflow); } - this.classList.toggle("red-ui-flow-link-selected", !!(selectedLinks.includes(d)||d.selected)); + + this.classList.toggle("red-ui-flow-link-selected", !!d.selected); var connectedToUnknown = !!(d.target.type == "unknown" || d.source.type == "unknown"); this.classList.toggle("red-ui-flow-link-unknown",!!(d.target.type == "unknown" || d.source.type == "unknown")) @@ -5122,10 +5148,9 @@ RED.view = (function() { if (allNodes.size > 0) { selection.nodes = Array.from(allNodes); } - if (selectedLinks.length > 0) { - selection.link = selectedLinks[0]; - // TODO: clone the array - selection.links = selectedLinks; + if (selectedLinks.length() > 0) { + selection.links = selectedLinks.toArray(); + selection.link = selection.links[0]; } return selection; }