├── .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 |
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 | 
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 | 
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":"","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 | 
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 | 
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 | 
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 | 
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 | 
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","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 | 
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 | 
244 |
--------------------------------------------------------------------------------
/docs/tabsheet_CSS.md:
--------------------------------------------------------------------------------
1 | # "CSS" tab sheet
2 |
3 | 
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 | 
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 | 
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 | 
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":"","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":"","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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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":" ","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 |
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 |
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 | 
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 | 
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 | 
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 | 
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":"","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 | 
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 | 
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 | 
25 | ```
26 | [{"id":"89244415.be9278","type":"ui_svg_graphics","z":"a03bd3cf.177578","group":"5ae1b679.de89c8","order":4,"width":"0","height":"0","svgString":"","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 | 
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 | 
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 | 
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 | 
63 |
64 | ```
65 | [{"id":"708b561a27277730","type":"ui_svg_graphics","z":"dd961d75822d1f62","group":"8d3148e0.0eee88","order":2,"width":"24","height":"14","svgString":"","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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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":"","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 | 
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 | 
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":"","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":" ","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":"","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":"","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","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":"","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":"","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":"","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":"","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":"","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":"","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":" ","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":" ","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 |
--------------------------------------------------------------------------------