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/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 1904494f1..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, @@ -2552,6 +2693,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 @@ -2635,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/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..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 @@ -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; @@ -560,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) { @@ -1384,7 +1387,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 +1492,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 +1848,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 0) { + historyEvents.push({ t:'add', links: addedLinks }) + } + removedLinks = removedLinks.concat(reconnectResult.removedLinks) + } var startDirty = RED.nodes.dirty(); var startChanged = false; @@ -2281,7 +2323,6 @@ RED.view = (function() { } } - var linkRemoveHistoryEvents = []; if (selectedLinks.length() > 0) { selectedLinks.forEach(function(link) { if (link.link) { @@ -2289,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); @@ -2344,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); @@ -2431,6 +2463,28 @@ RED.view = (function() { } } + + function detachSelectedNodes() { + var selection = RED.view.selection(); + 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: removedLinks }, + { t:'add', links: newLinks } + ], + dirty: RED.nodes.dirty() + }) + RED.nodes.dirty(true) + } + prepareDrag([selection.nodes[0].x,selection.nodes[0].y]); + mouse_mode = RED.state.DETACHED_DRAGGING; + RED.view.redraw(true); + } + } + function calculateTextWidth(str, className) { var result = convertLineBreakCharacter(str); var width = 0; @@ -2956,10 +3010,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 +3097,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 +3135,18 @@ RED.view = (function() { activeHoverGroup = null; } + if (mouse_mode == RED.state.DETACHED_DRAGGING) { + var ns = []; + for (var j=0;j