├── .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 | ![CI](https://github.com/Adorkable/node-red-contrib-ui-led/workflows/CI/badge.svg) 6 | [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2FAdorkable%2Fnode-red-contrib-ui-led.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2FAdorkable%2Fnode-red-contrib-ui-led?ref=badge_shield) 7 | [![dependencies](https://img.shields.io/librariesio/release/github/adorkable/node-red-contrib-ui-led?style=flat)](https://github.com/Adorkable/node-red-contrib-ui-led/network/dependencies) 8 | 9 | ![Examples Image](images/examples.png) 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 | Sizes 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 | ![Edit panel](images/preview_changes.gif) 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 | ![Colors for Values Image](images/colorsForValues.png) 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 | [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2FAdorkable%2Fnode-red-contrib-ui-led.svg?type=large)](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 | --------------------------------------------------------------------------------