diff --git a/Gruntfile.js b/Gruntfile.js index cbf70f4dc..425784341 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -177,6 +177,7 @@ module.exports = function(grunt) { "packages/node_modules/@node-red/editor-client/src/js/ui/actionList.js", "packages/node_modules/@node-red/editor-client/src/js/ui/typeSearch.js", "packages/node_modules/@node-red/editor-client/src/js/ui/subflow.js", + "packages/node_modules/@node-red/editor-client/src/js/ui/group.js", "packages/node_modules/@node-red/editor-client/src/js/ui/userSettings.js", "packages/node_modules/@node-red/editor-client/src/js/ui/projects/projects.js", "packages/node_modules/@node-red/editor-client/src/js/ui/projects/projectSettings.js", diff --git a/packages/node_modules/@node-red/editor-client/src/js/keymap.json b/packages/node_modules/@node-red/editor-client/src/js/keymap.json index b7d25e160..c4d666845 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/keymap.json +++ b/packages/node_modules/@node-red/editor-client/src/js/keymap.json @@ -61,6 +61,7 @@ "shift-down": "core:step-selection-down", "shift-left": "core:step-selection-left", "ctrl-shift-j": "core:show-previous-tab", - "ctrl-shift-k": "core:show-next-tab" + "ctrl-shift-k": "core:show-next-tab", + "ctrl-shift-g": "core:group-selection" } } diff --git a/packages/node_modules/@node-red/editor-client/src/js/nodes.js b/packages/node_modules/@node-red/editor-client/src/js/nodes.js index cdf2c8796..2bf8d4a21 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/nodes.js +++ b/packages/node_modules/@node-red/editor-client/src/js/nodes.js @@ -27,6 +27,9 @@ RED.nodes = (function() { var subflows = {}; var loadedFlowVersion = null; + var groups = {}; + var groupsByZ = {}; + var initialLoad; var dirty = false; @@ -1444,6 +1447,14 @@ RED.nodes = (function() { // var loadedFlowVersion = null; } + function addGroup(group) { + groupsByZ[group.z] = groupsByZ[group.z] || []; + groupsByZ[group.z].push(group); + groups[group.id] = group; + } + + + return { init: function() { RED.events.on("registry:node-type-added",function(type) { @@ -1539,6 +1550,10 @@ RED.nodes = (function() { subflow: getSubflow, subflowContains: subflowContains, + addGroup: addGroup, + group: function(id) { return groups[id] }, + groups: function(z) { return groupsByZ[z] }, + eachNode: function(cb) { for (var n=0;n", { + class: "red-ui-tray-footer-left" + }).appendTo(trayFooter) + var trayBody = tray.find('.red-ui-tray-body'); + trayBody.parent().css('overflow','hidden'); + + var editorTabEl = $('').appendTo(trayBody); + var editorContent = $('
').appendTo(trayBody); + + var editorTabs = RED.tabs.create({ + element:editorTabEl, + onchange:function(tab) { + editorContent.children().hide(); + if (tab.onchange) { + tab.onchange.call(tab); + } + tab.content.show(); + if (finishedBuilding) { + RED.tray.resize(); + } + }, + collapsible: true, + menu: false + }); + + var nodePropertiesTab = { + id: "editor-tab-properties", + label: RED._("editor-tab.properties"), + name: RED._("editor-tab.properties"), + content: $('
', {class:"red-ui-tray-content"}).appendTo(editorContent).hide(), + iconClass: "fa fa-cog" + }; + buildEditForm(nodePropertiesTab.content,"dialog-form","group","node-red",group); + + editorTabs.addTab(nodePropertiesTab); + + var descriptionTab = { + id: "editor-tab-description", + label: RED._("editor-tab.description"), + name: RED._("editor-tab.description"), + content: $('
', {class:"red-ui-tray-content"}).appendTo(editorContent).hide(), + iconClass: "fa fa-file-text-o", + onchange: function() { + nodeInfoEditor.focus(); + } + }; + editorTabs.addTab(descriptionTab); + nodeInfoEditor = buildDescriptionForm(descriptionTab.content,editing_node); + prepareEditDialog(group,group._def,"node-input", function() { + trayBody.i18n(); + finishedBuilding = true; + done(); + }); + }, + close: function() { + if (RED.view.state() != RED.state.IMPORT_DRAGGING) { + RED.view.state(RED.state.DEFAULT); + } + nodeInfoEditor.destroy(); + nodeInfoEditor = null; + editStack.pop(); + editing_node = null; + }, + show: function() { + } + } + + if (editTrayWidthCache.hasOwnProperty('group')) { + trayOptions.width = editTrayWidthCache['group']; + } + RED.tray.show(trayOptions); + } + function showTypeEditor(type, options) { if (customEditTypes.hasOwnProperty(type)) { if (editStack.length > 0) { @@ -2576,6 +2821,7 @@ RED.editor = (function() { edit: showEditDialog, editConfig: showEditConfigNodeDialog, editSubflow: showEditSubflowDialog, + editGroup: showEditGroupDialog, editJavaScript: function(options) { showTypeEditor("_js",options) }, editExpression: function(options) { showTypeEditor("_expression", options) }, editJSON: function(options) { showTypeEditor("_json", options) }, diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/state.js b/packages/node_modules/@node-red/editor-client/src/js/ui/state.js index 1631ba87b..a2c9b762d 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/state.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/state.js @@ -25,5 +25,7 @@ RED.state = { IMPORT_DRAGGING: 8, QUICK_JOINING: 9, PANNING: 10, - SELECTING_NODE: 11 + SELECTING_NODE: 11, + GROUP_DRAGGING: 12, + GROUP_RESIZE: 13 } diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/view.js b/packages/node_modules/@node-red/editor-client/src/js/ui/view.js index 44f75b481..47e618611 100755 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/view.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/view.js @@ -21,12 +21,15 @@ * \- .red-ui-workspace-chart-event-layer "eventLayer" * |- .red-ui-workspace-chart-background * |- .red-ui-workspace-chart-grid "gridLayer" + * |- "groupLayer" * |- "linkLayer" * |- "dragGroupLayer" * \- "nodeLayer" */ RED.view = (function() { + var DEBUG_EVENTS = false; + 2 var space_width = 5000, space_height = 5000, lineCurveScale = 0.75, @@ -48,22 +51,29 @@ RED.view = (function() { var activeSpliceLink; var spliceActive = false; var spliceTimer; + var groupHoverTimer; var activeSubflow = null; var activeNodes = []; var activeLinks = []; var activeFlowLinks = []; var activeLinkNodes = {}; + var activeGroup = null; + var activeHoverGroup = null; + var activeGroups = []; + var dirtyGroups = {}; var selected_link = null, mousedown_link = null, mousedown_node = null, + mousedown_group = null, mousedown_port_type = null, mousedown_port_index = 0, mouseup_node = null, mouse_offset = [0,0], mouse_position = null, mouse_mode = 0, + mousedown_group_handle = null; moving_set = [], lasso = null, ghostNode = null, @@ -75,7 +85,8 @@ RED.view = (function() { scroll_position = [], quickAddActive = false, quickAddLink = null, - showAllLinkPorts = -1; + showAllLinkPorts = -1, + groupNodeSelectPrimed = false; var selectNodesOptions; @@ -101,6 +112,7 @@ RED.view = (function() { var linkLayer; var dragGroupLayer; var nodeLayer; + var groupLayer; var drag_lines; function init() { @@ -253,6 +265,7 @@ RED.view = (function() { gridLayer = eventLayer.append("g").attr("class","red-ui-workspace-chart-grid"); updateGrid(); + groupLayer = eventLayer.append("g"); linkLayer = eventLayer.append("g"); dragGroupLayer = eventLayer.append("g"); nodeLayer = eventLayer.append("g"); @@ -517,6 +530,8 @@ RED.view = (function() { source:{z:activeWorkspace}, target:{z:activeWorkspace} }); + + activeGroups = RED.nodes.groups(activeWorkspace)||[]; } function generateLinkPath(origX,origY, destX, destY, sc) { @@ -671,6 +686,7 @@ RED.view = (function() { } function canvasMouseDown() { +if (DEBUG_EVENTS) { console.warn("canvasMouseDown", mouse_mode); } var point; if (mouse_mode === RED.state.SELECTING_NODE) { d3.event.stopPropagation(); @@ -684,7 +700,7 @@ RED.view = (function() { scroll_position = [chart.scrollLeft(),chart.scrollTop()]; return; } - if (!mousedown_node && !mousedown_link) { + if (!mousedown_node && !mousedown_link && !mousedown_group) { selected_link = null; updateSelection(); } @@ -697,7 +713,13 @@ RED.view = (function() { if (mouse_mode === 0 || mouse_mode === RED.state.QUICK_JOINING) { if (d3.event.metaKey || d3.event.ctrlKey) { d3.event.stopPropagation(); - showQuickAddDialog(d3.mouse(this)); + clearSelection(); + point = d3.mouse(this); + var clickedGroup = getGroupAt(point[0],point[1]); + if (drag_lines.length > 0) { + clickedGroup = clickedGroup || RED.nodes.group(drag_lines[0].node.g) + } + showQuickAddDialog(point, null, clickedGroup); } } if (mouse_mode === 0 && !(d3.event.metaKey || d3.event.ctrlKey)) { @@ -718,7 +740,15 @@ RED.view = (function() { } } - function showQuickAddDialog(point,spliceLink) { + function showQuickAddDialog(point, spliceLink, targetGroup) { + if (targetGroup && !targetGroup.active) { + targetGroup.active = true; + targetGroup.dirty = true; + selectGroup(targetGroup,false); + activeGroup = targetGroup; + RED.view.redraw(); + } + var ox = point[0]; var oy = point[1]; @@ -946,6 +976,11 @@ RED.view = (function() { } } } + if (targetGroup) { + RED.group.addToGroup(targetGroup, nn); + + } + if (spliceLink) { resetMouseVars(); // TODO: DRY - droppable/nodeMouseDown/canvasMouseUp/showQuickAddDialog @@ -972,6 +1007,12 @@ RED.view = (function() { // auto select dropped node - so info shows (if visible) clearSelection(); nn.selected = true; + if (targetGroup) { + selectGroup(targetGroup,false); + targetGroup.active = true + targetGroup.dirty = true; + activeGroup = targetGroup; + } moving_set.push({n:nn}); updateActiveNodes(); updateSelection(); @@ -1076,12 +1117,23 @@ RED.view = (function() { return; } - if (mouse_mode != RED.state.QUICK_JOINING && mouse_mode != RED.state.IMPORT_DRAGGING && !mousedown_node && selected_link == null) { + if (mouse_mode != RED.state.QUICK_JOINING && mouse_mode != RED.state.IMPORT_DRAGGING && !mousedown_node && !mousedown_group && selected_link == null) { return; } var mousePos; - 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; + } else 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) { @@ -1201,6 +1253,13 @@ RED.view = (function() { 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 (snapGrid != d3.event.shiftKey && moving_set.length > 0) { var gridOffset = [0,0]; node = moving_set[0]; @@ -1265,6 +1324,29 @@ RED.view = (function() { },100); } } + if (!node.n.g && activeGroups) { + if (!groupHoverTimer) { + groupHoverTimer = setTimeout(function() { + activeHoverGroup = null; + 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; + g.hovered = true; + activeHoverGroup = g; + } else { + // Mark dirty if it is selected + g.dirty = g.hovered; + g.hovered = false; + } + } + groupHoverTimer = null; + },50); + } + } } @@ -1275,6 +1357,7 @@ RED.view = (function() { } function canvasMouseUp() { +if (DEBUG_EVENTS) { console.warn("canvasMouseUp", mouse_mode); } var i; var historyEvent; if (mouse_mode === RED.state.PANNING) { @@ -1311,15 +1394,22 @@ RED.view = (function() { var y = parseInt(lasso.attr("y")); var x2 = x+parseInt(lasso.attr("width")); var y2 = y+parseInt(lasso.attr("height")); - if (!d3.event.ctrlKey) { + if (!d3.event.shiftKey) { clearSelection(); } - RED.nodes.eachNode(function(n) { + activeNodes.forEach(function(n) { if (n.z == RED.workspaces.active() && !n.selected) { - n.selected = (n.x > x && n.x < x2 && n.y > y && n.y < y2); - if (n.selected) { - n.dirty = true; - moving_set.push({n:n}); + if (n.x > x && n.x < x2 && n.y > y && n.y < y2) { + if (n.g) { + var group = RED.nodes.group(n.g); + if (!group.selected) { + selectGroup(RED.nodes.group(n.g),true); + } + } else { + n.selected = true; + n.dirty = true; + moving_set.push({n:n}); + } } } }); @@ -1355,6 +1445,20 @@ RED.view = (function() { } if (mouse_mode == RED.state.MOVING_ACTIVE) { if (moving_set.length > 0) { + if (activeHoverGroup) { + for (var j=0;j 0 ? 1: 0) : (d.direction == "in" ? 0: 1) var wasJoining = false; if (mouse_mode === RED.state.JOINING || mouse_mode === RED.state.QUICK_JOINING) { @@ -2316,7 +2485,23 @@ RED.view = (function() { } } + function prepareDrag(mouse) { + mouse_mode = RED.state.MOVING; + // Called when moving_set should be prepared to be dragged + for (i=0;i 0 && clickElapsed < 750) { + mouse_mode = RED.state.DEFAULT; + RED.editor.editGroup(g); + d3.event.stopPropagation(); + return; + } + + } + 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 + } + + focusView(); + if (d3.event.button === 1) { + return; + } + if (mouse_mode == RED.state.IMPORT_DRAGGING) { + RED.keyboard.remove("escape"); + console.log("Dragged a node into the group") + } else if (mouse_mode == RED.state.QUICK_JOINING) { + d3.event.stopPropagation(); + return; + } else if (mouse_mode === RED.state.SELECTING_NODE) { + d3.event.stopPropagation(); + return; + } + + mousedown_group = g; + + var now = Date.now(); + clickElapsed = now-clickTime; + clickTime = now; + + dblClickPrimed = ( + lastClickNode == g && + d3.event.button === 0 && + !d3.event.shiftKey && !d3.event.metaKey && !d3.event.altKey && !d3.event.ctrlKey + ); + lastClickNode = g; + + if (g.selected && (d3.event.ctrlKey||d3.event.metaKey)) { + if (g === activeGroup) { + exitActiveGroup(); + } + deselectGroup(g); + d3.event.stopPropagation(); + } else { + if (!g.selected) { + if (!d3.event.ctrlKey && !d3.event.metaKey) { + clearSelection(); + } + selectGroup(g,true);//!wasSelected); + } else { + exitActiveGroup(); + } + + + 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]; + } + } + + updateSelection(); + redraw(); + d3.event.stopPropagation(); + } + + function selectGroup(g, includeNodes) { + if (!g.selected) { + g.selected = true; + g.dirty = true; + } + if (includeNodes) { + var currentSet = new Set(moving_set.map(function(n) { return n.n })); + g.nodes.forEach(function(n) { + if (!currentSet.has(n)) { + moving_set.push({n:n}) + // n.selected = true; + } + n.dirty = true; + }) + } + } + function exitActiveGroup() { + if (activeGroup) { + activeGroup.active = false; + activeGroup.dirty = true; + deselectGroup(activeGroup); + selectGroup(activeGroup,true); + activeGroup = null; + } + } + function deselectGroup(g) { + if (g.selected) { + g.selected = false; + g.dirty = true; + } + var nodeSet = new Set(g.nodes); + for (var i = moving_set.length-1; i >= 0; i -= 1) { + if (nodeSet.has(moving_set[i].n)) { + moving_set[i].n.selected = false; + moving_set[i].n.dirty = true; + moving_set.splice(i,1); + } + } + } + function getGroupAt(x,y) { + for (var i=0;i= g.pos.x0 && x <= g.pos.x1 && y >= g.pos.y0 && y <= g.pos.y1) { + return g; + } + } + return null; + } + + 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; var ws = RED.nodes.workspace(RED.workspaces.active()); @@ -3294,6 +3712,19 @@ RED.view = (function() { } 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) + } + } } }); @@ -3324,7 +3755,9 @@ RED.view = (function() { d3.event.stopPropagation(); if (d3.event.metaKey || d3.event.ctrlKey) { l.classed("red-ui-flow-link-splice",true); - showQuickAddDialog(d3.mouse(this), selected_link); + var point = d3.mouse(this); + var clickedGroup = getGroupAt(point[0],point[1]); + showQuickAddDialog(point, selected_link, clickedGroup); } }) .on("touchstart",function(d) { @@ -3509,6 +3942,112 @@ RED.view = (function() { }) + + var group = groupLayer.selectAll(".red-ui-flow-group").data(activeGroups,function(d) { return d.id }); + group.exit().remove(); + var groupEnter = group.enter().insert("svg:g") + .attr("class", "red-ui-flow-group") + + groupEnter.each(function(d,i) { + var g = d3.select(this); + + // g.append('path').classed("red-ui-flow-group-handle red-ui-flow-group-handle-0",true).style({ + // "fill":"white", + // "stroke": "#ff7f0e", + // "stroke-width": 2 + // }).attr("d","m -5 6 v -2 q 0 -9 9 -9 h 2 v 11 z") + // + // g.append('path').classed("red-ui-flow-group-handle red-ui-flow-group-handle-1",true).style({ + // "fill": "white", + // "stroke": "#ff7f0e", + // "stroke-width": 2 + // }).attr("d","m -6 -5 h 2 q 9 0 9 9 v 2 h -11 z") + // + // g.append('path').classed("red-ui-flow-group-handle red-ui-flow-group-handle-2",true).style({ + // "fill": "white", + // "stroke": "#ff7f0e", + // "stroke-width": 2 + // }).attr("d","m 5 -6 v 2 q 0 9 -9 9 h -2 v -11 z") + // + // g.append('path').classed("red-ui-flow-group-handle red-ui-flow-group-handle-3",true).style({ + // "fill": "white", + // "stroke": "#ff7f0e", + // "stroke-width": 2 + // }).attr("d","m 6 5 h -2 q -9 0 -9 -9 v -2 h 11 z") + + + + g.append('rect').classed("red-ui-flow-group-outline",true) + .attr('rx',1).attr('ry',1).style({ + "fill":"none", + "stroke": "#ff7f0e", + "stroke-opacity": 0, + "stroke-width": 15 + }) + g.append('rect').classed("red-ui-flow-group-body",true) + .attr('rx',1).attr('ry',1).style({ + "fill":d.fill||"none", + "stroke": d.stroke||"none", + "stroke-width": 2 + }) + + + d.dirty = true; + g.on("mousedown",groupMouseDown).on("mouseup",groupMouseUp) + + }); + group.each(function(d,i) { + if (d.dirty || dirtyGroups[d.id]) { + var g = d3.select(this); + var minX = Number.POSITIVE_INFINITY; + var minY = Number.POSITIVE_INFINITY; + 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); + }); + + d.pos = { + x0: minX, y0: minY, + x1: maxX, y1: maxY + } + + g.attr("transform","translate("+d.pos.x0+","+d.pos.y0+")"); + g.selectAll(".red-ui-flow-group-outline") + .attr("width",d.pos.x1-d.pos.x0) + .attr("height",d.pos.y1-d.pos.y0) + .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) + .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"}) + .style("fill-opacity", 0.1) + + // g.selectAll(".red-ui-flow-group-handle-0") + // .style("opacity",function(d) { return d.selected?1:0}) + // g.selectAll(".red-ui-flow-group-handle-1") + // .attr("transform","translate("+(maxX-minX)+",0)") + // .style("opacity",function(d) { return d.selected?1:0}) + // g.selectAll(".red-ui-flow-group-handle-2") + // .attr("transform","translate("+(maxX-minX)+","+(maxY-minY)+")") + // .style("opacity",function(d) { return d.selected?1:0}) + // g.selectAll(".red-ui-flow-group-handle-3") + // .attr("transform","translate(0,"+(maxY-minY)+")") + // .style("opacity",function(d) { return d.selected?1:0}) + + + delete dirtyGroups[d.id]; + delete d.dirty; + } + }) + + } else { // JOINING - unselect any selected links linkLayer.selectAll(".red-ui-flow-link-selected").data( @@ -3785,10 +4324,17 @@ RED.view = (function() { selectedNode.dirty = true; moving_set = [{n:selectedNode}]; } + } else if (selection) { + if (selection.groups) { + updateActiveNodes(); + selection.groups.forEach(function(g) { + selectGroup(g,true); + }) + } } } updateSelection(); - redraw(); + redraw(true); }, selection: function() { var selection = {}; @@ -3798,6 +4344,10 @@ RED.view = (function() { 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() {