From 58c94b7773a6df2447cda03bbd835c3dbd1c260f Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Tue, 11 Jan 2022 00:16:24 +0000 Subject: [PATCH 1/3] Add core:detach-selected-nodes action --- .../@node-red/editor-client/src/js/history.js | 7 + .../@node-red/editor-client/src/js/nodes.js | 3 + .../editor-client/src/js/ui/state.js | 3 +- .../@node-red/editor-client/src/js/ui/view.js | 137 ++++++++++++++++-- 4 files changed, 140 insertions(+), 10 deletions(-) diff --git a/packages/node_modules/@node-red/editor-client/src/js/history.js b/packages/node_modules/@node-red/editor-client/src/js/history.js index 256500200..a8ffe581b 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/history.js +++ b/packages/node_modules/@node-red/editor-client/src/js/history.js @@ -684,6 +684,13 @@ RED.history = (function() { peek: function() { return undoHistory[undoHistory.length-1]; }, + replace: function(ev) { + if (undoHistory.length === 0) { + RED.history.push(ev); + } else { + undoHistory[undoHistory.length-1] = ev; + } + }, clear: function() { undoHistory = []; redoHistory = []; 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 1904494f1..8feb741ac 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 @@ -2552,6 +2552,9 @@ RED.nodes = (function() { addLink: addLink, removeLink: removeLink, getNodeLinks: function(id, portType) { + if (typeof id !== 'string') { + id = id.id; + } if (nodeLinks[id]) { if (portType === 1) { // Return cloned arrays so they can be safely modified by caller diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/state.js b/packages/node_modules/@node-red/editor-client/src/js/ui/state.js index a2c9b762d..bb455d235 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/state.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/state.js @@ -27,5 +27,6 @@ RED.state = { PANNING: 10, SELECTING_NODE: 11, GROUP_DRAGGING: 12, - GROUP_RESIZE: 13 + GROUP_RESIZE: 13, + DETACHED_DRAGGING: 14 } 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 653dea0a4..582a7bca1 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 @@ -538,6 +538,8 @@ RED.view = (function() { RED.actions.add("core:cut-selection-to-internal-clipboard",function(){copySelection();deleteSelection();}); RED.actions.add("core:paste-from-internal-clipboard",function(){importNodes(clipboard,{generateIds: true});}); + RED.actions.add("core:detach-selected-nodes", function() { detachSelectedNodes() }) + RED.events.on("view:selection-changed", function(selection) { var hasSelection = (selection.nodes && selection.nodes.length > 0); var hasMultipleSelection = hasSelection && selection.nodes.length > 1; @@ -1384,7 +1386,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 && mouse_mode != RED.state.DETACHED_DRAGGING && !mousedown_node && !mousedown_group && selectedLinks.length() === 0) { return; } @@ -1489,7 +1491,7 @@ RED.view = (function() { RED.nodes.filterLinks({ target: node.n }).length === 0; } } - } else if (mouse_mode == RED.state.MOVING_ACTIVE || mouse_mode == RED.state.IMPORT_DRAGGING) { + } else if (mouse_mode == RED.state.MOVING_ACTIVE || mouse_mode == RED.state.IMPORT_DRAGGING || mouse_mode == RED.state.DETACHED_DRAGGING) { mousePos = mouse_position; var minX = 0; var minY = 0; @@ -1845,10 +1847,30 @@ RED.view = (function() { // movingSet.add(mousedown_node); // } // } - if (mouse_mode == RED.state.MOVING || mouse_mode == RED.state.MOVING_ACTIVE) { + if (mouse_mode == RED.state.MOVING || mouse_mode == RED.state.MOVING_ACTIVE || mouse_mode == RED.state.DETACHED_DRAGGING) { // if (mousedown_node) { // delete mousedown_node.gSelected; // } + if (mouse_mode === RED.state.DETACHED_DRAGGING) { + var ns = []; + for (var j=0;j { + // var inboundLinks = RED.nodes.getNodeLinks(node, PORT_TYPE_INPUT); + // var outboundLinks = RED.nodes.getNodeLinks(node, PORT_TYPE_OUTPUT); + // inboundLinks.forEach(l => { + // if (!selectedNodes.has(l.source)) { + // allInboundLinks.push(l); + // } + // }) + // outboundLinks.forEach(l => { + // if (!selectedNodes.has(l.target)) { + // allOutboundLinks.push(l); + // } + // }) + // }) + + + var node = selection.nodes[0]; + // 1. find all the links attached to this node + var inboundLinks = RED.nodes.getNodeLinks(node, PORT_TYPE_INPUT); + var outboundLinks = RED.nodes.getNodeLinks(node, PORT_TYPE_OUTPUT); + + + // 2. delete all those links + inboundLinks.forEach(l => RED.nodes.removeLink(l)) + outboundLinks.forEach(l => RED.nodes.removeLink(l)) + + // 3. restore links from all source nodes to all target nodes + + var newLinks = []; + var createdLinkIds = new Set(); + + inboundLinks.forEach(inLink => { + outboundLinks.forEach(outLink => { + var linkId = inLink.source.id+":"+inLink.sourcePort+":"+outLink.target.id + if (!createdLinkIds.has(linkId)) { + createdLinkIds.add(linkId); + var link = { + source: inLink.source, + sourcePort: inLink.sourcePort, + target: outLink.target + }; + var existingLinks = RED.nodes.filterLinks(link) + if (existingLinks.length === 0) { + newLinks.push(link); + RED.nodes.addLink(link); + } + } + }) + }) + + var oldLinks = inboundLinks.concat(outboundLinks); + + if (oldLinks.length) { + RED.history.push({ + t: "multi", + events: [ + { t:'delete', links: oldLinks }, + { t:'add', links: newLinks } + ], + dirty: RED.nodes.dirty() + }) + RED.nodes.dirty(true) + } + + + prepareDrag([node.x,node.y]); + mouse_mode = RED.state.DETACHED_DRAGGING; + RED.view.redraw(true); + } + } + function calculateTextWidth(str, className) { var result = convertLineBreakCharacter(str); var width = 0; @@ -2956,10 +3060,13 @@ RED.view = (function() { msn.dx = msn.n.x-mouse[0]; msn.dy = msn.n.y-mouse[1]; } - - mouse_offset = d3.mouse(document.body); - if (isNaN(mouse_offset[0])) { - mouse_offset = d3.touches(document.body)[0]; + try { + mouse_offset = d3.mouse(document.body); + if (isNaN(mouse_offset[0])) { + mouse_offset = d3.touches(document.body)[0]; + } + } catch(err) { + mouse_offset = [0,0] } } @@ -3040,7 +3147,7 @@ RED.view = (function() { //var touch0 = d3.event; //var pos = [touch0.pageX,touch0.pageY]; //RED.touch.radialMenu.show(d3.select(this),pos); - if (mouse_mode == RED.state.IMPORT_DRAGGING) { + if (mouse_mode == RED.state.IMPORT_DRAGGING || mouse_mode == RED.state.DETACHED_DRAGGING) { var historyEvent = RED.history.peek(); if (activeSpliceLink) { // TODO: DRY - droppable/nodeMouseDown/canvasMouseUp @@ -3078,6 +3185,18 @@ RED.view = (function() { activeHoverGroup = null; } + if (mouse_mode == RED.state.DETACHED_DRAGGING) { + var ns = []; + for (var j=0;j Date: Tue, 11 Jan 2022 00:24:34 +0000 Subject: [PATCH 2/3] Pressing escape whilst DETACH_DRAGGING should revert change --- .../node_modules/@node-red/editor-client/src/js/ui/view.js | 4 ++++ 1 file changed, 4 insertions(+) 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 582a7bca1..22197b330 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 @@ -1916,7 +1916,11 @@ RED.view = (function() { return; } if (mouse_mode === RED.state.DETACHED_DRAGGING) { + var node = movingSet.get(0); + node.n.x = node.ox; + node.n.y = node.oy; clearSelection(); + RED.history.pop(); mouse_mode = 0; } else if (mouse_mode === RED.state.IMPORT_DRAGGING) { clearSelection(); From 154a4e23ddeb0827c994843cb498a659f5cac556 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Tue, 11 Jan 2022 14:11:55 +0000 Subject: [PATCH 3/3] Add delete-selection-and-reconnect action --- .../editor-client/src/js/keymap.json | 2 + .../@node-red/editor-client/src/js/nodes.js | 144 +++++++++++++++++- .../editor-client/src/js/ui/group.js | 8 +- .../@node-red/editor-client/src/js/ui/view.js | 136 +++++------------ 4 files changed, 191 insertions(+), 99 deletions(-) diff --git a/packages/node_modules/@node-red/editor-client/src/js/keymap.json b/packages/node_modules/@node-red/editor-client/src/js/keymap.json index 766f6bd9f..f1f3f46e7 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/keymap.json +++ b/packages/node_modules/@node-red/editor-client/src/js/keymap.json @@ -38,7 +38,9 @@ }, "red-ui-workspace": { "backspace": "core:delete-selection", + "ctrl-backspace": "core:delete-selection-and-reconnect", "delete": "core:delete-selection", + "ctrl-delete": "core:delete-selection-and-reconnect", "enter": "core:edit-selected-node", "ctrl-enter": "core:go-to-selection", "ctrl-c": "core:copy-selection-to-internal-clipboard", 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 8feb741ac..78b9deae6 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 @@ -15,6 +15,9 @@ **/ RED.nodes = (function() { + var PORT_TYPE_INPUT = 1; + var PORT_TYPE_OUTPUT = 0; + var node_defs = {}; var linkTabMap = {}; @@ -2458,6 +2461,144 @@ RED.nodes = (function() { return helpContent; } + function getNodeIslands(nodes) { + var selectedNodes = new Set(nodes); + // Maps node => island index + var nodeToIslandIndex = new Map(); + // Maps island index => [nodes in island] + var islandIndexToNodes = new Map(); + var internalLinks = new Set(); + nodes.forEach((node, index) => { + nodeToIslandIndex.set(node,index); + islandIndexToNodes.set(index, [node]); + var inboundLinks = RED.nodes.getNodeLinks(node, PORT_TYPE_INPUT); + var outboundLinks = RED.nodes.getNodeLinks(node, PORT_TYPE_OUTPUT); + inboundLinks.forEach(l => { + if (selectedNodes.has(l.source)) { + internalLinks.add(l) + } + }) + outboundLinks.forEach(l => { + if (selectedNodes.has(l.target)) { + internalLinks.add(l) + } + }) + }) + + internalLinks.forEach(l => { + let source = l.source; + let target = l.target; + if (nodeToIslandIndex.get(source) !== nodeToIslandIndex.get(target)) { + let sourceIsland = nodeToIslandIndex.get(source); + let islandToMove = nodeToIslandIndex.get(target); + let nodesToMove = islandIndexToNodes.get(islandToMove); + nodesToMove.forEach(n => { + nodeToIslandIndex.set(n,sourceIsland); + islandIndexToNodes.get(sourceIsland).push(n); + }) + islandIndexToNodes.delete(islandToMove); + } + }) + const result = []; + islandIndexToNodes.forEach((nodes,index) => { + result.push(nodes); + }) + return result; + } + + function detachNodes(nodes) { + let allSelectedNodes = []; + nodes.forEach(node => { + if (node.type === 'group') { + let groupNodes = RED.group.getNodes(node,true,true); + allSelectedNodes = allSelectedNodes.concat(groupNodes); + } else { + allSelectedNodes.push(node); + } + }) + if (allSelectedNodes.length > 0 ) { + const nodeIslands = RED.nodes.getNodeIslands(allSelectedNodes); + let removedLinks = []; + let newLinks = []; + let createdLinkIds = new Set(); + + nodeIslands.forEach(nodes => { + let selectedNodes = new Set(nodes); + let allInboundLinks = []; + let allOutboundLinks = []; + // Identify links that enter or exit this island of nodes + nodes.forEach(node => { + var inboundLinks = RED.nodes.getNodeLinks(node, PORT_TYPE_INPUT); + var outboundLinks = RED.nodes.getNodeLinks(node, PORT_TYPE_OUTPUT); + inboundLinks.forEach(l => { + if (!selectedNodes.has(l.source)) { + allInboundLinks.push(l) + } + }) + outboundLinks.forEach(l => { + if (!selectedNodes.has(l.target)) { + allOutboundLinks.push(l) + } + }) + }); + + + // Identify the links to restore + allInboundLinks.forEach(inLink => { + // For Each inbound link, + // - get source node. + // - trace through to all outbound links + let sourceNode = inLink.source; + let targetNodes = new Set(); + let visited = new Set(); + let stack = [inLink.target]; + while (stack.length > 0) { + let node = stack.pop(stack); + visited.add(node) + let links = RED.nodes.getNodeLinks(node, PORT_TYPE_OUTPUT); + links.forEach(l => { + if (visited.has(l.target)) { + return + } + visited.add(l.target); + if (selectedNodes.has(l.target)) { + // internal link + stack.push(l.target) + } else { + targetNodes.add(l.target) + } + }) + } + targetNodes.forEach(target => { + let linkId = `${sourceNode.id}[${inLink.sourcePort}] -> ${target.id}` + if (!createdLinkIds.has(linkId)) { + createdLinkIds.add(linkId); + let link = { + source: sourceNode, + sourcePort: inLink.sourcePort, + target: target + } + let existingLinks = RED.nodes.filterLinks(link) + if (existingLinks.length === 0) { + newLinks.push(link); + } + } + }) + }) + + // 2. delete all those links + allInboundLinks.forEach(l => { RED.nodes.removeLink(l); removedLinks.push(l)}) + allOutboundLinks.forEach(l => { RED.nodes.removeLink(l); removedLinks.push(l)}) + }) + + newLinks.forEach(l => RED.nodes.addLink(l)); + return { + newLinks, + removedLinks + } + } + } + return { init: function() { RED.events.on("registry:node-type-added",function(type) { @@ -2539,7 +2680,7 @@ RED.nodes = (function() { add: addNode, remove: removeNode, clear: clear, - + detachNodes: detachNodes, moveNodesForwards: moveNodesForwards, moveNodesBackwards: moveNodesBackwards, moveNodesToFront: moveNodesToFront, @@ -2638,6 +2779,7 @@ RED.nodes = (function() { getAllFlowNodes: getAllFlowNodes, getAllUpstreamNodes: getAllUpstreamNodes, getAllDownstreamNodes: getAllDownstreamNodes, + getNodeIslands: getNodeIslands, createExportableNodeSet: createExportableNodeSet, createCompleteNodeSet: createCompleteNodeSet, updateConfigNodeUsers: updateConfigNodeUsers, diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/group.js b/packages/node_modules/@node-red/editor-client/src/js/ui/group.js index a6e2492fd..15c48cc81 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/group.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/group.js @@ -590,12 +590,14 @@ RED.group = (function() { markDirty(group); } - function getNodes(group,recursive) { + function getNodes(group,recursive,excludeGroup) { var nodes = []; group.nodes.forEach(function(n) { - nodes.push(n); + if (n.type !== 'group' || !excludeGroup) { + nodes.push(n); + } if (recursive && n.type === 'group') { - nodes = nodes.concat(getNodes(n,recursive)) + nodes = nodes.concat(getNodes(n,recursive,excludeGroup)) } }) return nodes; 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 22197b330..c64e9af84 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 @@ -562,6 +562,7 @@ RED.view = (function() { }) RED.actions.add("core:delete-selection",deleteSelection); + RED.actions.add("core:delete-selection-and-reconnect",function() { deleteSelection(true) }); RED.actions.add("core:edit-selected-node",editSelection); RED.actions.add("core:go-to-selection",function() { if (movingSet.length() > 0) { @@ -1916,9 +1917,11 @@ RED.view = (function() { return; } if (mouse_mode === RED.state.DETACHED_DRAGGING) { - var node = movingSet.get(0); - node.n.x = node.ox; - node.n.y = node.oy; + for (var j=0;j 0) { + historyEvents.push({ t:'add', links: addedLinks }) + } + removedLinks = removedLinks.concat(reconnectResult.removedLinks) + } var startDirty = RED.nodes.dirty(); var startChanged = false; @@ -2310,7 +2323,6 @@ RED.view = (function() { } } - var linkRemoveHistoryEvents = []; if (selectedLinks.length() > 0) { selectedLinks.forEach(function(link) { if (link.link) { @@ -2318,31 +2330,22 @@ RED.view = (function() { var targetId = link.target.id; var sourceIdIndex = link.target.links.indexOf(sourceId); var targetIdIndex = link.source.links.indexOf(targetId); - - 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:startDirty + historyEvents.push({ + t: "edit", + node: link.source, + changed: link.source.changed, + changes: { + links: $.extend(true,{},{v:link.source.links}).v + } + }) + historyEvents.push({ + t: "edit", + node: link.target, + changed: link.target.changed, + changes: { + links: $.extend(true,{},{v:link.target.links}).v + } }) - link.source.changed = true; link.target.changed = true; link.target.links.splice(sourceIdIndex,1); @@ -2373,11 +2376,11 @@ RED.view = (function() { if (removedSubflowStatus) { historyEvent.subflow.status = removedSubflowStatus; } - if (linkRemoveHistoryEvents.length > 0) { - linkRemoveHistoryEvents.unshift(historyEvent); + if (historyEvents.length > 0) { + historyEvents.unshift(historyEvent); RED.history.push({ t:"multi", - events: linkRemoveHistoryEvents + events: historyEvents }) } else { RED.history.push(historyEvent); @@ -2460,80 +2463,23 @@ RED.view = (function() { } } + function detachSelectedNodes() { var selection = RED.view.selection(); - if (selection.nodes && selection.nodes.length === 1) { - // var selectedNodes = new Set(selection.nodes); - // - // var allInboundLinks = []; - // var allOutboundLinks = []; - // - // selection.nodes.forEach(node => { - // var inboundLinks = RED.nodes.getNodeLinks(node, PORT_TYPE_INPUT); - // var outboundLinks = RED.nodes.getNodeLinks(node, PORT_TYPE_OUTPUT); - // inboundLinks.forEach(l => { - // if (!selectedNodes.has(l.source)) { - // allInboundLinks.push(l); - // } - // }) - // outboundLinks.forEach(l => { - // if (!selectedNodes.has(l.target)) { - // allOutboundLinks.push(l); - // } - // }) - // }) - - - var node = selection.nodes[0]; - // 1. find all the links attached to this node - var inboundLinks = RED.nodes.getNodeLinks(node, PORT_TYPE_INPUT); - var outboundLinks = RED.nodes.getNodeLinks(node, PORT_TYPE_OUTPUT); - - - // 2. delete all those links - inboundLinks.forEach(l => RED.nodes.removeLink(l)) - outboundLinks.forEach(l => RED.nodes.removeLink(l)) - - // 3. restore links from all source nodes to all target nodes - - var newLinks = []; - var createdLinkIds = new Set(); - - inboundLinks.forEach(inLink => { - outboundLinks.forEach(outLink => { - var linkId = inLink.source.id+":"+inLink.sourcePort+":"+outLink.target.id - if (!createdLinkIds.has(linkId)) { - createdLinkIds.add(linkId); - var link = { - source: inLink.source, - sourcePort: inLink.sourcePort, - target: outLink.target - }; - var existingLinks = RED.nodes.filterLinks(link) - if (existingLinks.length === 0) { - newLinks.push(link); - RED.nodes.addLink(link); - } - } - }) - }) - - var oldLinks = inboundLinks.concat(outboundLinks); - - if (oldLinks.length) { + if (selection.nodes) { + const {newLinks, removedLinks} = RED.nodes.detachNodes(selection.nodes); + if (removedLinks.length || newLinks.length) { RED.history.push({ t: "multi", events: [ - { t:'delete', links: oldLinks }, + { t:'delete', links: removedLinks }, { t:'add', links: newLinks } ], dirty: RED.nodes.dirty() }) RED.nodes.dirty(true) } - - - prepareDrag([node.x,node.y]); + prepareDrag([selection.nodes[0].x,selection.nodes[0].y]); mouse_mode = RED.state.DETACHED_DRAGGING; RED.view.redraw(true); }