node-red/packages/node_modules/@node-red/editor-client/src/js/ui/view-tools.js

1428 lines
56 KiB
JavaScript

/**
* Copyright JS Foundation and other contributors, http://js.foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
RED.view.tools = (function() {
'use strict';
function selectConnected(type) {
var selection = RED.view.selection();
var visited = new Set();
if (selection.nodes && selection.nodes.length > 0) {
selection.nodes.forEach(function(n) {
if (!visited.has(n)) {
var connected;
if (type === 'all') {
connected = RED.nodes.getAllFlowNodes(n);
} else if (type === 'up') {
connected = [n].concat(RED.nodes.getAllUpstreamNodes(n));
} else if (type === 'down') {
connected = [n].concat(RED.nodes.getAllDownstreamNodes(n));
}
connected.forEach(function(nn) { visited.add(nn) })
}
});
RED.view.select({nodes:Array.from(visited)});
}
}
function alignToGrid() {
if (RED.workspaces.isLocked()) {
return
}
var selection = RED.view.selection();
if (selection.nodes) {
var changedNodes = [];
selection.nodes.forEach(function(n) {
var x = n.w/2 + Math.round((n.x-n.w/2)/RED.view.gridSize())*RED.view.gridSize();
var y = Math.round(n.y/RED.view.gridSize())*RED.view.gridSize();
if (n.x !== x || n.y !== y) {
changedNodes.push({
n:n,
ox: n.x,
oy: n.y,
moved: n.moved
});
n.x = x;
n.y = y;
n.dirty = true;
n.moved = true;
}
});
if (changedNodes.length > 0) {
RED.history.push({t:"move",nodes:changedNodes,dirty:RED.nodes.dirty()});
RED.nodes.dirty(true);
RED.view.redraw(true);
}
}
}
var moving_set = null;
var endMoveSet = false;
function endKeyboardMove() {
endMoveSet = false;
if (moving_set.length > 0) {
var ns = [];
for (var i=0;i<moving_set.length;i++) {
ns.push({n:moving_set[i].n,ox:moving_set[i].ox,oy:moving_set[i].oy,moved:moving_set[i].moved});
moving_set[i].n.moved = true;
moving_set[i].n.dirty = true;
delete moving_set[i].ox;
delete moving_set[i].oy;
}
RED.view.redraw();
RED.history.push({t:"move",nodes:ns,dirty:RED.nodes.dirty()});
RED.nodes.dirty(true);
moving_set = null;
}
}
function moveSelection(dx,dy) {
if (RED.workspaces.isLocked()) {
return
}
if (moving_set === null) {
moving_set = [];
var selection = RED.view.selection();
if (selection.nodes) {
while (selection.nodes.length > 0) {
var n = selection.nodes.shift();
moving_set.push({n:n});
if (n.type === "group") {
selection.nodes = selection.nodes.concat(n.nodes);
}
}
}
}
if (moving_set && moving_set.length > 0) {
if (!endMoveSet) {
$(document).one('keyup',endKeyboardMove);
endMoveSet = true;
}
var dim = RED.view.dimensions();
var space_width = dim.width;
var space_height = dim.height;
var minX = 0;
var minY = 0;
var node;
for (var i=0;i<moving_set.length;i++) {
node = moving_set[i];
if (node.ox == null && node.oy == null) {
node.ox = node.n.x;
node.oy = node.n.y;
node.moved = node.n.moved;
}
node.n.moved = true;
node.n.dirty = true;
node.n.x += dx;
node.n.y += dy;
if ((node.n.x +node.n.w/2) >= space_width) {
node.n.x = space_width -node.n.w/2;
}
if ((node.n.y +node.n.h/2) >= space_height) {
node.n.y = space_height -node.n.h/2;
}
node.n.dirty = true;
if (node.n.type === "group") {
RED.group.markDirty(node.n);
minX = Math.min(node.n.x - 5,minX);
minY = Math.min(node.n.y - 5,minY);
} else {
minX = Math.min(node.n.x-node.n.w/2-5,minX);
minY = Math.min(node.n.y-node.n.h/2-5,minY);
}
}
if (minX !== 0 || minY !== 0) {
for (var n = 0; n<moving_set.length; n++) {
node = moving_set[n];
node.n.x -= minX;
node.n.y -= minY;
}
}
RED.view.redraw();
} else {
RED.view.scroll(dx*10,dy*10);
}
}
function setSelectedNodeLabelState(labelShown) {
if (RED.workspaces.isLocked()) {
return
}
var selection = RED.view.selection();
var historyEvents = [];
var nodes = [];
if (selection.nodes) {
selection.nodes.forEach(function(n) {
if (n.type !== 'subflow' && n.type !== 'group') {
nodes.push(n);
} else if (n.type === 'group') {
nodes = nodes.concat( RED.group.getNodes(n,true));
}
});
}
nodes.forEach(function(n) {
var modified = false;
var oldValue = n.l === undefined?true:n.l;
var showLabel = n._def.hasOwnProperty("showLabel")?n._def.showLabel:true;
if (labelShown) {
if (n.l === false || (!showLabel && !n.hasOwnProperty('l'))) {
n.l = true;
modified = true;
}
} else {
if ((showLabel && (!n.hasOwnProperty('l') || n.l === true)) || (!showLabel && n.l === true) ) {
n.l = false;
modified = true;
}
}
if (modified) {
historyEvents.push({
t: "edit",
node: n,
changed: n.changed,
changes: {
l: oldValue
}
})
n.changed = true;
n.dirty = true;
n.resize = true;
}
})
if (historyEvents.length > 0) {
RED.history.push({
t: "multi",
events: historyEvents,
dirty: RED.nodes.dirty()
})
RED.nodes.dirty(true);
}
RED.view.redraw();
}
function selectFirstNode() {
var canidates;
var origin = {x:0, y:0};
var activeGroup = RED.view.getActiveGroup();
if (!activeGroup) {
candidates = RED.view.getActiveNodes();
} else {
candidates = RED.group.getNodes(activeGroup,false);
origin = activeGroup;
}
var distances = [];
candidates.forEach(function(node) {
var deltaX = node.x - origin.x;
var deltaY = node.y - origin.x;
var delta = deltaY*deltaY + deltaX*deltaX;
distances.push({node: node, delta: delta})
});
if (distances.length > 0) {
distances.sort(function(A,B) {
return A.delta - B.delta
})
var newNode = distances[0].node;
if (newNode) {
RED.view.select({nodes:[newNode]});
RED.view.reveal(newNode.id,false);
}
}
}
function gotoNextNode() {
var selection = RED.view.selection();
if (selection.nodes && selection.nodes.length === 1) {
var origin = selection.nodes[0];
var links = RED.nodes.filterLinks({source:origin});
if (links.length > 0) {
links.sort(function(A,B) {
return Math.abs(A.target.y - origin.y) - Math.abs(B.target.y - origin.y)
})
var newNode = links[0].target;
if (newNode) {
RED.view.select({nodes:[newNode]});
RED.view.reveal(newNode.id,false);
}
}
} else if (RED.workspaces.selection().length === 0) {
selectFirstNode();
}
}
function gotoPreviousNode() {
var selection = RED.view.selection();
if (selection.nodes && selection.nodes.length === 1) {
var origin = selection.nodes[0];
var links = RED.nodes.filterLinks({target:origin});
if (links.length > 0) {
links.sort(function(A,B) {
return Math.abs(A.source.y - origin.y) - Math.abs(B.source.y - origin.y)
})
var newNode = links[0].source;
if (newNode) {
RED.view.select({nodes:[newNode]});
RED.view.reveal(newNode.id,false);
}
}
} else if (RED.workspaces.selection().length === 0) {
selectFirstNode();
}
}
function getChildren(node) {
return RED.nodes.filterLinks({source:node}).map(function(l) { return l.target})
}
function getParents(node) {
return RED.nodes.filterLinks({target:node}).map(function(l) { return l.source})
}
function getSiblings(node) {
var siblings = new Set();
var parents = getParents(node);
parents.forEach(function(p) {
getChildren(p).forEach(function(c) { siblings.add(c) })
});
var children = getChildren(node);
children.forEach(function(p) {
getParents(p).forEach(function(c) { siblings.add(c) })
});
siblings.delete(node);
return Array.from(siblings);
}
function gotoNextSibling() {
// 'next' defined as nearest on the y-axis below this node
var selection = RED.view.selection();
if (selection.nodes && selection.nodes.length === 1) {
var origin = selection.nodes[0];
var siblings = getSiblings(origin);
if (siblings.length > 0) {
siblings = siblings.filter(function(n) { return n.y > origin. y})
siblings.sort(function(A,B) {
return Math.abs(A.y - origin.y) - Math.abs(B.y - origin.y)
})
var newNode = siblings[0];
if (newNode) {
RED.view.select({nodes:[newNode]});
RED.view.reveal(newNode.id,false);
}
}
} else if (RED.workspaces.selection().length === 0) {
selectFirstNode();
}
}
function gotoPreviousSibling() {
// 'next' defined as nearest on the y-axis above this node
var selection = RED.view.selection();
if (selection.nodes && selection.nodes.length === 1) {
var origin = selection.nodes[0];
var siblings = getSiblings(origin);
if (siblings.length > 0) {
siblings = siblings.filter(function(n) { return n.y < origin. y})
siblings.sort(function(A,B) {
return Math.abs(A.y - origin.y) - Math.abs(B.y - origin.y)
})
var newNode = siblings[0];
if (newNode) {
RED.view.select({nodes:[newNode]});
RED.view.reveal(newNode.id,false);
}
}
} else if (RED.workspaces.selection().length === 0) {
selectFirstNode();
}
}
// function addNode() {
// var selection = RED.view.selection();
// if (selection.nodes && selection.nodes.length === 1 && selection.nodes[0].outputs > 0) {
// var selectedNode = selection.nodes[0];
// RED.view.showQuickAddDialog([
// selectedNode.x + selectedNode.w + 50,selectedNode.y
// ])
// } else {
// RED.view.showQuickAddDialog();
// }
// }
function gotoNearestNode(direction) {
var selection = RED.view.selection();
if (selection.nodes && selection.nodes.length === 1) {
var origin = selection.nodes[0];
var candidates = RED.nodes.filterNodes({z:origin.z});
candidates = candidates.concat(RED.view.getSubflowPorts());
var distances = [];
candidates.forEach(function(node) {
if (node === origin) {
return;
}
var deltaX = node.x - origin.x;
var deltaY = node.y - origin.y;
var delta = deltaY*deltaY + deltaX*deltaX;
var angle = (180/Math.PI)*Math.atan2(deltaY,deltaX);
if (angle < 0) { angle += 360 }
if (angle > 360) { angle -= 360 }
var weight;
// 0 - right
// 270 - above
// 90 - below
// 180 - left
switch(direction) {
case 'up': if (angle < 210 || angle > 330) { return }
weight = Math.max(Math.abs(270 - angle)/60, 0.2);
break;
case 'down': if (angle < 30 || angle > 150) { return }
weight = Math.max(Math.abs(90 - angle)/60, 0.2);
break;
case 'left': if (angle < 140 || angle > 220) { return }
weight = Math.max(Math.abs(180 - angle)/40, 0.1 );
break;
case 'right': if (angle > 40 && angle < 320) { return }
weight = Math.max(Math.abs(angle)/40, 0.1);
break;
}
weight = Math.max(weight,0.1);
distances.push({
node: node,
d: delta,
w: weight,
delta: delta*weight
})
})
if (distances.length > 0) {
distances.sort(function(A,B) {
return A.delta - B.delta
})
var newNode = distances[0].node;
if (newNode) {
RED.view.select({nodes:[newNode]});
RED.view.reveal(newNode.id,false);
}
}
} else if (RED.workspaces.selection().length === 0) {
var candidates = RED.view.getActiveNodes();
var distances = [];
candidates.forEach(function(node) {
var deltaX = node.x;
var deltaY = node.y;
var delta = deltaY*deltaY + deltaX*deltaX;
distances.push({node: node, delta: delta})
});
if (distances.length > 0) {
distances.sort(function(A,B) {
return A.delta - B.delta
})
var newNode = distances[0].node;
if (newNode) {
RED.view.select({nodes:[newNode]});
RED.view.reveal(newNode.id,false);
}
}
}
}
function alignSelectionToEdge(direction) {
if (RED.workspaces.isLocked()) {
return;
}
var selection = RED.view.selection();
if (selection.nodes && selection.nodes.length > 1) {
var changedNodes = [];
var bounds = {
minX: Number.MAX_SAFE_INTEGER,
minY: Number.MAX_SAFE_INTEGER,
maxX: Number.MIN_SAFE_INTEGER,
maxY: Number.MIN_SAFE_INTEGER
}
selection.nodes.forEach(function(n) {
if (n.type === "group") {
bounds.minX = Math.min(bounds.minX, n.x);
bounds.minY = Math.min(bounds.minY, n.y);
bounds.maxX = Math.max(bounds.maxX, n.x + n.w);
bounds.maxY = Math.max(bounds.maxY, n.y + n.h);
} else {
bounds.minX = Math.min(bounds.minX, n.x - n.w/2);
bounds.minY = Math.min(bounds.minY, n.y - n.h/2);
bounds.maxX = Math.max(bounds.maxX, n.x + n.w/2);
bounds.maxY = Math.max(bounds.maxY, n.y + n.h/2);
}
});
bounds.midX = bounds.minX + (bounds.maxX - bounds.minX)/2;
bounds.midY = bounds.minY + (bounds.maxY - bounds.minY)/2;
selection.nodes.forEach(function(n) {
var targetX;
var targetY;
var isGroup = n.type==="group";
switch(direction) {
case 'top':
targetX = n.x;
targetY = bounds.minY + (isGroup?0:(n.h/2));
break;
case 'bottom':
targetX = n.x;
targetY = bounds.maxY - (isGroup?n.h:(n.h/2));
break;
case 'left':
targetX = bounds.minX + (isGroup?0:(n.w/2));
targetY = n.y;
break;
case 'right':
targetX = bounds.maxX - (isGroup?n.w:(n.w/2));
targetY = n.y;
break;
case 'middle':
targetX = n.x;
targetY = bounds.midY - (isGroup?n.h/2:0)
break;
case 'center':
targetX = bounds.midX - (isGroup?n.w/2:0)
targetY = n.y;
break;
}
if (n.x !== targetX || n.y !== targetY) {
if (!isGroup) {
changedNodes.push({
n:n,
ox: n.x,
oy: n.y,
moved: n.moved
});
n.x = targetX;
n.y = targetY;
n.dirty = true;
n.moved = true;
} else {
var groupNodes = RED.group.getNodes(n, true);
var deltaX = n.x - targetX;
var deltaY = n.y - targetY;
groupNodes.forEach(function(gn) {
if (gn.type !== "group" ) {
changedNodes.push({
n:gn,
ox: gn.x,
oy: gn.y,
moved: gn.moved
});
gn.x = gn.x - deltaX;
gn.y = gn.y - deltaY;
gn.dirty = true;
gn.moved = true;
}
})
}
}
});
if (changedNodes.length > 0) {
RED.history.push({t:"move",nodes:changedNodes,dirty:RED.nodes.dirty()});
RED.nodes.dirty(true);
RED.view.redraw(true);
}
}
}
function distributeSelection(direction) {
if (RED.workspaces.isLocked()) {
return
}
var selection = RED.view.selection();
if (selection.nodes && selection.nodes.length > 2) {
var changedNodes = [];
var bounds = {
minX: Number.MAX_SAFE_INTEGER,
minY: Number.MAX_SAFE_INTEGER,
maxX: Number.MIN_SAFE_INTEGER,
maxY: Number.MIN_SAFE_INTEGER
}
var startAnchors = [];
var endAnchors = [];
selection.nodes.forEach(function(n) {
var nx,ny;
if (n.type === "group") {
nx = n.x + n.w/2;
ny = n.y + n.h/2;
} else {
nx = n.x;
ny = n.y;
}
if (direction === "h") {
if (nx < bounds.minX) {
startAnchors = [];
bounds.minX = nx;
}
if (nx === bounds.minX) {
startAnchors.push(n);
}
if (nx > bounds.maxX) {
endAnchors = [];
bounds.maxX = nx;
}
if (nx === bounds.maxX) {
endAnchors.push(n);
}
} else {
if (ny < bounds.minY) {
startAnchors = [];
bounds.minY = ny;
}
if (ny === bounds.minY) {
startAnchors.push(n);
}
if (ny > bounds.maxY) {
endAnchors = [];
bounds.maxY = ny;
}
if (ny === bounds.maxY) {
endAnchors.push(n);
}
}
});
var startAnchor = startAnchors[0];
var endAnchor = endAnchors[0];
var nodeSpace = 0;
var nodesToMove = selection.nodes.filter(function(n) {
if (n.id !== startAnchor.id && n.id !== endAnchor.id) {
nodeSpace += direction === 'h'?n.w:n.h;
return true;
}
return false;
}).sort(function(A,B) {
if (direction === 'h') {
return A.x - B.x
} else {
return A.y - B.y
}
})
var saX = startAnchor.x + startAnchor.w/2;
var saY = startAnchor.y + startAnchor.h/2;
if (startAnchor.type === "group") {
saX = startAnchor.x + startAnchor.w;
saY = startAnchor.y + startAnchor.h;
}
var eaX = endAnchor.x;
var eaY = endAnchor.y;
if (endAnchor.type !== "group") {
eaX -= endAnchor.w/2;
eaY -= endAnchor.h/2;
}
var spaceToFill = direction === 'h'?(eaX - saX - nodeSpace): (eaY - saY - nodeSpace);
var spaceBetweenNodes = spaceToFill / (nodesToMove.length + 1);
var tx = saX;
var ty = saY;
while(nodesToMove.length > 0) {
if (direction === 'h') {
tx += spaceBetweenNodes;
} else {
ty += spaceBetweenNodes;
}
var nextNode = nodesToMove.shift();
var isGroup = nextNode.type==="group";
var nx = nextNode.x;
var ny = nextNode.y;
if (!isGroup) {
tx += nextNode.w/2;
ty += nextNode.h/2;
}
if ((direction === 'h' && nx !== tx) || (direction === 'v' && ny !== ty)) {
if (!isGroup) {
changedNodes.push({
n:nextNode,
ox: nextNode.x,
oy: nextNode.y,
moved: nextNode.moved
});
if (direction === 'h') {
nextNode.x = tx;
} else {
nextNode.y = ty;
}
nextNode.dirty = true;
nextNode.moved = true;
} else {
var groupNodes = RED.group.getNodes(nextNode, true);
var deltaX = direction === 'h'? nx - tx : 0;
var deltaY = direction === 'v'? ny - ty : 0;
groupNodes.forEach(function(gn) {
if (gn.type !== "group" ) {
changedNodes.push({
n:gn,
ox: gn.x,
oy: gn.y,
moved: gn.moved
});
gn.x = gn.x - deltaX;
gn.y = gn.y - deltaY;
gn.dirty = true;
gn.moved = true;
}
})
}
}
if (isGroup) {
tx += nextNode.w;
ty += nextNode.h;
} else {
tx += nextNode.w/2;
ty += nextNode.h/2;
}
}
if (changedNodes.length > 0) {
RED.history.push({t:"move",nodes:changedNodes,dirty:RED.nodes.dirty()});
RED.nodes.dirty(true);
RED.view.redraw(true);
}
}
}
function reorderSelection(dir) {
if (RED.workspaces.isLocked()) {
return
}
var selection = RED.view.selection();
if (selection.nodes) {
var nodesToMove = [];
selection.nodes.forEach(function(n) {
if (n.type === "group") {
nodesToMove.push(n)
nodesToMove = nodesToMove.concat(RED.group.getNodes(n, true))
} else if (n.type !== "subflow"){
nodesToMove.push(n);
}
})
if (nodesToMove.length > 0) {
var z = nodesToMove[0].z;
var existingOrder = RED.nodes.getNodeOrder(z);
var movedNodes;
if (dir === "forwards") {
movedNodes = RED.nodes.moveNodesForwards(nodesToMove);
} else if (dir === "backwards") {
movedNodes = RED.nodes.moveNodesBackwards(nodesToMove);
} else if (dir === "front") {
movedNodes = RED.nodes.moveNodesToFront(nodesToMove);
} else if (dir === "back") {
movedNodes = RED.nodes.moveNodesToBack(nodesToMove);
}
if (movedNodes.length > 0) {
var newOrder = RED.nodes.getNodeOrder(z);
RED.history.push({t:"reorder",nodes:{z:z,from:existingOrder,to:newOrder},dirty:RED.nodes.dirty()});
RED.nodes.dirty(true);
RED.view.redraw(true);
}
}
}
}
function wireSeriesOfNodes() {
if (RED.workspaces.isLocked()) {
return
}
var selection = RED.view.selection();
if (selection.nodes) {
if (selection.nodes.length > 1) {
var i = 0;
var newLinks = [];
while (i < selection.nodes.length - 1) {
var nodeA = selection.nodes[i];
var nodeB = selection.nodes[i+1];
if (nodeA.outputs > 0 && nodeB.inputs > 0) {
var existingLinks = RED.nodes.filterLinks({
source: nodeA,
target: nodeB,
sourcePort: 0
})
if (existingLinks.length === 0) {
var newLink = {
source: nodeA,
target: nodeB,
sourcePort: 0
}
RED.nodes.addLink(newLink);
newLinks.push(newLink);
}
}
i++;
}
if (newLinks.length > 0) {
RED.history.push({
t: 'add',
links: newLinks,
dirty: RED.nodes.dirty()
})
RED.nodes.dirty(true);
RED.view.redraw(true);
}
}
}
}
function wireNodeToMultiple() {
if (RED.workspaces.isLocked()) {
return
}
var selection = RED.view.selection();
if (selection.nodes) {
if (selection.nodes.length > 1) {
var sourceNode = selection.nodes[0];
if (sourceNode.outputs === 0) {
return;
}
var i = 1;
var newLinks = [];
while (i < selection.nodes.length) {
var targetNode = selection.nodes[i];
if (targetNode.inputs > 0) {
var existingLinks = RED.nodes.filterLinks({
source: sourceNode,
target: targetNode,
sourcePort: Math.min(sourceNode.outputs-1,i-1)
})
if (existingLinks.length === 0) {
var newLink = {
source: sourceNode,
target: targetNode,
sourcePort: Math.min(sourceNode.outputs-1,i-1)
}
RED.nodes.addLink(newLink);
newLinks.push(newLink);
}
}
i++;
}
if (newLinks.length > 0) {
RED.history.push({
t: 'add',
links: newLinks,
dirty: RED.nodes.dirty()
})
RED.nodes.dirty(true);
RED.view.redraw(true);
}
}
}
}
function wireMultipleToNode() {
if (RED.workspaces.isLocked()) {
return
}
var selection = RED.view.selection();
if (selection.nodes) {
if (selection.nodes.length > 1) {
var targetNode = selection.nodes[selection.nodes.length - 1];
if (targetNode.inputs === 0) {
return;
}
var i = 0;
var newLinks = [];
for (i = 0; i < selection.nodes.length - 1; i++) {
var sourceNode = selection.nodes[i];
if (sourceNode.outputs > 0) {
// Wire the first output to the target that has no link to the target yet.
// This allows for connecting all combinations of inputs/outputs.
// The user may then delete links quickly that aren't needed.
var sourceConnectedOutports = RED.nodes.filterLinks({
source: sourceNode,
target: targetNode
});
// Get outport indices that have no link yet
var sourceOutportIndices = Array.from({ length: sourceNode.outputs }, (_, i) => i);
var sourceConnectedOutportIndices = sourceConnectedOutports.map( x => x.sourcePort );
var sourceFreeOutportIndices = sourceOutportIndices.filter(x => !sourceConnectedOutportIndices.includes(x));
// Does an unconnected source port exist?
if (sourceFreeOutportIndices.length == 0) {
continue;
}
// Connect the first free outport to the target
var newLink = {
source: sourceNode,
target: targetNode,
sourcePort: sourceFreeOutportIndices[0]
}
RED.nodes.addLink(newLink);
newLinks.push(newLink);
}
}
if (newLinks.length > 0) {
RED.history.push({
t: 'add',
links: newLinks,
dirty: RED.nodes.dirty()
})
RED.nodes.dirty(true);
RED.view.redraw(true);
}
}
}
}
/**
* Splits selected wires and re-joins them with link-out+link-in
* @param {Object || Object[]} wires The wire(s) to split and replace with link-out, link-in nodes.
*/
function splitWiresWithLinkNodes(wires) {
if (RED.workspaces.isLocked()) {
return
}
let wiresToSplit = wires || (RED.view.selection().links && RED.view.selection().links.filter(e => !e.link));
if (!wiresToSplit) {
return
}
if (!Array.isArray(wiresToSplit)) {
wiresToSplit = [wiresToSplit];
}
if (wiresToSplit.length < 1) {
return; //nothing selected
}
const history = {
t: 'multi',
events: [],
dirty: RED.nodes.dirty()
}
const nodeSrcMap = {};
const nodeTrgMap = {};
const _gridSize = RED.view.gridSize();
for (let wireIdx = 0; wireIdx < wiresToSplit.length; wireIdx++) {
const wire = wiresToSplit[wireIdx];
//get source and target nodes of this wire link
const nSrc = wire.source;
const nTrg = wire.target;
var updateNewNodePosXY = function (origNode, newNode, alignLeft, snap, yOffset) {
const nnSize = RED.view.calculateNodeDimensions(newNode);
newNode.w = nnSize[0];
newNode.h = nnSize[1];
const coords = { x: origNode.x || 0, y: origNode.y || 0, w: origNode.w || RED.view.node_width, h: origNode.h || RED.view.node_height };
const x = coords.x - (coords.w/2.0);
if (alignLeft) {
coords.x = x - _gridSize - (newNode.w/2.0);
} else {
coords.x = x + coords.w + _gridSize + (newNode.w/2.0);
}
newNode.x = coords.x;
newNode.y = coords.y;
if (snap !== false) {
const offsets = RED.view.tools.calculateGridSnapOffsets(newNode);
newNode.x -= offsets.x;
newNode.y -= offsets.y;
}
newNode.y += (yOffset || 0);
}
const srcPort = (wire.sourcePort || 0);
let linkOutMapId = nSrc.id + ':' + srcPort;
let nnLinkOut = nodeSrcMap[linkOutMapId];
//Create a Link Out if one is not already present
if(!nnLinkOut) {
const nLinkOut = RED.view.createNode("link out"); //create link node
nnLinkOut = nLinkOut.node;
let yOffset = 0;
if(nSrc.outputs > 1) {
const CENTER_PORT = (((nSrc.outputs-1) / 2) + 1);
const offsetCount = Math.abs(CENTER_PORT - (srcPort + 1));
yOffset = (_gridSize * 2 * offsetCount);
if((srcPort + 1) < CENTER_PORT) {
yOffset = -yOffset;
}
updateNewNodePosXY(nSrc, nnLinkOut, false, false, yOffset);
} else {
updateNewNodePosXY(nSrc, nnLinkOut, false, RED.view.snapGrid, yOffset);
}
//add created node
nnLinkOut = RED.nodes.add(nnLinkOut);
nodeSrcMap[linkOutMapId] = nnLinkOut;
RED.editor.validateNode(nnLinkOut);
history.events.push(nLinkOut.historyEvent);
//connect node to link node
const link = {
source: nSrc,
sourcePort: wire.sourcePort || 0,
target: nnLinkOut
};
RED.nodes.addLink(link);
history.events.push({
t: 'add',
links: [link],
});
}
let nnLinkIn = nodeTrgMap[nTrg.id];
//Create a Link In if one is not already present
if(!nnLinkIn) {
const nLinkIn = RED.view.createNode("link in"); //create link node
nnLinkIn = nLinkIn.node;
updateNewNodePosXY(nTrg, nnLinkIn, true, RED.view.snapGrid, 0);
//add created node
nnLinkIn = RED.nodes.add(nnLinkIn);
nodeTrgMap[nTrg.id] = nnLinkIn;
RED.editor.validateNode(nnLinkIn);
history.events.push(nLinkIn.historyEvent);
//connect node to link node
const link = {
source: nnLinkIn,
sourcePort: 0,
target: nTrg
};
RED.nodes.addLink(link);
history.events.push({
t: 'add',
links: [link],
});
}
//connect the link out/link in virtual wires
if(nnLinkIn.links.indexOf(nnLinkOut.id) == -1) {
nnLinkIn.links.push(nnLinkOut.id);
}
if(nnLinkOut.links.indexOf(nnLinkIn.id) == -1) {
nnLinkOut.links.push(nnLinkIn.id);
}
//delete the original wire
RED.nodes.removeLink(wire);
history.events.push({
t: "delete",
links: [wire]
});
}
//add all history events to stack
RED.history.push(history);
//select all downstream of new link-in nodes so user can drag to new location
RED.view.clearSelection();
RED.view.select({nodes: Object.values(nodeTrgMap) });
selectConnected("down");
//update the view
RED.nodes.dirty(true);
RED.view.redraw(true);
}
/**
* Calculate the required offsets to snap a node
* @param {Object} node The node to calculate grid snap offsets for
* @param {Object} [options] Options: `align` can be "nearest", "left" or "right"
* @returns `{x:number, y:number}` as the offsets to deduct from `x` and `y`
*/
function calculateGridSnapOffsets(node, options) {
options = options || { align: "nearest" };
const gridOffset = { x: 0, y: 0 };
const gridSize = RED.view.gridSize();
const offsetLeft = node.x - (gridSize * Math.round((node.x - node.w / 2) / gridSize) + node.w / 2);
const offsetRight = node.x - (gridSize * Math.round((node.x + node.w / 2) / gridSize) - node.w / 2);
gridOffset.x = offsetRight;
if (options.align === "right") {
//skip - already set to right
} else if (options.align === "left" || Math.abs(offsetLeft) < Math.abs(offsetRight)) {
gridOffset.x = offsetLeft;
}
gridOffset.y = node.y - (gridSize * Math.round(node.y / gridSize));
return gridOffset;
}
/**
* Generate names for the select nodes.
* - it only sets the name if it is currently blank
* - it uses `<paletteLabel> <N>` - where N is the next available integer that
* doesn't clash with any existing nodes of that type
* @param {Object} node The node to set the name of - if not provided, uses current selection
* @param {{ renameBlank: boolean, renameClash: boolean, generateHistory: boolean }} options Possible options are `renameBlank`, `renameClash` and `generateHistory`
*/
function generateNodeNames(node, options) {
if (RED.workspaces.isLocked()) {
return
}
options = Object.assign({
renameBlank: true,
renameClash: true,
generateHistory: true
}, options)
let nodes = node;
if (node) {
if (!Array.isArray(node)) {
nodes = [ node ]
}
} else {
nodes = RED.view.selection().nodes;
}
if (nodes && nodes.length > 0) {
// Generate history event if using the workspace selection,
// or if the provided node already exists
const generateHistory = options.generateHistory && (!node || !!RED.nodes.node(node.id))
const historyEvents = []
const typeIndex = {}
let changed = false;
nodes.forEach(n => {
const nodeDef = n._def || RED.nodes.getType(n.type)
if (nodeDef && nodeDef.defaults && nodeDef.defaults.name) {
const paletteLabel = RED.utils.getPaletteLabel(n.type, nodeDef)
const defaultNodeNameRE = new RegExp('^'+paletteLabel.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')+' (\\d+)$')
if (!typeIndex.hasOwnProperty(n.type)) {
const existingNodes = RED.nodes.filterNodes({type: n.type})
let maxNameNumber = 0;
existingNodes.forEach(n => {
let match = defaultNodeNameRE.exec(n.name)
if (match) {
let nodeNumber = parseInt(match[1])
if (nodeNumber > maxNameNumber) {
maxNameNumber = nodeNumber
}
}
})
typeIndex[n.type] = maxNameNumber + 1
}
if ((options.renameBlank && n.name === '') || (options.renameClash && defaultNodeNameRE.test(n.name))) {
if (generateHistory) {
historyEvents.push({
t:'edit',
node: n,
changes: { name: n.name },
dirty: RED.nodes.dirty(),
changed: n.changed
})
}
n.name = paletteLabel+" "+typeIndex[n.type]
n.dirty = true
typeIndex[n.type]++
changed = true
}
}
})
if (changed) {
if (historyEvents.length > 0) {
RED.history.push({
t: 'multi',
events: historyEvents
})
}
RED.nodes.dirty(true)
RED.view.redraw()
}
}
}
function addJunctionsToWires(wires) {
if (RED.workspaces.isLocked()) {
return
}
let wiresToSplit = wires || (RED.view.selection().links && RED.view.selection().links.filter(e => !e.link));
if (!wiresToSplit) {
return
}
if (!Array.isArray(wiresToSplit)) {
wiresToSplit = [wiresToSplit];
}
if (wiresToSplit.length === 0) {
return;
}
var removedLinks = new Set()
var addedLinks = []
var addedJunctions = []
var groupedLinks = {}
wiresToSplit.forEach(function(l) {
var sourceId = l.source.id+":"+l.sourcePort
groupedLinks[sourceId] = groupedLinks[sourceId] || []
groupedLinks[sourceId].push(l)
groupedLinks[l.target.id] = groupedLinks[l.target.id] || []
groupedLinks[l.target.id].push(l)
});
var linkGroups = Object.keys(groupedLinks)
linkGroups.sort(function(A,B) {
return groupedLinks[B].length - groupedLinks[A].length
})
const wasDirty = RED.nodes.dirty()
linkGroups.forEach(function(gid) {
var links = groupedLinks[gid]
var junction = {
_def: {defaults:{}},
type: 'junction',
z: RED.workspaces.active(),
id: RED.nodes.id(),
x: 0,
y: 0,
w: 0, h: 0,
outputs: 1,
inputs: 1,
dirty: true,
moved: true
}
links = links.filter(function(l) { return !removedLinks.has(l) })
if (links.length === 0) {
return
}
let pointCount = 0
links.forEach(function(l) {
if (l._sliceLocation) {
junction.x += l._sliceLocation.x
junction.y += l._sliceLocation.y
delete l._sliceLocation
pointCount++
} else {
junction.x += l.source.x + l.source.w/2 + l.target.x - l.target.w/2
junction.y += l.source.y + l.target.y
pointCount += 2
}
})
junction.x = Math.round(junction.x/pointCount)
junction.y = Math.round(junction.y/pointCount)
if (RED.view.snapGrid) {
let gridSize = RED.view.gridSize()
junction.x = (gridSize*Math.round(junction.x/gridSize));
junction.y = (gridSize*Math.round(junction.y/gridSize));
}
var nodeGroups = new Set()
junction = RED.nodes.addJunction(junction)
addedJunctions.push(junction)
let newLink
if (gid === links[0].source.id+":"+links[0].sourcePort) {
newLink = {
source: links[0].source,
sourcePort: links[0].sourcePort,
target: junction
}
} else {
newLink = {
source: junction,
sourcePort: 0,
target: links[0].target
}
}
addedLinks.push(newLink)
RED.nodes.addLink(newLink)
links.forEach(function(l) {
removedLinks.add(l)
RED.nodes.removeLink(l)
let newLink
if (gid === l.target.id) {
newLink = {
source: l.source,
sourcePort: l.sourcePort,
target: junction
}
} else {
newLink = {
source: junction,
sourcePort: 0,
target: l.target
}
}
addedLinks.push(newLink)
RED.nodes.addLink(newLink)
nodeGroups.add(l.source.g || "__NONE__")
nodeGroups.add(l.target.g || "__NONE__")
})
if (nodeGroups.size === 1) {
var group = nodeGroups.values().next().value
if (group !== "__NONE__") {
RED.group.addToGroup(RED.nodes.group(group), junction)
}
}
})
if (addedJunctions.length > 0) {
RED.history.push({
dirty: wasDirty,
t: 'add',
links: addedLinks,
junctions: addedJunctions,
removedLinks: Array.from(removedLinks)
})
RED.nodes.dirty(true)
RED.view.select({nodes: addedJunctions });
}
RED.view.redraw(true);
}
function copyItemUrl(node, isEdit) {
if (!node) {
const selection = RED.view.selection();
if (selection.nodes && selection.nodes.length > 0) {
node = selection.nodes[0]
}
}
if (node) {
let thingType = 'node'
if (node.type === 'group') {
thingType = 'group'
} else if (node.type === 'tab' || node.type === 'subflow') {
thingType = 'flow'
}
let url = `${window.location.origin}${window.location.pathname}#${thingType}/${node.id}`
if (isEdit) {
url += '/edit'
}
if (RED.clipboard.copyText(url)) {
RED.notify(RED._("sidebar.info.copyURL2Clipboard"), { timeout: 2000 })
}
}
}
/**
* Determine if a point is within a node
* @param {*} node - A Node or Junction node
* @param {[Number,Number]} mouse_position The x,y position of the mouse
* @param {Number} [marginX=0] - A margin to add or deduct from the x position (to increase the hit area)
* @param {Number} [marginY=0] - A margin to add or deduct from the y position (to increase the hit area)
* @returns
*/
function isPointInNode (node, [x, y], marginX, marginY) {
marginX = marginX || 0
marginY = marginY || 0
let w = node.w || 10 // junctions dont have any w or h value
let h = node.h || 10
let x1, x2, y1, y2
if (node.type === "junction" || node.type === "group") {
// x/y is the top left of the node
x1 = node.x
y1 = node.y
x2 = node.x + w
y2 = node.y + h
} else {
// x/y is the center of the node
const [xMid, yMid] = [w/2, h/2]
x1 = node.x - xMid
y1 = node.y - yMid
x2 = node.x + xMid
y2 = node.y + yMid
}
return (x >= (x1 - marginX) && x <= (x2 + marginX) && y >= (y1 - marginY) && y <= (y2 + marginY))
}
return {
init: function() {
RED.actions.add("core:show-selected-node-labels", function() { setSelectedNodeLabelState(true); })
RED.actions.add("core:hide-selected-node-labels", function() { setSelectedNodeLabelState(false); })
RED.actions.add("core:scroll-view-up", function() { RED.view.scroll(0,-RED.view.gridSize());});
RED.actions.add("core:scroll-view-right", function() { RED.view.scroll(RED.view.gridSize(),0);});
RED.actions.add("core:scroll-view-down", function() { RED.view.scroll(0,RED.view.gridSize());});
RED.actions.add("core:scroll-view-left", function() { RED.view.scroll(-RED.view.gridSize(),0);});
RED.actions.add("core:step-view-up", function() { RED.view.scroll(0,-5*RED.view.gridSize());});
RED.actions.add("core:step-view-right", function() { RED.view.scroll(5*RED.view.gridSize(),0);});
RED.actions.add("core:step-view-down", function() { RED.view.scroll(0,5*RED.view.gridSize());});
RED.actions.add("core:step-view-left", function() { RED.view.scroll(-5*RED.view.gridSize(),0);});
RED.actions.add("core:move-selection-up", function() { moveSelection(0,-1);});
RED.actions.add("core:move-selection-right", function() { moveSelection(1,0);});
RED.actions.add("core:move-selection-down", function() { moveSelection(0,1);});
RED.actions.add("core:move-selection-left", function() { moveSelection(-1,0);});
RED.actions.add("core:move-selection-forwards", function() { reorderSelection('forwards') })
RED.actions.add("core:move-selection-backwards", function() { reorderSelection('backwards') })
RED.actions.add("core:move-selection-to-front", function() { reorderSelection('front') })
RED.actions.add("core:move-selection-to-back", function() { reorderSelection('back') })
RED.actions.add("core:step-selection-up", function() { moveSelection(0,-RED.view.gridSize());});
RED.actions.add("core:step-selection-right", function() { moveSelection(RED.view.gridSize(),0);});
RED.actions.add("core:step-selection-down", function() { moveSelection(0,RED.view.gridSize());});
RED.actions.add("core:step-selection-left", function() { moveSelection(-RED.view.gridSize(),0);});
RED.actions.add("core:select-connected-nodes", function() { selectConnected("all") });
RED.actions.add("core:select-downstream-nodes", function() { selectConnected("down") });
RED.actions.add("core:select-upstream-nodes", function() { selectConnected("up") });
RED.actions.add("core:go-to-next-node", function() { gotoNextNode() })
RED.actions.add("core:go-to-previous-node", function() { gotoPreviousNode() })
RED.actions.add("core:go-to-next-sibling", function() { gotoNextSibling() })
RED.actions.add("core:go-to-previous-sibling", function() { gotoPreviousSibling() })
RED.actions.add("core:go-to-nearest-node-on-left", function() { gotoNearestNode('left')})
RED.actions.add("core:go-to-nearest-node-on-right", function() { gotoNearestNode('right')})
RED.actions.add("core:go-to-nearest-node-above", function() { gotoNearestNode('up') })
RED.actions.add("core:go-to-nearest-node-below", function() { gotoNearestNode('down') })
RED.actions.add("core:align-selection-to-grid", alignToGrid);
RED.actions.add("core:align-selection-to-left", function() { alignSelectionToEdge('left') })
RED.actions.add("core:align-selection-to-right", function() { alignSelectionToEdge('right') })
RED.actions.add("core:align-selection-to-top", function() { alignSelectionToEdge('top') })
RED.actions.add("core:align-selection-to-bottom", function() { alignSelectionToEdge('bottom') })
RED.actions.add("core:align-selection-to-middle", function() { alignSelectionToEdge('middle') })
RED.actions.add("core:align-selection-to-center", function() { alignSelectionToEdge('center') })
RED.actions.add("core:distribute-selection-horizontally", function() { distributeSelection('h') })
RED.actions.add("core:distribute-selection-vertically", function() { distributeSelection('v') })
RED.actions.add("core:wire-series-of-nodes", function() { wireSeriesOfNodes() })
RED.actions.add("core:wire-node-to-multiple", function() { wireNodeToMultiple() })
RED.actions.add("core:wire-multiple-to-node", function() { wireMultipleToNode() })
RED.actions.add("core:split-wire-with-link-nodes", function () { splitWiresWithLinkNodes() });
RED.actions.add("core:split-wires-with-junctions", function () { addJunctionsToWires() });
RED.actions.add("core:generate-node-names", generateNodeNames )
RED.actions.add("core:copy-item-url", function (node) { copyItemUrl(node) })
RED.actions.add("core:copy-item-edit-url", function (node) { copyItemUrl(node, true) })
// RED.actions.add("core:add-node", function() { addNode() })
},
/**
* Aligns all selected nodes to the current grid
*/
alignSelectionToGrid: alignToGrid,
/**
* Moves all of the selected nodes by the specified amount
* @param {Number} dx
* @param {Number} dy
*/
moveSelection: moveSelection,
calculateGridSnapOffsets: calculateGridSnapOffsets,
isPointInNode: isPointInNode
}
})();