| @@ -0,0 +1,35 @@ | |||||
| { | |||||
| "env": { | |||||
| "node": false, | |||||
| "browser": true, | |||||
| "jquery": false, | |||||
| "es6": false | |||||
| }, | |||||
| "globals": { | |||||
| "Vue": true, | |||||
| "uibuilder": true | |||||
| }, | |||||
| "extends": "eslint:recommended", | |||||
| "rules": { | |||||
| "no-console": 0, | |||||
| "indent": [ | |||||
| "warn", | |||||
| 4, | |||||
| {"SwitchCase": 1} | |||||
| ], | |||||
| "linebreak-style": [ | |||||
| "error", | |||||
| "unix" | |||||
| ], | |||||
| "quotes": [ | |||||
| "warn", | |||||
| "single" | |||||
| ], | |||||
| "semi": [ | |||||
| "warn", | |||||
| "never" | |||||
| ], | |||||
| "comma-dangle":"off", | |||||
| "no-unused-vars": "warn" | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,14 @@ | |||||
| /* Cloak elements on initial load to hide the possible display of {{ ... }} | |||||
| * Add to the app tag or to specific tags | |||||
| * To display "loading...", change to the following: | |||||
| * [v-cloak] > * { display:none } | |||||
| * [v-cloak]::before { content: "loading…" } | |||||
| */ | |||||
| [v-cloak] { display: none; } | |||||
| /* Colours for Syntax Highlighted pre's */ | |||||
| .syntax-highlight {color:white;background-color:black;padding:5px 10px;} | |||||
| .syntax-highlight > .key {color:#ffbf35} | |||||
| .syntax-highlight > .string {color:#5dff39;} | |||||
| .syntax-highlight > .number {color:#70aeff;} | |||||
| .syntax-highlight > .boolean {color:#b993ff;} | |||||
| @@ -0,0 +1,147 @@ | |||||
| <!doctype html> | |||||
| <!-- Note that adding an appcache really speeds things up after the first load | |||||
| You need to amend the appcache file to meet your needs. | |||||
| Don't forget to change the appcache file if you update ANY | |||||
| of the files in it otherwise the old versions will ALWAYS be used. | |||||
| <html lang="en" manifest="./uibuilder.appcache"> | |||||
| --> | |||||
| <html lang="en"> | |||||
| <!-- | |||||
| This is the default, template html for uibuilder. | |||||
| It is meant to demonstrate the use of VueJS & bootstrap-vue to dynamically | |||||
| update the ui based on incoming/outgoing messages from/to the | |||||
| Node-RED server. | |||||
| You will want to alter this to suite your own needs. To do so, | |||||
| copy this file to <userDir>/uibuilder/<url>/src. | |||||
| --> | |||||
| <head> | |||||
| <meta charset="utf-8"> | |||||
| <meta http-equiv="X-UA-Compatible" content="IE=edge"> | |||||
| <meta name="viewport" content="width=device-width, minimum-scale=1, initial-scale=1, user-scalable=yes"> | |||||
| <title>Node-RED UI Builder</title> | |||||
| <meta name="description" content="Node-RED UI Builder - VueJS + bootstrap-vue version"> | |||||
| <link rel="icon" href="./images/node-blue.ico"> | |||||
| <!-- See https://goo.gl/OOhYW5 --> | |||||
| <link rel="manifest" href="./manifest.json"> | |||||
| <meta name="theme-color" content="#3f51b5"> | |||||
| <!-- Used if adding to homescreen for Chrome on Android. Fallback for manifest.json --> | |||||
| <meta name="mobile-web-app-capable" content="yes"> | |||||
| <meta name="application-name" content="Node-RED UI Builder"> | |||||
| <!-- Used if adding to homescreen for Safari on iOS --> | |||||
| <meta name="apple-mobile-web-app-capable" content="yes"> | |||||
| <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"> | |||||
| <meta name="apple-mobile-web-app-title" content="Node-RED UI Builder"> | |||||
| <!-- Homescreen icons for Apple mobile use if required | |||||
| <link rel="apple-touch-icon" href="./images/manifest/icon-48x48.png"> | |||||
| <link rel="apple-touch-icon" sizes="72x72" href="./images/manifest/icon-72x72.png"> | |||||
| <link rel="apple-touch-icon" sizes="96x96" href="./images/manifest/icon-96x96.png"> | |||||
| <link rel="apple-touch-icon" sizes="144x144" href="./images/manifest/icon-144x144.png"> | |||||
| <link rel="apple-touch-icon" sizes="192x192" href="./images/manifest/icon-192x192.png"> | |||||
| --> | |||||
| <link type="text/css" rel="stylesheet" href="../uibuilder/vendor/bootstrap/dist/css/bootstrap.min.css" /> | |||||
| <link type="text/css" rel="stylesheet" href="../uibuilder/vendor/bootstrap-vue/dist/bootstrap-vue.css" /> | |||||
| <link type="text/css" rel="stylesheet" href="./index.css" media="all"> | |||||
| </head> | |||||
| <body> | |||||
| <div id="disp_single1"></div> | |||||
| <div id="app" v-cloak> | |||||
| <b-container id="app_container"> | |||||
| <b-img src="./images/node-blue-192x192.png" rounded left v-bind="imgProps" alt="Blue Node-RED" class="mt-1 mr-2"></b-img> | |||||
| <h1> | |||||
| UIbuilder + Vue.js + bootstrap-vue for Node-RED | |||||
| </h1> | |||||
| <p> | |||||
| This is a uibuilder example using <a href="https://vuejs.org/">Vue.js</a> as a front-end library. | |||||
| See the | |||||
| <a | |||||
| href="https://github.com/TotallyInformation/node-red-contrib-uibuilder">node-red-contrib-uibuilder</a> | |||||
| README for details on how to use UIbuilder. | |||||
| </p> | |||||
| <h2>Simple input using Vue</h2> | |||||
| <b-form class="border p-3 m-2"> | |||||
| <p> | |||||
| You can very simply create a form using Vue & bootstrap-vue. | |||||
| The form sends data back to Node-RED. | |||||
| Look at the <code>increment</code> method in <code>index.js</code> to see how easy this is. | |||||
| </p> | |||||
| <p> | |||||
| <b-form-input v-model="inputText" type="text" placeholder="Enter some text to send to Node-RED"></b-form-input><br> | |||||
| <b-form-checkbox v-model="inputChkBox"> | |||||
| To tick or not to tick? That is the question | |||||
| </b-form-checkbox><br> | |||||
| <b-button id="btn_increment" pill variant="primary" v-on:click="increment">Increment</b-button> | |||||
| Click Counter: <b>{{counterBtn}}</b>. | |||||
| <p>Click on the button to increment the counter. It sends the data dynamically back to Node-RED as well.</p> | |||||
| </p> | |||||
| </b-form> | |||||
| <h2>Dynamic Data</h2> | |||||
| <p>Uses Vue to dynamically update in response to messages from Node-RED.</p> | |||||
| <p> | |||||
| Check out the <code>mounted</code> function in <code>index.js</code> to See | |||||
| how easy it is to update Vue data from Node-RED. | |||||
| </p> | |||||
| <b-card class="mt-3" header="Status" border-variant="info" header-bg-variant="info" header-text-variant="white" align="center" > | |||||
| <p class="float-left">Socket.io Connection Status: <b>{{socketConnectedState}}</b></p> | |||||
| <p class="float-right">Time offset between browser and server: <b>{{serverTimeOffset}}</b> hours</p> | |||||
| </b-card> | |||||
| <b-card class="mt-3" header="Normal Messages" border-variant="primary" header-bg-variant="primary" header-text-variant="white" align="left" > | |||||
| <p> | |||||
| Messages: Received=<b>{{msgsReceived}}</b>, Sent=<b>{{msgsSent}}</b> | |||||
| </p> | |||||
| <pre v-html="hLastRcvd" class="syntax-highlight"></pre> | |||||
| <pre v-html="hLastSent" class="syntax-highlight"></pre> | |||||
| <p slot="footer" class="mb-0"> | |||||
| The received message is from the input to the uibuilder node. | |||||
| The send message will appear out of port #1 of the node. | |||||
| </p> | |||||
| </b-card> | |||||
| <b-card class="mt-3" header="Control Messages" border-variant="secondary" header-bg-variant="secondary" header-text-variant="white" align="left" > | |||||
| <p> | |||||
| Control Messages: Received=<b>{{msgsControl}}</b>, Sent=<b>{{msgsCtrlSent}}</b> | |||||
| </p> | |||||
| <pre v-html="hLastCtrlRcvd" class="syntax-highlight"></pre> | |||||
| <pre v-html="hLastCtrlSent" class="syntax-highlight"></pre> | |||||
| <p slot="footer" class="mb-0"> | |||||
| Control messages always appear out of port #2 of the uibuilder node | |||||
| whether they are from the server or the client. The <code>from</code> property | |||||
| of the message tells you where it came from. | |||||
| </p> | |||||
| </b-card> | |||||
| </b-container> | |||||
| </div> | |||||
| <!-- These MUST be in the right order. Note no leading / --> | |||||
| <!-- REQUIRED: Socket.IO is loaded only once for all instances | |||||
| Without this, you don't get a websocket connection --> | |||||
| <script src="../uibuilder/vendor/socket.io/socket.io.js"></script> | |||||
| <!-- --- Vendor Libraries - Load in the right order --- --> | |||||
| <script src="../uibuilder/vendor/vue/dist/vue.js"></script> <!-- dev version with component compiler --> | |||||
| <!-- <script src="../uibuilder/vendor/vue/dist/vue.min.js"></script> prod version with component compiler --> | |||||
| <!-- <script src="../uibuilder/vendor/vue/dist/vue.runtime.min.js"></script> prod version without component compiler --> | |||||
| <script src="../uibuilder/vendor/bootstrap-vue/dist/bootstrap-vue.js"></script> | |||||
| <!-- REQUIRED: Sets up Socket listeners and the msg object --> | |||||
| <!-- <script src="./uibuilderfe.js"></script> //dev version --> | |||||
| <script src="./uibuilderfe.min.js"></script> <!-- //prod version --> | |||||
| <!-- OPTIONAL: You probably want this. Put your custom code here --> | |||||
| <script src="./index.js"></script> | |||||
| </body> | |||||
| </html> | |||||
| @@ -0,0 +1,197 @@ | |||||
| /* jshint browser: true, esversion: 5, asi: true */ | |||||
| /*globals Vue, uibuilder */ | |||||
| // @ts-nocheck | |||||
| /* | |||||
| Copyright (c) 2019 Julian Knight (Totally Information) | |||||
| 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. | |||||
| */ | |||||
| 'use strict' | |||||
| /** @see https://github.com/TotallyInformation/node-red-contrib-uibuilder/wiki/Front-End-Library---available-properties-and-methods */ | |||||
| // eslint-disable-next-line no-unused-vars | |||||
| var app1 = new Vue({ | |||||
| el: '#app', | |||||
| data: { | |||||
| startMsg : 'Vue has started, waiting for messages', | |||||
| feVersion : '', | |||||
| counterBtn : 0, | |||||
| inputText : null, | |||||
| inputChkBox : false, | |||||
| socketConnectedState : false, | |||||
| serverTimeOffset : '[unknown]', | |||||
| imgProps : { width: 75, height: 75 }, | |||||
| msgRecvd : '[Nothing]', | |||||
| msgsReceived: 0, | |||||
| msgCtrl : '[Nothing]', | |||||
| msgsControl : 0, | |||||
| msgSent : '[Nothing]', | |||||
| msgsSent : 0, | |||||
| msgCtrlSent : '[Nothing]', | |||||
| msgsCtrlSent: 0, | |||||
| }, // --- End of data --- // | |||||
| computed: { | |||||
| hLastRcvd: function() { | |||||
| var msgRecvd = this.msgRecvd | |||||
| if (typeof msgRecvd === 'string') return 'Last Message Received = ' + msgRecvd | |||||
| else return 'Last Message Received = ' + this.syntaxHighlight(msgRecvd) | |||||
| }, | |||||
| hLastSent: function() { | |||||
| var msgSent = this.msgSent | |||||
| if (typeof msgSent === 'string') return 'Last Message Sent = ' + msgSent | |||||
| else return 'Last Message Sent = ' + this.syntaxHighlight(msgSent) | |||||
| }, | |||||
| hLastCtrlRcvd: function() { | |||||
| var msgCtrl = this.msgCtrl | |||||
| if (typeof msgCtrl === 'string') return 'Last Control Message Received = ' + msgCtrl | |||||
| else return 'Last Control Message Received = ' + this.syntaxHighlight(msgCtrl) | |||||
| }, | |||||
| hLastCtrlSent: function() { | |||||
| var msgCtrlSent = this.msgCtrlSent | |||||
| if (typeof msgCtrlSent === 'string') return 'Last Control Message Sent = ' + msgCtrlSent | |||||
| //else return 'Last Message Sent = ' + this.callMethod('syntaxHighlight', [msgCtrlSent]) | |||||
| else return 'Last Control Message Sent = ' + this.syntaxHighlight(msgCtrlSent) | |||||
| }, | |||||
| }, // --- End of computed --- // | |||||
| methods: { | |||||
| increment: function(event) { | |||||
| console.log('Button Pressed. Event DatA: ', event) | |||||
| // Increment the count by one | |||||
| this.counterBtn = this.counterBtn + 1 | |||||
| var topic = this.msgRecvd.topic || 'uibuilder/vue' | |||||
| uibuilder.send( { | |||||
| 'topic': topic, | |||||
| 'payload': { | |||||
| 'type': 'counterBtn', | |||||
| 'btnCount': this.counterBtn, | |||||
| 'message': this.inputText, | |||||
| 'inputChkBox': this.inputChkBox | |||||
| } | |||||
| } ) | |||||
| }, // --- End of increment --- // | |||||
| // return formatted HTML version of JSON object | |||||
| syntaxHighlight: function(json) { | |||||
| json = JSON.stringify(json, undefined, 4) | |||||
| json = json.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>') | |||||
| json = json.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, function (match) { | |||||
| var cls = 'number' | |||||
| if (/^"/.test(match)) { | |||||
| if (/:$/.test(match)) { | |||||
| cls = 'key' | |||||
| } else { | |||||
| cls = 'string' | |||||
| } | |||||
| } else if (/true|false/.test(match)) { | |||||
| cls = 'boolean' | |||||
| } else if (/null/.test(match)) { | |||||
| cls = 'null' | |||||
| } | |||||
| return '<span class="' + cls + '">' + match + '</span>' | |||||
| }) | |||||
| return json | |||||
| }, // --- End of syntaxHighlight --- // | |||||
| }, // --- End of methods --- // | |||||
| // Available hooks: init,mounted,updated,destroyed | |||||
| mounted: function(){ | |||||
| //console.debug('[indexjs:Vue.mounted] app mounted - setting up uibuilder watchers') | |||||
| /** **REQUIRED** Start uibuilder comms with Node-RED @since v2.0.0-dev3 | |||||
| * Pass the namespace and ioPath variables if hosting page is not in the instance root folder | |||||
| * The namespace is the "url" you put in uibuilder's configuration in the Editor. | |||||
| * e.g. If you get continual `uibuilderfe:ioSetup: SOCKET CONNECT ERROR` error messages. | |||||
| * e.g. uibuilder.start('uib', '/nr/uibuilder/vendor/socket.io') // change to use your paths/names | |||||
| */ | |||||
| uibuilder.start() | |||||
| var vueApp = this | |||||
| // Example of retrieving data from uibuilder | |||||
| vueApp.feVersion = uibuilder.get('version') | |||||
| /** You can use the following to help trace how messages flow back and forth. | |||||
| * You can then amend this processing to suite your requirements. | |||||
| */ | |||||
| //#region ---- Trace Received Messages ---- // | |||||
| // If msg changes - msg is updated when a standard msg is received from Node-RED over Socket.IO | |||||
| // newVal relates to the attribute being listened to. | |||||
| uibuilder.onChange('msg', function(newVal){ | |||||
| //console.info('[indexjs:uibuilder.onChange] msg received from Node-RED server:', newVal) | |||||
| vueApp.msgRecvd = newVal | |||||
| }) | |||||
| // As we receive new messages, we get an updated count as well | |||||
| uibuilder.onChange('msgsReceived', function(newVal){ | |||||
| //console.info('[indexjs:uibuilder.onChange] Updated count of received msgs:', newVal) | |||||
| vueApp.msgsReceived = newVal | |||||
| }) | |||||
| // If we receive a control message from Node-RED, we can get the new data here - we pass it to a Vue variable | |||||
| uibuilder.onChange('ctrlMsg', function(newVal){ | |||||
| //console.info('[indexjs:uibuilder.onChange:ctrlMsg] CONTROL msg received from Node-RED server:', newVal) | |||||
| vueApp.msgCtrl = newVal | |||||
| }) | |||||
| // Updated count of control messages received | |||||
| uibuilder.onChange('msgsCtrl', function(newVal){ | |||||
| //console.info('[indexjs:uibuilder.onChange:msgsCtrl] Updated count of received CONTROL msgs:', newVal) | |||||
| vueApp.msgsControl = newVal | |||||
| }) | |||||
| //#endregion ---- End of Trace Received Messages ---- // | |||||
| //#region ---- Trace Sent Messages ---- // | |||||
| // You probably only need these to help you understand the order of processing // | |||||
| // If a message is sent back to Node-RED, we can grab a copy here if we want to | |||||
| uibuilder.onChange('sentMsg', function(newVal){ | |||||
| //console.info('[indexjs:uibuilder.onChange:sentMsg] msg sent to Node-RED server:', newVal) | |||||
| vueApp.msgSent = newVal | |||||
| }) | |||||
| // Updated count of sent messages | |||||
| uibuilder.onChange('msgsSent', function(newVal){ | |||||
| //console.info('[indexjs:uibuilder.onChange:msgsSent] Updated count of msgs sent:', newVal) | |||||
| vueApp.msgsSent = newVal | |||||
| }) | |||||
| // If we send a control message to Node-RED, we can get a copy of it here | |||||
| uibuilder.onChange('sentCtrlMsg', function(newVal){ | |||||
| //console.info('[indexjs:uibuilder.onChange:sentCtrlMsg] Control message sent to Node-RED server:', newVal) | |||||
| vueApp.msgCtrlSent = newVal | |||||
| }) | |||||
| // And we can get an updated count | |||||
| uibuilder.onChange('msgsSentCtrl', function(newVal){ | |||||
| //console.info('[indexjs:uibuilder.onChange:msgsSentCtrl] Updated count of CONTROL msgs sent:', newVal) | |||||
| vueApp.msgsCtrlSent = newVal | |||||
| }) | |||||
| //#endregion ---- End of Trace Sent Messages ---- // | |||||
| // If Socket.IO connects/disconnects, we get true/false here | |||||
| uibuilder.onChange('ioConnected', function(newVal){ | |||||
| //console.info('[indexjs:uibuilder.onChange:ioConnected] Socket.IO Connection Status Changed to:', newVal) | |||||
| vueApp.socketConnectedState = newVal | |||||
| }) | |||||
| // If Server Time Offset changes | |||||
| uibuilder.onChange('serverTimeOffset', function(newVal){ | |||||
| //console.info('[indexjs:uibuilder.onChange:serverTimeOffset] Offset of time between the browser and the server has changed to:', newVal) | |||||
| vueApp.serverTimeOffset = newVal | |||||
| }) | |||||
| } // --- End of mounted hook --- // | |||||
| }) // --- End of app1 --- // | |||||
| // EOF | |||||
| @@ -0,0 +1,15 @@ | |||||
| { | |||||
| "name": "Node-RED UI Builder", | |||||
| "short_name": "uibuilder", | |||||
| "description": "A web template builder for Node-RED", | |||||
| "display": "standalone", | |||||
| "lang": "en-GB", | |||||
| "icons": [ | |||||
| { | |||||
| "src": "./images/node-blue-192x192.png", | |||||
| "sizes": "192x192", | |||||
| "type": "image/png" | |||||
| } | |||||
| ], | |||||
| "start_url": "." | |||||
| } | |||||
| @@ -0,0 +1,37 @@ | |||||
| CACHE MANIFEST | |||||
| # 2019-02-02 19:48:57 | |||||
| # WARNING: These files will never be delivered | |||||
| # again until this file changes. If you are updating, | |||||
| # make sure you change something in this file. | |||||
| # The appcache isn't always good: https://alistapart.com/article/application-cache-is-a-douchebag | |||||
| # Resources that will be delivered from the CACHE | |||||
| # after first load. | |||||
| CACHE: | |||||
| # Required files | |||||
| images/node-blue-192x192.png | |||||
| images/node-blue.ico | |||||
| /uibuilder/socket.io/socket.io.js | |||||
| # Replace this with the non .min version if needed | |||||
| uibuilderfe.min.js | |||||
| # Specific for our app | |||||
| index.css | |||||
| index.js | |||||
| # Local vendor libraries | |||||
| vendor/normalize.css/normalize.css | |||||
| # Useful utility libraries | |||||
| #https://cdn.jsdelivr.net/npm/lodash@4/lodash.min.js | |||||
| #https://unpkg.com/babel-polyfill@latest/dist/polyfill.min.js | |||||
| # Resources that require the user to be online. | |||||
| NETWORK: | |||||
| * | |||||
| # Optional section specifying fallback pages if a resource is inaccessible | |||||
| # 1st URI is resource, 2nd is fallback if request fails or errors | |||||
| # Both must from same origin as manifest. Can use prefixes as well as file uris | |||||
| #FALLBACK: | |||||
| # e.g. display offline.html if cache is empty and we are offline | |||||
| #/ /offline.html | |||||