1
0
mirror of https://github.com/node-red/node-red.git synced 2023-10-10 13:36:53 +02:00

Merge pull request #458 from knolleary/subflows

Add Subflows
This commit is contained in:
Nick O'Leary 2014-10-29 22:04:48 +00:00
commit aff8a7802a
14 changed files with 1601 additions and 252 deletions

BIN
public/icons/subflow.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 439 B

View File

@ -51,11 +51,7 @@
<div id="workspace-add-tab"><a id="btn-workspace-add-tab" href="#"><i class="fa fa-plus"></i></a></div> <div id="workspace-add-tab"><a id="btn-workspace-add-tab" href="#"><i class="fa fa-plus"></i></a></div>
<div id="chart"></div> <div id="chart"></div>
<div id="workspace-toolbar"> <div id="workspace-toolbar">
<div class="btn-group"> <a class="button" id="workspace-edit-subflow" href="#"><i class="fa fa-pencil"></i> edit subflow properties</a>
<a class="btn btn-small" href="#"><i class="fa fa-search-minus"></i></a>
<a class="btn btn-small" href="#"><i class="fa fa-dot-circle-o"></i></a>
<a class="btn btn-small" href="#"><i class="fa fa-search-plus"></i></a>
</div>
</div> </div>
</div> </div>
@ -81,6 +77,20 @@
<div id="dialog" class="hide"><form id="dialog-form" class="form-horizontal"></form></div> <div id="dialog" class="hide"><form id="dialog-form" class="form-horizontal"></form></div>
<div id="node-config-dialog" class="hide"><form id="dialog-config-form" class="form-horizontal"></form><div class="form-tips" id="node-config-dialog-user-count"></div></div> <div id="node-config-dialog" class="hide"><form id="dialog-config-form" class="form-horizontal"></form><div class="form-tips" id="node-config-dialog-user-count"></div></div>
<div id="subflow-dialog" class="hide">
<form class="form-horizontal">
<div class="form-row">
<label>Name</label><input type="text" id="subflow-input-name">
</div>
<div class="form-row">
<label>Inputs</label><input style="width: 60px; height: 1.7em;" id="subflow-input-inCount">
</div>
<div class="form-row">
<label>Outputs</label><input style="width: 60px; height: 1.7em;" id="subflow-input-outCount">
</div>
</form>
<div class="form-tips" id="subflow-dialog-user-count"></div>
</div>
<div id="node-dialog-confirm-deploy" class="hide"> <div id="node-dialog-confirm-deploy" class="hide">
<form class="form-horizontal"> <form class="form-horizontal">
@ -199,7 +209,6 @@
</div> </div>
</form> </form>
</div> </div>
<script type="text/x-red" data-template-name="export-clipboard-dialog"> <script type="text/x-red" data-template-name="export-clipboard-dialog">
<div class="form-row"> <div class="form-row">
<label for="node-input-export" style="display: block; width:100%;"><i class="fa fa-clipboard"></i> Nodes:</label> <label for="node-input-export" style="display: block; width:100%;"><i class="fa fa-clipboard"></i> Nodes:</label>
@ -222,6 +231,13 @@
</div> </div>
</script> </script>
<script type="text/x-red" data-template-name="subflow">
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
<input type="text" id="node-input-name" placeholder="name">
</div>
</script>
<script src="jquery/js/jquery-1.11.1.min.js"></script> <script src="jquery/js/jquery-1.11.1.min.js"></script>
<script src="bootstrap/js/bootstrap.min.js"></script> <script src="bootstrap/js/bootstrap.min.js"></script>
<script src="jquery/js/jquery-ui-1.10.3.custom.min.js"></script> <script src="jquery/js/jquery-ui-1.10.3.custom.min.js"></script>

View File

