diff --git a/editor/js/ui/typedInput.js b/editor/js/ui/typedInput.js index 758bb51c8..94abdb9a0 100644 --- a/editor/js/ui/typedInput.js +++ b/editor/js/ui/typedInput.js @@ -150,7 +150,11 @@ this.typeField = $("",{type:'hidden'}).appendTo(this.uiSelect); } - this.selectTrigger = $('').prependTo(this.uiSelect); + this.selectTrigger = $('').prependTo(this.uiSelect); + $('').appendTo(this.selectTrigger); + if (this.types.length === 1) { + this.selectTrigger.addClass("disabled"); + } this.selectLabel = $('').appendTo(this.selectTrigger); this.element.on('focus', function() { @@ -162,11 +166,17 @@ this.element.on('change', function() { that.validate(); }) - - this.selectTrigger.click(function(event) { - event.preventDefault(); - that._showMenu(that.menu,that.selectTrigger); - }); + if (this.types.length > 1) { + this.selectTrigger.click(function(event) { + event.preventDefault(); + that._showMenu(that.menu,that.selectTrigger); + }); + } else { + this.selectTrigger.click(function(event) { + event.preventDefault(); + that.element.focus(); + }) + } if (hasSubOptions) { diff --git a/editor/sass/typedInput.scss b/editor/sass/typedInput.scss index 32644ee85..13063a744 100644 --- a/editor/sass/typedInput.scss +++ b/editor/sass/typedInput.scss @@ -57,21 +57,26 @@ margin-top: 1px; vertical-align: middle; } - + &.disabled { + cursor: default; + i { + color: #bbb; + } + } span { display: inline-block; height: 100%; padding: 0 1px 0 5px; } - &:hover { + &:not(.disabled):hover { text-decoration: none; background: $typedInput-button-background-hover; } &:focus { text-decoration: none; } - &:active { + &:not(.disabled):active { background: $typedInput-button-background-active; text-decoration: none; } diff --git a/nodes/core/logic/17-split.html b/nodes/core/logic/17-split.html new file mode 100644 index 000000000..e847baf0d --- /dev/null +++ b/nodes/core/logic/17-split.html @@ -0,0 +1,164 @@ + + + + + + + + + + + + + + diff --git a/nodes/core/logic/17-split.js b/nodes/core/logic/17-split.js new file mode 100644 index 000000000..ece2d9b69 --- /dev/null +++ b/nodes/core/logic/17-split.js @@ -0,0 +1,205 @@ +/** + * Copyright 2016 IBM Corp. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ + +module.exports = function(RED) { + "use strict"; + + function SplitNode(n) { + RED.nodes.createNode(this,n); + this.splt = n.splt || ""; + this.split = new RegExp(this.splt); + var node = this; + this.on("input", function(msg) { + if (msg.hasOwnProperty("payload")) { + var a = msg.payload; + if (msg.hasOwnProperty("parts")) { msg.parts = { parts:msg.parts }; } // push existing parts to a stack + else { msg.parts = {}; } + msg.parts.id = msg._msgid; // use the existing _msgid by default. + if (typeof msg.payload === "string") { // Split String into array + a = msg.payload.split(node.split); + msg.parts.ch = node.splt; // pass the split char to other end for rejoin + msg.parts.type = "string"; + } + if (Array.isArray(a)) { // then split array into messages + msg.parts.type = msg.parts.type || "array"; // if it wasn't a string in the first place + for (var i = 0; i < a.length; i++) { + msg.payload = a[i]; + msg.parts.index = i; + msg.parts.count = a.length; + node.send(msg); + } + } + else if ((typeof msg.payload === "object") && !Buffer.isBuffer(msg.payload)) { + var j = 0; + var l = Object.keys(msg.payload).length; + var pay = msg.payload; + msg.parts.type = "object"; + for (var p in pay) { + if (pay.hasOwnProperty(p)) { + msg.payload = pay[p]; + msg.parts.key = p; + msg.parts.index = j; + msg.parts.count = l; + node.send(msg); + j += 1; + } + } + } + // TODO not handling Buffers at present... + //else { } // otherwise drop the message. + } + }); + } + RED.nodes.registerType("split",SplitNode); + + + function JoinNode(n) { + RED.nodes.createNode(this,n); + this.timer = Number(n.timeout || 0); + this.timerr = n.timerr || "send"; + this.count = Number(n.count || 0); + this.joiner = n.joiner; + this.build = n.build || "array"; + var node = this; + var inflight = {}; + var misc = (this.build === "array") ? [] : {}; + var tout; + + function isObject (item) { + return (typeof item === "object" && !Array.isArray(item)&& ! Buffer.isBuffer(item) && item !== null); + } + + // if array came from a string then reassemble it and send it + var sendIt = function(m) { + if (inflight[m.parts.id].ch !== undefined) { // if it was a string - rejoin it using the split char + var jc = (node.joiner || inflight[m.parts.id].ch).replace(/\\n/,"\n").replace(/\\r/,"\r").replace(/\\t/,"\t").replace(/\\e/,"\e").replace(/\\f/,"\c").replace(/\\0/,"\0"); + m.payload = inflight[m.parts.id].a.join(jc); + } else { // leave it as an array + m.payload = inflight[m.parts.id].a; + } + m._msgid = m.parts.id; + clearTimeout(inflight[m.parts.id].timeout); // unset any timer + delete inflight[m.parts.id]; // remove from the keep track object + if (m.parts.hasOwnProperty("parts")) { + m.parts = m.parts.parts; // pop existing parts + } + else { delete m.parts; } // remove the parts flags + node.send(m); + } + + // check all elements of the array are strings (or claim to be). + var onlyString = function(a) { // check if the array is all strings + for (var i = 0; i < a.length; i++) { + if (typeof a[i] !== "string") { return false; } + } + return true; + } + + // send array of misc message that arrived. (convert to string if all were strings and need joining) + var sendMisc = function(m) { + if (tout) { clearTimeout(tout); tout = null; } + m.payload = misc; + if (node.joiner && onlyString(misc)) { // if the array is all strings and there is a join char set + m.payload = misc.join(node.joiner.replace(/\\n/,"\n").replace(/\\r/,"\r").replace(/\\t/,"\t").replace(/\\e/,"\e").replace(/\\f/,"\c").replace(/\\0/,"\0")); + } + if (node.build === "array") { misc = []; } + if (node.build === "object") { misc = {}; } + node.send(m); + } + + this.on("input", function(msg) { + if (msg.hasOwnProperty("payload")) { + if (msg.hasOwnProperty("parts")) { // only act if it has parts + var count = node.count || msg.parts.count || 1; + if (msg.parts.hasOwnProperty("index")) { // it's a numbered part (from a split node) + if (!inflight[msg.parts.id]) { // New message - create new empty array of correct size + if (msg.parts.type === "object") { + inflight[msg.parts.id] = {i:0, a:{}, c:msg.parts.count, ch:msg.parts.ch, t:msg.parts.type}; + } else { // it's an array or string + inflight[msg.parts.id] = {i:0, a:new Array(msg.parts.count), ch:msg.parts.ch, t:msg.parts.type}; + } + if (node.timer !== 0) { // If there is a timer to set start it now + inflight[msg.parts.id].timeout = setTimeout(function() { + if (node.timerr === "send") { sendIt(msg); } + if (node.timerr === "error") { node.error("Incomplete",msg); } + delete inflight[msg.parts.id]; + }, node.timer); + } + } + if (msg.parts.type === "object") { + inflight[msg.parts.id].a[msg.parts.key] = msg.payload; // Add to the tracking array + inflight[msg.parts.id].i = Object.keys(inflight[msg.parts.id].a).length; + } else { // it's an array or string + inflight[msg.parts.id].a[msg.parts.index] = msg.payload; // Add to the tracking array + inflight[msg.parts.id].i += 1; // Increment the count + } + if (inflight[msg.parts.id].i >= count) { sendIt(msg); } // All arrived - send + } // otherwise ignore it + if (msg.hasOwnProperty("complete")) { // if set then send right away anyway... + delete(msg.complete); + sendIt(msg); + } + } + + // The case for any messages arriving without parts - ie random messages you want to join. + else { + var l; + if (node.build === "array") { // simple case of build the array + misc.push(msg.payload); // Add the payload to an array + l = misc.length; + } else { // OK so let's build an object + if ((msg.key === undefined) && ((msg.topic === undefined) || (msg.topic === ''))) { + if (isObject(msg.payload)) { // if it's already an object (and no topic or key) just append it + misc = Object.assign(misc,msg.payload); + l = Object.keys(misc).length; + } + else { // if no topic or key and not an object then warn and drop it. + node.warn("key or topic not defined"); + return; + } + } + else { // if it's got a msg.key or msg.topic then use key||topic as the property name + misc[ msg.key || msg.topic ] = msg.payload; + //if (msg.topic) { msg.topic = (msg.topic.split('/')).slice(0,-1).join('/'); } + l = Object.keys(misc).length; + } + } + if (l >= node.count) { sendMisc(msg); } // if it's long enough send it + else if (msg.hasOwnProperty("complete")) { // if set then send right away anyway... + delete(msg.complete); + sendMisc(msg); + } + else if ((node.timer !== 0) && !tout) { // if not start the timer if there is one. + tout = setTimeout(function() { + if (node.timerr === "send") { sendMisc(msg); } + if (node.timerr === "error") { node.error("Timeout",msg); } + if (node.build === "array") { misc = []; } + if (node.build === "object") { misc = {}; } + }, node.timer); + } + } + } + }); + + this.on("close", function() { + if (tout) { clearTimeout(tout); } + for (var i in inflight) { + if (inflight[i].timeout) { clearTimeout(inflight[i].timeout); } + } + }); + } + RED.nodes.registerType("join",JoinNode); +} diff --git a/test/nodes/core/logic/17-split_spec.js b/test/nodes/core/logic/17-split_spec.js new file mode 100644 index 000000000..cc34c3274 --- /dev/null +++ b/test/nodes/core/logic/17-split_spec.js @@ -0,0 +1,379 @@ +/** + * Copyright 2016 IBM Corp. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ + +var should = require("should"); +var splitNode = require("../../../../nodes/core/logic/17-split.js"); +var joinNode = require("../../../../nodes/core/logic/17-split.js"); +var helper = require("../../helper.js"); + +describe('SPLIT node', function() { + + before(function(done) { + helper.startServer(done); + }); + + afterEach(function() { + helper.unload(); + }); + + it('should be loaded', function(done) { + var flow = [{id:"splitNode1", type:"split", name:"splitNode" }]; + helper.load(splitNode, flow, function() { + var splitNode1 = helper.getNode("splitNode1"); + splitNode1.should.have.property('name', 'splitNode'); + done(); + }); + }); + + it('should split an array into multiple messages', function(done) { + var flow = [{id:"sn1", type:"split", wires:[["sn2"]]}, + {id:"sn2", type:"helper"}]; + helper.load(splitNode, flow, function() { + var sn1 = helper.getNode("sn1"); + var sn2 = helper.getNode("sn2"); + sn2.on("input", function(msg) { + msg.should.have.property("parts"); + msg.parts.should.have.property("count",4); + msg.parts.should.have.property("type","array"); + msg.parts.should.have.property("index"); + if (msg.parts.index === 0) { msg.payload.should.equal(1); } + if (msg.parts.index === 1) { msg.payload.should.equal(2); } + if (msg.parts.index === 2) { msg.payload.should.equal(3); } + if (msg.parts.index === 3) { msg.payload.should.equal(4); done(); } + }); + sn1.receive({payload:[1,2,3,4]}); + }); + }); + + it('should split a string into characters', function(done) { + var flow = [{id:"sn1", type:"split", wires:[["sn2"]]}, + {id:"sn2", type:"helper"}]; + helper.load(splitNode, flow, function() { + var sn1 = helper.getNode("sn1"); + var sn2 = helper.getNode("sn2"); + sn2.on("input", function(msg) { + msg.should.have.property("parts"); + msg.parts.should.have.property("count",4); + msg.parts.should.have.property("type","string"); + msg.parts.should.have.property("index"); + if (msg.parts.index === 0) { msg.payload.should.equal("D"); } + if (msg.parts.index === 1) { msg.payload.should.equal("a"); } + if (msg.parts.index === 2) { msg.payload.should.equal("v"); } + if (msg.parts.index === 3) { msg.payload.should.equal("e"); done(); } + }); + sn1.receive({payload:"Dave"}); + }); + }); + + it('should split a string on a specified char', function(done) { + var flow = [{id:"sn1", type:"split", wires:[["sn2"]], splt:"\n"}, + {id:"sn2", type:"helper"}]; + helper.load(splitNode, flow, function() { + var sn1 = helper.getNode("sn1"); + var sn2 = helper.getNode("sn2"); + sn2.on("input", function(msg) { + msg.should.have.property("parts"); + msg.parts.should.have.property("count",3); + msg.parts.should.have.property("ch","\n"); + msg.parts.should.have.property("index"); + msg.parts.should.have.property("type","string"); + if (msg.parts.index === 0) { msg.payload.should.equal("1"); } + if (msg.parts.index === 1) { msg.payload.should.equal("2"); } + if (msg.parts.index === 2) { msg.payload.should.equal("3"); done(); } + }); + sn1.receive({payload:"1\n2\n3"}); + }); + }); + + it('should split an object into pieces', function(done) { + var flow = [{id:"sn1", type:"split", wires:[["sn2"]]}, + {id:"sn2", type:"helper"}]; + helper.load(splitNode, flow, function() { + var sn1 = helper.getNode("sn1"); + var sn2 = helper.getNode("sn2"); + var count = 0; + sn2.on("input", function(msg) { + msg.should.have.property("payload"); + msg.should.have.property("parts"); + msg.parts.should.have.property("type","object"); + msg.parts.should.have.property("key"); + msg.parts.should.have.property("count"); + msg.parts.should.have.property("index"); + if (msg.parts.index === 0) { msg.payload.should.equal(1); } + if (msg.parts.index === 1) { msg.payload.should.equal("2"); } + if (msg.parts.index === 2) { msg.payload.should.equal(true); done(); } + }); + sn1.receive({payload:{a:1,b:"2",c:true}}); + }); + }); + +}); + +describe('JOIN node', function() { + + before(function(done) { + helper.startServer(done); + }); + + afterEach(function() { + helper.unload(); + }); + + it('should be loaded', function(done) { + var flow = [{id:"joinNode1", type:"join", name:"joinNode" }]; + helper.load(joinNode, flow, function() { + var joinNode1 = helper.getNode("joinNode1"); + joinNode1.should.have.property('name', 'joinNode'); + joinNode1.should.have.property('count', 0); + joinNode1.should.have.property('timer', 0); + joinNode1.should.have.property('timerr', 'send'); + joinNode1.should.have.property('build', 'array'); + done(); + }); + }); + + it('should join things into an array after a count', function(done) { + var flow = [{id:"n1", type:"join", wires:[["n2"]], count:3, joiner:","}, + {id:"n2", type:"helper"}]; + helper.load(joinNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n2.on("input", function(msg) { + try { + msg.should.have.property("payload"); + msg.payload.should.be.an.Array; + msg.payload[0].should.equal(1); + msg.payload[1].should.equal(true); + //msg.payload[2].a.should.equal(1); + done(); + } + catch(e) { }//console.log(e); } + }); + n1.receive({payload:1}); + n1.receive({payload:true}); + n1.receive({payload:{a:1}}); + }); + }); + + it('should join things into an object after a count', function(done) { + var flow = [{id:"n1", type:"join", wires:[["n2"]], count:5, build:"object"}, + {id:"n2", type:"helper"}]; + helper.load(joinNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n2.on("input", function(msg) { + try { + msg.should.have.property("payload"); + msg.payload.should.have.property("a",1); + msg.payload.should.have.property("b","2"); + msg.payload.should.have.property("c",true); + msg.payload.should.have.property("d"); + msg.payload.d.should.have.property("e",5); + msg.payload.should.have.property("f",6); + done(); + } + catch(e) { }//console.log(e); } + }); + n1.receive({payload:1, topic:"a"}); + n1.receive({payload:"2", topic:"b"}); + n1.receive({payload:true, topic:"c"}); + n1.receive({payload:{e:5}, topic:"d"}); + n1.receive({payload:{f:6}}); + }); + }); + + it('should join strings with a specifed character after a timeout', function(done) { + var flow = [{id:"n1", type:"join", wires:[["n2"]], timeout:50, count:10, joiner:","}, + {id:"n2", type:"helper"}]; + helper.load(joinNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n2.on("input", function(msg) { + try { + msg.should.have.property("payload"); + msg.payload.should.equal("a,b,c"); + done(); + } + catch(e) { console.log(e); } + }); + n1.receive({payload:"a"}); + n1.receive({payload:"b"}); + n1.receive({payload:"c"}); + }); + }); + + it('should join strings with a specifed character and complete when told to', function(done) { + var flow = [{id:"n1", type:"join", wires:[["n2"]], timeout:5000, count:100, joiner:"\n"}, + {id:"n2", type:"helper"}]; + helper.load(joinNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n2.on("input", function(msg) { + try { + msg.should.have.property("payload"); + msg.payload.should.equal("Hello\nNodeRED\nWorld\n"); + done(); + } + catch(e) { console.log(e); } + }); + n1.receive({payload:"Hello"}); + n1.receive({payload:"NodeRED"}); + n1.receive({payload:"World"}); + n1.receive({payload:'', complete:true}); + }); + }); + + it('should join split things back into an array', function(done) { + var flow = [{id:"n1", type:"join", wires:[["n2"]]}, + {id:"n2", type:"helper"}]; + helper.load(joinNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n2.on("input", function(msg) { + try { + msg.should.have.property("payload"); + msg.payload.should.be.an.Array; + msg.payload[0].should.equal(1); + msg.payload[1].should.equal(2); + msg.payload[2].should.equal(3); + msg.payload[3].should.equal(4); + done(); + } + catch(e) { console.log(e); } + }); + n1.receive({payload:3, parts:{index:2, count:4, id:111}}); + n1.receive({payload:2, parts:{index:1, count:4, id:111}}); + n1.receive({payload:4, parts:{index:3, count:4, id:111}}); + n1.receive({payload:1, parts:{index:0, count:4, id:111}}); + }); + }); + + it('should join split things back into an object', function(done) { + var flow = [{id:"n1", type:"join", wires:[["n2"]]}, + {id:"n2", type:"helper"}]; + helper.load(joinNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n2.on("input", function(msg) { + try { + msg.should.have.property("payload"); + msg.payload.should.have.property("a",1); + msg.payload.should.have.property("b",2); + msg.payload.should.have.property("c",3); + msg.payload.should.have.property("d",4); + done(); + } + catch(e) { console.log(e); } + }); + n1.receive({payload:3, parts:{index:2, count:4, id:222, key:"c", type:"object"}}); + n1.receive({payload:2, parts:{index:1, count:4, id:222, key:"b", type:"object"}}); + n1.receive({payload:4, parts:{index:3, count:4, id:222, key:"d", type:"object"}}); + n1.receive({payload:1, parts:{index:0, count:4, id:222, key:"a", type:"object"}}); + }); + }); + + it.skip('should join split things, missing some after a timeout', function(done) { + var flow = [{id:"n1", type:"join", wires:[["n2"]], timeout:50}, + {id:"n2", type:"helper"}]; + helper.load(joinNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n2.on("input", function(msg) { + try { + msg.should.have.property("payload"); + msg.payload.should.be.an.Array; + (msg.payload[0] === undefined).should.be.true; + msg.payload[1].should.equal(2); + msg.payload[2].should.equal(3); + (msg.payload[3] === undefined).should.be.true; + done(); + } + catch(e) { console.log(e); } + }); + n1.receive({payload:3, parts:{index:2, count:4, id:333}}); + n1.receive({payload:2, parts:{index:1, count:4, id:333}}); + }); + }); + + it('should join split things, send when told complete', function(done) { + var flow = [{id:"n1", type:"join", wires:[["n2"]], timeout:250}, + {id:"n2", type:"helper"}]; + helper.load(joinNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n2.on("input", function(msg) { + try { + msg.should.have.property("payload"); + msg.payload.should.be.an.Array; + (msg.payload[0] === undefined).should.be.true; + msg.payload[1].should.equal(2); + msg.payload[2].should.equal(3); + msg.payload[3].should.equal(4); + done(); + } + catch(e) { console.log(e); } + }); + n1.receive({payload:3, parts:{index:2, count:4, id:444} }); + n1.receive({payload:2, parts:{index:1, count:4, id:444} }); + n1.receive({payload:4, parts:{index:3, count:4, id:444}, complete:true}); + }); + }); + + it('should join split strings back into a word', function(done) { + var flow = [{id:"n1", type:"join", wires:[["n2"]]}, + {id:"n2", type:"helper"}]; + helper.load(joinNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n2.on("input", function(msg) { + try { + msg.should.have.property("payload"); + msg.payload.should.be.an.Array; + msg.payload.should.equal("abcd"); + done(); + } + catch(e) { console.log(e); } + }); + n1.receive({payload:"a", parts:{index:0, count:4, ch:"", id:555}}); + n1.receive({payload:"d", parts:{index:3, count:4, ch:"", id:555}}); + n1.receive({payload:"c", parts:{index:2, count:4, ch:"", id:555}}); + n1.receive({payload:"b", parts:{index:1, count:4, ch:"", id:555}}); + }); + }); + + it('should join split strings back overriding the join char', function(done) { + var flow = [{id:"n1", type:"join", wires:[["n2"]], joiner:":"}, + {id:"n2", type:"helper"}]; + helper.load(joinNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n2.on("input", function(msg) { + try { + msg.should.have.property("payload"); + msg.payload.should.be.an.Array; + msg.payload.should.equal("a:b:c:d"); + done(); + } + catch(e) { console.log(e); } + }); + n1.receive({payload:"a", parts:{index:0, count:4, ch:"", id:666}}); + n1.receive({payload:"d", parts:{index:3, count:4, ch:"", id:666}}); + n1.receive({payload:"c", parts:{index:2, count:4, ch:"", id:666}}); + n1.receive({payload:"b", parts:{index:1, count:4, ch:"", id:666}}); + }); + }); + +});