@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.
Files changed (42) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/LICENSE +190 -0
  3. package/README.md +181 -2
  4. package/index.js +50 -0
  5. package/node-red-base/NodeRedNodeBase.js +143 -0
  6. package/node-red-base/README.md +215 -0
  7. package/node-red-editorcomms/EditorCommsBackend.js +155 -0
  8. package/node-red-editorcomms/EditorCommsFrontend.js +105 -0
  9. package/node-red-editorcomms/README.md +164 -0
  10. package/node-red-gpu/GpuBuffer.js +29 -0
  11. package/node-red-gpu/Kernel.js +35 -0
  12. package/node-red-gpu/README.md +220 -0
  13. package/node-red-gpu/backends/CpuBackend.js +109 -0
  14. package/node-red-gpu/backends/VulkanBackend.js +232 -0
  15. package/node-red-gpu/errors.js +48 -0
  16. package/node-red-gpu/index.js +6 -0
  17. package/node-red-gpu/log.js +18 -0
  18. package/node-red-gpu/native/binding.gyp +14 -0
  19. package/node-red-gpu/native/src/gpu_addon.cpp +1066 -0
  20. package/node-red-gpu/nodeRedGpuMain.js +39 -0
  21. package/node-red-gpu/nodeRedGpuWorker.js +242 -0
  22. package/node-red-gpu/transpiler/emitGlsl.js +497 -0
  23. package/node-red-gpu/transpiler/glslNames.js +60 -0
  24. package/node-red-gpu/transpiler/index.js +81 -0
  25. package/node-red-gpu/transpiler/inferTypes.js +302 -0
  26. package/node-red-gpu/transpiler/validate.js +461 -0
  27. package/node-red-popup/NodeRedPopupBackend.js +69 -0
  28. package/node-red-popup/NodeRedPopupFrontend.js +414 -0
  29. package/node-red-popup/README.md +110 -0
  30. package/node-red-pubsub-bus/NodeRedPubsubBus.js +617 -0
  31. package/node-red-pubsub-bus/NodeRedPubsubBusWorkerProxy.js +239 -0
  32. package/node-red-pubsub-bus/README.md +201 -0
  33. package/node-red-resourceendpoint/NodeRedResourceEndpoint.js +205 -0
  34. package/node-red-resourceendpoint/README.md +119 -0
  35. package/node-red-singleton/NodeRedSingletonBackend.js +30 -0
  36. package/node-red-singleton/NodeRedSingletonFrontend.js +54 -0
  37. package/node-red-singleton/README.md +72 -0
  38. package/node-red-worker/NodeRedWorker.js +438 -0
  39. package/node-red-worker/NodeRedWorkerRuntime.js +651 -0
  40. package/node-red-worker/README.md +265 -0
  41. package/node-red-worker/workerRuntimeLoader.js +55 -0
  42. 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)