@ -50,6 +50,12 @@ RED.history = (function() {
RED.view.removeWorkspace(ev.workspaces[i]); RED.view.removeWorkspace(ev.workspaces[i]);
} }
} }
if (ev.subflows) {
for (i=0;i<ev.subflows.length;i++) {
RED.nodes.removeSubflow(ev.subflows[i]);
RED.view.removeWorkspace(ev.subflows[i]);
}
}
} else if (ev.t == "delete") { } else if (ev.t == "delete") {
if (ev.workspaces) { if (ev.workspaces) {
for (i=0;i<ev.workspaces.length;i++) { for (i=0;i<ev.workspaces.length;i++) {
@ -57,6 +63,9 @@ RED.history = (function() {
RED.view.addWorkspace(ev.workspaces[i]); RED.view.addWorkspace(ev.workspaces[i]);
} }
} }
if (ev.subflow) {
RED.nodes.addSubflow(ev.subflow);
}
if (ev.nodes) { if (ev.nodes) {
for (i=0;i<ev.nodes.length;i++) { for (i=0;i<ev.nodes.length;i++) {
RED.nodes.add(ev.nodes[i]); RED.nodes.add(ev.nodes[i]);
@ -80,15 +89,66 @@ RED.history = (function() {
ev.node[i] = ev.changes[i]; ev.node[i] = ev.changes[i];
} }
} }
if (ev.subflow) {
if (ev.node.in.length > 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.updateNodeProperties(ev.node);
RED.editor.validateNode(ev.node);
}
if (ev.links) { if (ev.links) {
for (i=0;i<ev.links.length;i++) { for (i=0;i<ev.links.length;i++) {
RED.nodes.addLink(ev.links[i]); RED.nodes.addLink(ev.links[i]);
} }
} }
RED.editor.validateNode(ev.node);
ev.node.dirty = true; ev.node.dirty = true;
ev.node.changed = ev.changed; ev.node.changed = ev.changed;
} else if (ev.t == "createSubflow") {
if (ev.nodes) {
RED.nodes.eachNode(function(n) {
if (n.z === ev.subflow.id) {
n.z = ev.activeWorkspace;
n.dirty = true;
}
});
for (i=0;i<ev.nodes.length;i++) {
RED.nodes.remove(ev.nodes[i]);
}
}
if (ev.links) {
for (i=0;i<ev.links.length;i++) {
RED.nodes.removeLink(ev.links[i]);
}
}
RED.nodes.removeSubflow(ev.subflow);
RED.view.removeWorkspace(ev.subflow);
if (ev.removedLinks) {
for (i=0;i<ev.removedLinks.length;i++) {
RED.nodes.addLink(ev.removedLinks[i]);
}
}
} }
RED.view.dirty(ev.dirty); RED.view.dirty(ev.dirty);
RED.view.redraw(); RED.view.redraw();

View File

@ -301,6 +301,9 @@ var RED = (function() {
null, null,
{id:"btn-config-nodes",icon:"fa fa-th-list",label:"Configuration nodes...",onselect:RED.sidebar.config.show}, {id:"btn-config-nodes",icon:"fa fa-th-list",label:"Configuration nodes...",onselect:RED.sidebar.config.show},
null, null,
{id:"btn-create-subflow",icon:"fa fa-share-alt",label:"Create subflow",onselect:RED.view.createSubflow},
{id:"btn-convert-subflow",icon:"fa fa-share-alt",label:"Convert to subflow",disabled:true,onselect:RED.view.convertToSubflow},
null,
{id:"btn-workspace-menu",icon:"fa fa-th-large",label:"Workspaces",options:[ {id:"btn-workspace-menu",icon:"fa fa-th-large",label:"Workspaces",options:[
{id:"btn-workspace-add",icon:"fa fa-plus",label:"Add"}, {id:"btn-workspace-add",icon:"fa fa-plus",label:"Add"},
{id:"btn-workspace-edit",icon:"fa fa-pencil",label:"Rename"}, {id:"btn-workspace-edit",icon:"fa fa-pencil",label:"Rename"},

View File

@ -21,6 +21,7 @@ RED.nodes = (function() {
var links = []; var links = [];
var defaultWorkspace; var defaultWorkspace;
var workspaces = {}; var workspaces = {};
var subflows = {};
var registry = (function() { var registry = (function() {
var nodeList = []; var nodeList = [];
@ -98,13 +99,22 @@ RED.nodes = (function() {
}, },
registerNodeType: function(nt,def) { registerNodeType: function(nt,def) {
nodeDefinitions[nt] = def; nodeDefinitions[nt] = def;
if (def.category != "subflows") {
nodeSets[typeToId[nt]].added = true; nodeSets[typeToId[nt]].added = true;
// TODO: too tightly coupled into palette UI // TODO: too tightly coupled into palette UI
}
RED.palette.add(nt,def); RED.palette.add(nt,def);
if (def.onpaletteadd && typeof def.onpaletteadd === "function") { if (def.onpaletteadd && typeof def.onpaletteadd === "function") {
def.onpaletteadd.call(def); def.onpaletteadd.call(def);
} }
}, },
removeNodeType: function(nt) {
if (nt.substring(0,8) != "subflow:") {
throw new Error("this api is subflow only. called with:",nt);
}
delete nodeDefinitions[nt];
RED.palette.remove(nt);
},
getNodeType: function(nt) { getNodeType: function(nt) {
return nodeDefinitions[nt]; return nodeDefinitions[nt];
} }
@ -122,7 +132,6 @@ RED.nodes = (function() {
RED.sidebar.config.refresh(); RED.sidebar.config.refresh();
} else { } else {
n.dirty = true; n.dirty = true;
nodes.push(n);
var updatedConfigNode = false; var updatedConfigNode = false;
for (var d in n._def.defaults) { for (var d in n._def.defaults) {
if (n._def.defaults.hasOwnProperty(d)) { if (n._def.defaults.hasOwnProperty(d)) {
@ -142,6 +151,14 @@ RED.nodes = (function() {
if (updatedConfigNode) { if (updatedConfigNode) {
RED.sidebar.config.refresh(); RED.sidebar.config.refresh();
} }
if (n._def.category == "subflows" && typeof n.i === "undefined") {
var nextId = 0;
RED.nodes.eachNode(function(node) {
nextId = Math.max(nextId,node.i||0);
});
n.i = nextId+1;
}
nodes.push(n);
} }
} }
function addLink(l) { function addLink(l) {
@ -174,8 +191,7 @@ RED.nodes = (function() {
if (node) { if (node) {
nodes.splice(nodes.indexOf(node),1); nodes.splice(nodes.indexOf(node),1);
removedLinks = links.filter(function(l) { return (l.source === node) || (l.target === node); }); removedLinks = links.filter(function(l) { return (l.source === node) || (l.target === node); });
removedLinks.map(function(l) {links.splice(links.indexOf(l), 1); }); removedLinks.forEach(function(l) {links.splice(links.indexOf(l), 1); });
}
var updatedConfigNode = false; var updatedConfigNode = false;
for (var d in node._def.defaults) { for (var d in node._def.defaults) {
if (node._def.defaults.hasOwnProperty(d)) { if (node._def.defaults.hasOwnProperty(d)) {
@ -197,6 +213,7 @@ RED.nodes = (function() {
RED.sidebar.config.refresh(); RED.sidebar.config.refresh();
} }
} }
}
return removedLinks; return removedLinks;
} }
@ -237,6 +254,50 @@ RED.nodes = (function() {
return {nodes:removedNodes,links:removedLinks}; return {nodes:removedNodes,links:removedLinks};
} }
function addSubflow(sf) {
subflows[sf.id] = sf;
RED.nodes.registerType("subflow:"+sf.id, {
defaults:{name:{value:""}},
icon:"subflow.png",
category: "subflows",
inputs: sf.in.length,
outputs: sf.out.length,
color: "#da9",
label: function() { return this.name||RED.nodes.subflow(sf.id).name },
labelStyle: function() { return this.name?"node_label_italic":""; },
paletteLabel: function() { return RED.nodes.subflow(sf.id).name }
});
}
function getSubflow(id) {
return subflows[id];
}
function removeSubflow(sf) {
delete subflows[sf.id];
registry.removeNodeType("subflow:"+sf.id);
}
function subflowContains(sfid,nodeid) {
for (var i=0;i<nodes.length;i++) {
var node = nodes[i];
if (node.z === sfid) {
var m = /^subflow:(.+)$/.exec(node.type);
if (m) {
if (m[1] === nodeid) {
return true;
} else {
var result = subflowContains(m[1],nodeid);
if (result) {
return true;
}
}
}
}
}
return false;
}
function getAllFlowNodes(node) { function getAllFlowNodes(node) {
var visited = {}; var visited = {};
visited[node.id] = true; visited[node.id] = true;
@ -247,8 +308,12 @@ RED.nodes = (function() {
var childLinks = links.filter(function(d) { return (d.source === n) || (d.target === n);}); var childLinks = links.filter(function(d) { return (d.source === n) || (d.target === n);});
for (var i=0;i<childLinks.length;i++) { for (var i=0;i<childLinks.length;i++) {
var child = (childLinks[i].source === n)?childLinks[i].target:childLinks[i].source; var child = (childLinks[i].source === n)?childLinks[i].target:childLinks[i].source;
if (!visited[child.id]) { var id = child.id;
visited[child.id] = true; if (!id) {
id = child.direction+":"+child.i;
}
if (!visited[id]) {
visited[id] = true;
nns.push(child); nns.push(child);
stack.push(child); stack.push(child);
} }
@ -291,20 +356,74 @@ RED.nodes = (function() {
var wires = links.filter(function(d){return d.source === n;}); var wires = links.filter(function(d){return d.source === n;});
for (var j=0;j<wires.length;j++) { for (var j=0;j<wires.length;j++) {
var w = wires[j]; var w = wires[j];
if (w.target.type != "subflow") {
node.wires[w.sourcePort].push(w.target.id); node.wires[w.sourcePort].push(w.target.id);
} }
} }
}
return node; return node;
} }
function convertSubflow(n) {
var node = {};
node.id = n.id;
node.type = n.type;
node.name = n.name;
node.in = [];
node.out = [];
n.in.forEach(function(p) {
var nIn = {x:p.x,y:p.y,wires:[]};
var wires = links.filter(function(d) { return d.source === p });
for (var i=0;i<wires.length;i++) {
var w = wires[i];
if (w.target.id != p.id) {
nIn.wires.push({id:w.target.id})
}
}
node.in.push(nIn);
});
n.out.forEach(function(p,c) {
var nOut = {x:p.x,y:p.y,wires:[]};
var wires = links.filter(function(d) { return d.target === p });
for (i=0;i<wires.length;i++) {
if (wires[i].source.type != "subflow") {
nOut.wires.push({id:wires[i].source.id,port:wires[i].sourcePort})
} else {
nOut.wires.push({id:n.id,port:0})
}
}
node.out.push(nOut);
});
return node;
}
/** /**
* Converts the current node selection to an exportable JSON Object * Converts the current node selection to an exportable JSON Object
**/ **/
function createExportableNodeSet(set) { function createExportableNodeSet(set) {
var nns = []; var nns = [];
var exportedConfigNodes = {}; var exportedConfigNodes = {};
var exportedSubflows = {};
for (var n=0;n<set.length;n++) { for (var n=0;n<set.length;n++) {
var node = set[n].n; var node = set[n].n;
if (node.type.substring(0,8) == "subflow:") {
var subflowId = node.type.substring(8);
if (!exportedSubflows[subflowId]) {
exportedSubflows[subflowId] = true;
var subflow = getSubflow(subflowId);
var subflowSet = [{n:subflow}];
RED.nodes.eachNode(function(n) {
if (n.z == subflowId) {
subflowSet.push({n:n});
}
});
var exportableSubflow = createExportableNodeSet(subflowSet);
nns = exportableSubflow.concat(nns);
}
}
if (node.type != "subflow") {
var convertedNode = RED.nodes.convertNode(node); var convertedNode = RED.nodes.convertNode(node);
for (var d in node._def.defaults) { for (var d in node._def.defaults) {
if (node._def.defaults[d].type && node[d] in configNodes) { if (node._def.defaults[d].type && node[d] in configNodes) {
@ -320,8 +439,11 @@ RED.nodes = (function() {
} }
} }
} }
nns.push(convertedNode); nns.push(convertedNode);
} else {
var convertedSubflow = convertSubflow(node);
nns.push(convertedSubflow);
}
} }
return nns; return nns;
} }
@ -332,9 +454,16 @@ RED.nodes = (function() {
var i; var i;
for (i in workspaces) { for (i in workspaces) {
if (workspaces.hasOwnProperty(i)) { if (workspaces.hasOwnProperty(i)) {
if (workspaces[i].type == "tab") {
nns.push(workspaces[i]); nns.push(workspaces[i]);
} }
} }
}
for (i in subflows) {
if (subflows.hasOwnProperty(i)) {
nns.push(convertSubflow(subflows[i]));
}
}
for (i in configNodes) { for (i in configNodes) {
if (configNodes.hasOwnProperty(i)) { if (configNodes.hasOwnProperty(i)) {
nns.push(convertNode(configNodes[i], true)); nns.push(convertNode(configNodes[i], true));
@ -368,7 +497,11 @@ RED.nodes = (function() {
for (i=0;i<newNodes.length;i++) { for (i=0;i<newNodes.length;i++) {
n = newNodes[i]; n = newNodes[i];
// TODO: remove workspace in next release+1 // TODO: remove workspace in next release+1
if (n.type != "workspace" && n.type != "tab" && !registry.getNodeType(n.type)) { if (n.type != "workspace" &&
n.type != "tab" &&
n.type != "subflow" &&
!registry.getNodeType(n.type) &&
n.type.substring(0,8) != "subflow:") {
// TODO: get this UI thing out of here! (see below as well) // TODO: get this UI thing out of here! (see below as well)
n.name = n.type; n.name = n.type;
n.type = "unknown"; n.type = "unknown";
@ -389,9 +522,35 @@ RED.nodes = (function() {
//"DO NOT DEPLOY while in this state.<br/>Either, add missing types to Node-RED, restart and then reload page,<br/>or delete unknown "+n.name+", rewire as required, and then deploy.","error"); //"DO NOT DEPLOY while in this state.<br/>Either, add missing types to Node-RED, restart and then reload page,<br/>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;i<newNodes.length;i++) {
var m = /^subflow:(.+)$/.exec(newNodes[i].type);
if (m) {
var subflowId = m[1];
var err;
if (subflowId === activeSubflow.id) {
err = new Error("Cannot add subflow to itself");
}
if (subflowContains(m[1],activeSubflow.id)) {
err = new Error("Cannot add subflow - circular reference detected");
}
if (err) {
// TODO: standardise error codes
err.code = "NODE_RED";
throw err;
}
}
}
}
var new_workspaces = []; var new_workspaces = [];
var workspace_map = {}; var workspace_map = {};
var new_subflows = [];
var subflow_map = {};
var nid;
for (i=0;i<newNodes.length;i++) { for (i=0;i<newNodes.length;i++) {
n = newNodes[i]; n = newNodes[i];
// TODO: remove workspace in next release+1 // TODO: remove workspace in next release+1
@ -403,13 +562,34 @@ RED.nodes = (function() {
defaultWorkspace = n; defaultWorkspace = n;
} }
if (createNewIds) { if (createNewIds) {
var nid = getID(); nid = getID();
workspace_map[n.id] = nid; workspace_map[n.id] = nid;
n.id = nid; n.id = nid;
} }
addWorkspace(n); addWorkspace(n);
RED.view.addWorkspace(n); RED.view.addWorkspace(n);
new_workspaces.push(n); new_workspaces.push(n);
} else if (n.type === "subflow") {
subflow_map[n.id] = n;
if (createNewIds) {
nid = getID();
n.id = nid;
}
// TODO: handle createNewIds - map old to new subflow ids
n.in.forEach(function(input,i) {
input.type = "subflow";
input.direction = "in";
input.z = n.id;
input.i = i;
});
n.out.forEach(function(output,i) {
output.type = "subflow";
output.direction = "out";
output.z = n.id;
output.i = i;
});
new_subflows.push(n);
addSubflow(n);
} }
} }
if (defaultWorkspace == null) { if (defaultWorkspace == null) {
@ -426,7 +606,7 @@ RED.nodes = (function() {
for (i=0;i<newNodes.length;i++) { for (i=0;i<newNodes.length;i++) {
n = newNodes[i]; n = newNodes[i];
// TODO: remove workspace in next release+1 // TODO: remove workspace in next release+1
if (n.type !== "workspace" && n.type !== "tab") { if (n.type !== "workspace" && n.type !== "tab" && n.type !== "subflow") {
var def = registry.getNodeType(n.type); var def = registry.getNodeType(n.type);
if (def && def.category == "config") { if (def && def.category == "config") {
if (!RED.nodes.node(n.id)) { if (!RED.nodes.node(n.id)) {
@ -443,19 +623,35 @@ RED.nodes = (function() {
} else { } else {
var node = {x:n.x,y:n.y,z:n.z,type:0,wires:n.wires,changed:false}; var node = {x:n.x,y:n.y,z:n.z,type:0,wires:n.wires,changed:false};
if (createNewIds) { if (createNewIds) {
if (subflow_map[node.z]) {
node.z = subflow_map[node.z].id;
} else {
node.z = workspace_map[node.z]; node.z = workspace_map[node.z];
if (!workspaces[node.z]) { if (!workspaces[node.z]) {
node.z = RED.view.getWorkspace(); node.z = activeWorkspace;
}
} }
node.id = getID(); node.id = getID();
} else { } else {
node.id = n.id; node.id = n.id;
if (node.z == null || !workspaces[node.z]) { if (node.z == null || (!workspaces[node.z] && !subflow_map[node.z])) {
node.z = RED.view.getWorkspace(); node.z = activeWorkspace;
} }
} }
node.type = n.type; node.type = n.type;
node._def = def; node._def = def;
if (n.type.substring(0,7) === "subflow") {
var parentId = n.type.split(":")[1];
var subflow = subflow_map[parentId]||getSubflow(parentId);
if (createNewIds) {
parentId = subflow.id;
node.type = "subflow:"+parentId;
node._def = registry.getNodeType(node.type);
delete node.i;
}
node.outputs = subflow.out.length;
node.inputs = subflow.in.length;
} else {
if (!node._def) { if (!node._def) {
node._def = { node._def = {
color:"#fee", color:"#fee",
@ -465,6 +661,7 @@ RED.nodes = (function() {
outputs: n.outputs||n.wires.length outputs: n.outputs||n.wires.length
} }
} }
node.inputs = n.inputs||node._def.inputs;
node.outputs = n.outputs||node._def.outputs; node.outputs = n.outputs||node._def.outputs;
for (var d2 in node._def.defaults) { for (var d2 in node._def.defaults) {
@ -472,7 +669,7 @@ RED.nodes = (function() {
node[d2] = n[d2]; node[d2] = n[d2];
} }
} }
}
addNode(node); addNode(node);
RED.editor.validateNode(node); RED.editor.validateNode(node);
node_map[n.id] = node; node_map[n.id] = node;
@ -494,10 +691,38 @@ RED.nodes = (function() {
} }
delete n.wires; delete n.wires;
} }
return [new_nodes,new_links,new_workspaces]; for (i=0;i<new_subflows.length;i++) {
n = new_subflows[i];
n.in.forEach(function(input) {
input.wires.forEach(function(wire) {
var link = {source:input, sourcePort:0, target:node_map[wire.id]};
addLink(link);
new_links.push(link);
});
delete input.wires;
});
n.out.forEach(function(output) {
output.wires.forEach(function(wire) {
var link;
if (wire.id == n.id) {
link = {source:n.in[wire.port], sourcePort:wire.port,target:output};
} else {
link = {source:node_map[wire.id], sourcePort:wire.port,target:output};
}
addLink(link);
new_links.push(link);
});
delete output.wires;
});
}
return [new_nodes,new_links,new_workspaces,new_subflows];
} catch(error) { } catch(error) {
//TODO: get this UI thing out of here! (see above as well) if (error.code != "NODE_RED") {
console.log(error.stack);
RED.notify("<strong>Error</strong>: "+error,"error"); RED.notify("<strong>Error</strong>: "+error,"error");
} else {
RED.notify("<strong>Error</strong>: "+error.message,"error");
}
return null; return null;
} }
@ -516,13 +741,22 @@ RED.nodes = (function() {
registerType: registry.registerNodeType, registerType: registry.registerNodeType,
getType: registry.getNodeType, getType: registry.getNodeType,
convertNode: convertNode, convertNode: convertNode,
add: addNode, add: addNode,
addLink: addLink,
remove: removeNode, remove: removeNode,
addLink: addLink,
removeLink: removeLink, removeLink: removeLink,
addWorkspace: addWorkspace, addWorkspace: addWorkspace,
removeWorkspace: removeWorkspace, removeWorkspace: removeWorkspace,
workspace: getWorkspace, workspace: getWorkspace,
addSubflow: addSubflow,
removeSubflow: removeSubflow,
subflow: getSubflow,
subflowContains: subflowContains,
eachNode: function(cb) { eachNode: function(cb) {
for (var n=0;n<nodes.length;n++) { for (var n=0;n<nodes.length;n++) {
cb(nodes[n]); cb(nodes[n]);
@ -540,6 +774,13 @@ RED.nodes = (function() {
} }
} }
}, },
eachSubflow: function(cb) {
for (var id in subflows) {
if (subflows.hasOwnProperty(id)) {
cb(subflows[id]);
}
}
},
node: getNode, node: getNode,
import: importNodes, import: importNodes,
refreshValidation: refreshValidation, refreshValidation: refreshValidation,

View File

@ -95,6 +95,7 @@ RED.editor = (function() {
node.resize = true; node.resize = true;
node.dirty = true; node.dirty = true;
var removedLinks = []; var removedLinks = [];
if (node.ports) {
if (node.outputs < node.ports.length) { if (node.outputs < node.ports.length) {
while (node.outputs < node.ports.length) { while (node.outputs < node.ports.length) {
node.ports.pop(); node.ports.pop();
@ -104,14 +105,22 @@ RED.editor = (function() {
removedLinks.push(l); removedLinks.push(l);
} }
}); });
for (var l=0;l<removedLinks.length;l++) {
RED.nodes.removeLink(removedLinks[l]);
}
} else if (node.outputs > node.ports.length) { } else if (node.outputs > node.ports.length) {
while (node.outputs > node.ports.length) { while (node.outputs > node.ports.length) {
node.ports.push(node.ports.length); node.ports.push(node.ports.length);
} }
} }
}
if (node.inputs === 0) {
RED.nodes.eachLink(function(l) {
if (l.target === node) {
removedLinks.push(l);
}
});
}
for (var l=0;l<removedLinks.length;l++) {
RED.nodes.removeLink(removedLinks[l]);
}
return removedLinks; return removedLinks;
} }
@ -274,6 +283,11 @@ RED.editor = (function() {
RED.sidebar.info.refresh(editing_node); RED.sidebar.info.refresh(editing_node);
} }
RED.sidebar.config.refresh(); RED.sidebar.config.refresh();
var buttons = $( this ).dialog("option","buttons");
if (buttons.length == 3) {
$( this ).dialog("option","buttons",buttons.splice(1));
}
editing_node = null; editing_node = null;
} }
}); });
@ -459,10 +473,30 @@ RED.editor = (function() {
function showEditDialog(node) { function showEditDialog(node) {
editing_node = node; editing_node = node;
RED.view.state(RED.state.EDITING); RED.view.state(RED.state.EDITING);
$("#dialog-form").html($("script[data-template-name='"+node.type+"']").html()); var type = node.type;
if (node.type.substring(0,8) == "subflow:") {
type = "subflow";
var id = editing_node.type.substring(8);
var buttons = $( "#dialog" ).dialog("option","buttons");
buttons.unshift({
class: 'leftButton',
text: "Edit flow",
click: function() {
RED.view.showSubflow(id);
$("#node-dialog-ok").click();
}
});
$( "#dialog" ).dialog("option","buttons",buttons);
}
$("#dialog-form").html($("script[data-template-name='"+type+"']").html());
$('<input type="text" style="display: none;" />').appendTo("#dialog-form"); $('<input type="text" style="display: none;" />').appendTo("#dialog-form");
prepareEditDialog(node,node._def,"node-input"); 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) { 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<removedNodes.length;i++) {
var rmlinks = RED.nodes.remove(removedNodes[i].id);
removedLinks = removedLinks.concat(rmlinks);
}
RED.nodes.removeSubflow(editing_node);
RED.view.removeWorkspace(editing_node);
RED.history.push({
t:'delete',
nodes:removedNodes,
links:removedLinks,
subflow: editing_node,
dirty:startDirty
});
RED.view.dirty(true);
RED.view.redraw();
$( this ).dialog( "close" );
}
},
{
id: "subflow-dialog-ok",
text: "Ok",
click: function() {
if (editing_node) {
var i;
var changes = {};
var changed = false;
var wasDirty = RED.view.dirty();
var newName = $("#subflow-input-name").val();
var newInCount = $("#subflow-input-inCount").val();
var newOutCount = $("#subflow-input-outCount").val();
var oldInCount = editing_node.in.length;
var oldOutCount = editing_node.out.length;
if (newName != editing_node.name) {
changes['name'] = editing_node.name;
editing_node.name = newName;
changed = true;
$("#btn-workspace-menu-"+editing_node.id.replace(".","-")).text("Subflow: "+newName);
}
var xpos = 40;
var addedOutputs = [];
var removedOutputs = [];
var addedInputs = [];
var removedInputs = [];
var removedLinks = [];
if (editing_node.in.length < newInCount) {
var l = editing_node.in.length;
for (i=l;i<newInCount;i++) {
var newInput = {type:"subflow",direction:"in",z:editing_node.id,i:i,x:xpos,y:70};
addedInputs.push(newInput);
editing_node.in.push(newInput);
xpos += 55;
}
changed = true;
} else if (editing_node.in.length > newInCount) {
removedInputs = editing_node.in.splice(newInCount);
changed = true;
}
if (editing_node.out.length < newOutCount) {
for (i=editing_node.out.length;i<newOutCount;i++) {
var newOutput = {type:"subflow",direction:"out",z:editing_node.id,i:i,x:xpos,y:70};
addedOutputs.push(newOutput);
editing_node.out.push(newOutput);
xpos += 55;
}
changed = true;
} else if (editing_node.out.length > 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 { return {
edit: showEditDialog, edit: showEditDialog,
editConfig: showEditConfigNodeDialog, editConfig: showEditConfigNodeDialog,
editSubflow: showEditSubflowDialog,
validateNode: validateNode, validateNode: validateNode,
updateNodeProperties: updateNodeProperties // TODO: only exposed for edit-undo updateNodeProperties: updateNodeProperties // TODO: only exposed for edit-undo
} }

View File

@ -110,13 +110,28 @@ RED.menu = (function() {
$("#"+id).parent().remove(); $("#"+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 { return {
init: createMenu, init: createMenu,
setSelected: setSelected, setSelected: setSelected,
isSelected: isSelected, isSelected: isSelected,
setDisabled: setDisabled, setDisabled: setDisabled,
addItem: addItem, addItem: addItem,
removeItem: removeItem removeItem: removeItem,
setAction: setAction
//TODO: add an api for replacing a submenu - see library.js:loadFlowLibrary //TODO: add an api for replacing a submenu - see library.js:loadFlowLibrary
} }
})(); })();

View File

@ -17,7 +17,7 @@
RED.palette = (function() { RED.palette = (function() {
var exclusion = ['config','unknown','deprecated']; 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){ function createCategoryContainer(category){
var escapedCategory = category.replace(" ","_"); var escapedCategory = category.replace(" ","_");
@ -38,10 +38,66 @@ RED.palette = (function() {
core.forEach(createCategoryContainer); 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<words.length;i++) {
var newWidth = RED.view.calculateTextWidth(currentLine+" "+words[i], "palette_label", 0);
if (newWidth < nodeWidth) {
currentLine += " "+words[i];
currentLineWidth = newWidth;
} else {
displayLines.push(currentLine);
currentLine = words[i];
currentLineWidth = RED.view.calculateTextWidth(currentLine, "palette_label", 0);
}
}
displayLines.push(currentLine);
var lines = displayLines.join("<br/>");
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 = "<p><b>"+label+"</b></p>";
if (label != type) {
l = "<p><b>"+label+"</b><br/><i>"+type+"</i></p>";
}
popOverContent = $(l+($("script[data-help-name|='"+type+"']").html()||"<p>no information available</p>").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 = "<p><b>"+label+"</b></p><p>no information available</p>";
}
el.data('popover').options.content = popOverContent;
}
function escapeNodeType(nt) {
return nt.replace(" ","_").replace(".","_").replace(":","_");
}
function addNodeType(nt,def) { function addNodeType(nt,def) {
var nodeTypeId = nt.replace(" ","_"); var nodeTypeId = escapeNodeType(nt);
if ($("#palette_node_"+nodeTypeId).length) { if ($("#palette_node_"+nodeTypeId).length) {
return; return;
} }
@ -63,27 +119,12 @@ RED.palette = (function() {
label = (typeof def.paletteLabel === "function" ? def.paletteLabel.call(def) : def.paletteLabel)||""; label = (typeof def.paletteLabel === "function" ? def.paletteLabel.call(def) : def.paletteLabel)||"";
} }
var pixels = RED.view.calculateTextWidth(label, "palette_label", 0); d.innerHTML = '<div class="palette_label"></div>';
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 = '<div class="palette_label"'+
(multiLine ? 'style="line-height: '+
lineHeight + 'px; margin-top: 5px"' : '')+
'>'+label+"</div>";
d.className="palette_node"; d.className="palette_node";
if (def.icon) { if (def.icon) {
d.style.backgroundImage = "url(icons/"+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") { if (def.align == "right") {
d.style.backgroundPosition = "95% 50%"; d.style.backgroundPosition = "95% 50%";
} else if (def.inputs > 0) { } else if (def.inputs > 0) {
@ -92,23 +133,16 @@ RED.palette = (function() {
} }
d.style.backgroundColor = def.color; d.style.backgroundColor = def.color;
d.style.height = multiLineNodeHeight + "px";
if (def.outputs > 0) { if (def.outputs > 0) {
var portOut = document.createElement("div"); var portOut = document.createElement("div");
portOut.className = "palette_port palette_port_output"; portOut.className = "palette_port palette_port_output";
if (multiLine) {
portOut.style.top = ((multiLineNodeHeight - portHeight) / 2) + "px";
}
d.appendChild(portOut); d.appendChild(portOut);
} }
if (def.inputs > 0) { if (def.inputs > 0) {
var portIn = document.createElement("div"); var portIn = document.createElement("div");
portIn.className = "palette_port"; portIn.className = "palette_port palette_port_input";
if (multiLine) {
portIn.style.top = ((multiLineNodeHeight - portHeight) / 2) + "px";
}
d.appendChild(portIn); d.appendChild(portIn);
} }
@ -123,23 +157,13 @@ RED.palette = (function() {
$("#palette-"+category).append(d); $("#palette-"+category).append(d);
d.onmousedown = function(e) { e.preventDefault(); }; d.onmousedown = function(e) { e.preventDefault(); };
var popOverContent;
try {
popOverContent = $("<p><b>"+label+"</b></p>"+($("script[data-help-name|='"+nt+"']").html().trim()||"<p>no information available</p>")).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 = "<p><b>"+label+"</b></p><p>no information available</p>";
}
$(d).popover({ $(d).popover({
title:d.type, title:d.type,
placement:"right", placement:"right",
trigger: "hover", trigger: "hover",
delay: { show: 750, hide: 50 }, delay: { show: 750, hide: 50 },
html: true, html: true,
container:'body', container:'body'
content: popOverContent
}); });
$(d).click(function() { $(d).click(function() {
var help = '<div class="node-help">'+($("script[data-help-name|='"+d.type+"']").html()||"")+"</div>"; var help = '<div class="node-help">'+($("script[data-help-name|='"+d.type+"']").html()||"")+"</div>";
@ -151,23 +175,50 @@ RED.palette = (function() {
revert: true, revert: true,
revertDuration: 50 revertDuration: 50
}); });
setLabel(nt,$(d),label);
} }
} }
function removeNodeType(nt) { function removeNodeType(nt) {
var nodeTypeId = nt.replace(" ","_"); var nodeTypeId = escapeNodeType(nt);
$("#palette_node_"+nodeTypeId).remove(); $("#palette_node_"+nodeTypeId).remove();
} }
function hideNodeType(nt) { function hideNodeType(nt) {
var nodeTypeId = nt.replace(" ","_"); var nodeTypeId = escapeNodeType(nt);
$("#palette_node_"+nodeTypeId).hide(); $("#palette_node_"+nodeTypeId).hide();
} }
function showNodeType(nt) { function showNodeType(nt) {
var nodeTypeId = nt.replace(" ","_"); var nodeTypeId = escapeNodeType(nt);
$("#palette_node_"+nodeTypeId).show(); $("#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() { function filterChange() {
var val = $("#palette-search-input").val(); var val = $("#palette-search-input").val();
if (val === "") { if (val === "") {
@ -215,6 +266,7 @@ RED.palette = (function() {
add:addNodeType, add:addNodeType,
remove:removeNodeType, remove:removeNodeType,
hide:hideNodeType, hide:hideNodeType,
show:showNodeType show:showNodeType,
refresh:refreshNodeTypes
}; };
})(); })();

View File

@ -47,6 +47,20 @@ RED.sidebar.info = (function() {
table += "<tr><td>Type</td><td>&nbsp;"+node.type+"</td></tr>"; table += "<tr><td>Type</td><td>&nbsp;"+node.type+"</td></tr>";
table += "<tr><td>ID</td><td>&nbsp;"+node.id+"</td></tr>"; table += "<tr><td>ID</td><td>&nbsp;"+node.id+"</td></tr>";
table += '<tr class="blank"><td colspan="2">Properties</td></tr>'; table += '<tr class="blank"><td colspan="2">Properties</td></tr>';
if (node.type == "subflow") {
var userCount = 0;
var subflowType = "subflow:"+node.id;
RED.nodes.eachNode(function(n) {
if (n.type === subflowType) {
userCount++;
}
});
table += "<tr><td>name</td><td>"+node.name+"</td></tr>";
table += "<tr><td>inputs</td><td>"+node.in.length+"</td></tr>";
table += "<tr><td>outputs</td><td>"+node.out.length+"</td></tr>";
table += "<tr><td>instances</td><td>"+userCount+"</td></tr>";
}
if (node._def) {
for (var n in node._def.defaults) { for (var n in node._def.defaults) {
if (node._def.defaults.hasOwnProperty(n)) { if (node._def.defaults.hasOwnProperty(n)) {
var val = node[n]||""; var val = node[n]||"";
@ -73,11 +87,13 @@ RED.sidebar.info = (function() {
val = val.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;"); val = val.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;");
} }
table += "<tr><td>&nbsp;"+n+"</td><td>"+val+"</td></tr>"; table += "<tr><td>"+n+"</td><td>"+val+"</td></tr>";
}
} }
} }
table += "</tbody></table><br/>"; table += "</tbody></table><br/>";
table += '<div class="node-help">'+($("script[data-help-name|='"+node.type+"']").html()||"")+"</div>"; var helpText = $("script[data-help-name|='"+node.type+"']").html()||"";
table += '<div class="node-help">'+helpText+"</div>";
$("#tab-info").html(table); $("#tab-info").html(table);
} }

View File

@ -116,6 +116,13 @@ RED.tabs = (function() {
}, },
contains: function(id) { contains: function(id) {
return ul.find("a[href='#"+id+"']").length > 0; 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();
} }
} }

View File

@ -31,6 +31,8 @@ RED.view = (function() {
var activeWorkspace = 0; var activeWorkspace = 0;
var activeSubflow = null;
var workspaceScrollPositions = {}; var workspaceScrollPositions = {};
var selected_link = null, var selected_link = null,
@ -225,6 +227,11 @@ RED.view = (function() {
var drag_line = vis.append("svg:path").attr("class", "drag_line"); 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({ var workspace_tabs = RED.tabs.create({
id: "workspace-tabs", id: "workspace-tabs",
onchange: function(tab) { onchange: function(tab) {
@ -244,6 +251,8 @@ RED.view = (function() {
var scrollStartTop = chart.scrollTop(); var scrollStartTop = chart.scrollTop();
activeWorkspace = tab.id; activeWorkspace = tab.id;
activeSubflow = RED.nodes.subflow(activeWorkspace);
if (workspaceScrollPositions[activeWorkspace]) { if (workspaceScrollPositions[activeWorkspace]) {
chart.scrollLeft(workspaceScrollPositions[activeWorkspace].left); chart.scrollLeft(workspaceScrollPositions[activeWorkspace].left);
chart.scrollTop(workspaceScrollPositions[activeWorkspace].top); chart.scrollTop(workspaceScrollPositions[activeWorkspace].top);
@ -258,6 +267,9 @@ RED.view = (function() {
mouse_position[1] += scrollDeltaTop; mouse_position[1] += scrollDeltaTop;
} }
RED.menu.setDisabled("btn-workspace-edit", activeSubflow);
RED.menu.setDisabled("btn-workspace-delete",workspace_tabs.count() == 1 || activeSubflow);
clearSelection(); clearSelection();
RED.nodes.eachNode(function(n) { RED.nodes.eachNode(function(n) {
n.dirty = true; n.dirty = true;
@ -265,7 +277,11 @@ RED.view = (function() {
redraw(); redraw();
}, },
ondblclick: function(tab) { ondblclick: function(tab) {
if (tab.type != "subflow") {
showRenameWorkspaceDialog(tab.id); showRenameWorkspaceDialog(tab.id);
} else {
showSubflowDialog(tab.id);
}
}, },
onadd: function(tab) { onadd: function(tab) {
RED.menu.addItem("btn-workspace-menu",{ RED.menu.addItem("btn-workspace-menu",{
@ -300,11 +316,12 @@ RED.view = (function() {
} }
$(function() { $(function() {
$('#btn-workspace-add-tab').on("click",addWorkspace); $('#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); showRenameWorkspaceDialog(activeWorkspace);
}); });
$('#btn-workspace-delete').on("click",function() { RED.menu.setAction('btn-workspace-delete',function() {
deleteWorkspace(activeWorkspace); deleteWorkspace(activeWorkspace);
}); });
}); });
@ -349,7 +366,6 @@ RED.view = (function() {
function canvasMouseMove() { function canvasMouseMove() {
mouse_position = d3.touches(this)[0]||d3.mouse(this); mouse_position = d3.touches(this)[0]||d3.mouse(this);
// Prevent touch scrolling... // Prevent touch scrolling...
//if (d3.touches(this)[0]) { //if (d3.touches(this)[0]) {
// d3.event.preventDefault(); // d3.event.preventDefault();
@ -478,8 +494,10 @@ RED.view = (function() {
} }
} }
} }
if (mouse_mode !== 0) {
redraw(); redraw();
} }
}
function canvasMouseUp() { function canvasMouseUp() {
if (mousedown_node && mouse_mode == RED.state.JOINING) { if (mousedown_node && mouse_mode == RED.state.JOINING) {
@ -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(); updateSelection();
lasso.remove(); lasso.remove();
lasso = null; lasso = null;
@ -545,11 +579,28 @@ RED.view = (function() {
else { zoomIn(); } else { zoomIn(); }
} }
}); });
$("#chart").droppable({ $("#chart").droppable({
accept:".palette_node", accept:".palette_node",
drop: function( event, ui ) { drop: function( event, ui ) {
d3.event = event; d3.event = event;
var selected_tool = ui.draggable[0].type; 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("<strong>Error</strong>: Cannot add subflow to itself","error");
return;
}
if (RED.nodes.subflowContains(m[1],activeSubflow.id)) {
RED.notify("<strong>Error</strong>: Cannot add subflow - circular reference detected","error");
return;
}
}
var mousePos = d3.touches(this)[0]||d3.mouse(this); var mousePos = d3.touches(this)[0]||d3.mouse(this);
mousePos[1] += this.scrollTop; mousePos[1] += this.scrollTop;
mousePos[0] += this.scrollLeft; mousePos[0] += this.scrollLeft;
@ -560,6 +611,9 @@ RED.view = (function() {
nn.type = selected_tool; nn.type = selected_tool;
nn._def = RED.nodes.getType(nn.type); nn._def = RED.nodes.getType(nn.type);
if (!m) {
nn.inputs = nn._def.inputs || 0;
nn.outputs = nn._def.outputs; nn.outputs = nn._def.outputs;
nn.changed = true; nn.changed = true;
@ -572,6 +626,11 @@ RED.view = (function() {
if (nn._def.onadd) { if (nn._def.onadd) {
nn._def.onadd.call(nn); 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); nn.h = Math.max(node_height,(nn.outputs||0) * 15);
RED.history.push({t:'add',nodes:[nn.id],dirty:dirty}); RED.history.push({t:'add',nodes:[nn.id],dirty:dirty});
@ -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; selected_link = null;
updateSelection(); updateSelection();
redraw(); redraw();
@ -638,10 +714,12 @@ RED.view = (function() {
RED.menu.setDisabled("btn-export-menu",true); RED.menu.setDisabled("btn-export-menu",true);
RED.menu.setDisabled("btn-export-clipboard",true); RED.menu.setDisabled("btn-export-clipboard",true);
RED.menu.setDisabled("btn-export-library",true); RED.menu.setDisabled("btn-export-library",true);
RED.menu.setDisabled("btn-convert-subflow",true);
} else { } else {
RED.menu.setDisabled("btn-export-menu",false); RED.menu.setDisabled("btn-export-menu",false);
RED.menu.setDisabled("btn-export-clipboard",false); RED.menu.setDisabled("btn-export-clipboard",false);
RED.menu.setDisabled("btn-export-library",false); RED.menu.setDisabled("btn-export-library",false);
RED.menu.setDisabled("btn-convert-subflow",false);
} }
if (moving_set.length === 0 && selected_link == null) { if (moving_set.length === 0 && selected_link == null) {
RED.keyboard.remove(/* backspace */ 8); 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); 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) { if (moving_set.length == 1) {
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); RED.sidebar.info.refresh(moving_set[0].n);
}
} else if (moving_set.length === 0 && activeSubflow) {
RED.sidebar.info.refresh(activeSubflow);
} else { } else {
RED.sidebar.info.clear(); RED.sidebar.info.clear();
} }
@ -716,16 +800,22 @@ RED.view = (function() {
for (var i=0;i<moving_set.length;i++) { for (var i=0;i<moving_set.length;i++) {
var node = moving_set[i].n; var node = moving_set[i].n;
node.selected = false; node.selected = false;
if (node.type != "subflow") {
if (node.x < 0) { if (node.x < 0) {
node.x = 25 node.x = 25
} }
var rmlinks = RED.nodes.remove(node.id); var rmlinks = RED.nodes.remove(node.id);
removedNodes.push(node); removedNodes.push(node);
removedLinks = removedLinks.concat(rmlinks); removedLinks = removedLinks.concat(rmlinks);
} else {
node.dirty = true;
}
} }
moving_set = []; moving_set = [];
if (removedNodes.length > 0) {
setDirty(true); setDirty(true);
} }
}
if (selected_link) { if (selected_link) {
RED.nodes.removeLink(selected_link); RED.nodes.removeLink(selected_link);
removedLinks.push(selected_link); removedLinks.push(selected_link);
@ -743,10 +833,12 @@ RED.view = (function() {
var nns = []; var nns = [];
for (var n=0;n<moving_set.length;n++) { for (var n=0;n<moving_set.length;n++) {
var node = moving_set[n].n; var node = moving_set[n].n;
if (node.type != "subflow") {
nns.push(RED.nodes.convertNode(node)); nns.push(RED.nodes.convertNode(node));
} }
}
clipboard = JSON.stringify(nns); clipboard = JSON.stringify(nns);
RED.notify(moving_set.length+" node"+(moving_set.length>1?"s":"")+" copied"); RED.notify(nns.length+" node"+(nns.length>1?"s":"")+" copied");
} }
} }
@ -772,6 +864,7 @@ RED.view = (function() {
} }
function portMouseDown(d,portType,portIndex) { function portMouseDown(d,portType,portIndex) {
//console.log(d,portType,portIndex);
// disable zoom // disable zoom
//vis.call(d3.behavior.zoom().on("zoom"), null); //vis.call(d3.behavior.zoom().on("zoom"), null);
mousedown_node = d; mousedown_node = d;
@ -794,7 +887,7 @@ RED.view = (function() {
if (n.x-hw<mouse_position[0] && n.x+hw> mouse_position[0] && if (n.x-hw<mouse_position[0] && n.x+hw> mouse_position[0] &&
n.y-hh<mouse_position[1] && n.y+hh>mouse_position[1]) { n.y-hh<mouse_position[1] && n.y+hh>mouse_position[1]) {
mouseup_node = n; mouseup_node = n;
portType = mouseup_node._def.inputs>0?1:0; portType = mouseup_node.inputs>0?1:0;
portIndex = 0; portIndex = 0;
} }
} }
@ -817,7 +910,6 @@ RED.view = (function() {
dst = mousedown_node; dst = mousedown_node;
src_port = portIndex; src_port = portIndex;
} }
var existingLink = false; var existingLink = false;
RED.nodes.eachLink(function(d) { 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);
@ -827,6 +919,7 @@ RED.view = (function() {
RED.nodes.addLink(link); RED.nodes.addLink(link);
RED.history.push({t:'add',links:[link],dirty:dirty}); RED.history.push({t:'add',links:[link],dirty:dirty});
setDirty(true); setDirty(true);
} else {
} }
selected_link = null; selected_link = null;
redraw(); redraw();
@ -835,12 +928,18 @@ RED.view = (function() {
function nodeMouseUp(d) { function nodeMouseUp(d) {
if (dblClickPrimed && mousedown_node == d && clickElapsed > 0 && clickElapsed < 750) { if (dblClickPrimed && mousedown_node == d && clickElapsed > 0 && clickElapsed < 750) {
mouse_mode = RED.state.DEFAULT;
if (d.type != "subflow") {
RED.editor.edit(d); RED.editor.edit(d);
} else {
RED.editor.editSubflow(activeSubflow);
}
clickElapsed = 0; clickElapsed = 0;
d3.event.stopPropagation(); d3.event.stopPropagation();
return; 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) { function nodeMouseDown(d) {
@ -949,6 +1048,114 @@ RED.view = (function() {
if (mouse_mode != RED.state.JOINING) { if (mouse_mode != RED.state.JOINING) {
// Don't bother redrawing nodes if we're drawing links // 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}); var node = vis.selectAll(".nodegroup").data(RED.nodes.nodes.filter(function(d) { return d.z == activeWorkspace }),function(d){return d.id});
node.exit().remove(); node.exit().remove();
@ -1083,7 +1290,7 @@ RED.view = (function() {
//icon.attr('class','node_icon_shade_border node_icon_shade_border_'+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_shade.attr("width",35);
// icon.attr("transform","translate(5,0)"); // icon.attr("transform","translate(5,0)");
// icon_shade_border.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}); //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("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_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); 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)||""; 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.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.h = Math.max(node_height,(d.outputs||0) * 15);
d.resize = false;
} }
var thisNode = d3.select(this); var thisNode = d3.select(this);
//thisNode.selectAll(".centerDot").attr({"cx":function(d) { return d.w/2;},"cy":function(d){return d.h/2}}); //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_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)}); //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 numOutputs = d.outputs;
var y = (d.h/2)-((numOutputs-1)/2)*13; var y = (d.h/2)-((numOutputs-1)/2)*13;
d.ports = d.ports || d3.range(numOutputs); d.ports = d.ports || d3.range(numOutputs);
d._ports = thisNode.selectAll(".port_output").data(d.ports); 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("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("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("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("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("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);}); .on("mouseout",function(d,i) { var port = d3.select(this); port.classed("port_hovered",false);});
d._ports.exit().remove(); d._ports.exit().remove();
if (d._ports) { if (d._ports) {
numOutputs = d.outputs || 1; numOutputs = d.outputs || 1;
@ -1195,7 +1409,8 @@ RED.view = (function() {
var x = d.w - 5; var x = d.w - 5;
d._ports.each(function(d,i) { d._ports.each(function(d,i) {
var port = d3.select(this); 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){ thisNode.selectAll('text.node_label').text(function(d,i){
@ -1226,7 +1441,7 @@ RED.view = (function() {
thisNode.selectAll(".port_input").each(function(d,i) { thisNode.selectAll(".port_input").each(function(d,i) {
var port = d3.select(this); 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;}); 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"); var linkEnter = link.enter().insert("g",".node").attr("class","link");
@ -1323,7 +1545,8 @@ RED.view = (function() {
d3.event.stopPropagation(); d3.event.stopPropagation();
}); });
l.append("svg:path").attr("class","link_outline link_path"); 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(); link.exit().remove();
@ -1367,6 +1590,7 @@ RED.view = (function() {
if (d3.event) { if (d3.event) {
d3.event.preventDefault(); d3.event.preventDefault();
} }
} }
RED.keyboard.add(/* z */ 90,{ctrl:true},function(){RED.history.pop();}); RED.keyboard.add(/* z */ 90,{ctrl:true},function(){RED.history.pop();});
@ -1401,6 +1625,7 @@ RED.view = (function() {
var new_nodes = result[0]; var new_nodes = result[0];
var new_links = result[1]; var new_links = result[1];
var new_workspaces = result[2]; 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_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; }); var new_node_ids = new_nodes.map(function(n){ return n.id; });
@ -1452,14 +1677,25 @@ RED.view = (function() {
moving_set = new_ms; 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(); redraw();
} }
} catch(error) { } catch(error) {
if (error.code != "NODE_RED") {
console.log(error.stack); console.log(error.stack);
RED.notify("<strong>Error</strong>: "+error,"error"); RED.notify("<strong>Error</strong>: "+error,"error");
} else {
RED.notify("<strong>Error</strong>: "+error.message,"error");
}
} }
} }
@ -1513,6 +1749,11 @@ RED.view = (function() {
$( "#node-dialog-rename-workspace" ).dialog("open"); $( "#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 form" ).submit(function(e) { e.preventDefault();});
$( "#node-dialog-rename-workspace" ).dialog({ $( "#node-dialog-rename-workspace" ).dialog({
modal: true, modal: true,
@ -1535,11 +1776,10 @@ RED.view = (function() {
var workspace = $(this).dialog('option','workspace'); var workspace = $(this).dialog('option','workspace');
var label = $( "#node-input-workspace-name" ).val(); var label = $( "#node-input-workspace-name" ).val();
if (workspace.label != label) { if (workspace.label != label) {
workspace.label = label; workspace_tabs.renameTab(workspace.id,label);
var link = $("#workspace-tabs a[href='#"+workspace.id+"']");
link.attr("title",label);
link.text(label);
RED.view.dirty(true); RED.view.dirty(true);
$("#btn-workspace-menu-"+workspace.id.replace(".","-")).text(label);
// TODO: update entry in menu
} }
$( this ).dialog( "close" ); $( this ).dialog( "close" );
} }
@ -1607,7 +1847,9 @@ RED.view = (function() {
workspace_tabs.resize(); workspace_tabs.resize();
}, },
removeWorkspace: function(ws) { removeWorkspace: function(ws) {
if (workspace_tabs.contains(ws.id)) {
workspace_tabs.removeTab(ws.id); workspace_tabs.removeTab(ws.id);
}
}, },
getWorkspace: function() { getWorkspace: function() {
return activeWorkspace; return activeWorkspace;
@ -1615,7 +1857,14 @@ RED.view = (function() {
showWorkspace: function(id) { showWorkspace: function(id) {
workspace_tabs.activateTab(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) { dirty: function(d) {
if (d == null) { if (d == null) {
return dirty; return dirty;
@ -1638,6 +1887,220 @@ RED.view = (function() {
//TODO: should these move to an import/export module? //TODO: should these move to an import/export module?
showImportNodesDialog: showImportNodesDialog, showImportNodesDialog: showImportNodesDialog,
showExportNodesDialog: showExportNodesDialog, 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("<strong>Cannot create subflow</strong>: 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<moving_set.length;i++) {
var n = moving_set[i];
nodes[n.n.id] = {n:n.n,outputs:{}};
boundingBox = [
Math.min(boundingBox[0],n.n.x),
Math.min(boundingBox[1],n.n.y),
Math.max(boundingBox[2],n.n.x),
Math.max(boundingBox[3],n.n.y)
]
}
var center = [(boundingBox[2]+boundingBox[0]) / 2,(boundingBox[3]+boundingBox[1]) / 2];
RED.nodes.eachLink(function(link) {
if (nodes[link.source.id] && nodes[link.target.id]) {
// A link wholely within the selection
}
if (nodes[link.source.id] && !nodes[link.target.id]) {
// An outbound link from the selection
candidateOutputs.push(link);
removedLinks.push(link);
}
if (!nodes[link.source.id] && nodes[link.target.id]) {
// An inbound link
candidateInputs.push(link);
removedLinks.push(link);
}
});
var outputs = {};
candidateOutputs = candidateOutputs.filter(function(v) {
if (outputs[v.source.id+":"+v.sourcePort]) {
outputs[v.source.id+":"+v.sourcePort].targets.push(v.target);
return false;
}
v.targets = [];
v.targets.push(v.target);
outputs[v.source.id+":"+v.sourcePort] = v;
return true;
});
candidateOutputs.sort(function(a,b) { return a.source.y-b.source.y});
if (candidateInputs.length > 1) {
RED.notify("<strong>Cannot create subflow</strong>: multiple inputs to selection","error");
return;
}
//if (candidateInputs.length == 0) {
// RED.notify("<strong>Cannot create subflow</strong>: 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<removedLinks.length;i++) {
RED.nodes.removeLink(removedLinks[i]);
}
for (i=0;i<moving_set.length;i++) {
moving_set[i].n.z = subflow.id;
}
RED.history.push({
t:'createSubflow',
nodes:[subflowInstance.id],
links:new_links,
subflow: subflow,
activeWorkspace: activeWorkspace,
removedLinks: removedLinks,
dirty:RED.view.dirty()
});
setDirty(true);
redraw();
}
}; };
})(); })();

View File

@ -74,6 +74,14 @@ span.logo span {
span.logo img { span.logo img {
height: 18px; height: 18px;
} }
.button {
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
#header .button { #header .button {
line-height: 22px; line-height: 22px;
display: inline-block; display: inline-block;
@ -118,7 +126,26 @@ span.logo img {
outline: none; outline: none;
} }
#workspace-toolbar .button {
line-height: 18px;
display: inline-block;
font-size: 12px;
padding: 2px 8px;
text-decoration: none;
border-radius: 3px;
color: #666;
background: #f6f6f6;
vertical-align: middle;
box-shadow: 0 0 2px #888;
}
#workspace-toolbar .button:hover {
background: #e6e6e6;
box-shadow: 0 0 2px #666;
}
#workspace-toolbar .button:active {
background: #e0e0e0;
box-shadow: 0 0 2px #444;
}
#workspace { #workspace {
margin-left: 160px; margin-left: 160px;
@ -140,8 +167,9 @@ span.logo img {
position: absolute; position: absolute;
top: 30px; top: 30px;
left:0; left:0;
right: 18px; right: 20px;
padding: 5px; padding: 7px;
border-bottom-right-radius: 5px;
background: #f3f3f3; background: #f3f3f3;
} }
@ -263,9 +291,10 @@ span.logo img {
clear: both; clear: both;
} }
.palette_label { .palette_label {
line-height: 25px; margin: 4px 0;
line-height: 20px;
text-align: center; text-align: center;
overflow: hidden;
} }
.palette_node { .palette_node {
cursor:move; cursor:move;
@ -425,6 +454,22 @@ span.logo img {
user-select: none; user-select: none;
} }
.port_label {
stroke-width: 0;
fill: #888;
font-size: 16px;
alignment-baseline: middle;
text-anchor: middle;
pointer-events: none;
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.function_label { .function_label {
font-size: 12px; font-size: 12px;
} }
@ -460,9 +505,21 @@ span.logo img {
} }
.port { .port {
stroke: #999;
stroke-width: 2;
fill: #ddd; fill: #ddd;
cursor: crosshair; cursor: crosshair;
} }
.port_highlight {
stroke: #6DA332;
stroke-width: 3;
fill: #fff;
pointer-events:none;
fill-opacity: 0.5;
}
.node_error { .node_error {
stroke: #ff0000; stroke: #ff0000;
stroke-width: 2; stroke-width: 2;
@ -491,7 +548,7 @@ span.logo img {
stroke: #ff0000; stroke: #ff0000;
} }
.node_selected { .node_selected {
stroke: #ff7f0e; stroke: #ff7f0e !important;
} }
.node_highlighted { .node_highlighted {
stroke: #dd1616; stroke: #dd1616;
@ -505,6 +562,11 @@ span.logo img {
stroke: #ff7f0e; stroke: #ff7f0e;
fill: #ff7f0e; fill: #ff7f0e;
} }
.subflowport {
stroke-dasharray: 5,5;
fill: #eee;
stroke: #999;
}
.drag_line { .drag_line {
stroke: #ff7f0e; stroke: #ff7f0e;
@ -527,6 +589,12 @@ span.logo img {
pointer-events: none; pointer-events: none;
} }
.link_subflow {
stroke: #bbb;
stroke-dasharray: 10,5;
stroke-width: 3;
}
.link_outline { .link_outline {
stroke: #fff; stroke: #fff;
stroke-width: 6; stroke-width: 6;

View File

@ -15,6 +15,7 @@
**/ **/
var util = require("util"); var util = require("util");
var clone = require("clone");
var when = require("when"); var when = require("when");
var typeRegistry = require("./registry"); var typeRegistry = require("./registry");
@ -25,6 +26,7 @@ var events = require("../events");
var storage = null; var storage = null;
var nodes = {}; var nodes = {};
var subflows = {};
var activeConfig = []; var activeConfig = [];
var missingTypes = []; var missingTypes = [];
@ -41,19 +43,111 @@ events.on('type-registered',function(type) {
} }
}); });
function getID() {
return (1+Math.random()*4294967295).toString(16);
}
function createSubflow(sf,sfn) {
var node_map = {};
var newNodes = [];
var node;
var wires;
var i,j,k;
// Clone all of the subflow node definitions and give them new IDs
for (i=0;i<sf.nodes.length;i++) {
node = clone(sf.nodes[i]);
var nid = getID();
node_map[node.id] = node;
node.id = nid;
newNodes.push(node);
}
// Update all subflow interior wiring to reflect new node IDs
for (i=0;i<newNodes.length;i++) {
node = newNodes[i];
var outputs = node.wires;
for (j=0;j<outputs.length;j++) {
wires = outputs[j];
for (k=0;k<wires.length;k++) {
outputs[j][k] = node_map[outputs[j][k]].id
}
}
}
// Create a subflow node to accept inbound messages and route appropriately
var Node = require("./Node");
var subflowInstance = {
id: sfn.id,
type: sfn.type,
name: sfn.name,
wires: []
}
if (sf.in) {
subflowInstance.wires = sf.in.map(function(n) { return n.wires.map(function(w) { return node_map[w.id].id;})})
}
var subflowNode = new Node(subflowInstance);
subflowNode.on("input", function(msg) { this.send(msg);});
// Wire the subflow outputs
if (sf.out) {
for (i=0;i<sf.out.length;i++) {
wires = sf.out[i].wires;
for (j=0;j<wires.length;j++) {
if (wires[j].id === sf.id) {
node = subflowNode;
delete subflowNode._wire;
} else {
node = node_map[wires[j].id];
}
node.wires[wires[j].port] = node.wires[wires[j].port].concat(sfn.wires[i]);
}
}
}
// Instantiate the nodes
for (i=0;i<newNodes.length;i++) {
node = newNodes[i];
var nn = null;
var type = node.type;
var m = /^subflow:(.+)$/.exec(type);
if (!m) {
var nt = typeRegistry.get(type);
if (nt) {
try {
nn = new nt(node);
}
catch (err) {
util.log("[red] "+type+" : "+err);
}
}
if (nn === null) {
util.log("[red] unknown type: "+type);
}
} else {
var subflowId = m[1];
createSubflow(subflows[subflowId],node);
}
}
}
/** /**
* Parses the current activeConfig and creates the required node instances * Parses the current activeConfig and creates the required node instances
*/ */
function parseConfig() { function parseConfig() {
var i; var i;
var nt; var nt;
var type;
var subflow;
missingTypes = []; missingTypes = [];
// Scan the configuration for any unknown node types // Scan the configuration for any unknown node types
for (i=0;i<activeConfig.length;i++) { for (i=0;i<activeConfig.length;i++) {
var type = activeConfig[i].type; type = activeConfig[i].type;
// TODO: remove workspace in next release+1 // TODO: remove workspace in next release+1
if (type != "workspace" && type != "tab") { if (type != "workspace" && type != "tab" && !/^subflow($|:.+$)/.test(type)) {
nt = typeRegistry.get(type); nt = typeRegistry.get(type);
if (!nt && missingTypes.indexOf(type) == -1) { if (!nt && missingTypes.indexOf(type) == -1) {
missingTypes.push(type); missingTypes.push(type);
@ -72,25 +166,50 @@ function parseConfig() {
util.log("[red] Starting flows"); util.log("[red] Starting flows");
events.emit("nodes-starting"); events.emit("nodes-starting");
for (i=0;i<activeConfig.length;i++) {
type = activeConfig[i].type;
if (type === "subflow") {
subflow = activeConfig[i];
subflow.nodes = [];
subflow.instances = [];
subflows[subflow.id] = subflow;
}
}
for (i=0;i<activeConfig.length;i++) {
if (subflows[activeConfig[i].z]) {
subflow = subflows[activeConfig[i].z];
subflow.nodes.push(activeConfig[i]);
}
}
// Instantiate each node in the flow // Instantiate each node in the flow
for (i=0;i<activeConfig.length;i++) { for (i=0;i<activeConfig.length;i++) {
var nn = null; var nn = null;
type = activeConfig[i].type;
var m = /^subflow:(.+)$/.exec(type);
if (!m) {
// TODO: remove workspace in next release+1 // TODO: remove workspace in next release+1
if (activeConfig[i].type != "workspace" && activeConfig[i].type != "tab") { if (type != "workspace" && type != "tab" && type != "subflow" && !subflows[activeConfig[i].z]) {
nt = typeRegistry.get(activeConfig[i].type); nt = typeRegistry.get(type);
if (nt) { if (nt) {
try { try {
nn = new nt(activeConfig[i]); nn = new nt(activeConfig[i]);
} }
catch (err) { catch (err) {
util.log("[red] "+activeConfig[i].type+" : "+err); util.log("[red] "+type+" : "+err);
} }
} }
// console.log(nn);
if (nn === null) { if (nn === null) {
util.log("[red] unknown type: "+activeConfig[i].type); util.log("[red] unknown type: "+type);
} }
} }
} else {
var subflowId = m[1];
createSubflow(subflows[subflowId],activeConfig[i]);
}
} }
// Clean up any orphaned credentials // Clean up any orphaned credentials
credentials.clean(flowNodes.get); credentials.clean(flowNodes.get);

View File

@ -46,6 +46,12 @@ function loadFlows(testFlows, cb) {
describe('flows', function() { describe('flows', function() {
afterEach(function(done) {
flows.clear().then(function() {
loadFlows([],done);
});
});
describe('#add',function() { describe('#add',function() {
it('should be called by node constructor',function(done) { it('should be called by node constructor',function(done) {
var n = new RedNode({id:'123',type:'abc'}); var n = new RedNode({id:'123',type:'abc'});
@ -108,6 +114,58 @@ describe('flows', function() {
done(); done();
}); });
}); });
it('should not instantiate nodes of an unused subflow', function(done) {
RED.registerType('abc', function() {});
var typeRegistryGet = sinon.stub(typeRegistry,"get",function(nt) {
return function() {};
});
loadFlows([{"id":"n1","type":"subflow",inputs:[],outputs:[],wires:[]},
{"id":"n2","type":"abc","z":"n1",wires:[]}
],function() { });
events.once('nodes-started', function() {
(flows.get("n2") == null).should.be.true;
var ncount = 0
flows.each(function(n) {
ncount++;
});
ncount.should.equal(0);
console.log(ncount);
typeRegistryGet.restore();
done();
});
});
it('should instantiate nodes of an used subflow with new IDs', function(done) {
RED.registerType('abc', function() {});
var typeRegistryGet = sinon.stub(typeRegistry,"get",function(nt) {
return RedNode;
});
loadFlows([{"id":"n1","type":"subflow",inputs:[],outputs:[]},
{"id":"n2","type":"abc","z":"n1","name":"def",wires:[]},
{"id":"n3","type":"subflow:n1"}
], function() { });
events.once('nodes-started', function() {
// n2 should not get instantiated with that id
(flows.get("n2") == null).should.be.true;
var ncount = 0
var nodes = [];
flows.each(function(n) {
nodes.push(n);
});
nodes.should.have.lengthOf(2);
// Assume the nodes are instantiated in this order - not
// a requirement, but makes the test easier to write.
nodes[0].should.have.property("id","n3");
nodes[0].should.have.property("type","subflow:n1");
nodes[1].should.not.have.property("id","n2");
nodes[1].should.have.property("name","def");
// TODO: verify instance wiring is correct
typeRegistryGet.restore();
done();
});
});
}); });
describe('#setFlows',function() { describe('#setFlows',function() {