From 07f2f0cef3c36429b1cfd1ddb4d9896c4d902783 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Wed, 1 Mar 2023 10:02:26 +0000 Subject: [PATCH] Complete overhaul of Group UX --- .../@node-red/editor-client/src/js/history.js | 9 +- .../@node-red/editor-client/src/js/nodes.js | 219 ++- .../editor-client/src/js/ui/contextMenu.js | 15 +- .../editor-client/src/js/ui/group.js | 30 +- .../editor-client/src/js/ui/view-tools.js | 5 +- .../@node-red/editor-client/src/js/ui/view.js | 1197 +++++++---------- .../editor-client/src/sass/flow.scss | 33 +- 7 files changed, 645 insertions(+), 863 deletions(-) diff --git a/packages/node_modules/@node-red/editor-client/src/js/history.js b/packages/node_modules/@node-red/editor-client/src/js/history.js index 977ecb187..6741470af 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/history.js +++ b/packages/node_modules/@node-red/editor-client/src/js/history.js @@ -378,7 +378,8 @@ RED.history = (function() { if (ev.addToGroup) { RED.group.removeFromGroup(ev.addToGroup,ev.nodes.map(function(n) { return n.n }),false); inverseEv.removeFromGroup = ev.addToGroup; - } else if (ev.removeFromGroup) { + } + if (ev.removeFromGroup) { RED.group.addToGroup(ev.removeFromGroup,ev.nodes.map(function(n) { return n.n })); inverseEv.addToGroup = ev.removeFromGroup; } @@ -648,6 +649,12 @@ RED.history = (function() { ev.groups[i].nodes = []; RED.nodes.addGroup(ev.groups[i]); RED.group.addToGroup(ev.groups[i],nodes); + if (ev.groups[i].g) { + const parentGroup = RED.nodes.group(ev.groups[i].g) + if (parentGroup) { + RED.group.addToGroup(parentGroup, ev.groups[i]) + } + } } } } else if (ev.t == "addToGroup") { 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 36bc2a7fa..904c021c1 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 @@ -68,7 +68,6 @@ RED.nodes = (function() { } }; - var exports = { setModulePendingUpdated: function(module,version) { moduleList[module].pending_version = version; @@ -240,6 +239,43 @@ RED.nodes = (function() { var allNodes = (function() { var nodes = {}; var tabMap = {}; + + function changeCollectionDepth(tabNodes, toMove, direction, singleStep) { + const result = [] + const moved = new Set(); + const startIndex = direction ? tabNodes.length - 1 : 0 + const endIndex = direction ? -1 : tabNodes.length + const step = direction ? -1 : 1 + let target = startIndex // Only used for all-the-way moves + for (let i = startIndex; i != endIndex; i += step) { + if (toMove.size === 0) { + break; + } + const n = tabNodes[i] + if (toMove.has(n)) { + if (singleStep) { + if (i !== startIndex && !moved.has(tabNodes[i - step])) { + tabNodes.splice(i, 1) + tabNodes.splice(i - step, 0, n) + n._reordered = true + result.push(n) + } + } else { + if (i !== target) { + tabNodes.splice(i, 1) + tabNodes.splice(target, 0, n) + n._reordered = true + result.push(n) + } + target += step + } + toMove.delete(n); + moved.add(n); + } + } + return result + } + var api = { addTab: function(id) { tabMap[id] = []; @@ -280,152 +316,54 @@ RED.nodes = (function() { n.z = newZ; api.addNode(n) }, - moveNodesForwards: function(nodes) { - var result = []; + /** + * @param {array} nodes + * @param {boolean} direction true:forwards false:back + * @param {boolean} singleStep true:single-step false:all-the-way + */ + changeDepth: function(nodes, direction, singleStep) { if (!Array.isArray(nodes)) { nodes = [nodes] } - // Can only do this for nodes on the same tab. - // Use nodes[0] to get the z - var tabNodes = tabMap[nodes[0].z]; - var toMove = new Set(nodes.filter(function(n) { return n.type !== "group" && n.type !== "subflow" })); - var moved = new Set(); - for (var i = tabNodes.length-1; i >= 0; i--) { - if (toMove.size === 0) { - break; - } - var n = tabNodes[i]; - if (toMove.has(n)) { - // This is a node to move. - if (i < tabNodes.length-1 && !moved.has(tabNodes[i+1])) { - // Remove from current position - tabNodes.splice(i,1); - // Add it back one position higher - tabNodes.splice(i+1,0,n); - n._reordered = true; - result.push(n); - } - toMove.delete(n); - moved.add(n); + let result = [] + const tabNodes = tabMap[nodes[0].z]; + const toMove = new Set(nodes.filter(function(n) { return n.type !== "group" && n.type !== "subflow" })); + if (toMove.size > 0) { + result = result.concat(changeCollectionDepth(tabNodes, toMove, direction, singleStep)) + if (result.length > 0) { + RED.events.emit('nodes:reorder',{ + z: nodes[0].z, + nodes: result + }); } } - if (result.length > 0) { - RED.events.emit('nodes:reorder',{ - z: nodes[0].z, - nodes: result - }); + + const groupNodes = groupsByZ[nodes[0].z] || [] + const groupsToMove = new Set(nodes.filter(function(n) { return n.type === 'group'})) + if (groupsToMove.size > 0) { + const groupResult = changeCollectionDepth(groupNodes, groupsToMove, direction, singleStep) + if (groupResult.length > 0) { + result = result.concat(groupResult) + RED.events.emit('groups:reorder',{ + z: nodes[0].z, + nodes: groupResult + }); + } } - return result; + RED.view.redraw(true) + return result + }, + moveNodesForwards: function(nodes) { + return api.changeDepth(nodes, true, true) }, moveNodesBackwards: function(nodes) { - var result = []; - if (!Array.isArray(nodes)) { - nodes = [nodes] - } - // Can only do this for nodes on the same tab. - // Use nodes[0] to get the z - var tabNodes = tabMap[nodes[0].z]; - var toMove = new Set(nodes.filter(function(n) { return n.type !== "group" && n.type !== "subflow" })); - var moved = new Set(); - for (var i = 0; i < tabNodes.length; i++) { - if (toMove.size === 0) { - break; - } - var n = tabNodes[i]; - if (toMove.has(n)) { - // This is a node to move. - if (i > 0 && !moved.has(tabNodes[i-1])) { - // Remove from current position - tabNodes.splice(i,1); - // Add it back one position lower - tabNodes.splice(i-1,0,n); - n._reordered = true; - result.push(n); - } - toMove.delete(n); - moved.add(n); - } - } - if (result.length > 0) { - RED.events.emit('nodes:reorder',{ - z: nodes[0].z, - nodes: result - }); - } - return result; + return api.changeDepth(nodes, false, true) }, moveNodesToFront: function(nodes) { - var result = []; - if (!Array.isArray(nodes)) { - nodes = [nodes] - } - // Can only do this for nodes on the same tab. - // Use nodes[0] to get the z - var tabNodes = tabMap[nodes[0].z]; - var toMove = new Set(nodes.filter(function(n) { return n.type !== "group" && n.type !== "subflow" })); - var target = tabNodes.length-1; - for (var i = tabNodes.length-1; i >= 0; i--) { - if (toMove.size === 0) { - break; - } - var n = tabNodes[i]; - if (toMove.has(n)) { - // This is a node to move. - if (i < target) { - // Remove from current position - tabNodes.splice(i,1); - tabNodes.splice(target,0,n); - n._reordered = true; - result.push(n); - } - target--; - toMove.delete(n); - } - } - if (result.length > 0) { - RED.events.emit('nodes:reorder',{ - z: nodes[0].z, - nodes: result - }); - } - return result; + return api.changeDepth(nodes, true, false) }, moveNodesToBack: function(nodes) { - var result = []; - if (!Array.isArray(nodes)) { - nodes = [nodes] - } - // Can only do this for nodes on the same tab. - // Use nodes[0] to get the z - var tabNodes = tabMap[nodes[0].z]; - var toMove = new Set(nodes.filter(function(n) { return n.type !== "group" && n.type !== "subflow" })); - var target = 0; - for (var i = 0; i < tabNodes.length; i++) { - if (toMove.size === 0) { - break; - } - var n = tabNodes[i]; - if (toMove.has(n)) { - // This is a node to move. - if (i > target) { - // Remove from current position - tabNodes.splice(i,1); - // Add it back one position lower - tabNodes.splice(target,0,n); - n._reordered = true; - result.push(n); - } - target++; - toMove.delete(n); - } - } - if (result.length > 0) { - RED.events.emit('nodes:reorder',{ - z: nodes[0].z, - nodes: result - }); - } - return result; + return api.changeDepth(nodes, false, false) }, getNodes: function(z) { return tabMap[z]; @@ -498,7 +436,7 @@ RED.nodes = (function() { return result; }, getNodeOrder: function(z) { - return tabMap[z].map(function(n) { return n.id }) + return (groupsByZ[z] || []).concat(tabMap[z]).map(n => n.id) }, setNodeOrder: function(z, order) { var orderMap = {}; @@ -510,6 +448,11 @@ RED.nodes = (function() { B._reordered = true; return orderMap[A.id] - orderMap[B.id]; }) + if (groupsByZ[z]) { + groupsByZ[z].sort(function(A,B) { + return orderMap[A.id] - orderMap[B.id]; + }) + } } } return api; @@ -2615,6 +2558,10 @@ RED.nodes = (function() { delete groups[group.id]; RED.events.emit("groups:remove",group); } + function getGroupOrder(z) { + const groups = groupsByZ[z] + return groups.map(g => g.id) + } function addJunction(junction) { if (!junction.__isProxy__) { diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/contextMenu.js b/packages/node_modules/@node-red/editor-client/src/js/ui/contextMenu.js index 57d5bbcf9..d609ea6d3 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/contextMenu.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/contextMenu.js @@ -127,17 +127,24 @@ RED.contextMenu = (function () { options: [ { onselect: 'core:group-selection' }, { onselect: 'core:ungroup-selection', disabled: !hasGroup }, - null, - { onselect: 'core:copy-group-style', disabled: !hasGroup }, - { onselect: 'core:paste-group-style', disabled: !hasGroup} ] }) + if (hasGroup) { + menuItems[menuItems.length - 1].options.push( + { onselect: 'core:merge-selection-to-group', label: RED._("menu.label.groupMergeSelection") } + ) + + } if (canRemoveFromGroup) { menuItems[menuItems.length - 1].options.push( - null, { onselect: 'core:remove-selection-from-group', label: RED._("menu.label.groupRemoveSelection") } ) } + menuItems[menuItems.length - 1].options.push( + null, + { onselect: 'core:copy-group-style', disabled: !hasGroup }, + { onselect: 'core:paste-group-style', disabled: !hasGroup} + ) } if (canEdit && hasMultipleSelection) { menuItems.push({ 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 index 399e5e36e..f028d8847 100644 --- 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 @@ -325,7 +325,7 @@ RED.group = (function() { var selection = RED.view.selection(); if (selection.nodes) { var newSelection = []; - groups = selection.nodes.filter(function(n) { return n.type === "group" }); + let groups = selection.nodes.filter(function(n) { return n.type === "group" }); var historyEvent = { t:"ungroup", @@ -473,9 +473,17 @@ RED.group = (function() { if (nodes.length === 0) { return; } - if (nodes.filter(function(n) { return n.type === "subflow" }).length > 0) { - RED.notify(RED._("group.errors.cannotAddSubflowPorts"),"error"); - return; + const existingGroup = nodes[0].g + for (let i = 0; i < nodes.length; i++) { + const n = nodes[i] + if (n.type === 'subflow') { + RED.notify(RED._("group.errors.cannotAddSubflowPorts"),"error"); + return; + } + if (n.g !== existingGroup) { + console.warn("Cannot add nooes with different z properties") + return + } } // nodes is an array // each node must be on the same tab (z) @@ -494,6 +502,10 @@ RED.group = (function() { group.z = nodes[0].z; group = RED.nodes.addGroup(group); + if (existingGroup) { + addToGroup(RED.nodes.group(existingGroup), group) + } + try { addToGroup(group,nodes); } catch(err) { @@ -517,7 +529,7 @@ RED.group = (function() { if (!z) { z = n.z; } else if (z !== n.z) { - throw new Error("Cannot add nooes with different z properties") + throw new Error("Cannot add nodes with different z properties") } if (n.g) { // This is already in a group. @@ -534,14 +546,10 @@ RED.group = (function() { throw new Error(RED._("group.errors.cannotCreateDiffGroups")) } } - // The nodes are already in a group. The assumption is they should be - // wrapped in the newly provided group, and that group added to in their - // place to the existing containing group. + // The nodes are already in a group - so we need to remove them first if (g) { g = RED.nodes.group(g); - g.nodes.push(group); g.dirty = true; - group.g = g.id; } // Second pass - add them to the group for (i=0;i n.n === node) + if (index > -1) { + const removed = set.splice(index, 1) + set.unshift(...removed) + } + }, + find: function(func) { return set.find(func) } } return api; })(); - var selectedLinks = (function() { + const selectedLinks = (function() { var links = new Set(); - return { + const api = { add: function(link) { links.add(link); link.selected = true; @@ -200,8 +214,16 @@ RED.view = (function() { }, forEach: function(func) { links.forEach(func) }, has: function(link) { return links.has(link) }, - toArray: function() { return Array.from(links) } + toArray: function() { return Array.from(links) }, + clearUnselected: function () { + api.forEach(l => { + if (!l.source.selected || !l.target.selected) { + api.remove(l) + } + }) + } } + return api })(); @@ -244,6 +266,7 @@ RED.view = (function() { d3.select(document).on('mouseup.red-ui-workspace-tracker', null) if (lasso) { if (d3.event.buttons !== 1) { + outer.classed('red-ui-workspace-lasso-active', false) lasso.remove(); lasso = null; } @@ -378,6 +401,31 @@ RED.view = (function() { } d3.event.preventDefault(); }); + + + const handleAltToggle = (event) => { + if (mouse_mode === RED.state.MOVING_ACTIVE && event.key === 'Alt' && groupAddParentGroup) { + RED.nodes.group(groupAddParentGroup).dirty = true + for (let n = 0; n { + g._order = ii++ + g._childGroups.forEach(processGroup) } - }); + rootGroups.forEach(processGroup) + } } else { activeNodes = []; activeLinks = []; @@ -919,43 +960,13 @@ RED.view = (function() { activeGroups = []; } - var changed = false; - do { - changed = false; - activeGroups.forEach(function(g) { - if (g.g) { - var parentGroup = RED.nodes.group(g.g); - if (parentGroup) { - var parentDepth = parentGroup._depth; - if (g._depth !== parentDepth + 1) { - g._depth = parentDepth + 1; - changed = true; - } - if (g._root !== parentGroup._root) { - g._root = parentGroup._root; - changed = true; - } - } - } - }); - } while (changed) activeGroups.sort(function(a,b) { - if (a._root === b._root) { - return a._depth - b._depth; - } else { - // return a._root.localeCompare(b._root); - return a._index - b._index; - } + return a._order - b._order }); var group = groupLayer.selectAll(".red-ui-flow-group").data(activeGroups,function(d) { return d.id }); group.sort(function(a,b) { - if (a._root === b._root) { - return a._depth - b._depth; - } else { - return a._index - b._index; - // return a._root.localeCompare(b._root); - } + return a._order - b._order }) } @@ -1060,6 +1071,7 @@ RED.view = (function() { updateSelection(); } if (mouse_mode === 0 && lasso) { + outer.classed('red-ui-workspace-lasso-active', false) lasso.remove(); lasso = null; } @@ -1091,6 +1103,7 @@ RED.view = (function() { .attr("height", 0) .attr("class", "nr-ui-view-lasso"); d3.event.preventDefault(); + outer.classed('red-ui-workspace-lasso-active', true) } } else if (d3.event.altKey && !activeFlowLocked) { //Alt [+shift] held - Begin slicing @@ -1111,14 +1124,13 @@ RED.view = (function() { } options = options || {}; var point = options.position || lastClickPosition; - var spliceLink = options.splice; + var linkToSplice = options.splice; var spliceMultipleLinks = options.spliceMultiple var targetGroup = options.group; var touchTrigger = options.touchTrigger; - if (targetGroup && !targetGroup.active) { + if (targetGroup) { selectGroup(targetGroup,false); - enterActiveGroup(targetGroup); RED.view.redraw(); } @@ -1183,7 +1195,7 @@ RED.view = (function() { } hideDragLines(); } - if (spliceLink || spliceMultipleLinks) { + if (linkToSplice || spliceMultipleLinks) { filter = { input:true, output:true, @@ -1433,24 +1445,9 @@ RED.view = (function() { } } - if (spliceLink) { + if (linkToSplice) { resetMouseVars(); - // TODO: DRY - droppable/nodeMouseDown/canvasMouseUp/showQuickAddDialog - RED.nodes.removeLink(spliceLink); - var link1 = { - source:spliceLink.source, - sourcePort:spliceLink.sourcePort, - target: nn - }; - var link2 = { - source:nn, - sourcePort:0, - target: spliceLink.target - }; - RED.nodes.addLink(link1); - RED.nodes.addLink(link2); - historyEvent.links = (historyEvent.links || []).concat([link1,link2]); - historyEvent.removedLinks = [spliceLink]; + spliceLink(linkToSplice, nn, historyEvent) } RED.history.push(historyEvent); RED.nodes.dirty(true); @@ -1459,7 +1456,6 @@ RED.view = (function() { nn.selected = true; if (targetGroup) { selectGroup(targetGroup,false); - enterActiveGroup(targetGroup); } movingSet.add(nn); updateActiveNodes(); @@ -1674,16 +1670,11 @@ RED.view = (function() { if ((d > 3 && !dblClickPrimed) || (dblClickPrimed && d > 10)) { clickElapsed = 0; if (!activeFlowLocked) { - mouse_mode = RED.state.MOVING_ACTIVE; - spliceActive = false; - if (movingSet.length() === 1) { - node = movingSet.get(0); - spliceActive = node.n.hasOwnProperty("_def") && - ((node.n.hasOwnProperty("inputs") && node.n.inputs > 0) || (!node.n.hasOwnProperty("inputs") && node.n._def.inputs > 0)) && - ((node.n.hasOwnProperty("outputs") && node.n.outputs > 0) || (!node.n.hasOwnProperty("outputs") && node.n._def.outputs > 0)) && - RED.nodes.filterLinks({ source: node.n }).length === 0 && - RED.nodes.filterLinks({ target: node.n }).length === 0; + if (mousedown_node) { + movingSet.makePrimary(mousedown_node) } + mouse_mode = RED.state.MOVING_ACTIVE; + startSelectionMove() } } } else if (mouse_mode == RED.state.MOVING_ACTIVE || mouse_mode == RED.state.IMPORT_DRAGGING || mouse_mode == RED.state.DETACHED_DRAGGING) { @@ -1698,6 +1689,7 @@ RED.view = (function() { node.n.ox = node.n.x; node.n.oy = node.n.y; } + node.n._detachFromGroup = d3.event.altKey node.n.x = mousePos[0]+node.dx; node.n.y = mousePos[1]+node.dy; node.n.dirty = true; @@ -1766,9 +1758,8 @@ RED.view = (function() { } } - // Check link splice or group-add + // Check link splice if (movingSet.length() === 1 && movingSet.get(0).n.type !== "group") { - //}{//NIS node = movingSet.get(0); if (spliceActive) { if (!spliceTimer) { @@ -1816,23 +1807,39 @@ RED.view = (function() { },100); } } - if (node.n.type !== 'subflow' && !node.n.g && activeGroups) { - if (!groupHoverTimer) { - groupHoverTimer = setTimeout(function() { - activeHoverGroup = getGroupAt(node.n.x,node.n.y); - for (var i=0;i ag.x && y < ag.y+ag.h && y2 > ag.y) { - // There was an active group and the lasso intersects with it, - // so reenter the group - enterActiveGroup(ag); - activeGroup.selected = true; - } - } } - activeGroups.forEach(function(g) { - if (!g.selected) { - if (g.x > x && g.x+g.w < x2 && g.y > y && g.y+g.h < y2) { - if (!activeGroup || RED.group.contains(activeGroup,g)) { - while (g.g && (!activeGroup || g.g !== activeGroup.id)) { - g = RED.nodes.group(g.g); - } - if (!g.selected) { - selectGroup(g,true); - } - } + + activeGroups.forEach(function(n) { + if (!movingSet.has(n) && !n.selected) { + // group entirely within lasso + if (n.x > x && n.y > y && n.x + n.w < x2 && n.y + n.h < y2) { + n.selected = true + n.dirty = true + var groupNodes = RED.group.getNodes(n,true); + groupNodes.forEach(gn => movingSet.add(gn)) } } }) - activeNodes.forEach(function(n) { - if (!n.selected) { + if (!movingSet.has(n) && !n.selected) { if (n.x > x && n.x < x2 && n.y > y && n.y < y2) { - if (!activeGroup || RED.group.contains(activeGroup,n)) { - if (n.g && (!activeGroup || n.g !== activeGroup.id)) { - var group = RED.nodes.group(n.g); - while (group.g && (!activeGroup || group.g !== activeGroup.id)) { - group = RED.nodes.group(group.g); - } - if (!group.selected) { - selectGroup(group,true); - } - } else { - n.selected = true; - n.dirty = true; - movingSet.add(n); - } - } + n.selected = true; + n.dirty = true; + movingSet.add(n); } } }); activeJunctions.forEach(function(n) { - if (!n.selected) { + if (!movingSet.has(n) && !n.selected) { if (n.x > x && n.x < x2 && n.y > y && n.y < y2) { n.selected = true; n.dirty = true; @@ -1966,17 +1949,6 @@ RED.view = (function() { } }) - // var selectionChanged = false; - // do { - // selectionChanged = false; - // selectedGroups.forEach(function(g) { - // if (g.g && g.selected && RED.nodes.group(g.g).selected) { - // g.selected = false; - // selectionChanged = true; - // } - // }) - // } while(selectionChanged); - if (activeSubflow) { activeSubflow.in.forEach(function(n) { n.selected = (n.x > x && n.x < x2 && n.y > y && n.y < y2); @@ -2001,6 +1973,7 @@ RED.view = (function() { } } updateSelection(); + outer.classed('red-ui-workspace-lasso-active', false) lasso.remove(); lasso = null; } else if (mouse_mode == RED.state.DEFAULT && mousedown_link == null && !d3.event.ctrlKey && !d3.event.metaKey ) { @@ -2015,210 +1988,80 @@ RED.view = (function() { RED.actions.invoke("core:split-wires-with-junctions") slicePath.remove(); slicePath = null; - - // var removedLinks = new Set() - // var addedLinks = [] - // var addedJunctions = [] - // - // var groupedLinks = {} - // selectedLinks.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 - // }) - // 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 - // } - // links = links.filter(function(l) { return !removedLinks.has(l) }) - // if (links.length === 0) { - // return - // } - // links.forEach(function(l) { - // junction.x += l._sliceLocation.x - // junction.y += l._sliceLocation.y - // }) - // junction.x = Math.round(junction.x/links.length) - // junction.y = Math.round(junction.y/links.length) - // if (snapGrid) { - // junction.x = (gridSize*Math.round(junction.x/gridSize)); - // junction.y = (gridSize*Math.round(junction.y/gridSize)); - // } - // - // var nodeGroups = new Set() - // - // 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) - // } - // } - // }) - // slicePath.remove(); - // slicePath = null; - // - // if (addedJunctions.length > 0) { - // RED.history.push({ - // t: 'add', - // links: addedLinks, - // junctions: addedJunctions, - // removedLinks: Array.from(removedLinks) - // }) - // RED.nodes.dirty(true) - // } - // RED.view.redraw(true); } if (mouse_mode == RED.state.MOVING_ACTIVE) { if (movingSet.length() > 0) { - var addedToGroup = null; - var moveEvent = null; - if (activeHoverGroup) { - var oldX = activeHoverGroup.x; - var oldY = activeHoverGroup.y; - for (var j=0;j 0 && mouse_mode == RED.state.MOVING_ACTIVE) { - historyEvent = {t:"move",nodes:ns,dirty:RED.nodes.dirty()}; + // Check to see if we need to splice a link + if (moveEvent.nodes.length > 0) { + historyEvent.events.push(moveEvent) if (activeSpliceLink) { - // TODO: DRY - droppable/nodeMouseDown/canvasMouseUp - var spliceLink = d3.select(activeSpliceLink).data()[0]; - RED.nodes.removeLink(spliceLink); - var link1 = { - source:spliceLink.source, - sourcePort:spliceLink.sourcePort, - target: movingSet.get(0).n - }; - var link2 = { - source:movingSet.get(0).n, - sourcePort:0, - target: spliceLink.target - }; - RED.nodes.addLink(link1); - RED.nodes.addLink(link2); - historyEvent.links = [link1,link2]; - historyEvent.removedLinks = [spliceLink]; - updateActiveNodes(); - } - if (addedToGroup) { - historyEvent.addToGroup = addedToGroup; + var linkToSplice = d3.select(activeSpliceLink).data()[0]; + spliceLink(linkToSplice, movingSet.get(0).n, moveEvent) } + } + if (moveAndChangedGroupEvent.nodes.length > 0) { + historyEvent.events.push(moveAndChangedGroupEvent) + } + + // Only continue if something has moved + if (historyEvent.events.length > 0) { RED.nodes.dirty(true); - if (moveEvent) { - historyEvent = { - t: "multi", - events: [moveEvent, historyEvent] - }; + if (historyEvent.events.length === 1) { + // Keep history tidy - no need for multi-event + RED.history.push(historyEvent.events[0]); + } else { + // Multiple events - push the whole lot as one + RED.history.push(historyEvent); } - RED.history.push(historyEvent); + updateActiveNodes(); } } } - // if (mouse_mode === RED.state.MOVING && mousedown_node && mousedown_node.g) { - // if (mousedown_node.gSelected) { - // delete mousedown_node.gSelected - // } else { - // if (!d3.event.ctrlKey && !d3.event.metaKey) { - // clearSelection(); - // } - // RED.nodes.group(mousedown_node.g).selected = true; - // mousedown_node.selected = true; - // mousedown_node.dirty = true; - // movingSet.add(mousedown_node); - // } - // } if (mouse_mode == RED.state.MOVING || mouse_mode == RED.state.MOVING_ACTIVE || mouse_mode == RED.state.DETACHED_DRAGGING) { - // if (mousedown_node) { - // delete mousedown_node.gSelected; - // } if (mouse_mode === RED.state.DETACHED_DRAGGING) { var ns = []; for (var j=0;j { + if (g.hovered) { + g.hovered = false + g.dirty = true + } + }) + + return { + addedToGroup, + removedFromGroup, + groupMoveEvent, + rehomedNodes + } + + } + function zoomIn() { if (scaleFactor < 2) { zoomView(scaleFactor+0.1); @@ -2314,10 +2246,9 @@ RED.view = (function() { } clearSelection(); } else if (lasso) { + outer.classed('red-ui-workspace-lasso-active', false) lasso.remove(); lasso = null; - } else if (activeGroup) { - exitActiveGroup() } else { clearSelection(); } @@ -2328,82 +2259,61 @@ RED.view = (function() { return; } selectedLinks.clear(); - - if (activeGroup) { - var ag = activeGroup; - clearSelection(); - enterActiveGroup(ag); - - var groupNodes = RED.group.getNodes(ag,false); - groupNodes.forEach(function(n) { - if (n.type === 'group') { - selectGroup(n,true,true); - } else { - movingSet.add(n) - n.selected = true; - n.dirty = true; - } - }) - activeGroup.selected = true; - } else { - - clearSelection(); - exitActiveGroup(); - activeGroups.forEach(function(g) { - if (!g.g) { - selectGroup(g, true); - if (!g.selected) { - g.selected = true; - g.dirty = true; - } - } else { - g.selected = false; + clearSelection(); + activeGroups.forEach(function(g) { + if (!g.g) { + selectGroup(g, true); + if (!g.selected) { + g.selected = true; g.dirty = true; } - }) + } else { + g.selected = false; + g.dirty = true; + } + }) - activeNodes.forEach(function(n) { - if (mouse_mode === RED.state.SELECTING_NODE) { - if (selectNodesOptions.filter && !selectNodesOptions.filter(n)) { - return; - } + activeNodes.forEach(function(n) { + if (mouse_mode === RED.state.SELECTING_NODE) { + if (selectNodesOptions.filter && !selectNodesOptions.filter(n)) { + return; } - if (!n.g && !n.selected) { - n.selected = true; - n.dirty = true; - movingSet.add(n); - } - }); + } + if (!n.g && !n.selected) { + n.selected = true; + n.dirty = true; + movingSet.add(n); + } + }); - activeJunctions.forEach(function(n) { + activeJunctions.forEach(function(n) { + if (!n.selected) { + n.selected = true; + n.dirty = true; + movingSet.add(n); + } + }) + + if (mouse_mode !== RED.state.SELECTING_NODE && activeSubflow) { + activeSubflow.in.forEach(function(n) { if (!n.selected) { n.selected = true; n.dirty = true; movingSet.add(n); } - }) - - if (mouse_mode !== RED.state.SELECTING_NODE && activeSubflow) { - activeSubflow.in.forEach(function(n) { - if (!n.selected) { - n.selected = true; - n.dirty = true; - movingSet.add(n); - } - }); - activeSubflow.out.forEach(function(n) { - if (!n.selected) { - n.selected = true; - n.dirty = true; - movingSet.add(n); - } - }); - if (activeSubflow.status) { - if (!activeSubflow.status.selected) { - activeSubflow.status.selected = true; - activeSubflow.status.dirty = true; - movingSet.add(activeSubflow.status); - } + }); + activeSubflow.out.forEach(function(n) { + if (!n.selected) { + n.selected = true; + n.dirty = true; + movingSet.add(n); + } + }); + if (activeSubflow.status) { + if (!activeSubflow.status.selected) { + activeSubflow.status.selected = true; + activeSubflow.status.dirty = true; + movingSet.add(activeSubflow.status); } } } @@ -2422,11 +2332,6 @@ RED.view = (function() { } movingSet.clear(); selectedLinks.clear(); - if (activeGroup) { - activeGroup.active = false - activeGroup.dirty = true; - activeGroup = null; - } activeGroups.forEach(function(g) { g.selected = false; g.dirty = true; @@ -2986,6 +2891,7 @@ RED.view = (function() { mousedown_port_type = null; activeSpliceLink = null; spliceActive = false; + groupAddActive = false; if (activeHoverGroup) { activeHoverGroup.hovered = false; activeHoverGroup = null; @@ -3492,7 +3398,6 @@ RED.view = (function() { clearSelection(); selectGroup(RED.nodes.group(d.g), false); - enterActiveGroup(RED.nodes.group(d.g)) mousedown_node.selected = true; movingSet.add(mousedown_node); @@ -3545,57 +3450,25 @@ RED.view = (function() { //RED.touch.radialMenu.show(d3.select(this),pos); if (mouse_mode == RED.state.IMPORT_DRAGGING || mouse_mode == RED.state.DETACHED_DRAGGING) { var historyEvent = RED.history.peek(); - if (activeSpliceLink) { - // TODO: DRY - droppable/nodeMouseDown/canvasMouseUp - var spliceLink = d3.select(activeSpliceLink).data()[0]; - RED.nodes.removeLink(spliceLink); - var link1 = { - source:spliceLink.source, - sourcePort:spliceLink.sourcePort, - target: movingSet.get(0).n - }; - var link2 = { - source:movingSet.get(0).n, - sourcePort:0, - target: spliceLink.target - }; - RED.nodes.addLink(link1); - RED.nodes.addLink(link2); + // Check to see if we're dropping into a group + const { + addedToGroup, + removedFromGroup, + groupMoveEvent, + rehomedNodes + } = addMovingSetToGroup() - historyEvent.links = [link1,link2]; - historyEvent.removedLinks = [spliceLink]; + if (activeSpliceLink) { + var linkToSplice = d3.select(activeSpliceLink).data()[0]; + spliceLink(linkToSplice, movingSet.get(0).n, historyEvent) updateActiveNodes(); } - - var moveEvent = null; - if (activeHoverGroup) { - var oldX = activeHoverGroup.x; - var oldY = activeHoverGroup.y; - for (var j=0;j= 0; i -= 1) { - var msn = movingSet.get(i); + for (let i = movingSet.length()-1; i >= 0; i -= 1) { + const msn = movingSet.get(i); if (nodeSet.has(msn.n) || msn.n === g) { msn.n.selected = false; msn.n.dirty = true; movingSet.remove(msn.n,i) } } + selectedLinks.clearUnselected() } - function getGroupAt(x,y) { + function getGroupAt(x, y, ignoreSelected) { // x,y expected to be in node-co-ordinate space var candidateGroups = {}; for (var i=0;i= g.x && x <= g.x + g.w && y >= g.y && y <= g.y + g.h) { candidateGroups[g.id] = g; } @@ -4773,6 +4507,7 @@ RED.view = (function() { nodesReordered = true; delete d._reordered; } + if (d.dirty) { var self = this; var thisNode = d3.select(this); @@ -5426,23 +5161,30 @@ RED.view = (function() { g.attr("id",d.id); var groupBorderRadius = 4; - + var groupOutlineBorderRadius = 6 var selectGroup = groupSelectLayer.append('g').attr("class", "red-ui-flow-group").attr("id","group_select_"+d.id); - selectGroup.append('rect').classed("red-ui-flow-group-outline-select",true) + const groupBackground = selectGroup.append('rect') + .classed("red-ui-flow-group-outline-select",true) .classed("red-ui-flow-group-outline-select-background",true) - .attr('rx',groupBorderRadius).attr('ry',groupBorderRadius) - .attr("x",-4) - .attr("y",-4); - - - selectGroup.append('rect').classed("red-ui-flow-group-outline-select",true) - .attr('rx',groupBorderRadius).attr('ry',groupBorderRadius) - .attr("x",-4) - .attr("y",-4) - selectGroup.on("mousedown", function() {groupMouseDown.call(g[0][0],d)}); - selectGroup.on("mouseup", function() {groupMouseUp.call(g[0][0],d)}); - selectGroup.on("touchstart", function() {groupMouseDown.call(g[0][0],d); d3.event.preventDefault();}); - selectGroup.on("touchend", function() {groupMouseUp.call(g[0][0],d); d3.event.preventDefault();}); + .attr('rx',groupOutlineBorderRadius).attr('ry',groupOutlineBorderRadius) + .attr("x",-3) + .attr("y",-3); + selectGroup.append('rect') + .classed("red-ui-flow-group-outline-select",true) + .classed("red-ui-flow-group-outline-select-outline",true) + .attr('rx',groupOutlineBorderRadius).attr('ry',groupOutlineBorderRadius) + .attr("x",-3) + .attr("y",-3) + selectGroup.append('rect') + .classed("red-ui-flow-group-outline-select",true) + .classed("red-ui-flow-group-outline-select-line",true) + .attr('rx',groupOutlineBorderRadius).attr('ry',groupOutlineBorderRadius) + .attr("x",-3) + .attr("y",-3) + groupBackground.on("mousedown", function() {groupMouseDown.call(g[0][0],d)}); + groupBackground.on("mouseup", function() {groupMouseUp.call(g[0][0],d)}); + groupBackground.on("touchstart", function() {groupMouseDown.call(g[0][0],d); d3.event.preventDefault();}); + groupBackground.on("touchend", function() {groupMouseUp.call(g[0][0],d); d3.event.preventDefault();}); g.append('rect').classed("red-ui-flow-group-outline",true).attr('rx',0.5).attr('ry',0.5); @@ -5460,11 +5202,7 @@ RED.view = (function() { }); if (addedGroups) { group.sort(function(a,b) { - if (a._root === b._root) { - return a._depth - b._depth; - } else { - return a._index - b._index; - } + return a._order - b._order }) } group[0].reverse(); @@ -5489,6 +5227,11 @@ RED.view = (function() { var margin = 26; d.nodes.forEach(function(n) { groupOpCount++ + if (n._detachFromGroup) { + // Do not include this node when recalulating + // the group dimensions + return + } if (n.type !== "group") { minX = Math.min(minX,n.x-n.w/2-margin-((n._def.button && n._def.align!=="right")?20:0)); minY = Math.min(minY,n.y-n.h/2-margin); @@ -5501,11 +5244,12 @@ RED.view = (function() { maxY = Math.max(maxY,n.y+n.h+margin) } }); - - d.x = minX; - d.y = minY; - d.w = maxX - minX; - d.h = maxY - minY; + if (minX !== Number.POSITIVE_INFINITY && minY !== Number.POSITIVE_INFINITY) { + d.x = minX; + d.y = minY; + d.w = maxX - minX; + d.h = maxY - minY; + } recalculateLabelOffsets = true; // if set explicitly to false, this group has just been // imported so needed this initial resize calculation. @@ -5562,16 +5306,25 @@ RED.view = (function() { } else { selectGroup.classList.remove("red-ui-flow-group-hovered") } + if (d.selected) { + selectGroup.classList.add("red-ui-flow-group-selected") + } else { + selectGroup.classList.remove("red-ui-flow-group-selected") + } var selectGroupRect = selectGroup.children[0]; - selectGroupRect.setAttribute("width",d.w+8) - selectGroupRect.setAttribute("height",d.h+8) - selectGroupRect.style.strokeOpacity = (d.active || d.selected || d.highlighted)?0.8:0; - selectGroupRect.style.strokeDasharray = (d.active)?"10 4":""; + // Background + selectGroupRect.setAttribute("width",d.w+6) + selectGroupRect.setAttribute("height",d.h+6) + // Outline selectGroupRect = selectGroup.children[1]; - selectGroupRect.setAttribute("width",d.w+8) - selectGroupRect.setAttribute("height",d.h+8) - selectGroupRect.style.strokeOpacity = (d.active || d.selected || d.highlighted)?0.8:0; - selectGroupRect.style.strokeDasharray = (d.active)?"10 4":""; + selectGroupRect.setAttribute("width",d.w+6) + selectGroupRect.setAttribute("height",d.h+6) + selectGroupRect.style.strokeOpacity = (d.selected || d.highlighted)?0.8:0; + // Line + selectGroupRect = selectGroup.children[2]; + selectGroupRect.setAttribute("width",d.w+6) + selectGroupRect.setAttribute("height",d.h+6) + selectGroupRect.style.strokeOpacity = (d.selected || d.highlighted)?0.8:0; if (d.highlighted) { selectGroup.classList.add("red-ui-flow-node-highlighted"); @@ -5831,17 +5584,8 @@ RED.view = (function() { } if (!touchImport) { mouse_mode = RED.state.IMPORT_DRAGGING; - spliceActive = false; - if (movingSet.length() === 1) { - node = movingSet.get(0); - spliceActive = node.n.hasOwnProperty("_def") && - ((node.n.hasOwnProperty("inputs") && node.n.inputs > 0) || (!node.n.hasOwnProperty("inputs") && node.n._def.inputs > 0)) && - ((node.n.hasOwnProperty("outputs") && node.n.outputs > 0) || (!node.n.hasOwnProperty("outputs") && node.n._def.outputs > 0)) - - - } + startSelectionMove() } - } var historyEvent = { @@ -5979,6 +5723,59 @@ RED.view = (function() { } } + function startSelectionMove() { + spliceActive = false; + if (movingSet.length() === 1) { + const node = movingSet.get(0); + spliceActive = node.n.hasOwnProperty("_def") && + ((node.n.hasOwnProperty("inputs") && node.n.inputs > 0) || (!node.n.hasOwnProperty("inputs") && node.n._def.inputs > 0)) && + ((node.n.hasOwnProperty("outputs") && node.n.outputs > 0) || (!node.n.hasOwnProperty("outputs") && node.n._def.outputs > 0)) && + RED.nodes.filterLinks({ source: node.n }).length === 0 && + RED.nodes.filterLinks({ target: node.n }).length === 0; + } + groupAddActive = false + groupAddParentGroup = null + if (movingSet.length() > 0 && activeGroups) { + // movingSet includes the selection AND any nodes inside selected groups + // So we cannot simply check the `g` of all nodes match. + // Instead, we have to: + // - note all groups in movingSet + // - note all .g values referenced in movingSet + // - then check for g values for groups not in movingSet + let isValidSelection = true + let hasNullGroup = false + const selectedGroups = [] + const referencedGroups = new Set() + movingSet.forEach(n => { + if (n.n.type === 'subflow') { + isValidSelection = false + } + if (n.n.type === 'group') { + selectedGroups.push(n.n.id) + } + if (n.n.g) { + referencedGroups.add(n.n.g) + } else { + hasNullGroup = true + } + }) + if (isValidSelection) { + selectedGroups.forEach(g => referencedGroups.delete(g)) + // console.log('selectedGroups', selectedGroups) + // console.log('referencedGroups',referencedGroups) + // console.log('hasNullGroup', hasNullGroup) + if (referencedGroups.size === 0) { + groupAddActive = true + } else if (!hasNullGroup && referencedGroups.size === 1) { + groupAddParentGroup = referencedGroups.values().next().value + groupAddActive = true + } + } + // console.log('groupAddActive', groupAddActive) + // console.log('groupAddParentGroup', groupAddParentGroup) + } + } + function toggleShowGrid(state) { if (state) { gridLayer.style("visibility","visible"); @@ -6058,24 +5855,18 @@ RED.view = (function() { if (movingSet.length() > 0) { movingSet.forEach(function(n) { if (n.n.type !== 'group') { - allNodes.add(n.n); + allNodes.add(n.n); } }); } - var selectedGroups = activeGroups.filter(function(g) { return g.selected && !g.active }); - 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); - }); - } - } + var selectedGroups = activeGroups.filter(function(g) { return g.selected }); + 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); } @@ -6320,7 +6111,7 @@ RED.view = (function() { return result; }, getGroupAtPoint: getGroupAt, - getActiveGroup: function() { return activeGroup }, + getActiveGroup: function() { return null }, reveal: function(id,triggerHighlight) { if (RED.nodes.workspace(id) || RED.nodes.subflow(id)) { RED.workspaces.show(id, null, null, true); diff --git a/packages/node_modules/@node-red/editor-client/src/sass/flow.scss b/packages/node_modules/@node-red/editor-client/src/sass/flow.scss index 3e5be0645..0eb68656b 100644 --- a/packages/node_modules/@node-red/editor-client/src/sass/flow.scss +++ b/packages/node_modules/@node-red/editor-client/src/sass/flow.scss @@ -91,10 +91,13 @@ .red-ui-flow-group { &.red-ui-flow-group-hovered { - .red-ui-flow-group-outline-select { + .red-ui-flow-group-outline-select-line { stroke-opacity: 0.8 !important; stroke-dasharray: 10 4 !important; } + .red-ui-flow-group-outline-select-outline { + stroke-opacity: 0.8 !important; + } } &.red-ui-flow-group-active-hovered:not(.red-ui-flow-group-hovered) { .red-ui-flow-group-outline-select { @@ -113,15 +116,35 @@ .red-ui-flow-group-outline-select { fill: none; stroke: var(--red-ui-node-selected-color); - pointer-events: stroke; + pointer-events: none; stroke-opacity: 0; - stroke-width: 3; + stroke-width: 2; - &.red-ui-flow-group-outline-select-background { + &.red-ui-flow-group-outline-select-outline { stroke: var(--red-ui-view-background); - stroke-width: 6; + stroke-width: 4; + } + &.red-ui-flow-group-outline-select-background { + fill: white; + fill-opacity: 0; + pointer-events: stroke; + stroke-width: 16; } } + +svg:not(.red-ui-workspace-lasso-active) { + .red-ui-flow-group:not(.red-ui-flow-group-selected) { + .red-ui-flow-group-outline-select.red-ui-flow-group-outline-select-background:hover { + ~ .red-ui-flow-group-outline-select { + stroke-opacity: 0.4 !important; + } + ~ .red-ui-flow-group-outline-select-line { + stroke-dasharray: 10 4 !important; + } + } + } +} + .red-ui-flow-group-body { pointer-events: none; fill: var(--red-ui-group-default-fill);