node-red-contrib-short-circuit 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +51 -0
- package/package.json +41 -0
- package/short-circuit.html +796 -0
- package/short-circuit.js +77 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Robert W. (Robert0)
|
|
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.
|
package/README.md
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
An Node-RED editor plugin that replaces long wires with short, interactive endpoints to reduce clutter in large flows.
|
|
2
|
+
|
|
3
|
+
## Behaviour
|
|
4
|
+
|
|
5
|
+
When the direct distance between a wire's two endpoints exceeds the configured threshold:
|
|
6
|
+
|
|
7
|
+
- The normal wire is hidden
|
|
8
|
+
- A short stub and endpoints are shown at both connected nodes
|
|
9
|
+
<img width="538" height="174" alt="image" src="https://github.com/user-attachments/assets/ab0ecfd8-17bd-4afd-8c01-400149c2217b" />
|
|
10
|
+
|
|
11
|
+
- Hovering an endpoint shows Node-RED's native ghost-link preview (the inter-link-node) with the node name at the other end
|
|
12
|
+
<img width="258" height="146" alt="image" src="https://github.com/user-attachments/assets/5289f7c0-7070-460b-b797-9e69843b639f" />
|
|
13
|
+
|
|
14
|
+
- Clicking the ghosted preview pans to and highlights that node
|
|
15
|
+
- Clicking the circular endpoint reveals the true wire
|
|
16
|
+
<img width="487" height="230" alt="image" src="https://github.com/user-attachments/assets/72a8453f-5ea0-4d50-a160-cfcc037efa2f" />
|
|
17
|
+
|
|
18
|
+
- Selecting either connected node reveals all shortened wires attached to that node
|
|
19
|
+
|
|
20
|
+
## Distance configuration
|
|
21
|
+
|
|
22
|
+
The distance is configured in a file in your node-red directory after installing:
|
|
23
|
+
|
|
24
|
+
```text
|
|
25
|
+
.nmode-red/short-circuit.json
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
```json
|
|
29
|
+
{
|
|
30
|
+
"maxDistance": 1000
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
After editing the JSON file, refresh the Node-RED editor. The runtime reads the file again whenever the editor loads, so a Node-RED restart is not required.
|
|
35
|
+
|
|
36
|
+
If the file contains invalid JSON or an invalid distance, the plugin uses the default value of `1000` and writes a warning to the Node-RED log.
|
|
37
|
+
|
|
38
|
+
## Compatibility
|
|
39
|
+
|
|
40
|
+
- Node-RED 4.0 or newer
|
|
41
|
+
- Node.js 18 or newer
|
|
42
|
+
|
|
43
|
+
This plugin also works great with the quick-link plugin:
|
|
44
|
+
|
|
45
|
+
```text
|
|
46
|
+
[node-red-contrib-quick-link](https://flows.nodered.org/node/node-red-contrib-quick-link)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## License
|
|
50
|
+
|
|
51
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "node-red-contrib-short-circuit",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A Node-RED editor plugin that replaces long wires with clickable short-circuit endpoints.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"node-red",
|
|
7
|
+
"node-red-plugin",
|
|
8
|
+
"editor",
|
|
9
|
+
"wire",
|
|
10
|
+
"flow"
|
|
11
|
+
],
|
|
12
|
+
"homepage": "https://github.com/Roberto0o/node-red-contrib-short-circuit#readme",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/Roberto0o/node-red-contrib-short-circuit/issues"
|
|
15
|
+
},
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/Roberto0o/node-red-contrib-short-circuit.git"
|
|
19
|
+
},
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"author": {
|
|
22
|
+
"name": "Robert W. (Robert0)",
|
|
23
|
+
"url": "https://github.com/Roberto0o"
|
|
24
|
+
},
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=18"
|
|
27
|
+
},
|
|
28
|
+
"node-red": {
|
|
29
|
+
"version": ">=4.0.0",
|
|
30
|
+
"plugins": {
|
|
31
|
+
"short-circuit": "short-circuit.js"
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"short-circuit.js",
|
|
36
|
+
"short-circuit.html",
|
|
37
|
+
"README.md",
|
|
38
|
+
"LICENSE",
|
|
39
|
+
"examples/test-flow.json"
|
|
40
|
+
]
|
|
41
|
+
}
|
|
@@ -0,0 +1,796 @@
|
|
|
1
|
+
<style>
|
|
2
|
+
g.red-ui-flow-link.nr-short-circuit-hidden > .red-ui-flow-link-path {
|
|
3
|
+
visibility: hidden;
|
|
4
|
+
pointer-events: none;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
g.red-ui-flow-link.nr-short-circuit-hidden.nr-short-circuit-revealed
|
|
8
|
+
> .red-ui-flow-link-background {
|
|
9
|
+
visibility: visible;
|
|
10
|
+
pointer-events: stroke;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
g.red-ui-flow-link.nr-short-circuit-hidden.nr-short-circuit-revealed
|
|
14
|
+
> .red-ui-flow-link-outline {
|
|
15
|
+
visibility: visible;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
g.red-ui-flow-link.nr-short-circuit-hidden.nr-short-circuit-revealed
|
|
19
|
+
> .red-ui-flow-link-line {
|
|
20
|
+
visibility: visible;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.nr-short-circuit-overlay {
|
|
24
|
+
pointer-events: none;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.nr-short-circuit-end {
|
|
28
|
+
pointer-events: none;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.nr-short-circuit-stub {
|
|
32
|
+
fill: none;
|
|
33
|
+
stroke: var(--red-ui-link-color);
|
|
34
|
+
stroke-width: 3;
|
|
35
|
+
stroke-linecap: round;
|
|
36
|
+
pointer-events: none;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.nr-short-circuit-dot {
|
|
40
|
+
fill: var(--red-ui-link-color);
|
|
41
|
+
stroke: var(--red-ui-view-background);
|
|
42
|
+
stroke-width: 1.5;
|
|
43
|
+
pointer-events: none;
|
|
44
|
+
transition: r 80ms ease;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.nr-short-circuit-hit {
|
|
48
|
+
fill: transparent;
|
|
49
|
+
stroke: transparent;
|
|
50
|
+
stroke-width: 1;
|
|
51
|
+
cursor: pointer;
|
|
52
|
+
pointer-events: all;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.nr-short-circuit-end:hover .nr-short-circuit-dot,
|
|
56
|
+
.nr-short-circuit-end.nr-short-circuit-active .nr-short-circuit-dot {
|
|
57
|
+
r: 5.5px;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.nr-short-circuit-end:hover .nr-short-circuit-stub,
|
|
61
|
+
.nr-short-circuit-end.nr-short-circuit-active .nr-short-circuit-stub {
|
|
62
|
+
stroke-width: 4;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.nr-short-circuit-preview {
|
|
66
|
+
opacity: 0;
|
|
67
|
+
pointer-events: none;
|
|
68
|
+
transition: opacity 100ms ease;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.nr-short-circuit-end:hover .nr-short-circuit-preview,
|
|
72
|
+
.nr-short-circuit-end:focus-within .nr-short-circuit-preview {
|
|
73
|
+
opacity: 1;
|
|
74
|
+
pointer-events: all;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.nr-short-circuit-preview .red-ui-flow-link-group {
|
|
78
|
+
cursor: pointer;
|
|
79
|
+
opacity: 0.72;
|
|
80
|
+
transition: opacity 100ms ease;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.nr-short-circuit-preview:hover .red-ui-flow-link-group,
|
|
84
|
+
.nr-short-circuit-preview:focus-within .red-ui-flow-link-group {
|
|
85
|
+
opacity: 1;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.nr-short-circuit-preview .red-ui-flow-link-group > * {
|
|
89
|
+
pointer-events: none;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.nr-short-circuit-preview-bridge {
|
|
93
|
+
fill: none;
|
|
94
|
+
stroke: transparent;
|
|
95
|
+
stroke-width: 24;
|
|
96
|
+
pointer-events: stroke !important;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.nr-short-circuit-preview-hit {
|
|
100
|
+
fill: transparent;
|
|
101
|
+
stroke: none;
|
|
102
|
+
cursor: pointer;
|
|
103
|
+
pointer-events: all !important;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.nr-short-circuit-preview-label {
|
|
107
|
+
user-select: none;
|
|
108
|
+
pointer-events: none;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
</style>
|
|
112
|
+
|
|
113
|
+
<script type="text/javascript">
|
|
114
|
+
(function() {
|
|
115
|
+
"use strict";
|
|
116
|
+
|
|
117
|
+
const PLUGIN_ID = "node-red-contrib-short-circuit";
|
|
118
|
+
const SVG_NS = "http://www.w3.org/2000/svg";
|
|
119
|
+
|
|
120
|
+
const defaults = {
|
|
121
|
+
maxDistance: 1000,
|
|
122
|
+
stubLength: 24,
|
|
123
|
+
dotRadius: 4,
|
|
124
|
+
hitRadius: 12,
|
|
125
|
+
previewSpacing: 40
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const settings = Object.assign({}, defaults);
|
|
129
|
+
|
|
130
|
+
const activeLinkKeys = new Set();
|
|
131
|
+
let refreshTimer = null;
|
|
132
|
+
let refreshFrame = null;
|
|
133
|
+
let initialised = false;
|
|
134
|
+
|
|
135
|
+
function createSvgElement(name, className) {
|
|
136
|
+
const element = document.createElementNS(SVG_NS, name);
|
|
137
|
+
if (className) {
|
|
138
|
+
element.setAttribute("class", className);
|
|
139
|
+
}
|
|
140
|
+
return element;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function clampDistance(value) {
|
|
144
|
+
const parsed = Number(value);
|
|
145
|
+
if (!Number.isFinite(parsed)) {
|
|
146
|
+
return defaults.maxDistance;
|
|
147
|
+
}
|
|
148
|
+
return Math.min(5000, Math.max(50, Math.round(parsed)));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function loadConfiguration() {
|
|
152
|
+
return $.ajax({
|
|
153
|
+
url: "short-circuit/config",
|
|
154
|
+
type: "GET",
|
|
155
|
+
dataType: "json",
|
|
156
|
+
cache: false
|
|
157
|
+
}).done(function(config) {
|
|
158
|
+
settings.maxDistance = clampDistance(config && config.maxDistance);
|
|
159
|
+
}).fail(function(jqXHR, textStatus, errorThrown) {
|
|
160
|
+
settings.maxDistance = defaults.maxDistance;
|
|
161
|
+
console.warn(
|
|
162
|
+
"Short circuit could not load short-circuit.json; using the default distance.",
|
|
163
|
+
textStatus,
|
|
164
|
+
errorThrown
|
|
165
|
+
);
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function getLinkKey(link) {
|
|
170
|
+
const targetPort = Number.isFinite(link.targetPort) ? link.targetPort : (link.target.i || 0);
|
|
171
|
+
return [
|
|
172
|
+
link.source.id,
|
|
173
|
+
link.sourcePort || 0,
|
|
174
|
+
link.target.id,
|
|
175
|
+
targetPort
|
|
176
|
+
].join(":");
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function getLinkCoordinates(link) {
|
|
180
|
+
if (
|
|
181
|
+
Number.isFinite(link.x1) &&
|
|
182
|
+
Number.isFinite(link.y1) &&
|
|
183
|
+
Number.isFinite(link.x2) &&
|
|
184
|
+
Number.isFinite(link.y2)
|
|
185
|
+
) {
|
|
186
|
+
return {
|
|
187
|
+
x1: link.x1,
|
|
188
|
+
y1: link.y1,
|
|
189
|
+
x2: link.x2,
|
|
190
|
+
y2: link.y2
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (!link.source || !link.target) {
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const outputCount = link.source.outputs || 1;
|
|
199
|
+
const sourcePort = link.sourcePort || 0;
|
|
200
|
+
const sourcePortOffset = -((outputCount - 1) / 2) * 13 + (13 * sourcePort);
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
x1: link.source.x + ((link.source.w || 100) / 2),
|
|
204
|
+
y1: link.source.y + sourcePortOffset,
|
|
205
|
+
x2: link.target.x - ((link.target.w || 100) / 2),
|
|
206
|
+
y2: link.target.y
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function shouldShortenLink(link, coordinates) {
|
|
211
|
+
if (!link || !coordinates || link.__ghost || link.link) {
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Normal Node-RED wires remain inside one flow or subflow
|
|
216
|
+
if (!link.source || !link.target || link.source.z !== link.target.z) {
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const deltaX = coordinates.x2 - coordinates.x1;
|
|
221
|
+
const deltaY = coordinates.y2 - coordinates.y1;
|
|
222
|
+
return Math.hypot(deltaX, deltaY) > settings.maxDistance;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function restoreNativeLink(group) {
|
|
226
|
+
group.classList.remove("nr-short-circuit-hidden", "nr-short-circuit-revealed");
|
|
227
|
+
|
|
228
|
+
group.querySelectorAll(".red-ui-flow-link-path").forEach(function(path) {
|
|
229
|
+
const originalPath = path.getAttribute("data-nr-short-circuit-original-d");
|
|
230
|
+
const adjustedPath = path.getAttribute("data-nr-short-circuit-adjusted-d");
|
|
231
|
+
|
|
232
|
+
if (originalPath !== null) {
|
|
233
|
+
// Do not overwrite a fresh path if Node-RED redrew it while it was revealed
|
|
234
|
+
if (adjustedPath === null || path.getAttribute("d") === adjustedPath) {
|
|
235
|
+
path.setAttribute("d", originalPath);
|
|
236
|
+
}
|
|
237
|
+
path.removeAttribute("data-nr-short-circuit-original-d");
|
|
238
|
+
path.removeAttribute("data-nr-short-circuit-adjusted-d");
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (path.getAttribute("data-nr-short-circuit-link-class") === "added") {
|
|
242
|
+
path.classList.remove("red-ui-flow-link-link");
|
|
243
|
+
path.removeAttribute("data-nr-short-circuit-link-class");
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function getWireColour(group) {
|
|
249
|
+
const line = group.querySelector(".red-ui-flow-link-line");
|
|
250
|
+
if (!line) {
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return window.getComputedStyle(line).stroke || null;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function formatCoordinate(value) {
|
|
258
|
+
return Number(value.toFixed(3));
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function adjustCubicPathEndpoints(pathData, startX, startY, endX, endY) {
|
|
262
|
+
const cubicPattern = /^\s*M\s*(-?\d*\.?\d+)\s*[, ]\s*(-?\d*\.?\d+)\s*C\s*(-?\d*\.?\d+)\s*[, ]\s*(-?\d*\.?\d+)\s*[, ]\s*(-?\d*\.?\d+)\s*[, ]\s*(-?\d*\.?\d+)\s*[, ]\s*(-?\d*\.?\d+)\s*[, ]\s*(-?\d*\.?\d+)\s*$/i;
|
|
263
|
+
const match = cubicPattern.exec(pathData || "");
|
|
264
|
+
|
|
265
|
+
if (match) {
|
|
266
|
+
const originalStartX = Number(match[1]);
|
|
267
|
+
const originalStartY = Number(match[2]);
|
|
268
|
+
const firstControlX = Number(match[3]);
|
|
269
|
+
const firstControlY = Number(match[4]);
|
|
270
|
+
const secondControlX = Number(match[5]);
|
|
271
|
+
const secondControlY = Number(match[6]);
|
|
272
|
+
const originalEndX = Number(match[7]);
|
|
273
|
+
const originalEndY = Number(match[8]);
|
|
274
|
+
|
|
275
|
+
const sourceShiftX = startX - originalStartX;
|
|
276
|
+
const sourceShiftY = startY - originalStartY;
|
|
277
|
+
const targetShiftX = endX - originalEndX;
|
|
278
|
+
const targetShiftY = endY - originalEndY;
|
|
279
|
+
|
|
280
|
+
return [
|
|
281
|
+
"M", formatCoordinate(startX), formatCoordinate(startY),
|
|
282
|
+
"C",
|
|
283
|
+
formatCoordinate(firstControlX + sourceShiftX),
|
|
284
|
+
formatCoordinate(firstControlY + sourceShiftY),
|
|
285
|
+
formatCoordinate(secondControlX + targetShiftX),
|
|
286
|
+
formatCoordinate(secondControlY + targetShiftY),
|
|
287
|
+
formatCoordinate(endX), formatCoordinate(endY)
|
|
288
|
+
].join(" ");
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const curve = Math.max(30, Math.min(75, Math.abs(endX - startX) / 2));
|
|
292
|
+
return [
|
|
293
|
+
"M", formatCoordinate(startX), formatCoordinate(startY),
|
|
294
|
+
"C",
|
|
295
|
+
formatCoordinate(startX + curve), formatCoordinate(startY),
|
|
296
|
+
formatCoordinate(endX - curve), formatCoordinate(endY),
|
|
297
|
+
formatCoordinate(endX), formatCoordinate(endY)
|
|
298
|
+
].join(" ");
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function applyNativeLinkReveal(group, coordinates) {
|
|
302
|
+
const sourceTipX = coordinates.x1 + settings.stubLength + settings.dotRadius;
|
|
303
|
+
const targetTipX = coordinates.x2 - settings.stubLength - settings.dotRadius;
|
|
304
|
+
|
|
305
|
+
group.querySelectorAll(".red-ui-flow-link-path").forEach(function(path) {
|
|
306
|
+
const originalPath = path.getAttribute("d");
|
|
307
|
+
if (originalPath) {
|
|
308
|
+
const adjustedPath = adjustCubicPathEndpoints(
|
|
309
|
+
originalPath,
|
|
310
|
+
sourceTipX,
|
|
311
|
+
coordinates.y1,
|
|
312
|
+
targetTipX,
|
|
313
|
+
coordinates.y2
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
path.setAttribute("data-nr-short-circuit-original-d", originalPath);
|
|
317
|
+
path.setAttribute("data-nr-short-circuit-adjusted-d", adjustedPath);
|
|
318
|
+
path.setAttribute("d", adjustedPath);
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
[
|
|
323
|
+
group.querySelector(".red-ui-flow-link-background"),
|
|
324
|
+
group.querySelector(".red-ui-flow-link-line")
|
|
325
|
+
].forEach(function(path) {
|
|
326
|
+
if (path && !path.classList.contains("red-ui-flow-link-link")) {
|
|
327
|
+
path.classList.add("red-ui-flow-link-link");
|
|
328
|
+
path.setAttribute("data-nr-short-circuit-link-class", "added");
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function getNodeLabel(node) {
|
|
334
|
+
if (!node) {
|
|
335
|
+
return "Unknown node";
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (typeof node.name === "string" && node.name.trim()) {
|
|
339
|
+
return node.name.trim();
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (node._def && node._def.label) {
|
|
343
|
+
try {
|
|
344
|
+
const label = typeof node._def.label === "function"
|
|
345
|
+
? node._def.label.call(node)
|
|
346
|
+
: node._def.label;
|
|
347
|
+
|
|
348
|
+
if (typeof label === "string" && label.trim()) {
|
|
349
|
+
return label.trim();
|
|
350
|
+
}
|
|
351
|
+
} catch (error) {
|
|
352
|
+
// A node label function should not be able to break the overlay
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return node.type || "Node";
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function shortenLabel(label, maxLength) {
|
|
360
|
+
if (label.length <= maxLength) {
|
|
361
|
+
return label;
|
|
362
|
+
}
|
|
363
|
+
return label.slice(0, Math.max(1, maxLength - 1)) + "…";
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function stopWorkspaceAction(event) {
|
|
367
|
+
event.preventDefault();
|
|
368
|
+
event.stopPropagation();
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function revealNode(node, event) {
|
|
372
|
+
stopWorkspaceAction(event);
|
|
373
|
+
activeLinkKeys.clear();
|
|
374
|
+
scheduleRefresh();
|
|
375
|
+
|
|
376
|
+
if (node && RED.view && typeof RED.view.reveal === "function") {
|
|
377
|
+
RED.view.reveal(node.id);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function addNodePreview(endGroup, dotX, y, direction, endpoint, verticalOffset) {
|
|
382
|
+
const preview = createSvgElement("g", "nr-short-circuit-preview");
|
|
383
|
+
const nativeGroup = createSvgElement("g", "red-ui-flow-link-group");
|
|
384
|
+
const offset = settings.dotRadius - 30;
|
|
385
|
+
const previewY = y + verticalOffset;
|
|
386
|
+
const label = getNodeLabel(endpoint.otherNode);
|
|
387
|
+
const connectorStartX = 30 + offset;
|
|
388
|
+
const connectorEndX = 60 + offset;
|
|
389
|
+
const connectorStartY = -verticalOffset;
|
|
390
|
+
const connectorPath = [
|
|
391
|
+
"M", connectorStartX, connectorStartY,
|
|
392
|
+
"C",
|
|
393
|
+
connectorStartX + 18, connectorStartY,
|
|
394
|
+
connectorEndX - 18, 0,
|
|
395
|
+
connectorEndX, 0
|
|
396
|
+
].join(" ");
|
|
397
|
+
|
|
398
|
+
nativeGroup.setAttribute(
|
|
399
|
+
"transform",
|
|
400
|
+
"translate(" + dotX + " " + previewY + ") scale(" + direction + " 1)"
|
|
401
|
+
);
|
|
402
|
+
|
|
403
|
+
const connector = createSvgElement("path", "red-ui-flow-link-link");
|
|
404
|
+
connector.setAttribute("d", connectorPath);
|
|
405
|
+
|
|
406
|
+
// This invisible wide path keeps the preview open while the pointer
|
|
407
|
+
// moves along the curved connector from the endpoint to the label
|
|
408
|
+
const bridge = createSvgElement("path", "nr-short-circuit-preview-bridge");
|
|
409
|
+
bridge.setAttribute("d", connectorPath);
|
|
410
|
+
|
|
411
|
+
const portOutline = createSvgElement("path", "red-ui-flow-link-port");
|
|
412
|
+
portOutline.setAttribute(
|
|
413
|
+
"d",
|
|
414
|
+
"M " + (77 + offset) + " -12 h -10 a 3 3 45 0 0 -3 3 " +
|
|
415
|
+
"v 18 a 3 3 45 0 0 3 3 h 10"
|
|
416
|
+
);
|
|
417
|
+
|
|
418
|
+
const continuation = createSvgElement("path", "red-ui-flow-link-port");
|
|
419
|
+
continuation.setAttribute(
|
|
420
|
+
"d",
|
|
421
|
+
"M " + (80 + offset) + " -12 h 30 M " +
|
|
422
|
+
(80 + offset) + " 12 h 30"
|
|
423
|
+
);
|
|
424
|
+
continuation.style.strokeDasharray = "12, 3, 8, 4, 3";
|
|
425
|
+
|
|
426
|
+
const port = createSvgElement("rect", "red-ui-flow-port red-ui-flow-link-port");
|
|
427
|
+
port.setAttribute("x", 60 + offset);
|
|
428
|
+
port.setAttribute("y", -4);
|
|
429
|
+
port.setAttribute("rx", 2);
|
|
430
|
+
port.setAttribute("ry", 2);
|
|
431
|
+
port.setAttribute("width", 8);
|
|
432
|
+
port.setAttribute("height", 8);
|
|
433
|
+
|
|
434
|
+
const hit = createSvgElement("rect", "nr-short-circuit-preview-hit");
|
|
435
|
+
hit.setAttribute("x", 60 + offset);
|
|
436
|
+
hit.setAttribute("y", -12);
|
|
437
|
+
hit.setAttribute("width", 100);
|
|
438
|
+
hit.setAttribute("height", 24);
|
|
439
|
+
hit.setAttribute("role", "button");
|
|
440
|
+
hit.setAttribute("tabindex", "0");
|
|
441
|
+
hit.setAttribute("aria-label", "Go to " + label);
|
|
442
|
+
|
|
443
|
+
const title = createSvgElement("title");
|
|
444
|
+
title.textContent = "Go to " + label;
|
|
445
|
+
hit.appendChild(title);
|
|
446
|
+
|
|
447
|
+
const text = createSvgElement(
|
|
448
|
+
"text",
|
|
449
|
+
"red-ui-flow-port-label nr-short-circuit-preview-label"
|
|
450
|
+
);
|
|
451
|
+
text.setAttribute("x", dotX + (direction * (75 + offset)));
|
|
452
|
+
text.setAttribute("y", previewY + 1);
|
|
453
|
+
text.setAttribute(
|
|
454
|
+
"style",
|
|
455
|
+
"font-size: 10px; text-anchor: " + (direction === 1 ? "start" : "end") + ";"
|
|
456
|
+
);
|
|
457
|
+
text.textContent = shortenLabel(label, 18);
|
|
458
|
+
|
|
459
|
+
function goToNode(event) {
|
|
460
|
+
revealNode(endpoint.otherNode, event);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
hit.addEventListener("mousedown", stopWorkspaceAction);
|
|
464
|
+
hit.addEventListener("mouseup", stopWorkspaceAction);
|
|
465
|
+
hit.addEventListener("touchstart", stopWorkspaceAction, { passive: false });
|
|
466
|
+
hit.addEventListener("touchend", goToNode, { passive: false });
|
|
467
|
+
hit.addEventListener("click", goToNode);
|
|
468
|
+
hit.addEventListener("keydown", function(event) {
|
|
469
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
470
|
+
goToNode(event);
|
|
471
|
+
}
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
nativeGroup.appendChild(connector);
|
|
475
|
+
nativeGroup.appendChild(bridge);
|
|
476
|
+
nativeGroup.appendChild(portOutline);
|
|
477
|
+
nativeGroup.appendChild(continuation);
|
|
478
|
+
nativeGroup.appendChild(port);
|
|
479
|
+
nativeGroup.appendChild(hit);
|
|
480
|
+
preview.appendChild(nativeGroup);
|
|
481
|
+
preview.appendChild(text);
|
|
482
|
+
endGroup.appendChild(preview);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function addEndpointGroup(overlay, options) {
|
|
486
|
+
const linkKeys = options.entries.map(function(entry) {
|
|
487
|
+
return entry.linkKey;
|
|
488
|
+
});
|
|
489
|
+
const isActive = options.entries.some(function(entry) {
|
|
490
|
+
return entry.isActive;
|
|
491
|
+
});
|
|
492
|
+
const allExplicitlyActive = linkKeys.length > 0 && linkKeys.every(function(linkKey) {
|
|
493
|
+
return activeLinkKeys.has(linkKey);
|
|
494
|
+
});
|
|
495
|
+
const endGroup = createSvgElement(
|
|
496
|
+
"g",
|
|
497
|
+
"nr-short-circuit-end" + (isActive ? " nr-short-circuit-active" : "")
|
|
498
|
+
);
|
|
499
|
+
|
|
500
|
+
const dotX = options.x + (options.direction * settings.stubLength);
|
|
501
|
+
const lineEndX = dotX - (options.direction * settings.dotRadius);
|
|
502
|
+
const firstColour = options.entries.find(function(entry) {
|
|
503
|
+
return entry.colour;
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
const stub = createSvgElement("path", "nr-short-circuit-stub");
|
|
507
|
+
stub.setAttribute("d", "M " + options.x + " " + options.y + " H " + lineEndX);
|
|
508
|
+
|
|
509
|
+
const dot = createSvgElement("circle", "nr-short-circuit-dot");
|
|
510
|
+
dot.setAttribute("cx", dotX);
|
|
511
|
+
dot.setAttribute("cy", options.y);
|
|
512
|
+
dot.setAttribute("r", settings.dotRadius);
|
|
513
|
+
|
|
514
|
+
const hit = createSvgElement("circle", "nr-short-circuit-hit");
|
|
515
|
+
hit.setAttribute("cx", dotX);
|
|
516
|
+
hit.setAttribute("cy", options.y);
|
|
517
|
+
hit.setAttribute("r", settings.hitRadius);
|
|
518
|
+
hit.setAttribute("role", "button");
|
|
519
|
+
hit.setAttribute("tabindex", "0");
|
|
520
|
+
|
|
521
|
+
const wireWord = options.entries.length === 1 ? "wire" : "wires";
|
|
522
|
+
hit.setAttribute(
|
|
523
|
+
"aria-label",
|
|
524
|
+
(allExplicitlyActive ? "Hide " : "Show ") +
|
|
525
|
+
options.entries.length + " full " + wireWord
|
|
526
|
+
);
|
|
527
|
+
|
|
528
|
+
if (firstColour) {
|
|
529
|
+
stub.style.stroke = firstColour.colour;
|
|
530
|
+
dot.style.fill = firstColour.colour;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function toggleWires(event) {
|
|
534
|
+
stopWorkspaceAction(event);
|
|
535
|
+
|
|
536
|
+
const shouldHide = linkKeys.every(function(linkKey) {
|
|
537
|
+
return activeLinkKeys.has(linkKey);
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
activeLinkKeys.clear();
|
|
541
|
+
if (!shouldHide) {
|
|
542
|
+
linkKeys.forEach(function(linkKey) {
|
|
543
|
+
activeLinkKeys.add(linkKey);
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
refreshNow();
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
hit.addEventListener("mousedown", stopWorkspaceAction);
|
|
550
|
+
hit.addEventListener("mouseup", stopWorkspaceAction);
|
|
551
|
+
hit.addEventListener("touchstart", stopWorkspaceAction, { passive: false });
|
|
552
|
+
hit.addEventListener("touchend", toggleWires, { passive: false });
|
|
553
|
+
hit.addEventListener("click", toggleWires);
|
|
554
|
+
hit.addEventListener("keydown", function(event) {
|
|
555
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
556
|
+
toggleWires(event);
|
|
557
|
+
}
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
endGroup.appendChild(stub);
|
|
561
|
+
endGroup.appendChild(dot);
|
|
562
|
+
endGroup.appendChild(hit);
|
|
563
|
+
|
|
564
|
+
const orderedEntries = options.entries.slice().sort(function(a, b) {
|
|
565
|
+
const aY = a.otherNode && Number.isFinite(a.otherNode.y) ? a.otherNode.y : 0;
|
|
566
|
+
const bY = b.otherNode && Number.isFinite(b.otherNode.y) ? b.otherNode.y : 0;
|
|
567
|
+
if (aY !== bY) {
|
|
568
|
+
return aY - bY;
|
|
569
|
+
}
|
|
570
|
+
return getNodeLabel(a.otherNode).localeCompare(getNodeLabel(b.otherNode));
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
orderedEntries.forEach(function(entry, index) {
|
|
574
|
+
const verticalOffset =
|
|
575
|
+
(index - ((orderedEntries.length - 1) / 2)) * settings.previewSpacing;
|
|
576
|
+
|
|
577
|
+
addNodePreview(
|
|
578
|
+
endGroup,
|
|
579
|
+
dotX,
|
|
580
|
+
options.y,
|
|
581
|
+
options.direction,
|
|
582
|
+
entry,
|
|
583
|
+
verticalOffset
|
|
584
|
+
);
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
overlay.appendChild(endGroup);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
function isLinkRevealed(link, linkKey) {
|
|
591
|
+
return (
|
|
592
|
+
activeLinkKeys.has(linkKey) ||
|
|
593
|
+
link.selected === true ||
|
|
594
|
+
link.source.selected === true ||
|
|
595
|
+
link.target.selected === true
|
|
596
|
+
);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
function refreshNow() {
|
|
600
|
+
const oldOverlay = document.querySelector(
|
|
601
|
+
"#red-ui-workspace-chart svg .nr-short-circuit-overlay"
|
|
602
|
+
);
|
|
603
|
+
if (oldOverlay) {
|
|
604
|
+
oldOverlay.remove();
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
const firstLink = document.querySelector(
|
|
608
|
+
"#red-ui-workspace-chart svg g.red-ui-flow-link"
|
|
609
|
+
);
|
|
610
|
+
|
|
611
|
+
if (!firstLink || !firstLink.parentNode) {
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
const linkLayer = firstLink.parentNode;
|
|
616
|
+
const overlay = createSvgElement("g", "nr-short-circuit-overlay");
|
|
617
|
+
const linkGroups = Array.from(linkLayer.querySelectorAll("g.red-ui-flow-link"));
|
|
618
|
+
const activeKeys = new Set();
|
|
619
|
+
const endpointGroups = new Map();
|
|
620
|
+
|
|
621
|
+
function addEndpointToGroup(groupKey, endpoint) {
|
|
622
|
+
let endpointGroup = endpointGroups.get(groupKey);
|
|
623
|
+
if (!endpointGroup) {
|
|
624
|
+
endpointGroup = {
|
|
625
|
+
x: endpoint.x,
|
|
626
|
+
y: endpoint.y,
|
|
627
|
+
direction: endpoint.direction,
|
|
628
|
+
entries: []
|
|
629
|
+
};
|
|
630
|
+
endpointGroups.set(groupKey, endpointGroup);
|
|
631
|
+
}
|
|
632
|
+
endpointGroup.entries.push(endpoint.entry);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
linkGroups.forEach(function(group) {
|
|
636
|
+
const link = group.__data__;
|
|
637
|
+
restoreNativeLink(group);
|
|
638
|
+
|
|
639
|
+
if (!link) {
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
const coordinates = getLinkCoordinates(link);
|
|
644
|
+
if (!shouldShortenLink(link, coordinates)) {
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
const linkKey = getLinkKey(link);
|
|
649
|
+
const isRevealed = isLinkRevealed(link, linkKey);
|
|
650
|
+
const colour = getWireColour(group);
|
|
651
|
+
const sourcePort = Number.isFinite(link.sourcePort) ? link.sourcePort : 0;
|
|
652
|
+
const targetPort = Number.isFinite(link.targetPort)
|
|
653
|
+
? link.targetPort
|
|
654
|
+
: (link.target.i || 0);
|
|
655
|
+
|
|
656
|
+
activeKeys.add(linkKey);
|
|
657
|
+
group.classList.add("nr-short-circuit-hidden");
|
|
658
|
+
group.classList.toggle("nr-short-circuit-revealed", isRevealed);
|
|
659
|
+
|
|
660
|
+
if (isRevealed) {
|
|
661
|
+
applyNativeLinkReveal(group, coordinates);
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
addEndpointToGroup(
|
|
665
|
+
"source:" + link.source.id + ":" + sourcePort,
|
|
666
|
+
{
|
|
667
|
+
x: coordinates.x1,
|
|
668
|
+
y: coordinates.y1,
|
|
669
|
+
direction: 1,
|
|
670
|
+
entry: {
|
|
671
|
+
linkKey: linkKey,
|
|
672
|
+
colour: colour,
|
|
673
|
+
isActive: isRevealed,
|
|
674
|
+
otherNode: link.target
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
);
|
|
678
|
+
|
|
679
|
+
addEndpointToGroup(
|
|
680
|
+
"target:" + link.target.id + ":" + targetPort,
|
|
681
|
+
{
|
|
682
|
+
x: coordinates.x2,
|
|
683
|
+
y: coordinates.y2,
|
|
684
|
+
direction: -1,
|
|
685
|
+
entry: {
|
|
686
|
+
linkKey: linkKey,
|
|
687
|
+
colour: colour,
|
|
688
|
+
isActive: isRevealed,
|
|
689
|
+
otherNode: link.source
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
);
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
Array.from(activeLinkKeys).forEach(function(linkKey) {
|
|
696
|
+
if (!activeKeys.has(linkKey)) {
|
|
697
|
+
activeLinkKeys.delete(linkKey);
|
|
698
|
+
}
|
|
699
|
+
});
|
|
700
|
+
|
|
701
|
+
endpointGroups.forEach(function(endpointGroup) {
|
|
702
|
+
addEndpointGroup(overlay, endpointGroup);
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
if (overlay.childNodes.length > 0) {
|
|
706
|
+
linkLayer.appendChild(overlay);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
function scheduleRefresh() {
|
|
711
|
+
if (refreshTimer !== null) {
|
|
712
|
+
window.clearTimeout(refreshTimer);
|
|
713
|
+
}
|
|
714
|
+
if (refreshFrame !== null) {
|
|
715
|
+
window.cancelAnimationFrame(refreshFrame);
|
|
716
|
+
refreshFrame = null;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
refreshTimer = window.setTimeout(function() {
|
|
720
|
+
refreshTimer = null;
|
|
721
|
+
refreshFrame = window.requestAnimationFrame(function() {
|
|
722
|
+
refreshFrame = null;
|
|
723
|
+
refreshNow();
|
|
724
|
+
});
|
|
725
|
+
}, 0);
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
function clearEndpointReveal(event) {
|
|
729
|
+
if (activeLinkKeys.size === 0) {
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
if (event && event.target && event.target.closest) {
|
|
734
|
+
if (
|
|
735
|
+
event.target.closest(".nr-short-circuit-end") ||
|
|
736
|
+
event.target.closest("g.red-ui-flow-link.nr-short-circuit-revealed")
|
|
737
|
+
) {
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
activeLinkKeys.clear();
|
|
743
|
+
scheduleRefresh();
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
function handleKeyDown(event) {
|
|
747
|
+
if (event.key === "Escape" && activeLinkKeys.size > 0) {
|
|
748
|
+
activeLinkKeys.clear();
|
|
749
|
+
scheduleRefresh();
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
function registerEditorEvents() {
|
|
754
|
+
[
|
|
755
|
+
"flows:loaded",
|
|
756
|
+
"workspace:change",
|
|
757
|
+
"links:add",
|
|
758
|
+
"links:remove",
|
|
759
|
+
"nodes:add",
|
|
760
|
+
"nodes:remove",
|
|
761
|
+
"nodes:change",
|
|
762
|
+
"view:selection-changed",
|
|
763
|
+
"editor:save"
|
|
764
|
+
].forEach(function(eventName) {
|
|
765
|
+
RED.events.on(eventName, scheduleRefresh);
|
|
766
|
+
});
|
|
767
|
+
|
|
768
|
+
if (RED.hooks && RED.hooks.add) {
|
|
769
|
+
RED.hooks.add("viewAddNode." + PLUGIN_ID, scheduleRefresh);
|
|
770
|
+
RED.hooks.add("viewRedrawNode." + PLUGIN_ID, scheduleRefresh);
|
|
771
|
+
RED.hooks.add("viewRemoveNode." + PLUGIN_ID, scheduleRefresh);
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
document.addEventListener("mousedown", clearEndpointReveal, true);
|
|
775
|
+
document.addEventListener("keydown", handleKeyDown, true);
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
function initialise() {
|
|
779
|
+
if (initialised) {
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
initialised = true;
|
|
784
|
+
|
|
785
|
+
loadConfiguration().always(function() {
|
|
786
|
+
registerEditorEvents();
|
|
787
|
+
scheduleRefresh();
|
|
788
|
+
});
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
RED.plugins.registerPlugin(PLUGIN_ID, {
|
|
792
|
+
type: "editor",
|
|
793
|
+
onadd: initialise
|
|
794
|
+
});
|
|
795
|
+
}());
|
|
796
|
+
</script>
|
package/short-circuit.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
|
|
6
|
+
module.exports = function(RED) {
|
|
7
|
+
const PLUGIN_ID = "node-red-contrib-short-circuit";
|
|
8
|
+
const DEFAULT_CONFIG = {
|
|
9
|
+
maxDistance: 400
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const userDir = RED.settings.userDir || process.cwd();
|
|
13
|
+
const configPath = path.join(userDir, "short-circuit.json");
|
|
14
|
+
|
|
15
|
+
function clampDistance(value) {
|
|
16
|
+
const parsed = Number(value);
|
|
17
|
+
if (!Number.isFinite(parsed)) {
|
|
18
|
+
return DEFAULT_CONFIG.maxDistance;
|
|
19
|
+
}
|
|
20
|
+
return Math.min(5000, Math.max(50, Math.round(parsed)));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function createConfigFile() {
|
|
24
|
+
try {
|
|
25
|
+
fs.mkdirSync(userDir, { recursive: true });
|
|
26
|
+
fs.writeFileSync(
|
|
27
|
+
configPath,
|
|
28
|
+
JSON.stringify(DEFAULT_CONFIG, null, 4) + "\n",
|
|
29
|
+
{ encoding: "utf8", flag: "wx" }
|
|
30
|
+
);
|
|
31
|
+
RED.log.info("Short circuit created " + configPath);
|
|
32
|
+
} catch (error) {
|
|
33
|
+
if (error.code !== "EEXIST") {
|
|
34
|
+
throw error;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function readConfig() {
|
|
40
|
+
try {
|
|
41
|
+
createConfigFile();
|
|
42
|
+
|
|
43
|
+
const fileContent = fs.readFileSync(configPath, "utf8");
|
|
44
|
+
const parsed = JSON.parse(fileContent);
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
maxDistance: clampDistance(parsed.maxDistance)
|
|
48
|
+
};
|
|
49
|
+
} catch (error) {
|
|
50
|
+
RED.log.warn(
|
|
51
|
+
"Short circuit could not read " + configPath +
|
|
52
|
+
". Using maxDistance " + DEFAULT_CONFIG.maxDistance +
|
|
53
|
+
". " + error.message
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
return Object.assign({}, DEFAULT_CONFIG);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
RED.plugins.registerPlugin(PLUGIN_ID, {
|
|
61
|
+
onadd: function() {
|
|
62
|
+
const needsPermission = RED.auth.needsPermission("flows.read");
|
|
63
|
+
|
|
64
|
+
// Read the file for every editor request so a browser refresh picks up changes
|
|
65
|
+
RED.httpAdmin.get(
|
|
66
|
+
"/short-circuit/config",
|
|
67
|
+
needsPermission,
|
|
68
|
+
function(req, res) {
|
|
69
|
+
res.json(readConfig());
|
|
70
|
+
}
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
// Create the file immediately so the user can find and edit it
|
|
74
|
+
createConfigFile();
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
};
|