@nrutils/node-red-utils 0.0.0-stage → 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/CHANGELOG.md +15 -0
- package/LICENSE +190 -0
- package/README.md +181 -2
- package/index.js +50 -0
- package/node-red-base/NodeRedNodeBase.js +143 -0
- package/node-red-base/README.md +215 -0
- package/node-red-editorcomms/EditorCommsBackend.js +155 -0
- package/node-red-editorcomms/EditorCommsFrontend.js +105 -0
- package/node-red-editorcomms/README.md +164 -0
- package/node-red-gpu/GpuBuffer.js +29 -0
- package/node-red-gpu/Kernel.js +35 -0
- package/node-red-gpu/README.md +220 -0
- package/node-red-gpu/backends/CpuBackend.js +109 -0
- package/node-red-gpu/backends/VulkanBackend.js +232 -0
- package/node-red-gpu/errors.js +48 -0
- package/node-red-gpu/index.js +6 -0
- package/node-red-gpu/log.js +18 -0
- package/node-red-gpu/native/binding.gyp +14 -0
- package/node-red-gpu/native/src/gpu_addon.cpp +1066 -0
- package/node-red-gpu/nodeRedGpuMain.js +39 -0
- package/node-red-gpu/nodeRedGpuWorker.js +242 -0
- package/node-red-gpu/transpiler/emitGlsl.js +497 -0
- package/node-red-gpu/transpiler/glslNames.js +60 -0
- package/node-red-gpu/transpiler/index.js +81 -0
- package/node-red-gpu/transpiler/inferTypes.js +302 -0
- package/node-red-gpu/transpiler/validate.js +461 -0
- package/node-red-popup/NodeRedPopupBackend.js +69 -0
- package/node-red-popup/NodeRedPopupFrontend.js +414 -0
- package/node-red-popup/README.md +110 -0
- package/node-red-pubsub-bus/NodeRedPubsubBus.js +617 -0
- package/node-red-pubsub-bus/NodeRedPubsubBusWorkerProxy.js +239 -0
- package/node-red-pubsub-bus/README.md +201 -0
- package/node-red-resourceendpoint/NodeRedResourceEndpoint.js +205 -0
- package/node-red-resourceendpoint/README.md +119 -0
- package/node-red-singleton/NodeRedSingletonBackend.js +30 -0
- package/node-red-singleton/NodeRedSingletonFrontend.js +54 -0
- package/node-red-singleton/README.md +72 -0
- package/node-red-worker/NodeRedWorker.js +438 -0
- package/node-red-worker/NodeRedWorkerRuntime.js +651 -0
- package/node-red-worker/README.md +265 -0
- package/node-red-worker/workerRuntimeLoader.js +55 -0
- package/package.json +70 -5
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright 2024 Contributors
|
|
3
|
+
* Licensed under the Apache License, Version 2.0
|
|
4
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
//
|
|
8
|
+
// Begin waitForWorkspaceChange
|
|
9
|
+
//
|
|
10
|
+
// Helper function to wait for NodeRed to update the workspace because it sends out
|
|
11
|
+
// 'workspace:change' event BEFORE it draws stuff.
|
|
12
|
+
// This just watches the workspace and resolves if no activity for xxxx ms.
|
|
13
|
+
//
|
|
14
|
+
if (!window.nru_waitForWorkspaceChanges) {
|
|
15
|
+
|
|
16
|
+
window.nru_waitForWorkspaceChanges = function (quietPeriod = 100) {
|
|
17
|
+
return new Promise((resolve) => {
|
|
18
|
+
|
|
19
|
+
const observer = new MutationObserver((mutations) => {
|
|
20
|
+
if (timeoutId) { clearTimeout(timeoutId) }
|
|
21
|
+
|
|
22
|
+
timeoutId = setTimeout(() => { // Once we've had no changes for the quiet period, clean up and resolve
|
|
23
|
+
observer.disconnect();
|
|
24
|
+
resolve();
|
|
25
|
+
}, quietPeriod);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
let timeoutId = null;
|
|
29
|
+
|
|
30
|
+
const workspace = document.querySelector("#red-ui-workspace"); // Get the workspace container
|
|
31
|
+
if (!workspace) {
|
|
32
|
+
console.warn("nru_waitForWorkspaceChanges: Workspace element not found");
|
|
33
|
+
resolve(); // Resolve immediately if we can't find the workspace
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Configure and start the observer
|
|
38
|
+
observer.observe(workspace, {
|
|
39
|
+
childList: true, // Watch for added/removed nodes
|
|
40
|
+
subtree: true, // Watch all descendants
|
|
41
|
+
attributes: true // Watch for attribute changes
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
//
|
|
52
|
+
// Begin DOM Event Tracker
|
|
53
|
+
//
|
|
54
|
+
// This is needed since NodeRED will delete our Workspace elements
|
|
55
|
+
// without cleaning up events we may have added. Modern browser garbage collectors
|
|
56
|
+
// should work but we'll help it out to ensure no memory leaks by deleting any events we create.
|
|
57
|
+
//
|
|
58
|
+
|
|
59
|
+
if (!window.DOMEventTracker) { // just declare the class once
|
|
60
|
+
|
|
61
|
+
window.DOMEventTracker = class EventTracker {
|
|
62
|
+
|
|
63
|
+
constructor() {
|
|
64
|
+
this.listeners = new Map();
|
|
65
|
+
this.counter = 0;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Add an event listener and track it
|
|
69
|
+
addListener(element, eventType, handler, options = false) {
|
|
70
|
+
const id = this.counter++;
|
|
71
|
+
element.addEventListener(eventType, handler, options);
|
|
72
|
+
|
|
73
|
+
this.listeners.set(id, {
|
|
74
|
+
element,
|
|
75
|
+
eventType,
|
|
76
|
+
handler,
|
|
77
|
+
options
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
return id;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Remove a specific listener by its ID
|
|
84
|
+
removeListener(id) {
|
|
85
|
+
const listener = this.listeners.get(id);
|
|
86
|
+
if (listener) {
|
|
87
|
+
const { element, eventType, handler, options } = listener;
|
|
88
|
+
element.removeEventListener(eventType, handler, options);
|
|
89
|
+
this.listeners.delete(id);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Remove all listeners
|
|
94
|
+
removeAllListeners() {
|
|
95
|
+
this.listeners.forEach((listener, id) => {
|
|
96
|
+
const { element, eventType, handler, options } = listener;
|
|
97
|
+
element.removeEventListener(eventType, handler, options);
|
|
98
|
+
});
|
|
99
|
+
this.listeners.clear();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Check for and remove listeners for deleted elements
|
|
103
|
+
cleanupDeletedElements() {
|
|
104
|
+
this.listeners.forEach((listener, id) => {
|
|
105
|
+
if (!document.contains(listener.element)) {
|
|
106
|
+
this.listeners.delete(id);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
//
|
|
116
|
+
// Begin Popup Support
|
|
117
|
+
//
|
|
118
|
+
|
|
119
|
+
if (!window.popupInited) {
|
|
120
|
+
|
|
121
|
+
window.popupInited = true;
|
|
122
|
+
window.popupStates = {};
|
|
123
|
+
window.popupEventTracker = new window.DOMEventTracker();
|
|
124
|
+
|
|
125
|
+
const removePopups = async (workspace) => {
|
|
126
|
+
window.popupEventTracker.removeAllListeners();
|
|
127
|
+
RED.nodes.filterNodes( {z:workspace} ).forEach ( node => {
|
|
128
|
+
const existingGroup = document.getElementById('nru_popup.' + node.id + '.group');
|
|
129
|
+
if (existingGroup) {
|
|
130
|
+
window.popupStates[node.id] = existingGroup.style.display === 'none' ? 'closed' : 'open';
|
|
131
|
+
existingGroup.remove()
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const addPopups = async (workspace) => {
|
|
137
|
+
await window.nru_waitForWorkspaceChanges(100);
|
|
138
|
+
RED.nodes.filterNodes( {z:workspace} ).forEach ( async node => {
|
|
139
|
+
createPopup( node );
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const addPopup = async (node) => {
|
|
144
|
+
await window.nru_waitForWorkspaceChanges(100);
|
|
145
|
+
createPopup(node);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Smallest popup size in pixels. Smaller sizes are made this size.
|
|
149
|
+
const MIN_WIDTH = 100, MIN_HEIGHT = 50;
|
|
150
|
+
const warnedTypes = new Set();
|
|
151
|
+
|
|
152
|
+
const createPopup = async ( node ) => {
|
|
153
|
+
|
|
154
|
+
if (!node?._def?.defaults?.nru_popupConfig?.value) return;
|
|
155
|
+
|
|
156
|
+
// The node's own config (saved after a resize) wins over the default of the type
|
|
157
|
+
const config = { ...node._def.defaults.nru_popupConfig.value, ...node.nru_popupConfig };
|
|
158
|
+
const problem = ['width', 'height', 'autoCloseTime']
|
|
159
|
+
.find(key => typeof config[key] !== 'number' || !(config[key] >= 0));
|
|
160
|
+
if (problem) {
|
|
161
|
+
console.error(`Popup of node ${node.id}: nru_popupConfig.${problem} must be a number, 0 or more; got`, config[problem]);
|
|
162
|
+
if (!warnedTypes.has(node.type)) {
|
|
163
|
+
warnedTypes.add(node.type);
|
|
164
|
+
RED.notify(`No popup for "${node.type}" nodes: nru_popupConfig.${problem} must be a number, 0 or more.`, 'warning');
|
|
165
|
+
}
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Exit if already added bc we are getting multiple workspace change events or something
|
|
170
|
+
if (document.getElementById("nru_popup." + node.id + ".group") ) return;
|
|
171
|
+
|
|
172
|
+
// Just in case we got here without NodeRED being finished adding nodes to the workspace
|
|
173
|
+
if (!document.getElementById(node.id)) return;
|
|
174
|
+
|
|
175
|
+
const rectElement = document.getElementById(node.id).querySelector('rect');
|
|
176
|
+
if(!rectElement) return;
|
|
177
|
+
|
|
178
|
+
const width = Math.max(MIN_WIDTH, config.width);
|
|
179
|
+
const height = Math.max(MIN_HEIGHT, config.height);
|
|
180
|
+
const autoCloseTime = config.autoCloseTime;
|
|
181
|
+
|
|
182
|
+
const svgElement = document.getElementById(node.id);
|
|
183
|
+
let group = document.createElementNS("http://www.w3.org/2000/svg", "g");
|
|
184
|
+
group.setAttribute("id", "nru_popup." + node.id + ".group");
|
|
185
|
+
group.setAttribute("transform", "translate(0, 50)");
|
|
186
|
+
group.style.display = (window.popupStates[node.id] === 'open') ? 'block' : 'none';
|
|
187
|
+
|
|
188
|
+
// Create base elements
|
|
189
|
+
let rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
|
|
190
|
+
rect.setAttribute("id", "nru_popup." + node.id + ".rect");
|
|
191
|
+
rect.setAttribute("x", "0");
|
|
192
|
+
rect.setAttribute("y", "0");
|
|
193
|
+
rect.setAttribute("width", width);
|
|
194
|
+
rect.setAttribute("height", height);
|
|
195
|
+
rect.setAttribute("fill", "#f1f1f1");
|
|
196
|
+
rect.setAttribute("stroke", "#d4d4d4");
|
|
197
|
+
rect.setAttribute("stroke-width", "1");
|
|
198
|
+
rect.setAttribute("rx", "0");
|
|
199
|
+
|
|
200
|
+
let foreignObject = document.createElementNS("http://www.w3.org/2000/svg", "foreignObject");
|
|
201
|
+
foreignObject.setAttribute("id", "nru_popup." + node.id + ".foreignObject");
|
|
202
|
+
const sourceText = svgElement.querySelector('.red-ui-flow-node-status-label');
|
|
203
|
+
for (const attr of sourceText.attributes) {
|
|
204
|
+
foreignObject.setAttribute(attr.name, attr.value)
|
|
205
|
+
};
|
|
206
|
+
foreignObject.setAttribute("x", "6");
|
|
207
|
+
foreignObject.setAttribute("y", "6");
|
|
208
|
+
foreignObject.setAttribute("width", width - 12);
|
|
209
|
+
foreignObject.setAttribute("height", height - 20 - 12); // Leave room for status bar
|
|
210
|
+
|
|
211
|
+
let divElement = document.createElement("div");
|
|
212
|
+
divElement.setAttribute("id", "nru_popup." + node.id + ".div");
|
|
213
|
+
divElement.style.width = (width - 12) + "px";
|
|
214
|
+
divElement.style.height = (height - 20 - 12) + "px";
|
|
215
|
+
divElement.style.margin = "0";
|
|
216
|
+
divElement.style.overflow = "auto";
|
|
217
|
+
divElement.style.boxSizing = "border-box";
|
|
218
|
+
foreignObject.appendChild(divElement);
|
|
219
|
+
|
|
220
|
+
// Create resize handle as a small path in bottom right
|
|
221
|
+
const resizeHandle = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
222
|
+
resizeHandle.setAttribute("id", "nru_popup." + node.id + ".resizeHandle");
|
|
223
|
+
resizeHandle.setAttribute("d", `M${width - 6},${height} l6,-6 l0,6 z`);
|
|
224
|
+
resizeHandle.setAttribute("fill", "#b3b3b3 ");
|
|
225
|
+
resizeHandle.setAttribute("stroke", "#b3b3b3 ");
|
|
226
|
+
resizeHandle.setAttribute("style", "cursor: se-resize");
|
|
227
|
+
|
|
228
|
+
// Create status bar rectangle
|
|
229
|
+
let statusBar = document.createElementNS("http://www.w3.org/2000/svg", "rect");
|
|
230
|
+
statusBar.setAttribute("id", "nru_popup." + node.id + ".statusBar");
|
|
231
|
+
statusBar.setAttribute("x", "0");
|
|
232
|
+
statusBar.setAttribute("y", (parseInt(rect.getAttribute("height")) - 20).toString());
|
|
233
|
+
statusBar.setAttribute("width", width);
|
|
234
|
+
statusBar.setAttribute("height", "20"); // Height of status bar
|
|
235
|
+
statusBar.setAttribute("fill", "#e3e3e3"); // Light gray background
|
|
236
|
+
statusBar.setAttribute("stroke", "#d4d4d4");
|
|
237
|
+
statusBar.setAttribute("stroke-width", "1");
|
|
238
|
+
|
|
239
|
+
// Create text element for node info
|
|
240
|
+
let statusText = document.createElementNS("http://www.w3.org/2000/svg", "text");
|
|
241
|
+
statusText.setAttribute("id", "nru_popup." + node.id + ".statusText");
|
|
242
|
+
statusText.setAttribute("x", "6");
|
|
243
|
+
statusText.setAttribute("y", (parseInt(rect.getAttribute("height")) - 6).toString());
|
|
244
|
+
statusText.setAttribute("font-family", "Arial, Helvetica, sans-serif");
|
|
245
|
+
statusText.setAttribute("font-size", "10");
|
|
246
|
+
statusText.setAttribute("stroke-width", "0.2");
|
|
247
|
+
statusText.setAttribute("fill", "#222");
|
|
248
|
+
statusText.textContent = '';
|
|
249
|
+
statusText.fullText = '';
|
|
250
|
+
statusText.setStatusText = function( text ) {
|
|
251
|
+
const width = parseInt(rect.getAttribute('width'));
|
|
252
|
+
statusText.textContent = text;
|
|
253
|
+
statusText.fullText = text;
|
|
254
|
+
sizeTextToFit( statusText, width);
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
// Add elements to group
|
|
258
|
+
group.appendChild(rect);
|
|
259
|
+
group.appendChild(statusBar);
|
|
260
|
+
group.appendChild(statusText);
|
|
261
|
+
group.appendChild(foreignObject);
|
|
262
|
+
group.appendChild(resizeHandle);
|
|
263
|
+
svgElement.appendChild(group);
|
|
264
|
+
|
|
265
|
+
// Resize functionality
|
|
266
|
+
function sizeTextToFit( svgTextObject, width ) {
|
|
267
|
+
while ( svgTextObject.getBBox().width < width - 5 && svgTextObject.textContent.length !== svgTextObject.fullText.length ) {
|
|
268
|
+
svgTextObject.textContent = svgTextObject.fullText.slice(0, svgTextObject.textContent.length + 1);
|
|
269
|
+
}
|
|
270
|
+
while ( svgTextObject.getBBox().width > width - 5 && svgTextObject.textContent !== '') {
|
|
271
|
+
svgTextObject.textContent = svgTextObject.fullText.slice( 0, svgTextObject.textContent.length - 1);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
//---------------------------------------------------------------------------------------------
|
|
276
|
+
// Event Handlers
|
|
277
|
+
//---------------------------------------------------------------------------------------------
|
|
278
|
+
let clickTimer = null;
|
|
279
|
+
let autoCloseTimeout = null;
|
|
280
|
+
let mouseDownX = 0, mouseDownY = 0;
|
|
281
|
+
const DRAG_THRESHOLD = 3;
|
|
282
|
+
const handleRectElementMouseDown = (event) => {
|
|
283
|
+
mouseDownX = event.clientX;
|
|
284
|
+
mouseDownY = event.clientY;
|
|
285
|
+
}
|
|
286
|
+
const handleRectElementClick = (event) => {
|
|
287
|
+
// Suppress toggle if the pointer moved far enough to count as a drag
|
|
288
|
+
const dx = event.clientX - mouseDownX;
|
|
289
|
+
const dy = event.clientY - mouseDownY;
|
|
290
|
+
if (Math.hypot(dx, dy) > DRAG_THRESHOLD) return;
|
|
291
|
+
|
|
292
|
+
if (clickTimer) { // clear if double-clicked
|
|
293
|
+
clearTimeout(clickTimer);
|
|
294
|
+
clickTimer = null;
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
clickTimer = setTimeout(() => {
|
|
298
|
+
const isHidden = group.style.display === 'none';
|
|
299
|
+
group.style.display = isHidden ? 'block' : 'none';
|
|
300
|
+
// Save the popup state
|
|
301
|
+
window.popupStates[node.id] = isHidden ? 'open' : 'closed';
|
|
302
|
+
if (isHidden) {
|
|
303
|
+
svgElement.parentNode.appendChild(svgElement);
|
|
304
|
+
if (autoCloseTime > 0) {
|
|
305
|
+
clearTimeout(autoCloseTimeout);
|
|
306
|
+
autoCloseTimeout = setTimeout(() => { group.style.display = 'none';}, autoCloseTime);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
clickTimer = null;
|
|
310
|
+
}, 250);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
let startWidth, startHeight, startX, startY;
|
|
314
|
+
let docResizeMouseMoveListener, docResizeMouseUpListener;
|
|
315
|
+
const resizeHandleMouseDown = (event) => {
|
|
316
|
+
|
|
317
|
+
// Get initial sizes and position
|
|
318
|
+
[startWidth, startHeight] = [parseInt(rect.getAttribute('width')), parseInt(rect.getAttribute('height'))];
|
|
319
|
+
( {startX, startY} = {startX: event.clientX, startY: event.clientY} );
|
|
320
|
+
resized = false;
|
|
321
|
+
|
|
322
|
+
// Add event listeners for drag operations
|
|
323
|
+
docResizeMouseMoveListener = window.popupEventTracker.addListener(document, 'mousemove', resize);
|
|
324
|
+
docResizeMouseUpListener = window.popupEventTracker.addListener(document, 'mouseup', stopResize);
|
|
325
|
+
|
|
326
|
+
event.preventDefault();
|
|
327
|
+
event.stopPropagation();
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
let newWidth = 0, newHeight = 0, resized = false;
|
|
331
|
+
const resize = (event)=> {
|
|
332
|
+
resized = true;
|
|
333
|
+
const {dx, dy} = {dx: event.clientX - startX, dy: event.clientY - startY};
|
|
334
|
+
newWidth = Math.max(MIN_WIDTH, startWidth + dx);
|
|
335
|
+
newHeight = Math.max(MIN_HEIGHT, startHeight + dy);
|
|
336
|
+
statusBar.setAttribute('width', newWidth);
|
|
337
|
+
statusBar.setAttribute("y", (newHeight - 20).toString());
|
|
338
|
+
statusText.setAttribute("y", (newHeight - 6).toString());
|
|
339
|
+
sizeTextToFit( statusText, newWidth );
|
|
340
|
+
rect.setAttribute('width', newWidth);
|
|
341
|
+
rect.setAttribute('height', newHeight);
|
|
342
|
+
foreignObject.setAttribute('width', newWidth - 12);
|
|
343
|
+
foreignObject.setAttribute('height', newHeight - 20 - 6 - 6); // total height - statusbar height - top padding - bottom padding
|
|
344
|
+
divElement.style.width = (newWidth - 12) + 'px';
|
|
345
|
+
divElement.style.height = (newHeight - 20 - 12) + 'px';
|
|
346
|
+
resizeHandle.setAttribute("d", `M${newWidth - 6},${newHeight} l6,-6 l0,6 z`);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const stopResize = () => {
|
|
350
|
+
window.popupEventTracker.removeListener(docResizeMouseMoveListener);
|
|
351
|
+
window.popupEventTracker.removeListener(docResizeMouseUpListener);
|
|
352
|
+
|
|
353
|
+
// A click with no drag does not change the size
|
|
354
|
+
if (!resized) return;
|
|
355
|
+
node.nru_popupConfig = {
|
|
356
|
+
width: newWidth,
|
|
357
|
+
height: newHeight,
|
|
358
|
+
autoCloseTime: autoCloseTime
|
|
359
|
+
};
|
|
360
|
+
node.changed = true;
|
|
361
|
+
RED.nodes.dirty(true);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
window.popupEventTracker.addListener(rectElement, "mousedown", handleRectElementMouseDown);
|
|
365
|
+
window.popupEventTracker.addListener(rectElement, "click", handleRectElementClick);
|
|
366
|
+
window.popupEventTracker.addListener(resizeHandle, "mousedown", resizeHandleMouseDown);
|
|
367
|
+
|
|
368
|
+
// Run the node's nru_popupInit function
|
|
369
|
+
try {
|
|
370
|
+
if (!node?._def?.nru_popupInit) return;
|
|
371
|
+
if (typeof node._def.nru_popupInit !== 'function') {
|
|
372
|
+
console.error('Popup init failed: nru_popupInit is not a function', {
|
|
373
|
+
exists: !!node._def.nru_popupInit,
|
|
374
|
+
type: typeof node._def.nru_popupInit
|
|
375
|
+
});
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
node._def.nru_popupInit.call(node);
|
|
379
|
+
} catch (error) {
|
|
380
|
+
console.error('Running nru_popupInit failed with error:', error.message, {
|
|
381
|
+
errorName: error.name,
|
|
382
|
+
stack: error.stack,
|
|
383
|
+
node: node?.id
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
// add/remove popup elements on workspace change
|
|
391
|
+
RED.events.on('workspace:change', async (event) => {
|
|
392
|
+
if (event?.old !== 0) { removePopups(event.old) };
|
|
393
|
+
addPopups(event.workspace);
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
// add popup elements on when new node is added
|
|
397
|
+
RED.events.on('nodes:add', async (node) => {
|
|
398
|
+
addPopup(node);
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
// Re-bind popups after deploy. Node-RED may replace a node's SVG rect
|
|
402
|
+
// in place during redeploy, orphaning the click handler and removing the
|
|
403
|
+
// popup group from the DOM. Neither workspace:change nor nodes:add fires
|
|
404
|
+
// for unchanged nodes on the same tab, so without this the only recovery
|
|
405
|
+
// was a full page refresh.
|
|
406
|
+
RED.events.on('deploy', async () => {
|
|
407
|
+
const active = RED.workspaces.active();
|
|
408
|
+
if (!active) return;
|
|
409
|
+
await removePopups(active);
|
|
410
|
+
await addPopups(active);
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
}
|
|
414
|
+
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# node-red-popup
|
|
2
|
+
|
|
3
|
+
EXPERIMENTAL. The API and the behavior can change in a minor release.
|
|
4
|
+
|
|
5
|
+
A small resizable panel attached to a node in the editor. A click on the node opens or closes it.
|
|
6
|
+
|
|
7
|
+
## Usage
|
|
8
|
+
|
|
9
|
+
A popup that shows the last message of the node on the canvas, without opening the edit dialog. It uses [`nodeRedEditorComms`](../node-red-editorcomms/README.md) for the runtime data.
|
|
10
|
+
|
|
11
|
+
The runtime mixin `nodeRedPopup` has no API that you call. It exists to serve the editor script `NodeRedPopupFrontend.js`, so that the `<script>` tag below can load it, also before the first deploy. Add the mixin even though you do not call anything on it:
|
|
12
|
+
|
|
13
|
+
```js
|
|
14
|
+
const { NodeRedNodeBase, nodeRedPopup, nodeRedEditorComms } = require('@nrutils/node-red-utils');
|
|
15
|
+
|
|
16
|
+
module.exports = NodeRedNodeBase.createNode('popup-demo', [nodeRedPopup, nodeRedEditorComms], (RED, thisNode, config) => {
|
|
17
|
+
let last = null;
|
|
18
|
+
|
|
19
|
+
// A new popup asks for the last message
|
|
20
|
+
thisNode.nodeRedEditorComms.subscribe('get-last', () => last);
|
|
21
|
+
|
|
22
|
+
thisNode.on('input', (msg, send, done) => {
|
|
23
|
+
last = { payload: msg.payload, time: new Date().toLocaleTimeString() };
|
|
24
|
+
thisNode.nodeRedEditorComms.publish('last', last); // updates the popup in every open editor
|
|
25
|
+
send(msg);
|
|
26
|
+
done();
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
The editor side does the work:
|
|
32
|
+
|
|
33
|
+
```html
|
|
34
|
+
<script type="text/javascript" src="node-red-resourceendpoint/node-red-popup/NodeRedPopupFrontend.js"></script>
|
|
35
|
+
<script type="text/javascript" src="node-red-resourceendpoint/node-red-editorcomms/EditorCommsFrontend.js"></script>
|
|
36
|
+
|
|
37
|
+
<script type="text/javascript">
|
|
38
|
+
(function () {
|
|
39
|
+
function show(node, last) {
|
|
40
|
+
const div = document.getElementById('nru_popup.' + node.id + '.div');
|
|
41
|
+
if (!div || !last) return; // the node is on another tab, or no message yet
|
|
42
|
+
div.textContent = JSON.stringify(last.payload); // text, never HTML
|
|
43
|
+
document.getElementById('nru_popup.' + node.id + '.statusText').setStatusText(last.time);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
RED.nodes.registerType('popup-demo', {
|
|
47
|
+
category: 'function',
|
|
48
|
+
color: '#d4c4fb',
|
|
49
|
+
defaults: {
|
|
50
|
+
name: { value: '' },
|
|
51
|
+
nru_popupConfig: { value: { width: 200, height: 120, autoCloseTime: 0 } }
|
|
52
|
+
},
|
|
53
|
+
inputs: 1,
|
|
54
|
+
outputs: 1,
|
|
55
|
+
label: function () { return this.name || 'popup-demo'; },
|
|
56
|
+
|
|
57
|
+
// Runs each time the editor builds the popup; "this" is the node
|
|
58
|
+
nru_popupInit: function () {
|
|
59
|
+
const node = this;
|
|
60
|
+
node.nodeRedEditorComms.publish('get-last').then((last) => show(node, last)).catch(() => {});
|
|
61
|
+
},
|
|
62
|
+
nodeRedEditorComms_subscriptions: {
|
|
63
|
+
last: function (last) { show(this, last); }
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
})();
|
|
67
|
+
</script>
|
|
68
|
+
|
|
69
|
+
<script type="text/html" data-template-name="popup-demo">
|
|
70
|
+
<div class="form-row">
|
|
71
|
+
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
|
72
|
+
<input type="text" id="node-input-name" placeholder="Name">
|
|
73
|
+
</div>
|
|
74
|
+
</script>
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
The editor script reads two properties of the node definition. Both names start with `nru_` (for node-red-utils), so they do not collide with Node-RED's own properties or yours:
|
|
78
|
+
|
|
79
|
+
- `nru_popupConfig` in `defaults`: the size of the popup. Because it is in `defaults`, Node-RED saves it with each node.
|
|
80
|
+
- `nru_popupInit`: a function that the script calls after it has made the popup elements, with `this` set to the editor node. Fill the popup here.
|
|
81
|
+
|
|
82
|
+
When the editor builds the popup, it asks the runtime for the last message. After that, the runtime pushes each new message to the popup. A node that is not deployed answers 404, and the popup stays empty. Runtime data shown in the popup must be set with `textContent` or jQuery `.text()`, never with `innerHTML`: a message payload can hold HTML that would run as script in the editor.
|
|
83
|
+
|
|
84
|
+
## Configuration
|
|
85
|
+
|
|
86
|
+
A node gets a popup only if `defaults` has `nru_popupConfig`:
|
|
87
|
+
|
|
88
|
+
- `width`: pixels, for the full popup. The minimum is 100; a smaller value gives 100.
|
|
89
|
+
- `height`: pixels, for the full popup, with the 20 px status bar. The minimum is 50; a smaller value gives 50.
|
|
90
|
+
- `autoCloseTime`: milliseconds until the popup hides itself. `0` keeps it open.
|
|
91
|
+
|
|
92
|
+
The content area (`nru_popup.<nodeId>.div`) is 12 px narrower and 32 px lower than the popup. A resize by the user cannot go below 100 x 50 either.
|
|
93
|
+
|
|
94
|
+
All three values must be numbers, 0 or more. If one is not, the node gets no popup, the editor shows a warning one time for the node type, and the browser console names the bad value.
|
|
95
|
+
|
|
96
|
+
When the user resizes a popup, the new size goes into the node's `nru_popupConfig`, so it is saved with the flow.
|
|
97
|
+
|
|
98
|
+
## Elements
|
|
99
|
+
|
|
100
|
+
Each popup creates elements with these ids:
|
|
101
|
+
|
|
102
|
+
- `nru_popup.<nodeId>.div`: the content area. Put your elements here.
|
|
103
|
+
- `nru_popup.<nodeId>.statusText`: the status line. Call its `setStatusText(text)`; long text is cut to fit.
|
|
104
|
+
|
|
105
|
+
## Behavior
|
|
106
|
+
|
|
107
|
+
- The popup is rebuilt when you change tabs, add the node, or deploy. `nru_popupInit` runs each time, so make it safe to call again.
|
|
108
|
+
- The script draws into the editor's SVG (the node's `<g id="<nodeId>">` and `#red-ui-workspace`). If a future editor changes that structure, the popup can break. It was written for the Node-RED 4.1 editor.
|
|
109
|
+
|
|
110
|
+
[Back to node-red-utils](../README.md)
|