diff --git a/public/icons/subflow.png b/public/icons/subflow.png
new file mode 100644
index 000000000..7c1c14f38
Binary files /dev/null and b/public/icons/subflow.png differ
diff --git a/public/index.html b/public/index.html
index f8f6e0d4d..355146ff0 100644
--- a/public/index.html
+++ b/public/index.html
@@ -51,11 +51,7 @@
@@ -81,6 +77,20 @@
+
-
+
+
diff --git a/public/red/history.js b/public/red/history.js
index 3a6d55133..c1d3808f5 100644
--- a/public/red/history.js
+++ b/public/red/history.js
@@ -50,6 +50,12 @@ RED.history = (function() {
RED.view.removeWorkspace(ev.workspaces[i]);
}
}
+ if (ev.subflows) {
+ for (i=0;i ev.subflow.inputCount) {
+ ev.node.in.splice(ev.subflow.inputCount);
+ } else if (ev.subflow.inputs.length > 0) {
+ ev.node.in = ev.node.in.concat(ev.subflow.inputs);
+ }
+ if (ev.node.out.length > ev.subflow.outputCount) {
+ ev.node.out.splice(ev.subflow.outputCount);
+ } else if (ev.subflow.outputs.length > 0) {
+ ev.node.out = ev.node.out.concat(ev.subflow.outputs);
+ }
+ RED.nodes.eachNode(function(n) {
+ if (n.type == "subflow:"+ev.node.id) {
+ n.changed = ev.changed;
+ n.inputs = ev.node.in.length;
+ n.outputs = ev.node.out.length;
+ RED.editor.updateNodeProperties(n);
+ }
+ });
+
+
+
+ RED.palette.refresh();
+ } else {
+ RED.editor.updateNodeProperties(ev.node);
+ RED.editor.validateNode(ev.node);
+ }
if (ev.links) {
for (i=0;iEither, add missing types to Node-RED, restart and then reload page,
or delete unknown "+n.name+", rewire as required, and then deploy.","error");
}
+ var activeWorkspace = RED.view.getWorkspace();
+ var activeSubflow = getSubflow(activeWorkspace);
+ if (activeSubflow) {
+ for (i=0;iError: "+error,"error");
+ if (error.code != "NODE_RED") {
+ console.log(error.stack);
+ RED.notify("Error: "+error,"error");
+ } else {
+ RED.notify("Error: "+error.message,"error");
+ }
return null;
}
@@ -516,13 +741,22 @@ RED.nodes = (function() {
registerType: registry.registerNodeType,
getType: registry.getNodeType,
convertNode: convertNode,
+
add: addNode,
- addLink: addLink,
remove: removeNode,
+
+ addLink: addLink,
removeLink: removeLink,
+
addWorkspace: addWorkspace,
removeWorkspace: removeWorkspace,
workspace: getWorkspace,
+
+ addSubflow: addSubflow,
+ removeSubflow: removeSubflow,
+ subflow: getSubflow,
+ subflowContains: subflowContains,
+
eachNode: function(cb) {
for (var n=0;n= node.outputs) {
+ removedLinks.push(l);
+ }
+ });
+ } else if (node.outputs > node.ports.length) {
+ while (node.outputs > node.ports.length) {
+ node.ports.push(node.ports.length);
+ }
}
+ }
+ if (node.inputs === 0) {
RED.nodes.eachLink(function(l) {
- if (l.source === node && l.sourcePort >= node.outputs) {
- removedLinks.push(l);
- }
+ if (l.target === node) {
+ removedLinks.push(l);
+ }
});
- for (var l=0;l node.ports.length) {
- while (node.outputs > node.ports.length) {
- node.ports.push(node.ports.length);
- }
+ }
+ for (var l=0;l').appendTo("#dialog-form");
prepareEditDialog(node,node._def,"node-input");
- $( "#dialog" ).dialog("option","title","Edit "+node.type+" node").dialog( "open" );
+
+
+
+
+
+ $( "#dialog" ).dialog("option","title","Edit "+type+" node").dialog( "open" );
}
function showEditConfigNodeDialog(name,type,id) {
@@ -655,10 +689,207 @@ RED.editor = (function() {
}
});
+ $( "#subflow-dialog" ).dialog({
+ modal: true,
+ autoOpen: false,
+ closeOnEscape: false,
+ width: 500,
+ buttons: [
+ {
+ class: 'leftButton',
+ text: "Delete",
+ click: function() {
+ var removedNodes = [];
+ var removedLinks = [];
+ var startDirty = RED.view.dirty();
+ RED.nodes.eachNode(function(n) {
+ if (n.type == "subflow:"+editing_node.id) {
+ removedNodes.push(n);
+ }
+ if (n.z == editing_node.id) {
+ removedNodes.push(n);
+ }
+ });
+
+ for (var i=0;i newInCount) {
+ removedInputs = editing_node.in.splice(newInCount);
+ changed = true;
+ }
+ if (editing_node.out.length < newOutCount) {
+ for (i=editing_node.out.length;i newOutCount) {
+ removedOutputs = editing_node.out.splice(newOutCount);
+ changed = true;
+ }
+
+ if (removedOutputs.length > 0 || removedInputs.length > 0) {
+ RED.nodes.eachLink(function(l) {
+ if (newInCount === 0 && l.source.type == "subflow" && l.source.z == editing_node.id) {
+ removedLinks.push(l);
+ return;
+ }
+ if (l.target.type == "subflow" && l.target.z == editing_node.id && l.target.i >= newOutCount) {
+ removedLinks.push(l);
+ return;
+ }
+ });
+ removedLinks.forEach(function(l) { RED.nodes.removeLink(l)});
+ }
+ RED.palette.refresh();
+
+ if (changed) {
+ RED.nodes.eachNode(function(n) {
+ if (n.type == "subflow:"+editing_node.id) {
+ n.changed = true;
+ n.inputs = editing_node.in.length;
+ n.outputs = editing_node.out.length;
+ removedLinks = removedLinks.concat(updateNodeProperties(n));
+ }
+ });
+ var wasChanged = editing_node.changed;
+ editing_node.changed = true;
+ RED.view.dirty(true);
+ var historyEvent = {
+ t:'edit',
+ node:editing_node,
+ changes:changes,
+ links:removedLinks,
+ dirty:wasDirty,
+ changed:wasChanged,
+ subflow: {
+ outputCount: oldOutCount,
+ inputCount: oldInCount,
+ outputs: removedOutputs,
+ inputs: removedInputs
+ }
+ };
+
+ RED.history.push(historyEvent);
+ }
+ editing_node.dirty = true;
+ RED.view.redraw();
+ }
+ $( this ).dialog( "close" );
+ }
+ },
+ {
+ id: "subflow-dialog-cancel",
+ text: "Cancel",
+ click: function() {
+ $( this ).dialog( "close" );
+ editing_node = null;
+ }
+ }
+ ],
+ open: function(e) {
+ RED.keyboard.disable();
+ },
+ close: function(e) {
+ RED.keyboard.enable();
+
+ if (RED.view.state() != RED.state.IMPORT_DRAGGING) {
+ RED.view.state(RED.state.DEFAULT);
+ }
+ RED.sidebar.info.refresh(editing_node);
+ editing_node = null;
+ }
+ });
+
+ function showEditSubflowDialog(subflow) {
+ editing_node = subflow;
+ RED.view.state(RED.state.EDITING);
+ $("#subflow-input-name").val(subflow.name);
+ $("#subflow-input-inCount").spinner({ min:0, max:1 }).val(subflow.in.length);
+ $("#subflow-input-outCount").spinner({ min:0 }).val(subflow.out.length);
+ var userCount = 0;
+ var subflowType = "subflow:"+editing_node.id;
+
+ RED.nodes.eachNode(function(n) {
+ if (n.type === subflowType) {
+ userCount++;
+ }
+ });
+
+
+ $("#subflow-dialog-user-count").html("There "+(userCount==1?"is":"are")+" "+userCount+" instance"+(userCount==1?" ":"s")+" of this subflow").show();
+ $("#subflow-dialog").dialog("option","title","Edit flow "+subflow.name).dialog( "open" );
+ }
+
+
+
return {
edit: showEditDialog,
editConfig: showEditConfigNodeDialog,
+ editSubflow: showEditSubflowDialog,
validateNode: validateNode,
updateNodeProperties: updateNodeProperties // TODO: only exposed for edit-undo
}
diff --git a/public/red/ui/menu.js b/public/red/ui/menu.js
index 07ea5fd2e..ce0ab9199 100644
--- a/public/red/ui/menu.js
+++ b/public/red/ui/menu.js
@@ -36,14 +36,14 @@ RED.menu = (function() {
if (opt.onselect) {
link.click(function() {
- if ($(this).parent().hasClass("disabled")) {
- return;
- }
- if (opt.toggle) {
- setSelected(opt.id,!isSelected(opt.id));
- } else {
- opt.onselect.call(opt);
- }
+ if ($(this).parent().hasClass("disabled")) {
+ return;
+ }
+ if (opt.toggle) {
+ setSelected(opt.id,!isSelected(opt.id));
+ } else {
+ opt.onselect.call(opt);
+ }
})
} else if (opt.href) {
link.attr("target","_blank").attr("href",opt.href);
@@ -110,13 +110,28 @@ RED.menu = (function() {
$("#"+id).parent().remove();
}
+ function setAction(id,action) {
+ menuItems[id].onselect = action;
+ $("#"+id).click(function() {
+ if ($(this).parent().hasClass("disabled")) {
+ return;
+ }
+ if (menuItems[id].toggle) {
+ setSelected(id,!isSelected(id));
+ } else {
+ menuItems[id].onselect.call(menuItems[id]);
+ }
+ });
+ }
+
return {
init: createMenu,
setSelected: setSelected,
isSelected: isSelected,
setDisabled: setDisabled,
addItem: addItem,
- removeItem: removeItem
+ removeItem: removeItem,
+ setAction: setAction
//TODO: add an api for replacing a submenu - see library.js:loadFlowLibrary
}
})();
diff --git a/public/red/ui/palette.js b/public/red/ui/palette.js
index 27664a576..e0fbcc9c4 100644
--- a/public/red/ui/palette.js
+++ b/public/red/ui/palette.js
@@ -17,7 +17,7 @@
RED.palette = (function() {
var exclusion = ['config','unknown','deprecated'];
- var core = ['input', 'output', 'function', 'social', 'storage', 'analysis', 'advanced'];
+ var core = ['input', 'output', 'function', 'subflows', 'social', 'storage', 'analysis', 'advanced'];
function createCategoryContainer(category){
var escapedCategory = category.replace(" ","_");
@@ -38,10 +38,66 @@ RED.palette = (function() {
core.forEach(createCategoryContainer);
+ function setLabel(type, el,label) {
+ var nodeWidth = 80;
+ var nodeHeight = 25;
+ var lineHeight = 20;
+ var portHeight = 10;
+
+ var words = label.split(" ");
+
+ var displayLines = [];
+
+ var currentLine = words[0];
+ var currentLineWidth = RED.view.calculateTextWidth(currentLine, "palette_label", 0);
+
+ for (var i=1;i");
+ var multiLineNodeHeight = 8+(lineHeight*displayLines.length);
+ el.css({height:multiLineNodeHeight+"px"});
+
+ var labelElement = el.find(".palette_label");
+ labelElement.html(lines);
+
+ el.find(".palette_port").css({top:(multiLineNodeHeight/2-5)+"px"});
+
+ var popOverContent;
+ try {
+ var l = ""+label+"
";
+ if (label != type) {
+ l = ""+label+"
"+type+"
";
+ }
+ popOverContent = $(l+($("script[data-help-name|='"+type+"']").html()||"no information available
").trim()).slice(0,2);
+ } catch(err) {
+ // Malformed HTML may cause errors. TODO: need to understand what can break
+ console.log("Error generating pop-over label for '"+type+"'.");
+ console.log(err.toString());
+ popOverContent = ""+label+"
no information available
";
+ }
+
+
+ el.data('popover').options.content = popOverContent;
+ }
+
+ function escapeNodeType(nt) {
+ return nt.replace(" ","_").replace(".","_").replace(":","_");
+ }
+
function addNodeType(nt,def) {
-
- var nodeTypeId = nt.replace(" ","_");
-
+
+ var nodeTypeId = escapeNodeType(nt);
if ($("#palette_node_"+nodeTypeId).length) {
return;
}
@@ -63,27 +119,12 @@ RED.palette = (function() {
label = (typeof def.paletteLabel === "function" ? def.paletteLabel.call(def) : def.paletteLabel)||"";
}
- var pixels = RED.view.calculateTextWidth(label, "palette_label", 0);
- var nodeWidth = 90;
- var numLines = Math.ceil(pixels / nodeWidth);
- var multiLine = numLines > 1;
-
- // styles matching with style.css
- var nodeHeight = 25;
- var lineHeight = 16;
- var portHeight = 10;
- var multiLineNodeHeight = lineHeight * numLines + (nodeHeight - lineHeight);
-
- d.innerHTML = ''+label+"
";
+ d.innerHTML = '';
+
d.className="palette_node";
if (def.icon) {
d.style.backgroundImage = "url(icons/"+def.icon+")";
- if (multiLine) {
- d.style.backgroundSize = "18px 27px";
- }
+ d.style.backgroundSize = "18px 27px";
if (def.align == "right") {
d.style.backgroundPosition = "95% 50%";
} else if (def.inputs > 0) {
@@ -92,23 +133,16 @@ RED.palette = (function() {
}
d.style.backgroundColor = def.color;
- d.style.height = multiLineNodeHeight + "px";
if (def.outputs > 0) {
var portOut = document.createElement("div");
portOut.className = "palette_port palette_port_output";
- if (multiLine) {
- portOut.style.top = ((multiLineNodeHeight - portHeight) / 2) + "px";
- }
d.appendChild(portOut);
}
if (def.inputs > 0) {
var portIn = document.createElement("div");
- portIn.className = "palette_port";
- if (multiLine) {
- portIn.style.top = ((multiLineNodeHeight - portHeight) / 2) + "px";
- }
+ portIn.className = "palette_port palette_port_input";
d.appendChild(portIn);
}
@@ -123,23 +157,13 @@ RED.palette = (function() {
$("#palette-"+category).append(d);
d.onmousedown = function(e) { e.preventDefault(); };
- var popOverContent;
- try {
- popOverContent = $(""+label+"
"+($("script[data-help-name|='"+nt+"']").html().trim()||"no information available
")).slice(0,2);
- } catch(err) {
- // Malformed HTML may cause errors. TODO: need to understand what can break
- console.log("Error generating pop-over label for '"+nt+"'.");
- console.log(err.toString());
- popOverContent = ""+label+"
no information available
";
- }
$(d).popover({
title:d.type,
placement:"right",
trigger: "hover",
delay: { show: 750, hide: 50 },
html: true,
- container:'body',
- content: popOverContent
+ container:'body'
});
$(d).click(function() {
var help = ''+($("script[data-help-name|='"+d.type+"']").html()||"")+"
";
@@ -151,23 +175,50 @@ RED.palette = (function() {
revert: true,
revertDuration: 50
});
+
+ setLabel(nt,$(d),label);
}
}
function removeNodeType(nt) {
- var nodeTypeId = nt.replace(" ","_");
+ var nodeTypeId = escapeNodeType(nt);
$("#palette_node_"+nodeTypeId).remove();
}
function hideNodeType(nt) {
- var nodeTypeId = nt.replace(" ","_");
+ var nodeTypeId = escapeNodeType(nt);
$("#palette_node_"+nodeTypeId).hide();
}
function showNodeType(nt) {
- var nodeTypeId = nt.replace(" ","_");
+ var nodeTypeId = escapeNodeType(nt);
$("#palette_node_"+nodeTypeId).show();
}
-
+
+ function refreshNodeTypes() {
+ RED.nodes.eachSubflow(function(sf) {
+ var paletteNode = $("#palette_node_subflow_"+sf.id.replace(".","_"));
+ var portInput = paletteNode.find(".palette_port_input");
+ var portOutput = paletteNode.find(".palette_port_output");
+
+ if (portInput.length === 0 && sf.in.length > 0) {
+ var portIn = document.createElement("div");
+ portIn.className = "palette_port palette_port_input";
+ paletteNode.append(portIn);
+ } else if (portInput.length !== 0 && sf.in.length === 0) {
+ portInput.remove();
+ }
+
+ if (portOutput === 0 && sf.out.length > 0) {
+ var portOut = document.createElement("div");
+ portOut.className = "palette_port palette_port_output";
+ paletteNode.append(portOut);
+ } else if (portOutput !== 0 && sf.out.length === 0) {
+ portOutput.remove();
+ }
+ setLabel(sf.type+":"+sf.id,paletteNode,sf.name);
+ });
+ }
+
function filterChange() {
var val = $("#palette-search-input").val();
if (val === "") {
@@ -215,6 +266,7 @@ RED.palette = (function() {
add:addNodeType,
remove:removeNodeType,
hide:hideNodeType,
- show:showNodeType
+ show:showNodeType,
+ refresh:refreshNodeTypes
};
})();
diff --git a/public/red/ui/tab-info.js b/public/red/ui/tab-info.js
index bb8eb044c..9aa194872 100644
--- a/public/red/ui/tab-info.js
+++ b/public/red/ui/tab-info.js
@@ -47,37 +47,53 @@ RED.sidebar.info = (function() {
table += "Type | "+node.type+" |
";
table += "ID | "+node.id+" |
";
table += 'Properties |
';
- for (var n in node._def.defaults) {
- if (node._def.defaults.hasOwnProperty(n)) {
- var val = node[n]||"";
- var type = typeof val;
- if (type === "string") {
- if (val.length > 30) {
- val = val.substring(0,30)+" ...";
- }
- val = val.replace(/&/g,"&").replace(//g,">");
- } else if (type === "number") {
- val = val.toString();
- } else if ($.isArray(val)) {
- val = "[
";
- for (var i=0;i/g,">");
- val += " "+i+": "+vv+"
";
- }
- if (node[n].length > 10) {
- val += " ... "+node[n].length+" items
";
- }
- val += "]";
- } else {
- val = JSON.stringify(val,jsonFilter," ");
- val = val.replace(/&/g,"&").replace(//g,">");
+ if (node.type == "subflow") {
+ var userCount = 0;
+ var subflowType = "subflow:"+node.id;
+ RED.nodes.eachNode(function(n) {
+ if (n.type === subflowType) {
+ userCount++;
+ }
+ });
+ table += "name | "+node.name+" |
";
+ table += "inputs | "+node.in.length+" |
";
+ table += "outputs | "+node.out.length+" |
";
+ table += "instances | "+userCount+" |
";
+ }
+ if (node._def) {
+ for (var n in node._def.defaults) {
+ if (node._def.defaults.hasOwnProperty(n)) {
+ var val = node[n]||"";
+ var type = typeof val;
+ if (type === "string") {
+ if (val.length > 30) {
+ val = val.substring(0,30)+" ...";
+ }
+ val = val.replace(/&/g,"&").replace(//g,">");
+ } else if (type === "number") {
+ val = val.toString();
+ } else if ($.isArray(val)) {
+ val = "[
";
+ for (var i=0;i/g,">");
+ val += " "+i+": "+vv+"
";
+ }
+ if (node[n].length > 10) {
+ val += " ... "+node[n].length+" items
";
+ }
+ val += "]";
+ } else {
+ val = JSON.stringify(val,jsonFilter," ");
+ val = val.replace(/&/g,"&").replace(//g,">");
+ }
+
+ table += ""+n+" | "+val+" |
";
}
-
- table += " "+n+" | "+val+" |
";
}
}
table += "
";
- table += ''+($("script[data-help-name|='"+node.type+"']").html()||"")+"
";
+ var helpText = $("script[data-help-name|='"+node.type+"']").html()||"";
+ table += ''+helpText+"
";
$("#tab-info").html(table);
}
diff --git a/public/red/ui/tabs.js b/public/red/ui/tabs.js
index aa2075835..c9239fe32 100644
--- a/public/red/ui/tabs.js
+++ b/public/red/ui/tabs.js
@@ -116,6 +116,13 @@ RED.tabs = (function() {
},
contains: function(id) {
return ul.find("a[href='#"+id+"']").length > 0;
+ },
+ renameTab: function(id,name) {
+ tabs[id].name = name;
+ var tab = ul.find("a[href='#"+id+"']");
+ tab.attr("title",name);
+ tab.text(name);
+ updateTabWidths();
}
}
diff --git a/public/red/ui/view.js b/public/red/ui/view.js
index 275cbe1c2..e56cbbed1 100644
--- a/public/red/ui/view.js
+++ b/public/red/ui/view.js
@@ -31,6 +31,8 @@ RED.view = (function() {
var activeWorkspace = 0;
+ var activeSubflow = null;
+
var workspaceScrollPositions = {};
var selected_link = null,
@@ -225,6 +227,11 @@ RED.view = (function() {
var drag_line = vis.append("svg:path").attr("class", "drag_line");
+ $("#workspace-edit-subflow").click(function(event) {
+ showSubflowDialog(activeSubflow.id);
+ event.preventDefault();
+ });
+
var workspace_tabs = RED.tabs.create({
id: "workspace-tabs",
onchange: function(tab) {
@@ -244,6 +251,8 @@ RED.view = (function() {
var scrollStartTop = chart.scrollTop();
activeWorkspace = tab.id;
+ activeSubflow = RED.nodes.subflow(activeWorkspace);
+
if (workspaceScrollPositions[activeWorkspace]) {
chart.scrollLeft(workspaceScrollPositions[activeWorkspace].left);
chart.scrollTop(workspaceScrollPositions[activeWorkspace].top);
@@ -257,6 +266,9 @@ RED.view = (function() {
mouse_position[0] += scrollDeltaLeft;
mouse_position[1] += scrollDeltaTop;
}
+
+ RED.menu.setDisabled("btn-workspace-edit", activeSubflow);
+ RED.menu.setDisabled("btn-workspace-delete",workspace_tabs.count() == 1 || activeSubflow);
clearSelection();
RED.nodes.eachNode(function(n) {
@@ -265,7 +277,11 @@ RED.view = (function() {
redraw();
},
ondblclick: function(tab) {
- showRenameWorkspaceDialog(tab.id);
+ if (tab.type != "subflow") {
+ showRenameWorkspaceDialog(tab.id);
+ } else {
+ showSubflowDialog(tab.id);
+ }
},
onadd: function(tab) {
RED.menu.addItem("btn-workspace-menu",{
@@ -300,11 +316,12 @@ RED.view = (function() {
}
$(function() {
$('#btn-workspace-add-tab').on("click",addWorkspace);
- $('#btn-workspace-add').on("click",addWorkspace);
- $('#btn-workspace-edit').on("click",function() {
+
+ RED.menu.setAction('btn-workspace-add',addWorkspace);
+ RED.menu.setAction('btn-workspace-edit',function() {
showRenameWorkspaceDialog(activeWorkspace);
});
- $('#btn-workspace-delete').on("click",function() {
+ RED.menu.setAction('btn-workspace-delete',function() {
deleteWorkspace(activeWorkspace);
});
});
@@ -349,7 +366,6 @@ RED.view = (function() {
function canvasMouseMove() {
mouse_position = d3.touches(this)[0]||d3.mouse(this);
-
// Prevent touch scrolling...
//if (d3.touches(this)[0]) {
// d3.event.preventDefault();
@@ -478,7 +494,9 @@ RED.view = (function() {
}
}
}
- redraw();
+ if (mouse_mode !== 0) {
+ redraw();
+ }
}
function canvasMouseUp() {
@@ -502,6 +520,22 @@ RED.view = (function() {
}
}
});
+ if (activeSubflow) {
+ activeSubflow.in.forEach(function(n) {
+ 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});
+ }
+ });
+ activeSubflow.out.forEach(function(n) {
+ 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});
+ }
+ });
+ }
updateSelection();
lasso.remove();
lasso = null;
@@ -545,11 +579,28 @@ RED.view = (function() {
else { zoomIn(); }
}
});
+
$("#chart").droppable({
accept:".palette_node",
drop: function( event, ui ) {
d3.event = event;
var selected_tool = ui.draggable[0].type;
+
+ var m = /^subflow:(.+)$/.exec(selected_tool);
+
+ if (activeSubflow && m) {
+ var subflowId = m[1];
+ if (subflowId === activeSubflow.id) {
+ RED.notify("Error: Cannot add subflow to itself","error");
+ return;
+ }
+ if (RED.nodes.subflowContains(m[1],activeSubflow.id)) {
+ RED.notify("Error: Cannot add subflow - circular reference detected","error");
+ return;
+ }
+
+ }
+
var mousePos = d3.touches(this)[0]||d3.mouse(this);
mousePos[1] += this.scrollTop;
mousePos[0] += this.scrollLeft;
@@ -560,17 +611,25 @@ RED.view = (function() {
nn.type = selected_tool;
nn._def = RED.nodes.getType(nn.type);
- nn.outputs = nn._def.outputs;
- nn.changed = true;
- for (var d in nn._def.defaults) {
- if (nn._def.defaults.hasOwnProperty(d)) {
- nn[d] = nn._def.defaults[d].value;
+ if (!m) {
+ nn.inputs = nn._def.inputs || 0;
+ nn.outputs = nn._def.outputs;
+ nn.changed = true;
+
+ for (var d in nn._def.defaults) {
+ if (nn._def.defaults.hasOwnProperty(d)) {
+ nn[d] = nn._def.defaults[d].value;
+ }
}
- }
-
- if (nn._def.onadd) {
- nn._def.onadd.call(nn);
+
+ if (nn._def.onadd) {
+ nn._def.onadd.call(nn);
+ }
+ } else {
+ var subflow = RED.nodes.subflow(m[1]);
+ nn.inputs = subflow.in.length;
+ nn.outputs = subflow.out.length;
}
nn.h = Math.max(node_height,(nn.outputs||0) * 15);
@@ -618,6 +677,23 @@ RED.view = (function() {
}
}
});
+ if (activeSubflow) {
+ activeSubflow.in.forEach(function(n) {
+ if (!n.selected) {
+ n.selected = true;
+ n.dirty = true;
+ moving_set.push({n:n});
+ }
+ });
+ activeSubflow.out.forEach(function(n) {
+ if (!n.selected) {
+ n.selected = true;
+ n.dirty = true;
+ moving_set.push({n:n});
+ }
+ });
+ }
+
selected_link = null;
updateSelection();
redraw();
@@ -638,10 +714,12 @@ RED.view = (function() {
RED.menu.setDisabled("btn-export-menu",true);
RED.menu.setDisabled("btn-export-clipboard",true);
RED.menu.setDisabled("btn-export-library",true);
+ RED.menu.setDisabled("btn-convert-subflow",true);
} else {
RED.menu.setDisabled("btn-export-menu",false);
RED.menu.setDisabled("btn-export-clipboard",false);
RED.menu.setDisabled("btn-export-library",false);
+ RED.menu.setDisabled("btn-convert-subflow",false);
}
if (moving_set.length === 0 && selected_link == null) {
RED.keyboard.remove(/* backspace */ 8);
@@ -666,7 +744,13 @@ RED.view = (function() {
RED.keyboard.add(/* right*/ 39, function() { if(d3.event.shiftKey){moveSelection( 20, 0)}else{moveSelection( 1, 0);}d3.event.preventDefault();},endKeyboardMove);
}
if (moving_set.length == 1) {
- RED.sidebar.info.refresh(moving_set[0].n);
+ if (moving_set[0].n.type === "subflow" && !moving_set[0].n.id) {
+ RED.sidebar.info.refresh(RED.nodes.subflow(moving_set[0].n.z));
+ } else {
+ RED.sidebar.info.refresh(moving_set[0].n);
+ }
+ } else if (moving_set.length === 0 && activeSubflow) {
+ RED.sidebar.info.refresh(activeSubflow);
} else {
RED.sidebar.info.clear();
}
@@ -716,15 +800,21 @@ RED.view = (function() {
for (var i=0;i 0) {
+ setDirty(true);
+ }
}
if (selected_link) {
RED.nodes.removeLink(selected_link);
@@ -743,10 +833,12 @@ RED.view = (function() {
var nns = [];
for (var n=0;n1?"s":"")+" copied");
+ RED.notify(nns.length+" node"+(nns.length>1?"s":"")+" copied");
}
}
@@ -772,6 +864,7 @@ RED.view = (function() {
}
function portMouseDown(d,portType,portIndex) {
+ //console.log(d,portType,portIndex);
// disable zoom
//vis.call(d3.behavior.zoom().on("zoom"), null);
mousedown_node = d;
@@ -794,7 +887,7 @@ RED.view = (function() {
if (n.x-hw mouse_position[0] &&
n.y-hhmouse_position[1]) {
mouseup_node = n;
- portType = mouseup_node._def.inputs>0?1:0;
+ portType = mouseup_node.inputs>0?1:0;
portIndex = 0;
}
}
@@ -817,16 +910,16 @@ RED.view = (function() {
dst = mousedown_node;
src_port = portIndex;
}
-
var existingLink = false;
RED.nodes.eachLink(function(d) {
- existingLink = existingLink || (d.source === src && d.target === dst && d.sourcePort == src_port);
+ existingLink = existingLink || (d.source === src && d.target === dst && d.sourcePort == src_port);
});
if (!existingLink) {
var link = {source: src, sourcePort:src_port, target: dst};
RED.nodes.addLink(link);
RED.history.push({t:'add',links:[link],dirty:dirty});
setDirty(true);
+ } else {
}
selected_link = null;
redraw();
@@ -835,12 +928,18 @@ RED.view = (function() {
function nodeMouseUp(d) {
if (dblClickPrimed && mousedown_node == d && clickElapsed > 0 && clickElapsed < 750) {
- RED.editor.edit(d);
+ mouse_mode = RED.state.DEFAULT;
+ if (d.type != "subflow") {
+ RED.editor.edit(d);
+ } else {
+ RED.editor.editSubflow(activeSubflow);
+ }
clickElapsed = 0;
d3.event.stopPropagation();
return;
}
- portMouseUp(d, d._def.inputs > 0 ? 1 : 0, 0);
+ var direction = d._def? (d.inputs > 0 ? 1: 0) : (d.direction == "in" ? 0: 1)
+ portMouseUp(d, direction, 0);
}
function nodeMouseDown(d) {
@@ -949,6 +1048,114 @@ RED.view = (function() {
if (mouse_mode != RED.state.JOINING) {
// Don't bother redrawing nodes if we're drawing links
+ if (activeSubflow) {
+ var subflowOutputs = vis.selectAll(".subflowoutput").data(activeSubflow.out,function(d,i){ return d.z+":"+i;});
+ subflowOutputs.exit().remove();
+ var outGroup = subflowOutputs.enter().insert("svg:g").attr("class","node subflowoutput").attr("transform",function(d) { return "translate("+(d.x-20)+","+(d.y-20)+")"});
+ outGroup.each(function(d,i) {
+ d.w=40;
+ d.h=40;
+ });
+ outGroup.append("rect").attr("class","subflowport").attr("rx",8).attr("ry",8).attr("width",40).attr("height",40)
+ // TODO: This is exactly the same set of handlers used for regular nodes - DRY
+ .on("mouseup",nodeMouseUp)
+ .on("mousedown",nodeMouseDown)
+ .on("touchstart",function(d) {
+ var obj = d3.select(this);
+ var touch0 = d3.event.touches.item(0);
+ var pos = [touch0.pageX,touch0.pageY];
+ startTouchCenter = [touch0.pageX,touch0.pageY];
+ startTouchDistance = 0;
+ touchStartTime = setTimeout(function() {
+ showTouchMenu(obj,pos);
+ },touchLongPressTimeout);
+ nodeMouseDown.call(this,d)
+ })
+ .on("touchend", function(d) {
+ clearTimeout(touchStartTime);
+ touchStartTime = null;
+ if (RED.touch.radialMenu.active()) {
+ d3.event.stopPropagation();
+ return;
+ }
+ nodeMouseUp.call(this,d);
+ });
+
+ outGroup.append("rect").attr("class","port").attr("rx",3).attr("ry",3).attr("width",10).attr("height",10).attr("x",-5).attr("y",15)
+ .on("mousedown", function(d,i){portMouseDown(d,1,0);} )
+ .on("touchstart", function(d,i){portMouseDown(d,1,0);} )
+ .on("mouseup", function(d,i){portMouseUp(d,1,0);})
+ .on("touchend",function(d,i){portMouseUp(d,1,0);} )
+ .on("mouseover",function(d,i) { var port = d3.select(this); port.classed("port_hovered",(mouse_mode!=RED.state.JOINING || mousedown_port_type !== 0 ));})
+ .on("mouseout",function(d,i) { var port = d3.select(this); port.classed("port_hovered",false);});
+
+ outGroup.append("svg:text").attr('class','port_label').attr('x',20).attr('y',8).style("font-size","10px").text("output");
+ outGroup.append("svg:text").attr('class','port_label').attr('x',20).attr('y',24).text(function(d,i){ return i+1});
+
+ var subflowInputs = vis.selectAll(".subflowinput").data(activeSubflow.in,function(d,i){ return d.z+":"+i;});
+ subflowInputs.exit().remove();
+ var inGroup = subflowInputs.enter().insert("svg:g").attr("class","node subflowinput").attr("transform",function(d) { return "translate("+(d.x-20)+","+(d.y-20)+")"});
+ inGroup.each(function(d,i) {
+ d.w=40;
+ d.h=40;
+ });
+ inGroup.append("rect").attr("class","subflowport").attr("rx",8).attr("ry",8).attr("width",40).attr("height",40)
+ // TODO: This is exactly the same set of handlers used for regular nodes - DRY
+ .on("mouseup",nodeMouseUp)
+ .on("mousedown",nodeMouseDown)
+ .on("touchstart",function(d) {
+ var obj = d3.select(this);
+ var touch0 = d3.event.touches.item(0);
+ var pos = [touch0.pageX,touch0.pageY];
+ startTouchCenter = [touch0.pageX,touch0.pageY];
+ startTouchDistance = 0;
+ touchStartTime = setTimeout(function() {
+ showTouchMenu(obj,pos);
+ },touchLongPressTimeout);
+ nodeMouseDown.call(this,d)
+ })
+ .on("touchend", function(d) {
+ clearTimeout(touchStartTime);
+ touchStartTime = null;
+ if (RED.touch.radialMenu.active()) {
+ d3.event.stopPropagation();
+ return;
+ }
+ nodeMouseUp.call(this,d);
+ });
+
+ inGroup.append("rect").attr("class","port").attr("rx",3).attr("ry",3).attr("width",10).attr("height",10).attr("x",35).attr("y",15)
+ .on("mousedown", function(d,i){portMouseDown(d,0,i);} )
+ .on("touchstart", function(d,i){portMouseDown(d,0,i);} )
+ .on("mouseup", function(d,i){portMouseUp(d,0,i);})
+ .on("touchend",function(d,i){portMouseUp(d,0,i);} )
+ .on("mouseover",function(d,i) { var port = d3.select(this); port.classed("port_hovered",(mouse_mode!=RED.state.JOINING || mousedown_port_type !== 0 ));})
+ .on("mouseout",function(d,i) { var port = d3.select(this); port.classed("port_hovered",false);});
+ inGroup.append("svg:text").attr('class','port_label').attr('x',18).attr('y',20).style("font-size","10px").text("input");
+
+
+
+ subflowOutputs.each(function(d) {
+ if (d.dirty) {
+ var output = d3.select(this);
+ output.selectAll(".subflowport").classed("node_selected",function(d) { return d.selected; })
+ output.attr("transform", function(d) { return "translate(" + (d.x-d.w/2) + "," + (d.y-d.h/2) + ")"; });
+ d.dirty = false;
+ }
+ });
+ subflowInputs.each(function(d) {
+ if (d.dirty) {
+ var input = d3.select(this);
+ input.selectAll(".subflowport").classed("node_selected",function(d) { return d.selected; })
+ input.attr("transform", function(d) { return "translate(" + (d.x-d.w/2) + "," + (d.y-d.h/2) + ")"; });
+ d.dirty = false;
+ }
+ });
+ } else {
+ vis.selectAll(".subflowoutput").remove();
+ vis.selectAll(".subflowinput").remove();
+ }
+
var node = vis.selectAll(".nodegroup").data(RED.nodes.nodes.filter(function(d) { return d.z == activeWorkspace }),function(d){return d.id});
node.exit().remove();
@@ -1082,8 +1289,8 @@ RED.view = (function() {
//icon.attr('class','node_icon_shade node_icon_shade_'+d._def.align);
//icon.attr('class','node_icon_shade_border node_icon_shade_border_'+d._def.align);
}
-
- //if (d._def.inputs > 0 && d._def.align == null) {
+
+ //if (d.inputs > 0 && d._def.align == null) {
// icon_shade.attr("width",35);
// icon.attr("transform","translate(5,0)");
// icon_shade_border.attr("transform","translate(5,0)");
@@ -1133,17 +1340,6 @@ RED.view = (function() {
//node.append("circle").attr({"class":"centerDot","cx":0,"cy":0,"r":5});
- if (d._def.inputs > 0) {
- text.attr("x",38);
- node.append("rect").attr("class","port port_input").attr("rx",3).attr("ry",3).attr("x",-5).attr("width",10).attr("height",10)
- .on("mousedown",function(d){portMouseDown(d,1,0);})
- .on("touchstart",function(d){portMouseDown(d,1,0);})
- .on("mouseup",function(d){portMouseUp(d,1,0);} )
- .on("touchend",function(d){portMouseUp(d,1,0);} )
- .on("mouseover",function(d) { var port = d3.select(this); port.classed("port_hovered",(mouse_mode!=RED.state.JOINING || mousedown_port_type != 1 ));})
- .on("mouseout",function(d) { var port = d3.select(this); port.classed("port_hovered",false);})
- }
-
//node.append("path").attr("class","node_error").attr("d","M 3,-3 l 10,0 l -5,-8 z");
node.append("image").attr("class","node_error hidden").attr("xlink:href","icons/node-error.png").attr("x",0).attr("y",-6).attr("width",10).attr("height",9);
node.append("image").attr("class","node_changed hidden").attr("xlink:href","icons/node-changed.png").attr("x",12).attr("y",-6).attr("width",10).attr("height",10);
@@ -1157,6 +1353,7 @@ RED.view = (function() {
l = (typeof l === "function" ? l.call(d) : l)||"";
d.w = Math.max(node_width,calculateTextWidth(l, "node_label", 50)+(d._def.inputs>0?7:0) );
d.h = Math.max(node_height,(d.outputs||0) * 15);
+ d.resize = false;
}
var thisNode = d3.select(this);
//thisNode.selectAll(".centerDot").attr({"cx":function(d) { return d.w/2;},"cy":function(d){return d.h/2}});
@@ -1176,18 +1373,35 @@ RED.view = (function() {
//thisNode.selectAll(".node_icon_shade_right").attr("x",function(d){return d.w-30;});
//thisNode.selectAll(".node_icon_shade_border_right").attr("d",function(d){return "M "+(d.w-30)+" 1 l 0 "+(d.h-2)});
-
+ var inputPorts = thisNode.selectAll(".port_input");
+ if (d.inputs === 0 && !inputPorts.empty()) {
+ inputPorts.remove();
+ //nodeLabel.attr("x",30);
+ } else if (d.inputs === 1 && inputPorts.empty()) {
+ var inputGroup = thisNode.append("g").attr("class","port_input");
+ inputGroup.append("rect").attr("class","port").attr("rx",3).attr("ry",3).attr("width",10).attr("height",10)
+ .on("mousedown",function(d){portMouseDown(d,1,0);})
+ .on("touchstart",function(d){portMouseDown(d,1,0);})
+ .on("mouseup",function(d){portMouseUp(d,1,0);} )
+ .on("touchend",function(d){portMouseUp(d,1,0);} )
+ .on("mouseover",function(d) { var port = d3.select(this); port.classed("port_hovered",(mouse_mode!=RED.state.JOINING || mousedown_port_type != 1 ));})
+ .on("mouseout",function(d) { var port = d3.select(this); port.classed("port_hovered",false);})
+ }
+
var numOutputs = d.outputs;
var y = (d.h/2)-((numOutputs-1)/2)*13;
d.ports = d.ports || d3.range(numOutputs);
d._ports = thisNode.selectAll(".port_output").data(d.ports);
- d._ports.enter().append("rect").attr("class","port port_output").attr("rx",3).attr("ry",3).attr("width",10).attr("height",10)
+ var output_group = d._ports.enter().append("g").attr("class","port_output");
+
+ output_group.append("rect").attr("class","port").attr("rx",3).attr("ry",3).attr("width",10).attr("height",10)
.on("mousedown",(function(){var node = d; return function(d,i){portMouseDown(node,0,i);}})() )
.on("touchstart",(function(){var node = d; return function(d,i){portMouseDown(node,0,i);}})() )
.on("mouseup",(function(){var node = d; return function(d,i){portMouseUp(node,0,i);}})() )
.on("touchend",(function(){var node = d; return function(d,i){portMouseUp(node,0,i);}})() )
.on("mouseover",function(d,i) { var port = d3.select(this); port.classed("port_hovered",(mouse_mode!=RED.state.JOINING || mousedown_port_type !== 0 ));})
.on("mouseout",function(d,i) { var port = d3.select(this); port.classed("port_hovered",false);});
+
d._ports.exit().remove();
if (d._ports) {
numOutputs = d.outputs || 1;
@@ -1195,7 +1409,8 @@ RED.view = (function() {
var x = d.w - 5;
d._ports.each(function(d,i) {
var port = d3.select(this);
- port.attr("y",(y+13*i)-5).attr("x",x);
+ //port.attr("y",(y+13*i)-5).attr("x",x);
+ port.attr("transform", function(d) { return "translate("+x+","+((y+13*i)-5)+")";});
});
}
thisNode.selectAll('text.node_label').text(function(d,i){
@@ -1207,7 +1422,7 @@ RED.view = (function() {
}
}
return "";
- })
+ })
.attr('y', function(d){return (d.h/2)-1;})
.attr('class',function(d){
return 'node_label'+
@@ -1226,7 +1441,7 @@ RED.view = (function() {
thisNode.selectAll(".port_input").each(function(d,i) {
var port = d3.select(this);
- port.attr("y",function(d){return (d.h/2)-5;})
+ port.attr("transform",function(d){return "translate(-5,"+((d.h/2)-5)+")";})
});
thisNode.selectAll(".node_icon").attr("y",function(d){return (d.h-d3.select(this).attr("height"))/2;});
@@ -1299,7 +1514,14 @@ RED.view = (function() {
});
}
- var link = vis.selectAll(".link").data(RED.nodes.links.filter(function(d) { return d.source.z == activeWorkspace && d.target.z == activeWorkspace }),function(d) { return d.source.id+":"+d.sourcePort+":"+d.target.id;});
+ var link = vis.selectAll(".link").data(
+ RED.nodes.links.filter(function(d) {
+ return d.source.z == activeWorkspace && d.target.z == activeWorkspace;
+ }),
+ function(d) {
+ return d.source.id+":"+d.sourcePort+":"+d.target.id+":"+d.target.i;
+ }
+ );
var linkEnter = link.enter().insert("g",".node").attr("class","link");
@@ -1323,7 +1545,8 @@ RED.view = (function() {
d3.event.stopPropagation();
});
l.append("svg:path").attr("class","link_outline link_path");
- l.append("svg:path").attr("class","link_line link_path");
+ l.append("svg:path").attr("class","link_line link_path")
+ .classed("link_subflow", function(d) { return activeSubflow && (d.source.type === "subflow" || d.target.type === "subflow") });
});
link.exit().remove();
@@ -1367,6 +1590,7 @@ RED.view = (function() {
if (d3.event) {
d3.event.preventDefault();
}
+
}
RED.keyboard.add(/* z */ 90,{ctrl:true},function(){RED.history.pop();});
@@ -1401,7 +1625,8 @@ RED.view = (function() {
var new_nodes = result[0];
var new_links = result[1];
var new_workspaces = result[2];
-
+ var new_subflows = result[3];
+
var new_ms = new_nodes.filter(function(n) { return n.z == activeWorkspace }).map(function(n) { return {n:n};});
var new_node_ids = new_nodes.map(function(n){ return n.id; });
@@ -1452,14 +1677,25 @@ RED.view = (function() {
moving_set = new_ms;
}
- RED.history.push({t:'add',nodes:new_node_ids,links:new_links,workspaces:new_workspaces,dirty:RED.view.dirty()});
+ RED.history.push({
+ t:'add',
+ nodes:new_node_ids,
+ links:new_links,
+ workspaces:new_workspaces,
+ subflows:new_subflows,
+ dirty:RED.view.dirty()
+ });
redraw();
}
} catch(error) {
- console.log(error.stack);
- RED.notify("Error: "+error,"error");
+ if (error.code != "NODE_RED") {
+ console.log(error.stack);
+ RED.notify("Error: "+error,"error");
+ } else {
+ RED.notify("Error: "+error.message,"error");
+ }
}
}
@@ -1512,6 +1748,11 @@ RED.view = (function() {
$( "#node-input-workspace-name" ).val(ws.label);
$( "#node-dialog-rename-workspace" ).dialog("open");
}
+
+ function showSubflowDialog(id) {
+ RED.editor.editSubflow(RED.nodes.subflow(id));
+
+ }
$("#node-dialog-rename-workspace form" ).submit(function(e) { e.preventDefault();});
$( "#node-dialog-rename-workspace" ).dialog({
@@ -1535,11 +1776,10 @@ RED.view = (function() {
var workspace = $(this).dialog('option','workspace');
var label = $( "#node-input-workspace-name" ).val();
if (workspace.label != label) {
- workspace.label = label;
- var link = $("#workspace-tabs a[href='#"+workspace.id+"']");
- link.attr("title",label);
- link.text(label);
+ workspace_tabs.renameTab(workspace.id,label);
RED.view.dirty(true);
+ $("#btn-workspace-menu-"+workspace.id.replace(".","-")).text(label);
+ // TODO: update entry in menu
}
$( this ).dialog( "close" );
}
@@ -1607,7 +1847,9 @@ RED.view = (function() {
workspace_tabs.resize();
},
removeWorkspace: function(ws) {
- workspace_tabs.removeTab(ws.id);
+ if (workspace_tabs.contains(ws.id)) {
+ workspace_tabs.removeTab(ws.id);
+ }
},
getWorkspace: function() {
return activeWorkspace;
@@ -1615,7 +1857,14 @@ RED.view = (function() {
showWorkspace: function(id) {
workspace_tabs.activateTab(id);
},
- redraw:redraw,
+ redraw: function() {
+ RED.nodes.eachSubflow(function(sf) {
+ if (workspace_tabs.contains(sf.id)) {
+ workspace_tabs.renameTab(sf.id,"Subflow: "+sf.name);
+ }
+ });
+ redraw();
+ },
dirty: function(d) {
if (d == null) {
return dirty;
@@ -1638,6 +1887,220 @@ RED.view = (function() {
//TODO: should these move to an import/export module?
showImportNodesDialog: showImportNodesDialog,
showExportNodesDialog: showExportNodesDialog,
- showExportNodesLibraryDialog: showExportNodesLibraryDialog
+ showExportNodesLibraryDialog: showExportNodesLibraryDialog,
+ addFlow: function() {
+ var ws = {type:"subflow",id:RED.nodes.id(),label:"Flow 1", closeable: true};
+ RED.nodes.addWorkspace(ws);
+ workspace_tabs.addTab(ws);
+ workspace_tabs.activateTab(ws.id);
+ return ws;
+ },
+
+ showSubflow: function(id) {
+ if (!workspace_tabs.contains(id)) {
+ var sf = RED.nodes.subflow(id);
+ workspace_tabs.addTab({type:"subflow",id:id,label:"Subflow: "+sf.name, closeable: true});
+ workspace_tabs.resize();
+ }
+ workspace_tabs.activateTab(id);
+ },
+
+ createSubflow: function() {
+ var lastIndex = 0;
+ RED.nodes.eachSubflow(function(sf) {
+ var m = (new RegExp("^Subflow (\\d+)$")).exec(sf.name);
+ if (m) {
+ lastIndex = Math.max(lastIndex,m[1]);
+ }
+ });
+
+ var name = "Subflow "+(lastIndex+1);
+
+ var subflowId = RED.nodes.id();
+ var subflow = {
+ type:"subflow",
+ id:subflowId,
+ name:name,
+ in: [],
+ out: []
+ };
+ RED.nodes.addSubflow(subflow);
+ RED.history.push({
+ t:'createSubflow',
+ subflow: subflow,
+ dirty:RED.view.dirty()
+ });
+ RED.view.showSubflow(subflowId);
+ },
+
+ convertToSubflow: function() {
+ if (moving_set.length === 0) {
+ RED.notify("Cannot create subflow: no nodes selected","error");
+ return;
+ }
+ var i;
+ var nodes = {};
+ var new_links = [];
+ var removedLinks = [];
+
+ var candidateInputs = [];
+ var candidateOutputs = [];
+
+ var boundingBox = [moving_set[0].n.x,moving_set[0].n.y,moving_set[0].n.x,moving_set[0].n.y];
+
+ for (i=0;i 1) {
+ RED.notify("Cannot create subflow: multiple inputs to selection","error");
+ return;
+ }
+ //if (candidateInputs.length == 0) {
+ // RED.notify("Cannot create subflow: no input to selection","error");
+ // return;
+ //}
+
+
+ var lastIndex = 0;
+ RED.nodes.eachSubflow(function(sf) {
+ var m = (new RegExp("^Subflow (\\d+)$")).exec(sf.name);
+ if (m) {
+ lastIndex = Math.max(lastIndex,m[1]);
+ }
+ });
+
+ var name = "Subflow "+(lastIndex+1);
+
+ var subflowId = RED.nodes.id();
+ var subflow = {
+ type:"subflow",
+ id:subflowId,
+ name:name,
+ in: candidateInputs.map(function(v,i) { return {
+ type:"subflow",
+ direction:"in",
+ x:v.target.x-(v.target.w/2)-80,
+ y:v.target.y,
+ z:subflowId,
+ wires:[{id:v.target.id}]
+ }}),
+ out: candidateOutputs.map(function(v) { return {
+ type:"subflow",
+ direction:"in",
+ x:v.source.x+(v.source.w/2)+80,
+ y:v.source.y,
+ z:subflowId,
+ wires:[{id:v.source.id,port:v.sourcePort}]
+ }})
+ };
+ RED.nodes.addSubflow(subflow);
+
+ var subflowInstance = {
+ id:RED.nodes.id(),
+ type:"subflow:"+subflow.id,
+ x: center[0],
+ y: center[1],
+ z: activeWorkspace,
+ inputs: subflow.in.length,
+ outputs: subflow.out.length,
+ h: Math.max(node_height,(subflow.out.length||0) * 15),
+ changed:true
+ }
+ subflowInstance._def = RED.nodes.getType(subflowInstance.type);
+ RED.editor.validateNode(subflowInstance);
+ RED.nodes.add(subflowInstance);
+
+ candidateInputs.forEach(function(l) {
+ var link = {source:l.source, sourcePort:l.sourcePort, target: subflowInstance};
+ new_links.push(link);
+ RED.nodes.addLink(link);
+ });
+
+ candidateOutputs.forEach(function(output,i) {
+ output.targets.forEach(function(target) {
+ var link = {source:subflowInstance, sourcePort:i, target: target};
+ new_links.push(link);
+ RED.nodes.addLink(link);
+ });
+ });
+
+ subflow.in.forEach(function(input) {
+ input.wires.forEach(function(wire) {
+ var link = {source: input, sourcePort: 0, target: RED.nodes.node(wire.id) }
+ new_links.push(link);
+ RED.nodes.addLink(link);
+ });
+ });
+ subflow.out.forEach(function(output,i) {
+ output.wires.forEach(function(wire) {
+ var link = {source: RED.nodes.node(wire.id), sourcePort: wire.port , target: output }
+ new_links.push(link);
+ RED.nodes.addLink(link);
+ });
+ });
+
+ for (i=0;i