mirror of
				https://github.com/node-red/node-red.git
				synced 2025-03-01 10:36:34 +00:00 
			
		
		
		
	Add subflow support
This commit is contained in:
		
							
								
								
									
										
											BIN
										
									
								
								public/icons/subflow.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								public/icons/subflow.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 439 B | 
| @@ -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="chart"></div> | ||||
|         <div id="workspace-toolbar"> | ||||
|             <div class="btn-group"> | ||||
|                 <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> | ||||
|             <a class="button" id="workspace-edit-subflow" href="#"><i class="fa fa-pencil"></i> edit subflow properties</a> | ||||
|         </div> | ||||
|     </div> | ||||
|  | ||||
| @@ -81,6 +77,20 @@ | ||||
|  | ||||
| <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="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"> | ||||
|     <form class="form-horizontal"> | ||||
| @@ -199,7 +209,6 @@ | ||||
|         </div> | ||||
|     </form> | ||||
| </div> | ||||
|  | ||||
| <script type="text/x-red" data-template-name="export-clipboard-dialog"> | ||||
|     <div class="form-row"> | ||||
|         <label for="node-input-export" style="display: block; width:100%;"><i class="fa fa-clipboard"></i> Nodes:</label> | ||||
| @@ -222,6 +231,13 @@ | ||||
|     </div> | ||||
| </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="bootstrap/js/bootstrap.min.js"></script> | ||||
| <script src="jquery/js/jquery-ui-1.10.3.custom.min.js"></script> | ||||
|   | ||||
| @@ -50,6 +50,12 @@ RED.history = (function() { | ||||
|                             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") { | ||||
|                     if (ev.workspaces) { | ||||
|                         for (i=0;i<ev.workspaces.length;i++) { | ||||
| @@ -57,6 +63,9 @@ RED.history = (function() { | ||||
|                             RED.view.addWorkspace(ev.workspaces[i]); | ||||
|                         } | ||||
|                     } | ||||
|                     if (ev.subflow) { | ||||
|                         RED.nodes.addSubflow(ev.subflow); | ||||
|                     } | ||||
|                     if (ev.nodes) { | ||||
|                         for (i=0;i<ev.nodes.length;i++) { | ||||
|                             RED.nodes.add(ev.nodes[i]); | ||||
| @@ -80,15 +89,66 @@ RED.history = (function() { | ||||
|                             ev.node[i] = ev.changes[i]; | ||||
|                         } | ||||
|                     } | ||||
|                     RED.editor.updateNodeProperties(ev.node); | ||||
|                     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.validateNode(ev.node); | ||||
|                     } | ||||
|                     if (ev.links) { | ||||
|                         for (i=0;i<ev.links.length;i++) { | ||||
|                             RED.nodes.addLink(ev.links[i]); | ||||
|                         } | ||||
|                     } | ||||
|                     RED.editor.validateNode(ev.node); | ||||
|                     ev.node.dirty = true; | ||||
|                     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.redraw(); | ||||
|   | ||||
| @@ -301,6 +301,9 @@ var RED = (function() { | ||||
|                 null, | ||||
|                 {id:"btn-config-nodes",icon:"fa fa-th-list",label:"Configuration nodes...",onselect:RED.sidebar.config.show}, | ||||
|                 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-add",icon:"fa fa-plus",label:"Add"}, | ||||
|                     {id:"btn-workspace-edit",icon:"fa fa-pencil",label:"Rename"}, | ||||
|   | ||||
| @@ -21,6 +21,7 @@ RED.nodes = (function() { | ||||
|     var links = []; | ||||
|     var defaultWorkspace; | ||||
|     var workspaces = {}; | ||||
|     var subflows = {}; | ||||
|      | ||||
|     var registry = (function() { | ||||
|         var nodeList = []; | ||||
| @@ -98,13 +99,22 @@ RED.nodes = (function() { | ||||
|             }, | ||||
|             registerNodeType: function(nt,def) { | ||||
|                 nodeDefinitions[nt] = def; | ||||
|                 nodeSets[typeToId[nt]].added = true; | ||||
|                 // TODO: too tightly coupled into palette UI | ||||
|                 if (def.category != "subflows") { | ||||
|                     nodeSets[typeToId[nt]].added = true; | ||||
|                     // TODO: too tightly coupled into palette UI | ||||
|                 } | ||||
|                 RED.palette.add(nt,def); | ||||
|                 if (def.onpaletteadd && typeof def.onpaletteadd === "function") { | ||||
|                     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) { | ||||
|                 return nodeDefinitions[nt]; | ||||
|             } | ||||
| @@ -122,7 +132,6 @@ RED.nodes = (function() { | ||||
|             RED.sidebar.config.refresh(); | ||||
|         } else { | ||||
|             n.dirty = true; | ||||
|             nodes.push(n); | ||||
|             var updatedConfigNode = false; | ||||
|             for (var d in n._def.defaults) { | ||||
|                 if (n._def.defaults.hasOwnProperty(d)) { | ||||
| @@ -142,6 +151,14 @@ RED.nodes = (function() { | ||||
|             if (updatedConfigNode) { | ||||
|                 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) { | ||||
| @@ -174,27 +191,27 @@ RED.nodes = (function() { | ||||
|             if (node) { | ||||
|                 nodes.splice(nodes.indexOf(node),1); | ||||
|                 removedLinks = links.filter(function(l) { return (l.source === node) || (l.target === node); }); | ||||
|                 removedLinks.map(function(l) {links.splice(links.indexOf(l), 1); }); | ||||
|             } | ||||
|             var updatedConfigNode = false; | ||||
|             for (var d in node._def.defaults) { | ||||
|                 if (node._def.defaults.hasOwnProperty(d)) { | ||||
|                     var property = node._def.defaults[d]; | ||||
|                     if (property.type) { | ||||
|                         var type = registry.getNodeType(property.type); | ||||
|                         if (type && type.category == "config") { | ||||
|                             var configNode = configNodes[node[d]]; | ||||
|                             if (configNode) { | ||||
|                                 updatedConfigNode = true; | ||||
|                                 var users = configNode.users; | ||||
|                                 users.splice(users.indexOf(node),1); | ||||
|                 removedLinks.forEach(function(l) {links.splice(links.indexOf(l), 1); }); | ||||
|                 var updatedConfigNode = false; | ||||
|                 for (var d in node._def.defaults) { | ||||
|                     if (node._def.defaults.hasOwnProperty(d)) { | ||||
|                         var property = node._def.defaults[d]; | ||||
|                         if (property.type) { | ||||
|                             var type = registry.getNodeType(property.type); | ||||
|                             if (type && type.category == "config") { | ||||
|                                 var configNode = configNodes[node[d]]; | ||||
|                                 if (configNode) { | ||||
|                                     updatedConfigNode = true; | ||||
|                                     var users = configNode.users; | ||||
|                                     users.splice(users.indexOf(node),1); | ||||
|                                 } | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|             if (updatedConfigNode) { | ||||
|                 RED.sidebar.config.refresh(); | ||||
|                 if (updatedConfigNode) { | ||||
|                     RED.sidebar.config.refresh(); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         return removedLinks; | ||||
| @@ -237,6 +254,50 @@ RED.nodes = (function() { | ||||
|         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) { | ||||
|         var visited = {}; | ||||
|         visited[node.id] = true; | ||||
| @@ -247,8 +308,12 @@ RED.nodes = (function() { | ||||
|             var childLinks = links.filter(function(d) { return (d.source === n) || (d.target === n);}); | ||||
|             for (var i=0;i<childLinks.length;i++) { | ||||
|                 var child = (childLinks[i].source === n)?childLinks[i].target:childLinks[i].source; | ||||
|                 if (!visited[child.id]) { | ||||
|                     visited[child.id] = true; | ||||
|                 var id = child.id; | ||||
|                 if (!id) { | ||||
|                     id = child.direction+":"+child.i; | ||||
|                 } | ||||
|                 if (!visited[id]) { | ||||
|                     visited[id] = true; | ||||
|                     nns.push(child); | ||||
|                     stack.push(child); | ||||
|                 } | ||||
| @@ -291,37 +356,94 @@ RED.nodes = (function() { | ||||
|             var wires = links.filter(function(d){return d.source === n;}); | ||||
|             for (var j=0;j<wires.length;j++) { | ||||
|                 var w = wires[j]; | ||||
|                 node.wires[w.sourcePort].push(w.target.id); | ||||
|                 if (w.target.type != "subflow") { | ||||
|                     node.wires[w.sourcePort].push(w.target.id); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         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 | ||||
|      **/ | ||||
|     function createExportableNodeSet(set) { | ||||
|         var nns = []; | ||||
|         var exportedConfigNodes = {}; | ||||
|         var exportedSubflows = {}; | ||||
|         for (var n=0;n<set.length;n++) { | ||||
|             var node = set[n].n; | ||||
|             var convertedNode = RED.nodes.convertNode(node); | ||||
|             for (var d in node._def.defaults) { | ||||
|                 if (node._def.defaults[d].type && node[d] in configNodes) { | ||||
|                     var confNode = configNodes[node[d]]; | ||||
|                     var exportable = registry.getNodeType(node._def.defaults[d].type).exportable; | ||||
|                     if ((exportable == null || exportable)) { | ||||
|                         if (!(node[d] in exportedConfigNodes)) { | ||||
|                             exportedConfigNodes[node[d]] = true; | ||||
|                             nns.unshift(RED.nodes.convertNode(confNode)); | ||||
|             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}); | ||||
|                         } | ||||
|                     } else { | ||||
|                         convertedNode[d] = ""; | ||||
|                     } | ||||
|                     }); | ||||
|                     var exportableSubflow = createExportableNodeSet(subflowSet); | ||||
|                     nns = exportableSubflow.concat(nns); | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             nns.push(convertedNode); | ||||
|             if (node.type != "subflow") { | ||||
|                 var convertedNode = RED.nodes.convertNode(node); | ||||
|                 for (var d in node._def.defaults) { | ||||
|                     if (node._def.defaults[d].type && node[d] in configNodes) { | ||||
|                         var confNode = configNodes[node[d]]; | ||||
|                         var exportable = registry.getNodeType(node._def.defaults[d].type).exportable; | ||||
|                         if ((exportable == null || exportable)) { | ||||
|                             if (!(node[d] in exportedConfigNodes)) { | ||||
|                                 exportedConfigNodes[node[d]] = true; | ||||
|                                 nns.unshift(RED.nodes.convertNode(confNode)); | ||||
|                             } | ||||
|                         } else { | ||||
|                             convertedNode[d] = ""; | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|                 nns.push(convertedNode); | ||||
|             } else { | ||||
|                 var convertedSubflow = convertSubflow(node); | ||||
|                 nns.push(convertedSubflow); | ||||
|             } | ||||
|         } | ||||
|         return nns; | ||||
|     } | ||||
| @@ -332,7 +454,14 @@ RED.nodes = (function() { | ||||
|         var i; | ||||
|         for (i in workspaces) { | ||||
|             if (workspaces.hasOwnProperty(i)) { | ||||
|                 nns.push(workspaces[i]); | ||||
|                 if (workspaces[i].type == "tab") { | ||||
|                     nns.push(workspaces[i]); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         for (i in subflows) { | ||||
|             if (subflows.hasOwnProperty(i)) { | ||||
|                 nns.push(convertSubflow(subflows[i])); | ||||
|             } | ||||
|         } | ||||
|         for (i in configNodes) { | ||||
| @@ -368,7 +497,11 @@ RED.nodes = (function() { | ||||
|             for (i=0;i<newNodes.length;i++) { | ||||
|                 n = newNodes[i]; | ||||
|                 // 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) | ||||
|                     n.name = n.type; | ||||
|                     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"); | ||||
|             } | ||||
|  | ||||
|             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 workspace_map = {}; | ||||
|              | ||||
|             var new_subflows = []; | ||||
|             var subflow_map = {}; | ||||
|             var nid; | ||||
|             for (i=0;i<newNodes.length;i++) { | ||||
|                 n = newNodes[i]; | ||||
|                 // TODO: remove workspace in next release+1 | ||||
| @@ -403,13 +562,34 @@ RED.nodes = (function() { | ||||
|                         defaultWorkspace = n; | ||||
|                     } | ||||
|                     if (createNewIds) { | ||||
|                         var nid = getID(); | ||||
|                         nid = getID(); | ||||
|                         workspace_map[n.id] = nid; | ||||
|                         n.id = nid; | ||||
|                     } | ||||
|                     addWorkspace(n); | ||||
|                     RED.view.addWorkspace(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) { | ||||
| @@ -426,7 +606,7 @@ RED.nodes = (function() { | ||||
|             for (i=0;i<newNodes.length;i++) { | ||||
|                 n = newNodes[i]; | ||||
|                 // 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); | ||||
|                     if (def && def.category == "config") { | ||||
|                         if (!RED.nodes.node(n.id)) { | ||||
| @@ -443,36 +623,53 @@ RED.nodes = (function() { | ||||
|                     } else { | ||||
|                         var node = {x:n.x,y:n.y,z:n.z,type:0,wires:n.wires,changed:false}; | ||||
|                         if (createNewIds) { | ||||
|                             node.z = workspace_map[node.z]; | ||||
|                             if (!workspaces[node.z]) { | ||||
|                                 node.z = RED.view.getWorkspace(); | ||||
|                             if (subflow_map[node.z]) { | ||||
|                                 node.z = subflow_map[node.z].id; | ||||
|                             } else { | ||||
|                                 node.z = workspace_map[node.z]; | ||||
|                                 if (!workspaces[node.z]) { | ||||
|                                     node.z = activeWorkspace; | ||||
|                                 } | ||||
|                             } | ||||
|                             node.id = getID(); | ||||
|                         } else { | ||||
|                             node.id = n.id; | ||||
|                             if (node.z == null || !workspaces[node.z]) { | ||||
|                                 node.z = RED.view.getWorkspace(); | ||||
|                             if (node.z == null || (!workspaces[node.z] && !subflow_map[node.z])) { | ||||
|                                 node.z = activeWorkspace; | ||||
|                             } | ||||
|                         } | ||||
|                         node.type = n.type; | ||||
|                         node._def = def; | ||||
|                         if (!node._def) { | ||||
|                             node._def = { | ||||
|                                 color:"#fee", | ||||
|                                 defaults: {}, | ||||
|                                 label: "unknown: "+n.type, | ||||
|                                 labelStyle: "node_label_italic", | ||||
|                                 outputs: n.outputs||n.wires.length | ||||
|                         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) { | ||||
|                                 node._def = { | ||||
|                                     color:"#fee", | ||||
|                                     defaults: {}, | ||||
|                                     label: "unknown: "+n.type, | ||||
|                                     labelStyle: "node_label_italic", | ||||
|                                     outputs: n.outputs||n.wires.length | ||||
|                                 } | ||||
|                             } | ||||
|                             node.inputs = n.inputs||node._def.inputs; | ||||
|                             node.outputs = n.outputs||node._def.outputs; | ||||
|      | ||||
|                             for (var d2 in node._def.defaults) { | ||||
|                                 if (node._def.defaults.hasOwnProperty(d2)) { | ||||
|                                     node[d2] = n[d2]; | ||||
|                                 } | ||||
|                             } | ||||
|                         } | ||||
|                         node.outputs = n.outputs||node._def.outputs; | ||||
|  | ||||
|                         for (var d2 in node._def.defaults) { | ||||
|                             if (node._def.defaults.hasOwnProperty(d2)) { | ||||
|                                 node[d2] = n[d2]; | ||||
|                             } | ||||
|                         } | ||||
|  | ||||
|                         addNode(node); | ||||
|                         RED.editor.validateNode(node); | ||||
|                         node_map[n.id] = node; | ||||
| @@ -494,10 +691,38 @@ RED.nodes = (function() { | ||||
|                 } | ||||
|                 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) { | ||||
|             //TODO: get this UI thing out of here! (see above as well) | ||||
|             RED.notify("<strong>Error</strong>: "+error,"error"); | ||||
|             if (error.code != "NODE_RED") { | ||||
|                 console.log(error.stack); | ||||
|                 RED.notify("<strong>Error</strong>: "+error,"error"); | ||||
|             } else { | ||||
|                 RED.notify("<strong>Error</strong>: "+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<nodes.length;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, | ||||
|         import: importNodes, | ||||
|         refreshValidation: refreshValidation, | ||||
|   | ||||
| @@ -95,22 +95,31 @@ RED.editor = (function() { | ||||
|         node.resize = true; | ||||
|         node.dirty = true; | ||||
|         var removedLinks = []; | ||||
|         if (node.outputs < node.ports.length) { | ||||
|             while (node.outputs < node.ports.length) { | ||||
|                 node.ports.pop(); | ||||
|         if (node.ports) { | ||||
|             if (node.outputs < node.ports.length) { | ||||
|                 while (node.outputs < node.ports.length) { | ||||
|                     node.ports.pop(); | ||||
|                 } | ||||
|                 RED.nodes.eachLink(function(l) { | ||||
|                         if (l.source === node && l.sourcePort >= 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<removedLinks.length;l++) { | ||||
|                 RED.nodes.removeLink(removedLinks[l]); | ||||
|             } | ||||
|         } else if (node.outputs > node.ports.length) { | ||||
|             while (node.outputs > node.ports.length) { | ||||
|                 node.ports.push(node.ports.length); | ||||
|             } | ||||
|         } | ||||
|         for (var l=0;l<removedLinks.length;l++) { | ||||
|             RED.nodes.removeLink(removedLinks[l]); | ||||
|         } | ||||
|         return removedLinks; | ||||
|     } | ||||
| @@ -274,6 +283,11 @@ RED.editor = (function() { | ||||
|                     RED.sidebar.info.refresh(editing_node); | ||||
|                 } | ||||
|                 RED.sidebar.config.refresh(); | ||||
|                  | ||||
|                 var buttons = $( this ).dialog("option","buttons"); | ||||
|                 if (buttons.length == 3) { | ||||
|                     $( this ).dialog("option","buttons",buttons.splice(1)); | ||||
|                 } | ||||
|                 editing_node = null; | ||||
|             } | ||||
|     }); | ||||
| @@ -459,10 +473,30 @@ RED.editor = (function() { | ||||
|     function showEditDialog(node) { | ||||
|         editing_node = node; | ||||
|         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"); | ||||
|         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<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 { | ||||
|         edit: showEditDialog, | ||||
|         editConfig: showEditConfigNodeDialog, | ||||
|         editSubflow: showEditSubflowDialog, | ||||
|         validateNode: validateNode, | ||||
|         updateNodeProperties: updateNodeProperties // TODO: only exposed for edit-undo | ||||
|     } | ||||
|   | ||||
| @@ -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 | ||||
|     } | ||||
| })(); | ||||
|   | ||||
| @@ -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<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) { | ||||
|  | ||||
|         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 = '<div class="palette_label"'+ | ||||
|                 (multiLine ? 'style="line-height: '+ | ||||
|                     lineHeight + 'px; margin-top: 5px"' : '')+ | ||||
|                 '>'+label+"</div>"; | ||||
|             d.innerHTML = '<div class="palette_label"></div>'; | ||||
|              | ||||
|             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 = $("<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({ | ||||
|                 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 = '<div class="node-help">'+($("script[data-help-name|='"+d.type+"']").html()||"")+"</div>"; | ||||
| @@ -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 | ||||
|     }; | ||||
| })(); | ||||
|   | ||||
| @@ -47,37 +47,53 @@ RED.sidebar.info = (function() { | ||||
|         table += "<tr><td>Type</td><td> "+node.type+"</td></tr>"; | ||||
|         table += "<tr><td>ID</td><td> "+node.id+"</td></tr>"; | ||||
|         table += '<tr class="blank"><td colspan="2">Properties</td></tr>'; | ||||
|         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,"<").replace(/>/g,">"); | ||||
|                 } else if (type === "number") { | ||||
|                     val = val.toString(); | ||||
|                 } else if ($.isArray(val)) { | ||||
|                     val = "[<br/>"; | ||||
|                     for (var i=0;i<Math.min(node[n].length,10);i++) { | ||||
|                         var vv = JSON.stringify(node[n][i],jsonFilter," ").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">"); | ||||
|                         val += " "+i+": "+vv+"<br/>"; | ||||
|                     } | ||||
|                     if (node[n].length > 10) { | ||||
|                         val += " ... "+node[n].length+" items<br/>"; | ||||
|                     } | ||||
|                     val += "]"; | ||||
|                 } else { | ||||
|                     val = JSON.stringify(val,jsonFilter," "); | ||||
|                     val = val.replace(/&/g,"&").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 += "<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) { | ||||
|                 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,"<").replace(/>/g,">"); | ||||
|                     } else if (type === "number") { | ||||
|                         val = val.toString(); | ||||
|                     } else if ($.isArray(val)) { | ||||
|                         val = "[<br/>"; | ||||
|                         for (var i=0;i<Math.min(node[n].length,10);i++) { | ||||
|                             var vv = JSON.stringify(node[n][i],jsonFilter," ").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">"); | ||||
|                             val += " "+i+": "+vv+"<br/>"; | ||||
|                         } | ||||
|                         if (node[n].length > 10) { | ||||
|                             val += " ... "+node[n].length+" items<br/>"; | ||||
|                         } | ||||
|                         val += "]"; | ||||
|                     } else { | ||||
|                         val = JSON.stringify(val,jsonFilter," "); | ||||
|                         val = val.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">"); | ||||
|                     } | ||||
|                      | ||||
|                     table += "<tr><td>"+n+"</td><td>"+val+"</td></tr>"; | ||||
|                 } | ||||
|                  | ||||
|                 table += "<tr><td> "+n+"</td><td>"+val+"</td></tr>"; | ||||
|             } | ||||
|         } | ||||
|         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); | ||||
|     } | ||||
|      | ||||
|   | ||||
| @@ -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(); | ||||
|             } | ||||
|  | ||||
|         } | ||||
|   | ||||
| @@ -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("<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); | ||||
|                 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<moving_set.length;i++) { | ||||
|                 var node = moving_set[i].n; | ||||
|                 node.selected = false; | ||||
|                 if (node.x < 0) { | ||||
|                     node.x = 25 | ||||
|                 if (node.type != "subflow") { | ||||
|                     if (node.x < 0) { | ||||
|                         node.x = 25 | ||||
|                     } | ||||
|                     var rmlinks = RED.nodes.remove(node.id); | ||||
|                     removedNodes.push(node); | ||||
|                     removedLinks = removedLinks.concat(rmlinks); | ||||
|                 } else { | ||||
|                     node.dirty = true; | ||||
|                 } | ||||
|                 var rmlinks = RED.nodes.remove(node.id); | ||||
|                 removedNodes.push(node); | ||||
|                 removedLinks = removedLinks.concat(rmlinks); | ||||
|             } | ||||
|             moving_set = []; | ||||
|             setDirty(true); | ||||
|             if (removedNodes.length > 0) { | ||||
|                 setDirty(true); | ||||
|             } | ||||
|         } | ||||
|         if (selected_link) { | ||||
|             RED.nodes.removeLink(selected_link); | ||||
| @@ -743,10 +833,12 @@ RED.view = (function() { | ||||
|             var nns = []; | ||||
|             for (var n=0;n<moving_set.length;n++) { | ||||
|                 var node = moving_set[n].n; | ||||
|                 nns.push(RED.nodes.convertNode(node)); | ||||
|                 if (node.type != "subflow") { | ||||
|                     nns.push(RED.nodes.convertNode(node)); | ||||
|                 } | ||||
|             } | ||||
|             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) { | ||||
|         //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.x+hw> mouse_position[0] && | ||||
|                                 n.y-hh<mouse_position[1] && n.y+hh>mouse_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("<strong>Error</strong>: "+error,"error"); | ||||
|             if (error.code != "NODE_RED") { | ||||
|                 console.log(error.stack); | ||||
|                 RED.notify("<strong>Error</strong>: "+error,"error"); | ||||
|             } else { | ||||
|                 RED.notify("<strong>Error</strong>: "+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("<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(); | ||||
|         } | ||||
|     }; | ||||
| })(); | ||||
|   | ||||
| @@ -74,6 +74,14 @@ span.logo span { | ||||
| span.logo img { | ||||
|     height: 18px; | ||||
| } | ||||
| .button { | ||||
|    -webkit-user-select: none; | ||||
|    -khtml-user-select: none; | ||||
|    -moz-user-select: none; | ||||
|    -ms-user-select: none; | ||||
|    user-select: none; | ||||
| } | ||||
|  | ||||
| #header .button { | ||||
|     line-height: 22px; | ||||
|     display: inline-block; | ||||
| @@ -118,7 +126,26 @@ span.logo img { | ||||
|     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 { | ||||
|    margin-left: 160px; | ||||
| @@ -140,8 +167,9 @@ span.logo img { | ||||
|     position: absolute; | ||||
|     top: 30px; | ||||
|     left:0; | ||||
|     right: 18px; | ||||
|     padding: 5px; | ||||
|     right: 20px; | ||||
|     padding: 7px; | ||||
|     border-bottom-right-radius: 5px; | ||||
|     background: #f3f3f3; | ||||
| } | ||||
|  | ||||
| @@ -263,9 +291,10 @@ span.logo img { | ||||
|     clear: both; | ||||
| } | ||||
| .palette_label { | ||||
|    line-height: 25px; | ||||
|    text-align: center; | ||||
|  | ||||
|     margin: 4px 0; | ||||
|     line-height: 20px; | ||||
|     text-align: center; | ||||
|     overflow: hidden; | ||||
| } | ||||
| .palette_node { | ||||
|    cursor:move; | ||||
| @@ -425,6 +454,22 @@ span.logo img { | ||||
|    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 { | ||||
|    font-size: 12px; | ||||
| } | ||||
| @@ -460,9 +505,21 @@ span.logo img { | ||||
|  | ||||
| } | ||||
| .port { | ||||
|    stroke: #999; | ||||
|    stroke-width: 2; | ||||
|    fill: #ddd; | ||||
|    cursor: crosshair; | ||||
| } | ||||
|  | ||||
| .port_highlight { | ||||
|    stroke: #6DA332; | ||||
|    stroke-width: 3; | ||||
|    fill: #fff; | ||||
|    pointer-events:none; | ||||
|    fill-opacity: 0.5; | ||||
| } | ||||
|  | ||||
|  | ||||
| .node_error { | ||||
|     stroke: #ff0000; | ||||
|     stroke-width: 2; | ||||
| @@ -491,7 +548,7 @@ span.logo img { | ||||
|    stroke: #ff0000; | ||||
| } | ||||
| .node_selected { | ||||
|    stroke: #ff7f0e; | ||||
|    stroke: #ff7f0e !important; | ||||
| } | ||||
| .node_highlighted { | ||||
|    stroke: #dd1616; | ||||
| @@ -505,6 +562,11 @@ span.logo img { | ||||
|   stroke: #ff7f0e; | ||||
|   fill:  #ff7f0e; | ||||
| } | ||||
| .subflowport { | ||||
|     stroke-dasharray: 5,5; | ||||
|     fill: #eee; | ||||
|     stroke: #999; | ||||
| } | ||||
|  | ||||
| .drag_line { | ||||
|   stroke: #ff7f0e; | ||||
| @@ -527,6 +589,12 @@ span.logo img { | ||||
|   pointer-events: none; | ||||
| } | ||||
|  | ||||
| .link_subflow { | ||||
|   stroke: #bbb; | ||||
|   stroke-dasharray: 10,5; | ||||
|   stroke-width: 3; | ||||
| } | ||||
|  | ||||
| .link_outline { | ||||
|   stroke: #fff; | ||||
|   stroke-width: 6; | ||||
|   | ||||
| @@ -15,6 +15,7 @@ | ||||
|  **/ | ||||
|  | ||||
| var util = require("util"); | ||||
| var clone = require("clone"); | ||||
| var when = require("when"); | ||||
|  | ||||
| var typeRegistry = require("./registry"); | ||||
| @@ -25,6 +26,7 @@ var events = require("../events"); | ||||
| var storage = null; | ||||
|  | ||||
| var nodes = {}; | ||||
| var subflows = {}; | ||||
| var activeConfig = []; | ||||
| var missingTypes = []; | ||||
|  | ||||
| @@ -40,6 +42,95 @@ 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, | ||||
|         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 | ||||
| @@ -47,13 +138,15 @@ events.on('type-registered',function(type) { | ||||
| function parseConfig() { | ||||
|     var i; | ||||
|     var nt; | ||||
|     var type; | ||||
|     var subflow; | ||||
|     missingTypes = []; | ||||
|      | ||||
|     // Scan the configuration for any unknown node types | ||||
|     for (i=0;i<activeConfig.length;i++) { | ||||
|         var type = activeConfig[i].type; | ||||
|         type = activeConfig[i].type; | ||||
|         // TODO: remove workspace in next release+1 | ||||
|         if (type != "workspace" && type != "tab") { | ||||
|         if (type != "workspace" && type != "tab" && !/^subflow($|:.+$)/.test(type)) { | ||||
|             nt = typeRegistry.get(type); | ||||
|             if (!nt && missingTypes.indexOf(type) == -1) { | ||||
|                 missingTypes.push(type); | ||||
| @@ -71,25 +164,50 @@ function parseConfig() { | ||||
|  | ||||
|     util.log("[red] Starting flows"); | ||||
|     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 | ||||
|     for (i=0;i<activeConfig.length;i++) { | ||||
|         var nn = null; | ||||
|         // TODO: remove workspace in next release+1 | ||||
|         if (activeConfig[i].type != "workspace" && activeConfig[i].type != "tab") { | ||||
|             nt = typeRegistry.get(activeConfig[i].type); | ||||
|             if (nt) { | ||||
|                 try { | ||||
|                     nn = new nt(activeConfig[i]); | ||||
|         type = activeConfig[i].type; | ||||
|          | ||||
|         var m = /^subflow:(.+)$/.exec(type); | ||||
|         if (!m) { | ||||
|             // TODO: remove workspace in next release+1 | ||||
|             if (type != "workspace" && type != "tab" && type != "subflow" && !subflows[activeConfig[i].z]) { | ||||
|                 nt = typeRegistry.get(type); | ||||
|                 if (nt) { | ||||
|                     try { | ||||
|                         nn = new nt(activeConfig[i]); | ||||
|                     } | ||||
|                     catch (err) { | ||||
|                         util.log("[red] "+type+" : "+err); | ||||
|                     } | ||||
|                 } | ||||
|                 catch (err) { | ||||
|                     util.log("[red] "+activeConfig[i].type+" : "+err); | ||||
|                 if (nn === null) { | ||||
|                     util.log("[red] unknown type: "+type); | ||||
|                 } | ||||
|             } | ||||
|             // console.log(nn); | ||||
|             if (nn === null) { | ||||
|                 util.log("[red] unknown type: "+activeConfig[i].type); | ||||
|             } | ||||
|         } else { | ||||
|             var subflowId = m[1]; | ||||
|             createSubflow(subflows[subflowId],activeConfig[i]); | ||||
|         } | ||||
|     } | ||||
|     // Clean up any orphaned credentials | ||||
|   | ||||
		Reference in New Issue
	
	Block a user