From 97d58e34f2818fb1f7c958c7a5e31ada07c7dc15 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Wed, 4 Mar 2020 21:48:38 +0000 Subject: [PATCH] [groups] Support nested groups in editor --- .../editor-client/src/js/ui/group.js | 217 ++++++++++++++++ .../@node-red/editor-client/src/js/ui/view.js | 243 ++++++++++-------- 2 files changed, 356 insertions(+), 104 deletions(-) create mode 100644 packages/node_modules/@node-red/editor-client/src/js/ui/group.js 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 new file mode 100644 index 000000000..7c9bd67f9 --- /dev/null +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/group.js @@ -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 = ''; + + 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 -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 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= 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= 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() {