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

[groups] Support nested groups in editor

This commit is contained in:
Nick O'Leary 2020-03-04 21:48:38 +00:00
parent 86ce5c591b
commit 97d58e34f2
No known key found for this signature in database
GPG Key ID: 4F2157149161A6C9
2 changed files with 356 additions and 104 deletions

View File

@ -0,0 +1,217 @@
/**
* 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.group = (function() {
var _groupEditTemplate = '<script type="text/x-red" data-template-name="group">'+
'<div class="form-row">'+
'<label for="node-input-name" data-i18n="[append]editor:common.label.name"><i class="fa fa-tag"></i> </label>'+
'<input type="text" id="node-input-name" data-i18n="[placeholder]common.label.name">'+
'</div>'+
'<div class="form-row">'+
'<label for="node-input-style-stroke" data-i18n="[append]editor:group.label.stroke">Stroke</label>'+
'<input type="text" id="node-input-style-stroke">'+
'</div>'+
'<div class="form-row">'+
'<label for="node-input-style-fill" data-i18n="[append]editor:group.label.fill">Fill</label>'+
'<input type="text" id="node-input-style-fill">'+
'</div>'+
'</script>';
var groupDef = {
defaults:{
name:{value:""},
style:{value:{}}
},
category: "config",
oneditprepare: function() {
var style = this.style || {};
$("#node-input-style-stroke").val(style.stroke || "#eeeeee")
$("#node-input-style-fill").val(style.fill || "none")
},
oneditresize: function(size) {
},
oneditsave: function() {
this.style.stroke = $("#node-input-style-stroke").val();
this.style.fill = $("#node-input-style-fill").val();
},
set:{
module: "node-red"
}
}
function init() {
RED.actions.add("core:group-selection", function() { groupSelection() })
$(_groupEditTemplate).appendTo("#red-ui-editor-node-configs");
}
function groupSelection() {
var selection = RED.view.selection();
// var groupNodes = new Set();
//
// if (selection.groups) {
// selection.groups.forEach(function(g) {
// g.nodes.forEach()
// })
// }
//
// if (selection.nodes) {
//
// }
if (selection.nodes) {
var group = createGroup(selection.nodes);
if (group) {
RED.view.select({groups:[group]})
}
}
}
function createGroup(nodes) {
if (nodes.length === 0) {
return;
}
// nodes is an array
// each node must be on the same tab (z)
var group = {
id: RED.nodes.id(),
type: 'group',
nodes: [],
style: {
stroke: "#999",
fill: "none"
},
x: Number.POSITIVE_INFINITY,
y: Number.POSITIVE_INFINITY,
w: 0,
h: 0,
_def: RED.group.def
}
try {
addToGroup(group,nodes);
} catch(err) {
RED.notify(err,"error");
return;
}
group.z = nodes[0].z;
RED.nodes.addGroup(group);
return group;
}
function addToGroup(group,nodes) {
if (!Array.isArray(nodes)) {
nodes = [nodes];
}
var i,n,z;
var g;
// First pass - validate we can safely add these nodes to the group
for (i=0;i<nodes.length;i++) {
n = nodes[i]
if (!n.z) {
throw new Error("Cannot add node without a z property to a group")
}
if (!z) {
z = n.z;
} else if (z !== n.z) {
throw new Error("Cannot create group using nodes with different z properties")
}
if (n.g) {
// This is already in a group.
// - check they are all in the same group
if (!g) {
if (i!==0) {
// TODO: this might be ok when merging groups
throw new Error("Cannot create group using nodes from different groups")
}
g = n.g
}
}
if (g !== n.g) {
throw new Error("Cannot create group using nodes from different groups")
}
}
if (g) {
g = RED.nodes.group(g);
g.nodes.push(group);
group.g = g.id;
}
// Second pass - add them to the group
for (i=0;i<nodes.length;i++) {
n = nodes[i];
if (g && n.g === g.id) {
var ni = g.nodes.indexOf(n);
if (ni > -1) {
g.nodes.splice(ni,1)
}
}
n.g = group.id;
group.nodes.push(n);
group.x = Math.min(group.x,n.x-n.w/2-25-((n._def.button && n._def.align!=="right")?20:0));
group.y = Math.min(group.y,n.y-n.h/2-25);
group.w = Math.max(group.w,n.x+n.w/2+25+((n._def.button && n._def.align=="right")?20:0) - group.x),
group.h = Math.max(group.h,n.y+n.h/2+25-group.y);
}
}
function getNodes(group,recursive) {
var nodes = [];
group.nodes.forEach(function(n) {
if (!recursive || n.type !== 'group') {
nodes.push(n);
} else {
nodes = nodes.concat(getNodes(n,recursive))
}
})
return nodes;
}
function groupContains(group,item) {
if (item.g === group.id) {
return true;
}
for (var i=0;i<group.nodes.length;i++) {
if (group.nodes[i].type === "group") {
if (groupContains(group.nodes[i],item)) {
return true;
}
}
}
return false;
}
function getRootGroup(group) {
if (!group.g) {
return group;
}
return getRootGroup(RED.nodes.group(group.g))
}
return {
def: groupDef,
init: init,
createGroup: createGroup,
addToGroup: addToGroup,
getNodes: getNodes,
contains: groupContains
}
})();

View File

@ -1122,18 +1122,19 @@ if (DEBUG_EVENTS) { console.warn("canvasMouseDown", mouse_mode); }
} }
var mousePos; var mousePos;
if (mouse_mode === RED.state.GROUP_RESIZE) { // if (mouse_mode === RED.state.GROUP_RESIZE) {
mousePos = mouse_position; // mousePos = mouse_position;
var nx = mousePos[0] + mousedown_group.dx; // var nx = mousePos[0] + mousedown_group.dx;
var ny = mousePos[1] + mousedown_group.dy; // var ny = mousePos[1] + mousedown_group.dy;
switch(mousedown_group.activeHandle) { // switch(mousedown_group.activeHandle) {
case 0: mousedown_group.pos.x0 = nx; mousedown_group.pos.y0 = ny; break; // case 0: mousedown_group.pos.x0 = nx; mousedown_group.pos.y0 = ny; break;
case 1: mousedown_group.pos.x1 = nx; mousedown_group.pos.y0 = ny; break; // case 1: mousedown_group.pos.x1 = nx; mousedown_group.pos.y0 = ny; break;
case 2: mousedown_group.pos.x1 = nx; mousedown_group.pos.y1 = ny; break; // case 2: mousedown_group.pos.x1 = nx; mousedown_group.pos.y1 = ny; break;
case 3: mousedown_group.pos.x0 = nx; mousedown_group.pos.y1 = ny; break; // case 3: mousedown_group.pos.x0 = nx; mousedown_group.pos.y1 = ny; break;
} // }
mousedown_group.dirty = true; // mousedown_group.dirty = true;
} else if (mouse_mode == RED.state.JOINING || mouse_mode === RED.state.QUICK_JOINING) { // }
if (mouse_mode == RED.state.JOINING || mouse_mode === RED.state.QUICK_JOINING) {
// update drag line // update drag line
if (drag_lines.length === 0 && mousedown_port_type !== null) { if (drag_lines.length === 0 && mousedown_port_type !== null) {
if (d3.event.shiftKey) { if (d3.event.shiftKey) {
@ -1253,13 +1254,11 @@ if (DEBUG_EVENTS) { console.warn("canvasMouseDown", mouse_mode); }
node.n.y -= (maxY - space_height); node.n.y -= (maxY - space_height);
} }
} }
if (mousedown_group) { // if (mousedown_group) {
mousedown_group.pos.x0 = mousePos[0] + mousedown_group.dx0; // mousedown_group.x = mousePos[0] + mousedown_group.dx;
mousedown_group.pos.y0 = mousePos[1] + mousedown_group.dy0; // mousedown_group.y = mousePos[1] + mousedown_group.dy;
mousedown_group.pos.x1 = mousePos[0] + mousedown_group.dx1; // mousedown_group.dirty = true;
mousedown_group.pos.y1 = mousePos[1] + mousedown_group.dy1; // }
mousedown_group.dirty = true;
}
if (snapGrid != d3.event.shiftKey && moving_set.length > 0) { if (snapGrid != d3.event.shiftKey && moving_set.length > 0) {
var gridOffset = [0,0]; var gridOffset = [0,0];
node = moving_set[0]; node = moving_set[0];
@ -1327,20 +1326,15 @@ if (DEBUG_EVENTS) { console.warn("canvasMouseDown", mouse_mode); }
if (!node.n.g && activeGroups) { if (!node.n.g && activeGroups) {
if (!groupHoverTimer) { if (!groupHoverTimer) {
groupHoverTimer = setTimeout(function() { groupHoverTimer = setTimeout(function() {
activeHoverGroup = null; activeHoverGroup = getGroupAt(node.n.x,node.n.y);
for (var i=0;i<activeGroups.length;i++) { for (var i=0;i<activeGroups.length;i++) {
var g = activeGroups[i]; var g = activeGroups[i];
if ( !activeHoverGroup && if (g === activeHoverGroup) {
node.n.x >= g.pos.x0 && node.n.x <= g.pos.x1 &&
node.n.y >= g.pos.y0 && node.n.y <= g.pos.y1
) {
g.dirty = !g.hovered;
g.hovered = true; g.hovered = true;
activeHoverGroup = g; g.dirty = true;
} else { } else if (g.hovered) {
// Mark dirty if it is selected
g.dirty = g.hovered;
g.hovered = false; g.hovered = false;
g.dirty = true;
} }
} }
groupHoverTimer = null; groupHoverTimer = null;
@ -2714,11 +2708,12 @@ if (DEBUG_EVENTS) { console.warn("nodeMouseDown", mouse_mode,d); }
} }
} }
function groupMouseDown(g) { function groupMouseDown(g) {
var mouse = d3.touches(this.parentNode)[0]||d3.mouse(this.parentNode); var mouse = d3.touches(this.parentNode)[0]||d3.mouse(this.parentNode);
if (! (mouse[0] < g.pos.x0+10 || mouse[0] > g.pos.x1-10 || mouse[1] < g.pos.y0+10 || mouse[1] > g.pos.y1-10) ) { // if (! (mouse[0] < g.x+10 || mouse[0] > g.x+g.w-10 || mouse[1] < g.y+10 || mouse[1] > g.y+g.h-10) ) {
return // return
} // }
focusView(); focusView();
if (d3.event.button === 1) { if (d3.event.button === 1) {
@ -2768,10 +2763,8 @@ if (DEBUG_EVENTS) { console.warn("nodeMouseDown", mouse_mode,d); }
if (d3.event.button != 2) { if (d3.event.button != 2) {
var d = g.nodes[0]; var d = g.nodes[0];
prepareDrag(mouse); prepareDrag(mouse);
mousedown_group.dx0 = mousedown_group.pos.x0 - mouse[0]; mousedown_group.dx = mousedown_group.x - mouse[0];
mousedown_group.dy0 = mousedown_group.pos.y0 - mouse[1]; mousedown_group.dy = mousedown_group.y - mouse[1];
mousedown_group.dx1 = mousedown_group.pos.x1 - mouse[0];
mousedown_group.dy1 = mousedown_group.pos.y1 - mouse[1];
} }
} }
@ -2787,7 +2780,8 @@ if (DEBUG_EVENTS) { console.warn("nodeMouseDown", mouse_mode,d); }
} }
if (includeNodes) { if (includeNodes) {
var currentSet = new Set(moving_set.map(function(n) { return n.n })); var currentSet = new Set(moving_set.map(function(n) { return n.n }));
g.nodes.forEach(function(n) { var allNodes = RED.group.getNodes(g,true);
allNodes.forEach(function(n) {
if (!currentSet.has(n)) { if (!currentSet.has(n)) {
moving_set.push({n:n}) moving_set.push({n:n})
// n.selected = true; // n.selected = true;
@ -2820,48 +2814,62 @@ if (DEBUG_EVENTS) { console.warn("nodeMouseDown", mouse_mode,d); }
} }
} }
function getGroupAt(x,y) { function getGroupAt(x,y) {
var candidateGroups = {};
for (var i=0;i<activeGroups.length;i++) { for (var i=0;i<activeGroups.length;i++) {
var g = activeGroups[i]; var g = activeGroups[i];
if (x >= g.pos.x0 && x <= g.pos.x1 && y >= g.pos.y0 && y <= g.pos.y1) { if (x >= g.x && x <= g.x + g.w && y >= g.y && y <= g.y + g.h) {
return g; candidateGroups[g.id] = g;
} }
} }
var ids = Object.keys(candidateGroups);
if (ids.length > 1) {
ids.forEach(function(id) {
if (candidateGroups[id] && candidateGroups[id].g) {
delete candidateGroups[candidateGroups[id].g]
}
})
ids = Object.keys(candidateGroups);
}
if (ids.length === 0) {
return null; return null;
} else {
return candidateGroups[ids[0]]
}
} }
function groupHandleMouseDown(group, groupEl, handle,handleIndex) { // function groupHandleMouseDown(group, groupEl, handle,handleIndex) {
d3.event.stopPropagation(); // d3.event.stopPropagation();
console.log("GHANDLE MD"); // console.log("GHANDLE MD");
if (d3.event.button != 2) { // if (d3.event.button != 2) {
mousedown_group = group; // mousedown_group = group;
group.activeHandle = handleIndex; // group.activeHandle = handleIndex;
var mouse = d3.touches(handle.parentNode.parentNode)[0]||d3.mouse(handle.parentNode.parentNode); // var mouse = d3.touches(handle.parentNode.parentNode)[0]||d3.mouse(handle.parentNode.parentNode);
switch(handleIndex) { // switch(handleIndex) {
case 0: group.ox = group.pos.x0; group.oy = group.pos.y0; break; // case 0: group.ox = group.pos.x0; group.oy = group.pos.y0; break;
case 1: group.ox = group.pos.x1; group.oy = group.pos.y0; break; // case 1: group.ox = group.pos.x1; group.oy = group.pos.y0; break;
case 2: group.ox = group.pos.x1; group.oy = group.pos.y1; break; // case 2: group.ox = group.pos.x1; group.oy = group.pos.y1; break;
case 3: group.ox = group.pos.x0; group.oy = group.pos.y1; break; // case 3: group.ox = group.pos.x0; group.oy = group.pos.y1; break;
} // }
group.dx = group.ox - mouse[0]; // group.dx = group.ox - mouse[0];
group.dy = group.oy - mouse[1]; // group.dy = group.oy - mouse[1];
console.log("START",group.ox, group.oy); // console.log("START",group.ox, group.oy);
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];
} // }
mouse_mode = RED.state.GROUP_RESIZE; // mouse_mode = RED.state.GROUP_RESIZE;
} // }
} // }
function groupHandleMouseUp(group,groupEl,handle,handleIndex) { // function groupHandleMouseUp(group,groupEl,handle,handleIndex) {
console.log("GHANDLE MU"); // console.log("GHANDLE MU");
d3.event.stopPropagation(); // d3.event.stopPropagation();
delete group.ox; // delete group.ox;
delete group.oy; // delete group.oy;
delete group.dx; // delete group.dx;
delete group.dy; // delete group.dy;
resetMouseVars(); // resetMouseVars();
mouse_mode = RED.state.DEFAULT; // mouse_mode = RED.state.DEFAULT;
} // }
function isButtonEnabled(d) { function isButtonEnabled(d) {
var buttonEnabled = true; var buttonEnabled = true;
@ -3712,18 +3720,19 @@ if (DEBUG_EVENTS) { console.warn("nodeMouseDown", mouse_mode,d); }
} }
d.dirty = false; d.dirty = false;
if (d.g) { if (d.g) {
if (!dirtyGroups[d.g]) { if (!dirtyGroups[d.g]) {
dirtyGroups[d.g] = RED.nodes.group(d.g); var gg = d.g;
while (gg && !dirtyGroups[gg]) {
dirtyGroups[gg] = RED.nodes.group(gg);
gg = dirtyGroups[gg].g;
} }
var group = dirtyGroups[d.g];
group.pos = {
x0: Math.min(group.pos.x0,d.x-d.w/2-25-((d._def.button && d._def.align!=="right")?20:0)),
y0: Math.min(group.pos.y0,d.y-d.h/2-25),
x1: Math.max(group.pos.x1,d.x+d.w/2+25+((d._def.button && d._def.align=="right")?20:0)),
y1: Math.max(group.pos.y1,d.y+d.h/2+25)
} }
// var group = dirtyGroups[d.g];
// group.x = Math.min(group.x,d.x-d.w/2-25-((d._def.button && d._def.align!=="right")?20:0));
// group.y = Math.min(group.y,d.y-d.h/2-25),
// group.w = Math.max(group.w, d.x+d.w/2+25+((d._def.button && d._def.align=="right")?20:0) - group.x),
// group.h = Math.max(group.h, d.y+d.h/2+25 - group.y)
} }
} }
}); });
@ -3981,20 +3990,21 @@ if (DEBUG_EVENTS) { console.warn("nodeMouseDown", mouse_mode,d); }
.attr('rx',1).attr('ry',1).style({ .attr('rx',1).attr('ry',1).style({
"fill":"none", "fill":"none",
"stroke": "#ff7f0e", "stroke": "#ff7f0e",
"pointer-events": "stroke",
"stroke-opacity": 0, "stroke-opacity": 0,
"stroke-width": 15 "stroke-width": 15
}) })
g.append('rect').classed("red-ui-flow-group-body",true) g.append('rect').classed("red-ui-flow-group-body",true)
.attr('rx',1).attr('ry',1).style({ .attr('rx',1).attr('ry',1).style({
"pointer-events": "none",
"fill":d.fill||"none", "fill":d.fill||"none",
"stroke": d.stroke||"none", "stroke": d.stroke||"none",
"stroke-width": 2 "stroke-width": 2
}) })
d.dirty = true;
g.on("mousedown",groupMouseDown).on("mouseup",groupMouseUp) g.on("mousedown",groupMouseDown).on("mouseup",groupMouseUp)
d.dirty = true;
}); });
group.each(function(d,i) { group.each(function(d,i) {
if (d.dirty || dirtyGroups[d.id]) { if (d.dirty || dirtyGroups[d.id]) {
@ -4004,26 +4014,33 @@ if (DEBUG_EVENTS) { console.warn("nodeMouseDown", mouse_mode,d); }
var maxX = 0; var maxX = 0;
var maxY = 0; var maxY = 0;
d.nodes.forEach(function(n) { d.nodes.forEach(function(n) {
if (n.type !== "group") {
minX = Math.min(minX,n.x-n.w/2-25-((n._def.button && n._def.align!=="right")?20:0)); minX = Math.min(minX,n.x-n.w/2-25-((n._def.button && n._def.align!=="right")?20:0));
minY = Math.min(minY,n.y-n.h/2-25); minY = Math.min(minY,n.y-n.h/2-25);
maxX = Math.max(maxX,n.x+n.w/2+25+((n._def.button && n._def.align=="right")?20:0)); maxX = Math.max(maxX,n.x+n.w/2+25+((n._def.button && n._def.align=="right")?20:0));
maxY = Math.max(maxY,n.y+n.h/2+25); maxY = Math.max(maxY,n.y+n.h/2+25);
} else {
minX = Math.min(minX,n.x-25)
minY = Math.min(minY,n.y-25)
maxX = Math.max(maxX,n.x+n.w+25)
maxY = Math.max(maxY,n.y+n.h+25)
}
}); });
d.pos = { d.x = minX;
x0: minX, y0: minY, d.y = minY;
x1: maxX, y1: maxY d.w = maxX - minX;
} d.h = maxY - minY;
g.attr("transform","translate("+d.pos.x0+","+d.pos.y0+")"); g.attr("transform","translate("+d.x+","+d.y+")");
g.selectAll(".red-ui-flow-group-outline") g.selectAll(".red-ui-flow-group-outline")
.attr("width",d.pos.x1-d.pos.x0) .attr("width",d.w)
.attr("height",d.pos.y1-d.pos.y0) .attr("height",d.h)
.style("stroke-opacity",function(d) { if (d.selected) { return 0.3 } return 0}); .style("stroke-opacity",function(d) { if (d.selected) { return 0.3 } return 0});
g.selectAll(".red-ui-flow-group-body") g.selectAll(".red-ui-flow-group-body")
.attr("width",d.pos.x1-d.pos.x0) .attr("width",d.w)
.attr("height",d.pos.y1-d.pos.y0) .attr("height",d.h)
.style("stroke",function(d) { /*if (d.selected) { return "#ff7f0e" } */return d.style.stroke || "none"}) .style("stroke",function(d) { /*if (d.selected) { return "#ff7f0e" } */return d.style.stroke || "none"})
.style("stroke-dasharray", function(d) { return (d.active||d.hovered)?"10 4":"none"}) .style("stroke-dasharray", function(d) { return (d.active||d.hovered)?"10 4":"none"})
.style("fill", function(d) { return d.style.fill || "none"}) .style("fill", function(d) { return d.style.fill || "none"})
@ -4338,16 +4355,34 @@ if (DEBUG_EVENTS) { console.warn("nodeMouseDown", mouse_mode,d); }
}, },
selection: function() { selection: function() {
var selection = {}; var selection = {};
var allNodes = new Set();
if (moving_set.length > 0) { if (moving_set.length > 0) {
selection.nodes = moving_set.map(function(n) { return n.n;}); moving_set.forEach(function(n) {
allNodes.add(n.n);
});
}
var selectedGroups = activeGroups.filter(function(g) { return g.selected });
if (selectedGroups.length > 0) {
if (selectedGroups.length === 1 && selectedGroups[0].active) {
// Let nodes be nodes
} else {
selectedGroups.forEach(function(g) {
var groupNodes = RED.group.getNodes(g,true);
groupNodes.forEach(function(n) {
allNodes.delete(n);
});
allNodes.add(g);
});
}
}
if (allNodes.size > 0) {
selection.nodes = Array.from(allNodes);
} }
if (selected_link != null) { if (selected_link != null) {
selection.link = selected_link; selection.link = selected_link;
} }
selection.groups = activeGroups.filter(function(g) { return g.selected })
if (selection.groups.length === 0) {
delete selection.groups;
}
return selection; return selection;
}, },
scale: function() { scale: function() {