[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;
if (mouse_mode === RED.state.GROUP_RESIZE) {
mousePos = mouse_position;
var nx = mousePos[0] + mousedown_group.dx;
var ny = mousePos[1] + mousedown_group.dy;
switch(mousedown_group.activeHandle) {
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 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;
}
mousedown_group.dirty = true;
} else if (mouse_mode == RED.state.JOINING || mouse_mode === RED.state.QUICK_JOINING) {
// if (mouse_mode === RED.state.GROUP_RESIZE) {
// mousePos = mouse_position;
// var nx = mousePos[0] + mousedown_group.dx;
// var ny = mousePos[1] + mousedown_group.dy;
// switch(mousedown_group.activeHandle) {
// 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 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;
// }
// mousedown_group.dirty = true;
// }
if (mouse_mode == RED.state.JOINING || mouse_mode === RED.state.QUICK_JOINING) {
// update drag line
if (drag_lines.length === 0 && mousedown_port_type !== null) {
if (d3.event.shiftKey) {
@ -1253,13 +1254,11 @@ if (DEBUG_EVENTS) { console.warn("canvasMouseDown", mouse_mode); }
node.n.y -= (maxY - space_height);
}
}
if (mousedown_group) {
mousedown_group.pos.x0 = mousePos[0] + mousedown_group.dx0;
mousedown_group.pos.y0 = mousePos[1] + mousedown_group.dy0;
mousedown_group.pos.x1 = mousePos[0] + mousedown_group.dx1;
mousedown_group.pos.y1 = mousePos[1] + mousedown_group.dy1;
mousedown_group.dirty = true;
}
// if (mousedown_group) {
// mousedown_group.x = mousePos[0] + mousedown_group.dx;
// mousedown_group.y = mousePos[1] + mousedown_group.dy;
// mousedown_group.dirty = true;
// }
if (snapGrid != d3.event.shiftKey && moving_set.length > 0) {
var gridOffset = [0,0];
node = moving_set[0];
@ -1327,20 +1326,15 @@ if (DEBUG_EVENTS) { console.warn("canvasMouseDown", mouse_mode); }
if (!node.n.g && activeGroups) {
if (!groupHoverTimer) {
groupHoverTimer = setTimeout(function() {
activeHoverGroup = null;
activeHoverGroup = getGroupAt(node.n.x,node.n.y);
for (var i=0;i<activeGroups.length;i++) {
var g = activeGroups[i];
if ( !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;
if (g === activeHoverGroup) {
g.hovered = true;
activeHoverGroup = g;
} else {
// Mark dirty if it is selected
g.dirty = g.hovered;
g.dirty = true;
} else if (g.hovered) {
g.hovered = false;
g.dirty = true;
}
}
groupHoverTimer = null;
@ -1972,7 +1966,7 @@ if (DEBUG_EVENTS) { console.warn("clearSelection", mouse_mode); }
RED.notify(RED._("clipboard.nodeCopied",{count:nns.length}),{id:"clipboard"});
}
}
function calculateTextWidth(str, className, offset) {
var result=convertLineBreakCharacter(str);
var width = 0;
@ -2714,11 +2708,12 @@ if (DEBUG_EVENTS) { console.warn("nodeMouseDown", mouse_mode,d); }
}
}
function groupMouseDown(g) {
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) ) {
return
}
// 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
// }
focusView();
if (d3.event.button === 1) {
@ -2768,10 +2763,8 @@ if (DEBUG_EVENTS) { console.warn("nodeMouseDown", mouse_mode,d); }
if (d3.event.button != 2) {
var d = g.nodes[0];
prepareDrag(mouse);
mousedown_group.dx0 = mousedown_group.pos.x0 - mouse[0];
mousedown_group.dy0 = mousedown_group.pos.y0 - mouse[1];
mousedown_group.dx1 = mousedown_group.pos.x1 - mouse[0];
mousedown_group.dy1 = mousedown_group.pos.y1 - mouse[1];
mousedown_group.dx = mousedown_group.x - mouse[0];
mousedown_group.dy = mousedown_group.y - mouse[1];
}
}
@ -2787,7 +2780,8 @@ if (DEBUG_EVENTS) { console.warn("nodeMouseDown", mouse_mode,d); }
}
if (includeNodes) {
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)) {
moving_set.push({n:n})
// n.selected = true;
@ -2820,48 +2814,62 @@ if (DEBUG_EVENTS) { console.warn("nodeMouseDown", mouse_mode,d); }
}
}
function getGroupAt(x,y) {
var candidateGroups = {};
for (var i=0;i<activeGroups.length;i++) {
var g = activeGroups[i];
if (x >= g.pos.x0 && x <= g.pos.x1 && y >= g.pos.y0 && y <= g.pos.y1) {
return g;
if (x >= g.x && x <= g.x + g.w && y >= g.y && y <= g.y + g.h) {
candidateGroups[g.id] = g;
}
}
return null;
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;
} else {
return candidateGroups[ids[0]]
}
}
function groupHandleMouseDown(group, groupEl, handle,handleIndex) {
d3.event.stopPropagation();
console.log("GHANDLE MD");
if (d3.event.button != 2) {
mousedown_group = group;
group.activeHandle = handleIndex;
var mouse = d3.touches(handle.parentNode.parentNode)[0]||d3.mouse(handle.parentNode.parentNode);
switch(handleIndex) {
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 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;
}
group.dx = group.ox - mouse[0];
group.dy = group.oy - mouse[1];
console.log("START",group.ox, group.oy);
mouse_offset = d3.mouse(document.body);
if (isNaN(mouse_offset[0])) {
mouse_offset = d3.touches(document.body)[0];
}
mouse_mode = RED.state.GROUP_RESIZE;
}
}
function groupHandleMouseUp(group,groupEl,handle,handleIndex) {
console.log("GHANDLE MU");
d3.event.stopPropagation();
delete group.ox;
delete group.oy;
delete group.dx;
delete group.dy;
resetMouseVars();
mouse_mode = RED.state.DEFAULT;
}
// function groupHandleMouseDown(group, groupEl, handle,handleIndex) {
// d3.event.stopPropagation();
// console.log("GHANDLE MD");
// if (d3.event.button != 2) {
// mousedown_group = group;
// group.activeHandle = handleIndex;
// var mouse = d3.touches(handle.parentNode.parentNode)[0]||d3.mouse(handle.parentNode.parentNode);
// switch(handleIndex) {
// 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 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;
// }
// group.dx = group.ox - mouse[0];
// group.dy = group.oy - mouse[1];
// console.log("START",group.ox, group.oy);
// mouse_offset = d3.mouse(document.body);
// if (isNaN(mouse_offset[0])) {
// mouse_offset = d3.touches(document.body)[0];
// }
// mouse_mode = RED.state.GROUP_RESIZE;
// }
// }
// function groupHandleMouseUp(group,groupEl,handle,handleIndex) {
// console.log("GHANDLE MU");
// d3.event.stopPropagation();
// delete group.ox;
// delete group.oy;
// delete group.dx;
// delete group.dy;
// resetMouseVars();
// mouse_mode = RED.state.DEFAULT;
// }
function isButtonEnabled(d) {
var buttonEnabled = true;
@ -3712,18 +3720,19 @@ if (DEBUG_EVENTS) { console.warn("nodeMouseDown", mouse_mode,d); }
}
d.dirty = false;
if (d.g) {
if (!dirtyGroups[d.g]) {
dirtyGroups[d.g] = RED.nodes.group(d.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 gg = d.g;
while (gg && !dirtyGroups[gg]) {
dirtyGroups[gg] = RED.nodes.group(gg);
gg = dirtyGroups[gg].g;
}
}
// 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({
"fill":"none",
"stroke": "#ff7f0e",
"pointer-events": "stroke",
"stroke-opacity": 0,
"stroke-width": 15
})
g.append('rect').classed("red-ui-flow-group-body",true)
.attr('rx',1).attr('ry',1).style({
"pointer-events": "none",
"fill":d.fill||"none",
"stroke": d.stroke||"none",
"stroke-width": 2
})
d.dirty = true;
g.on("mousedown",groupMouseDown).on("mouseup",groupMouseUp)
d.dirty = true;
});
group.each(function(d,i) {
if (d.dirty || dirtyGroups[d.id]) {
@ -4004,26 +4014,33 @@ if (DEBUG_EVENTS) { console.warn("nodeMouseDown", mouse_mode,d); }
var maxX = 0;
var maxY = 0;
d.nodes.forEach(function(n) {
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);
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);
if (n.type !== "group") {
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);
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);
} 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 = {
x0: minX, y0: minY,
x1: maxX, y1: maxY
}
d.x = minX;
d.y = minY;
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")
.attr("width",d.pos.x1-d.pos.x0)
.attr("height",d.pos.y1-d.pos.y0)
.attr("width",d.w)
.attr("height",d.h)
.style("stroke-opacity",function(d) { if (d.selected) { return 0.3 } return 0});
g.selectAll(".red-ui-flow-group-body")
.attr("width",d.pos.x1-d.pos.x0)
.attr("height",d.pos.y1-d.pos.y0)
.attr("width",d.w)
.attr("height",d.h)
.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("fill", function(d) { return d.style.fill || "none"})
@ -4338,16 +4355,34 @@ if (DEBUG_EVENTS) { console.warn("nodeMouseDown", mouse_mode,d); }
},
selection: function() {
var selection = {};
var allNodes = new Set();
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) {
selection.link = selected_link;
}
selection.groups = activeGroups.filter(function(g) { return g.selected })
if (selection.groups.length === 0) {
delete selection.groups;
}
return selection;
},
scale: function() {