diff --git a/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json b/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json index 1ab148217..8c08397bf 100755 --- a/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json +++ b/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json @@ -497,7 +497,8 @@ "redoChange": "Redo", "searchBox": "Open search box", "managePalette": "Manage palette", - "actionList":"Action list" + "actionList": "Action list", + "splitWiresWithLinks": "Split selection with Link nodes" }, "library": { "library": "Library", 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 a8ffe581b..59531c48a 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 @@ -13,6 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. **/ + +/** + * An API for undo / redo history buffer + * @namespace RED.history +*/ RED.history = (function() { var undoHistory = []; var redoHistory = []; 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 f1f3f46e7..2e121b112 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 @@ -90,6 +90,7 @@ "alt-a m": "core:align-selection-to-middle", "alt-a c": "core:align-selection-to-center", "alt-a h": "core:distribute-selection-horizontally", - "alt-a v": "core:distribute-selection-vertically" + "alt-a v": "core:distribute-selection-vertically", + "alt-l": "core:split-wire-with-link-nodes" } } 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 78b9deae6..0805d5132 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 @@ -13,6 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. **/ + +/** + * An Interface to nodes and utility functions for creating/adding/deleting nodes and links + * @namespace RED.nodes +*/ RED.nodes = (function() { var PORT_TYPE_INPUT = 1; @@ -1160,6 +1165,93 @@ RED.nodes = (function() { return node; } + /** + * Create a node from a type string. + * **NOTE:** Can throw on error - use `try` `catch` block when calling + * @param {string} type The node type to create + * @param {number} [x] (optional) The horizontal position on the workspace + * @param {number} [y] (optional)The vertical on the workspace + * @param {string} [z] (optional) The flow tab this node will belong to. Defaults to active workspace. + * @returns An object containing the `node` and a `historyEvent` + * @private + */ + function createNode(type, x, y, z) { + var m = /^subflow:(.+)$/.exec(type); + var activeSubflow = z ? RED.nodes.subflow(z) : null; + if (activeSubflow && m) { + var subflowId = m[1]; + if (subflowId === activeSubflow.id) { + throw new Error(RED._("notification.error", { message: RED._("notification.errors.cannotAddSubflowToItself") })) + } + if (RED.nodes.subflowContains(m[1], activeSubflow.id)) { + throw new Error(RED._("notification.error", { message: RED._("notification.errors.cannotAddCircularReference") })) + } + } + + var nn = { id: RED.nodes.id(), z: z || RED.workspaces.active() }; + + nn.type = type; + nn._def = RED.nodes.getType(nn.type); + + if (!m) { + nn.inputs = nn._def.inputs || 0; + nn.outputs = nn._def.outputs; + + for (var d in nn._def.defaults) { + if (nn._def.defaults.hasOwnProperty(d)) { + if (nn._def.defaults[d].value !== undefined) { + nn[d] = JSON.parse(JSON.stringify(nn._def.defaults[d].value)); + } + } + } + + if (nn._def.onadd) { + try { + nn._def.onadd.call(nn); + } catch (err) { + console.log("Definition error: " + nn.type + ".onadd:", err); + } + } + } else { + var subflow = RED.nodes.subflow(m[1]); + nn.name = ""; + nn.inputs = subflow.in.length; + nn.outputs = subflow.out.length; + } + + nn.changed = true; + nn.moved = true; + + nn.w = RED.view.node_width; + nn.h = Math.max(RED.view.node_height, (nn.outputs || 0) * 15); + nn.resize = true; + if (x != null && typeof x == "number" && x >= 0) { + nn.x = x; + } + if (y != null && typeof y == "number" && y >= 0) { + nn.y = y; + } + var historyEvent = { + t: "add", + nodes: [nn.id], + dirty: RED.nodes.dirty() + } + if (activeSubflow) { + var subflowRefresh = RED.subflow.refresh(true); + if (subflowRefresh) { + historyEvent.subflow = { + id: activeSubflow.id, + changed: activeSubflow.changed, + instances: subflowRefresh.instances + } + } + } + return { + node: nn, + historyEvent: historyEvent + } + } + function convertSubflow(n, opts) { var exportCreds = true; var exportDimensions = false; @@ -2676,7 +2768,7 @@ RED.nodes = (function() { getType: registry.getNodeType, getNodeHelp: getNodeHelp, convertNode: convertNode, - + createNode: createNode, add: addNode, remove: removeNode, clear: clear, diff --git a/packages/node_modules/@node-red/editor-client/src/js/red.js b/packages/node_modules/@node-red/editor-client/src/js/red.js index 2b1c8f2e4..ca3f17241 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/red.js +++ b/packages/node_modules/@node-red/editor-client/src/js/red.js @@ -602,7 +602,10 @@ var RED = (function() { null, {id: "menu-item-edit-select-all", label:RED._("keyboard.selectAll"), onselect: "core:select-all-nodes"}, {id: "menu-item-edit-select-connected", label:RED._("keyboard.selectAllConnected"), onselect: "core:select-connected-nodes"}, - {id: "menu-item-edit-select-none", label:RED._("keyboard.selectNone"), onselect: "core:select-none"} + {id: "menu-item-edit-select-none", label:RED._("keyboard.selectNone"), onselect: "core:select-none"}, + null, + {id: "menu-item-edit-split-wire-with-links", label:RED._("keyboard.splitWireWithLinks"), onselect: "core:split-wire-with-link-nodes"}, + ]}); menuOptions.push({id:"menu-item-view-menu",label:RED._("menu.label.view.view"),options:[ diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/view-tools.js b/packages/node_modules/@node-red/editor-client/src/js/ui/view-tools.js index 7d92f6491..e0bb06915 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/view-tools.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/view-tools.js @@ -809,6 +809,167 @@ RED.view.tools = (function() { } } + /** + * Splits selected wires and re-joins them with link-out+link-in + * @param {Object || Object[]} wires The wire(s) to split and replace with link-out, link-in nodes. + */ + function splitWiresWithLinkNodes(wires) { + let wiresToSplit = wires || RED.view.selectedLinks.toArray(); + if (!Array.isArray(wiresToSplit)) { + wiresToSplit = [wiresToSplit]; + } + if (wiresToSplit.length < 1) { + return; //nothing selected + } + + const history = { + t: 'multi', + events: [], + dirty: RED.nodes.dirty() + } + const nodeSrcMap = {}; + const nodeTrgMap = {}; + const _gridSize = RED.view.gridSize(); + + for (let wireIdx = 0; wireIdx < wiresToSplit.length; wireIdx++) { + const wire = wiresToSplit[wireIdx]; + + //get source and target nodes of this wire link + const nSrc = wire.source; + const nTrg = wire.target; + + var updateNewNodePosXY = function (origNode, newNode, alignLeft, snap, yOffset) { + const nnSize = RED.view.calculateNodeDimensions(newNode); + newNode.w = nnSize[0]; + newNode.h = nnSize[1]; + const coords = { x: origNode.x || 0, y: origNode.y || 0, w: origNode.w || RED.view.node_width, h: origNode.h || RED.view.node_height }; + const x = coords.x - (coords.w/2.0); + if (alignLeft) { + coords.x = x - _gridSize - (newNode.w/2.0); + } else { + coords.x = x + coords.w + _gridSize + (newNode.w/2.0); + } + newNode.x = coords.x; + newNode.y = coords.y; + if (snap !== false) { + const offsets = RED.view.tools.calculateGridSnapOffsets(newNode); + newNode.x -= offsets.x; + newNode.y -= offsets.y; + } + newNode.y += (yOffset || 0); + } + const srcPort = (wire.sourcePort || 0); + let linkOutMapId = nSrc.id + ':' + srcPort; + let nnLinkOut = nodeSrcMap[linkOutMapId]; + //Create a Link Out if one is not already present + if(!nnLinkOut) { + const nLinkOut = RED.nodes.createNode("link out"); //create link node + nnLinkOut = nLinkOut.node; + nodeSrcMap[linkOutMapId] = nnLinkOut; + let yOffset = 0; + if(nSrc.outputs > 1) { + + const CENTER_PORT = (((nSrc.outputs-1) / 2) + 1); + const offsetCount = Math.abs(CENTER_PORT - (srcPort + 1)); + yOffset = (_gridSize * 2 * offsetCount); + if((srcPort + 1) < CENTER_PORT) { + yOffset = -yOffset; + } + updateNewNodePosXY(nSrc, nnLinkOut, false, false, yOffset); + } else { + updateNewNodePosXY(nSrc, nnLinkOut, false, RED.view.snapGrid, yOffset); + } + //add created node + RED.nodes.add(nnLinkOut); + RED.editor.validateNode(nnLinkOut); + history.events.push(nLinkOut.historyEvent); + //connect node to link node + const link = { + source: nSrc, + sourcePort: wire.sourcePort || 0, + target: nnLinkOut + }; + RED.nodes.addLink(link); + history.events.push({ + t: 'add', + links: [link], + }); + } + + let nnLinkIn = nodeTrgMap[nTrg.id]; + //Create a Link In if one is not already present + if(!nnLinkIn) { + const nLinkIn = RED.nodes.createNode("link in"); //create link node + nnLinkIn = nLinkIn.node; + nodeTrgMap[nTrg.id] = nnLinkIn; + updateNewNodePosXY(nTrg, nnLinkIn, true, RED.view.snapGrid, 0); + //add created node + RED.nodes.add(nnLinkIn); + RED.editor.validateNode(nnLinkIn); + history.events.push(nLinkIn.historyEvent); + //connect node to link node + const link = { + source: nnLinkIn, + sourcePort: 0, + target: nTrg + }; + RED.nodes.addLink(link); + history.events.push({ + t: 'add', + links: [link], + }); + } + + //connect the link out/link in virtual wires + if(nnLinkIn.links.indexOf(nnLinkOut.id) == -1) { + nnLinkIn.links.push(nnLinkOut.id); + } + if(nnLinkOut.links.indexOf(nnLinkIn.id) == -1) { + nnLinkOut.links.push(nnLinkIn.id); + } + + //delete the original wire + RED.nodes.removeLink(wire); + history.events.push({ + t: "delete", + links: [wire] + }); + } + //add all history events to stack + RED.history.push(history); + + //select all downstream of new link-in nodes so user can drag to new location + RED.view.clearSelection(); + RED.view.select({nodes: Object.values(nodeTrgMap) }); + selectConnected("down"); + + //update the view + RED.nodes.dirty(true); + RED.view.redraw(true); + } + + /** + * Calculate the required offsets to snap a node + * @param {Object} node The node to calculate grid snap offsets for + * @param {Object} [options] Options: `align` can be "nearest", "left" or "right" + * @returns `{x:number, y:number}` as the offsets to deduct from `x` and `y` + */ + function calculateGridSnapOffsets(node, options) { + options = options || { align: "nearest" }; + const gridOffset = { x: 0, y: 0 }; + const gridSize = RED.view.gridSize(); + const offsetLeft = node.x - (gridSize * Math.round((node.x - node.w / 2) / gridSize) + node.w / 2); + const offsetRight = node.x - (gridSize * Math.round((node.x + node.w / 2) / gridSize) - node.w / 2); + gridOffset.x = offsetRight; + if (options.align === "right") { + //skip - already set to right + } else if (options.align === "left" || Math.abs(offsetLeft) < Math.abs(offsetRight)) { + gridOffset.x = offsetLeft; + } + gridOffset.y = node.y - (gridSize * Math.round(node.y / gridSize)); + return gridOffset; + } + return { init: function() { RED.actions.add("core:show-selected-node-labels", function() { setSelectedNodeLabelState(true); }) @@ -870,6 +1031,8 @@ RED.view.tools = (function() { RED.actions.add("core:wire-series-of-nodes", function() { wireSeriesOfNodes() }) RED.actions.add("core:wire-node-to-multiple", function() { wireNodeToMultiple() }) + RED.actions.add("core:split-wire-with-link-nodes",function(){splitWiresWithLinkNodes(RED.view.selectedLinks.length() ? RED.view.selectedLinks.toArray() : null);}); + // RED.actions.add("core:add-node", function() { addNode() }) }, /** @@ -881,7 +1044,8 @@ RED.view.tools = (function() { * @param {Number} dx * @param {Number} dy */ - moveSelection: moveSelection + moveSelection: moveSelection, + calculateGridSnapOffsets: calculateGridSnapOffsets } })(); 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 5b9285152..c20140376 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 @@ -449,7 +449,7 @@ RED.view = (function() { drop: function( event, ui ) { d3.event = event; var selected_tool = $(ui.draggable[0]).attr("data-palette-type"); - var result = addNode(selected_tool); + var result = RED.nodes.createNode(selected_tool); if (!result) { return; } @@ -493,15 +493,7 @@ RED.view = (function() { nn.y = mousePos[1]; if (snapGrid) { - var gridOffset = [0,0]; - var offsetLeft = nn.x-(gridSize*Math.round((nn.x-nn.w/2)/gridSize)+nn.w/2); - var offsetRight = nn.x-(gridSize*Math.round((nn.x+nn.w/2)/gridSize)-nn.w/2); - if (Math.abs(offsetLeft) < Math.abs(offsetRight)) { - gridOffset[0] = offsetLeft - } else { - gridOffset[0] = offsetRight - } - gridOffset[1] = nn.y-(gridSize*Math.round(nn.y/gridSize)); + var gridOffset = calculateGridSnapOffsets(nn); nn.x -= gridOffset[0]; nn.y -= gridOffset[1]; } @@ -927,81 +919,6 @@ RED.view = (function() { } } - function addNode(type,x,y) { - var m = /^subflow:(.+)$/.exec(type); - - if (activeSubflow && m) { - var subflowId = m[1]; - if (subflowId === activeSubflow.id) { - RED.notify(RED._("notification.error",{message: RED._("notification.errors.cannotAddSubflowToItself")}),"error"); - return; - } - if (RED.nodes.subflowContains(m[1],activeSubflow.id)) { - RED.notify(RED._("notification.error",{message: RED._("notification.errors.cannotAddCircularReference")}),"error"); - return; - } - } - - var nn = { id:RED.nodes.id(),z:RED.workspaces.active()}; - - nn.type = type; - nn._def = RED.nodes.getType(nn.type); - - if (!m) { - nn.inputs = nn._def.inputs || 0; - nn.outputs = nn._def.outputs; - - for (var d in nn._def.defaults) { - if (nn._def.defaults.hasOwnProperty(d)) { - if (nn._def.defaults[d].value !== undefined) { - nn[d] = JSON.parse(JSON.stringify(nn._def.defaults[d].value)); - } - } - } - - if (nn._def.onadd) { - try { - nn._def.onadd.call(nn); - } catch(err) { - console.log("Definition error: "+nn.type+".onadd:",err); - } - } - } else { - var subflow = RED.nodes.subflow(m[1]); - nn.name = ""; - nn.inputs = subflow.in.length; - nn.outputs = subflow.out.length; - } - - nn.changed = true; - nn.moved = true; - - nn.w = node_width; - nn.h = Math.max(node_height,(nn.outputs||0) * 15); - nn.resize = true; - - var historyEvent = { - t:"add", - nodes:[nn.id], - dirty:RED.nodes.dirty() - } - if (activeSubflow) { - var subflowRefresh = RED.subflow.refresh(true); - if (subflowRefresh) { - historyEvent.subflow = { - id:activeSubflow.id, - changed: activeSubflow.changed, - instances: subflowRefresh.instances - } - } - } - return { - node: nn, - historyEvent: historyEvent - } - - } - function canvasMouseDown() { if (RED.view.DEBUG) { console.warn("canvasMouseDown", mouse_mode); } var point; @@ -1190,7 +1107,7 @@ RED.view = (function() { keepAdding = false; resetMouseVars(); } - var result = addNode(type); + var result = RED.nodes.createNode(type); if (!result) { return; } @@ -1643,16 +1560,9 @@ RED.view = (function() { gridOffset[0] = node.n.x-(gridSize*Math.floor(node.n.x/gridSize))-gridSize/2; gridOffset[1] = node.n.y-(gridSize*Math.floor(node.n.y/gridSize))-gridSize/2; } else { - var offsetLeft = node.n.x-(gridSize*Math.round((node.n.x-node.n.w/2)/gridSize)+node.n.w/2); - var offsetRight = node.n.x-(gridSize*Math.round((node.n.x+node.n.w/2)/gridSize)-node.n.w/2); - // gridOffset[0] = node.n.x-(gridSize*Math.floor((node.n.x-node.n.w/2)/gridSize)+node.n.w/2); - if (Math.abs(offsetLeft) < Math.abs(offsetRight)) { - gridOffset[0] = offsetLeft - } else { - gridOffset[0] = offsetRight - } - gridOffset[1] = node.n.y-(gridSize*Math.round(node.n.y/gridSize)); - // console.log(offsetLeft, offsetRight); + const snapOffsets = RED.view.tools.calculateGridSnapOffsets(node.n); + gridOffset[0] = snapOffsets.x; + gridOffset[1] = snapOffsets.y; } if (gridOffset[0] !== 0 || gridOffset[1] !== 0) { for (i = 0; i