├── .eslintignore
├── .eslintrc.js
├── .github
├── ISSUE_TEMPLATE
│ └── bug_report.md
└── workflows
│ └── ci.yml
├── .gitignore
├── .npmignore
├── .vscode
└── launch.json
├── LICENSE
├── README.md
├── examples
├── Allow Colors for Values in msg - store.json
├── Allow Colors for Values in msg.json
├── Basic.json
├── Mixed Payload Types.json
├── Sizing.json
└── Styling.json
├── images
├── colorPicker.gif
├── colorsForValues.png
├── examples.png
├── glow.png
├── icon.png
├── preview_changes.gif
└── sizes.png
├── package-lock.json
├── package.json
├── rollup.config.editor.js
├── src
├── nodes
│ └── ui_led
│ │ ├── icons
│ │ └── ui_led.png
│ │ ├── miscellanious.ts
│ │ ├── processing.ts
│ │ ├── rendering.ts
│ │ ├── shared
│ │ ├── rendering.ts
│ │ ├── types.ts
│ │ └── utility.ts
│ │ ├── types.ts
│ │ ├── ui_led.html
│ │ ├── constants.ts
│ │ ├── css.ts
│ │ ├── defaults.ts
│ │ ├── editor.html
│ │ ├── help.html
│ │ ├── index.ts
│ │ ├── interaction.ts
│ │ ├── processing.ts
│ │ ├── rendering.ts
│ │ └── types.ts
│ │ ├── ui_led.ts
│ │ └── utility.ts
└── types
│ ├── angular
│ └── index.d.ts
│ └── node-red-dashboard
│ └── index.d.ts
├── tsconfig.json
├── tsconfig.runtime.json
└── tsconfig.runtime.watch.json
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | /dist
3 | /rollup.config.editor.js
4 | /utils
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | parser: '@typescript-eslint/parser',
3 | extends: [
4 | 'plugin:@typescript-eslint/recommended',
5 | 'prettier',
6 | 'prettier/@typescript-eslint'
7 | ],
8 | plugins: ['jest', '@typescript-eslint'],
9 | parserOptions: {
10 | ecmaVersion: 2018,
11 | sourceType: 'module'
12 | },
13 | env: {
14 | node: true,
15 | jest: true,
16 | es6: true
17 | },
18 | rules: {
19 | '@typescript-eslint/no-unused-vars': [
20 | 'error',
21 | {
22 | argsIgnorePattern: '^_',
23 | varsIgnorePattern: '^_',
24 | args: 'after-used',
25 | ignoreRestSiblings: true
26 | }
27 | ],
28 | 'no-unused-expressions': [
29 | 'error',
30 | {
31 | allowTernary: true
32 | }
33 | ],
34 | 'no-console': 0,
35 | 'no-confusing-arrow': 0,
36 | 'no-else-return': 0,
37 | 'no-return-assign': [2, 'except-parens'],
38 | 'no-underscore-dangle': 0,
39 | 'jest/no-focused-tests': 2,
40 | 'jest/no-identical-title': 2,
41 | camelcase: 0,
42 | 'prefer-arrow-callback': [
43 | 'error',
44 | {
45 | allowNamedFunctions: true
46 | }
47 | ],
48 | 'class-methods-use-this': 0,
49 | 'no-restricted-syntax': 0,
50 | 'no-param-reassign': [
51 | 'error',
52 | {
53 | props: false
54 | }
55 | ],
56 |
57 | 'import/no-extraneous-dependencies': 0,
58 |
59 | 'arrow-body-style': 0,
60 | 'no-nested-ternary': 0
61 | },
62 | overrides: [
63 | {
64 | files: ['src/**/*.d.ts'],
65 | rules: {
66 | '@typescript-eslint/triple-slash-reference': 0
67 | }
68 | }
69 | ]
70 | }
71 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 | ---
8 |
9 | **Describe the bug**
10 | A clear and concise description of what the bug is.
11 |
12 | **To Reproduce**
13 | Steps to reproduce the behavior:
14 |
15 | 1. ...
16 | 2. ...
17 |
18 | **Expected behavior**
19 | A clear and concise description of what you expected to happen vs what actually happens.
20 |
21 | **Screenshots**
22 | If applicable, add screenshots to help explain your problem.
23 |
24 | **Versions**
25 |
26 | - node-red-contrib-ui-led:
27 | - node-red:
28 | - node-red-dashboard:
29 |
30 | **Platform**
31 |
32 | - OS: [e.g. iOS]
33 | - Browser [e.g. chrome, safari]
34 | - Version [e.g. 22]
35 |
36 | **Additional context**
37 | Add any other context about the problem here.
38 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 | build:
7 | runs-on: ubuntu-latest
8 |
9 | strategy:
10 | matrix:
11 | node-version: [12.x]
12 |
13 | steps:
14 | - uses: actions/checkout@v2.3.4
15 | - name: Setup Node.js ${{ matrix.node-version }} environment
16 | uses: actions/setup-node@v2.1.2
17 | with:
18 | node-version: ${{ matrix.node-version }}
19 |
20 | - name: npm install, lint, build and test
21 | run: |
22 | npm install
23 | npm run lint
24 | npm run build
25 | npm run test
26 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | .vscode
3 | yarn-debug.log*
4 | yarn-error.log*
5 | node_modules/
6 | *.tsbuildinfo
7 | dist
8 | /testing
9 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | *
2 | !dist/**/*
3 | !LICENSE
4 | !README.md
5 | !package.json
6 | !yarn.lock
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [
7 | {
8 | "type": "node",
9 | "request": "launch",
10 | "name": "Launch Example",
11 | "runtimeExecutable": "npm",
12 | "args": ["run", "start"],
13 | "cwd": "${workspaceFolder}/example"
14 | }
15 | ]
16 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Ian Grossberg
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Node-RED UI LED
2 |
3 | A simple LED status indicator for the Node-RED Dashboard
4 |
5 | 
6 | [](https://app.fossa.io/projects/git%2Bgithub.com%2FAdorkable%2Fnode-red-contrib-ui-led?ref=badge_shield)
7 | [](https://github.com/Adorkable/node-red-contrib-ui-led/network/dependencies)
8 |
9 | 
10 |
11 | The node uses `msg.payload`'s value to determine status. By default:
12 |
13 | - `msg.payload` === `true` - **Green**
14 | - `msg.payload` === `false` - **Red**
15 | - no `msg` received yet or `msg.payload` !== `true` and `msg.payload` !== `false` - **Gray**
16 |
17 | ## Install
18 |
19 | To install the node run the following from your Node-RED user directory (`~/.node-red`):
20 |
21 | ```bash
22 | npm install node-red-contrib-ui-led
23 | ```
24 |
25 | Or install the node from the Palette section of your Node-RED editor by searching by name (`node-red-contrib-ui-led`).
26 |
27 | ## Aesthetics
28 |
29 | There are a number of options when it comes to the node's aesthetics.
30 |
31 |
32 |
33 |
By default the LED itself will grow and shrink to fit the vertical height of the space it is locked to, auto-size to fit the group if marked `auto`.
34 |
35 |
36 |
37 |
38 | Most other customization happens in the **Edit panel**, which includes a preview so you can tweak to your heart's content.
39 |
40 | 
41 |
42 | ## Custom Statuses
43 |
44 | Although `true` => Green and `false` => Red is the default, one can map other payload values to any color.
45 |
46 | To customize the mappings open the node's configuration panel and scroll to the _Colors for Values_ list.
47 |
48 | 
49 |
50 | To add a value mapping press the **+Color** button at the bottom of the list.
51 |
52 | Next fill in a color in a [CSS](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value) format (color name, hex, rgb, rgba...), select the value type (`string`, `boolean`...) and fill in an appropriate value.
53 |
54 | Similarly existing Value => Color maps can be modified.
55 |
56 | Finally to delete a mapping simply press the X button on the far right!
57 |
58 | ## Custom Statuses in `msg`
59 |
60 | By enabling _Allow Color For Value map in msg_ in a node that node will use dictionaries passed via `msg.colorForValue` to override any previous color to value mappings.
61 |
62 | The format should be `value` => `color`, ie an object whose key values return color values.
63 |
64 | Example:
65 |
66 | ```js
67 | msg.colorForValue = {}
68 | msg.colorForValue[true] = 'purple'
69 | msg.colorForValue[false] = 'orange'
70 | ```
71 |
72 | ## Further Examples
73 |
74 | To see usages already set up check out the examples included with the project by using _Import_ in your Node-RED editor!
75 |
76 | ## License
77 |
78 | [](https://app.fossa.io/projects/git%2Bgithub.com%2FAdorkable%2Fnode-red-contrib-ui-led?ref=badge_large)
79 |
80 | ## Thanks to
81 |
82 | - [@alexk111](https://github.com/alexk111) for his great [Node-RED Typescript Starter](https://github.com/alexk111/node-red-node-typescript-starter) which made it a breeze to convert the project over to Typescript
83 |
--------------------------------------------------------------------------------
/examples/Allow Colors for Values in msg - store.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "id": "1f7b06cb.8d7e49",
4 | "type": "function",
5 | "z": "72f153f9.2cca8c",
6 | "name": "optionally store colorForValue",
7 | "func": "if (msg.colorForValue !== undefined) {\n global.set(\"colorForValue\", msg.colorForValue);\n}\nreturn msg;",
8 | "outputs": 1,
9 | "noerr": 0,
10 | "x": 650,
11 | "y": 480,
12 | "wires": [
13 | [
14 | "c62e1b9b.7df298"
15 | ]
16 | ]
17 | },
18 | {
19 | "id": "c7637ee6.1a73b",
20 | "type": "ui_button",
21 | "z": "72f153f9.2cca8c",
22 | "name": "true",
23 | "group": "688be5f8.e8e01c",
24 | "order": 2,
25 | "width": 0,
26 | "height": 0,
27 | "passthru": false,
28 | "label": "true",
29 | "tooltip": "",
30 | "color": "",
31 | "bgcolor": "",
32 | "icon": "",
33 | "payload": "true",
34 | "payloadType": "bool",
35 | "topic": "",
36 | "x": 390,
37 | "y": 340,
38 | "wires": [
39 | [
40 | "1f7b06cb.8d7e49"
41 | ]
42 | ]
43 | },
44 | {
45 | "id": "61e8a2b2.0a38ec",
46 | "type": "ui_button",
47 | "z": "72f153f9.2cca8c",
48 | "name": "false",
49 | "group": "688be5f8.e8e01c",
50 | "order": 3,
51 | "width": 0,
52 | "height": 0,
53 | "passthru": false,
54 | "label": "false",
55 | "tooltip": "",
56 | "color": "",
57 | "bgcolor": "",
58 | "icon": "",
59 | "payload": "false",
60 | "payloadType": "bool",
61 | "topic": "",
62 | "x": 390,
63 | "y": 380,
64 | "wires": [
65 | [
66 | "1f7b06cb.8d7e49"
67 | ]
68 | ]
69 | },
70 | {
71 | "id": "853ce247.e99db",
72 | "type": "ui_led",
73 | "z": "72f153f9.2cca8c",
74 | "group": "688be5f8.e8e01c",
75 | "order": 1,
76 | "width": 0,
77 | "height": 0,
78 | "label": "write / read example",
79 | "labelPlacement": "left",
80 | "labelAlignment": "left",
81 | "colorForValue": [
82 | {
83 | "color": "red",
84 | "value": "false",
85 | "valueType": "bool"
86 | },
87 | {
88 | "color": "green",
89 | "value": "true",
90 | "valueType": "bool"
91 | }
92 | ],
93 | "allowColorForValueInMessage": true,
94 | "name": "write / read example",
95 | "x": 940,
96 | "y": 540,
97 | "wires": []
98 | },
99 | {
100 | "id": "f614fc25.8b883",
101 | "type": "ui_button",
102 | "z": "72f153f9.2cca8c",
103 | "name": "true",
104 | "group": "688be5f8.e8e01c",
105 | "order": 4,
106 | "width": 0,
107 | "height": 0,
108 | "passthru": false,
109 | "label": "true + P / O",
110 | "tooltip": "",
111 | "color": "",
112 | "bgcolor": "",
113 | "icon": "",
114 | "payload": "true",
115 | "payloadType": "bool",
116 | "topic": "",
117 | "x": 130,
118 | "y": 440,
119 | "wires": [
120 | [
121 | "8d691358.01419"
122 | ]
123 | ]
124 | },
125 | {
126 | "id": "d0baa947.546268",
127 | "type": "ui_button",
128 | "z": "72f153f9.2cca8c",
129 | "name": "true",
130 | "group": "688be5f8.e8e01c",
131 | "order": 5,
132 | "width": 0,
133 | "height": 0,
134 | "passthru": false,
135 | "label": "true + B / Y",
136 | "tooltip": "",
137 | "color": "",
138 | "bgcolor": "",
139 | "icon": "",
140 | "payload": "true",
141 | "payloadType": "bool",
142 | "topic": "",
143 | "x": 130,
144 | "y": 480,
145 | "wires": [
146 | [
147 | "aff8dbe4.a6a598"
148 | ]
149 | ]
150 | },
151 | {
152 | "id": "8d691358.01419",
153 | "type": "function",
154 | "z": "72f153f9.2cca8c",
155 | "name": "+ msg.colorForValue[P/O]",
156 | "func": "msg.colorForValue = {};\nmsg.colorForValue[true] = \"purple\";\nmsg.colorForValue[false] = \"orange\";\n\nreturn msg;",
157 | "outputs": 1,
158 | "noerr": 0,
159 | "x": 330,
160 | "y": 440,
161 | "wires": [
162 | [
163 | "1f7b06cb.8d7e49"
164 | ]
165 | ]
166 | },
167 | {
168 | "id": "aff8dbe4.a6a598",
169 | "type": "function",
170 | "z": "72f153f9.2cca8c",
171 | "name": "+ msg.colorForValue[B/Y]",
172 | "func": "msg.colorForValue = {};\nmsg.colorForValue[true] = \"blue\";\nmsg.colorForValue[false] = \"yellow\";\n\nreturn msg;",
173 | "outputs": 1,
174 | "noerr": 0,
175 | "x": 330,
176 | "y": 480,
177 | "wires": [
178 | [
179 | "1f7b06cb.8d7e49"
180 | ]
181 | ]
182 | },
183 | {
184 | "id": "59c9ecf7.56a054",
185 | "type": "ui_button",
186 | "z": "72f153f9.2cca8c",
187 | "name": "true",
188 | "group": "688be5f8.e8e01c",
189 | "order": 6,
190 | "width": 0,
191 | "height": 0,
192 | "passthru": false,
193 | "label": "true - msg.colorForValue",
194 | "tooltip": "",
195 | "color": "",
196 | "bgcolor": "",
197 | "icon": "",
198 | "payload": "true",
199 | "payloadType": "bool",
200 | "topic": "",
201 | "x": 130,
202 | "y": 540,
203 | "wires": [
204 | [
205 | "bd18cc96.95e84"
206 | ]
207 | ]
208 | },
209 | {
210 | "id": "bd18cc96.95e84",
211 | "type": "function",
212 | "z": "72f153f9.2cca8c",
213 | "name": "- msg.colorForValue",
214 | "func": "msg.colorForValue = null;\n\nreturn msg;",
215 | "outputs": 1,
216 | "noerr": 0,
217 | "x": 340,
218 | "y": 540,
219 | "wires": [
220 | [
221 | "1f7b06cb.8d7e49"
222 | ]
223 | ]
224 | },
225 | {
226 | "id": "c62e1b9b.7df298",
227 | "type": "function",
228 | "z": "72f153f9.2cca8c",
229 | "name": "get optionally stored colorForValue",
230 | "func": "var colorForValue = global.get(\"colorForValue\");\nif (colorForValue !== undefined) {\n msg.colorForValue = colorForValue;\n}\nreturn msg;",
231 | "outputs": 1,
232 | "noerr": 0,
233 | "x": 660,
234 | "y": 540,
235 | "wires": [
236 | [
237 | "853ce247.e99db"
238 | ]
239 | ]
240 | },
241 | {
242 | "id": "688be5f8.e8e01c",
243 | "type": "ui_group",
244 | "z": "",
245 | "name": "LED example - Allow Colors for Values in msg - stored",
246 | "tab": "a27f71ee.672e7",
247 | "order": 2,
248 | "disp": true,
249 | "width": "6",
250 | "collapse": false
251 | },
252 | {
253 | "id": "a27f71ee.672e7",
254 | "type": "ui_tab",
255 | "z": "",
256 | "name": "Home",
257 | "icon": "dashboard",
258 | "order": 1,
259 | "disabled": false,
260 | "hidden": false
261 | }
262 | ]
--------------------------------------------------------------------------------
/examples/Allow Colors for Values in msg.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "id": "849b733f.867a",
4 | "type": "ui_button",
5 | "z": "72f153f9.2cca8c",
6 | "name": "",
7 | "group": "a198c200.9ac0a",
8 | "order": 2,
9 | "width": 0,
10 | "height": 0,
11 | "passthru": false,
12 | "label": "true",
13 | "tooltip": "",
14 | "color": "",
15 | "bgcolor": "",
16 | "icon": "",
17 | "payload": "true",
18 | "payloadType": "bool",
19 | "topic": "",
20 | "x": 530,
21 | "y": 40,
22 | "wires": [
23 | [
24 | "eb88ae93.7d1bf"
25 | ]
26 | ]
27 | },
28 | {
29 | "id": "24ecfc38.6c5184",
30 | "type": "ui_button",
31 | "z": "72f153f9.2cca8c",
32 | "name": "",
33 | "group": "a198c200.9ac0a",
34 | "order": 3,
35 | "width": 0,
36 | "height": 0,
37 | "passthru": false,
38 | "label": "false",
39 | "tooltip": "",
40 | "color": "",
41 | "bgcolor": "",
42 | "icon": "",
43 | "payload": "false",
44 | "payloadType": "bool",
45 | "topic": "",
46 | "x": 530,
47 | "y": 80,
48 | "wires": [
49 | [
50 | "eb88ae93.7d1bf"
51 | ]
52 | ]
53 | },
54 | {
55 | "id": "eb88ae93.7d1bf",
56 | "type": "ui_led",
57 | "z": "72f153f9.2cca8c",
58 | "group": "a198c200.9ac0a",
59 | "order": 1,
60 | "width": 0,
61 | "height": 0,
62 | "label": "basic example",
63 | "labelPlacement": "left",
64 | "labelAlignment": "left",
65 | "colorForValue": [
66 | {
67 | "color": "red",
68 | "value": "false",
69 | "valueType": "bool"
70 | },
71 | {
72 | "color": "green",
73 | "value": "true",
74 | "valueType": "bool"
75 | }
76 | ],
77 | "allowColorForValueInMessage": true,
78 | "name": "basic example",
79 | "x": 760,
80 | "y": 160,
81 | "wires": []
82 | },
83 | {
84 | "id": "89ca2bd8.0de448",
85 | "type": "ui_button",
86 | "z": "72f153f9.2cca8c",
87 | "name": "true",
88 | "group": "a198c200.9ac0a",
89 | "order": 4,
90 | "width": 0,
91 | "height": 0,
92 | "passthru": false,
93 | "label": "true + P / O",
94 | "tooltip": "",
95 | "color": "",
96 | "bgcolor": "",
97 | "icon": "",
98 | "payload": "true",
99 | "payloadType": "bool",
100 | "topic": "",
101 | "x": 230,
102 | "y": 160,
103 | "wires": [
104 | [
105 | "1c146448.5da1dc"
106 | ]
107 | ]
108 | },
109 | {
110 | "id": "32c20357.b5bc6c",
111 | "type": "ui_button",
112 | "z": "72f153f9.2cca8c",
113 | "name": "true",
114 | "group": "a198c200.9ac0a",
115 | "order": 5,
116 | "width": 0,
117 | "height": 0,
118 | "passthru": false,
119 | "label": "true + B / Y",
120 | "tooltip": "",
121 | "color": "",
122 | "bgcolor": "",
123 | "icon": "",
124 | "payload": "true",
125 | "payloadType": "bool",
126 | "topic": "",
127 | "x": 230,
128 | "y": 200,
129 | "wires": [
130 | [
131 | "db6a5516.1fd9b8"
132 | ]
133 | ]
134 | },
135 | {
136 | "id": "1c146448.5da1dc",
137 | "type": "function",
138 | "z": "72f153f9.2cca8c",
139 | "name": "+ msg.colorForValue[P/O]",
140 | "func": "msg.colorForValue = {};\nmsg.colorForValue[true] = \"purple\";\nmsg.colorForValue[false] = \"orange\";\n\nreturn msg;",
141 | "outputs": 1,
142 | "noerr": 0,
143 | "x": 470,
144 | "y": 160,
145 | "wires": [
146 | [
147 | "eb88ae93.7d1bf"
148 | ]
149 | ]
150 | },
151 | {
152 | "id": "db6a5516.1fd9b8",
153 | "type": "function",
154 | "z": "72f153f9.2cca8c",
155 | "name": "+ msg.colorForValue[B/Y]",
156 | "func": "msg.colorForValue = {};\nmsg.colorForValue[true] = \"blue\";\nmsg.colorForValue[false] = \"yellow\";\n\nreturn msg;",
157 | "outputs": 1,
158 | "noerr": 0,
159 | "x": 470,
160 | "y": 200,
161 | "wires": [
162 | [
163 | "eb88ae93.7d1bf"
164 | ]
165 | ]
166 | },
167 | {
168 | "id": "d9f72bc4.8e69e8",
169 | "type": "ui_button",
170 | "z": "72f153f9.2cca8c",
171 | "name": "true",
172 | "group": "a198c200.9ac0a",
173 | "order": 6,
174 | "width": 0,
175 | "height": 0,
176 | "passthru": false,
177 | "label": "true - msg.colorForValue",
178 | "tooltip": "",
179 | "color": "",
180 | "bgcolor": "",
181 | "icon": "",
182 | "payload": "true",
183 | "payloadType": "bool",
184 | "topic": "",
185 | "x": 230,
186 | "y": 260,
187 | "wires": [
188 | [
189 | "7fbceb18.b28d44"
190 | ]
191 | ]
192 | },
193 | {
194 | "id": "7fbceb18.b28d44",
195 | "type": "function",
196 | "z": "72f153f9.2cca8c",
197 | "name": "- msg.colorForValue",
198 | "func": "msg.colorForValue = null;\n\nreturn msg;",
199 | "outputs": 1,
200 | "noerr": 0,
201 | "x": 480,
202 | "y": 260,
203 | "wires": [
204 | [
205 | "eb88ae93.7d1bf"
206 | ]
207 | ]
208 | },
209 | {
210 | "id": "a198c200.9ac0a",
211 | "type": "ui_group",
212 | "z": "",
213 | "name": "LED example - Allow Colors for Values in msg",
214 | "tab": "a27f71ee.672e7",
215 | "disp": true,
216 | "width": "6",
217 | "collapse": false
218 | },
219 | {
220 | "id": "a27f71ee.672e7",
221 | "type": "ui_tab",
222 | "z": "",
223 | "name": "Home",
224 | "icon": "dashboard",
225 | "order": 1,
226 | "disabled": false,
227 | "hidden": false
228 | }
229 | ]
--------------------------------------------------------------------------------
/examples/Basic.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "id": "d8ffd877.337a48",
4 | "type": "ui_button",
5 | "z": "d83779ea.f368f8",
6 | "name": "false",
7 | "group": "4188b82.fe0a348",
8 | "order": 3,
9 | "width": 0,
10 | "height": 0,
11 | "passthru": false,
12 | "label": "false",
13 | "tooltip": "",
14 | "color": "white",
15 | "bgcolor": "red",
16 | "icon": "",
17 | "payload": "false",
18 | "payloadType": "json",
19 | "topic": "",
20 | "x": 190,
21 | "y": 100,
22 | "wires": [
23 | [
24 | "271b65fe.ba368a"
25 | ]
26 | ]
27 | },
28 | {
29 | "id": "271b65fe.ba368a",
30 | "type": "ui_led",
31 | "z": "d83779ea.f368f8",
32 | "group": "4188b82.fe0a348",
33 | "order": 1,
34 | "width": "",
35 | "height": "",
36 | "label": "Default (red / green)",
37 | "colorForValue": [
38 | {
39 | "color": "red",
40 | "value": "false",
41 | "valueType": "bool"
42 | },
43 | {
44 | "color": "green",
45 | "value": "true",
46 | "valueType": "bool"
47 | }
48 | ],
49 | "name": "Default",
50 | "x": 420,
51 | "y": 60,
52 | "wires": [
53 |
54 | ]
55 | },
56 | {
57 | "id": "f15be515.aab838",
58 | "type": "ui_button",
59 | "z": "d83779ea.f368f8",
60 | "name": "true",
61 | "group": "4188b82.fe0a348",
62 | "order": 2,
63 | "width": 0,
64 | "height": 0,
65 | "passthru": false,
66 | "label": "true",
67 | "tooltip": "",
68 | "color": "white",
69 | "bgcolor": "green",
70 | "icon": "",
71 | "payload": "true",
72 | "payloadType": "json",
73 | "topic": "",
74 | "x": 190,
75 | "y": 60,
76 | "wires": [
77 | [
78 | "271b65fe.ba368a"
79 | ]
80 | ]
81 | },
82 | {
83 | "id": "8d5de918.097738",
84 | "type": "ui_button",
85 | "z": "d83779ea.f368f8",
86 | "name": "[no match]",
87 | "group": "4188b82.fe0a348",
88 | "order": 5,
89 | "width": 0,
90 | "height": 0,
91 | "passthru": false,
92 | "label": "[no match]",
93 | "tooltip": "",
94 | "color": "black",
95 | "bgcolor": "gray",
96 | "icon": "",
97 | "payload": "",
98 | "payloadType": "str",
99 | "topic": "",
100 | "x": 210,
101 | "y": 180,
102 | "wires": [
103 | [
104 | "271b65fe.ba368a"
105 | ]
106 | ]
107 | },
108 | {
109 | "id": "cb77195a.fd9cb8",
110 | "type": "ui_switch",
111 | "z": "d83779ea.f368f8",
112 | "name": "",
113 | "label": "Active/Inactive",
114 | "tooltip": "",
115 | "group": "4188b82.fe0a348",
116 | "order": 4,
117 | "width": 4,
118 | "height": 1,
119 | "passthru": true,
120 | "decouple": "false",
121 | "topic": "",
122 | "style": "",
123 | "onvalue": "true",
124 | "onvalueType": "bool",
125 | "onicon": "",
126 | "oncolor": "",
127 | "offvalue": "false",
128 | "offvalueType": "bool",
129 | "officon": "",
130 | "offcolor": "",
131 | "x": 220,
132 | "y": 140,
133 | "wires": [
134 | [
135 | "271b65fe.ba368a"
136 | ]
137 | ]
138 | },
139 | {
140 | "id": "4188b82.fe0a348",
141 | "type": "ui_group",
142 | "z": "",
143 | "name": "LED example",
144 | "tab": "a27f71ee.672e7",
145 | "order": 1,
146 | "disp": true,
147 | "width": "6",
148 | "collapse": false
149 | },
150 | {
151 | "id": "a27f71ee.672e7",
152 | "type": "ui_tab",
153 | "z": "",
154 | "name": "Home",
155 | "icon": "dashboard",
156 | "order": 1,
157 | "disabled": false,
158 | "hidden": false
159 | }
160 | ]
--------------------------------------------------------------------------------
/examples/Mixed Payload Types.json:
--------------------------------------------------------------------------------
1 | [{"id":"ce53d961.734018","type":"ui_button","z":"d83779ea.f368f8","name":"\"crass\"","group":"4188b82.fe0a348","order":3,"width":0,"height":0,"passthru":false,"label":"\"crass\"","tooltip":"","color":"white","bgcolor":"black","icon":"","payload":"crass","payloadType":"str","topic":"","x":100,"y":120,"wires":[["70e1b787.73a5e8"]]},{"id":"70e1b787.73a5e8","type":"ui_led","z":"d83779ea.f368f8","group":"4188b82.fe0a348","order":1,"width":"","height":"","label":"Mixed Type","colorForValue":[{"color":"black","value":"crass","valueType":"str"},{"color":"yellow","value":"nirvana","valueType":"str"},{"color":"purple","value":"prince","valueType":"str"},{"color":"green","value":"true","valueType":"bool"},{"color":"blue","value":"{\"what\":\"Check It\"}","valueType":"json"}],"name":"Mixed Type","x":310,"y":80,"wires":[]},{"id":"e129865.976bc78","type":"ui_button","z":"d83779ea.f368f8","name":"\"prince\"","group":"4188b82.fe0a348","order":2,"width":0,"height":0,"passthru":false,"label":"\"prince\"","tooltip":"","color":"white","bgcolor":"purple","icon":"","payload":"prince","payloadType":"str","topic":"","x":100,"y":60,"wires":[["70e1b787.73a5e8"]]},{"id":"c72a0854.decbb8","type":"ui_button","z":"d83779ea.f368f8","name":"\"nirvana\"","group":"4188b82.fe0a348","order":4,"width":0,"height":0,"passthru":false,"label":"\"nirvana\"","tooltip":"","color":"black","bgcolor":"yellow","icon":"","payload":"nirvana","payloadType":"str","topic":"","x":100,"y":180,"wires":[["70e1b787.73a5e8"]]},{"id":"c0df7168.69559","type":"ui_button","z":"d83779ea.f368f8","name":"[no match]","group":"4188b82.fe0a348","order":5,"width":0,"height":0,"passthru":false,"label":"[no match]","tooltip":"","color":"black","bgcolor":"gray","icon":"","payload":"","payloadType":"str","topic":"","x":110,"y":240,"wires":[["70e1b787.73a5e8"]]},{"id":"fa00068d.bf4408","type":"ui_button","z":"d83779ea.f368f8","name":"true","group":"4188b82.fe0a348","order":6,"width":0,"height":0,"passthru":false,"label":"true","tooltip":"","color":"white","bgcolor":"green","icon":"","payload":"true","payloadType":"json","topic":"","x":90,"y":300,"wires":[["70e1b787.73a5e8"]]},{"id":"8c6ff854.c71858","type":"ui_button","z":"d83779ea.f368f8","name":"{ \"what\": \"Check It\" }","group":"4188b82.fe0a348","order":7,"width":0,"height":0,"passthru":false,"label":"{ \"what\": \"Check It\" }","tooltip":"","color":"black","bgcolor":"blue","icon":"","payload":"{\"what\":\"Check It\"}","payloadType":"json","topic":"","x":140,"y":360,"wires":[["70e1b787.73a5e8"]]},{"id":"4188b82.fe0a348","type":"ui_group","z":"","name":"LED example","tab":"a27f71ee.672e7","order":1,"disp":true,"width":"6","collapse":false},{"id":"a27f71ee.672e7","type":"ui_tab","z":"","name":"Home","icon":"dashboard","order":1,"disabled":false,"hidden":false}]
--------------------------------------------------------------------------------
/examples/Sizing.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "id": "18f6c18.7bb303f",
4 | "type": "ui_led",
5 | "z": "79f94315.37bdbc",
6 | "group": "396876ec.369d7a",
7 | "order": 1,
8 | "width": "1",
9 | "height": "5",
10 | "label": "",
11 | "colorForValue": [
12 | {
13 | "color": "red",
14 | "value": "false",
15 | "valueType": "bool"
16 | },
17 | {
18 | "color": "green",
19 | "value": "true",
20 | "valueType": "bool"
21 | }
22 | ],
23 | "name": "1x5",
24 | "x": 230,
25 | "y": 60,
26 | "wires": []
27 | },
28 | {
29 | "id": "4dd3707f.db897",
30 | "type": "ui_led",
31 | "z": "79f94315.37bdbc",
32 | "group": "46947ad.658bf84",
33 | "order": 2,
34 | "width": "6",
35 | "height": "1",
36 | "label": "6x1",
37 | "colorForValue": [
38 | {
39 | "color": "red",
40 | "value": "false",
41 | "valueType": "bool"
42 | },
43 | {
44 | "color": "green",
45 | "value": "true",
46 | "valueType": "bool"
47 | }
48 | ],
49 | "name": "6x1",
50 | "x": 430,
51 | "y": 60,
52 | "wires": []
53 | },
54 | {
55 | "id": "8e947748.05ab28",
56 | "type": "ui_led",
57 | "z": "79f94315.37bdbc",
58 | "group": "396876ec.369d7a",
59 | "order": 2,
60 | "width": "2",
61 | "height": "5",
62 | "label": "2x5",
63 | "colorForValue": [
64 | {
65 | "color": "red",
66 | "value": "false",
67 | "valueType": "bool"
68 | },
69 | {
70 | "color": "green",
71 | "value": "true",
72 | "valueType": "bool"
73 | }
74 | ],
75 | "name": "2x5",
76 | "x": 230,
77 | "y": 120,
78 | "wires": []
79 | },
80 | {
81 | "id": "fef02acc.5e5b38",
82 | "type": "ui_led",
83 | "z": "79f94315.37bdbc",
84 | "group": "46947ad.658bf84",
85 | "order": 2,
86 | "width": "6",
87 | "height": "2",
88 | "label": "6x2",
89 | "colorForValue": [
90 | {
91 | "color": "red",
92 | "value": "false",
93 | "valueType": "bool"
94 | },
95 | {
96 | "color": "green",
97 | "value": "true",
98 | "valueType": "bool"
99 | }
100 | ],
101 | "name": "6x2",
102 | "x": 430,
103 | "y": 120,
104 | "wires": []
105 | },
106 | {
107 | "id": "d244b7a2.70eeb8",
108 | "type": "ui_led",
109 | "z": "79f94315.37bdbc",
110 | "group": "46947ad.658bf84",
111 | "order": 2,
112 | "width": "6",
113 | "height": "6",
114 | "label": "6x6",
115 | "colorForValue": [
116 | {
117 | "color": "red",
118 | "value": "false",
119 | "valueType": "bool"
120 | },
121 | {
122 | "color": "green",
123 | "value": "true",
124 | "valueType": "bool"
125 | }
126 | ],
127 | "name": "6x6",
128 | "x": 430,
129 | "y": 180,
130 | "wires": []
131 | },
132 | {
133 | "id": "dd5464e2.e899a8",
134 | "type": "ui_led",
135 | "z": "79f94315.37bdbc",
136 | "group": "46947ad.658bf84",
137 | "order": 2,
138 | "width": "3",
139 | "height": "3",
140 | "label": "3x3",
141 | "colorForValue": [
142 | {
143 | "color": "red",
144 | "value": "false",
145 | "valueType": "bool"
146 | },
147 | {
148 | "color": "green",
149 | "value": "true",
150 | "valueType": "bool"
151 | }
152 | ],
153 | "name": "3x3",
154 | "x": 430,
155 | "y": 240,
156 | "wires": []
157 | },
158 | {
159 | "id": "32c350aa.5a274",
160 | "type": "ui_led",
161 | "z": "79f94315.37bdbc",
162 | "group": "396876ec.369d7a",
163 | "order": 3,
164 | "width": "3",
165 | "height": "5",
166 | "label": "3x5",
167 | "colorForValue": [
168 | {
169 | "color": "red",
170 | "value": "false",
171 | "valueType": "bool"
172 | },
173 | {
174 | "color": "green",
175 | "value": "true",
176 | "valueType": "bool"
177 | }
178 | ],
179 | "name": "3x5",
180 | "x": 230,
181 | "y": 180,
182 | "wires": []
183 | },
184 | {
185 | "id": "298c65d5.daa7ea",
186 | "type": "ui_led",
187 | "z": "79f94315.37bdbc",
188 | "group": "396876ec.369d7a",
189 | "order": 4,
190 | "width": "4",
191 | "height": "5",
192 | "label": "4x5",
193 | "colorForValue": [
194 | {
195 | "color": "red",
196 | "value": "false",
197 | "valueType": "bool"
198 | },
199 | {
200 | "color": "green",
201 | "value": "true",
202 | "valueType": "bool"
203 | }
204 | ],
205 | "name": "4x5",
206 | "x": 230,
207 | "y": 240,
208 | "wires": []
209 | },
210 | {
211 | "id": "ab88409.85f80c",
212 | "type": "inject",
213 | "z": "79f94315.37bdbc",
214 | "name": "",
215 | "topic": "",
216 | "payload": "true",
217 | "payloadType": "bool",
218 | "repeat": "",
219 | "crontab": "",
220 | "once": true,
221 | "onceDelay": 0.1,
222 | "x": 50,
223 | "y": 100,
224 | "wires": [
225 | [
226 | "18f6c18.7bb303f",
227 | "32c350aa.5a274",
228 | "fef02acc.5e5b38",
229 | "dd5464e2.e899a8"
230 | ]
231 | ]
232 | },
233 | {
234 | "id": "a42295d1.4c40d8",
235 | "type": "inject",
236 | "z": "79f94315.37bdbc",
237 | "name": "",
238 | "topic": "",
239 | "payload": "false",
240 | "payloadType": "bool",
241 | "repeat": "",
242 | "crontab": "",
243 | "once": true,
244 | "onceDelay": 0.1,
245 | "x": 50,
246 | "y": 180,
247 | "wires": [
248 | [
249 | "8e947748.05ab28",
250 | "298c65d5.daa7ea",
251 | "4dd3707f.db897",
252 | "d244b7a2.70eeb8"
253 | ]
254 | ]
255 | },
256 | {
257 | "id": "396876ec.369d7a",
258 | "type": "ui_group",
259 | "z": "",
260 | "name": "LED example - Sizing - Tall",
261 | "tab": "a27f71ee.672e7",
262 | "order": 1,
263 | "disp": true,
264 | "width": "6",
265 | "collapse": false
266 | },
267 | {
268 | "id": "46947ad.658bf84",
269 | "type": "ui_group",
270 | "z": "",
271 | "name": "LED example - Sizing - Wide",
272 | "tab": "a27f71ee.672e7",
273 | "order": 2,
274 | "disp": true,
275 | "width": "6",
276 | "collapse": false
277 | },
278 | {
279 | "id": "a27f71ee.672e7",
280 | "type": "ui_tab",
281 | "z": "",
282 | "name": "Home",
283 | "icon": "dashboard",
284 | "order": 1,
285 | "disabled": false,
286 | "hidden": false
287 | }
288 | ]
--------------------------------------------------------------------------------
/examples/Styling.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "id": "8de3835a.aa386",
4 | "type": "ui_led",
5 | "z": "23fae03e.fa9e9",
6 | "group": "2612c343.79af2c",
7 | "order": 1,
8 | "width": 0,
9 | "height": 0,
10 | "label": "Left P Left A",
11 | "labelPlacement": "left",
12 | "labelAlignment": "left",
13 | "colorForValue": [
14 | {
15 | "color": "red",
16 | "value": "false",
17 | "valueType": "bool"
18 | },
19 | {
20 | "color": "green",
21 | "value": "true",
22 | "valueType": "bool"
23 | }
24 | ],
25 | "name": "Label Left P Left A",
26 | "x": 200,
27 | "y": 80,
28 | "wires": []
29 | },
30 | {
31 | "id": "1beffb2a.f69395",
32 | "type": "ui_led",
33 | "z": "23fae03e.fa9e9",
34 | "group": "2612c343.79af2c",
35 | "order": 4,
36 | "width": 0,
37 | "height": 0,
38 | "label": "Right P Left A",
39 | "labelPlacement": "right",
40 | "labelAlignment": "left",
41 | "colorForValue": [
42 | {
43 | "color": "red",
44 | "value": "false",
45 | "valueType": "bool"
46 | },
47 | {
48 | "color": "green",
49 | "value": "true",
50 | "valueType": "bool"
51 | }
52 | ],
53 | "name": "Label Right P Left A",
54 | "x": 240,
55 | "y": 160,
56 | "wires": []
57 | },
58 | {
59 | "id": "5ed10df9.4609a4",
60 | "type": "ui_led",
61 | "z": "23fae03e.fa9e9",
62 | "group": "2612c343.79af2c",
63 | "order": 3,
64 | "width": 0,
65 | "height": 0,
66 | "label": "Left P Right A",
67 | "labelPlacement": "left",
68 | "labelAlignment": "right",
69 | "colorForValue": [
70 | {
71 | "color": "red",
72 | "value": "false",
73 | "valueType": "bool"
74 | },
75 | {
76 | "color": "green",
77 | "value": "true",
78 | "valueType": "bool"
79 | }
80 | ],
81 | "name": "Label Left P Right A",
82 | "x": 260,
83 | "y": 200,
84 | "wires": []
85 | },
86 | {
87 | "id": "f57a35df.a539d8",
88 | "type": "ui_led",
89 | "z": "23fae03e.fa9e9",
90 | "group": "2612c343.79af2c",
91 | "order": 6,
92 | "width": 0,
93 | "height": 0,
94 | "label": "Right P Right A",
95 | "labelPlacement": "right",
96 | "labelAlignment": "right",
97 | "colorForValue": [
98 | {
99 | "color": "red",
100 | "value": "false",
101 | "valueType": "bool"
102 | },
103 | {
104 | "color": "green",
105 | "value": "true",
106 | "valueType": "bool"
107 | }
108 | ],
109 | "name": "Label Right P Right A",
110 | "x": 300,
111 | "y": 280,
112 | "wires": []
113 | },
114 | {
115 | "id": "1cef38fc.08d8c7",
116 | "type": "ui_led",
117 | "z": "23fae03e.fa9e9",
118 | "group": "2612c343.79af2c",
119 | "order": 2,
120 | "width": 0,
121 | "height": 0,
122 | "label": "Left P Center A",
123 | "labelPlacement": "left",
124 | "labelAlignment": "center",
125 | "colorForValue": [
126 | {
127 | "color": "red",
128 | "value": "false",
129 | "valueType": "bool"
130 | },
131 | {
132 | "color": "green",
133 | "value": "true",
134 | "valueType": "bool"
135 | }
136 | ],
137 | "name": "Label Left P Center A",
138 | "x": 220,
139 | "y": 120,
140 | "wires": []
141 | },
142 | {
143 | "id": "a759c566.734118",
144 | "type": "ui_led",
145 | "z": "23fae03e.fa9e9",
146 | "group": "2612c343.79af2c",
147 | "order": 5,
148 | "width": 0,
149 | "height": 0,
150 | "label": "Right P Center A",
151 | "labelPlacement": "right",
152 | "labelAlignment": "center",
153 | "colorForValue": [
154 | {
155 | "color": "red",
156 | "value": "false",
157 | "valueType": "bool"
158 | },
159 | {
160 | "color": "green",
161 | "value": "true",
162 | "valueType": "bool"
163 | }
164 | ],
165 | "name": "Label Right P Center A",
166 | "x": 280,
167 | "y": 240,
168 | "wires": []
169 | },
170 | {
171 | "id": "2612c343.79af2c",
172 | "type": "ui_group",
173 | "z": "",
174 | "name": "LED example - Styling",
175 | "tab": "a27f71ee.672e7",
176 | "order": 2,
177 | "disp": true,
178 | "width": "6",
179 | "collapse": false
180 | },
181 | {
182 | "id": "a27f71ee.672e7",
183 | "type": "ui_tab",
184 | "z": "",
185 | "name": "Home",
186 | "icon": "dashboard",
187 | "order": 1,
188 | "disabled": false,
189 | "hidden": false
190 | }
191 | ]
--------------------------------------------------------------------------------
/images/colorPicker.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Adorkable/node-red-contrib-ui-led/940734d8ddf0ddaf4e2ea6964dd3a548254ad171/images/colorPicker.gif
--------------------------------------------------------------------------------
/images/colorsForValues.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Adorkable/node-red-contrib-ui-led/940734d8ddf0ddaf4e2ea6964dd3a548254ad171/images/colorsForValues.png
--------------------------------------------------------------------------------
/images/examples.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Adorkable/node-red-contrib-ui-led/940734d8ddf0ddaf4e2ea6964dd3a548254ad171/images/examples.png
--------------------------------------------------------------------------------
/images/glow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Adorkable/node-red-contrib-ui-led/940734d8ddf0ddaf4e2ea6964dd3a548254ad171/images/glow.png
--------------------------------------------------------------------------------
/images/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Adorkable/node-red-contrib-ui-led/940734d8ddf0ddaf4e2ea6964dd3a548254ad171/images/icon.png
--------------------------------------------------------------------------------
/images/preview_changes.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Adorkable/node-red-contrib-ui-led/940734d8ddf0ddaf4e2ea6964dd3a548254ad171/images/preview_changes.gif
--------------------------------------------------------------------------------
/images/sizes.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Adorkable/node-red-contrib-ui-led/940734d8ddf0ddaf4e2ea6964dd3a548254ad171/images/sizes.png
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "node-red-contrib-ui-led",
3 | "version": "0.4.11",
4 | "description": "A simple LED status indicator for the Node-RED Dashboard",
5 | "author": "Ian G ",
6 | "license": "MIT",
7 | "node-red": {
8 | "nodes": {
9 | "ui_led": "./dist/nodes/ui_led/ui_led.js"
10 | }
11 | },
12 | "keywords": [
13 | "node-red",
14 | "led",
15 | "dashboard",
16 | "ui"
17 | ],
18 | "bugs": {
19 | "url": "https://github.com/Adorkable/node-red-contrib-ui-led/issues"
20 | },
21 | "homepage": "https://github.com/Adorkable/node-red-contrib-ui-led",
22 | "repository": {
23 | "type": "git",
24 | "url": "git+https://github.com/Adorkable/node-red-contrib-ui-led.git"
25 | },
26 | "scripts": {
27 | "copy": "copyfiles -u 2 \"./src/nodes/**/*.{png,svg}\" \"./dist/nodes/\"",
28 | "build:editor": "rollup -c rollup.config.editor.js",
29 | "build:editor:watch": "rollup -c rollup.config.editor.js -w",
30 | "build:runtime": "tsc -p tsconfig.runtime.json",
31 | "build:runtime:watch": "tsc -p tsconfig.runtime.watch.json --watch --preserveWatchOutput",
32 | "build": "rm -rf dist && npm run copy && npm run build:editor && npm run build:runtime",
33 | "test": "jest --forceExit --detectOpenHandles --colors --passWithNoTests",
34 | "test:watch": "jest --forceExit --detectOpenHandles --watchAll --passWithNoTests",
35 | "dev": "rm -rf dist && npm run copy && concurrently --kill-others --names 'COPY,EDITOR,RUNTIME,LINT,TEST' --prefix '({name})' --prefix-colors 'greenBright.bold,yellow.bold,cyan.bold,redBright.bold,magenta.bold' 'onchange -v \"src/**/*.png\" \"src/**/*.svg\" -- npm run copy' 'npm run lint:watch' 'npm run build:editor:watch' 'npm run build:runtime:watch' 'sleep 10; npm run test:watch'",
36 | "lint": "prettier --ignore-path .eslintignore --check '**/*.{js,ts,md}'; eslint --ext .js,.ts .",
37 | "lint:fix": "prettier --ignore-path .eslintignore --write '**/*.{js,ts,md}'; eslint --ext .js,.ts . --fix",
38 | "lint:watch": "onchange -v \"**/*.{js,ts,md}\" -- npm run lint",
39 | "release": "npm run build && npm publish"
40 | },
41 | "dependencies": {
42 | },
43 | "devDependencies": {
44 | "@rollup/plugin-typescript": "^8.0.0",
45 | "@types/angular": "^1.8.0",
46 | "@types/express": "^4.17.9",
47 | "@types/jest": "^26.0.15",
48 | "@types/jqueryui": "^1.12.14",
49 | "@types/node": "^14.14.10",
50 | "@types/node-red": "^1.1.1",
51 | "@types/node-red-node-test-helper": "^0.2.1",
52 | "@types/sinon": "^9.0.9",
53 | "@types/supertest": "^2.0.10",
54 | "@typescript-eslint/eslint-plugin": "^4.9.0",
55 | "@typescript-eslint/parser": "^4.9.0",
56 | "colorette": "^1.2.1",
57 | "concurrently": "^5.3.0",
58 | "copyfiles": "^2.4.1",
59 | "eslint": "^7.14.0",
60 | "eslint-config-prettier": "^7.1.0",
61 | "eslint-plugin-jest": "^24.1.3",
62 | "eslint-plugin-prettier": "^3.1.4",
63 | "glob": "^7.1.6",
64 | "jest": "^26.6.3",
65 | "mustache": "^4.0.1",
66 | "node-red": "^3.0.2",
67 | "node-red-dashboard": ">=2.23.3",
68 | "node-red-node-test-helper": "^0.3.0",
69 | "onchange": "^7.0.2",
70 | "prettier": "^2.2.1",
71 | "rollup": "^2.23.0",
72 | "ts-jest": "^26.4.4",
73 | "typescript": "^4.1.2"
74 | },
75 | "peerDependencies": {
76 | "node-red-dashboard": ">=2.23.3"
77 | },
78 | "jest": {
79 | "testEnvironment": "node",
80 | "roots": [
81 | "/src"
82 | ],
83 | "transform": {
84 | "^.+\\.ts$": "ts-jest"
85 | },
86 | "testMatch": [
87 | "**/__tests__/**/*.test.ts"
88 | ]
89 | },
90 | "prettier": {
91 | "printWidth": 80,
92 | "semi": false,
93 | "singleQuote": true,
94 | "trailingComma": "none"
95 | }
96 | }
--------------------------------------------------------------------------------
/rollup.config.editor.js:
--------------------------------------------------------------------------------
1 | import fs from 'fs'
2 | import glob from 'glob'
3 | import path from 'path'
4 | import typescript from '@rollup/plugin-typescript'
5 |
6 | import packageJson from './package.json'
7 |
8 | const allNodeTypes = Object.keys(packageJson['node-red'].nodes)
9 |
10 | const htmlWatch = () => {
11 | return {
12 | name: 'htmlWatch',
13 | load(id) {
14 | const editorDir = path.dirname(id)
15 | const htmlFiles = glob.sync(path.join(editorDir, '*.html'))
16 | htmlFiles.map((file) => this.addWatchFile(file))
17 | }
18 | }
19 | }
20 |
21 | const htmlBundle = () => {
22 | return {
23 | name: 'htmlBundle',
24 | renderChunk(code, chunk, _options) {
25 | const editorDir = path.dirname(chunk.facadeModuleId)
26 | const htmlFiles = glob.sync(path.join(editorDir, '*.html'))
27 | const htmlContents = htmlFiles.map((fPath) => fs.readFileSync(fPath))
28 |
29 | code =
30 | '\n' +
34 | htmlContents.join('\n')
35 |
36 | return {
37 | code,
38 | map: { mappings: '' }
39 | }
40 | }
41 | }
42 | }
43 |
44 | const makePlugins = (nodeType) => [
45 | htmlWatch(),
46 | typescript({
47 | lib: ['es5', 'es6', 'dom'],
48 | include: [
49 | `src/nodes/${nodeType}/${nodeType}.html/**/*.ts`,
50 | `src/nodes/${nodeType}/shared/**/*.ts`,
51 | 'src/nodes/shared/**/*.ts'
52 | ],
53 | target: 'es5',
54 | tsconfig: false,
55 | noEmitOnError: process.env.ROLLUP_WATCH ? false : true
56 | }),
57 | htmlBundle()
58 | ]
59 |
60 | const makeConfigItem = (nodeType) => ({
61 | input: `src/nodes/${nodeType}/${nodeType}.html/index.ts`,
62 | output: {
63 | file: `dist/nodes/${nodeType}/${nodeType}.html`,
64 | format: 'iife'
65 | },
66 | plugins: makePlugins(nodeType),
67 | watch: {
68 | clearScreen: false
69 | }
70 | })
71 |
72 | export default allNodeTypes.map((nodeType) => makeConfigItem(nodeType))
73 |
--------------------------------------------------------------------------------
/src/nodes/ui_led/icons/ui_led.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Adorkable/node-red-contrib-ui-led/940734d8ddf0ddaf4e2ea6964dd3a548254ad171/src/nodes/ui_led/icons/ui_led.png
--------------------------------------------------------------------------------
/src/nodes/ui_led/miscellanious.ts:
--------------------------------------------------------------------------------
1 | export declare const WebKitMutationObserver: MutationObserver
2 |
3 | export type ObserveCallback = (event: Event | MutationRecord[]) => void
4 |
--------------------------------------------------------------------------------
/src/nodes/ui_led/processing.ts:
--------------------------------------------------------------------------------
1 | import nodeRed, { NodeAPI } from 'node-red'
2 | import {
3 | BeforeEmitMessage,
4 | Emit,
5 | InitController,
6 | Payload,
7 | UiEvents,
8 | UITemplateScope
9 | } from '../../types/node-red-dashboard'
10 | import { ObserveCallback, WebKitMutationObserver } from './miscellanious'
11 |
12 | import { ColorForValueArray } from './shared/types'
13 | import {
14 | ColorForValueMap,
15 | ControllerMessage,
16 | LEDBeforeEmitMessage,
17 | LEDNode
18 | } from './types'
19 |
20 | const getColorForValue = (
21 | colorForValue: ColorForValueArray | ColorForValueMap,
22 | value: Payload,
23 | RED: NodeAPI
24 | ): [string, boolean] => {
25 | let color: string | undefined,
26 | found = false
27 |
28 | try {
29 | if (Array.isArray(colorForValue)) {
30 | for (let index = 0; index < colorForValue.length; index++) {
31 | const compareWith = colorForValue[index]
32 |
33 | if (RED.util.compareObjects(compareWith.value, value)) {
34 | color = compareWith.color
35 | found = true
36 | break
37 | }
38 | }
39 | } else if (typeof colorForValue === 'object') {
40 | color = colorForValue[value]
41 | found = color !== undefined && color !== null
42 | }
43 | } catch (_error) {
44 | // TODO: Not an error to receive an unaccounted for value, but we should log to Node-RED debug log
45 | // console.log("Error trying to find color for value '" + value + "'", error)
46 | }
47 | if (found === false || color === undefined) {
48 | color = 'gray'
49 | }
50 | return [color, found]
51 | }
52 |
53 | export const beforeEmitFactory = (
54 | node: LEDNode,
55 | RED: NodeAPI
56 | ) => {
57 | return (
58 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
59 | msg: BeforeEmitMessage,
60 | value: Payload
61 | ): Emit => {
62 | if (
63 | node.allowColorForValueInMessage === true &&
64 | typeof msg.colorForValue !== 'undefined'
65 | ) {
66 | const ledMsg = msg as LEDBeforeEmitMessage
67 | const msgColorForValue = ledMsg.colorForValue
68 | if (msgColorForValue !== undefined) {
69 | node.overrideColorForValue = msgColorForValue
70 | }
71 | }
72 | const colorForValue = node.overrideColorForValue || node.colorForValue
73 |
74 | const [color, glow] = getColorForValue(colorForValue, value, RED)
75 |
76 | return {
77 | msg: {
78 | ...msg,
79 | color,
80 | glow: node.showGlow ? glow : false,
81 | sizeMultiplier: node.height
82 | }
83 | }
84 | }
85 | }
86 |
87 | // TODO: why is initController stringed and evaled??? we have to move erryone into this file :/
88 | export const initController: InitController = (
89 | $scope: UITemplateScope,
90 | _events: UiEvents
91 | ): void => {
92 | $scope.flag = true
93 |
94 | // TODO: From miscellanious.ts, we need to resolve this issue
95 | // Based on: https://stackoverflow.com/a/14570614
96 | const observeDOMFactory = (): ((
97 | observe: Node,
98 | callback: ObserveCallback
99 | ) => void) => {
100 | const MutationObserver = window.MutationObserver || WebKitMutationObserver
101 |
102 | return (observe: Node, callback: ObserveCallback) => {
103 | if (!observe) {
104 | throw new Error('Element to observe not provided')
105 | }
106 |
107 | if (
108 | observe.nodeType !== 1 &&
109 | observe.nodeType !== 9 &&
110 | observe.nodeType !== 11
111 | ) {
112 | throw new Error(
113 | 'Unexpected Node type (' + observe.nodeType + ') provided: ' + observe
114 | )
115 | }
116 |
117 | if (MutationObserver) {
118 | const observer = new MutationObserver((mutations, observer) => {
119 | observer.disconnect()
120 | callback(mutations)
121 | })
122 |
123 | observer.observe(observe, {
124 | childList: true,
125 | subtree: true
126 | })
127 | } else if (window.addEventListener !== undefined) {
128 | const options = {
129 | capture: false,
130 | once: true
131 | }
132 | observe.addEventListener('DOMNodeInserted', callback, options)
133 | observe.addEventListener('DOMNodeRemoved', callback, options)
134 | }
135 | }
136 | }
137 |
138 | const glowSize = 7
139 |
140 | const ledStyle = (
141 | color: string,
142 | glow: boolean,
143 | sizeMultiplier: number
144 | ): string => {
145 | if (glow) {
146 | return `
147 | background-color: ${color};
148 | box-shadow:
149 | #0000009e 0 0px ${2 / window.devicePixelRatio}px 0px,
150 | ${color} 0 0px ${glowSize * sizeMultiplier}px ${Math.floor(
151 | (glowSize * sizeMultiplier) / 3
152 | )}px,
153 | inset #00000017 0 -1px 1px 0px;`
154 | } else {
155 | // TODO: duplicate code because of execution scope, fix this shit :|
156 | return `
157 | background-color: ${color};
158 | box-shadow:
159 | #0000009e 0 0px ${2 / window.devicePixelRatio}px 0px,
160 | inset #ffffff8c 0px 1px 2px,
161 | inset #00000033 0 -1px 1px 0px,
162 | inset ${color} 0 -1px 2px;`
163 | }
164 | }
165 |
166 | const update = (msg: ControllerMessage, element: Element) => {
167 | if (!msg) {
168 | return
169 | }
170 |
171 | if (!element) {
172 | return
173 | }
174 |
175 | const color = msg.color
176 | const glow = msg.glow
177 | const sizeMultiplier = msg.sizeMultiplier
178 |
179 | $(element).attr('style', ledStyle(color, glow, sizeMultiplier))
180 | }
181 |
182 | const retrieveElementFromDocument = (id: string, document: Document) => {
183 | // TODO: share code to make sure we're always using the same id composure
184 | const elementId = 'led_' + id
185 | if (!document) {
186 | return undefined
187 | }
188 | return document.getElementById(elementId)
189 | }
190 |
191 | const observeDOM = observeDOMFactory()
192 |
193 | const updateWithScope = (msg: ControllerMessage) => {
194 | if (!$scope) {
195 | return
196 | }
197 |
198 | const id = $scope.$eval('$id')
199 | const attemptUpdate = () => {
200 | const element = retrieveElementFromDocument(id, document)
201 |
202 | if (element) {
203 | update(msg, element)
204 | } else {
205 | // HACK: is there a proper way to wait for this node's element to be rendered?
206 | observeDOM(document, (_change) => {
207 | attemptUpdate()
208 | })
209 | }
210 | }
211 | attemptUpdate()
212 | }
213 |
214 | $scope.$watch('msg', updateWithScope)
215 | }
216 |
--------------------------------------------------------------------------------
/src/nodes/ui_led/rendering.ts:
--------------------------------------------------------------------------------
1 | import { control } from './shared/rendering'
2 | import { LEDNodeDef } from './types'
3 |
4 | // TODO: switch from _ to - to ensure we don't ever collide with any class and to make more readable as different than a description (led_container vs led-31343, etc)
5 | export const composeLEDElementIdTemplate = (): string => {
6 | return String.raw`led_{{$id}}`
7 | }
8 | export const controlClassTemplate = String.raw`led_{{$id}}`
9 |
10 | /**
11 | * Generate our dashboard HTML code
12 | * @param {object} config - The node's config instance
13 | * @param {object} ledStyle - Style attribute of our LED span in in-line CSS format
14 | */
15 | export const HTML = (
16 | config: LEDNodeDef,
17 | color: string,
18 | glow: boolean,
19 | sizeMultiplier: number
20 | ): string => {
21 | // text-align: ` + config.labelAlignment + `
22 |
23 | return control(
24 | controlClassTemplate,
25 | composeLEDElementIdTemplate(),
26 | config.label,
27 | config.labelPlacement || 'left',
28 | config.labelAlignment || 'left',
29 | config.shape || 'circle',
30 | color,
31 | glow,
32 | sizeMultiplier
33 | )
34 | }
35 |
--------------------------------------------------------------------------------
/src/nodes/ui_led/shared/rendering.ts:
--------------------------------------------------------------------------------
1 | import { Shape } from './types'
2 |
3 | const glowSize = 7
4 |
5 | export const ledStyle = (
6 | color: string,
7 | glow: boolean,
8 | sizeMultiplier: number
9 | ): string => {
10 | if (glow) {
11 | return `
12 | background-color: ${color};
13 | box-shadow:
14 | #0000009e 0 0px 1px 0px,
15 | ${color} 0 0px ${glowSize * sizeMultiplier}px ${Math.floor(
16 | (glowSize * sizeMultiplier) / 3
17 | )}px,
18 | inset #00000017 0 -1px 1px 0px;`
19 | } else {
20 | // TODO: duplicate code because of execution scope, fix this shit :|
21 | return `
22 | background-color: ${color};
23 | box-shadow:
24 | #0000009e 0 0px 1px 0px,
25 | inset #ffffff8c 0px 1px 2px,
26 | inset #00000033 0 -1px 1px 0px,
27 | inset ${color} 0 -1px 2px;`
28 | }
29 | }
30 |
31 | // HACK: workaround because ratio trick isn't working consistently across browsers
32 | const nodeRedDashboardUICardVerticalPadding = 3 * 2
33 | // const nodeRedDashboardUICardHorizontalPadding = 6 * 2
34 | const nodeRedDashboardUICardHeight = (sizeMultiplier: number): number => {
35 | let height = 48 + (sizeMultiplier - 1) * 54
36 | if (height >= 4) {
37 | height -= 6 // For some reason the difference between 3 and 4 is 48
38 | }
39 | return height - nodeRedDashboardUICardVerticalPadding
40 | }
41 |
42 | export const ledElement = (
43 | controlClass: string,
44 | ledId: string,
45 | shape: Shape,
46 | color: string,
47 | glow: boolean,
48 | sizeMultiplier: number
49 | ): string => {
50 | const showCurveReflection = false // TODO: Needs visual work and potentially make an option for poeple who like the old style better
51 |
52 | const ledContainerPadding = (glowSize + 4) * sizeMultiplier
53 |
54 | const length =
55 | nodeRedDashboardUICardHeight(sizeMultiplier) - ledContainerPadding * 2
56 |
57 | // TODO: if show glow is turned off we should not include this padding for the glow?
58 | const ledContentsStyle = String.raw`
59 | div.${controlClass}.led_contents {
60 | min-height: ${length}px;
61 | min-width: ${length}px;
62 | height: ${length}px;
63 | width: ${length}px;
64 | max-height: ${length}px;
65 | max-width: ${length}px;
66 | text-align: center;
67 | margin: ${ledContainerPadding}px;
68 | ${shape === 'circle' ? `border-radius: 50%;` : ''}
69 | }`
70 |
71 | const ledCurveShineReflectionStyle = String.raw`
72 | .${controlClass}.curveShine {
73 | width: 70%;
74 | height: 70%;
75 | background: radial-gradient(circle, rgba(255,255,255,0.15) 0%, rgba(0,255,31,0) 60%);'
76 | }`
77 | const styles = String.raw`
78 | ${ledContentsStyle}
79 | ${ledCurveShineReflectionStyle}
80 | `
81 |
82 | const ledCurveReflection = String.raw`
83 |
`
84 |
85 | const ledContentsElement = String.raw`
86 |
90 | ${showCurveReflection ? ledCurveReflection : ''}
91 |
`
92 |
93 | return String.raw`
94 |
97 | ${ledContentsElement}`
98 | }
99 |
100 | export const control = (
101 | controlClass: string,
102 | ledId: string,
103 | label: string,
104 | labelPlacement: string,
105 | labelAlignment: string,
106 | shape: Shape,
107 | color: string,
108 | glow: boolean,
109 | sizeMultiplier: number
110 | ): string => {
111 | const hasName = () => {
112 | return typeof label === 'string' && label !== ''
113 | }
114 |
115 | const name = () => {
116 | if (hasName()) {
117 | return `` + label + ` `
118 | }
119 | return ''
120 | }
121 |
122 | const optionalName = (display: boolean) => {
123 | if (display) {
124 | return name()
125 | }
126 | return ''
127 | }
128 |
129 | const controlStyle = String.raw`
130 | div.${controlClass}.control {
131 | display: flex;
132 | flex-direction: row;
133 | flex-wrap: nowrap;
134 |
135 | justify-content: ${hasName() ? 'space-between' : 'center'};
136 | align-items: center;
137 |
138 | height: 100%;
139 | width: 100%;
140 | position: relative;
141 |
142 | overflow: hidden;
143 | }`
144 |
145 | const labelStyle = String.raw`
146 | div.${controlClass} > span.name {
147 | text-align: ${labelAlignment};
148 | margin-left: 6px;
149 | margin-right: 6px;
150 | overflow-wrap: break-word;
151 | overflow: hidden;
152 | text-overflow: ellipsis;
153 | flex-grow: 1;
154 | }`
155 |
156 | const style = String.raw``
160 |
161 | const allElements = String.raw`
162 |
163 | ${optionalName(labelPlacement !== 'right')}
164 | ${ledElement(controlClass, ledId, shape, color, glow, sizeMultiplier)}
165 | ${optionalName(labelPlacement === 'right')}
166 |
`
167 | return style + allElements
168 | }
169 |
--------------------------------------------------------------------------------
/src/nodes/ui_led/shared/types.ts:
--------------------------------------------------------------------------------
1 | import { Payload } from '../../../types/node-red-dashboard'
2 |
3 | export type LabelPlacement = 'left' | 'right'
4 | export type LabelAlignment = 'left' | 'center' | 'right'
5 | export type Shape = 'circle' | 'square'
6 |
7 | export interface LEDNodeOptions {
8 | label: string
9 | labelPlacement?: LabelPlacement | void
10 | labelAlignment?: LabelAlignment | void
11 |
12 | width?: number | string | void
13 | height?: number | string | void
14 |
15 | order: number
16 |
17 | group: string
18 |
19 | colorForValue: ColorForValueArray
20 |
21 | allowColorForValueInMessage: boolean
22 |
23 | shape: Shape
24 |
25 | showGlow: boolean
26 | }
27 |
28 | // Copied from node-red :P
29 | export type ValueType =
30 | // | 'msg'
31 | // | 'flow'
32 | // | 'global'
33 | 'str' | 'num' | 'bool' | 'json' | 'bin'
34 | // | 're'
35 | // | 'date'
36 | // | 'jsonata'
37 | // | 'env'
38 |
39 | export const SupportedValueTypes: ValueType[] = [
40 | 'str',
41 | 'num',
42 | 'bool',
43 | 'json',
44 | 'bin'
45 | ] // TODO: add further support
46 |
47 | export type ColorForValue = {
48 | color: string
49 | value: Payload
50 | valueType: ValueType
51 | }
52 |
53 | export type ColorForValueArray = ColorForValue[]
54 |
--------------------------------------------------------------------------------
/src/nodes/ui_led/shared/utility.ts:
--------------------------------------------------------------------------------
1 | export const guaranteeInt = (
2 | value: unknown,
3 | fallback: number,
4 | base = 10
5 | ): number => {
6 | const maybeInt = tryForInt(value, base)
7 | if (typeof maybeInt === 'number') {
8 | return maybeInt
9 | }
10 | return fallback
11 | }
12 |
13 | export const tryForInt = (value: unknown, base = 10): number | void => {
14 | if (typeof value === 'number') {
15 | return Math.floor(value)
16 | }
17 | if (typeof value === 'string') {
18 | try {
19 | return parseInt(value, base)
20 | } catch (_error) {
21 | return undefined
22 | }
23 | }
24 | return undefined
25 | }
26 |
--------------------------------------------------------------------------------
/src/nodes/ui_led/types.ts:
--------------------------------------------------------------------------------
1 | import { Node, NodeDef, NodeMessage } from 'node-red'
2 | import { BeforeEmitMessage, Payload } from '../../types/node-red-dashboard'
3 | import { ColorForValueArray, LEDNodeOptions, Shape } from './shared/types'
4 |
5 | export interface LEDNodeDef extends NodeDef, LEDNodeOptions {}
6 |
7 | // TODO: test if Typescript really is forcing our key to type string
8 | export type ColorForValueMap = Record
9 |
10 | export type LEDNodeCredentials = Record
11 |
12 | export interface LEDNode extends Node {
13 | colorForValue: ColorForValueArray
14 |
15 | allowColorForValueInMessage: boolean
16 | overrideColorForValue: ColorForValueArray | ColorForValueMap
17 |
18 | shape: Shape
19 |
20 | showGlow: boolean
21 |
22 | height: number
23 | }
24 |
25 | export interface LEDBeforeEmitMessage extends BeforeEmitMessage {
26 | colorForValue?: ColorForValueArray | ColorForValueMap | void
27 | }
28 |
29 | export interface ControllerMessage extends NodeMessage {
30 | color: string
31 | glow: boolean
32 | sizeMultiplier: number
33 | }
34 |
--------------------------------------------------------------------------------
/src/nodes/ui_led/ui_led.html/constants.ts:
--------------------------------------------------------------------------------
1 | export const groupId = 'node-input-group'
2 |
3 | export const sizeId = 'node-input-size'
4 | export const widthId = 'node-input-width'
5 | export const heightId = 'node-input-height'
6 |
7 | export const labelId = 'node-input-label'
8 | export const labelPlacementId = 'node-input-labelPlacement'
9 | export const labelAlignmentId = 'node-input-labelAlignment'
10 |
11 | export const shapeId = 'node-input-shape'
12 |
13 | export const showGlowId = 'node-input-showGlow'
14 |
15 | export const showPreviewId = 'showPreview'
16 | export const showPreviewShowingClass = 'showingPreview'
17 |
18 | export const previewId = 'ui_led-preview'
19 | export const previewsContainerClass = 'previewsContainer'
20 | export const previewContainerClass = 'previewContainer'
21 |
22 | export const colorForValueEditContainerId = 'node-input-colorForValue-container'
23 | export const contextPrefix = 'node-input-colorForValue'
24 |
25 | export const rowHandleClass = contextPrefix + '-handle'
26 | export const colorFieldClass = contextPrefix + '-color'
27 | export const valueFieldClass = contextPrefix + 'value'
28 | export const valueTypeFieldClass = contextPrefix + '-valueType'
29 | export const inputErrorClass = 'input-error'
30 |
31 | export const addColorForValueId = 'node-input-add-color'
32 |
--------------------------------------------------------------------------------
/src/nodes/ui_led/ui_led.html/css.ts:
--------------------------------------------------------------------------------
1 | // from W3
2 | interface ColorNameToHex {
3 | name: string
4 | hex: string
5 | }
6 |
7 | export const Values: ColorNameToHex[] = [
8 | { name: 'aqua', hex: '#00FFFF' },
9 | { name: 'aliceblue', hex: '#F0F8FF' },
10 | { name: 'antiquewhite', hex: '#FAEBD7' },
11 | { name: 'black', hex: '#000000' },
12 | { name: 'blue', hex: '#0000FF' },
13 | { name: 'cyan', hex: '#00FFFF' },
14 | { name: 'darkblue', hex: '#00008B' },
15 | { name: 'darkcyan', hex: '#008B8B' },
16 | { name: 'darkgreen', hex: '#006400' },
17 | { name: 'darkturquoise', hex: '#00CED1' },
18 | { name: 'deepskyblue', hex: '#00BFFF' },
19 | { name: 'green', hex: '#008000' },
20 | { name: 'lime', hex: '#00FF00' },
21 | { name: 'mediumblue', hex: '#0000CD' },
22 | { name: 'mediumspringgreen', hex: '#00FA9A' },
23 | { name: 'navy', hex: '#000080' },
24 | { name: 'springgreen', hex: '#00FF7F' },
25 | { name: 'teal', hex: '#008080' },
26 | { name: 'midnightblue', hex: '#191970' },
27 | { name: 'dodgerblue', hex: '#1E90FF' },
28 | { name: 'lightseagreen', hex: '#20B2AA' },
29 | { name: 'forestgreen', hex: '#228B22' },
30 | { name: 'seagreen', hex: '#2E8B57' },
31 | { name: 'darkslategray', hex: '#2F4F4F' },
32 | { name: 'darkslategrey', hex: '#2F4F4F' },
33 | { name: 'limegreen', hex: '#32CD32' },
34 | { name: 'mediumseagreen', hex: '#3CB371' },
35 | { name: 'turquoise', hex: '#40E0D0' },
36 | { name: 'royalblue', hex: '#4169E1' },
37 | { name: 'steelblue', hex: '#4682B4' },
38 | { name: 'darkslateblue', hex: '#483D8B' },
39 | { name: 'mediumturquoise', hex: '#48D1CC' },
40 | { name: 'indigo', hex: '#4B0082' },
41 | { name: 'darkolivegreen', hex: '#556B2F' },
42 | { name: 'cadetblue', hex: '#5F9EA0' },
43 | { name: 'cornflowerblue', hex: '#6495ED' },
44 | { name: 'mediumaquamarine', hex: '#66CDAA' },
45 | { name: 'dimgray', hex: '#696969' },
46 | { name: 'dimgrey', hex: '#696969' },
47 | { name: 'slateblue', hex: '#6A5ACD' },
48 | { name: 'olivedrab', hex: '#6B8E23' },
49 | { name: 'slategray', hex: '#708090' },
50 | { name: 'slategrey', hex: '#708090' },
51 | { name: 'lightslategray', hex: '#778899' },
52 | { name: 'lightslategrey', hex: '#778899' },
53 | { name: 'mediumslateblue', hex: '#7B68EE' },
54 | { name: 'lawngreen', hex: '#7CFC00' },
55 | { name: 'aquamarine', hex: '#7FFFD4' },
56 | { name: 'chartreuse', hex: '#7FFF00' },
57 | { name: 'gray', hex: '#808080' },
58 | { name: 'grey', hex: '#808080' },
59 | { name: 'maroon', hex: '#800000' },
60 | { name: 'olive', hex: '#808000' },
61 | { name: 'purple', hex: '#800080' },
62 | { name: 'lightskyblue', hex: '#87CEFA' },
63 | { name: 'skyblue', hex: '#87CEEB' },
64 | { name: 'blueviolet', hex: '#8A2BE2' },
65 | { name: 'darkmagenta', hex: '#8B008B' },
66 | { name: 'darkred', hex: '#8B0000' },
67 | { name: 'saddlebrown', hex: '#8B4513' },
68 | { name: 'darkseagreen', hex: '#8FBC8F' },
69 | { name: 'lightgreen', hex: '#90EE90' },
70 | { name: 'mediumpurple', hex: '#9370DB' },
71 | { name: 'darkviolet', hex: '#9400D3' },
72 | { name: 'palegreen', hex: '#98FB98' },
73 | { name: 'darkorchid', hex: '#9932CC' },
74 | { name: 'yellowgreen', hex: '#9ACD32' },
75 | { name: 'sienna', hex: '#A0522D' },
76 | { name: 'brown', hex: '#A52A2A' },
77 | { name: 'darkgray', hex: '#A9A9A9' },
78 | { name: 'darkgrey', hex: '#A9A9A9' },
79 | { name: 'greenyellow', hex: '#ADFF2F' },
80 | { name: 'lightblue', hex: '#ADD8E6' },
81 | { name: 'paleturquoise', hex: '#AFEEEE' },
82 | { name: 'lightsteelblue', hex: '#B0C4DE' },
83 | { name: 'powderblue', hex: '#B0E0E6' },
84 | { name: 'firebrick', hex: '#B22222' },
85 | { name: 'darkgoldenrod', hex: '#B8860B' },
86 | { name: 'mediumorchid', hex: '#BA55D3' },
87 | { name: 'rosybrown', hex: '#BC8F8F' },
88 | { name: 'darkkhaki', hex: '#BDB76B' },
89 | { name: 'silver', hex: '#C0C0C0' },
90 | { name: 'mediumvioletred', hex: '#C71585' },
91 | { name: 'indianred', hex: '#CD5C5C' },
92 | { name: 'peru', hex: '#CD853F' },
93 | { name: 'chocolate', hex: '#D2691E' },
94 | { name: 'tan', hex: '#D2B48C' },
95 | { name: 'lightgray', hex: '#D3D3D3' },
96 | { name: 'lightgrey', hex: '#D3D3D3' },
97 | { name: 'thistle', hex: '#D8BFD8' },
98 | { name: 'goldenrod', hex: '#DAA520' },
99 | { name: 'orchid', hex: '#DA70D6' },
100 | { name: 'palevioletred', hex: '#DB7093' },
101 | { name: 'crimson', hex: '#DC143C' },
102 | { name: 'gainsboro', hex: '#DCDCDC' },
103 | { name: 'plum', hex: '#DDA0DD' },
104 | { name: 'burlywood', hex: '#DEB887' },
105 | { name: 'lightcyan', hex: '#E0FFFF' },
106 | { name: 'lavender', hex: '#E6E6FA' },
107 | { name: 'darksalmon', hex: '#E9967A' },
108 | { name: 'palegoldenrod', hex: '#EEE8AA' },
109 | { name: 'violet', hex: '#EE82EE' },
110 | { name: 'azure', hex: '#F0FFFF' },
111 | { name: 'honeydew', hex: '#F0FFF0' },
112 | { name: 'khaki', hex: '#F0E68C' },
113 | { name: 'lightcoral', hex: '#F08080' },
114 | { name: 'sandybrown', hex: '#F4A460' },
115 | { name: 'beige', hex: '#F5F5DC' },
116 | { name: 'mintcream', hex: '#F5FFFA' },
117 | { name: 'wheat', hex: '#F5DEB3' },
118 | { name: 'whitesmoke', hex: '#F5F5F5' },
119 | { name: 'ghostwhite', hex: '#F8F8FF' },
120 | { name: 'lightgoldenrodyellow', hex: '#FAFAD2' },
121 | { name: 'linen', hex: '#FAF0E6' },
122 | { name: 'salmon', hex: '#FA8072' },
123 | { name: 'oldlace', hex: '#FDF5E6' },
124 | { name: 'bisque', hex: '#FFE4C4' },
125 | { name: 'blanchedalmond', hex: '#FFEBCD' },
126 | { name: 'coral', hex: '#FF7F50' },
127 | { name: 'cornsilk', hex: '#FFF8DC' },
128 | { name: 'darkorange', hex: '#FF8C00' },
129 | { name: 'deeppink', hex: '#FF1493' },
130 | { name: 'floralwhite', hex: '#FFFAF0' },
131 | { name: 'fuchsia', hex: '#FF00FF' },
132 | { name: 'gold', hex: '#FFD700' },
133 | { name: 'hotpink', hex: '#FF69B4' },
134 | { name: 'ivory', hex: '#FFFFF0' },
135 | { name: 'lavenderblush', hex: '#FFF0F5' },
136 | { name: 'lemonchiffon', hex: '#FFFACD' },
137 | { name: 'lightpink', hex: '#FFB6C1' },
138 | { name: 'lightsalmon', hex: '#FFA07A' },
139 | { name: 'lightyellow', hex: '#FFFFE0' },
140 | { name: 'magenta', hex: '#FF00FF' },
141 | { name: 'mistyrose', hex: '#FFE4E1' },
142 | { name: 'moccasin', hex: '#FFE4B5' },
143 | { name: 'navajowhite', hex: '#FFDEAD' },
144 | { name: 'orange', hex: '#FFA500' },
145 | { name: 'orangered', hex: '#FF4500' },
146 | { name: 'papayawhip', hex: '#FFEFD5' },
147 | { name: 'peachpuff', hex: '#FFDAB9' },
148 | { name: 'pink', hex: '#FFC0CB' },
149 | { name: 'red', hex: '#FF0000' },
150 | { name: 'seashell', hex: '#FFF5EE' },
151 | { name: 'snow', hex: '#FFFAFA' },
152 | { name: 'tomato', hex: '#FF6347' },
153 | { name: 'white', hex: '#FFFFFF' },
154 | { name: 'yellow', hex: '#FFFF00' }
155 | ]
156 |
157 | const HexForNameMapDefault: Record = {}
158 | export const HexForNameMap = Values.reduce((map, object) => {
159 | map[object.name] = object.hex
160 | return map
161 | }, HexForNameMapDefault)
162 |
163 | export const hexForName = (name: string): string | undefined => {
164 | const normalized = name.toLowerCase()
165 | return HexForNameMap[normalized]
166 | }
167 |
168 | const NameForHexMapDefault: Record = {}
169 | export const NameForHexMap = Values.reduce((map, object) => {
170 | map[object.hex] = object.name
171 | return map
172 | }, NameForHexMapDefault)
173 |
174 | export const nameForHex = (hex: string): string | undefined => {
175 | let normalized = hex.toUpperCase()
176 | if (!normalized.startsWith('#')) {
177 | normalized = '#' + normalized
178 | }
179 | return NameForHexMap[normalized]
180 | }
181 |
--------------------------------------------------------------------------------
/src/nodes/ui_led/ui_led.html/defaults.ts:
--------------------------------------------------------------------------------
1 | import { NodePropertiesDef } from '@node-red/editor-client'
2 | import { EditorRED } from 'node-red'
3 | import { ColorForValueArray } from '../shared/types'
4 | import {
5 | validateColorForValueFactory,
6 | validateLabelFactory,
7 | validateWidthFactory
8 | } from './processing'
9 | import { LEDEditorNodeProperties } from './types'
10 |
11 | export const colorForValueDefault: ColorForValueArray = [
12 | {
13 | color: 'red',
14 | value: false,
15 | valueType: 'bool'
16 | },
17 | {
18 | color: 'green',
19 | value: true,
20 | valueType: 'bool'
21 | }
22 | ]
23 |
24 | export const defaultsFactory = (
25 | RED: EditorRED
26 | ): NodePropertiesDef => {
27 | return {
28 | order: { value: 0 },
29 | group: { value: 'ui_group', type: 'ui_group', required: true },
30 | width: {
31 | value: 0,
32 | validate: validateWidthFactory(RED)
33 | },
34 | height: { value: 0 },
35 | label: {
36 | value: '',
37 | validate: validateLabelFactory(RED)
38 | },
39 | labelPlacement: { value: 'left' },
40 | labelAlignment: { value: 'left' },
41 |
42 | colorForValue: {
43 | required: true,
44 | // TODO: switch to object sorted by value
45 | value: colorForValueDefault,
46 | validate: validateColorForValueFactory(RED)
47 | },
48 | allowColorForValueInMessage: { value: false },
49 |
50 | shape: { value: 'circle' },
51 |
52 | showGlow: { value: true },
53 |
54 | name: { value: '' }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/nodes/ui_led/ui_led.html/editor.html:
--------------------------------------------------------------------------------
1 |
101 |
--------------------------------------------------------------------------------
/src/nodes/ui_led/ui_led.html/help.html:
--------------------------------------------------------------------------------
1 |
10 |
--------------------------------------------------------------------------------
/src/nodes/ui_led/ui_led.html/index.ts:
--------------------------------------------------------------------------------
1 | import { EditorRED } from 'node-red'
2 | import { colorForValueDefault, defaultsFactory } from './defaults'
3 | import {
4 | addColorForValueId,
5 | colorForValueEditContainerId,
6 | groupId,
7 | heightId,
8 | rowHandleClass,
9 | shapeId,
10 | showGlowId,
11 | showPreviewId,
12 | sizeId,
13 | widthId
14 | } from './constants'
15 | import {
16 | addColorForValue,
17 | fieldKeyUpValidateNotEmpty,
18 | setChecked,
19 | setupPreviewUpdating,
20 | togglePreview
21 | } from './interaction'
22 | import { generateValueFormRow, label, labelStyle } from './rendering'
23 | import { LEDEditorNodeInstance } from './types'
24 | import { getColorForValueFromContainer } from './processing'
25 |
26 | declare const RED: EditorRED
27 |
28 | const setupColorForValue = (node: LEDEditorNodeInstance) => {
29 | for (let index = 0; index < node.colorForValue.length; index++) {
30 | const rowValue = node.colorForValue[index]
31 | generateValueFormRow(index + 1, rowValue, fieldKeyUpValidateNotEmpty)
32 | }
33 |
34 | $('#' + colorForValueEditContainerId).sortable({
35 | axis: 'y',
36 | handle: '.' + rowHandleClass,
37 | cursor: 'move'
38 | })
39 |
40 | $('#' + addColorForValueId).on('click', addColorForValue)
41 | }
42 |
43 | const oneditprepare = function (this: LEDEditorNodeInstance) {
44 | // TODO: why isn't this picking up on the interface definition additions
45 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
46 | ;($('#' + sizeId) as any).elementSizer({
47 | width: '#' + widthId,
48 | height: '#' + heightId,
49 | group: '#' + groupId
50 | })
51 |
52 | if (typeof this.colorForValue === 'undefined') {
53 | this.colorForValue = colorForValueDefault
54 | }
55 |
56 | if (typeof this.shape === 'undefined') {
57 | this.shape = 'circle'
58 | $('#' + shapeId).val('circle')
59 | }
60 |
61 | if (typeof this.showGlow === 'undefined') {
62 | this.showGlow = true
63 | setChecked('#' + showGlowId, this.showGlow)
64 | }
65 |
66 | setupColorForValue(this)
67 |
68 | $('#' + showPreviewId).on('click', () => {
69 | togglePreview()
70 | })
71 | setupPreviewUpdating(this, RED)
72 | }
73 |
74 | const oneditsave = function (this: LEDEditorNodeInstance) {
75 | this.colorForValue = getColorForValueFromContainer()
76 | }
77 |
78 | RED.nodes.registerType('ui_led', {
79 | category: 'dashboard',
80 | paletteLabel: 'led',
81 | color: 'rgb(63, 173, 181)',
82 | defaults: defaultsFactory(RED),
83 | inputs: 1,
84 | inputLabels: 'value',
85 | outputs: 0,
86 | align: 'right',
87 | label: label,
88 | labelStyle: labelStyle,
89 | icon: 'ui_led.png',
90 |
91 | oneditprepare,
92 | oneditsave
93 | })
94 |
--------------------------------------------------------------------------------
/src/nodes/ui_led/ui_led.html/interaction.ts:
--------------------------------------------------------------------------------
1 | import { EditorNodeInstance, EditorRED } from 'node-red'
2 | import { GroupNodeDef } from '../../../types/node-red-dashboard'
3 | import { LabelAlignment, LabelPlacement, Shape } from '../shared/types'
4 | import { guaranteeInt } from '../shared/utility'
5 | import {
6 | colorFieldClass,
7 | colorForValueEditContainerId,
8 | heightId,
9 | inputErrorClass,
10 | labelAlignmentId,
11 | labelId,
12 | labelPlacementId,
13 | previewId,
14 | shapeId,
15 | showGlowId,
16 | showPreviewId,
17 | showPreviewShowingClass,
18 | widthId
19 | } from './constants'
20 | import { getGroupId } from './processing'
21 | import { generateValueFormRow, preview, PreviewConfig } from './rendering'
22 | import { LEDEditorNodeInstance } from './types'
23 |
24 | export const togglePreview = (
25 | showPreviewToggleId: string = showPreviewId,
26 | previewContainerId: string = previewId
27 | ): void => {
28 | const showPreviewToggle = $('#' + showPreviewToggleId)
29 | const preview = $('#' + previewContainerId)
30 | if (showPreviewToggle.hasClass(showPreviewShowingClass)) {
31 | showPreviewToggle.removeClass(showPreviewShowingClass)
32 | preview.css('display', 'none')
33 | } else {
34 | showPreviewToggle.addClass(showPreviewShowingClass)
35 | preview.css('display', '')
36 | }
37 | }
38 |
39 | export const updatePreview = (config: PreviewConfig): void => {
40 | $('#' + previewId).html(preview(config))
41 | }
42 |
43 | export const setupPreviewUpdating = (
44 | node: LEDEditorNodeInstance,
45 | RED: EditorRED
46 | ): void => {
47 | const latestGroup = RED.nodes.node(getGroupId()) as GroupNodeDef
48 |
49 | // TODO: update on group change in case group width is different
50 | const latestConfig: PreviewConfig = {
51 | color:
52 | node.colorForValue.length > 0 ? node.colorForValue[0].color : 'green',
53 | width: guaranteeInt(node.width, 0),
54 | maxWidth: latestGroup !== undefined ? latestGroup.width : 0,
55 | height: guaranteeInt(node.height, 0),
56 | shape: node.shape,
57 | showGlow: node.showGlow,
58 | label: node.label,
59 | labelPlacement: node.labelPlacement || 'left',
60 | labelAlignment: node.labelAlignment || 'left'
61 | }
62 |
63 | const doUpdatePreview = () => {
64 | updatePreview(latestConfig)
65 | }
66 |
67 | doUpdatePreview()
68 |
69 | $('#' + widthId).on('change', (event) => {
70 | latestConfig.width = parseInt((event.target as HTMLInputElement).value, 10)
71 | doUpdatePreview()
72 | })
73 | $('#' + heightId).on('change', (event) => {
74 | latestConfig.height = parseInt((event.target as HTMLInputElement).value, 10)
75 | doUpdatePreview()
76 | })
77 | $('#' + labelId).on('change', (event) => {
78 | latestConfig.label = (event.target as HTMLInputElement).value
79 | doUpdatePreview()
80 | })
81 | $('#' + labelPlacementId).on('change', (event) => {
82 | latestConfig.labelPlacement = (event.target as HTMLInputElement)
83 | .value as LabelPlacement
84 | doUpdatePreview()
85 | })
86 | $('#' + labelAlignmentId).on('change', (event) => {
87 | latestConfig.labelAlignment = (event.target as HTMLInputElement)
88 | .value as LabelAlignment
89 | doUpdatePreview()
90 | })
91 | $('#' + shapeId).on('change', (event) => {
92 | latestConfig.shape = (event.target as HTMLInputElement).value as Shape
93 | doUpdatePreview()
94 | })
95 | $('#' + showGlowId).on('change', (event) => {
96 | latestConfig.showGlow = (event.target as HTMLInputElement).checked
97 | doUpdatePreview()
98 | })
99 |
100 | const colorChanged = (color: string) => {
101 | latestConfig.color =
102 | color !== undefined && color.length > 0 ? color : 'green'
103 | doUpdatePreview()
104 | }
105 | const colorInputChanged = (event: Event) => {
106 | colorChanged((event.target as HTMLInputElement).value)
107 | }
108 | const listChanged = (list: HTMLOListElement) => {
109 | if (list.firstChild === null) {
110 | colorChanged('green')
111 | return
112 | }
113 | for (const child of $(list).children().toArray()) {
114 | if (child === list.firstChild) {
115 | const colorInput = $(child).find('.' + colorFieldClass)
116 | colorChanged(colorInput.val())
117 |
118 | $(child).on('change', colorInputChanged)
119 | } else {
120 | $(child).off('change', colorInputChanged)
121 | }
122 | }
123 | }
124 |
125 | $('#' + colorForValueEditContainerId).on('DOMSubtreeModified', (event) => {
126 | listChanged(event.target as HTMLOListElement)
127 | })
128 | }
129 |
130 | export const addColorForValue = (): void => {
131 | generateValueFormRow(
132 | $('#' + colorForValueEditContainerId).children().length + 1,
133 | {},
134 | fieldKeyUpValidateNotEmpty
135 | )
136 | // TODO: scroll to new element
137 | $('#' + colorForValueEditContainerId).scrollTop(
138 | $('#' + colorForValueEditContainerId).get(0).scrollHeight
139 | )
140 | }
141 |
142 | export const setChecked = (query: string, checked: boolean): void => {
143 | $(query).prop('checked', checked)
144 | }
145 |
146 | export const fieldKeyUpValidateNotEmpty = function (
147 | this: EditorNodeInstance | HTMLElement
148 | ): void {
149 | const value = $(this).val()
150 |
151 | if (value && $(this).hasClass(inputErrorClass)) {
152 | $(this).removeClass(inputErrorClass)
153 | } else {
154 | if (!value) {
155 | $(this).addClass(inputErrorClass)
156 | }
157 | }
158 | }
159 |
--------------------------------------------------------------------------------
/src/nodes/ui_led/ui_led.html/processing.ts:
--------------------------------------------------------------------------------
1 | import { EditorRED } from 'node-red'
2 | import { GroupNodeDef } from '../../../types/node-red-dashboard'
3 | import { ColorForValueArray, ValueType } from '../shared/types'
4 | import { guaranteeInt } from '../shared/utility'
5 | import {
6 | colorFieldClass,
7 | colorForValueEditContainerId,
8 | groupId,
9 | valueFieldClass,
10 | valueTypeFieldClass
11 | } from './constants'
12 | import { LEDEditorNodeInstance } from './types'
13 |
14 | const WidthAutosize = 0
15 |
16 | export function getSelectedGroupNodeDef(
17 | node: LEDEditorNodeInstance,
18 | RED: EditorRED
19 | ): GroupNodeDef | undefined {
20 | const group =
21 | $('#' + groupId)
22 | .val()
23 | ?.toString() || node.group
24 | if (!group) {
25 | return undefined
26 | }
27 |
28 | return RED.nodes.node(group) as GroupNodeDef | undefined
29 | }
30 |
31 | export const willFitInGroup = (
32 | width: number,
33 | groupNode: GroupNodeDef | undefined
34 | ): boolean => {
35 | if (width === WidthAutosize) {
36 | return true
37 | }
38 |
39 | if (!groupNode) {
40 | return true
41 | }
42 |
43 | if (width > groupNode.width) {
44 | return false
45 | }
46 |
47 | return true
48 | }
49 |
50 | export const willFitWithLabel = (
51 | width: number,
52 | label: string,
53 | groupNode: GroupNodeDef | undefined
54 | ): boolean => {
55 | if (label.length === 0) {
56 | return true
57 | }
58 |
59 | if (groupNode && groupNode.width === 1) {
60 | return false
61 | }
62 |
63 | if (width === 1) {
64 | return false
65 | }
66 |
67 | return true
68 | }
69 |
70 | export const validateWidthFactory = (RED: EditorRED) => {
71 | return function (
72 | this: LEDEditorNodeInstance,
73 | newValue: string | number
74 | ): boolean {
75 | let newWidthValue: number
76 | if (typeof newValue === 'number') {
77 | newWidthValue = newValue
78 | } else if (typeof newValue === 'string') {
79 | newWidthValue = parseInt(newValue, 10)
80 | } else {
81 | return false
82 | }
83 |
84 | const groupNode = getSelectedGroupNodeDef(this, RED)
85 |
86 | const fitsInGroup = willFitInGroup(newWidthValue, groupNode)
87 | const fitsWithLabel = willFitWithLabel(newWidthValue, this.label, groupNode)
88 |
89 | $('#node-input-size').toggleClass('input-error', !fitsInGroup)
90 | $('#node-input-label').toggleClass('input-error', !fitsWithLabel)
91 | return fitsInGroup
92 | }
93 | }
94 |
95 | export const validateLabelFactory = (RED: EditorRED) => {
96 | return function (this: LEDEditorNodeInstance, newValue: string): boolean {
97 | if (newValue.length === 0) {
98 | return true
99 | }
100 |
101 | const groupNode = getSelectedGroupNodeDef(this, RED)
102 | const width = guaranteeInt(this.width, WidthAutosize)
103 | const fitsWithLabel = willFitWithLabel(width, newValue, groupNode)
104 |
105 | $('#node-input-label').toggleClass('input-error', !fitsWithLabel)
106 | return fitsWithLabel
107 | }
108 | }
109 |
110 | export const validateColorForValueFactory = (RED: EditorRED) => {
111 | return function (
112 | this: LEDEditorNodeInstance,
113 | newValue: string | ColorForValueArray
114 | ): boolean {
115 | let useNewValue: ColorForValueArray
116 | if (typeof newValue === 'string') {
117 | useNewValue = JSON.parse(newValue)
118 | } else {
119 | useNewValue = newValue
120 | }
121 | // TODO: check for duplicate values
122 | for (let index = 0; index < useNewValue.length; index++) {
123 | const colorForValue = useNewValue[index]
124 | if (!colorForValue.color || colorForValue.color.length === 0) {
125 | return false
126 | }
127 | // We're allowing undefined as a valid value
128 | // if (colorForValue.value === undefined) {
129 | // return false;
130 | // }
131 | if (!colorForValue.valueType) {
132 | // || colorForValue.color.valueType === 0) {
133 | return false
134 | }
135 | if (!RED.validators.typedInput(colorForValue.valueType)) {
136 | // console.log('Typed', colorForValue.valueType)
137 | return false
138 | }
139 | }
140 | return true
141 | }
142 | }
143 |
144 | export const getColorForValueFromContainer = (
145 | containerId: string = colorForValueEditContainerId
146 | ): ColorForValueArray => {
147 | const colorsElement = $('#' + containerId).children()
148 | return colorsElement
149 | .map((index, element) => {
150 | const jElement = $(element)
151 | const color =
152 | jElement
153 | .find('.' + colorFieldClass)
154 | .val()
155 | ?.toString() || ''
156 | const value =
157 | jElement
158 | .find('.' + valueFieldClass)
159 | .val()
160 | ?.toString() || 'null'
161 | const valueType = (jElement
162 | .find('.' + valueTypeFieldClass)
163 | .val()
164 | ?.toString() || 'str') as ValueType
165 | return {
166 | color,
167 | value,
168 | valueType
169 | }
170 | })
171 | .toArray()
172 | }
173 |
174 | export const getGroupId = (): string => {
175 | return $('#' + groupId).val()
176 | }
177 |
--------------------------------------------------------------------------------
/src/nodes/ui_led/ui_led.html/rendering.ts:
--------------------------------------------------------------------------------
1 | import { EditorNodeInstance } from 'node-red'
2 | import { hexForName, nameForHex } from './css'
3 | import { Payload } from '../../../types/node-red-dashboard'
4 | import { control } from '../shared/rendering'
5 | import {
6 | LabelAlignment,
7 | LabelPlacement,
8 | Shape,
9 | SupportedValueTypes
10 | } from '../shared/types'
11 | import {
12 | colorFieldClass,
13 | colorForValueEditContainerId,
14 | contextPrefix,
15 | inputErrorClass,
16 | previewContainerClass,
17 | previewsContainerClass,
18 | rowHandleClass,
19 | valueFieldClass,
20 | valueTypeFieldClass
21 | } from './constants'
22 |
23 | export const label = function (this: EditorNodeInstance): string {
24 | return this.name || 'led'
25 | }
26 |
27 | export const labelStyle = function (this: EditorNodeInstance): string {
28 | return this.name ? 'node_label_italic' : ''
29 | }
30 |
31 | export interface PreviewConfig {
32 | color: string
33 | width: number
34 | maxWidth: number
35 | height: number
36 | shape: Shape
37 | showGlow: boolean
38 | label: string
39 | labelPlacement: LabelPlacement
40 | labelAlignment: LabelAlignment
41 | }
42 |
43 | export const preview = (config: PreviewConfig): string => {
44 | const previewsContainerStyle = String.raw`
45 | .${previewsContainerClass} {
46 | width: calc(100% - 6px * 2);
47 | justify-content: space-around;
48 | height: ${config.height !== 0 ? `${42 * config.height + 8}px` : '50px'};
49 | display: flex;
50 | flex-direction: row;
51 | background-color: #f7f7f7;
52 | box-shadow: inset black 0px 0px 2px 0px;
53 | padding: 6px;
54 | overflow-x: scroll;
55 | border: 1px solid #00000026;
56 | }
57 | .${previewContainerClass} {
58 | justify-content: center;
59 | height: 100%;
60 | display: flex;
61 | flex-direction: row;
62 | min-width: ${config.width === 0 ? `42px` : ''};
63 | width: ${config.width !== 0 ? `${config.width * 42}px` : '100%'};
64 | max-width: ${config.maxWidth !== 0 ? `${config.maxWidth * 42}px` : ''};
65 | padding-left: 3px;
66 | padding-right: 3px;
67 | background-color: white;
68 | border: 1px solid #d3d3d3;
69 | border-radius: 5px;
70 | }
71 | `
72 | return String.raw`
73 |
76 |
77 |
78 | ${control(
79 | 'preview-no_glow',
80 | '',
81 | config.label,
82 | config.labelPlacement,
83 | config.labelAlignment,
84 | config.shape,
85 | 'gray',
86 | false,
87 | config.height !== 0 ? config.height : 1
88 | )}
89 |
90 |
91 | ${control(
92 | 'preview-glow',
93 | '',
94 | config.label,
95 | config.labelPlacement,
96 | config.labelAlignment,
97 | config.shape,
98 | config.color,
99 | config.showGlow ? true : false,
100 | config.height !== 0 ? config.height : 1
101 | )}
102 |
103 |
104 | `
105 | }
106 |
107 | export const generateValueFormRow = (
108 | index: number,
109 | value: Payload,
110 | fieldKeyUpValidateNotEmpty: (this: EditorNodeInstance | HTMLElement) => void
111 | ): void => {
112 | const requiredFieldClasses: string[] = []
113 |
114 | const containerId = 'ValueFormRow-' + index
115 | // const elementByClassInContainer = (elementClass) => {
116 | // return '#' + containerId + ' .' + elementClass
117 | // }
118 |
119 | const container = $(' ', {
120 | style:
121 | 'background: #fff; margin:0; padding:8px 0px 0px; border-bottom: 1px solid #ccc;'
122 | })
123 | const row = $('
', {
124 | style:
125 | 'width: 100%; display: flex; flex-direction: row; align-items: center; padding-bottom: 8px;'
126 | }).appendTo(container)
127 |
128 | $(' ', {
129 | class: rowHandleClass + ' fa fa-bars',
130 | style: 'color: #eee; cursor: move; margin-left: 3px;'
131 | }).appendTo(row)
132 |
133 | const valueWrapper = $('
', {
134 | style: 'min-width:30%; flex-grow:1; margin-left:10px;'
135 | })
136 |
137 | const valueField = $(' ', {
138 | type: 'text',
139 | class: valueFieldClass,
140 | // style: "min-width: 30%; flex-grow: 1; margin-left: 30px;",
141 | style: 'width: 100%',
142 | placeholder: 'Value',
143 | value: value.value
144 | }).appendTo(valueWrapper)
145 | const valueTypeField = $(' ', {
146 | type: 'hidden',
147 | class: valueTypeFieldClass,
148 | value: value.valueType
149 | }).appendTo(valueWrapper)
150 | valueWrapper.appendTo(row)
151 |
152 | valueField.typedInput({
153 | default: 'bool',
154 | typeField: valueTypeField,
155 | types: SupportedValueTypes
156 | })
157 |
158 | let rowColorFieldClass = colorFieldClass
159 | if (!value.color) {
160 | rowColorFieldClass = rowColorFieldClass + ' ' + inputErrorClass
161 | }
162 |
163 | const colorFields = $('
', {
164 | style:
165 | 'border: 1px solid #ccc; border-radius: 5px; margin-left:10px; display: flex; flex-direction: row;'
166 | }).appendTo(row)
167 |
168 | const convertOrFallback = (
169 | value: string,
170 | convert: (value: string) => string | undefined
171 | ): string => {
172 | const converted = convert(value)
173 | if (converted === undefined) {
174 | return value
175 | }
176 |
177 | return converted
178 | }
179 |
180 | const colorString: string = (value.color as string) || 'green'
181 | const colorField = $(' ', {
182 | class: rowColorFieldClass,
183 | type: 'color',
184 | style: 'width: 30px; border-width: 0;',
185 | value: convertOrFallback(colorString, hexForName)
186 | }).appendTo(colorFields)
187 |
188 | const colorTextField = $(' ', {
189 | class: rowColorFieldClass,
190 | type: 'text',
191 | style: 'flex-grow: 1; margin-left: 1px;; border-width: 0;',
192 | placeholder: 'Color',
193 | required: true,
194 | value: convertOrFallback(colorString, nameForHex)
195 | }).appendTo(colorFields)
196 |
197 | colorField.on('change', () => {
198 | if (colorTextField !== undefined && colorField !== undefined) {
199 | colorTextField.val(convertOrFallback(colorField.val(), nameForHex))
200 | }
201 | })
202 |
203 | colorTextField.on('change', () => {
204 | if (colorField !== undefined && colorTextField !== undefined) {
205 | colorField.val(convertOrFallback(colorTextField.val(), hexForName))
206 | }
207 | })
208 |
209 | requiredFieldClasses.push(colorFieldClass)
210 |
211 | colorTextField.keyup(fieldKeyUpValidateNotEmpty)
212 |
213 | const deleteButton = $(' ', {
214 | href: '#',
215 | class: 'editor-button editor-button-small',
216 | style: 'margin-left:13px; width: 20px; margin-right: 10px; right: 0px'
217 | }).appendTo(row)
218 |
219 | $(' ', {
220 | class: 'fa fa-remove',
221 | style: ''
222 | }).appendTo(deleteButton)
223 |
224 | deleteButton.click(() => {
225 | for (
226 | let requiredIndex = 0;
227 | requiredIndex < requiredFieldClasses.length;
228 | requiredIndex++
229 | ) {
230 | container
231 | .find('.' + requiredFieldClasses[requiredIndex])
232 | .removeAttr('required')
233 | }
234 | container.css({ background: '#fee' })
235 | container.fadeOut(300, function () {
236 | $(this).remove()
237 | })
238 | })
239 |
240 | $('#' + colorForValueEditContainerId).append(container)
241 | }
242 |
--------------------------------------------------------------------------------
/src/nodes/ui_led/ui_led.html/types.ts:
--------------------------------------------------------------------------------
1 | import { EditorNodeInstance, EditorNodeProperties } from 'node-red'
2 | import { LEDNodeOptions } from '../shared/types'
3 |
4 | export interface LEDEditorNodeProperties
5 | extends EditorNodeProperties,
6 | LEDNodeOptions {}
7 |
8 | export type LEDEditorNodeInstance = EditorNodeInstance
9 |
--------------------------------------------------------------------------------
/src/nodes/ui_led/ui_led.ts:
--------------------------------------------------------------------------------
1 | import { NodeInitializer } from 'node-red'
2 | import { GroupNodeInstance, NodeRedUI } from '../../types/node-red-dashboard'
3 |
4 | import { beforeEmitFactory, initController } from './processing'
5 | import { HTML } from './rendering'
6 | import { guaranteeInt, tryForInt } from './shared/utility'
7 | import { LEDNode, LEDNodeDef } from './types'
8 | import { checkConfig, mapColorForValue, nodeToStringFactory } from './utility'
9 |
10 | const nodeInit: NodeInitializer = (RED): void => {
11 | function LEDNodeConstructor(this: LEDNode, config: LEDNodeDef): void {
12 | try {
13 | const NodeREDDashboard = RED.require('node-red-dashboard')
14 | if (!NodeREDDashboard) {
15 | throw new Error(
16 | 'Node-RED dashboard is a peer requirement of this library, please install it via npm or in the palette panel.'
17 | )
18 | }
19 |
20 | RED.nodes.createNode(this, config)
21 |
22 | if (!checkConfig(config, this, RED)) {
23 | return
24 | }
25 |
26 | this.colorForValue = mapColorForValue(this, config.colorForValue, RED)
27 | this.allowColorForValueInMessage = config.allowColorForValueInMessage
28 | this.showGlow = config.showGlow !== undefined ? config.showGlow : true
29 | this.toString = nodeToStringFactory(config)
30 |
31 | // TODO: support theme and dark
32 | const ui: NodeRedUI = NodeREDDashboard(RED)
33 |
34 | const groupNode = RED.nodes.getNode(config.group) as GroupNodeInstance
35 |
36 | const width =
37 | tryForInt(config.width) ||
38 | (config.group && groupNode.config.width) ||
39 | undefined
40 | const height = guaranteeInt(config.height, 1) || 1
41 |
42 | const format = HTML(config, 'gray', false, height)
43 |
44 | this.height = height
45 |
46 | const done = ui.addWidget({
47 | node: this,
48 | width,
49 | height,
50 |
51 | format,
52 |
53 | templateScope: 'local',
54 | order: config.order,
55 |
56 | group: config.group,
57 |
58 | emitOnlyNewValues: false,
59 |
60 | beforeEmit: beforeEmitFactory(this, RED),
61 |
62 | initController
63 | })
64 |
65 | this.on('close', done)
66 | } catch (error) {
67 | console.log(error)
68 | }
69 | }
70 |
71 | RED.nodes.registerType('ui_led', LEDNodeConstructor)
72 | }
73 |
74 | export = nodeInit
75 |
--------------------------------------------------------------------------------
/src/nodes/ui_led/utility.ts:
--------------------------------------------------------------------------------
1 | import nodeRed, { NodeAPI } from 'node-red'
2 | import { ColorForValue, ColorForValueArray } from './shared/types'
3 |
4 | import { LEDNode, LEDNodeDef } from './types'
5 |
6 | /**
7 | * Check for that we have a config instance and that our config instance has a group selected, otherwise report an error
8 | * @param {object} config - The config instance
9 | * @param {object} node - The node to report the error on
10 | * @returns {boolean} `false` if we encounter an error, otherwise `true`
11 | */
12 | export const checkConfig = (
13 | config: LEDNodeDef,
14 | node: LEDNode,
15 | RED: NodeAPI
16 | ): boolean => {
17 | if (!config) {
18 | // TODO: have to think further if it makes sense to separate these out, it isn't clear what the user can do if they encounter this besides use the explicit error to more clearly debug the code
19 | node.error(RED._('ui_led.error.no-config'))
20 | return false
21 | }
22 | if (!config.group) {
23 | node.error(RED._('ui_led.error.no-group'))
24 | return false
25 | }
26 | if (RED.nodes.getNode(config.group) === undefined) {
27 | node.error(RED._('ui_led.error.invalid-group'))
28 | return false
29 | }
30 | return true
31 | }
32 |
33 | export const nodeToStringFactory = (config: LEDNodeDef) => {
34 | return (): string => {
35 | let result = 'LED'
36 | if (config.name) {
37 | result += ' name: ' + config.label
38 | }
39 | if (config.label) {
40 | result += ' label: ' + config.label
41 | }
42 | return result
43 | }
44 | }
45 |
46 | // TODO: should we be doing this at message time? less performant but does it allow config changes where doing before doesn't?
47 | export const mapColorForValue = (
48 | node: LEDNode,
49 | config: ColorForValueArray,
50 | RED: NodeAPI
51 | ): ColorForValue[] => {
52 | return config.map((value) => {
53 | return {
54 | color: value.color,
55 | value: RED.util.evaluateNodeProperty(
56 | value.value,
57 | value.valueType,
58 | node,
59 | {}
60 | ),
61 | valueType: value.valueType
62 | }
63 | })
64 | }
65 |
--------------------------------------------------------------------------------
/src/types/angular/index.d.ts:
--------------------------------------------------------------------------------
1 | import 'angular'
2 |
3 | // TODO: fill out
4 | export declare interface IScopeWatcher {
5 | eq: boolean
6 | // eslint-disable-next-line @typescript-eslint/ban-types
7 | exp: Function | string
8 | // eslint-disable-next-line @typescript-eslint/ban-types
9 | fn: Function
10 | // eslint-disable-next-line @typescript-eslint/ban-types
11 | get: Function
12 | last: string | void
13 | }
14 |
15 | // TODO: fill out
16 | export declare interface IChildScope extends ng.IScope {
17 | $$ChildScope: ng.IScope | null
18 | $$childHead: ng.IScope | null
19 | $$childTail: ng.IScope | null
20 |
21 | $$listenerCount: any
22 | $$listeners: any
23 |
24 | $$nextSibling: IChildScope | null
25 | $$prevSibling: IChildScope | null
26 |
27 | $$suspended: boolean
28 |
29 | $$watchers: IScopeWatcher[]
30 | $$watchersCount: number
31 | }
32 |
--------------------------------------------------------------------------------
/src/types/node-red-dashboard/index.d.ts:
--------------------------------------------------------------------------------
1 | import { Node, NodeMessage } from 'node-red'
2 | import { IChildScope } from '../angular'
3 |
4 | export declare type Payload = any
5 | export declare interface PayloadUpdate {
6 | update: true
7 |
8 | /**
9 | * if this update includes a new value
10 | */
11 | newPoint: boolean
12 | /**
13 | * updated value
14 | */
15 | updatedValues: Payload
16 | }
17 |
18 | export declare interface UITemplateScope extends IChildScope {
19 | msg: NodeMessage | void
20 |
21 | send: (msg: NodeMessage) => void
22 |
23 | theme: Theme | void
24 |
25 | width: string | void
26 | height: string | void
27 | label: string | void
28 |
29 | flag: boolean | void
30 |
31 | $destroy: () => void
32 | }
33 |
34 | // TODO: fill out types: https://github.com/node-red/node-red-dashboard/blob/d3bbcd5e0b24d9f0cf1425e1790e3c9bfcc28f0d/src/services/events.js
35 | export declare interface UiEvents {
36 | id: string
37 | connect: (onuiloaded: any, replaydone: any) => void
38 | emit: (event: any, msg: any) => void
39 |
40 | /**
41 | * @returns cancel function
42 | */
43 | on: (event: any, handler: any) => () => void
44 | }
45 |
46 | export declare type Convert = (
47 | value: Payload,
48 | oldValue: Payload,
49 | msg: NodeMessage,
50 | controlStep: number
51 | ) => Payload | PayloadUpdate | undefined
52 |
53 | export declare type CustomMessage = NodeMessage & Record
54 |
55 | export declare interface BeforeEmitMessage extends CustomMessage {
56 | ui_control?: Record | void
57 | }
58 | export declare interface Emit extends Record {
59 | msg: Payload
60 |
61 | id?: number | undefined
62 | }
63 | export declare type BeforeEmit = (
64 | msg: BeforeEmitMessage,
65 | value: Payload
66 | ) => Emit
67 |
68 | export declare type ConvertBack = (value: Payload) => Payload
69 |
70 | export declare type BeforeSend = (
71 | toSend: { payload: Payload },
72 | msg: CustomMessage
73 | ) => NodeMessage | void
74 |
75 | export declare type InitController = (
76 | scope: UITemplateScope,
77 | events: UiEvents
78 | ) => void
79 |
80 | export declare interface WidgetOptions<
81 | TCreds extends Record = Record
82 | > {
83 | /** [node] - the node that represents the control on a flow */
84 | node: Node
85 |
86 | /** format - HTML code of widget */
87 | format: string
88 |
89 | /** [group] - group name (optional if templateScope = 'global') */
90 | group?: string | void
91 |
92 | /** [width] - width of widget (default automatic) */
93 | width?: number | void
94 |
95 | /** [height] - height of widget (default automatic) */
96 | height?: number | void
97 |
98 | /** [order] - property to hold the placement order of the widget */
99 | order: number
100 |
101 | /** [templateScope] - scope of widget/global or local (default local) */
102 | templateScope?: 'global' | string | void
103 |
104 | /**
105 | * [emitOnlyNewValues] - `boolean` (default true).
106 | * If true, it checks if the payload changed before sending it
107 | * to the front-end. If the payload is the same no message is sent.
108 | */
109 | emitOnlyNewValues?: boolean | void
110 |
111 | /**
112 | * [forwardInputMessages] - `boolean` (default true).
113 | * If true, forwards input messages to the output
114 | */
115 | forwardInputMessages?: boolean | void
116 |
117 | /**
118 | * storeFrontEndInputAsState] - `boolean` (default true).
119 | * If true, any message received from front-end is stored as state
120 | */
121 | storeFrontEndInputAsState?: boolean | void
122 |
123 | /**
124 | * [persistantFrontEndValue] - `boolean` (default true).
125 | * If true, last received message is send again when front end reconnect.
126 | */
127 | persistantFrontEndValue?: boolean | void
128 |
129 | /**
130 | * [convert] - callback to convert the value before sending it to the front-end
131 | * @returns `Payload`, `PayloadUpdate` or `undefined`. If `undefined` is returned `oldValue` is used and marked as a new value
132 | */
133 | convert?: Convert | void
134 |
135 | /** [beforeEmit] - callback to prepare the message that is emitted to the front-end */
136 | beforeEmit?: BeforeEmit | void
137 |
138 | /** [convertBack] - callback to convert the message from front-end before sending it to the next connected node */
139 | convertBack?: ConvertBack | void
140 |
141 | /** [beforeSend] - callback to prepare the message that is sent to the output */
142 | beforeSend?: BeforeSend | void
143 |
144 | /** [initController] - callback to initialize in controller */
145 | initController?: InitController | void
146 | }
147 |
148 | export declare interface NodeRedUI<
149 | TCreds extends Record = Record
150 | > {
151 | addWidget(options: WidgetOptions): () => void
152 | }
153 |
154 | export declare interface GroupNodeDef {
155 | width: number
156 | height: number
157 | }
158 |
159 | export declare interface GroupNodeInstance extends Node {
160 | config: GroupNodeDef
161 | }
162 |
163 | // TODO: fill in
164 | export declare type Theme = any
165 |
166 | export interface JQuery {
167 | elementSizer(options: {
168 | width: string
169 | height: string
170 | group: string
171 | auto?: boolean | void
172 | }): void
173 |
174 | elementSizerByNum(options: {
175 | label: string
176 | has_height: boolean
177 | pos: number
178 | width: number
179 | height: number
180 | groupNode: {
181 | width: number
182 | height: number
183 | }
184 | }): void
185 | }
186 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2018",
4 | "module": "commonjs",
5 | "lib": ["es2018", "esnext.asynciterable", "dom"],
6 | "allowJs": false,
7 | "noEmitOnError": true,
8 | "sourceMap": true,
9 | "outDir": "dist",
10 | "rootDir": "src",
11 | "removeComments": false,
12 | "noEmit": false,
13 | "importHelpers": false,
14 | "preserveWatchOutput": true,
15 |
16 | "strict": true,
17 | "noImplicitAny": true,
18 | "strictNullChecks": true,
19 |
20 | "noUnusedLocals": false,
21 | "noUnusedParameters": false,
22 | "noFallthroughCasesInSwitch": true,
23 |
24 | "moduleResolution": "node",
25 | "baseUrl": "./",
26 | "allowSyntheticDefaultImports": true,
27 | "esModuleInterop": true,
28 | "preserveConstEnums": true,
29 | "forceConsistentCasingInFileNames": true,
30 |
31 | "typeRoots": ["./src/types", "./node_modules/@types"]
32 | },
33 | "include": ["src"],
34 | "exclude": ["node_modules"]
35 | }
36 |
--------------------------------------------------------------------------------
/tsconfig.runtime.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "include": ["src"],
4 | "exclude": ["node_modules", "src/__tests__", "src/nodes/*/*.html"]
5 | }
6 |
--------------------------------------------------------------------------------
/tsconfig.runtime.watch.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.runtime.json",
3 | "compilerOptions": {
4 | "incremental": true,
5 | "tsBuildInfoFile": "dist/tsconfig.tsbuildinfo"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------