Skip to content

Instantly share code, notes, and snippets.

@TotallyInformation
Last active May 10, 2023 19:49
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save TotallyInformation/02eb3716157db586f3f5b8a85c241009 to your computer and use it in GitHub Desktop.
Save TotallyInformation/02eb3716157db586f3f5b8a85c241009 to your computer and use it in GitHub Desktop.
uibuilder, VueJS and SVG: Quick floorplan IoT example

UPDATE March 2023: There is now a shiny new version of this example that uses vanilla HMTL and no longer needs a framework. Please check it out at uibuilder dynamic SVG example - no framework needed (flows.nodered.org).


This was inspired by Steve and Bart's SVG addon for Dashboard. It made me wonder how hard it would be to combine an SVG floorplan with SVG icons showing things like whether lights are on or off and even being able to control them.

Well, turns out that it is crazy simple!

ui

So I created a simple VueJS component which means that all you have to do is write a couple of lines of HTML & a few lines of JavaScript then you have a fully dynamic visual floorplan with IoT overlays.

The flow to drive this is also trivial.

flow

The flow seems rather long because of the embedded SVG data, be patient when pasting it. Note that the html and js files seem a bit long as well but that is only because the component is embedded, normally that would be in a separate file.

The flow contains the html and JavaScript as well as the SVG floorplan used for the background. Just copy the content of the SVG to a file called background.svg then copy/paste the html and JavaScript to the appropriate files.

