1
0
mirror of https://github.com/node-red/node-red.git synced 2023-10-10 13:36:53 +02:00

Merge pull request #3338 from node-red/detach-action

Detach action
This commit is contained in:
Nick O'Leary 2022-01-12 17:52:32 +00:00 committed by GitHub
commit 44ef9a13d6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 269 additions and 43 deletions

View File

@ -684,6 +684,13 @@ RED.history = (function() {
peek: function() { peek: function() {
return undoHistory[undoHistory.length-1]; return undoHistory[undoHistory.length-1];
}, },
replace: function(ev) {
if (undoHistory.length === 0) {
RED.history.push(ev);
} else {
undoHistory[undoHistory.length-1] = ev;
}
},
clear: function() { clear: function() {
undoHistory = []; undoHistory = [];
redoHistory = []; redoHistory = [];

View File

@ -38,7 +38,9 @@
}, },
"red-ui-workspace": { "red-ui-workspace": {
"backspace": "core:delete-selection", "backspace": "core:delete-selection",
"ctrl-backspace": "core:delete-selection-and-reconnect",
"delete": "core:delete-selection", "delete": "core:delete-selection",
"ctrl-delete": "core:delete-selection-and-reconnect",
"enter": "core:edit-selected-node", "enter": "core:edit-selected-node",
"ctrl-enter": "core:go-to-selection", "ctrl-enter": "core:go-to-selection",
"ctrl-c": "core:copy-selection-to-internal-clipboard", "ctrl-c": "core:copy-selection-to-internal-clipboard",

View File

@ -15,6 +15,9 @@
**/ **/
RED.nodes = (function() { RED.nodes = (function() {
var PORT_TYPE_INPUT = 1;
var PORT_TYPE_OUTPUT = 0;
var node_defs = {}; var node_defs = {};
var linkTabMap = {}; var linkTabMap = {};
@ -2458,6 +2461,144 @@ RED.nodes = (function() {
return helpContent; 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 { return {
init: function() { init: function() {
RED.events.on("registry:node-type-added",function(type) { RED.events.on("registry:node-type-added",function(type) {
@ -2539,7 +2680,7 @@ RED.nodes = (function() {
add: addNode, add: addNode,
remove: removeNode, remove: removeNode,
clear: clear, clear: clear,
detachNodes: detachNodes,
moveNodesForwards: moveNodesForwards, moveNodesForwards: moveNodesForwards,
moveNodesBackwards: moveNodesBackwards, moveNodesBackwards: moveNodesBackwards,
moveNodesToFront: moveNodesToFront, moveNodesToFront: moveNodesToFront,
@ -2552,6 +2693,9 @@ RED.nodes = (function() {
addLink: addLink, addLink: addLink,
removeLink: removeLink, removeLink: removeLink,
getNodeLinks: function(id, portType) { getNodeLinks: function(id, portType) {
if (typeof id !== 'string') {
id = id.id;
}
if (nodeLinks[id]) { if (nodeLinks[id]) {
if (portType === 1) { if (portType === 1) {
// Return cloned arrays so they can be safely modified by caller // Return cloned arrays so they can be safely modified by caller
@ -2635,6 +2779,7 @@ RED.nodes = (function() {
getAllFlowNodes: getAllFlowNodes, getAllFlowNodes: getAllFlowNodes,
getAllUpstreamNodes: getAllUpstreamNodes, getAllUpstreamNodes: getAllUpstreamNodes,
getAllDownstreamNodes: getAllDownstreamNodes, getAllDownstreamNodes: getAllDownstreamNodes,
getNodeIslands: getNodeIslands,
createExportableNodeSet: createExportableNodeSet, createExportableNodeSet: createExportableNodeSet,
createCompleteNodeSet: createCompleteNodeSet, createCompleteNodeSet: createCompleteNodeSet,
updateConfigNodeUsers: updateConfigNodeUsers, updateConfigNodeUsers: updateConfigNodeUsers,

View File

@ -590,12 +590,14 @@ RED.group = (function() {
markDirty(group); markDirty(group);
} }
function getNodes(group,recursive) { function getNodes(group,recursive,excludeGroup) {
var nodes = []; var nodes = [];
group.nodes.forEach(function(n) { group.nodes.forEach(function(n) {
if (n.type !== 'group' || !excludeGroup) {
nodes.push(n); nodes.push(n);
}
if (recursive && n.type === 'group') { if (recursive && n.type === 'group') {
nodes = nodes.concat(getNodes(n,recursive)) nodes = nodes.concat(getNodes(n,recursive,excludeGroup))
} }
}) })
return nodes; return nodes;

View File

@ -27,5 +27,6 @@ RED.state = {
PANNING: 10, PANNING: 10,
SELECTING_NODE: 11, SELECTING_NODE: 11,
GROUP_DRAGGING: 12, GROUP_DRAGGING: 12,
GROUP_RESIZE: 13 GROUP_RESIZE: 13,
DETACHED_DRAGGING: 14
} }

View File

@ -538,6 +538,8 @@ RED.view = (function() {
RED.actions.add("core:cut-selection-to-internal-clipboard",function(){copySelection();deleteSelection();}); 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: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) { RED.events.on("view:selection-changed", function(selection) {
var hasSelection = (selection.nodes && selection.nodes.length > 0); var hasSelection = (selection.nodes && selection.nodes.length > 0);
var hasMultipleSelection = hasSelection && selection.nodes.length > 1; 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",deleteSelection);
RED.actions.add("core:delete-selection-and-reconnect",function() { deleteSelection(true) });
RED.actions.add("core:edit-selected-node",editSelection); RED.actions.add("core:edit-selected-node",editSelection);
RED.actions.add("core:go-to-selection",function() { RED.actions.add("core:go-to-selection",function() {
if (movingSet.length() > 0) { if (movingSet.length() > 0) {
@ -1384,7 +1387,7 @@ RED.view = (function() {
return; 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; return;
} }
@ -1489,7 +1492,7 @@ RED.view = (function() {
RED.nodes.filterLinks({ target: node.n }).length === 0; 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; mousePos = mouse_position;
var minX = 0; var minX = 0;
var minY = 0; var minY = 0;
@ -1845,10 +1848,30 @@ RED.view = (function() {
// movingSet.add(mousedown_node); // 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) { // if (mousedown_node) {
// delete mousedown_node.gSelected; // delete mousedown_node.gSelected;
// } // }
if (mouse_mode === RED.state.DETACHED_DRAGGING) {
var ns = [];
for (var j=0;j<movingSet.length();j++) {
var n = movingSet.get(j);
if (n.ox !== n.n.x || n.oy !== n.n.y) {
ns.push({n:n.n,ox:n.ox,oy:n.oy,moved:n.n.moved});
n.n.dirty = true;
n.n.moved = true;
}
}
var detachEvent = RED.history.peek();
// The last event in the stack *should* be a multi-event from
// where the links were added/removed
var historyEvent = {t:"move",nodes:ns,dirty:RED.nodes.dirty()}
if (detachEvent.t === "multi") {
detachEvent.events.push(historyEvent)
} else {
RED.history.push(historyEvent)
}
}
for (i=0;i<movingSet.length();i++) { for (i=0;i<movingSet.length();i++) {
var node = movingSet.get(i); var node = movingSet.get(i);
delete node.ox; delete node.ox;
@ -1893,7 +1916,16 @@ RED.view = (function() {
if (mouse_mode === RED.state.MOVING || mouse_mode === RED.state.MOVING_ACTIVE) { if (mouse_mode === RED.state.MOVING || mouse_mode === RED.state.MOVING_ACTIVE) {
return; return;
} }
if (mouse_mode === RED.state.IMPORT_DRAGGING) { if (mouse_mode === RED.state.DETACHED_DRAGGING) {
for (var j=0;j<movingSet.length();j++) {
var n = movingSet.get(j);
n.n.x = n.ox;
n.n.y = n.oy;
}
clearSelection();
RED.history.pop();
mouse_mode = 0;
} else if (mouse_mode === RED.state.IMPORT_DRAGGING) {
clearSelection(); clearSelection();
RED.history.pop(); RED.history.pop();
mouse_mode = 0; mouse_mode = 0;
@ -2133,7 +2165,7 @@ RED.view = (function() {
} }
} }
} }
function deleteSelection() { function deleteSelection(reconnectWires) {
if (mouse_mode === RED.state.SELECTING_NODE) { if (mouse_mode === RED.state.SELECTING_NODE) {
return; return;
} }
@ -2191,6 +2223,16 @@ RED.view = (function() {
var removedSubflowInputs = []; var removedSubflowInputs = [];
var removedSubflowStatus; var removedSubflowStatus;
var subflowInstances = []; var subflowInstances = [];
var historyEvents = [];
if (reconnectWires) {
var reconnectResult = RED.nodes.detachNodes(movingSet.nodes())
var addedLinks = reconnectResult.newLinks;
if (addedLinks.length > 0) {
historyEvents.push({ t:'add', links: addedLinks })
}
removedLinks = removedLinks.concat(reconnectResult.removedLinks)
}
var startDirty = RED.nodes.dirty(); var startDirty = RED.nodes.dirty();
var startChanged = false; var startChanged = false;
@ -2281,7 +2323,6 @@ RED.view = (function() {
} }
} }
var linkRemoveHistoryEvents = [];
if (selectedLinks.length() > 0) { if (selectedLinks.length() > 0) {
selectedLinks.forEach(function(link) { selectedLinks.forEach(function(link) {
if (link.link) { if (link.link) {
@ -2289,31 +2330,22 @@ RED.view = (function() {
var targetId = link.target.id; var targetId = link.target.id;
var sourceIdIndex = link.target.links.indexOf(sourceId); var sourceIdIndex = link.target.links.indexOf(sourceId);
var targetIdIndex = link.source.links.indexOf(targetId); var targetIdIndex = link.source.links.indexOf(targetId);
historyEvents.push({
linkRemoveHistoryEvents.push({
t:"multi",
events: [
{
t: "edit", t: "edit",
node: link.source, node: link.source,
changed: link.source.changed, changed: link.source.changed,
changes: { changes: {
links: $.extend(true,{},{v:link.source.links}).v links: $.extend(true,{},{v:link.source.links}).v
} }
}, })
{ historyEvents.push({
t: "edit", t: "edit",
node: link.target, node: link.target,
changed: link.target.changed, changed: link.target.changed,
changes: { changes: {
links: $.extend(true,{},{v:link.target.links}).v links: $.extend(true,{},{v:link.target.links}).v
} }
}
],
dirty:startDirty
}) })
link.source.changed = true; link.source.changed = true;
link.target.changed = true; link.target.changed = true;
link.target.links.splice(sourceIdIndex,1); link.target.links.splice(sourceIdIndex,1);
@ -2344,11 +2376,11 @@ RED.view = (function() {
if (removedSubflowStatus) { if (removedSubflowStatus) {
historyEvent.subflow.status = removedSubflowStatus; historyEvent.subflow.status = removedSubflowStatus;
} }
if (linkRemoveHistoryEvents.length > 0) { if (historyEvents.length > 0) {
linkRemoveHistoryEvents.unshift(historyEvent); historyEvents.unshift(historyEvent);
RED.history.push({ RED.history.push({
t:"multi", t:"multi",
events: linkRemoveHistoryEvents events: historyEvents
}) })
} else { } else {
RED.history.push(historyEvent); 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) { function calculateTextWidth(str, className) {
var result = convertLineBreakCharacter(str); var result = convertLineBreakCharacter(str);
var width = 0; var width = 0;
@ -2956,11 +3010,14 @@ RED.view = (function() {
msn.dx = msn.n.x-mouse[0]; msn.dx = msn.n.x-mouse[0];
msn.dy = msn.n.y-mouse[1]; msn.dy = msn.n.y-mouse[1];
} }
try {
mouse_offset = d3.mouse(document.body); mouse_offset = d3.mouse(document.body);
if (isNaN(mouse_offset[0])) { if (isNaN(mouse_offset[0])) {
mouse_offset = d3.touches(document.body)[0]; mouse_offset = d3.touches(document.body)[0];
} }
} catch(err) {
mouse_offset = [0,0]
}
} }
function nodeMouseUp(d) { function nodeMouseUp(d) {
@ -3040,7 +3097,7 @@ RED.view = (function() {
//var touch0 = d3.event; //var touch0 = d3.event;
//var pos = [touch0.pageX,touch0.pageY]; //var pos = [touch0.pageX,touch0.pageY];
//RED.touch.radialMenu.show(d3.select(this),pos); //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(); var historyEvent = RED.history.peek();
if (activeSpliceLink) { if (activeSpliceLink) {
// TODO: DRY - droppable/nodeMouseDown/canvasMouseUp // TODO: DRY - droppable/nodeMouseDown/canvasMouseUp
@ -3078,6 +3135,18 @@ RED.view = (function() {
activeHoverGroup = null; activeHoverGroup = null;
} }
if (mouse_mode == RED.state.DETACHED_DRAGGING) {
var ns = [];
for (var j=0;j<movingSet.length();j++) {
var n = movingSet.get(j);
if (n.ox !== n.n.x || n.oy !== n.n.y) {
ns.push({n:n.n,ox:n.ox,oy:n.oy,moved:n.n.moved});
n.n.dirty = true;
n.n.moved = true;
}
}
RED.history.replace({t:"multi",events:[historyEvent,{t:"move",nodes:ns}],dirty: historyEvent.dirty})
}
updateSelection(); updateSelection();
RED.nodes.dirty(true); RED.nodes.dirty(true);