├── .gitignore ├── LICENSE ├── README.md ├── docs ├── msg_control.md ├── tabsheet_CSS.md ├── tabsheet_SVG.md ├── tabsheet_animation.md ├── tabsheet_binding.md ├── tabsheet_editor.md ├── tabsheet_event.md ├── tabsheet_js.md └── tabsheet_settings.md ├── examples ├── chaining_animations.json ├── change_fill_color_style.json ├── click_event_handling.json ├── corona-game.json ├── font_awesome_icon_demo.json ├── hmi-demo.json ├── interactive_light_bulb.json ├── javascript_events.json ├── pan_zoom_demo.json ├── partial_filled_shape.json ├── replace_svg.json ├── trigger_animations_via_msg.json └── use_databinding.json ├── icons └── svg.png ├── lib ├── beautify-html.js └── jschannel.js ├── package.json ├── svg_graphics.html ├── svg_graphics.js └── svg_utils.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | | :warning: This svg node for the old AngularJs dashboard won't be maintained anymore! Due to lack of free time I can only work on the [new svg node](https://github.com/bartbutenaers/node-red-dashboard-2-ui-svg) for the new [VueJs dashboard](https://github.com/FlowFuse/node-red-dashboard/blob/main/README.md). Note that the new svg node is still in experimental phase! | 2 | |:---------------------------| 3 | 4 | 5 | # node-red-contrib-ui-svg 6 | A Node-RED widget node to show interactive SVG (vector graphics) in the dashboard 7 | 8 | Special thanks to [Stephen McLaughlin](https://github.com/Steve-Mcl), my partner in crime for this node! 9 | 10 | And also, lots of credits to Joseph Liard, the author of [DrawSvg](#DrawSvg-drawing-editor) for his assistance! 11 | 12 | | :warning: Please have a look at the "Getting started" [tutorial](https://github.com/bartbutenaers/node-red-contrib-ui-svg/wiki/Getting-started-with-the-UI-SVG-node) on the wiki | 13 | |:---------------------------| 14 | 15 | ## Install 16 | Run the following npm command in your Node-RED user directory (typically ~/.node-red): 17 | ``` 18 | npm install node-red-contrib-ui-svg 19 | ``` 20 | It is advised to use ***Dashboard version 2.16.3 or above***. 21 | 22 | ## Introduction to SVG 23 | Scalable Vector Graphics (SVG) is an XML-based vector image format for two-dimensional graphics with support for interactivity and animation. We won't explain here how it works, because the internet is full of information about it. 24 | 25 | An SVG drawing contains a series of SVG elements, which will be rendered by the browser from top to bottom. For example: 26 | ``` 27 | 28 | 29 | 30 | 31 | 32 | ``` 33 | The browser will first draw the (background) image, then the circle (on top of the image), and so on ... 34 | 35 | Each of those SVG elements has attributes (fill colour, ...), can respond to events (clicked, ...) and can be animated (e.g. shrink...). 36 | 37 | ## Node usage 38 | 39 | | :boom: HAVE A LOOK AT THE [WIKI](https://github.com/bartbutenaers/node-red-contrib-ui-svg/wiki) FOR STEP-BY-STEP TUTORIALS | 40 | |:---------------------------| 41 | 42 | This node can be used to visualize all kind of graphical stuff in the Node-RED dashboard. This can range from simple graphics (e.g. a round button, ...) to very complex graphics (floorplans, industrial processes, piping, wiring, ...). But even those complex graphics will consist out of several simple graphical shapes. For example, a ***floorplan*** is in fact a simple image of your floor, and a series of other SVG elements (e.g. Fontawesome icons) drawn on top of that (background) image. 43 | 44 | Simply deploy your SVG string in the config screen, and the Node-RED dashboard will render your vector graphics: 45 | 46 | ![svg_demo](https://user-images.githubusercontent.com/14224149/65639986-94e63680-dfe9-11e9-8086-89d78394301b.gif) 47 | 48 | But what if you are not familiar with the SVG syntax. Do not worry, we have integrated a [DrawSvg](#DrawSvg-drawing-editor) drawing editor in the config screen of our node. 49 | 50 | ## Config screen tabsheets 51 | 52 | The node's config screen consists of a series of tab sheets: 53 | 54 | + [Editor](https://github.com/bartbutenaers/node-red-contrib-ui-svg/tree/master/docs/tabsheet_editor.md) tab sheet 55 | + [SVG](https://github.com/bartbutenaers/node-red-contrib-ui-svg/tree/master/docs/tabsheet_SVG.md) tab sheet 56 | + [Animation](https://github.com/bartbutenaers/node-red-contrib-ui-svg/tree/master/docs/tabsheet_animation.md) tab sheet 57 | + [Event](https://github.com/bartbutenaers/node-red-contrib-ui-svg/tree/master/docs/tabsheet_event.md) tab sheet 58 | + [JS](https://github.com/bartbutenaers/node-red-contrib-ui-svg/tree/master/docs/tabsheet_js.md) tab sheet 59 | + [Binding](https://github.com/bartbutenaers/node-red-contrib-ui-svg/tree/master/docs/tabsheet_binding.md) tab sheet 60 | + [Settings](https://github.com/bartbutenaers/node-red-contrib-ui-svg/tree/master/docs/tabsheet_settings.md) tab sheet 61 | + [CSS](https://github.com/bartbutenaers/node-red-contrib-ui-svg/tree/master/docs/tabsheet_CSS.md) tab sheet 62 | 63 | ## Control via messages 64 | Most of the SVG information can be manipulated by sending input messages to this node. 65 | 66 | ### Some general msg guidelines: 67 | + In most messages, you need to specify on which SVG element(s) the control message needs to be applied. To specify a single element, the `elementId` field can be specified: 68 | ``` 69 | "payload": { 70 | "command": "update_text", 71 | "elementId": "some_element_id", 72 | "textContent": "my title" 73 | } 74 | ``` 75 | However it is also possible to specify one or more elements via a [CSS selector](https://www.w3schools.com/cssref/css_selectors.asp). This is a very powerful query mechanism that allows you to apply the control message to multiple SVG elements at once! For example, set all texts with class 'titleText' to value 'my title': 76 | ``` 77 | "payload": { 78 | "command": "update_text", 79 | "selector": ".titleText", //standard dom selector '#' for id, '.' for class etc. 80 | "textContent": "my title" 81 | } 82 | ``` 83 | This can be used to do the ***same update on multiple elements with a single message***. 84 | Note that a `selector` can also be used to specify a single element id (similar to `elementId`), by using a hashtag like *"#some_element_id"*. 85 | + A message can contain a single command. For example: 86 | ``` 87 | "payload": { 88 | "command": "update_attribute", 89 | "selector": "#cam_living_room", 90 | "attributeName": "fill", 91 | "attributeValue": "orange" 92 | } 93 | ``` 94 | But it is also possible to specify ***multiple commands (as an array)*** in a single control message. For example: 95 | ``` 96 | "payload": [ 97 | { 98 | "command": "update_attribute", 99 | "elementId": "cam_kitchen", /*use elementId or selector*/ 100 | "attributeName": "fill", 101 | "attributeValue": "orange" 102 | }, 103 | { 104 | "command": "set_attribute", 105 | "selector": "#cam_living", /*use elementId or selector*/ 106 | "attributeName": "fill", 107 | "attributeValue": "red" 108 | } 109 | ] 110 | ``` 111 | 112 | + When multiple identical commands are being used in a single message, the message might be simplified by specifying the command inside the ```msg.topic```: 113 | ``` 114 | "payload": [ 115 | { 116 | "elementId": "cam_kitchen", /*use elementId or selector*/ 117 | "attributeName": "fill", 118 | "attributeValue": "orange" 119 | }, 120 | { 121 | "selector": "#cam_living", /*use elementId or selector*/ 122 | "attributeName": "fill", 123 | "attributeValue": "red" 124 | }, 125 | ], 126 | "topic": "update_attribute" 127 | ``` 128 | This can be used to do ***multiple commands with a single message***. 129 | 130 | + To further simplify the message, the CSS selector - when it is required - can also be added to the topic (separated by `|`): 131 | ``` 132 | { 133 | "topic": "update_text|#myRect > .faultMessage", 134 | "payload": "hello" 135 | } 136 | ``` 137 | This way the message becomes yet shorter, but you can only use 1 selector or command value (even when the payload contains an array). 138 | 139 | ### Supported commands: 140 | 141 | + [Update/set an attribute value](https://github.com/bartbutenaers/node-red-contrib-ui-svg/tree/master/docs/msg_control.md#updateset-an-attribute-value-via-msg) via msg 142 | + [Update/set a style attribute value](https://github.com/bartbutenaers/node-red-contrib-ui-svg/tree/master/docs/msg_control.md#updateset-a-style-attribute-value-via-msg) via msg 143 | + [Remove an attribute](https://github.com/bartbutenaers/node-red-contrib-ui-svg/tree/master/docs/msg_control.md#remove-an-attribute-via-msg) via msg 144 | + [Replace an attribute value](https://github.com/bartbutenaers/node-red-contrib-ui-svg/blob/master/docs/msg_control.md#replace-an-attribute-value-via-msg) via msg 145 | + [Set text content](https://github.com/bartbutenaers/node-red-contrib-ui-svg/tree/master/docs/msg_control.md#set-text-content-via-msg) via msg 146 | + [Get text content](https://github.com/bartbutenaers/node-red-contrib-ui-svg/tree/master/docs/msg_control.md#get-text-content-via-msg) via msg 147 | + [Start/stop animations](https://github.com/bartbutenaers/node-red-contrib-ui-svg/tree/master/docs/msg_control.md#startstop-animations-via-msg) via msg 148 | + [Add events](https://github.com/bartbutenaers/node-red-contrib-ui-svg/tree/master/docs/msg_control.md#add-events-via-msg) via msg 149 | + [Remove events](https://github.com/bartbutenaers/node-red-contrib-ui-svg/tree/master/docs/msg_control.md#remove-events-via-msg) via msg 150 | + [Add Javascript events](https://github.com/bartbutenaers/node-red-contrib-ui-svg/tree/master/docs/msg_control.md#add-javascript-events-via-msg) via msg 151 | + [Remove Javascript events](https://github.com/bartbutenaers/node-red-contrib-ui-svg/tree/master/docs/msg_control.md#remove-javascript-events-via-msg) via msg 152 | + [Add elements](https://github.com/bartbutenaers/node-red-contrib-ui-svg/tree/master/docs/msg_control.md#add-elements-via-msg) via msg 153 | + [Remove elements](https://github.com/bartbutenaers/node-red-contrib-ui-svg/tree/master/docs/msg_control.md#remove-elements-via-msg) via msg 154 | + [Update (input) value](https://github.com/bartbutenaers/node-red-contrib-ui-svg/tree/master/docs/msg_control.md#update-input-value-via-msg) via msg 155 | + [Set entire SVG](https://github.com/bartbutenaers/node-red-contrib-ui-svg/tree/master/docs/msg_control.md#set-entire-svg-via-msg) via msg 156 | + [Get entire SVG](https://github.com/bartbutenaers/node-red-contrib-ui-svg/tree/master/docs/msg_control.md#get-entire-svg) via msg 157 | + [Zoom in/out](https://github.com/bartbutenaers/node-red-contrib-ui-svg/tree/master/docs/msg_control.md#zoom-inout-via-msg) via msg 158 | + [Panning](https://github.com/bartbutenaers/node-red-contrib-ui-svg/tree/master/docs/msg_control.md#panning-via-msg) via msg 159 | + [Reset pan/zoom](https://github.com/bartbutenaers/node-red-contrib-ui-svg/tree/master/docs/msg_control.md#reset-panzoom-via-msg) via msg 160 | 161 | ### Enable/disable the SVG 162 | The Node-RED dashboard allows to enable/disable ui widgets by injecting a message with `msg.enabled` set to a boolean true or false. This can also be used to enable or disable all user input in an SVG drawing. Note that all next injected messages will keep being processed while the node is disabled, like with all other UI nodes (i.e. standard behaviour). 163 | 164 | ![svg_enable](https://github.com/bartbutenaers/node-red-contrib-ui-svg/assets/14224149/f4485aa4-1877-4451-babb-570e95ce03cd) 165 | ``` 166 | [{"id":"8734fd29475b3ca3","type":"inject","z":"bf0833e74936a0c1","name":"Enable","props":[{"p":"enabled","v":"true","vt":"bool"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","_mcu":{"mcu":false},"x":750,"y":80,"wires":[["662f5a4f61d45302"]]},{"id":"0793ba691f3622e3","type":"debug","z":"bf0833e74936a0c1","name":"Events","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","_mcu":{"mcu":false},"x":1090,"y":80,"wires":[]},{"id":"7f880977992bf324","type":"inject","z":"bf0833e74936a0c1","name":"Disable","props":[{"p":"enabled","v":"false","vt":"bool"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","_mcu":{"mcu":false},"x":750,"y":140,"wires":[["662f5a4f61d45302"]]},{"id":"662f5a4f61d45302","type":"ui_svg_graphics","z":"bf0833e74936a0c1","group":"e8509dc7be30844e","order":1,"width":0,"height":0,"svgString":"\n Click me!\n","clickableShapes":[{"targetId":"#clickable_text","action":"click","payload":"Text clicked","payloadType":"str","topic":"clicked"}],"javascriptHandlers":[],"smilAnimations":[],"bindings":[],"showCoordinates":false,"autoFormatAfterEdit":false,"showBrowserErrors":false,"showBrowserEvents":false,"enableJsDebugging":false,"sendMsgWhenLoaded":false,"noClickWhenDblClick":false,"outputField":"payload","editorUrl":"//drawsvg.org/drawsvg.html","directory":"","panning":"disabled","zooming":"disabled","panOnlyWhenZoomed":false,"doubleClickZoomEnabled":false,"mouseWheelZoomEnabled":false,"dblClickZoomPercentage":150,"cssString":"div.ui-svg svg{\n color: var(--nr-dashboard-widgetColor);\n fill: currentColor !important;\n}\ndiv.ui-svg path {\n fill: inherit;\n}","name":"","_mcu":{"mcu":false},"x":920,"y":80,"wires":[["0793ba691f3622e3"]]},{"id":"e8509dc7be30844e","type":"ui_group","name":"Svg disable demo","tab":"d8520920.0128d8","order":4,"disp":true,"width":"12","collapse":false,"className":"","_mcu":{"mcu":false}},{"id":"d8520920.0128d8","type":"ui_tab","name":"Home","icon":"dashboard","order":3,"disabled":false,"hidden":false}] 167 | ``` 168 | 169 | ## Various stuff 170 | 171 | ### Fontawesome icons 172 | Fontawesome icons are used widely in Node-RED and are in fact little SVG drawings on their own. They are a very easy way e.g. to represent devices on a floorplan. Such an icon can easily be added via DrawSvg, as demonstrated in this animation: 173 | 174 | ![icons via drawsvg](https://user-images.githubusercontent.com/14224149/66722326-17edf600-ee0c-11e9-94b9-225edcc12250.gif) 175 | 176 | By specifying an identifier for the icon (like in the above animation), the icon can be updated afterwards via input messages (like any other SVG element). 177 | 178 | When you want to enter your SVG source ***manually*** (without using DrawSvg), there is another mechanism provided: 179 | 180 | 1. Search the [Fontawesome](https://fontawesome.com/v4.7.0/icons/) website for an icon that fits your needs. For example, 'fa-video-camera'. 181 | 182 | 2. Create a text element (with font family *"FontAwesome"*) containing that icon name: 183 | ``` 184 | fa-video-camera 185 | ``` 186 | 187 | 3. The result will be the FontAwesome icon at the specified location: 188 | 189 | ![icon](https://user-images.githubusercontent.com/14224149/63217104-29828c80-c140-11e9-957b-22ea8eb9a0ed.png) 190 | 191 | Some remarks: 192 | + The node will automatically lookup the ***unicode*** value for that icon, based on this [list](https://fontawesome.com/v4.7.0/cheatsheet/): 193 | 194 | ![unicode](https://user-images.githubusercontent.com/14224149/63217056-9e08fb80-c13f-11e9-8b48-0ec516752d90.png) 195 | 196 | As a result, in the generated dashboard html you will see only the unicode value (instead of the original fa-video-camera value): 197 | ``` 198 | 199 | ``` 200 | + Currently [DrawSvg](#DrawSvg-drawing-editor) doesn't support the FontAwesome font. See this [issue](https://github.com/bartbutenaers/node-red-contrib-ui-svg/issues/34). 201 | ```diff 202 | ! This means in the current DrawSvg version you will see "fa-xxx" instead of the FontAwesome icon: 203 | 204 | ``` 205 | 206 | ![DrawSvg FA](https://user-images.githubusercontent.com/14224149/65816859-4317f900-e201-11e9-83e8-0d46d06198ef.png) 207 | 208 | + Since FontAwesome icons are displayed in `````` SVG elements, it is very easy to change the icon using a ***update_text*** (see 'Control messages' section above): 209 | 210 | ![svg_dynamic_icon](https://user-images.githubusercontent.com/14224149/65432498-95c96d80-de1b-11e9-86c1-8ee7aa147d15.gif) 211 | 212 | ``` 213 | [{"id":"f369eb92.6c5558","type":"ui_svg_graphics","z":"553defb0.b99fb","group":"9ec8b304.368cc","order":0,"width":"15","height":"15","svgString":"\n\n\n fa-thermometer-empty\n","clickableShapes":[{"targetId":"#camera_living","action":"click","payload":"#camera_living","payloadType":"str","topic":"#camera_living"},{"targetId":"#camera_balcony","action":"click","payload":"#camera_balcony","payloadType":"str","topic":"#camera_balcony"},{"targetId":"#camera_entry","action":"click","payload":"#camera_entry","payloadType":"str","topic":"#camera_entry"}],"smilAnimations":[],"bindings":[{"selector":"#camera_living","bindSource":"payload.attributeValue","bindType":"attr","attribute":"fill"},{"selector":"#camera_entry","bindSource":"payload.attribueValue","bindType":"attr","attribute":"fill"},{"selector":"#camera_balcony","bindSource":"payload.attributeValue","bindType":"attr","attribute":"fill"}],"showCoordinates":true,"autoFormatAfterEdit":false,"outputField":"anotherField","editorUrl":"","directory":"","name":"Home Floor Plan","x":1130,"y":520,"wires":[[]]},{"id":"866e2e46.ba033","type":"inject","z":"553defb0.b99fb","name":"fa-thermometer-three-quarters","topic":"","payload":"{\"command\":\"update_text\",\"selector\":\"#my_text\",\"textContent\":\"fa-thermometer-three-quarters\"}","payloadType":"json","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":820,"y":520,"wires":[["f369eb92.6c5558"]]},{"id":"68c4730b.af00bc","type":"inject","z":"553defb0.b99fb","name":"fa-thermometer-full ","topic":"","payload":"{\"command\":\"update_text\",\"selector\":\"#my_text\",\"textContent\":\"fa-thermometer-full\"}","payloadType":"json","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":790,"y":560,"wires":[["f369eb92.6c5558"]]},{"id":"46183ab5.42fd54","type":"inject","z":"553defb0.b99fb","name":"fa-thermometer-empty","topic":"","payload":"{\"command\":\"update_text\",\"selector\":\"#my_text\",\"textContent\":\"fa-thermometer-empty\"}","payloadType":"json","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":800,"y":400,"wires":[["f369eb92.6c5558"]]},{"id":"501c7f9a.08ac4","type":"inject","z":"553defb0.b99fb","name":"fa-thermometer-half ","topic":"","payload":"{\"command\":\"update_text\",\"selector\":\"#my_text\",\"textContent\":\"fa-thermometer-half\"}","payloadType":"json","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":790,"y":480,"wires":[["f369eb92.6c5558"]]},{"id":"d3ea2538.fa9458","type":"inject","z":"553defb0.b99fb","name":"fa-thermometer-quarter","topic":"","payload":"{\"command\":\"update_text\",\"selector\":\"#my_text\",\"textContent\":\"fa-thermometer-quarter\"}","payloadType":"json","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":800,"y":440,"wires":[["f369eb92.6c5558"]]},{"id":"9ec8b304.368cc","type":"ui_group","z":"","name":"Home Floor Plan","tab":"bb4f2a94.83b338","disp":true,"width":"15","collapse":false},{"id":"bb4f2a94.83b338","type":"ui_tab","z":"","name":"Home Floor Plan","icon":"dashboard","disabled":false,"hidden":false}] 214 | ``` 215 | 216 | ### Display images 217 | In an SVG drawing, an *"image"* element can be used to display an image inside an SVG drawing. See this [tutorial](https://github.com/bartbutenaers/node-red-contrib-ui-svg/wiki/Add-an-image-to-an-SVG-drawing) on the wiki for more information! 218 | 219 | ## Troubleshooting 220 | Some tips and tricks to solve known problems: 221 | 222 | 1. When SVG ***path*** elements get the same colour as the dashboard theme, like in this example where the shapes become blue: 223 | 224 | ![Dashboard color](https://user-images.githubusercontent.com/14224149/79540632-c2c7c100-8088-11ea-9ba7-6c5dd4f0a842.png) 225 | 226 | You can avoid this by applying the fill colour as a ```style``` attribute (e.g. ) to the path, instead of as a normal attribute (e.g. ). And the normal `fill` attribute on an SVG path will get overwritten by the dashboard theme colour... 227 | 228 | Remark: drawings created with DrawSvg are already correct, but some third-party editors use the ```fill``` attribute. 229 | 230 | 2. Some basic input messages validation has been added on the server-side, and validation errors will be showed in the debug side-panel. 231 | 232 | 3. See the [DrawSvg](#show-browser-errors-on-the-server) how to show client-side errors in your Node-RED debug panel. 233 | 234 | Remark: when N drawings are visible now (e.g. running in N dashboards simultaneously), then N duplicate messages will be displayed (where N can be 0 is no dashboards are open...). 235 | 236 | 4. If you have doubts that this node is generating the requested SVG DOM structure, you might have a look at it. Here is briefly explained how to do it using Chrome: 237 | 1. Open the developer tools of your browser, starting from your dashboard window. 238 | 2. Right click on your SVG drawing in the dashboard, and select "Inspect". 239 | 3. Now you should be able to see the generated SVG DOM tree. 240 | 241 | 5. When the *"show browser errors on the server"* has been activated, the error messages will appear in the left Debug sidebar. However if lots of messages are being injected, it is difficult to determine which error belongs to which message. To assist with that, the error message will contain the message id (which caused that error). So simply put a debug node (to display the input messages), and compare the ***message id*** to find the related message: 242 | 243 | ![message id](https://user-images.githubusercontent.com/14224149/145280978-89c98cc8-816b-472f-8651-7c2df456ccb1.png) 244 | -------------------------------------------------------------------------------- /docs/tabsheet_CSS.md: -------------------------------------------------------------------------------- 1 | # "CSS" tab sheet 2 | 3 | ![editor](https://user-images.githubusercontent.com/14224149/137393521-6aa58d3f-0e79-466c-8d02-654c95664f53.png) 4 | 5 | Enter you CSS (Cascading Style Sheet) in this editor. Note that this CSS is ***scoped***, which means it will only be applied to the SVG shapes used in this node! 6 | 7 | The default CSS (which is already available at startup) will be sufficient for most users, and should only be removed in very specific use cases. Of course it is always possible to keep the original default CSS and append your own custom CSS at the end (see example below). 8 | 9 | At the bottom of the "CSS" tab sheet, a series of buttons are available: 10 | 11 | ![buttons](https://user-images.githubusercontent.com/14224149/137394458-98be045e-c462-424f-8139-75c9a5fd0bea.png) 12 | 13 | + *Expand CSS*: show the CSS in full screen mode. 14 | + *Format CSS*: by formatting the CSS, it will be beatified. This means the indents will be corrected, ... 15 | + *Reset CSS*: remove the current CSS, and replace it by the original default CSS. 16 | 17 | ## Example of adding custom CSS 18 | 19 | In this example flow we have two SVG nodes, and each one will display a circle. But we add custom CSS to each node, to style each circle with its own color: 20 | 21 | ![CSS differences](https://user-images.githubusercontent.com/14224149/137398885-8ceb1219-1e41-4602-955b-37e06d65e619.png) 22 | 23 | Since the scope of the CSS is limited to the SVG used in the same node, each circle will get its own color: 24 | 25 | ![Dashboard result](https://user-images.githubusercontent.com/14224149/137399213-59400b25-6d8c-4048-9e46-81b34c8172ee.png) 26 | 27 | The example flow can be found here: 28 | ``` 29 | [{"id":"e1855e93be9d39d7","type":"ui_svg_graphics","z":"7f9646080c92c297","group":"a80872b69dcf44c9","order":11,"width":0,"height":0,"svgString":"\n \n","clickableShapes":[],"javascriptHandlers":[],"smilAnimations":[],"bindings":[],"showCoordinates":false,"autoFormatAfterEdit":false,"showBrowserErrors":false,"showBrowserEvents":false,"enableJsDebugging":false,"sendMsgWhenLoaded":false,"noClickWhenDblClick":false,"outputField":"payload","editorUrl":"//drawsvg.org/drawsvg.html","directory":"","panning":"disabled","zooming":"disabled","panOnlyWhenZoomed":false,"doubleClickZoomEnabled":false,"mouseWheelZoomEnabled":false,"dblClickZoomPercentage":150,"cssString":"div.ui-svg svg{\n color: var(--nr-dashboard-widgetColor);\n fill: currentColor !important;\n}\ndiv.ui-svg path {\n fill: inherit !important;\n}\ncircle {\n fill: red;\n}\n\n","name":"","x":1140,"y":240,"wires":[[]]},{"id":"d3ee148a86dd7dfd","type":"ui_svg_graphics","z":"7f9646080c92c297","group":"a80872b69dcf44c9","order":11,"width":0,"height":0,"svgString":"\n \n","clickableShapes":[],"javascriptHandlers":[],"smilAnimations":[],"bindings":[],"showCoordinates":false,"autoFormatAfterEdit":false,"showBrowserErrors":false,"showBrowserEvents":false,"enableJsDebugging":false,"sendMsgWhenLoaded":false,"noClickWhenDblClick":false,"outputField":"payload","editorUrl":"//drawsvg.org/drawsvg.html","directory":"","panning":"disabled","zooming":"disabled","panOnlyWhenZoomed":false,"doubleClickZoomEnabled":false,"mouseWheelZoomEnabled":false,"dblClickZoomPercentage":150,"cssString":"div.ui-svg svg{\n color: var(--nr-dashboard-widgetColor);\n fill: currentColor !important;\n}\ndiv.ui-svg path {\n fill: inherit !important;\n}\ncircle {\n fill: blue;\n}\n\n","name":"","x":1140,"y":280,"wires":[[]]},{"id":"a80872b69dcf44c9","type":"ui_group","name":"Scoped style demo","tab":"6f1c95444a84c32c","order":1,"disp":true,"width":"6","collapse":false},{"id":"6f1c95444a84c32c","type":"ui_tab","name":"SVG styles","icon":"dashboard","disabled":false,"hidden":false}] 30 | ``` 31 | -------------------------------------------------------------------------------- /docs/tabsheet_SVG.md: -------------------------------------------------------------------------------- 1 | # "SVG" tab sheet 2 | 3 | ![editor](https://user-images.githubusercontent.com/14224149/65357446-5faba400-dbf7-11e9-9824-886238dba228.png) 4 | 5 | Enter you (XML-based) SVG graphics in this editor. This can be done in different ways: 6 | + If you are a die-hard SVG fanatic, you can enter the SVG string manually in the *"SVG"* tab sheet. 7 | + If you prefer to use an SVG drawing editor, you can use the embedded [DrawSvg](#DrawSvg-drawing-editor) editor. 8 | + If you need very specific types of drawings, you can use a third party SVG editor to create your drawing (and simple paste the generated SVG string into this tab sheet). Multiple (online) editors are free available, each with their own dedicated speciality: 9 | + [Floorplanner](http://floorplanner.com) 10 | + [Floorplancreator](https://floorplancreator.net/#pricing) 11 | + ... 12 | 13 | However: 14 | + Be aware that those third-party SVG editors could create rather complex SVG strings, which are harder to understand when you want to change them manually afterwards. 15 | + Be aware that the browser has a lot of work to render all the SVG elements in the drawing! In some cases, it might be useful - to gain performance - to convert your SVG once to an image and use that as a background image in this SVG node (and draw other shapes on top of that image). For example, in Floorplanner website, the SVG drawing can be saved as a JPEG/PNG image. That image can be loaded into an SVG *'image'* element, like I have done in the example flows on this readme page ... 16 | 17 | At the bottom of the "SVG source" tab sheet, a series of buttons are available: 18 | 19 | ![buttons](https://user-images.githubusercontent.com/14224149/66707892-5621e180-ed48-11e9-8d66-e3add751e7c8.png) 20 | 21 | + *Expand source*: show the SVG source in full screen mode. 22 | + *Format SVG*: by formatting the SVG source, the source will be beatified. This means the empty lines will be removed, each line will get a single SVG element, indents will be corrected ... 23 | -------------------------------------------------------------------------------- /docs/tabsheet_animation.md: -------------------------------------------------------------------------------- 1 | # "Animation" tab sheet 2 | 3 | ![animation](https://user-images.githubusercontent.com/14224149/65359120-d2b71980-dbfb-11e9-83ea-5bbc6e155673.png) 4 | 5 | SVG allows users to animate element attributes over time. For example, you can make the radius of a circle grow in 3 seconds from 10 pixels to 40 pixels. 6 | 7 | Adding animations to your SVG graphics can be done in different ways: 8 | + *Via the "SVG" tab sheet* manually, for die-hard SVG fanatics: 9 | ``` 10 | 11 | 12 | 13 | ``` 14 | The animation will be applied by the browser to the parent element (in this example the circle). 15 | However it is also possible to add an animation element on its own (i.e. not as child element), with a link to the SVG element it needs to be applied to: 16 | ``` 17 | 18 | 19 | ``` 20 | 21 | + *Via the "Animation" tab sheet*, to keep the drawing and the animations separated. Click the *'add'* button to create a new animation record, where following properties need to be entered: 22 | + ***Animation id***: The id of this SVG animate element (in this example *"myanimation"*). 23 | + ***Target element id***: The id of the SVG element that you want to animate (in this example *"mycircle"*). 24 | + ***Class***: By setting a value in class, you can use a selector to start or stop multiple animations. 25 | + ***Attribute name***: The name of the element's attribute that you want to animate (in this example *"r"*). 26 | + ***From***: The attribute value at the start of the animation (in this example *"10"*). 27 | + ***To***: The attribute value at the end of the animation (in this example *"40"*). 28 | + ***Duration***: How long the animation will take. 29 | + ***Repeat count***: How many times the animation needs to be repeated (in this example *"1" which means only once). Caution: when *"0"* is selected, this means that the animation will be repeated ***"indefinite"***! 30 | + ***Animation end***: What to do with the new value when the animation is ended. 31 | + *Freeze new value*: the attribute value will keep the new *'To'* value (in this example *"40"*). 32 | + *Restore original value*: the attribute value will be restored to its original value (in this example *"5"*), from the start of the animation. 33 | + ***Trigger***: Which trigger will result in the animation being started. 34 | + Input message: the animation will be started by injecting an input message (see below). 35 | + Time delay: the animation will be started after a specified time. 36 | + Custom: the animation will be started using standard [begin](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/begin) options. For example: 37 | ``` 38 | 2s; myRect.click; myAnim.end-400ms 39 | ``` 40 | 41 | Creating animations via this tab sheet has the advantage that the SVG source and the animations are being kept separate. More specifically when the SVG is being created in a third-party SVG editor (which most of the time don't support animations), your manually inserted animation elements would be overwritten each time you need to update your SVG... 42 | 43 | Remark: it is also possible to animate transformations. Indeed, when the attribute name is *"transform"* an extra "animation type" dropdown will appear: 44 | 45 | ![demo_config_screen](https://user-images.githubusercontent.com/14224149/73695310-23766d00-46da-11ea-9960-065dc1bf7004.gif) 46 | 47 | + *Via an input message* as explained in the [Control via messages](https://github.com/bartbutenaers/node-red-contrib-ui-svg/tree/master/docs/msg_control.md#startstop-animations-via-msg) section below. 48 | -------------------------------------------------------------------------------- /docs/tabsheet_binding.md: -------------------------------------------------------------------------------- 1 | # "Binding" tab sheet 2 | As explained in the section [Control via messages](#control-via-messages) below, this node can be controlled via input messages. For example, to change the fill colour of circle with id "mycircle" to green. As a result, the input messages need to contain a lot of information (element id, attribute name, attribute value ...), to let this node know what you want it to do. This means the flow will become quite complex, since a lot of extra nodes are required to put all that information in the message. 3 | 4 | Another way to control this node is by using bindings. This means that you must specify most of the information in the binding, so the input message will only need to contain the new value itself. Since the input messages need to contain less information, the flow can be simplified ... 5 | 6 | ![bindings](https://user-images.githubusercontent.com/14224149/65362302-2bd87a80-dc07-11e9-9409-76fe1a205abc.png) 7 | 8 | Input bindings can be added to link sources (= input message fields) to destinations (= element attribute/text values). 9 | 10 | Several properties need to be entered: 11 | + ***Binding source***: the field of the input message that will contain the new value. 12 | + ***Selector***: the selection of (one or more) SVG elements on which the new attribute value will be applied. See the syntax of [CSS selectors](https://www.w3schools.com/cssref/css_selectors.asp). 13 | + ***Binding destination***: on which attribute of those selected SVG elements the new values will be applied. 14 | + *Text content*: when this option is selected, the value (from the input message) will be applied to the inner text content of the element. 15 | 16 | For example, set the text content in `Some text content`. 17 | + *Attribute value*: when this option is selected, the value (from the input message) will be applied to an attribute. This means an extra "attribute name" will have to be specified, to make sure the new value will be applied to the attribute with that name. 18 | 19 | For example, set the fill colour value in ``. 20 | + *Style value*: when this option is selected, the value (from the input message) will be applied to a style attribute. This means an extra style "attribute name" will have to be specified, to make sure the new value will be applied to the attribute with that name. 21 | 22 | For example, set the fill colour value in ``. 23 | 24 | For example: 25 | 26 | ![Binding example](https://user-images.githubusercontent.com/14224149/86181786-f6b26e80-bb2e-11ea-8440-3f335b07a3da.png) 27 | 28 | When e.g. the input message contains ```msg.payload.position.x```, then that value (250) will be set to the "x" attribute of SVG element with id "camera_living". 29 | 30 | The following flow shows the above binding example in action: 31 | 32 | ![Binding demo](https://user-images.githubusercontent.com/44235289/65389024-7b26c400-dd49-11e9-9792-94c6216e53ef.gif) 33 | ``` 34 | [{"id":"c9ab8554.337588","type":"debug","z":"60ad596.8120ba8","name":"Floorplan output","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","x":1380,"y":440,"wires":[]},{"id":"56869c57.d65c74","type":"ui_svg_graphics","z":"60ad596.8120ba8","group":"d4ee73ea.a7676","order":1,"width":"14","height":"10","svgString":"\n \n This is the #banner\n \n \n ","clickableShapes":[{"targetId":"#camera_living","action":"click","payload":"camera_living","payloadType":"str","topic":"camera_living"}],"smilAnimations":[],"bindings":[{"selector":"#banner","bindSource":"payload.title","bindType":"text","attribute":""},{"selector":"#camera_living","bindSource":"payload.position.x","bindType":"attr","attribute":"x"},{"selector":"#camera_living","bindSource":"payload.camera.colour","bindType":"attr","attribute":"fill"}],"showCoordinates":false,"autoFormatAfterEdit":false,"outputField":"","editorUrl":"http://drawsvg.org/drawsvg.html","directory":"","name":"","x":1180,"y":440,"wires":[["c9ab8554.337588"]]},{"id":"62a285fb.bd046c","type":"inject","z":"60ad596.8120ba8","name":"databind","topic":"databind","payload":"{\"camera\":{\"colour\":\"yellow\"},\"position\":{\"x\":320},\"title\":\"databind strikes again\"}","payloadType":"json","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":980,"y":460,"wires":[["56869c57.d65c74"]]},{"id":"132d184e.ff0ab8","type":"inject","z":"60ad596.8120ba8","name":"databind","topic":"databind","payload":"{\"camera\":{\"colour\":\"green\"},\"position\":{\"x\":250},\"title\":\"New banner title by databind\"}","payloadType":"json","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":980,"y":420,"wires":[["56869c57.d65c74"]]},{"id":"d4ee73ea.a7676","type":"ui_group","z":"","name":"Floorplan test","tab":"b4bb5633.ba92b8","disp":true,"width":"14","collapse":false},{"id":"b4bb5633.ba92b8","type":"ui_tab","z":"","name":"SVG","icon":"dashboard","disabled":false,"hidden":false}] 35 | ``` 36 | 37 | Note that there is another way to implement data binding (without using the *"Binding"* tabsheet), by using user attributes inside the SVG source: 38 | 39 | + Use a `data-bind-text` to set the text content of an element For example: 40 | ``` 41 | 42 | Temporary text content 43 | 44 | ``` 45 | Then the text content can be updated by injecting the following message: 46 | ``` 47 | "payload": { 48 | "SystemStateDesc": "Updated text content"}, 49 | "topic": "databind" 50 | } 51 | ``` 52 | + Use `data-bind-attributes` to specify which SVG element attributes need to be binded to which message fields (specified in `data-bind-values`). For example: 53 | ``` 54 | 55 | ; 56 | 57 | ``` 58 | Then the circle fill color and radius attributes can be updated by injecting following message: 59 | ``` 60 | "payload": { 61 | "fill": "circleColour", 62 | "size": 25 63 | }, 64 | "topic":"databind" 65 | ``` 66 | -------------------------------------------------------------------------------- /docs/tabsheet_editor.md: -------------------------------------------------------------------------------- 1 | # "Editor" tab sheet 2 | 3 | [DrawSvg](http://drawsvg.org/) is a free SVG drawing editor that will run entirely in your browser, so no installation required. We have integrated DrawSvg into this node, to allow users to edit their SVG source via a nice drawing program. 4 | 5 | ***!!! DrawSvg is free software. Note that DrawSvg and the online service is used as is without warranty of bugs !!!*** 6 | 7 | ## Update: DrawSvg 9.5 is only partly free 8 | Some users reported that starting from DrawSvg version 9.5, there are now two separate profiles. See the [readme](https://github.com/bartbutenaers/node-red-contrib-drawsvg#update-drawsvg-95-is-only-partly-free) page of the node-red-contrib-drawsvg node for more information about this. 9 | 10 | ## Getting started 11 | 12 | ![launch_editor](https://user-images.githubusercontent.com/44235289/66716981-f40ac000-edcb-11e9-96b5-69e11220b71d.gif) 13 | 14 | Steps to use DrawSvg: 15 | 1. Click the *"Open SVG editor"*, to show the SVG in the [DrawSvg](#DrawSvg-drawing-editor) drawing editor. 16 | 2. DrawSvg will be opened in a popup dialog window, and it will visualize the SVG source (from this node). 17 | 3. The SVG drawing can be edited. 18 | 4. You can intermediately save your changes (to this node), using the *"Save"* button in the upper right corner of the popup dialog window. 19 | 5. As soon as the popup dialog window is being closed, a notification will appear. There you can choose to ignore all changes (i.e. you do not need them anymore), or to save all the changes (to this node). 20 | 6. The updated SVG source will appear in the *"SVG source"* tab sheet of this node. 21 | 22 | By default, this node will use the free online DrawSvg service (see *"Editor URL"* in the "Settings" tab sheet). However we it is also possible to use the [node-red-contrib-drawsvg](https://github.com/bartbutenaers/node-red-contrib-drawsvg) node, which can host a DrawSvg service locally for offline systems. 23 | -------------------------------------------------------------------------------- /docs/tabsheet_event.md: -------------------------------------------------------------------------------- 1 | # "Event" tab sheet 2 | 3 | ![event tabsheet](https://user-images.githubusercontent.com/14224149/65360241-70f8ae80-dbff-11e9-8c6a-65f3a14e22a7.png) 4 | 5 | An SVG element can be added here, to make that element able to intercept one of the following events: 6 | + *Click*: when a mouse-down and mouse-up on the same element. 7 | + *Double click*: when a double mouse click on an element. Note that there is a [setting](https://github.com/bartbutenaers/node-red-contrib-ui-svg/blob/master/docs/tabsheet_settings.md#send-output-msg-when-the-client-is-reloaded) to configure this event. 8 | + *Change*: when the value of a (foreign) input element is changed. 9 | + *Context menu*: when a right mouse click on an element. 10 | + *Mouse down*: when a mouse button is pressed down on an element. 11 | + *Mouse up*: when a mouse button is released on an element. 12 | + *Mouse over*: when the mouse is moved onto an element. 13 | + *Mouse out*: when the mouse is moved away from an element. 14 | + *Focus*: when an element receives focus. 15 | + *Focus in*: when an element is about to receive focus. 16 | + *Focus out*: when an element is about to lose focus. 17 | + *Blur*: when an element loses focus. 18 | + *Key down*: when a key is pressed down. 19 | + *Key up*: when a key is released. 20 | + *Touch start*: when a touch event starts (on mobile/tablet only). 21 | + *Touch end*: when a touch event ends (on mobile/tablet only). 22 | 23 | When adding a new line in this tab sheet, several properties need to be entered: 24 | + ***Selector***: the selection of (one or more) SVG elements that needs to intercept events. See the syntax of [CSS selectors](https://www.w3schools.com/cssref/css_selectors.asp). 25 | + ***Action***: the event that the shape needs to intercept. 26 | + ***Payload***: the ```msg.payload``` content of the output message, which will be sent when the event occurs. 27 | + ***Topic***: the ```msg.topic``` content of the output message, which will be sent when the event occurs. 28 | 29 | By default the content will be stored in ```msg.payload``` of the output message. However when the result needs to end up in ```msg.anotherField```, this message field can be specified at the top of the tab sheet: 30 | 31 | ![image](https://user-images.githubusercontent.com/14224149/65385332-dd71cb80-dd2d-11e9-8ae9-7b604d3f077e.png) 32 | 33 | Two things will happen when an event occurs on such an SVG element: 34 | 1. The mouse ***cursor*** will change when hoovering above the element, to visualize that an element responds to events. 35 | 1. An ***output message*** will be send as soon as the element is clicked, with a Node-RED [standard](https://discourse.nodered.org/t/contextmenu-location/22780/71?u=bartbutenaers) format: 36 | ``` 37 | "elementId": "light_bulb_kitchen", 38 | "selector": "#light_bulb_kitchen", 39 | "event": { 40 | "type":"click", 41 | "svgX":28.02083396911621, 42 | "svgY":78.66666412353516, 43 | "pageX":1105, 44 | "pageY":310, 45 | "screenX":829, 46 | "screenY":304, 47 | "clientX":1105, 48 | "clientY":310, 49 | "bbox": [ 50 | 1076.979248046875, 51 | 311.3333435058594, 52 | 1136.979248046875, 53 | 251.33334350585938 54 | ] 55 | } 56 | ``` 57 | The coordinates (where the event occurs) in the output message allow the next nodes in the flow to display information at that location. For example we have developed the [node-red-contrib-ui-contextmenu](https://github.com/bartbutenaers/node-red-contrib-ui-contextmenu) to show a popup context menu in the dashboard above the SVG drawing, at the location where a shape has been clicked. The following demo explains this combination of both nodes: 58 | 59 | ![2019-09-22_14-12-06](https://user-images.githubusercontent.com/44235289/65387884-149ea780-dd43-11e9-9cd4-a6bb4fb59d65.gif) 60 | ``` 61 | [{"id":"107fa0c1.cb755f","type":"debug","z":"60ad596.8120ba8","name":"Floorplan output","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","x":1340,"y":440,"wires":[]},{"id":"58329d91.3fc564","type":"ui_svg_graphics","z":"60ad596.8120ba8","group":"f014eb03.a3c618","order":1,"width":"14","height":"10","svgString":"\n \n \n \n","clickableShapes":[{"targetId":"#camera_living","action":"click","payload":"camera_living","payloadType":"str","topic":"camera_living"}],"smilAnimations":[],"bindings":[],"showCoordinates":false,"autoFormatAfterEdit":false,"outputField":"","editorUrl":"http://drawsvg.org/drawsvg.html","directory":"","name":"","x":1140,"y":440,"wires":[["107fa0c1.cb755f"]]},{"id":"f014eb03.a3c618","type":"ui_group","z":"","name":"Floorplan test","tab":"80068970.6e2868","disp":true,"width":"14","collapse":false},{"id":"80068970.6e2868","type":"ui_tab","z":"","name":"SVG","icon":"dashboard","disabled":false,"hidden":false}] 62 | ``` 63 | The `msg.event` object contains multiple coordinates, corresponding to different available coordinate systems in a browser: 64 | 65 | ![Coordinate systems](https://user-images.githubusercontent.com/14224149/85235300-3fbe4080-b414-11ea-931d-acceb28a7789.png) 66 | 67 | + *SVG* coordinates to the borders of the SVG editor, i.e. relative to the origin of the SVG drawing. 68 | + *Client* coordinqtes to the borders of the Browser's visible window. 69 | + *Page* coordinates to the top of the current of the dashboard page, (which will only become visible after scrolling, since it is too short to show in the browser window). 70 | + *Screen* coordinates to the border of your monitor screen. 71 | 72 | Remark: the `msg.bbox` contains the bounding box (left / bottom / right / top) of the SVG element where the event occurs 73 | 74 | Instead of specifying events in the config screen, it is also possible to add or remove events via input messages. This is explained in the [Control via messages](#control-via-messages) section below. 75 | -------------------------------------------------------------------------------- /docs/tabsheet_js.md: -------------------------------------------------------------------------------- 1 | # "JS" tab sheet 2 | 3 | ![JS tabsheet](https://user-images.githubusercontent.com/14224149/97640631-7864cb00-1a41-11eb-94b8-742a526978fa.gif) 4 | 5 | An SVG element can be added here, to make that element able to intercept one of the events: see "Event" tabsheet section above. 6 | 7 | When adding a new line in this tab sheet, several properties need to be entered: 8 | + ***Selector***: the selection of (one or more) SVG elements that needs to intercept events. See the syntax of [CSS selectors](https://www.w3schools.com/cssref/css_selectors.asp). 9 | + ***Action***: the event that the shape needs to intercept. 10 | + ***Javascript code***: the Javascript code that needs to be executed on the client side (i.e. inside the dashboard), when the event occurs. 11 | 12 | Note that there is a *'fullscreen"* button at every row, to show the Javascript code in a fullscreen editor with syntax highlighting! 13 | 14 | ## Js events explained 15 | 16 | Two things will happen when an event occurs on such an SVG element: 17 | 1. The mouse ***cursor*** will change when hoovering above the element, to visualize that an element responds to events. 18 | 1. The Javascript code will be executed in the dashboard. 19 | 20 | Instead of specifying Javascript events in the config screen, it is also possible to add or remove events via input messages. This is explained in the [Control via messages](#control-via-messages) section below. When your Javascript code doesn't work correctly, the wiki [page](https://github.com/bartbutenaers/node-red-contrib-ui-svg/wiki/Troubleshooting-JS-event-handlers) contains some tips and tricks. 21 | 22 | The following example flow shows how to change the color of the circle, every time the circle has been clicked. The flow also shows that the Javascript event handler can be removed, and another Javascript event handler (to show an alert) can be injected via an input message: 23 | 24 | ![javascript flow](https://user-images.githubusercontent.com/14224149/98599183-e16aff00-22db-11eb-8051-3d996ce46052.png) 25 | ``` 26 | [{"id":"89244415.be9278","type":"ui_svg_graphics","z":"a03bd3cf.177578","group":"5ae1b679.de89c8","order":4,"width":"0","height":"0","svgString":"\n\n\n","clickableShapes":[],"javascriptHandlers":[{"selector":"#my_circle","action":"click","sourceCode":"var letters = '0123456789ABCDEF';\n var color = '#';\n for (var i = 0; i < 6; i++) {\n color += letters[Math.floor(Math.random() * 16)];\n }\n\n$(\"#my_circle\")[0].style.fill = color;\n \n$scope.send({payload: color, topic: 'circle_color'})"}],"smilAnimations":[{"id":"","targetId":"","classValue":"","attributeName":"transform","transformType":"rotate","fromValue":"","toValue":"","trigger":"msg","duration":"1","durationUnit":"s","repeatCount":"0","end":"restore","delay":"1","delayUnit":"s","custom":""}],"bindings":[],"showCoordinates":false,"autoFormatAfterEdit":true,"showBrowserErrors":true,"showBrowserEvents":true,"outputField":"payload","editorUrl":"http://drawsvg.org/drawsvg.html","directory":"","panning":"disabled","zooming":"disabled","panOnlyWhenZoomed":false,"doubleClickZoomEnabled":false,"mouseWheelZoomEnabled":false,"name":"SVG with Javascript","x":540,"y":180,"wires":[["e06da0e0.2c837"]]},{"id":"d9df6292.785bc","type":"inject","z":"a03bd3cf.177578","name":"Show alert at click","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"{\"command\":\"add_js_event\",\"event\":\"click\",\"selector\":\"#my_circle\",\"script\":\"alert('Click event handled on the client ...')\"}","payloadType":"json","x":230,"y":140,"wires":[["89244415.be9278"]]},{"id":"5074f893.d378d8","type":"inject","z":"a03bd3cf.177578","name":"Remove clicked event","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"{\"command\":\"remove_js_event\",\"event\":\"click\",\"selector\":\"#my_circle\"}","payloadType":"json","x":240,"y":180,"wires":[["89244415.be9278"]]},{"id":"e06da0e0.2c837","type":"debug","z":"a03bd3cf.177578","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":750,"y":180,"wires":[]},{"id":"8572fad7.dd39b8","type":"inject","z":"a03bd3cf.177578","name":"change color at click","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"{\"command\":\"add_js_event\",\"event\":\"click\",\"selector\":\"#my_circle\",\"script\":\"var letters = '0123456789ABCDEF'; var color = '#'; for (var i = 0; i < 6; i++) { color += letters[Math.floor(Math.random() * 16)]; } $('#my_circle')[0].style.fill = color; $scope.send({payload: color, topic: 'circle_color'})\"}","payloadType":"json","x":230,"y":280,"wires":[["89244415.be9278"]]},{"id":"f678a359.157b4","type":"comment","z":"a03bd3cf.177578","name":"Multiline program ...","info":"","x":220,"y":240,"wires":[]},{"id":"5ae1b679.de89c8","type":"ui_group","name":"Press Demo","tab":"3667e211.c08f0e","order":1,"disp":true,"width":"5","collapse":false},{"id":"3667e211.c08f0e","type":"ui_tab","name":"Home","icon":"dashboard","order":1,"disabled":false,"hidden":false}] 27 | ``` 28 | Which will result in this: 29 | 30 | ![javascript flow demo](https://user-images.githubusercontent.com/14224149/97641343-f83f6500-1a42-11eb-957e-4180e64f37cb.gif) 31 | 32 | ## Events versus Js events 33 | 34 | The main ***difference*** between the events on the "Event" tabsheet and Javascript events on the "JS" tabsheet: 35 | + The "Event" tabsheet is used when an event simply needs to send an output message, which in turn can trigger some other nodes in the flow on the server. E.g. click a light-bulb icon to turn on the lights in your smart home. 36 | 37 | + The "JS" tabsheet is used when an event simply needs to trigger some Javascript code, to trigger functionality directly in the SVG. 38 | 39 | Note that there is some overlap between the events on both tabsheets: 40 | + The "Event" tabsheet could be used to trigger an output message on the server flow, which triggers in turn an input message for this SVG node. That input message can manipulate the SVG. However then we have an entire *roundtrip* (from dashboard via the server flow back to the dashboard) to trigger functionality on the dashboard: 41 | 42 | ![roundtrip](https://user-images.githubusercontent.com/14224149/97758960-47979b00-1b00-11eb-8bda-c5aaec44102b.png) 43 | 44 | Instead it is much easier to use a JS event handler to stay inside the dashboard and run some Javascript code immediately: 45 | 46 | ![event handler](https://user-images.githubusercontent.com/14224149/97759364-2c795b00-1b01-11eb-926e-a3f66455daf8.png) 47 | 48 | + The "JS" tabsheet could be used to trigger some Javascript code to send a message to the server flow. For example: 49 | ``` 50 | $scope.send({payload: color, topic: 'circle_color'}) 51 | ``` 52 | However it is much easier to use a normal event handler, which sends a message (incl. bounding box and all coordinates) without any coding... 53 | 54 | ## Input msg event 55 | 56 | It is possible to select the "input msg" event. This isn't a real event (on an SVG shape), but it means the corresponding JS event handler will be triggered as soon as an input message arrives. 57 | 58 | When the input message is only used to trigger such an event, the `msg.topic` can be set to ***"custom_msg"***. In that case the message will be ignored by this node, which means it will not be validated and it will be used only to trigger input-msg event handlers. This way you can store custom data in the `msg.payload`, which can be used inside the JS event handler. 59 | 60 | In the following example flow, the JS event handler will apply a color to the circle based on the payload value: 61 | 62 | ![custom_msg_demo](https://user-images.githubusercontent.com/14224149/142291397-6c507ea4-c927-40e2-ba35-4d2b4d2991d5.gif) 63 | 64 | ``` 65 | [{"id":"708b561a27277730","type":"ui_svg_graphics","z":"dd961d75822d1f62","group":"8d3148e0.0eee88","order":2,"width":"24","height":"14","svgString":"\n \n","clickableShapes":[],"javascriptHandlers":[{"selector":"","action":"msg","sourceCode":"debugger\nswitch (msg.payload) {\n case 'A':\n $(\"#my_circle\").css(\"fill\", \"red\");\n break;\n case 'B':\n $(\"#my_circle\").css(\"fill\", \"blue\");\n break; \n}"}],"smilAnimations":[],"bindings":[],"showCoordinates":false,"autoFormatAfterEdit":false,"showBrowserErrors":false,"showBrowserEvents":false,"enableJsDebugging":false,"sendMsgWhenLoaded":false,"noClickWhenDblClick":true,"outputField":"payload","editorUrl":"//drawsvg.org/drawsvg.html","directory":"","panning":"disabled","zooming":"disabled","panOnlyWhenZoomed":false,"doubleClickZoomEnabled":false,"mouseWheelZoomEnabled":false,"dblClickZoomPercentage":150,"cssString":"div.ui-svg svg{\n color: var(--nr-dashboard-widgetColor);\n fill: currentColor !important;\n}\ndiv.ui-svg path {\n fill: inherit !important;\n}","name":"","x":1640,"y":220,"wires":[[]]},{"id":"9b0da432d1259189","type":"inject","z":"dd961d75822d1f62","name":"Value A","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"custom_msg","payload":"A","payloadType":"str","x":1450,"y":220,"wires":[["708b561a27277730"]]},{"id":"77d60b330942c145","type":"inject","z":"dd961d75822d1f62","name":"Value B","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"custom_msg","payload":"B","payloadType":"str","x":1450,"y":260,"wires":[["708b561a27277730"]]},{"id":"8d3148e0.0eee88","type":"ui_group","name":"7Shield","tab":"9cdb817b.45e12","order":1,"disp":false,"width":"24","collapse":false,"className":""},{"id":"9cdb817b.45e12","type":"ui_tab","name":"Custom msg demo","icon":"dashboard","order":41,"disabled":false,"hidden":false}] 66 | ``` 67 | -------------------------------------------------------------------------------- /docs/tabsheet_settings.md: -------------------------------------------------------------------------------- 1 | # "Settings" tab sheet 2 | 3 | ## Show coordinates 4 | When this option is selected, a ***tooltip*** will be displayed to show the current mouse location (i.e. X and Y coordinate): 5 | 6 | ![svg_tooltip_coordinates](https://user-images.githubusercontent.com/14224149/63231067-79cc1e00-c216-11e9-83de-f93931f6d489.gif) 7 | 8 | This option has been introduced to simplify lay outing during manual editing of the SVG string (without external SVG drawing tool). Without this option determining the location of your shapes would require a lot of calculations or guessing ... 9 | 10 | Remark: The location is measured in the SVG coordinate system, which means the origin (X=Y=0) is in the top left of your drawing. 11 | 12 | ## Auto format SVG Source after saving edits in SVG Editor 13 | When editing the SVG source via [DrawSvg](#DrawSvg-drawing-editor), the manipulated SVG source is not very pretty: the SVG source will contain empty lines, multiple SVG elements on a single line ... This SVG source can be manually beautified using the "*Format SVG*" button, or automatically (every time the DrawSvg popup dialog window is closed - by activating this checkbox. 14 | 15 | ## Show browser errors on the server 16 | Unfortunately, not all kind of errors can be validated on the server, but instead they will occur on the client side. For example, when an input message arrives, but no SVG element can be found for the specified selector. As a result, your drawing will not be updated, and in the 1.x.x version you had to figure out yourself what is going wrong... Of course, you can have a look in the browser console log to have a look at the client side errors. However, on some systems (e.g. Android smartphones) it is very difficult to get access to that console log (unless you setup a remote connection via USB with your desktop browser). 17 | 18 | To simplify troubleshooting, the client-side errors will appear in the Node-RED debug panel when this checkbox is activated. But keep in mind that if you have N drawings visible simultaneously (when your dashboard is currently displayed in N browsers), then you will get N errors instead of 1 ... 19 | 20 | ## Show browser events on the server 21 | Rather similar to the previous option (about browser errors), except that here browser events (click, ...) are being logged on the server: 22 | 23 | ![Browser event](https://user-images.githubusercontent.com/14224149/98601227-08770000-22df-11eb-8373-4083a6fce5b6.png) 24 | 25 | ## Enable JS event debugging 26 | When this setting is active (and you have opened your browser's development tools), the browser's debugger will automatically halt when a JS event handler will be executed. This allows you to experiment live with your Javascript code, to troubleshoot problems with that code. 27 | 28 | See the wiki [page](https://github.com/bartbutenaers/node-red-contrib-ui-svg/wiki/Troubleshooting-JS-event-handlers) for more information about debugging JS code. 29 | 30 | ## Send output msg when the client is (re)loaded 31 | When this setting is active, an output message will be send every time the client side widget is (re)loaded. This can be useful to trigger the flow to start ***preloading data*** into the SVG drawing when it is opened. The output msg will look like this: 32 | ``` 33 | "payload": , 34 | "topic": "loaded" 35 | ``` 36 | 37 | ## Don't send click events in case of a double click event 38 | This settings is useful when both a click and a double-click handler have been added to the same SVG shape, to achieve something like this: 39 | + If the shape is clicked, you want to perform action A. 40 | + If the shape is double clicked, you want to perform action B. 41 | 42 | The default behaviour of SVG DOM events, is that a double click will result in 3 events. So 3 output messages will sent: 43 | 44 | ![3 events](https://user-images.githubusercontent.com/14224149/131240459-01c30fae-a188-48fd-9c17-6be36a68e892.png) 45 | 46 | In the current use case we don't want the single click messages, in case a double click has been executed. Otherwise both action A and action B would be executed. 47 | 48 | By activating this setting, the single-click messages won't be send in case of a double-click by the user. 49 | 50 | ## Editor URL 51 | This is the URL where the [DrawSvg](#DrawSvg-drawing-editor) editor instance is being hosted. By default this field contains a link to the official [DrawSvg cloud](http://drawsvg.org/drawsvg.html) system, but it can also contain a link to a local DrawSvg installation (hosted via a [node-red-contrib-drawsvg](https://github.com/bartbutenaers/node-red-contrib-drawsvg) node). 52 | 53 | *Be aware that this is a free system, so there is no guarantee about availability of the cloud system!* 54 | 55 | ## Directory 56 | This directory of your local system (where your Node-RED instance is running) can be used to make your local images available, to both your dashboard and your flow editor. 57 | 58 | ## Pan and zoom 59 | A series of options are available to allow panning and zooming, which is useful for large drawings (like buildings, process flows, ...): 60 | 61 | + ***"Panning"***: enable panning in X, Y or in both directions. 62 | + ***"Zooming"***: enable zooming. 63 | + ***"Pan only when zoomed"***: when this option is activated, the SVG drawing can only be panned when it has been zoomed previously. Indeed, when the drawing is at its original size, it might in some cases be pointless to allow panning. 64 | + ***"Enable mouse-wheel zooming"***: allow zooming in/out by rotating the mouse wheel. 65 | + ***"Enable double click/tap zooming"***: the first double mouse click (or double tap on a touch screen) will trigger zooming in. The second double tap will trigger zooming out. And so on ... 66 | 67 | The following demo shows how to pan and zoom via the mouse (mouse-wheel and dragging): 68 | 69 | ![svg_panzoom_mouse](https://user-images.githubusercontent.com/14224149/85945109-cd7dbc80-b93b-11ea-8dde-86f32be2b89e.gif) 70 | 71 | When a ***touch device*** has been detected, panning and zoom through touch events is also supported. Thanks to [tkirchm](https://github.com/tkirchm) for getting us started with these new features! The following hand gestures are currently supported: 72 | 73 | ![gestures](https://user-images.githubusercontent.com/14224149/71647175-f6a5f300-2cf2-11ea-9389-ae1ab84ec18c.png) 74 | 75 | It is also possible to control panning and zooming via input messages, as explained in the section [Control via messages](#control-via-messages) below. 76 | The following example flow shows how to control panning and zooming from the flow editor (using Inject nodes) and via the dashboard (using dashboard button widgets): 77 | 78 | ![Pan zoom flow](https://user-images.githubusercontent.com/14224149/85944980-e20d8500-b93a-11ea-8e9e-7226634de35d.png) 79 | 80 | ``` 81 | [{"id":"a289199.a1714e8","type":"debug","z":"4ae15451.7b2f5c","name":"Floorplan output","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","x":2140,"y":380,"wires":[]},{"id":"32dfda30.706666","type":"ui_svg_graphics","z":"4ae15451.7b2f5c","group":"8d1b9121.83b3c","order":1,"width":"14","height":"10","svgString":"ABFCDHEKPGLQOTJINSMR100200300400500100200300400XY","clickableShapes":[{"targetId":"#mycircle","action":"click","payload":"cam living clicked","payloadType":"str","topic":"camera_living"}],"smilAnimations":[],"bindings":[{"selector":"#e26_texte","bindSource":"payload.title","bindType":"text","attribute":""}],"showCoordinates":false,"autoFormatAfterEdit":false,"showBrowserErrors":false,"selectorAsElementId":false,"outputField":"","editorUrl":"https://drawsvg.org/drawsvg.html","directory":"","panning":"both","zooming":"enabled","panOnlyWhenZoomed":false,"doubleClickZoomEnabled":true,"mouseWheelZoomEnabled":true,"name":"","x":1940,"y":380,"wires":[["a289199.a1714e8"]]},{"id":"91163354.31b2f","type":"inject","z":"4ae15451.7b2f5c","name":"Zoom in","topic":"","payload":"{\"command\":\"zoom_in\"}","payloadType":"json","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":1480,"y":260,"wires":[["32dfda30.706666"]]},{"id":"8c7c165f.dee1a8","type":"inject","z":"4ae15451.7b2f5c","name":"Zoom out","topic":"","payload":"{\"command\":\"zoom_out\"}","payloadType":"json","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":1480,"y":300,"wires":[["32dfda30.706666"]]},{"id":"d984058a.02d818","type":"inject","z":"4ae15451.7b2f5c","name":"Zoom 200%","topic":"","payload":"{\"command\":\"zoom_by_percentage\",\"percentage\":200}","payloadType":"json","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":1490,"y":340,"wires":[["32dfda30.706666"]]},{"id":"62277e70.5c676","type":"inject","z":"4ae15451.7b2f5c","name":"Zoom point (x=300, y=300) 200%","topic":"","payload":"{\"command\":\"zoom_by_percentage\",\"percentage\":200,\"x\":300,\"y\":300}","payloadType":"json","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":1550,"y":380,"wires":[["32dfda30.706666"]]},{"id":"365d5483.fa174c","type":"inject","z":"4ae15451.7b2f5c","name":"Pan to point (x=200 / y= 100)","topic":"","payload":"{\"command\":\"pan_to_point\",\"x\":200,\"y\":100}","payloadType":"json","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":1540,"y":420,"wires":[["32dfda30.706666"]]},{"id":"4d1e70dd.f51f3","type":"inject","z":"4ae15451.7b2f5c","name":"Pan to direction (x=200, y= 100)","topic":"","payload":"{\"command\":\"pan_to_direction\",\"x\":200,\"y\":100}","payloadType":"json","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":1550,"y":460,"wires":[["32dfda30.706666"]]},{"id":"79ebf8c1.adadd8","type":"inject","z":"4ae15451.7b2f5c","name":"Reset","topic":"","payload":"{\"command\":\"reset_panzoom\"}","payloadType":"json","repeat":"","crontab":"","once":false,"onceDelay":0.1,"showConfirmation":false,"confirmationLabel":"","x":1470,"y":220,"wires":[["32dfda30.706666"]]},{"id":"61be354.f7175cc","type":"ui_button","z":"4ae15451.7b2f5c","name":"Reset","group":"8d1b9121.83b3c","order":4,"width":"3","height":"1","passthru":false,"label":"Reset","tooltip":"","color":"","bgcolor":"","icon":"","payload":"{\"command\":\"reset_panzoom\"}","payloadType":"json","topic":"","x":1730,"y":140,"wires":[["32dfda30.706666"]]},{"id":"d43dfb27.a0f308","type":"ui_button","z":"4ae15451.7b2f5c","name":"Zoom in","group":"8d1b9121.83b3c","order":4,"width":"3","height":"1","passthru":false,"label":"Zoom in","tooltip":"","color":"","bgcolor":"","icon":"","payload":"{\"command\":\"zoom_in\"}","payloadType":"json","topic":"","x":1740,"y":180,"wires":[["32dfda30.706666"]]},{"id":"ef78f0f5.c44b3","type":"ui_button","z":"4ae15451.7b2f5c","name":"Zoom out","group":"8d1b9121.83b3c","order":4,"width":"3","height":"1","passthru":false,"label":"Zoom out","tooltip":"","color":"","bgcolor":"","icon":"","payload":"{\"command\":\"zoom_out\"}","payloadType":"json","topic":"","x":1740,"y":220,"wires":[["32dfda30.706666"]]},{"id":"8d1b9121.83b3c","type":"ui_group","z":"","name":"Pan/zoom test","tab":"5021fcf2.ee7ac4","order":1,"disp":true,"width":"14","collapse":false},{"id":"5021fcf2.ee7ac4","type":"ui_tab","z":"","name":"SVG","icon":"dashboard","disabled":false,"hidden":false}] 82 | ``` 83 | Which results in this dashboard behaviour: 84 | 85 | ![svg_panzoom_demo](https://user-images.githubusercontent.com/14224149/85945042-4b8d9380-b93b-11ea-9724-8ee04e3d442c.gif) 86 | 87 | Notice the different behaviour between the two types of buttons in this flow: 88 | + The dashboard buttons will trigger a message that contains the ***socketid***: 89 | 90 | ![socketid](https://user-images.githubusercontent.com/14224149/85958547-925ca700-b996-11ea-911d-d60778d7a933.png) 91 | 92 | Based on that socketid, only that client - where the button has been pressed - will receive the pan/zoom command (which is exactly what we want). 93 | 94 | + The Inject node buttons will trigger a message that contains no socketid (since there is no specific dashboard client involved here), so ALL clients will receive the same pan/zoom command (which is most of the time not useful). 95 | 96 | Caution: make sure the panning and zooming is enabled in the Settings tab sheet, otherwise it will not be possible to control panning and zooming via input messages! 97 | -------------------------------------------------------------------------------- /examples/chaining_animations.json: -------------------------------------------------------------------------------- 1 | [{"id":"3c858c3e34cb735d","type":"ui_svg_graphics","z":"b72068aa707669b0","group":"28cdac6db4804909","order":1,"width":"10","height":"6","svgString":"\n \n","clickableShapes":[],"javascriptHandlers":[],"smilAnimations":[{"id":"first_animation","targetId":"my_circle","classValue":"","attributeName":"r","transformType":"rotate","fromValue":"20","toValue":"40","trigger":"msg","duration":"3","durationUnit":"s","repeatCount":"1","end":"freeze","delay":"1","delayUnit":"s","custom":""},{"id":"second_animation","targetId":"my_circle","classValue":"","attributeName":"fill","transformType":"rotate","fromValue":"red","toValue":"green","trigger":"cust","duration":"3","durationUnit":"s","repeatCount":"1","end":"restore","delay":"1","delayUnit":"s","custom":"first_animation.end; "}],"bindings":[],"showCoordinates":false,"autoFormatAfterEdit":false,"showBrowserErrors":false,"showBrowserEvents":false,"enableJsDebugging":false,"sendMsgWhenLoaded":false,"noClickWhenDblClick":false,"outputField":"payload","editorUrl":"//drawsvg.org/drawsvg.html","directory":"","panning":"disabled","zooming":"disabled","panOnlyWhenZoomed":false,"doubleClickZoomEnabled":false,"mouseWheelZoomEnabled":false,"dblClickZoomPercentage":150,"cssString":"div.ui-svg svg{\n color: var(--nr-dashboard-widgetColor);\n fill: currentColor !important;\n}\ndiv.ui-svg path {\n fill: inherit;\n}","name":"","x":570,"y":580,"wires":[[]]},{"id":"bf34f5fe1650ea49","type":"inject","z":"b72068aa707669b0","name":"Start first animation","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"{\"command\":\"trigger_animation\",\"selector\":\"#first_animation\",\"action\":\"start\"}","payloadType":"json","x":350,"y":580,"wires":[["3c858c3e34cb735d"]]},{"id":"28cdac6db4804909","type":"ui_group","name":"Chained animation demo","tab":"8bbab1b47b8e87c8","order":1,"disp":true,"width":"10","collapse":false,"className":""},{"id":"8bbab1b47b8e87c8","type":"ui_tab","name":"SVG","icon":"dashboard","order":1,"disabled":false,"hidden":false}] 2 | -------------------------------------------------------------------------------- /examples/change_fill_color_style.json: -------------------------------------------------------------------------------- 1 | [{"id":"8f0e54d7.4d1668","type":"ui_svg_graphics","z":"d9a54719.b13a88","group":"8d3148e0.0eee88","order":1,"width":"14","height":"10","svgString":"\n \n \n \n ","clickableShapes":[{"targetId":"#cam_living_room","action":"click","payload":"camera_living","payloadType":"str","topic":"camera_living"}],"smilAnimations":[],"bindings":[],"showCoordinates":false,"autoFormatAfterEdit":false,"outputField":"","editorUrl":"http://drawsvg.org/drawsvg.html","directory":"","name":"","x":560,"y":500,"wires":[[]]},{"id":"588ccdd4.5ad384","type":"inject","z":"d9a54719.b13a88","name":"fill with red","topic":"","payload":"{\"command\":\"update_style\",\"selector\":\"#cam_living_room\",\"attributeName\":\"fill\",\"attributeValue\":\"red\"}","payloadType":"json","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":320,"y":500,"wires":[["8f0e54d7.4d1668"]]},{"id":"3272d837.2122e8","type":"inject","z":"d9a54719.b13a88","name":"fill with blue","topic":"","payload":"{\"command\":\"update_style\",\"selector\":\"#cam_living_room\",\"attributeName\":\"fill\",\"attributeValue\":\"blue\"}","payloadType":"json","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":330,"y":540,"wires":[["8f0e54d7.4d1668"]]},{"id":"8d3148e0.0eee88","type":"ui_group","z":"","name":"Floorplan test","tab":"93e19e4f.b80ed","disp":true,"width":"14","collapse":false},{"id":"93e19e4f.b80ed","type":"ui_tab","z":"","name":"SVG","icon":"dashboard","disabled":false,"hidden":false}] 2 | -------------------------------------------------------------------------------- /examples/click_event_handling.json: -------------------------------------------------------------------------------- 1 | [{"id":"3b053fec.6e949","type":"ui_svg_graphics","z":"ae5f2a27.a96178","group":"87e79a83.f45268","order":1,"width":"14","height":"14","svgString":"\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n Clipart by Nicu Buculei - baloon1_04\n \n \n Nicu Buculei\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n image/svg+xml\n \n \n \n \n Openclipart\n \n \n \n \n \n \n \n \n \n \n","clickableShapes":[{"targetId":"path[id^=\"ball_\"]","action":"click","payload":"circle","payloadType":"str","topic":"circle"}],"smilAnimations":[],"bindings":[],"showCoordinates":false,"autoFormatAfterEdit":false,"outputField":"payload","editorUrl":"https://drawsvg.org/drawsvg.html","directory":"","panEnabled":true,"zoomEnabled":true,"controlIconsEnabled":true,"dblClickZoomEnabled":true,"mouseWheelZoomEnabled":true,"name":"","x":1320,"y":120,"wires":[["fed8df5a.4c652"]]},{"id":"fed8df5a.4c652","type":"debug","z":"ae5f2a27.a96178","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","x":1490,"y":120,"wires":[]},{"id":"e4241244.0efc3","type":"inject","z":"ae5f2a27.a96178","name":"","topic":"","payload":"[{\"command\":\"update_attribute\",\"selector\":\"#micro_bertje\",\"attributeName\":\"x\",\"attributeValue\":\"50\"}]","payloadType":"json","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":1130,"y":120,"wires":[["3b053fec.6e949"]]},{"id":"87e79a83.f45268","type":"ui_group","z":"","name":"Pan and zoom feature","tab":"935e3c31.38046","disp":true,"width":"14","collapse":false},{"id":"935e3c31.38046","type":"ui_tab","z":"","name":"SVG demo","icon":"dashboard","disabled":false,"hidden":false}] 2 | -------------------------------------------------------------------------------- /examples/corona-game.json: -------------------------------------------------------------------------------- 1 | [{"id":"7775c39b.91f56c","type":"ui_svg_graphics","z":"24d4dcbf.c53534","group":"35642e22.275302","order":1,"width":"11","height":"11","svgString":"\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n","clickableShapes":[{"targetId":"path[id^=\"ball_\"]","action":"click","payload":"circle","payloadType":{"length":0,"prevObject":{"0":{},"length":1}},"topic":"circle"}],"smilAnimations":[],"bindings":[],"showCoordinates":false,"autoFormatAfterEdit":false,"outputField":"payload","editorUrl":"https://drawsvg.org/drawsvg.html","directory":"","panEnabled":true,"zoomEnabled":true,"controlIconsEnabled":true,"dblClickZoomEnabled":true,"mouseWheelZoomEnabled":true,"name":"Show virus SVG","x":800,"y":260,"wires":[["19ea5c30.bd6de4"]]},{"id":"4a4c851.1694b7c","type":"inject","z":"24d4dcbf.c53534","name":"Update drawing","topic":"","payload":"","payloadType":"date","repeat":"2","crontab":"","once":false,"onceDelay":0.1,"x":310,"y":260,"wires":[["4cd2fff.21555"]]},{"id":"4cd2fff.21555","type":"function","z":"24d4dcbf.c53534","name":"Make random circle clickable","func":"msg.payload = [];\n\nvar previousNumber = flow.get(\"randomNumber\");\n\nif (previousNumber !== undefined) {\n // Make the previous circle unclickable\n msg.payload.push({\n command : \"remove_event\",\n event : \"click\",\n selector : \"#circle_\" + previousNumber\n })\n \n // Make the previous circle yellow again\n msg.payload.push({\n \"command\": \"set_attribute\",\n \"selector\": \"#circle_\" + previousNumber,\n \"attributeName\": \"fill\",\n \"attributeValue\": \"#ffe07d\"\n })\n}\n\n// Calculate a random number between 1 and 28\nvar randomNumber = Math.floor(Math.random() * 28) + 1;\n\n// Make the new circle unclickable\nmsg.payload.push({\n command : \"add_event\",\n event : \"click\",\n selector : \"#circle_\" + randomNumber, \n payload : \"circle_\" + randomNumber + \" has been clicked\",\n topic : \"CIRCLE_\" + randomNumber + \"_CLICKED\"\n})\n\n// Make the new circle red\nmsg.payload.push({\n \"command\": \"set_attribute\",\n \"selector\": \"#circle_\" + randomNumber,\n \"attributeName\": \"fill\",\n \"attributeValue\": \"#ff0000\"\n})\n\nflow.set(\"randomNumber\", randomNumber);\n\nreturn msg;","outputs":1,"noerr":0,"x":560,"y":260,"wires":[["7775c39b.91f56c"]]},{"id":"d593ed93.5b47d","type":"ui_audio","z":"24d4dcbf.c53534","name":"","group":"22787703.a0e968","voice":"en-US","always":true,"x":1180,"y":260,"wires":[]},{"id":"19ea5c30.bd6de4","type":"change","z":"24d4dcbf.c53534","name":"","rules":[{"t":"set","p":"payload","pt":"msg","to":"Very good","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":1000,"y":260,"wires":[["d593ed93.5b47d"]]},{"id":"35642e22.275302","type":"ui_group","z":"","name":"Corona quarantaine game","tab":"f41f368.f6b99c8","disp":true,"width":"11","collapse":false},{"id":"22787703.a0e968","type":"ui_group","z":"","name":"Web push notifications","tab":"80f0e178.bbf4a","disp":true,"width":"6","collapse":false},{"id":"f41f368.f6b99c8","type":"ui_tab","z":"","name":"SVG demo","icon":"dashboard","disabled":false,"hidden":false},{"id":"80f0e178.bbf4a","type":"ui_tab","z":"","name":"Home","icon":"dashboard","order":1,"disabled":false,"hidden":false}] 2 | -------------------------------------------------------------------------------- /examples/font_awesome_icon_demo.json: -------------------------------------------------------------------------------- 1 | [{"id":"f369eb92.6c5558","type":"ui_svg_graphics","z":"553defb0.b99fb","group":"9ec8b304.368cc","order":0,"width":"15","height":"15","svgString":"\n\n\n fa-thermometer-empty\n","clickableShapes":[{"targetId":"#camera_living","action":"click","payload":"#camera_living","payloadType":"str","topic":"#camera_living"},{"targetId":"#camera_balcony","action":"click","payload":"#camera_balcony","payloadType":"str","topic":"#camera_balcony"},{"targetId":"#camera_entry","action":"click","payload":"#camera_entry","payloadType":"str","topic":"#camera_entry"}],"smilAnimations":[],"bindings":[{"selector":"#camera_living","bindSource":"payload.attributeValue","bindType":"attr","attribute":"fill"},{"selector":"#camera_entry","bindSource":"payload.attribueValue","bindType":"attr","attribute":"fill"},{"selector":"#camera_balcony","bindSource":"payload.attributeVale","bindType":"attr","attribute":"fill"}],"showCoordinates":true,"autoFormatAfterEdit":false,"outputField":"anotherField","editorUrl":"","directory":"","name":"Home Floor Plan","x":1130,"y":520,"wires":[[]]},{"id":"866e2e46.ba033","type":"inject","z":"553defb0.b99fb","name":"fa-thermometer-three-quarters","topic":"","payload":"{\"command\":\"update_text\",\"selector\":\"#my_text\",\"textContent\":\"fa-thermometer-three-quarters\"}","payloadType":"json","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":820,"y":520,"wires":[["f369eb92.6c5558"]]},{"id":"68c4730b.af00bc","type":"inject","z":"553defb0.b99fb","name":"fa-thermometer-full ","topic":"","payload":"{\"command\":\"update_text\",\"selector\":\"#my_text\",\"textContent\":\"fa-thermometer-full\"}","payloadType":"json","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":790,"y":560,"wires":[["f369eb92.6c5558"]]},{"id":"46183ab5.42fd54","type":"inject","z":"553defb0.b99fb","name":"fa-thermometer-empty","topic":"","payload":"{\"command\":\"update_text\",\"selector\":\"#my_text\",\"textContent\":\"fa-thermometer-empty\"}","payloadType":"json","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":800,"y":400,"wires":[["f369eb92.6c5558"]]},{"id":"501c7f9a.08ac4","type":"inject","z":"553defb0.b99fb","name":"fa-thermometer-half ","topic":"","payload":"{\"command\":\"update_text\",\"selector\":\"#my_text\",\"textContent\":\"fa-thermometer-half\"}","payloadType":"json","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":790,"y":480,"wires":[["f369eb92.6c5558"]]},{"id":"d3ea2538.fa9458","type":"inject","z":"553defb0.b99fb","name":"fa-thermometer-quarter","topic":"","payload":"{\"command\":\"update_text\",\"selector\":\"#my_text\",\"textContent\":\"fa-thermometer-quarter\"}","payloadType":"json","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":800,"y":440,"wires":[["f369eb92.6c5558"]]},{"id":"9ec8b304.368cc","type":"ui_group","z":"","name":"Home Floor Plan","tab":"bb4f2a94.83b338","disp":true,"width":"15","collapse":false},{"id":"bb4f2a94.83b338","type":"ui_tab","z":"","name":"Home Floor Plan","icon":"dashboard","disabled":false,"hidden":false}] 2 | -------------------------------------------------------------------------------- /examples/hmi-demo.json: -------------------------------------------------------------------------------- 1 | [{"id":"db080335.91c06","type":"inject","z":"60ad596.8120ba8","name":"","topic":"","payload":"","payloadType":"date","repeat":"5","crontab":"","once":false,"onceDelay":0.1,"x":930,"y":580,"wires":[["d874352a.1495e8"]]},{"id":"d874352a.1495e8","type":"function","z":"60ad596.8120ba8","name":"random mode generator","func":"let r1 = Math.floor(Math.random() * 5000) + 1000; \n\nfunction randomItem(arr){\n var len = arr.length;\n var rand = Math.floor(Math.random() * len);\n return arr[rand]\n}\n\nlet station = randomItem([\"STN10\",\"STN20\",\"STN30\",\"STN40\",\"STN50\",\"STN60\",\"STN70\",\"STN80\"]);\nlet mode = randomItem([\"auto\",\"manual\",\"fault\",\"auto\",\"other\",\"auto\",\"fault\",\"auto\"]);\nlet fault = randomItem([\"sensor fault\",\"coolant empty\",\"CP tripped\",\"over speed\",\"E-Stop\",\"Safety Mat\"]);\nlet other = randomItem([\"full\",\"waiting\",\"over cycle\"]);\n\nlet statusText = mode == \"other\" ? other : (mode == \"fault\" ? fault : mode);\n\nlet state = flow.get(\"state\") || {}\nif(!state[station]){\n state[station] = {mode:\"manual\", status:\"manual\", count:0} \n}\nstate[station].mode = mode;\nstate[station].status = statusText;\nflow.set(\"state\",state)\n\n//$(\"#STN50 > .background\").attr(\"fill\",\"url('#fault')\")\n//$(\"#STN50 > .status\").text(\"hi\")\n\nlet newmsg = {payload:[\n {\n \"command\": \"update_attribute\",\n \"selector\": \"#\" + station + \" > .background\",\n \"attributeName\": \"fill\",\n \"attributeValue\": \"url('#\" + mode + \"')\"\n },\n {\n \"command\": \"update_text\",\n selector: \"#\" + station + \" > .status\",\n textContent: statusText\n },\n {\n \"command\": \"update_text\",\n selector: \"#\" + station + \" > .count\",\n textContent: state[station].count\n }\n ]\n}\n\n\nsetTimeout(function () {\n node.send(newmsg)\n}, r1 - 100);\n\n \n//return msg;","outputs":1,"noerr":0,"x":1130,"y":580,"wires":[["9ae97533.8f9bc8"]]},{"id":"9ae97533.8f9bc8","type":"ui_svg_graphics","z":"60ad596.8120ba8","group":"d634d288.20fb8","order":0,"width":"12","height":"9","svgString":"\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n background\n \n \n \n \n \n Main Overview\n \n \n Station 10\n Manual\n 0\n \n \n \n Station 20\n Manual\n 0\n \n \n \n Station 30\n Manual\n 0\n \n \n \n Station 40\n Manual\n 0\n \n \n \n Station 50\n Manual\n 0\n \n \n \n Station 60\n Manual\n 0\n \n \n \n Station 70\n Manual\n 0\n \n \n \n Station 80\n Manual\n 0\n \n \n \n Main Overview\n \n \n \n \n Station 10 Overview\n \n \n Station 10\n \n \n \n Air Supply\n \n \n \n Water Supply\n \n \n \n Control Power\n \n \n \n Close\n \n \n","clickableShapes":[{"targetId":"STN10","action":"click","payload":"page2","payloadType":"str","topic":"page2"},{"targetId":"STN10_home_button","action":"click","payload":"page1","payloadType":"str","topic":"page1"}],"smilAnimations":[{"id":"hide_page1","targetId":"page1","classValue":"","attributeName":"display","fromValue":"inline","toValue":"none","trigger":"cust","duration":"500","durationUnit":"ms","repeatCount":"1","end":"restore","delay":"1","delayUnit":"s","custom":"STN10.click; "},{"id":"show_page2","targetId":"page2","classValue":"","attributeName":"display","fromValue":"none","toValue":"inline","trigger":"cust","duration":"500","durationUnit":"ms","repeatCount":"1","end":"restore","delay":"1","delayUnit":"s","custom":"STN10.click; "},{"id":"hide_page2","targetId":"page2","classValue":"","attributeName":"display","fromValue":"inline","toValue":"none","trigger":"cust","duration":"500","durationUnit":"ms","repeatCount":"1","end":"restore","delay":"1","delayUnit":"s","custom":"STN10_home_button.click; "},{"id":"show_page1","targetId":"page1","classValue":"","attributeName":"display","fromValue":"none","toValue":"inline","trigger":"cust","duration":"500","durationUnit":"ms","repeatCount":"1","end":"restore","delay":"1","delayUnit":"s","custom":"STN10_home_button.click; "},{"id":"show_page1_copy","targetId":"page1","classValue":"","attributeName":"display","fromValue":"none","toValue":"inline","trigger":"cust","duration":"500","durationUnit":"ms","repeatCount":"1","end":"restore","delay":"1","delayUnit":"s","custom":"STN10_home_button.click; "}],"bindings":[],"showCoordinates":false,"autoFormatAfterEdit":false,"outputField":"payload","editorUrl":"http://drawsvg.org/drawsvg.html","directory":"","name":"","x":1360,"y":580,"wires":[["16c10ad9.ea5c75"]]},{"id":"289594d4.e955ac","type":"function","z":"60ad596.8120ba8","name":"random count generator","func":"function randomInc(station, delayms){\n\n setTimeout(function () {\n var state = flow.get(\"state\") || {};\n if(!state[station]){\n state[station] = {count: 0} \n }\n if(state[station].mode == \"auto\"){\n state[station].count += 1;\n flow.set(\"state\",state)\n var newmsg = {\n payload:{\n \"command\": \"update_text\",\n selector: \"#\" + station + \" > .count\",\n textContent: state[station].count\n }\n }\n node.send(newmsg);\n }\n \n }, delayms-5);\n}\n\nvar stns = [\"STN10\",\"STN20\",\"STN30\",\"STN40\",\"STN50\",\"STN60\",\"STN70\",\"STN80\"]\nvar i = 0;\nfor(i=0; i\n \n /* When the openFullscreen() function is executed, open the video in fullscreen.\n Note that we must include prefixes for different browsers, as they don't support the requestFullscreen method yet */\n function openFullscreen() {\n debugger\n /* Get the element you want displayed in fullscreen mode (a video in this example): */\n var elem = document.getElementById(\"svghmi\");\n if (elem.requestFullscreen) {\n elem.requestFullscreen();\n } else if (elem.mozRequestFullScreen) { /* Firefox */\n elem.mozRequestFullScreen();\n } else if (elem.webkitRequestFullscreen) { /* Chrome, Safari and Opera */\n elem.webkitRequestFullscreen();\n } else if (elem.msRequestFullscreen) { /* IE/Edge */\n elem.msRequestFullscreen();\n }\n }\n \n //this seems to be too quick\n $(\".header_bar\").click(openFullscreen);\n \n // hack / catch all - something has to work\n setTimeout(function(){\n debugger\n $(\".header_bar\").click(openFullscreen);\n }, 4000)\n \n \n $(document).ready(function(){\n //this doesnt seem to work!\n $('#svghmi').on('load', function() {\n $(\".header_bar\").click(openFullscreen);\n }, true);\n \n //this seems to be too quick\n $(\".header_bar\").click(openFullscreen);\n \n // hack / catch all - something has to work\n setTimeout(function(){\n debugger\n $(\".header_bar\").click(openFullscreen);\n }, 2000)\n })\n\n\n \n \n ","storeOutMessages":true,"fwdInMessages":true,"templateScope":"global","x":1170,"y":500,"wires":[[]]},{"id":"16c10ad9.ea5c75","type":"debug","z":"60ad596.8120ba8","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","x":1550,"y":580,"wires":[]},{"id":"b30ed944.1945b8","type":"function","z":"60ad596.8120ba8","name":"init mode generator","func":"let stations = [\"STN10\",\"STN20\",\"STN30\",\"STN40\",\"STN50\",\"STN60\",\"STN70\",\"STN80\"];\nlet state = flow.get(\"state\") || {}\n\nfunction buildUpdates(station, state){\n if(isNaN(state.count)){\n state.count = 0;\n }\n msgData.push({\n \"command\": \"update_attribute\",\n \"selector\": \"#\" + station + \" > .background\",\n \"attributeName\": \"fill\",\n \"attributeValue\": \"url('#\" + (state.mode || \"manual\") + \"')\"\n })\n msgData.push({\n \"command\": \"update_text\",\n selector: \"#\" + station + \" > .status\",\n textContent: state.status || \"manual\"\n })\n msgData.push({\n \"command\": \"update_text\",\n selector: \"#\" + station + \" > .count\",\n textContent: state.count || 0\n })\n}\nlet msgData = [];\n\n[\"STN10\",\"STN20\",\"STN30\",\"STN40\",\"STN50\",\"STN60\",\"STN70\",\"STN80\"].forEach(function(station){\n if(!state[station]){\n state[station] = {mode:\"manual\", status:\"manual\", count:0} \n }\n buildUpdates(station, state[station]);\n})\n\nflow.set(\"state\",state)\nmsg.payload = msgData;\nreturn msg;","outputs":1,"noerr":0,"x":1110,"y":540,"wires":[["9ae97533.8f9bc8","9f176c0.9c6b298"]]},{"id":"e52caa81.e12af8","type":"inject","z":"60ad596.8120ba8","name":"","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":true,"onceDelay":"0.5","x":930,"y":540,"wires":[["b30ed944.1945b8"]]},{"id":"9f176c0.9c6b298","type":"debug","z":"60ad596.8120ba8","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","x":1350,"y":540,"wires":[]},{"id":"358d15b3.6bf2aa","type":"ui_ui_control","z":"60ad596.8120ba8","name":"","x":940,"y":500,"wires":[["b30ed944.1945b8"]]},{"id":"e9f581ab.2a03a","type":"comment","z":"60ad596.8120ba8","name":"HMI Demo with fake data. Click Title bar to full screen, click Station 10 for page 2","info":"","x":1120,"y":460,"wires":[]},{"id":"d634d288.20fb8","type":"ui_group","z":"","name":"HMI","tab":"978a957f.bdd1b8","disp":true,"width":"12","collapse":false},{"id":"978a957f.bdd1b8","type":"ui_tab","z":"","name":"Home","icon":"dashboard"}] 2 | -------------------------------------------------------------------------------- /examples/interactive_light_bulb.json: -------------------------------------------------------------------------------- 1 | [{"id":"d933559c.703508","type":"ui_svg_graphics","z":"9f1652ec.e8236","group":"6bfff10e.f6969","order":1,"width":"14","height":"10","svgString":"\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n","clickableShapes":[{"targetId":"#my_light_bulb","action":"click","payload":"my_output_value","payloadType":"str","topic":"my_topic"}],"smilAnimations":[],"bindings":[{"selector":"#banner","bindSource":"payload.title","bindType":"text","attribute":""},{"selector":"#camera_living","bindSource":"payload.position.x","bindType":"attr","attribute":"x"},{"selector":"#camera_living","bindSource":"payload.camera.colour","bindType":"attr","attribute":"fill"}],"showCoordinates":false,"autoFormatAfterEdit":false,"outputField":"my_output_field","editorUrl":"http://drawsvg.org/drawsvg.html","directory":"","name":"","x":500,"y":160,"wires":[["43d0f52a.3a275c"]]},{"id":"63aa05f8.1f41cc","type":"inject","z":"9f1652ec.e8236","name":"Set color red","topic":"","payload":"{\"command\":\"update_style\",\"selector\":\"#my_light_bulb\",\"attributeName\":\"fill\",\"attributeValue\":\"red\"}","payloadType":"json","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":290,"y":160,"wires":[["d933559c.703508"]]},{"id":"43d0f52a.3a275c","type":"debug","z":"9f1652ec.e8236","name":"SVG event","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","x":720,"y":160,"wires":[]},{"id":"6bfff10e.f6969","type":"ui_group","z":"","name":"Floorplan test","tab":"142bc95.bfbb637","disp":true,"width":"14","collapse":false},{"id":"142bc95.bfbb637","type":"ui_tab","z":"","name":"SVG","icon":"dashboard","disabled":false,"hidden":false}] 2 | -------------------------------------------------------------------------------- /examples/javascript_events.json: -------------------------------------------------------------------------------- 1 | [{"id":"89244415.be9278","type":"ui_svg_graphics","z":"a03bd3cf.177578","group":"5ae1b679.de89c8","order":4,"width":"0","height":"0","svgString":"\n\n\n","clickableShapes":[],"javascriptHandlers":[{"selector":"#my_circle","action":"click","sourceCode":"var letters = '0123456789ABCDEF';\n var color = '#';\n for (var i = 0; i < 6; i++) {\n color += letters[Math.floor(Math.random() * 16)];\n }\n\n$(\"#my_circle\")[0].style.fill = color;\n \n$scope.send({payload: color, topic: 'circle_color'})"}],"smilAnimations":[],"bindings":[],"showCoordinates":false,"autoFormatAfterEdit":true,"showBrowserErrors":true,"outputField":"payload","editorUrl":"http://drawsvg.org/drawsvg.html","directory":"","panning":"disabled","zooming":"disabled","panOnlyWhenZoomed":false,"doubleClickZoomEnabled":false,"mouseWheelZoomEnabled":false,"name":"SVG with Javascript","x":540,"y":180,"wires":[["e06da0e0.2c837"]]},{"id":"d9df6292.785bc","type":"inject","z":"a03bd3cf.177578","name":"Show alert at double click","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"{\"command\":\"add_js_event\",\"event\":\"focus\",\"selector\":\"#my_circle\",\"script\":\"alert('Click event handled on the client ...')\"}","payloadType":"json","x":250,"y":140,"wires":[["89244415.be9278"]]},{"id":"5074f893.d378d8","type":"inject","z":"a03bd3cf.177578","name":"Remove clicked event","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"{\"command\":\"remove_js_event\",\"event\":\"click\",\"selector\":\"#my_circle\"}","payloadType":"json","x":240,"y":180,"wires":[["89244415.be9278"]]},{"id":"e06da0e0.2c837","type":"debug","z":"a03bd3cf.177578","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":750,"y":180,"wires":[]},{"id":"5ae1b679.de89c8","type":"ui_group","name":"Press Demo","tab":"3667e211.c08f0e","order":1,"disp":true,"width":"5","collapse":false},{"id":"3667e211.c08f0e","type":"ui_tab","name":"Home","icon":"dashboard","order":1,"disabled":false,"hidden":false}] 2 | -------------------------------------------------------------------------------- /examples/pan_zoom_demo.json: -------------------------------------------------------------------------------- 1 | [{"id":"a289199.a1714e8","type":"debug","z":"4ae15451.7b2f5c","name":"Floorplan output","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","x":2140,"y":380,"wires":[]},{"id":"32dfda30.706666","type":"ui_svg_graphics","z":"4ae15451.7b2f5c","group":"8d1b9121.83b3c","order":1,"width":"14","height":"10","svgString":"ABFCDHEKPGLQOTJINSMR100200300400500100200300400XY","clickableShapes":[{"targetId":"#mycircle","action":"click","payload":"cam living clicked","payloadType":"str","topic":"camera_living"}],"smilAnimations":[],"bindings":[{"selector":"#e26_texte","bindSource":"payload.title","bindType":"text","attribute":""}],"showCoordinates":false,"autoFormatAfterEdit":false,"showBrowserErrors":false,"selectorAsElementId":false,"outputField":"","editorUrl":"https://drawsvg.org/drawsvg.html","directory":"","panning":"both","zooming":"enabled","panOnlyWhenZoomed":false,"doubleClickZoomEnabled":true,"mouseWheelZoomEnabled":true,"name":"","x":1940,"y":380,"wires":[["a289199.a1714e8"]]},{"id":"91163354.31b2f","type":"inject","z":"4ae15451.7b2f5c","name":"Zoom in","topic":"","payload":"{\"command\":\"zoom_in\"}","payloadType":"json","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":1480,"y":260,"wires":[["32dfda30.706666"]]},{"id":"8c7c165f.dee1a8","type":"inject","z":"4ae15451.7b2f5c","name":"Zoom out","topic":"","payload":"{\"command\":\"zoom_out\"}","payloadType":"json","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":1480,"y":300,"wires":[["32dfda30.706666"]]},{"id":"d984058a.02d818","type":"inject","z":"4ae15451.7b2f5c","name":"Zoom 200%","topic":"","payload":"{\"command\":\"zoom_by_percentage\",\"percentage\":200}","payloadType":"json","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":1490,"y":340,"wires":[["32dfda30.706666"]]},{"id":"62277e70.5c676","type":"inject","z":"4ae15451.7b2f5c","name":"Zoom point (x=300, y=300) 200%","topic":"","payload":"{\"command\":\"zoom_by_percentage\",\"percentage\":200,\"x\":300,\"y\":300}","payloadType":"json","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":1550,"y":380,"wires":[["32dfda30.706666"]]},{"id":"365d5483.fa174c","type":"inject","z":"4ae15451.7b2f5c","name":"Pan to point (x=200 / y= 100)","topic":"","payload":"{\"command\":\"pan_to_point\",\"x\":200,\"y\":100}","payloadType":"json","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":1540,"y":420,"wires":[["32dfda30.706666"]]},{"id":"4d1e70dd.f51f3","type":"inject","z":"4ae15451.7b2f5c","name":"Pan to direction (x=200, y= 100)","topic":"","payload":"{\"command\":\"pan_to_direction\",\"x\":200,\"y\":100}","payloadType":"json","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":1550,"y":460,"wires":[["32dfda30.706666"]]},{"id":"79ebf8c1.adadd8","type":"inject","z":"4ae15451.7b2f5c","name":"Reset","topic":"","payload":"{\"command\":\"reset_panzoom\"}","payloadType":"json","repeat":"","crontab":"","once":false,"onceDelay":0.1,"showConfirmation":false,"confirmationLabel":"","x":1470,"y":220,"wires":[["32dfda30.706666"]]},{"id":"61be354.f7175cc","type":"ui_button","z":"4ae15451.7b2f5c","name":"Reset","group":"8d1b9121.83b3c","order":4,"width":"3","height":"1","passthru":false,"label":"Reset","tooltip":"","color":"","bgcolor":"","icon":"","payload":"{\"command\":\"reset_panzoom\"}","payloadType":"json","topic":"","x":1730,"y":140,"wires":[["32dfda30.706666"]]},{"id":"d43dfb27.a0f308","type":"ui_button","z":"4ae15451.7b2f5c","name":"Zoom in","group":"8d1b9121.83b3c","order":4,"width":"3","height":"1","passthru":false,"label":"Zoom in","tooltip":"","color":"","bgcolor":"","icon":"","payload":"{\"command\":\"zoom_in\"}","payloadType":"json","topic":"","x":1740,"y":180,"wires":[["32dfda30.706666"]]},{"id":"ef78f0f5.c44b3","type":"ui_button","z":"4ae15451.7b2f5c","name":"Zoom out","group":"8d1b9121.83b3c","order":4,"width":"3","height":"1","passthru":false,"label":"Zoom out","tooltip":"","color":"","bgcolor":"","icon":"","payload":"{\"command\":\"zoom_out\"}","payloadType":"json","topic":"","x":1740,"y":220,"wires":[["32dfda30.706666"]]},{"id":"8d1b9121.83b3c","type":"ui_group","z":"","name":"Pan/zoom test","tab":"5021fcf2.ee7ac4","order":1,"disp":true,"width":"14","collapse":false},{"id":"5021fcf2.ee7ac4","type":"ui_tab","z":"","name":"SVG","icon":"dashboard","disabled":false,"hidden":false}] 2 | -------------------------------------------------------------------------------- /examples/partial_filled_shape.json: -------------------------------------------------------------------------------- 1 | [{"id":"98d8ef2e2cd646d7","type":"ui_svg_graphics","z":"8fee6c7e9e28fe94","group":"f014eb03.a3c618","order":1,"width":"14","height":"10","svgString":"\n \n \n \n \n \n \n \n \n \n","clickableShapes":[{"targetId":"#temp_living","action":"change","payload":"temperature_living","payloadType":"str","topic":"temperature_living"}],"javascriptHandlers":[{"selector":"#temp_living","action":"change","sourceCode":"if(evt.target.valueAsNumber < 10) {\n $(\"#temp_living_label\")[0].style.color=\"blue\";\n}\nelse if(evt.target.valueAsNumber > 20) {\n $(\"#temp_living_label\")[0].style.color=\"red\";\n}\nelse {\n $(\"#temp_living_label\")[0].style.color=\"orange\";\n}"}],"smilAnimations":[],"bindings":[],"showCoordinates":false,"autoFormatAfterEdit":false,"showBrowserErrors":true,"showBrowserEvents":true,"enableJsDebugging":true,"sendMsgWhenLoaded":false,"noClickWhenDblClick":false,"outputField":"","editorUrl":"http://drawsvg.org/drawsvg.html","directory":"","panning":"disabled","zooming":"disabled","panOnlyWhenZoomed":false,"doubleClickZoomEnabled":false,"mouseWheelZoomEnabled":false,"dblClickZoomPercentage":"150","cssString":"div.ui-svg svg{\n color: var(--nr-dashboard-widgetColor);\n fill: currentColor !important;\n}\ndiv.ui-svg path {\n fill: inherit;\n}","name":"","x":700,"y":320,"wires":[[]]},{"id":"3b736443b32a9749","type":"inject","z":"8fee6c7e9e28fe94","name":"Value 0","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"0","payloadType":"num","x":250,"y":320,"wires":[["b2ae01c2ad154562"]]},{"id":"b2ae01c2ad154562","type":"function","z":"8fee6c7e9e28fe94","name":"Change gradient offset","func":"msg.payload = [\n {\n command: \"set_attribute\",\n selector: \"#circle_offset_1\",\n attributeName: \"offset\",\n attributeValue: msg.payload + \"%\"\n },\n {\n command: \"set_attribute\",\n selector: \"#circle_offset_2\",\n attributeName: \"offset\",\n attributeValue: msg.payload + \"%\"\n }\n]\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":480,"y":320,"wires":[["98d8ef2e2cd646d7"]]},{"id":"2b9ecbd548389816","type":"inject","z":"8fee6c7e9e28fe94","name":"Value 25","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"25","payloadType":"num","x":260,"y":360,"wires":[["b2ae01c2ad154562"]]},{"id":"41b7ebbd718019ab","type":"inject","z":"8fee6c7e9e28fe94","name":"Value 50","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"50","payloadType":"num","x":260,"y":400,"wires":[["b2ae01c2ad154562"]]},{"id":"aea6bf6a34253e3c","type":"inject","z":"8fee6c7e9e28fe94","name":"Value 75","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"75","payloadType":"num","x":260,"y":440,"wires":[["b2ae01c2ad154562"]]},{"id":"54e2adbe3704e9c3","type":"inject","z":"8fee6c7e9e28fe94","name":"Value 100","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"100","payloadType":"num","x":260,"y":480,"wires":[["b2ae01c2ad154562"]]},{"id":"f014eb03.a3c618","type":"ui_group","name":"Partial fill demo","tab":"80068970.6e2868","order":1,"disp":true,"width":"18","collapse":false,"className":""},{"id":"80068970.6e2868","type":"ui_tab","name":"SVG","icon":"dashboard","order":14,"disabled":false,"hidden":false}] 2 | -------------------------------------------------------------------------------- /examples/replace_svg.json: -------------------------------------------------------------------------------- 1 | [{"id":"16a953b2.21beec","type":"ui_svg_graphics","z":"5598090d.febad8","group":"925439b0.2863c8","order":0,"width":"3","height":"3","svgString":"\n\n","clickableShapes":[{"targetId":"#myShape","action":"click","payload":"myShape clicked","payloadType":"str","topic":"#myShape"}],"smilAnimations":[],"bindings":[],"showCoordinates":false,"autoFormatAfterEdit":false,"showBrowserErrors":true,"outputField":"payload","editorUrl":"//drawsvg.org/drawsvg.html","directory":"","panning":"both","zooming":"disabled","panOnlyWhenZoomed":false,"doubleClickZoomEnabled":false,"mouseWheelZoomEnabled":false,"name":"svg-graphics","x":590,"y":260,"wires":[["4672821a.2f296c"]]},{"id":"e105ccfb.c2849","type":"inject","z":"5598090d.febad8","name":"SVG with yellow circle","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"{\"command\":\"replace_svg\",\"svg\":\"\"}","payloadType":"json","x":380,"y":260,"wires":[["16a953b2.21beec"]]},{"id":"b232e2bf.52efb","type":"inject","z":"5598090d.febad8","name":"SVG with blue rectangle","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"{\"command\":\"replace_svg\",\"svg\":\"\"}","payloadType":"json","x":380,"y":320,"wires":[["16a953b2.21beec"]]},{"id":"5f208d4d.047af4","type":"inject","z":"5598090d.febad8","name":"SVG with pink ellipse","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"{\"command\":\"replace_svg\",\"svg\":\"\"}","payloadType":"json","x":380,"y":380,"wires":[["16a953b2.21beec"]]},{"id":"4672821a.2f296c","type":"debug","z":"5598090d.febad8","name":"log events","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":770,"y":260,"wires":[]},{"id":"925439b0.2863c8","type":"ui_group","z":"","name":"svg-panning-test","tab":"5c613937.fe7368","order":4,"disp":true,"width":"3","collapse":false},{"id":"5c613937.fe7368","type":"ui_tab","z":"","name":"Home","icon":"home","order":1,"disabled":false,"hidden":false}] 2 | -------------------------------------------------------------------------------- /examples/trigger_animations_via_msg.json: -------------------------------------------------------------------------------- 1 | [{"id":"c997135f.8035f","type":"debug","z":"f939feb8.8dc6","name":"Floorplan output","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","x":520,"y":220,"wires":[]},{"id":"bb93fff5.927ba","type":"ui_svg_graphics","z":"f939feb8.8dc6","group":"997e40da.b5acc","order":1,"width":"14","height":"10","svgString":"\n \n This is the #banner\n \n \n ","clickableShapes":[{"targetId":"#camera_living","action":"click","payload":"camera_living","payloadType":"str","topic":"camera_living"}],"smilAnimations":[{"id":"myAnimation","targetId":"pir_living","classValue":"all_animation","attributeName":"r","fromValue":"1","toValue":"30","trigger":"cust","duration":"500","durationUnit":"ms","repeatCount":"5","end":"restore","delay":"1","delayUnit":"s","custom":"camera_living.click; "},{"id":"textRotate","targetId":"banner","classValue":"all_animation","attributeName":"rotate","fromValue":"0","toValue":"360","trigger":"msg","duration":"750","durationUnit":"ms","repeatCount":"3","end":"restore","delay":"1","delayUnit":"s","custom":""}],"bindings":[{"selector":"#banner","bindSource":"payload.title","bindType":"text","attribute":""},{"selector":"#camera_living","bindSource":"payload.position.x","bindType":"attr","attribute":"x"},{"selector":"#camera_living","bindSource":"payload.camera.colour","bindType":"attr","attribute":"fill"}],"showCoordinates":false,"autoFormatAfterEdit":false,"outputField":"","editorUrl":"","directory":"","name":"","x":420,"y":180,"wires":[["c997135f.8035f"]]},{"id":"356e2a8f.a08fe6","type":"inject","z":"f939feb8.8dc6","name":"databind","topic":"databind","payload":"{\"camera\":{\"colour\":\"yellow\"},\"position\":{\"x\":320},\"title\":\"databind strikes again\"}","payloadType":"json","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":240,"y":260,"wires":[["bb93fff5.927ba"]]},{"id":"4e2e2d82.5950e4","type":"inject","z":"f939feb8.8dc6","name":"databind","topic":"databind","payload":"{\"camera\":{\"colour\":\"green\"},\"position\":{\"x\":250},\"title\":\"New banner title by databind\"}","payloadType":"json","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":240,"y":220,"wires":[["bb93fff5.927ba"]]},{"id":"97b80c2d.b5c35","type":"inject","z":"f939feb8.8dc6","name":"Fill camera green + rotate 90","topic":"update_attribute","payload":"[{\"command\":\"update_attribute\",\"selector\":\"#camera_living\",\"attributeName\":\"fill\",\"attributeValue\":\"green\"},{\"command\":\"set_attribute\",\"selector\":\"#camera_living\",\"attributeName\":\"rotate\",\"attributeValue\":\"90\"}]","payloadType":"json","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":300,"y":360,"wires":[["bb93fff5.927ba"]]},{"id":"4be0130f.78b13c","type":"inject","z":"f939feb8.8dc6","name":"Fill camera orange + rotate 180","topic":"update_attribute","payload":"[{\"command\":\"update_attribute\",\"selector\":\"#camera_living\",\"attributeName\":\"fill\",\"attributeValue\":\"orange\"},{\"command\":\"set_attribute\",\"selector\":\"#camera_living\",\"attributeName\":\"rotate\",\"attributeValue\":\"180\"}]","payloadType":"json","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":310,"y":400,"wires":[["bb93fff5.927ba"]]},{"id":"46128135.4fcdd","type":"inject","z":"f939feb8.8dc6","name":"Fill camera icon blue","topic":"update_attribute","payload":"[{\"command\":\"update_attribute\",\"selector\":\"#camera_living\",\"attributeName\":\"fill\",\"attributeValue\":\"blue\"},{\"command\":\"set_attribute\",\"selector\":\"#camera_living\",\"attributeName\":\"rotate\",\"attributeValue\":\"0\"}]","payloadType":"json","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":270,"y":320,"wires":[["bb93fff5.927ba"]]},{"id":"735182fa.b0c50c","type":"inject","z":"f939feb8.8dc6","name":"Start animation","topic":"trigger_animation","payload":"[{\"command\":\"trigger_animation\",\"selector\":\".all_animation\",\"action\":\"start\"}]","payloadType":"json","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":240,"y":60,"wires":[["bb93fff5.927ba"]]},{"id":"7b608d5b.d892a4","type":"inject","z":"f939feb8.8dc6","name":"Stop animation","topic":"","payload":"[{\"command\":\"trigger_animation\",\"selector\":\"#myAnimation\",\"action\":\"stop\"},{\"command\":\"trigger_animation\",\"selector\":\"#textRotate\",\"action\":\"stop\"}]","payloadType":"json","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":240,"y":100,"wires":[["bb93fff5.927ba"]]},{"id":"997e40da.b5acc","type":"ui_group","z":"","name":"Floorplan test","tab":"95801a22.bd5f18","disp":true,"width":"14","collapse":false},{"id":"95801a22.bd5f18","type":"ui_tab","z":"","name":"SVG","icon":"dashboard","disabled":false,"hidden":false}] 2 | -------------------------------------------------------------------------------- /examples/use_databinding.json: -------------------------------------------------------------------------------- 1 | [{"id":"8f0e54d7.4d1668","type":"ui_svg_graphics","z":"d9a54719.b13a88","group":"8d3148e0.0eee88","order":1,"width":"14","height":"10","svgString":"\n \n \n \n ","clickableShapes":[{"targetId":"#cam_living_room","action":"click","payload":"camera_living","payloadType":"str","topic":"camera_living"}],"smilAnimations":[],"bindings":[{"selector":"#cam_living_room","bindSource":"payload.color","bindType":"attr","attribute":"fill"}],"showCoordinates":false,"autoFormatAfterEdit":false,"outputField":"","editorUrl":"http://drawsvg.org/drawsvg.html","directory":"","name":"","x":560,"y":460,"wires":[[]]},{"id":"a5398780.b8dc68","type":"inject","z":"d9a54719.b13a88","name":"set fill attribute to yellow","topic":"databind","payload":"{\"color\":\"yellow\"}","payloadType":"json","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":300,"y":460,"wires":[["8f0e54d7.4d1668"]]},{"id":"f3033699.baf7b8","type":"inject","z":"d9a54719.b13a88","name":"set fill attribute to pink","topic":"databind","payload":"{\"color\":\"pink\"}","payloadType":"json","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":300,"y":500,"wires":[["8f0e54d7.4d1668"]]},{"id":"8d3148e0.0eee88","type":"ui_group","z":"","name":"Floorplan test","tab":"93e19e4f.b80ed","disp":true,"width":"14","collapse":false},{"id":"93e19e4f.b80ed","type":"ui_tab","z":"","name":"SVG","icon":"dashboard","disabled":false,"hidden":false}] 2 | -------------------------------------------------------------------------------- /icons/svg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bartbutenaers/node-red-contrib-ui-svg/e11ded0ef31994e8d49afacec8841d07cb8229fa/icons/svg.png -------------------------------------------------------------------------------- /lib/jschannel.js: -------------------------------------------------------------------------------- 1 | /* 2 | * js_channel is a very lightweight abstraction on top of 3 | * postMessage which defines message formats and semantics 4 | * to support interactions more rich than just message passing 5 | * js_channel supports: 6 | * + query/response - traditional rpc 7 | * + query/update/response - incremental async return of results 8 | * to a query 9 | * + notifications - fire and forget 10 | * + error handling 11 | * 12 | * js_channel is based heavily on json-rpc, but is focused at the 13 | * problem of inter-iframe RPC. 14 | * 15 | * Message types: 16 | * There are 5 types of messages that can flow over this channel, 17 | * and you may determine what type of message an object is by 18 | * examining its parameters: 19 | * 1. Requests 20 | * + integer id 21 | * + string method 22 | * + (optional) any params 23 | * 2. Callback Invocations (or just "Callbacks") 24 | * + integer id 25 | * + string callback 26 | * + (optional) params 27 | * 3. Error Responses (or just "Errors) 28 | * + integer id 29 | * + string error 30 | * + (optional) string message 31 | * 4. Responses 32 | * + integer id 33 | * + (optional) any result 34 | * 5. Notifications 35 | * + string method 36 | * + (optional) any params 37 | */ 38 | 39 | ;var Channel = (function() { 40 | "use strict"; 41 | 42 | // current transaction id, start out at a random *odd* number between 1 and a million 43 | // There is one current transaction counter id per page, and it's shared between 44 | // channel instances. That means of all messages posted from a single javascript 45 | // evaluation context, we'll never have two with the same id. 46 | var s_curTranId = Math.floor(Math.random()*1000001); 47 | 48 | // no two bound channels in the same javascript evaluation context may have the same origin, scope, and window. 49 | // futher if two bound channels have the same window and scope, they may not have *overlapping* origins 50 | // (either one or both support '*'). This restriction allows a single onMessage handler to efficiently 51 | // route messages based on origin and scope. The s_boundChans maps origins to scopes, to message 52 | // handlers. Request and Notification messages are routed using this table. 53 | // Finally, channels are inserted into this table when built, and removed when destroyed. 54 | var s_boundChans = { }; 55 | 56 | // add a channel to s_boundChans, throwing if a dup exists 57 | function s_addBoundChan(win, origin, scope, handler) { 58 | function hasWin(arr) { 59 | for (var i = 0; i < arr.length; i++) if (arr[i].win === win) return true; 60 | return false; 61 | } 62 | 63 | // does she exist? 64 | var exists = false; 65 | 66 | 67 | if (origin === '*') { 68 | // we must check all other origins, sadly. 69 | for (var k in s_boundChans) { 70 | if (!s_boundChans.hasOwnProperty(k)) continue; 71 | if (k === '*') continue; 72 | if (typeof s_boundChans[k][scope] === 'object') { 73 | exists = hasWin(s_boundChans[k][scope]); 74 | if (exists) break; 75 | } 76 | } 77 | } else { 78 | // we must check only '*' 79 | if ((s_boundChans['*'] && s_boundChans['*'][scope])) { 80 | exists = hasWin(s_boundChans['*'][scope]); 81 | } 82 | if (!exists && s_boundChans[origin] && s_boundChans[origin][scope]) 83 | { 84 | exists = hasWin(s_boundChans[origin][scope]); 85 | } 86 | } 87 | if (exists) throw "A channel is already bound to the same window which overlaps with origin '"+ origin +"' and has scope '"+scope+"'"; 88 | 89 | if (typeof s_boundChans[origin] != 'object') s_boundChans[origin] = { }; 90 | if (typeof s_boundChans[origin][scope] != 'object') s_boundChans[origin][scope] = [ ]; 91 | s_boundChans[origin][scope].push({win: win, handler: handler}); 92 | } 93 | 94 | function s_removeBoundChan(win, origin, scope) { 95 | var arr = s_boundChans[origin][scope]; 96 | for (var i = 0; i < arr.length; i++) { 97 | if (arr[i].win === win) { 98 | arr.splice(i,1); 99 | } 100 | } 101 | if (s_boundChans[origin][scope].length === 0) { 102 | delete s_boundChans[origin][scope]; 103 | } 104 | } 105 | 106 | function s_isArray(obj) { 107 | if (Array.isArray) return Array.isArray(obj); 108 | else { 109 | return (obj.constructor.toString().indexOf("Array") != -1); 110 | } 111 | } 112 | 113 | // No two outstanding outbound messages may have the same id, period. Given that, a single table 114 | // mapping "transaction ids" to message handlers, allows efficient routing of Callback, Error, and 115 | // Response messages. Entries are added to this table when requests are sent, and removed when 116 | // responses are received. 117 | var s_transIds = { }; 118 | 119 | // class singleton onMessage handler 120 | // this function is registered once and all incoming messages route through here. This 121 | // arrangement allows certain efficiencies, message data is only parsed once and dispatch 122 | // is more efficient, especially for large numbers of simultaneous channels. 123 | var s_onMessage = function(e) { 124 | try { 125 | var m = JSON.parse(e.data); 126 | if (typeof m !== 'object' || m === null) throw "malformed"; 127 | } catch(e) { 128 | // just ignore any posted messages that do not consist of valid JSON 129 | return; 130 | } 131 | 132 | var w = e.source; 133 | var o = e.origin; 134 | var s, i, meth; 135 | 136 | if (typeof m.method === 'string') { 137 | var ar = m.method.split('::'); 138 | if (ar.length == 2) { 139 | s = ar[0]; 140 | meth = ar[1]; 141 | } else { 142 | meth = m.method; 143 | } 144 | } 145 | 146 | if (typeof m.id !== 'undefined') i = m.id; 147 | 148 | // w is message source window 149 | // o is message origin 150 | // m is parsed message 151 | // s is message scope 152 | // i is message id (or undefined) 153 | // meth is unscoped method name 154 | // ^^ based on these factors we can route the message 155 | 156 | // if it has a method it's either a notification or a request, 157 | // route using s_boundChans 158 | if (typeof meth === 'string') { 159 | var delivered = false; 160 | if (s_boundChans[o] && s_boundChans[o][s]) { 161 | for (var j = 0; j < s_boundChans[o][s].length; j++) { 162 | if (s_boundChans[o][s][j].win === w) { 163 | s_boundChans[o][s][j].handler(o, meth, m); 164 | delivered = true; 165 | break; 166 | } 167 | } 168 | } 169 | 170 | if (!delivered && s_boundChans['*'] && s_boundChans['*'][s]) { 171 | for (var j = 0; j < s_boundChans['*'][s].length; j++) { 172 | if (s_boundChans['*'][s][j].win === w) { 173 | s_boundChans['*'][s][j].handler(o, meth, m); 174 | break; 175 | } 176 | } 177 | } 178 | } 179 | // otherwise it must have an id (or be poorly formed 180 | else if (typeof i != 'undefined') { 181 | if (s_transIds[i]) s_transIds[i](o, meth, m); 182 | } 183 | }; 184 | 185 | // Setup postMessage event listeners 186 | if (window.addEventListener) window.addEventListener('message', s_onMessage, false); 187 | else if(window.attachEvent) window.attachEvent('onmessage', s_onMessage); 188 | 189 | /* a messaging channel is constructed from a window and an origin. 190 | * the channel will assert that all messages received over the 191 | * channel match the origin 192 | * 193 | * Arguments to Channel.build(cfg): 194 | * 195 | * cfg.window - the remote window with which we'll communicate 196 | * cfg.origin - the expected origin of the remote window, may be '*' 197 | * which matches any origin 198 | * cfg.scope - the 'scope' of messages. a scope string that is 199 | * prepended to message names. local and remote endpoints 200 | * of a single channel must agree upon scope. Scope may 201 | * not contain double colons ('::'). 202 | * cfg.debugOutput - A boolean value. If true and window.console.log is 203 | * a function, then debug strings will be emitted to that 204 | * function. 205 | * cfg.debugOutput - A boolean value. If true and window.console.log is 206 | * a function, then debug strings will be emitted to that 207 | * function. 208 | * cfg.postMessageObserver - A function that will be passed two arguments, 209 | * an origin and a message. It will be passed these immediately 210 | * before messages are posted. 211 | * cfg.gotMessageObserver - A function that will be passed two arguments, 212 | * an origin and a message. It will be passed these arguments 213 | * immediately after they pass scope and origin checks, but before 214 | * they are processed. 215 | * cfg.onReady - A function that will be invoked when a channel becomes "ready", 216 | * this occurs once both sides of the channel have been 217 | * instantiated and an application level handshake is exchanged. 218 | * the onReady function will be passed a single argument which is 219 | * the channel object that was returned from build(). 220 | */ 221 | return { 222 | build: function(cfg) { 223 | var debug = function(m) { 224 | if (cfg.debugOutput && window.console && window.console.log) { 225 | // try to stringify, if it doesn't work we'll let javascript's built in toString do its magic 226 | try { if (typeof m !== 'string') m = JSON.stringify(m); } catch(e) { } 227 | console.log("["+chanId+"] " + m); 228 | } 229 | }; 230 | 231 | /* browser capabilities check */ 232 | if (!window.postMessage) throw("jschannel cannot run this browser, no postMessage"); 233 | if (!window.JSON || !window.JSON.stringify || ! window.JSON.parse) { 234 | throw("jschannel cannot run this browser, no JSON parsing/serialization"); 235 | } 236 | 237 | /* basic argument validation */ 238 | if (typeof cfg != 'object') throw("Channel build invoked without a proper object argument"); 239 | 240 | if (!cfg.window || !cfg.window.postMessage) throw("Channel.build() called without a valid window argument"); 241 | 242 | /* we'd have to do a little more work to be able to run multiple channels that intercommunicate the same 243 | * window... Not sure if we care to support that */ 244 | if (window === cfg.window) throw("target window is same as present window -- not allowed"); 245 | 246 | // let's require that the client specify an origin. if we just assume '*' we'll be 247 | // propagating unsafe practices. that would be lame. 248 | var validOrigin = false; 249 | if (typeof cfg.origin === 'string') { 250 | var oMatch; 251 | if (cfg.origin === "*") validOrigin = true; 252 | // allow valid domains under http and https. Also, trim paths off otherwise valid origins. 253 | else if (null !== (oMatch = cfg.origin.match(/^https?:\/\/(?:[-a-zA-Z0-9_\.])+(?::\d+)?/))) { 254 | cfg.origin = oMatch[0].toLowerCase(); 255 | validOrigin = true; 256 | } 257 | } 258 | 259 | if (!validOrigin) throw ("Channel.build() called with an invalid origin"); 260 | 261 | if (typeof cfg.scope !== 'undefined') { 262 | if (typeof cfg.scope !== 'string') throw 'scope, when specified, must be a string'; 263 | if (cfg.scope.split('::').length > 1) throw "scope may not contain double colons: '::'"; 264 | } 265 | 266 | /* private variables */ 267 | // generate a random and psuedo unique id for this channel 268 | var chanId = (function () { 269 | var text = ""; 270 | var alpha = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; 271 | for(var i=0; i < 5; i++) text += alpha.charAt(Math.floor(Math.random() * alpha.length)); 272 | return text; 273 | })(); 274 | 275 | // registrations: mapping method names to call objects 276 | var regTbl = { }; 277 | // current oustanding sent requests 278 | var outTbl = { }; 279 | // current oustanding received requests 280 | var inTbl = { }; 281 | // are we ready yet? when false we will block outbound messages. 282 | var ready = false; 283 | var pendingQueue = [ ]; 284 | 285 | var createTransaction = function(id,origin,callbacks) { 286 | var shouldDelayReturn = false; 287 | var completed = false; 288 | 289 | return { 290 | origin: origin, 291 | invoke: function(cbName, v) { 292 | // verify in table 293 | if (!inTbl[id]) throw "attempting to invoke a callback of a nonexistent transaction: " + id; 294 | // verify that the callback name is valid 295 | var valid = false; 296 | for (var i = 0; i < callbacks.length; i++) if (cbName === callbacks[i]) { valid = true; break; } 297 | if (!valid) throw "request supports no such callback '" + cbName + "'"; 298 | 299 | // send callback invocation 300 | postMessage({ id: id, callback: cbName, params: v}); 301 | }, 302 | error: function(error, message) { 303 | completed = true; 304 | // verify in table 305 | if (!inTbl[id]) throw "error called for nonexistent message: " + id; 306 | 307 | // remove transaction from table 308 | delete inTbl[id]; 309 | 310 | // send error 311 | postMessage({ id: id, error: error, message: message }); 312 | }, 313 | complete: function(v) { 314 | completed = true; 315 | // verify in table 316 | if (!inTbl[id]) throw "complete called for nonexistent message: " + id; 317 | // remove transaction from table 318 | delete inTbl[id]; 319 | // send complete 320 | postMessage({ id: id, result: v }); 321 | }, 322 | delayReturn: function(delay) { 323 | if (typeof delay === 'boolean') { 324 | shouldDelayReturn = (delay === true); 325 | } 326 | return shouldDelayReturn; 327 | }, 328 | completed: function() { 329 | return completed; 330 | } 331 | }; 332 | }; 333 | 334 | var setTransactionTimeout = function(transId, timeout, method) { 335 | return window.setTimeout(function() { 336 | if (outTbl[transId]) { 337 | // XXX: what if client code raises an exception here? 338 | var msg = "timeout (" + timeout + "ms) exceeded on method '" + method + "'"; 339 | (1,outTbl[transId].error)("timeout_error", msg); 340 | delete outTbl[transId]; 341 | delete s_transIds[transId]; 342 | } 343 | }, timeout); 344 | }; 345 | 346 | var onMessage = function(origin, method, m) { 347 | // if an observer was specified at allocation time, invoke it 348 | if (typeof cfg.gotMessageObserver === 'function') { 349 | // pass observer a clone of the object so that our 350 | // manipulations are not visible (i.e. method unscoping). 351 | // This is not particularly efficient, but then we expect 352 | // that message observers are primarily for debugging anyway. 353 | try { 354 | cfg.gotMessageObserver(origin, m); 355 | } catch (e) { 356 | debug("gotMessageObserver() raised an exception: " + e.toString()); 357 | } 358 | } 359 | 360 | // now, what type of message is this? 361 | if (m.id && method) { 362 | // a request! do we have a registered handler for this request? 363 | if (regTbl[method]) { 364 | var trans = createTransaction(m.id, origin, m.callbacks ? m.callbacks : [ ]); 365 | inTbl[m.id] = { }; 366 | try { 367 | // callback handling. we'll magically create functions inside the parameter list for each 368 | // callback 369 | if (m.callbacks && s_isArray(m.callbacks) && m.callbacks.length > 0) { 370 | for (var i = 0; i < m.callbacks.length; i++) { 371 | var path = m.callbacks[i]; 372 | var obj = m.params; 373 | var pathItems = path.split('/'); 374 | for (var j = 0; j < pathItems.length - 1; j++) { 375 | var cp = pathItems[j]; 376 | if (typeof obj[cp] !== 'object') obj[cp] = { }; 377 | obj = obj[cp]; 378 | } 379 | obj[pathItems[pathItems.length - 1]] = (function() { 380 | var cbName = path; 381 | return function(params) { 382 | return trans.invoke(cbName, params); 383 | }; 384 | })(); 385 | } 386 | } 387 | var resp = regTbl[method](trans, m.params); 388 | if (!trans.delayReturn() && !trans.completed()) trans.complete(resp); 389 | } catch(e) { 390 | // automagic handling of exceptions: 391 | var error = "runtime_error"; 392 | var message = null; 393 | // * if it's a string then it gets an error code of 'runtime_error' and string is the message 394 | if (typeof e === 'string') { 395 | message = e; 396 | } else if (typeof e === 'object') { 397 | // either an array or an object 398 | // * if it's an array of length two, then array[0] is the code, array[1] is the error message 399 | if (e && s_isArray(e) && e.length == 2) { 400 | error = e[0]; 401 | message = e[1]; 402 | } 403 | // * if it's an object then we'll look form error and message parameters 404 | else if (typeof e.error === 'string') { 405 | error = e.error; 406 | if (!e.message) message = ""; 407 | else if (typeof e.message === 'string') message = e.message; 408 | else e = e.message; // let the stringify/toString message give us a reasonable verbose error string 409 | } 410 | } 411 | 412 | // message is *still* null, let's try harder 413 | if (message === null) { 414 | try { 415 | message = JSON.stringify(e); 416 | /* On MSIE8, this can result in 'out of memory', which 417 | * leaves message undefined. */ 418 | if (typeof(message) == 'undefined') 419 | message = e.toString(); 420 | } catch (e2) { 421 | message = e.toString(); 422 | } 423 | } 424 | 425 | trans.error(error,message); 426 | } 427 | } 428 | } else if (m.id && m.callback) { 429 | if (!outTbl[m.id] ||!outTbl[m.id].callbacks || !outTbl[m.id].callbacks[m.callback]) 430 | { 431 | debug("ignoring invalid callback, id:"+m.id+ " (" + m.callback +")"); 432 | } else { 433 | // XXX: what if client code raises an exception here? 434 | outTbl[m.id].callbacks[m.callback](m.params); 435 | } 436 | } else if (m.id) { 437 | if (!outTbl[m.id]) { 438 | debug("ignoring invalid response: " + m.id); 439 | } else { 440 | // XXX: what if client code raises an exception here? 441 | if (m.error) { 442 | (1,outTbl[m.id].error)(m.error, m.message); 443 | } else { 444 | if (m.result !== undefined) (1,outTbl[m.id].success)(m.result); 445 | else (1,outTbl[m.id].success)(); 446 | } 447 | delete outTbl[m.id]; 448 | delete s_transIds[m.id]; 449 | } 450 | } else if (method) { 451 | // tis a notification. 452 | if (regTbl[method]) { 453 | // yep, there's a handler for that. 454 | // transaction has only origin for notifications. 455 | regTbl[method]({ origin: origin }, m.params); 456 | // if the client throws, we'll just let it bubble out 457 | // what can we do? Also, here we'll ignore return values 458 | } 459 | } 460 | }; 461 | 462 | // now register our bound channel for msg routing 463 | s_addBoundChan(cfg.window, cfg.origin, ((typeof cfg.scope === 'string') ? cfg.scope : ''), onMessage); 464 | 465 | // scope method names based on cfg.scope specified when the Channel was instantiated 466 | var scopeMethod = function(m) { 467 | if (typeof cfg.scope === 'string' && cfg.scope.length) m = [cfg.scope, m].join("::"); 468 | return m; 469 | }; 470 | 471 | // a small wrapper around postmessage whose primary function is to handle the 472 | // case that clients start sending messages before the other end is "ready" 473 | var postMessage = function(msg, force) { 474 | if (!msg) throw "postMessage called with null message"; 475 | 476 | // delay posting if we're not ready yet. 477 | var verb = (ready ? "post " : "queue "); 478 | debug(verb + " message: " + JSON.stringify(msg)); 479 | if (!force && !ready) { 480 | pendingQueue.push(msg); 481 | } else { 482 | if (typeof cfg.postMessageObserver === 'function') { 483 | try { 484 | cfg.postMessageObserver(cfg.origin, msg); 485 | } catch (e) { 486 | debug("postMessageObserver() raised an exception: " + e.toString()); 487 | } 488 | } 489 | 490 | cfg.window.postMessage(JSON.stringify(msg), cfg.origin); 491 | } 492 | }; 493 | 494 | var onReady = function(trans, type) { 495 | debug('ready msg received'); 496 | if (ready) throw "received ready message while in ready state. help!"; 497 | 498 | if (type === 'ping') { 499 | chanId += '-R'; 500 | } else { 501 | chanId += '-L'; 502 | } 503 | 504 | obj.unbind('__ready'); // now this handler isn't needed any more. 505 | ready = true; 506 | debug('ready msg accepted.'); 507 | 508 | if (type === 'ping') { 509 | obj.notify({ method: '__ready', params: 'pong' }); 510 | } 511 | 512 | // flush queue 513 | while (pendingQueue.length) { 514 | postMessage(pendingQueue.pop()); 515 | } 516 | 517 | // invoke onReady observer if provided 518 | if (typeof cfg.onReady === 'function') cfg.onReady(obj); 519 | }; 520 | 521 | var obj = { 522 | // tries to unbind a bound message handler. returns false if not possible 523 | unbind: function (method) { 524 | if (regTbl[method]) { 525 | if (!(delete regTbl[method])) throw ("can't delete method: " + method); 526 | return true; 527 | } 528 | return false; 529 | }, 530 | bind: function (method, cb) { 531 | if (!method || typeof method !== 'string') throw "'method' argument to bind must be string"; 532 | if (!cb || typeof cb !== 'function') throw "callback missing from bind params"; 533 | 534 | if (regTbl[method]) throw "method '"+method+"' is already bound!"; 535 | regTbl[method] = cb; 536 | return this; 537 | }, 538 | call: function(m) { 539 | if (!m) throw 'missing arguments to call function'; 540 | if (!m.method || typeof m.method !== 'string') throw "'method' argument to call must be string"; 541 | if (!m.success || typeof m.success !== 'function') throw "'success' callback missing from call"; 542 | 543 | // now it's time to support the 'callback' feature of jschannel. We'll traverse the argument 544 | // object and pick out all of the functions that were passed as arguments. 545 | var callbacks = { }; 546 | var callbackNames = [ ]; 547 | var seen = [ ]; 548 | 549 | var pruneFunctions = function (path, obj) { 550 | if (seen.indexOf(obj) >= 0) { 551 | throw "params cannot be a recursive data structure" 552 | } 553 | seen.push(obj); 554 | 555 | if (typeof obj === 'object') { 556 | for (var k in obj) { 557 | if (!obj.hasOwnProperty(k)) continue; 558 | var np = path + (path.length ? '/' : '') + k; 559 | if (typeof obj[k] === 'function') { 560 | callbacks[np] = obj[k]; 561 | callbackNames.push(np); 562 | delete obj[k]; 563 | } else if (typeof obj[k] === 'object') { 564 | pruneFunctions(np, obj[k]); 565 | } 566 | } 567 | } 568 | }; 569 | pruneFunctions("", m.params); 570 | 571 | // build a 'request' message and send it 572 | var msg = { id: s_curTranId, method: scopeMethod(m.method), params: m.params }; 573 | if (callbackNames.length) msg.callbacks = callbackNames; 574 | 575 | if (m.timeout) 576 | // XXX: This function returns a timeout ID, but we don't do anything with it. 577 | // We might want to keep track of it so we can cancel it using clearTimeout() 578 | // when the transaction completes. 579 | setTransactionTimeout(s_curTranId, m.timeout, scopeMethod(m.method)); 580 | 581 | // insert into the transaction table 582 | outTbl[s_curTranId] = { callbacks: callbacks, error: m.error, success: m.success }; 583 | s_transIds[s_curTranId] = onMessage; 584 | 585 | // increment current id 586 | s_curTranId++; 587 | 588 | postMessage(msg); 589 | }, 590 | notify: function(m) { 591 | if (!m) throw 'missing arguments to notify function'; 592 | if (!m.method || typeof m.method !== 'string') throw "'method' argument to notify must be string"; 593 | 594 | // no need to go into any transaction table 595 | postMessage({ method: scopeMethod(m.method), params: m.params }); 596 | }, 597 | destroy: function () { 598 | s_removeBoundChan(cfg.window, cfg.origin, ((typeof cfg.scope === 'string') ? cfg.scope : '')); 599 | if (window.removeEventListener) window.removeEventListener('message', onMessage, false); 600 | else if(window.detachEvent) window.detachEvent('onmessage', onMessage); 601 | ready = false; 602 | regTbl = { }; 603 | inTbl = { }; 604 | outTbl = { }; 605 | cfg.origin = null; 606 | pendingQueue = [ ]; 607 | debug("channel destroyed"); 608 | chanId = ""; 609 | } 610 | }; 611 | 612 | obj.bind('__ready', onReady); 613 | setTimeout(function() { 614 | postMessage({ method: scopeMethod('__ready'), params: "ping" }, true); 615 | }, 0); 616 | 617 | return obj; 618 | } 619 | }; 620 | })(); 621 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-red-contrib-ui-svg", 3 | "version": "2.3.3", 4 | "description": "A Node-RED widget node to show interactive SVG (vector graphics) in the dashboard", 5 | "dependencies": { 6 | "mime": "^2.4.4", 7 | "@panzoom/panzoom": "^4.1.0", 8 | "hammerjs": "^2.0.8", 9 | "js-beautify": "^1.11.0", 10 | "postcss": "^8.3.9", 11 | "postcss-prefix-selector": "^1.13.0", 12 | "svgson": "^5.3.1" 13 | }, 14 | "author": { 15 | "name": "Bart Butenaers" 16 | }, 17 | "contributors": [ 18 | { 19 | "name": "Stephen McLaughlin", 20 | "url": "https://github.com/Steve-Mcl" 21 | } 22 | ], 23 | "license": "Apache-2.0", 24 | "keywords": [ 25 | "node-red", 26 | "svg", 27 | "graphics", 28 | "interactive", 29 | "drawing", 30 | "floorplan", 31 | "floor plan", 32 | "hmi" 33 | ], 34 | "bugs": { 35 | "url": "https://github.com/bartbutenaers/node-red-contrib-ui-svg/issues" 36 | }, 37 | "homepage": "https://github.com/bartbutenaers/node-red-contrib-ui-svg", 38 | "repository": { 39 | "type": "git", 40 | "url": "https://github.com/bartbutenaers/node-red-contrib-ui-svg.git" 41 | }, 42 | "node-red": { 43 | "nodes": { 44 | "ui_svg_graphics": "svg_graphics.js" 45 | } 46 | } 47 | } 48 | --------------------------------------------------------------------------------