Display the source blob
Display the rendered blob
Raw
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
[
{
"id": "1678f3c7ab967e39",
"type": "group",
"z": "278e7d113ed09433",
"style": {
"stroke": "#999999",
"stroke-opacity": "1",
"fill": "none",
"fill-opacity": "1",
"label": true,
"label-position": "nw",
"color": "#a4a4a4"
},
"nodes": [
"18b02b8e78a54427",
"68208d9442bc03c1",
"5f6a472f6435e598",
"389e05735379ed2e",
"db89d982a40f6c4d",
"3feb6c0d7c1c07b2",
"8ce7dd9a5d97b83b",
"06fa35d1c6063d4c",
"891a5f86c7c89917",
"3939b0bbde991557",
"2e40ef6c2172741a"
],
"x": 128,
"y": 95,
"w": 1018,
"h": 532
},
{
"id": "18b02b8e78a54427",
"type": "uibuilder",
"z": "278e7d113ed09433",
"g": "1678f3c7ab967e39",
"name": "",
"topic": "",
"url": "uib-dynamic-svg-eg",
"fwdInMessages": false,
"allowScripts": false,
"allowStyles": false,
"copyIndex": true,
"templateFolder": "blank",
"extTemplate": "",
"showfolder": false,
"reload": true,
"sourceFolder": "src",
"deployedVersion": "6.4.1",
"showMsgUib": true,
"x": 620,
"y": 240,
"wires": [
[
"95af327f377fa565"
],
[
"5f6a472f6435e598"
]
]
},
{
"id": "68208d9442bc03c1",
"type": "debug",
"z": "278e7d113ed09433",
"g": "1678f3c7ab967e39",
"name": "Std output",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": true,
"complete": "true",
"targetType": "full",
"statusVal": "",
"statusType": "counter",
"x": 1005,
"y": 300,
"wires": [],
"l": false
},
{
"id": "5f6a472f6435e598",
"type": "debug",
"z": "278e7d113ed09433",
"g": "1678f3c7ab967e39",
"name": "Ctrl output",
"active": false,
"tosidebar": true,
"console": false,
"tostatus": true,
"complete": "true",
"targetType": "full",
"statusVal": "",
"statusType": "counter",
"x": 785,
"y": 300,
"wires": [],
"l": false
},
{
"id": "389e05735379ed2e",
"type": "comment",
"z": "278e7d113ed09433",
"g": "1678f3c7ab967e39",
"name": "uibuilder Dynamic SVG - Vanilla HTML, no framework needed. \\n Updated version of old example that used VueJS \\n Read this comment for details. Requires uibuilder v6.1 or above",
"info": "This example demonstrates how to use uibuilder\nwith SVG images to create a dynamic home\nlighting dashboard.\n\nUse a background-image (see index.css) and\nthen clone the included \"bulb\" SVG \n(see index.html `<template>`) and change the\nproperties of each bulb using uibuilder's \nreduced-code functions. Colours, size, position,\netc are all controlled by CSS classes.\n\n## To use the example\n\nAs always deploy your flow after adding a\nuibuilder node, making sure the url setting \nis unique.\n\nThen update the `index.html`, `index.js` and \n`index.css` files with the code provided.\n\nOpen the resulting page and play with\ncontrolling from Node-RED and try clicking\non the bulb symbols on the page to see \nhow all the interactions work.\n\nThis example demonstrates a hybrid way of \nworking with uibuilder to create web pages.\n\nThere is some code and some Node-RED flow.\n\nHopefully, this illustrates how a little code \ncan go a long way and that you are not \nconstrained to use just one approach but can \nmix and match as desired.",
"x": 430,
"y": 160,
"wires": []
},
{
"id": "db89d982a40f6c4d",
"type": "inject",
"z": "278e7d113ed09433",
"g": "1678f3c7ab967e39",
"name": "Toggle Visible Msgs",
"props": [
{
"p": "_uib",
"v": "{\"command\":\"showMsg\"}",
"vt": "json"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "",
"x": 290,
"y": 300,
"wires": [
[
"06fa35d1c6063d4c"
]
]
},
{
"id": "3feb6c0d7c1c07b2",
"type": "debug",
"z": "278e7d113ed09433",
"g": "1678f3c7ab967e39",
"name": "Std output",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": true,
"complete": "true",
"targetType": "full",
"statusVal": "",
"statusType": "counter",
"x": 1085,
"y": 180,
"wires": [],
"l": false
},
{
"id": "8ce7dd9a5d97b83b",
"type": "link in",
"z": "278e7d113ed09433",
"g": "1678f3c7ab967e39",
"name": "link in 10",
"links": [
"a4fb2160fb46d36c",
"06fa35d1c6063d4c"
],
"x": 485,
"y": 240,
"wires": [
[
"18b02b8e78a54427"
]
]
},
{
"id": "06fa35d1c6063d4c",
"type": "link out",
"z": "278e7d113ed09433",
"g": "1678f3c7ab967e39",
"name": "link out 35",
"mode": "link",
"links": [
"8ce7dd9a5d97b83b"
],
"x": 565,
"y": 360,
"wires": []
},
{
"id": "891a5f86c7c89917",
"type": "group",
"z": "278e7d113ed09433",
"g": "1678f3c7ab967e39",
"name": "Control lights from Node-RED",
"style": {
"fill": "#ffbfbf",
"fill-opacity": "0.23",
"label": true,
"color": "#000000"
},
"nodes": [
"ad6de73ba713120d",
"b2b85b92b1cf7da5",
"77193b09f79ec310",
"4c9d8f9adc268c35",
"f86567acf2326bad",
"36595c40107736a2"
],
"x": 154,
"y": 339,
"w": 302,
"h": 242
},
{
"id": "ad6de73ba713120d",
"type": "inject",
"z": "278e7d113ed09433",
"g": "891a5f86c7c89917",
"name": "LIGHTS (random)",
"props": [
{
"p": "topic",
"vt": "str"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "LIGHTS",
"x": 290,
"y": 380,
"wires": [
[
"b2b85b92b1cf7da5"
]
]
},
{
"id": "b2b85b92b1cf7da5",
"type": "change",
"z": "278e7d113ed09433",
"g": "891a5f86c7c89917",
"name": "Randomly turn on/off all bulbs",
"rules": [
{
"t": "set",
"p": "_ui",
"pt": "msg",
"to": "(\t /* Generate a true/false for each bulb */\t $b1 := $random() >= 0.5;\t $b2 := $random() >= 0.5;\t $b3 := $random() >= 0.5;\t $b4 := $random() >= 0.5;\t /* Apply to msg._ui to randomly update all bulbs */\t [\t {\t \"method\":\"update\",\t \"components\": [\t {\t \"id\":\"bulb1\",\t \"attributes\": {\t /* NB: Give this one a different colour to the others */\t \"class\":\"bulb posn1 \" & ($b1 ? \"bulb-fail\" : \"\"),\t /* We use a data attribute to make it easier to track on/off state */\t \"data-state\": ($b1 ? \"on\" : \"off\")\t }\t }\t ]\t },\t {\t \"method\":\"update\",\t \"components\": [\t {\t \"id\":\"bulb2\",\t \"attributes\": {\t \"class\":\"bulb posn2 \" & ($b2 ? \"bulb-warn\" : \"\"),\t \"data-state\": ($b2 ? \"on\" : \"off\")\t }\t }\t ]\t },\t {\t \"method\":\"update\",\t \"components\": [\t {\t \"id\":\"bulb3\",\t \"attributes\": {\t \"class\":\"bulb posn3 \" & ($b3 ? \"bulb-warn\" : \"\"),\t \"data-state\": ($b3 ? \"on\" : \"off\")\t }\t }\t ]\t },\t {\t \"method\":\"update\",\t \"components\": [\t {\t \"id\":\"bulb4\",\t \"attributes\": {\t \"class\":\"bulb posn4 \" & ($b4 ? \"bulb-warn\" : \"\"),\t \"data-state\": ($b4 ? \"on\" : \"off\")\t }\t }\t ]\t }\t ]\t)",
"tot": "jsonata"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 415,
"y": 380,
"wires": [
[
"06fa35d1c6063d4c"
]
],
"l": false
},
{
"id": "77193b09f79ec310",
"type": "inject",
"z": "278e7d113ed09433",
"g": "891a5f86c7c89917",
"name": "LIGHT 1 on",
"props": [
{
"p": "_ui",
"v": "[{\"method\":\"update\",\"components\":[{\"id\":\"bulb1\",\"attributes\":{\"class\":\"bulb posn1 bulb-fail\",\"data-state\":\"on\"}}]}]",
"vt": "json"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "LIGHT-1",
"x": 310,
"y": 420,
"wires": [
[
"06fa35d1c6063d4c"
]
]
},
{
"id": "4c9d8f9adc268c35",
"type": "inject",
"z": "278e7d113ed09433",
"g": "891a5f86c7c89917",
"name": "LIGHT 1 off",
"props": [
{
"p": "_ui",
"v": "[{\"method\":\"update\",\"components\":[{\"id\":\"bulb1\",\"attributes\":{\"class\":\"bulb posn1\",\"data-state\":\"off\"}}]}]",
"vt": "json"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "LIGHT-1",
"x": 310,
"y": 460,
"wires": [
[
"06fa35d1c6063d4c"
]
]
},
{
"id": "f86567acf2326bad",
"type": "inject",
"z": "278e7d113ed09433",
"g": "891a5f86c7c89917",
"name": "LIGHT 2 on",
"props": [
{
"p": "_ui",
"v": "[{\"method\":\"update\",\"components\":[{\"id\":\"bulb2\",\"attributes\":{\"class\":\"bulb posn2 bulb-warn\",\"data-state\":\"on\"}}]}]",
"vt": "json"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "LIGHT-2",
"x": 310,
"y": 500,
"wires": [
[
"06fa35d1c6063d4c"
]
]
},
{
"id": "36595c40107736a2",
"type": "inject",
"z": "278e7d113ed09433",
"g": "891a5f86c7c89917",
"name": "LIGHT 2 off",
"props": [
{
"p": "_ui",
"v": "[{\"method\":\"update\",\"components\":[{\"id\":\"bulb2\",\"attributes\":{\"class\":\"bulb posn2\",\"data-state\":\"off\"}}]}]",
"vt": "json"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "LIGHT-2",
"x": 310,
"y": 540,
"wires": [
[
"06fa35d1c6063d4c"
]
]
},
{
"id": "3939b0bbde991557",
"type": "group",
"z": "278e7d113ed09433",
"g": "1678f3c7ab967e39",
"name": "User clicks turn on/off",
"style": {
"fill": "#e3f3d3",
"fill-opacity": "0.26",
"label": true,
"color": "#000000"
},
"nodes": [
"95af327f377fa565",
"c3bec95c24885417",
"a4fb2160fb46d36c"
],
"x": 794,
"y": 159,
"w": 252,
"h": 109.5
},
{
"id": "95af327f377fa565",
"type": "switch",
"z": "278e7d113ed09433",
"g": "3939b0bbde991557",
"name": "Switch out bulb clicks",
"property": "payload.state",
"propertyType": "msg",
"rules": [
{
"t": "eq",
"v": "on",
"vt": "str"
},
{
"t": "eq",
"v": "off",
"vt": "str"
},
{
"t": "else"
}
],
"checkall": "false",
"repair": false,
"outputs": 3,
"x": 835,
"y": 220,
"wires": [
[
"c3bec95c24885417"
],
[
"c3bec95c24885417"
],
[
"68208d9442bc03c1"
]
],
"outputLabels": [
"A bulb is currently ON",
"A bulb is currently OFF",
"Anything else"
],
"l": false
},
{
"id": "c3bec95c24885417",
"type": "change",
"z": "278e7d113ed09433",
"g": "3939b0bbde991557",
"name": "Toggle clicked bulb state",
"rules": [
{
"t": "set",
"p": "_ui",
"pt": "msg",
"to": "(\t [\t {\t \"method\":\"update\",\t \"components\":[\t {\t \"id\": _ui.id,\t \"attributes\":{\t /* Toggle fill colour */\t \"class\":\"bulb \" & payload.posn & \" \" & (payload.state = \"off\" ? \"bulb-warn\" : \"\"),\t /* Toggle state */\t \"data-state\": (payload.state = \"on\" ? \"off\" : \"on\")\t }\t }\t ]\t }\t]\t)",
"tot": "jsonata"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 905,
"y": 200,
"wires": [
[
"3feb6c0d7c1c07b2",
"a4fb2160fb46d36c"
]
],
"l": false
},
{
"id": "a4fb2160fb46d36c",
"type": "link out",
"z": "278e7d113ed09433",
"g": "3939b0bbde991557",
"name": "link out 34",
"mode": "link",
"links": [
"8ce7dd9a5d97b83b"
],
"x": 1005,
"y": 220,
"wires": []
},
{
"id": "2e40ef6c2172741a",
"type": "group",
"z": "278e7d113ed09433",
"g": "1678f3c7ab967e39",
"name": "Copy these to the \\n appropriate files \\n ",
"style": {
"fill": "#ffffbf",
"fill-opacity": "0.15",
"label": true,
"color": "#000000",
"label-position": "n"
},
"nodes": [
"fbe4c7573f698409",
"4ca9ba62878dbad8",
"83f5b29fe8ebb1d7",
"5dce296a96d3f95e"
],
"x": 654,
"y": 367,
"w": 212,
"h": 234
},
{
"id": "fbe4c7573f698409",
"type": "comment",
"z": "278e7d113ed09433",
"g": "2e40ef6c2172741a",
"name": "index.html",
"info": "<!doctype html>\n<html lang=\"en\"><head>\n\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n <link rel=\"icon\" href=\"../uibuilder/images/node-blue.ico\">\n\n <title>Dynamic SVG Example - Node-RED uibuilder</title>\n <meta name=\"description\" content=\"Node-RED uibuilder - Dynamic SVG Example\">\n\n <!-- Your own CSS (defaults to loading uibuilders css)-->\n <link type=\"text/css\" rel=\"stylesheet\" href=\"./index.css\" media=\"all\">\n\n <!-- #region Supporting Scripts. These MUST be in the right order. Note no leading / -->\n <script defer src=\"../uibuilder/uibuilder.iife.min.js\"></script>\n <script defer src=\"./index.js\">/* custom code */</script>\n <!-- #endregion -->\n\n</head><body class=\"uib\">\n <template id=\"bulb-template\">\n <svg id=\"mybulb\" class=\"bulb\" height=\"3rem\" viewBox=\"0 0 1024 1024\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\">\n <defs>\n <filter id=\"shadow\">\n <feDropShadow dx=\"1\" dy=\"1\" stdDeviation=\"5\" flood-opacity=\"50%\" />\n </filter>\n <filter id=\"glow\" filterUnits=\"userSpaceOnUse\"\n x=\"-50%\" y=\"-50%\" width=\"200%\" height=\"200%\">\n <!-- blur the text at different levels-->\n <feGaussianBlur in=\"SourceGraphic\" stdDeviation=\"5\" result=\"blur5\"/>\n <feGaussianBlur in=\"SourceGraphic\" stdDeviation=\"10\" result=\"blur10\"/>\n <feGaussianBlur in=\"SourceGraphic\" stdDeviation=\"20\" result=\"blur20\"/>\n <feGaussianBlur in=\"SourceGraphic\" stdDeviation=\"30\" result=\"blur30\"/>\n <feGaussianBlur in=\"SourceGraphic\" stdDeviation=\"50\" result=\"blur50\"/>\n <!-- merge all the blurs except for the first one -->\n <feMerge result=\"blur-merged\">\n <feMergeNode in=\"blur10\"/>\n <feMergeNode in=\"blur20\"/>\n <feMergeNode in=\"blur30\"/>\n <feMergeNode in=\"blur50\"/>\n </feMerge>\n <!-- recolour the merged blurs red-->\n <feColorMatrix result=\"red-blur\" in=\"blur-merged\" type=\"matrix\"\n values=\"1 0 0 0 0\n 0 0.06 0 0 0\n 0 0 0.44 0 0\n 0 0 0 1 0\" />\n <feMerge>\n <!--<feMergeNode in=\"red-blur\"/> largest blurs coloured red -->\n <feMergeNode in=\"blur-merged\"/>\n <feMergeNode in=\"blur5\"/> <!-- smallest blur left white -->\n <feMergeNode in=\"SourceGraphic\"/> <!-- original -->\n </feMerge>\n </filter>\n </defs>\n <title>TITLE</title>\n <path name=\"icon\" d=\"M511.549861 803.293331H408.419043a73.232959 73.232959 0 0 1-67.1862-41.991375 59.795719 59.795719 0 0 1-6.71862-30.569722 207.60536 207.60536 0 0 0-33.593101-113.88061 196.519637 196.519637 0 0 0-27.882273-33.5931A463.248853 463.248853 0 0 1 217.274302 504.314738a399.086031 399.086031 0 0 1-36.95241-75.248544 242.542184 242.542184 0 0 1-15.116895-77.264131 349.032312 349.032312 0 0 1 8.062344-84.990544 314.76735 314.76735 0 0 1 51.733375-114.888403A367.172586 367.172586 0 0 1 361.724634 34.011334 327.532728 327.532728 0 0 1 433.949799 8.144647 369.524103 369.524103 0 0 1 528.682342 0.418234a333.579486 333.579486 0 0 1 126.310057 29.225997 326.860866 326.860866 0 0 1 70.881442 44.678824A382.625412 382.625412 0 0 1 808.848799 168.383736a314.095488 314.095488 0 0 1 41.991375 105.146403 312.751764 312.751764 0 0 1 6.382689 92.045095 275.799353 275.799353 0 0 1-20.15586 76.256338 449.139751 449.139751 0 0 1-61.139443 107.16199 497.513815 497.513815 0 0 1-33.5931 39.639858 160.575019 160.575019 0 0 0-31.241583 48.038134 215.331773 215.331773 0 0 0-18.812136 55.428615c-1.679655 11.757585 0 23.179239-2.687448 33.5931a171.660742 171.660742 0 0 1-3.695241 25.194826 69.873649 69.873649 0 0 1-33.593101 40.647651 74.576683 74.576683 0 0 1-39.639858 10.07793zM490.050277 88.768088c-11.085723 0-22.171446 2.351517-33.5931 4.031172a210.96467 210.96467 0 0 0-74.240752 26.538549 244.221839 244.221839 0 0 0-55.428616 44.342893 222.386324 222.386324 0 0 0-43.335099 63.82689 230.784599 230.784599 0 0 0-19.483998 94.732543 28.218204 28.218204 0 0 0 33.5931 28.890066 28.890066 28.890066 0 0 0 22.171446-26.202618v-13.773171a167.965501 167.965501 0 0 1 9.406068-49.045927 184.762052 184.762052 0 0 1 64.834684-83.98275 167.965501 167.965501 0 0 1 93.72475-33.593101 142.770676 142.770676 0 0 0 18.140274 0 23.851101 23.851101 0 0 0 19.148067-15.452826 33.5931 33.5931 0 0 0 0-19.483998 23.51517 23.51517 0 0 0-20.491791-18.140274 122.950747 122.950747 0 0 0-15.116895 0zM647.601917 943.040628a15.788757 15.788757 0 0 1-13.773171 15.116895H400.356699a17.468412 17.468412 0 0 1-16.460619-8.734206 18.812136 18.812136 0 0 1 0-20.15586 16.124688 16.124688 0 0 1 16.460619-4.703034h227.089358a19.148067 19.148067 0 0 1 19.148067 20.827722zM405.731595 899.369598a18.140274 18.140274 0 0 1-16.460619-12.765378 17.804343 17.804343 0 0 1 15.452826-23.851101H635.508401a18.812136 18.812136 0 0 1 17.804343 13.773171 19.819929 19.819929 0 0 1-10.749792 21.499584 24.187032 24.187032 0 0 1-8.734206 0H423.535938zM437.64504 1022.992207a17.132481 17.132481 0 0 1-15.452826-9.406068 18.140274 18.140274 0 0 1 15.116895-26.202618h139.411367a19.819929 19.819929 0 0 1 19.483998 17.804343 16.124688 16.124688 0 0 1-8.734206 15.788757 19.148067 19.148067 0 0 1-9.741999 3.023379H442.348074z\" />\n <!-- <circle name=\"default\" cx=\"50\" cy=\"50\" r=\"50\"></circle> -->\n </svg>\n </template>\n \n <h1 class=\"with-subtitle\">Dynamic SVG Example</h1>\n <div role=\"doc-subtitle\">Using the uibuilder IIFE library.</div>\n <p>\n This is a uibuilder example looking at how easy it is to create a dynamic view of IoT devices in a building\n using SVG images both for the background (floor-plan) and device indicators.\n </p>\n\n <div id=\"more\"><!-- '#more' is used as a parent for dynamic HTML content in examples --></div>\n\n <h2>My House, Ground Floor</h2>\n <div id=\"floorplan\" class=\"plan\"><!-- Bulb icons dynamically inserted here --></div>\n\n</body></html>\n",
"x": 740,
"y": 440,
"wires": []
},
{
"id": "4ca9ba62878dbad8",
"type": "comment",
"z": "278e7d113ed09433",
"g": "2e40ef6c2172741a",
"name": "index.css",
"info": "/* Load defaults from `<userDir>/node_modules/node-red-contrib-uibuilder/front-end/uib-brand.css`\n * This version auto-adjusts for light/dark browser settings but might not be as complete.\n */\n@import url(\"../uibuilder/uib-brand.css\");\n\n/* These variables build on existing variables in uib-brand.css\n * They will be incorporated into that file in uibuilder v6.2\n */\n:root {\n --warning-intense: hsl(\n var(--warning-hue) 100% 50%\n );\n --failure-intense: hsl(\n var(--failure-hue) 100% 50%\n );\n --surface5: hsl( /* additional background shade */\n var(--brand-hue)\n calc(100% * var(--surfaces-saturation))\n calc(\n 100% * (var(--surfaces-lightness)\n - (var(--surfaces-factor) * .20)\n + (var(--surfaces-factor) * var(--surfaces-bias)))\n )\n );\n}\n\n\n/* Bulb classes control look, colour and position */\n\n.bulb { /* Default \"off\" class plus standard style */\n z-index: 9999 !important; /* Bulbs HAVE to be in the top z-layer */\n cursor: pointer; \n position: absolute; /* allows exact positioning within the parent div */\n transition: filter 2s ease-in-out 0s;\n background-color: rgba(0, 0, 0, 0.001); /* transparent background */\n filter: url(\"#shadow\"); /* selects the shadow filter */\n}\n.bulb path {\n fill: grey;\n}\n\n.bulb-warn { /* Standard \"on\" class */\n filter: url('#glow'); /* selects the glow filter instead of shadow */\n}\n.bulb-warn path {\n fill: var(--warning-intense);\n}\n\n.bulb-fail { /* Alternative \"on\" class with different colour */\n filter: url('#glow');\n}\n.bulb-fail path {\n fill: var(--failure-intense);\n}\n\n/* Bulb position classes, change as needed\n * Positions are relative to the parent floorplan div\n */\n.posn1 {\n top: 100px; left: 100px; \n}\n.posn2 {\n top: 120px; left: 270px; \n}\n.posn3 {\n top: 120px; left: 650px; \n}\n.posn4 {\n top: 270px; left: 250px; \n}\n\n/* floorplan div class change anything\n * except the position:relative.\n * The background image location is relative\n * to your uibuilder front-end files.\n */\n.plan {\n position:relative; \n width:99%; height:30rem; \n background:url(\"./background.svg\");\n background-color: var(--surface5);\n}\n",
"x": 740,
"y": 480,
"wires": []
},
{
"id": "83f5b29fe8ebb1d7",
"type": "comment",
"z": "278e7d113ed09433",
"g": "2e40ef6c2172741a",
"name": "index.js",
"info": "// @ts-nocheck\n/** Dynamic SVG example */\n\n// uibuilder.logLevel = 1\n\n/** A clone of the uibuilder $ function so you don't have to install v6.2-dev\n * Simplistic jQuery-like document CSS query selector, returns an HTML Element\n * If the selected element is a <template>, returns the first child element.\n * type {HTMLElement}\n * @param {string} cssSelector A CSS Selector that identifies the element to return\n * @returns {HTMLElement|HTMLTemplateElement|null}\n */\nfunction mySelector(cssSelector) {\n let el = document.querySelector(cssSelector)\n\n if (!el) {\n log(1, 'mySelector', `No element found for CSS selector ${cssSelector}`)()\n return null\n }\n\n if (el.nodeName === 'TEMPLATE') {\n el = el.content.firstElementChild\n if (!el) {\n log(0, 'mySelector', `Template selected for CSS selector ${cssSelector} but it is empty`)()\n return null\n }\n }\n\n return el\n}\n\nfunction doMe(event) {\n uibuilder.eventSend(event)\n}\n\n/** Insert a clone of a template tag\n * NB: Template should have only a single direct child tag, nothing other than that tag and its contents will be cloned.\n * @param {HTMLTemplateElement} template The template to clone and insert\n * @param {HTMLElement} parent The parent within which to insert the clone\n * @param {object} [ui] Optional uib UI object that will apply changes to the cloned element (e.g. attribs, slot)\n */\nfunction htmlClone(template, parent, ui) {\n console.log(ui)\n if (!template || !(template instanceof Element)) {\n console.error('Template HTMLElement not provided or is not an HTML Element')\n return\n }\n if (!parent || !(parent instanceof Element)) {\n console.error('Parent HTMLElement not provided or is not an HTML Element')\n return\n }\n if (!ui) ui = {}\n if (!ui.position) ui.position = 'last'\n\n const clone = template.cloneNode(true)\n\n // Oops! Fns starting with `_` should not have been used - sorry. This fn no longer available directly.\n // Will add an equivalent in a future release (post v6.4.1) probably called `uiEnhanceElement`\n // uibuilder._uiComposeComponent(clone, ui)\n clone.id = ui.id\n clone.classList.add(ui.attributes['data-posn'])\n clone.dataset.state = ui.attributes['data-state']\n clone.dataset.posn = ui.attributes['data-posn']\n clone.onclick = doMe //uibuilder.eventSend\n\n if (ui.position === 'first') {\n // Insert new el before the first child of the parent. Ref: https://developer.mozilla.org/en-US/docs/Web/API/Node/insertBefore#example_3\n parent.insertBefore(clone, parent.firstChild)\n } else if (Number.isInteger(Number(ui.position))) {\n elParent.insertBefore(clone, parent.children[ui.position])\n } else {\n // Append to the required parent\n parent.appendChild(clone)\n }\n\n}\nconst x = {\n \"id\": \"bulb1\",\n \"attributes\": {\n \"class\": \"bulb posn1\",\n \"data-state\": \"off\",\n \"data-posn\": \"posn1\"\n },\n \"events\": {\n \"click\": \"uibuilder.eventSend\"\n },\n \"position\": \"last\"\n}\n\n// The Template tag in index.html contains a template \"bulb\" SVG image\n// Here, we clone that multiple times and set some properties.\n// Note that `htmlClone` is a function that will land in the uibuilder client in v6.2\n// Also, the $ function is improved in v6.2 so a copy of that is included here for convenience.\n//\n// We track state and position class on data-* attributes so that it is much easier to process\n// click events in Node-RED without having to create a custom click handler, we can just use the standard eventSend.\n// CSS classes do all the clever stuff 😁\n\nhtmlClone($('#bulb-template'), $('#floorplan'), {\n // As always, we set a unique ID for every created element so it can be updated easily later\n id: 'bulb1',\n // You only need this if you want to choose 'first' or a position number,\n // the clone will be added at the specified child position of the parent.\n // position: 'last', \n attributes: {\n // Apply base bulb class and a positioning class\n class: 'bulb posn1',\n // Track the on/off state separately - makes processing in Node-RED easier\n 'data-state': 'off',\n // Track the position class separately - makes processing in Node-RED easier\n 'data-posn': 'posn1'\n },\n // We have to add event handlers after a clone, they cannot be included in the template\n events: {\n click: 'uibuilder.eventSend'\n }\n})\n\nhtmlClone(mySelector('#bulb-template'), mySelector('#floorplan'), {\n id: 'bulb2',\n attributes: {\n class: 'bulb posn2',\n 'data-state': 'off',\n 'data-posn': 'posn2'\n },\n events: {\n click: 'uibuilder.eventSend'\n }\n})\n\nhtmlClone(mySelector('#bulb-template'), mySelector('#floorplan'), {\n id: 'bulb3',\n attributes: {\n class: 'bulb posn3',\n 'data-state': 'off',\n 'data-posn': 'posn3'\n },\n events: {\n click: 'uibuilder.eventSend'\n }\n})\n\nhtmlClone(mySelector('#bulb-template'), mySelector('#floorplan'), {\n id: 'bulb4',\n attributes: {\n class: 'bulb posn4',\n 'data-state': 'off',\n 'data-posn': 'posn4'\n },\n events: {\n click: 'uibuilder.eventSend'\n },\n})\n",
"x": 730,
"y": 520,
"wires": []
},
{
"id": "5dce296a96d3f95e",
"type": "comment",
"z": "278e7d113ed09433",
"g": "2e40ef6c2172741a",
"name": "background.svg",
"info": "Can't include this directly here otherwise\nthe flow cannot be posted to the forum.\n\nThe example background file can be obtained\nhere:\n\nhttps://gist.github.com/TotallyInformation/02eb3716157db586f3f5b8a85c241009#file-background-svg\n\nCopy the text out of it and paste into a new \nfile, `background1.svg` in the same location \nas your `index.html` file.\n",
"x": 760,
"y": 560,
"wires": []
}
]
@TimAudenaert
Copy link

Hi Julian,
Nice work! I'm trying to duplicate, but I'm having trouble with showing the background. Could you elaborate about what to do with background.svg? All I'm seeing is the lamps, which i can turn on and off (see picture below).

image

@simonbuehler
Copy link

simonbuehler commented May 7, 2020

the svg location is set to a images subdirectory, search/replace that in the source files

btw: subdir editing would be great!

@TotallyInformation
Copy link
Author

Thanks for responding Simon, I'd missed Tim's previous comment, sorry for that. I've been rather wrapped up with COVID-19 responses I'm afraid.

Yes, subdir editing would be tremendous and is certainly on the list. Of course it would come faster if someone wanted to contribute! ;-)

@GamerWindow
Copy link

GamerWindow commented Sep 23, 2020

Thanks for your work on this, but in Firefox and Edge the lights are all stuck top left and the click event leads to an error. In Chrome everything works fine. I solved the possitions of the lights, with adding inline style. For example:
<bulb id="c" :ison="isonc" :clickable="true" style="left: 650px; top: 120px" x="650" y="120" @bulbclicked="myClick" title="C"></bulb>
Does someone know why this happens in Firefox and Edge but works fine in Chrome? And how can i fix it?
VueFloorplan_click

Edit:
After some testing, i found the problem but doesn't know how to solve it. In the js file there is this line:
var whichIcon = this.findWithAttr(event.path,'tagName','svg')
"event.path" matches with Chrome but in firefox ".path" doesn't exist for me in the event.

@TotallyInformation
Copy link
Author

TotallyInformation commented Oct 3, 2020

You are right that this doesn't work on Firefox. It seems that event handling on FF is different to Chromium. I've had a quick look and I can see a couple of problems but I don't know how to fix them right now.

The findWithAttr function doesn't work on FF because FF does not provide the event.path array which is used to find the actual component that was clicked (rather than the tag that was clicked).

In truth, this would be resolved by using the VueJS ref attribute rather than relying on the HTML id. I've started using ref in uibuilder (see the security branch which will soon be merged into master) which gives reliable access to the Vue component.

In fact, you could use this.$vnode instead of the event object to obtain the correct component that has been clicked.

To fix this problem, replace the code that sets returnData.srcId with:

returnData.srcId = this.$vnode.elm.id

I have no idea why FF doesn't position the bulb images correctly though. For some reason, it refuses to set the top and left CSS attributes. I can set them manually in dev tools & Vue is setting and returning data as expected. Most of the CSS attribs are being set, just not top and left. This would appear to be a bug in FF.

Found it. To fix the positioning on FF, you need to change the ciconstyle method to (Seems that FF is picky about having the px on the end):

        ciconstyle: function() {
            return {
                //color: this.cfillcolor, //this.ison ? '#d4af37' : this.color,
                top: this.y + "px",
                left: this.x + "px",
                cursor: this.clickable ? 'pointer' : 'default',
                position: 'absolute',
                filter: this.glow || this.ison ? 'url(#glow)' : 'url(#shadow)',
                transition: 'filter 2s ease-in-out', //'filter 10s',
            }
        },

I have updated the flow with both of these fixes.

If you are using new Edge, it works fine and that is what I use for development as I stopped using Chrome due to security & privacy issues. You really shouldn't be using legacy Edge as it is no longer in development.

@rogerjames99
Copy link

I am struggling to get the background image to appear. Looking at the code it appears that that the name of the background should actually be images/serveimage.svg. This is confirmed by web console on the client. However I tried that name and it still did not work. Can anyone give me pointers to how to get this going. It seems to me that the code I downloaded does not match the instructions here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment