@vitormnm/node-red-simple-opcua 1.3.2 → 1.4.1

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.
@@ -1,1094 +1,6 @@
1
- <style>
2
- body.opcua-tree-modal-open {
3
- overflow: hidden;
4
- }
1
+ <link rel="stylesheet" type="text/css" href="opcua-client-resource/style.css">
5
2
 
6
- .opcua-tree-editor {
7
- width: 100%;
8
- min-height: 120px;
9
- border: 1px solid var(--red-ui-form-input-border-color, #d9d9d9);
10
- background: var(--red-ui-form-input-background, #fff);
11
- border-radius: 4px;
12
- overflow: auto;
13
- }
14
-
15
- .opcua-tree-row {
16
- display: flex;
17
- align-items: center;
18
- gap: 5px;
19
- min-height: 24px;
20
- padding: 2px 6px;
21
- font-size: 12px;
22
- border-bottom: 1px solid transparent;
23
- }
24
-
25
- .opcua-tree-row:hover {
26
- background: var(--red-ui-list-item-background-hover, #f3f7fd);
27
- }
28
-
29
- .opcua-tree-row.is-selected {
30
- background: var(--red-ui-list-item-background-selected, #d9ecff);
31
- color: var(--red-ui-list-item-color-selected, inherit);
32
- }
33
-
34
- .opcua-tree-indent { flex: 0 0 auto; width: 14px; }
35
- .opcua-tree-twisty { width: 16px; text-align: center; color: #777; flex: 0 0 16px; cursor: pointer; }
36
- .opcua-tree-icon { width: 14px; text-align: center; color: #777; flex: 0 0 14px; }
37
- .opcua-tree-label { flex: 1 1 auto; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
38
- .opcua-tree-type { color: #777; font-size: 11px; flex: 0 0 auto; }
39
-
40
- .opcua-tree-title {
41
- min-width: 100px;
42
- font-weight: 600;
43
- color: #444;
44
- }
45
-
46
- .opcua-client-nodeid-label {
47
- margin-left: auto;
48
- text-align: right;
49
- word-break: break-all;
50
- max-width: 320px;
51
- color: #777;
52
- font-size: 12px;
53
- }
54
-
55
- .opcua-tree-actions {
56
- margin-left: auto;
57
- display: flex;
58
- gap: 6px;
59
- align-items: center;
60
- }
61
-
62
- .opcua-tree-empty {
63
- padding: 14px;
64
- border: 1px dashed #d9d9d9;
65
- border-radius: 4px;
66
- background: #fafafa;
67
- color: #777;
68
- text-align: center;
69
- }
70
-
71
- .opcua-client-json {
72
- width: 100%;
73
- box-sizing: border-box;
74
- }
75
-
76
- .opcua-client-tag-box {
77
- width: 100%;
78
- min-height: 70px;
79
- }
80
-
81
- .opcua-client-tag-chip {
82
- display: grid;
83
- grid-template-columns: 18px 1fr auto;
84
- grid-template-areas:
85
- "icon name name"
86
- "icon write right";
87
- column-gap: 8px;
88
- row-gap: 6px;
89
- align-items: center;
90
- border: 1px solid #d9d9d9;
91
- border-radius: 4px;
92
- background: #fafafa;
93
- padding: 6px 8px;
94
- margin-bottom: 6px;
95
- }
96
- .opcua-client-tag-chip .opcua-tree-icon { grid-area: icon; }
97
- .opcua-client-tag-chip .opcua-tree-title { grid-area: name; min-width: 0; }
98
- .opcua-client-tag-right {
99
- grid-area: right;
100
- display: flex;
101
- align-items: center;
102
- gap: 8px;
103
- }
104
- .opcua-client-tag-right .opcua-client-nodeid-label {
105
- margin: 0;
106
- max-width: 320px;
107
- }
108
- .opcua-client-tag-right .opcua-tree-actions {
109
- margin: 0;
110
- }
111
-
112
- .opcua-client-tag-chip-meta,
113
- .opcua-client-description {
114
- color: #777;
115
- font-size: 12px;
116
- }
117
- .opcua-client-tag-write {
118
- display: flex;
119
- gap: 8px;
120
- align-items: center;
121
- width: 100%;
122
- margin-top: 0;
123
- margin-left: 0;
124
- grid-area: write;
125
- }
126
- .opcua-client-tag-write input {
127
- font-size: 12px;
128
- }
129
- .opcua-client-tag-write input {
130
- flex: 1 1 auto;
131
- min-width: 120px;
132
- }
133
- .opcua-client-tag-write .red-ui-typedInput-container {
134
- flex: 1 1 auto;
135
- min-width: 220px;
136
- width: 100% !important;
137
- }
138
- .opcua-client-tag-write .red-ui-typedInput-container input.red-ui-typedInput-input {
139
- width: calc(100% - 92px);
140
- min-width: 120px;
141
- }
142
-
143
- .opcua-client-load-row {
144
- display: flex;
145
- gap: 8px;
146
- align-items: center;
147
- }
148
-
149
- .opcua-client-load-row input {
150
- flex: 1 1 auto;
151
- }
152
-
153
- .opcua-tree-search {
154
- display: flex;
155
- align-items: center;
156
- gap: 8px;
157
- width: 100%;
158
- padding: 6px 10px;
159
- border: 1px solid #d9d9d9;
160
- border-radius: 6px;
161
- background: #fafafa;
162
- }
163
-
164
- .opcua-tree-search input {
165
- flex: 1 1 auto;
166
- min-width: 120px;
167
- border: 0;
168
- outline: 0;
169
- background: transparent;
170
- box-shadow: none;
171
- margin: 0;
172
- }
173
-
174
- .opcua-tree-modal {
175
- position: fixed;
176
- inset: 0;
177
- z-index: 2000;
178
- background: rgba(0, 0, 0, 0.45);
179
- padding: 24px;
180
- box-sizing: border-box;
181
- }
182
-
183
- .opcua-tree-modal__dialog {
184
- display: flex;
185
- flex-direction: column;
186
- width: 100%;
187
- height: 100%;
188
- background: #f3f3f3;
189
- border-radius: 8px;
190
- overflow: hidden;
191
- box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
192
- }
193
-
194
- .opcua-tree-modal__header,
195
- .opcua-tree-modal__toolbar {
196
- display: flex;
197
- align-items: center;
198
- flex-wrap: wrap;
199
- gap: 10px;
200
- padding: 14px 18px;
201
- background: #fff;
202
- border-bottom: 1px solid #d9d9d9;
203
- }
204
-
205
- .opcua-tree-modal__title {
206
- font-size: 18px;
207
- font-weight: 600;
208
- color: #333;
209
- }
210
-
211
- .opcua-tree-modal__header .editor-button-small {
212
- margin-left: auto;
213
- }
214
-
215
- .opcua-tree-modal__body {
216
- flex: 1 1 auto;
217
- overflow: auto;
218
- padding: 18px;
219
- }
220
-
221
- .opcua-tree-context-menu {
222
- position: fixed;
223
- z-index: 2200;
224
- min-width: 170px;
225
- background: #fff;
226
- border: 1px solid #d9d9d9;
227
- border-radius: 6px;
228
- box-shadow: 0 6px 18px rgba(0, 0, 0, 0.18);
229
- padding: 6px 0;
230
- }
231
-
232
- .opcua-tree-context-menu a {
233
- display: block;
234
- padding: 7px 12px;
235
- color: #333;
236
- text-decoration: none;
237
- font-size: 12px;
238
- }
239
-
240
- .opcua-tree-context-menu a:hover {
241
- background: #f3f7fd;
242
- }
243
- </style>
244
-
245
- <script type="text/javascript">
246
- (function () {
247
- var selectedItemsState = [];
248
- var browseState = null;
249
- var expansionState = {};
250
- var browseSearchValue = "";
251
- var browseSearchTerm = "";
252
- var contextMenuPath = "";
253
- var browseSelectedPath = "";
254
-
255
- function openBrowseModal() { $("#node-input-browse-modal").show(); $("body").addClass("opcua-tree-modal-open"); }
256
- function closeBrowseModal() {
257
- hideTreeContextMenu();
258
- $("#node-input-browse-modal").hide();
259
- $("body").removeClass("opcua-tree-modal-open");
260
- }
261
-
262
- function getBrowseCacheKey() {
263
- var connectionId = $("#node-input-connection").val() || "";
264
- if (!connectionId) return "";
265
- return "opcua-client-browse-cache:" + connectionId;
266
- }
267
-
268
- function saveBrowseSession() {
269
- var key = getBrowseCacheKey();
270
- if (!key || !window.sessionStorage) return;
271
- try {
272
- sessionStorage.setItem(key, JSON.stringify({
273
- browseState: browseState,
274
- expansionState: expansionState
275
- }));
276
- } catch (error) {}
277
- }
278
-
279
- function loadBrowseSession() {
280
- var key = getBrowseCacheKey();
281
- if (!key || !window.sessionStorage) return false;
282
- try {
283
- var raw = sessionStorage.getItem(key);
284
- if (!raw) return false;
285
- var parsed = JSON.parse(raw);
286
- if (!parsed || typeof parsed !== "object") return false;
287
- browseState = parsed.browseState && typeof parsed.browseState === "object" ? parsed.browseState : null;
288
- expansionState = parsed.expansionState && typeof parsed.expansionState === "object" ? parsed.expansionState : {};
289
- return !!browseState;
290
- } catch (error) {
291
- return false;
292
- }
293
- }
294
-
295
- function parseSelectedItems(rawValue) {
296
- if (!rawValue) {
297
- return [];
298
- }
299
-
300
- try {
301
- var parsed = JSON.parse(rawValue);
302
- if (!Array.isArray(parsed)) {
303
- return [];
304
- }
305
-
306
- return parsed
307
- .filter(function (item) {
308
- return item && typeof item === "object" && !Array.isArray(item);
309
- })
310
- .map(function (item) {
311
- return {
312
- name: typeof item.name === "string" ? item.name.trim() : "",
313
- nodeID: typeof (item.nodeID || item.nodeId) === "string" ? String(item.nodeID || item.nodeId).trim() : "",
314
- type: typeof (item.type || item.dataType) === "string" ? String(item.type || item.dataType).trim() : "",
315
- nodeClass: typeof item.nodeClass === "string" ? item.nodeClass.trim() : "",
316
- typeDefinition: typeof (item.typeDefinition || item.typeDefinitionName) === "string" ? String(item.typeDefinition || item.typeDefinitionName).trim() : "",
317
- hasTypeDefinition: item.hasTypeDefinition && typeof item.hasTypeDefinition === "object" ? item.hasTypeDefinition : null,
318
- valueProperty: typeof item.valueProperty === "string" && item.valueProperty.trim() ? item.valueProperty.trim() : "payload",
319
- valuePropertyType: (item.valuePropertyType === "msg" || item.valuePropertyType === "flow" || item.valuePropertyType === "global") ? item.valuePropertyType : "msg"
320
- };
321
- })
322
- .filter(function (item) {
323
- return !!item.nodeID;
324
- });
325
- } catch (error) {
326
- return [];
327
- }
328
- }
329
-
330
- function serializeSelectedItems(items) {
331
- return JSON.stringify(items.map(function (item) {
332
- var result = {
333
- name: item.name || item.nodeID,
334
- nodeID: item.nodeID
335
- };
336
-
337
- if (item.type) {
338
- result.type = item.type;
339
- }
340
- if (item.nodeClass) {
341
- result.nodeClass = item.nodeClass;
342
- }
343
- if (item.typeDefinition) {
344
- result.typeDefinition = item.typeDefinition;
345
- }
346
- if (item.hasTypeDefinition) {
347
- result.hasTypeDefinition = item.hasTypeDefinition;
348
- }
349
- if (item.valueProperty) {
350
- result.valueProperty = item.valueProperty;
351
- }
352
- if (item.valuePropertyType) {
353
- result.valuePropertyType = item.valuePropertyType;
354
- }
355
-
356
- return result;
357
- }), null, 2);
358
- }
359
-
360
- function escapeHtml(value) {
361
- return String(value || "").replace(/[&<>"']/g, function (char) {
362
- return {
363
- "&": "&amp;",
364
- "<": "&lt;",
365
- ">": "&gt;",
366
- '"': "&quot;",
367
- "'": "&#39;"
368
- }[char];
369
- });
370
- }
371
-
372
- function updateSelectedItemsField() {
373
- $("#node-input-selectedItems").val(serializeSelectedItems(selectedItemsState));
374
- }
375
-
376
- function normalizeSearchTerm(value) {
377
- return String(value || "").trim().toLowerCase();
378
- }
379
-
380
- function textForSearch(item) {
381
- if (!item || typeof item !== "object") return "";
382
- return [
383
- item.name,
384
- item.displayName,
385
- item.browseName,
386
- item.nodeID,
387
- item.nodeClass,
388
- item.dataType,
389
- item.description
390
- ].filter(Boolean).join(" ").toLowerCase();
391
- }
392
-
393
- function nodeMatchesSearch(item, term) {
394
- if (!term) return true;
395
- return textForSearch(item).indexOf(term) >= 0;
396
- }
397
-
398
- function branchHasSearchMatch(item, term) {
399
- if (nodeMatchesSearch(item, term)) return true;
400
- if (!item || !Array.isArray(item.browse)) return false;
401
- for (var i = 0; i < item.browse.length; i += 1) {
402
- if (branchHasSearchMatch(item.browse[i], term)) return true;
403
- }
404
- return false;
405
- }
406
-
407
- function renderSelectedTags() {
408
- var container = $("#node-input-selected-tags");
409
- container.empty();
410
- var writeMode = $("#node-input-mode").val() === "write";
411
-
412
- if (!selectedItemsState.length) {
413
- container.append('<div class="opcua-tree-empty">No tags selected.</div>');
414
- return;
415
- }
416
-
417
- selectedItemsState.forEach(function (item, index) {
418
- var row = $('<div class="opcua-client-tag-chip"></div>');
419
- var icon = browseIconFor(item);
420
- row.append('<div class="opcua-tree-icon"><i class="fa ' + icon + '"></i></div>');
421
- row.append('<div class="opcua-tree-title">' + escapeHtml(item.name || item.nodeID) + '</div>');
422
- if (writeMode) {
423
- var type = (item.valuePropertyType === "flow" || item.valuePropertyType === "global") ? item.valuePropertyType : "msg";
424
- var prop = item.valueProperty || "payload";
425
- row.append('<div class="opcua-client-tag-write">'
426
- + '<input type="text" class="opcua-client-item-value-prop" id="opcua-client-item-value-prop-' + index + '" data-index="' + index + '" value="' + escapeHtml(prop) + '" placeholder="payload">'
427
- + '<input type="hidden" class="opcua-client-item-value-type" id="opcua-client-item-value-type-' + index + '" data-index="' + index + '" value="' + escapeHtml(type) + '">'
428
- + "</div>");
429
- }
430
- row.append('<div class="opcua-client-tag-right">'
431
- + '<div class="opcua-client-nodeid-label">' + escapeHtml(item.nodeID) + '</div>'
432
- + '<div class="opcua-tree-actions"><a href="#" class="editor-button editor-button-small opcua-client-remove-tag" data-index="' + index + '"><i class="fa fa-trash"></i></a></div>'
433
- + "</div>");
434
- container.append(row);
435
- });
436
- initializeSelectedItemTypedInputs();
437
- }
438
-
439
- function initializeSelectedItemTypedInputs() {
440
- $(".opcua-client-item-value-prop").each(function () {
441
- var input = $(this);
442
- var index = Number(input.attr("data-index"));
443
- var typeField = "#opcua-client-item-value-type-" + index;
444
- if (input.data("typedInputInitialized")) {
445
- input.typedInput("types", ["msg", "flow", "global"]);
446
- return;
447
- }
448
-
449
- input.typedInput({
450
- type: $(typeField).val() || "msg",
451
- types: ["msg", "flow", "global"],
452
- typeField: typeField
453
- });
454
- input.data("typedInputInitialized", true);
455
- });
456
- }
457
-
458
- function syncSelectedItems() {
459
- updateSelectedItemsField();
460
- renderSelectedTags();
461
- renderBrowseTree();
462
- }
463
-
464
- function isExpanded(path, defaultValue) {
465
- if (expansionState[path] === undefined) {
466
- expansionState[path] = !!defaultValue;
467
- }
468
-
469
- return expansionState[path];
470
- }
471
-
472
- function selectedIndexByNodeId(nodeId) {
473
- return selectedItemsState.findIndex(function (item) {
474
- return item.nodeID === nodeId;
475
- });
476
- }
477
-
478
- function canExpand(item) {
479
- // return item && item.nodeClass !== "Variable" && item.nodeClass !== "Method";
480
- return item && item.nodeClass !== "Variable"
481
- }
482
-
483
- function isVariable(item) {
484
- return String(item && item.nodeClass || "").toLowerCase() === "variable";
485
- }
486
-
487
- function nodeIdOf(item) {
488
- return item && (item.nodeID || item.nodeId) ? String(item.nodeID || item.nodeId) : "";
489
- }
490
-
491
- function loadBrowse(nodeId) {
492
- var connectionId = $("#node-input-connection").val();
493
- if (!connectionId) {
494
- RED.notify("Select an OPC UA connection before browsing.", "warning");
495
- return $.Deferred().reject().promise();
496
- }
497
-
498
- return $.getJSON("opcua-client-config/" + encodeURIComponent(connectionId) + "/browse", {
499
- nodeId: nodeId || "i=84"
500
- });
501
- }
502
-
503
- function renderBrowseTree() {
504
- var container = $("#node-input-browse-tree");
505
- container.empty();
506
-
507
- if (!browseState) {
508
- container.append('<div class="opcua-tree-empty">Click Browse to load the server tree.</div>');
509
- return;
510
- }
511
-
512
- if (browseSearchTerm) {
513
- if (!branchHasSearchMatch(browseState, browseSearchTerm)) {
514
- container.append('<div class="opcua-tree-empty">No items found in the already explored items.</div>');
515
- return;
516
- }
517
- renderBrowseRootFiltered(browseState, "root", 0, container, browseSearchTerm);
518
- return;
519
- }
520
-
521
- renderBrowseRoot(browseState, "root", 0, container);
522
- }
523
-
524
- function isFolderNode(item) {
525
- if (!item) return false;
526
- var nodeClass = String(item.nodeClass || "");
527
- var typeDefinition = String(item.typeDefinition || item.typeDefinitionName || "").toLowerCase();
528
- var hasTypeDefinitionBrowseName = String(
529
- item.hasTypeDefinition && item.hasTypeDefinition.browseName || ""
530
- ).toLowerCase();
531
- var explicitType = String(item.type || item.kind || "").toLowerCase();
532
- if (nodeClass === "Folder") return true;
533
- if (hasTypeDefinitionBrowseName === "foldertype") return true;
534
- if (typeDefinition.indexOf("folder") >= 0) return true;
535
- if (explicitType === "folder") return true;
536
- return false;
537
- }
538
-
539
- function browseIconFor(item) {
540
- var nodeClass = String((item && item.nodeClass) || "");
541
- if (isFolderNode(item)) return "fa-folder";
542
- if (nodeClass === "Object") return "fa-cube";
543
- if (nodeClass === "Method") return "fa-cog";
544
- if (nodeClass === "Variable") return "fa-tag";
545
- if (nodeClass === "ObjectType") return "fa-cubes";
546
- if (nodeClass === "View") return "fa-eye";
547
- if (nodeClass === "DataType") return "fa-database";
548
- if (nodeClass === "ReferenceType") return "fa-random";
549
- return "fa-tag";
550
- }
551
-
552
- function renderBrowseRoot(root, path, depth, container) {
553
- var expanded = isExpanded(path, true);
554
- var row = $('<div class="opcua-tree-row"></div>').attr("data-path", path);
555
- row.append('<span class="opcua-tree-indent"></span>');
556
- row.append('<span class="opcua-tree-twisty opcua-client-toggle-tree" data-path="' + path + '">' + (expanded ? '<i class="fa fa-caret-down"></i>' : '<i class="fa fa-caret-right"></i>') + '</span>');
557
- row.append('<span class="opcua-tree-icon"><i class="fa fa-sitemap"></i></span>');
558
- row.append('<span class="opcua-tree-label">' + escapeHtml(root.name || root.nodeID || "RootFolder") + '</span>');
559
- row.append('<span class="opcua-tree-type">' + escapeHtml(root.nodeID || "") + '</span>');
560
- container.append(row);
561
-
562
- if (!expanded) return;
563
-
564
- if (!Array.isArray(root.browse) || !root.browse.length) {
565
- container.append('<div class="opcua-tree-empty">No items found..</div>');
566
- } else {
567
- root.browse.forEach(function (item, index) {
568
- renderBrowseItem(item, path + ".browse." + index, depth + 1, container);
569
- });
570
- }
571
- }
572
-
573
- function renderBrowseItem(item, path, depth, container) {
574
- var expanded = isExpanded(path, false);
575
- var nodeId = nodeIdOf(item);
576
- var selectedIndex = selectedIndexByNodeId(nodeId);
577
- var hasChildren = canExpand(item);
578
- var row = $('<div class="opcua-tree-row"></div>').attr("data-path", path);
579
- if (selectedIndex >= 0) row.addClass("is-selected");
580
-
581
- for (var i = 0; i < depth; i += 1) {
582
- row.append('<span class="opcua-tree-indent"></span>');
583
- }
584
- row.append('<span class="opcua-tree-twisty' + (hasChildren ? ' opcua-client-toggle-tree' : '') + '" data-path="' + path + '">' + (hasChildren ? '<i class="fa ' + (expanded ? 'fa-caret-down' : 'fa-caret-right') + '"></i>' : '') + '</span>');
585
- row.append('<span class="opcua-tree-icon"><i class="fa ' + browseIconFor(item) + '"></i></span>');
586
- row.append('<span class="opcua-tree-label">' + escapeHtml(item.displayName || item.browseName || item.nodeID) + '</span>');
587
- row.append('<span class="opcua-tree-type">' + escapeHtml(item.nodeClass || "") + (item.dataType ? " | " + escapeHtml(item.dataType) : "") + '</span>');
588
- row.append('<span class="opcua-client-nodeid-label">' + escapeHtml(nodeId) + '</span>');
589
-
590
- if (nodeId) {
591
- var actionLabel = selectedIndex >= 0 ? "Remove" : "Add";
592
- var actionIcon = selectedIndex >= 0 ? "fa-minus" : "fa-plus";
593
- row.append('<div class="opcua-tree-actions"><a href="#" class="editor-button editor-button-small opcua-client-toggle-tag" data-nodeid="' + escapeHtml(nodeId) + '" data-path="' + path + '"><i class="fa ' + actionIcon + '"></i> ' + actionLabel + '</a></div>');
594
- }
595
- container.append(row);
596
-
597
- if (item.description) {
598
- container.append('<div class="opcua-client-description" style="padding: 0 10px 8px ' + String((depth + 2) * 14) + 'px;">' + escapeHtml(item.description) + '</div>');
599
- }
600
-
601
- if (expanded && hasChildren) {
602
- if (Array.isArray(item.browse)) {
603
- if (!item.browse.length) {
604
- container.append('<div class="opcua-tree-empty">No children found..</div>');
605
- } else {
606
- item.browse.forEach(function (child, index) {
607
- renderBrowseItem(child, path + ".browse." + index, depth + 1, container);
608
- });
609
- }
610
- } else {
611
- container.append('<div class="opcua-tree-empty">Searching for items...</div>');
612
- }
613
- }
614
- }
615
-
616
- function renderBrowseRootFiltered(root, path, depth, container, term) {
617
- var row = $('<div class="opcua-tree-row"></div>').attr("data-path", path);
618
- row.append('<span class="opcua-tree-indent"></span>');
619
- row.append('<span class="opcua-tree-twisty"><i class="fa fa-caret-down"></i></span>');
620
- row.append('<span class="opcua-tree-icon"><i class="fa fa-sitemap"></i></span>');
621
- row.append('<span class="opcua-tree-label">' + escapeHtml(root.name || root.nodeID || "RootFolder") + '</span>');
622
- row.append('<span class="opcua-tree-type">' + escapeHtml(root.nodeID || "") + '</span>');
623
- container.append(row);
624
-
625
- if (!Array.isArray(root.browse) || !root.browse.length) {
626
- container.append('<div class="opcua-tree-empty">Nenhum item encontrado.</div>');
627
- return;
628
- }
629
-
630
- root.browse.forEach(function (item, index) {
631
- if (branchHasSearchMatch(item, term)) {
632
- renderBrowseItemFiltered(item, path + ".browse." + index, depth + 1, container, term, false);
633
- }
634
- });
635
- }
636
-
637
- function renderBrowseItemFiltered(item, path, depth, container, term, ancestorMatched) {
638
- if (!branchHasSearchMatch(item, term)) return;
639
-
640
- var nodeId = nodeIdOf(item);
641
- var selectedIndex = selectedIndexByNodeId(nodeId);
642
- var hasChildren = canExpand(item);
643
- var subtreeVisible = !!ancestorMatched || nodeMatchesSearch(item, term);
644
- var hasMatchingLoadedChild = hasChildren && Array.isArray(item.browse) && item.browse.some(function (child) {
645
- return branchHasSearchMatch(child, term);
646
- });
647
- var hasExplicitExpansion = expansionState[path] !== undefined;
648
- var expanded = hasChildren && (hasExplicitExpansion
649
- ? !!expansionState[path]
650
- : ((subtreeVisible && Array.isArray(item.browse)) || hasMatchingLoadedChild));
651
- var row = $('<div class="opcua-tree-row"></div>').attr("data-path", path);
652
- if (selectedIndex >= 0) row.addClass("is-selected");
653
-
654
- for (var i = 0; i < depth; i += 1) row.append('<span class="opcua-tree-indent"></span>');
655
- row.append('<span class="opcua-tree-twisty' + (hasChildren ? ' opcua-client-toggle-tree' : '') + '" data-path="' + path + '">' + (hasChildren ? '<i class="fa ' + (expanded ? 'fa-caret-down' : 'fa-caret-right') + '"></i>' : '') + '</span>');
656
- row.append('<span class="opcua-tree-icon"><i class="fa ' + browseIconFor(item) + '"></i></span>');
657
- row.append('<span class="opcua-tree-label">' + escapeHtml(item.displayName || item.browseName || item.nodeID) + '</span>');
658
- row.append('<span class="opcua-tree-type">' + escapeHtml(item.nodeClass || "") + (item.dataType ? " | " + escapeHtml(item.dataType) : "") + '</span>');
659
- row.append('<span class="opcua-client-nodeid-label">' + escapeHtml(nodeId) + '</span>');
660
-
661
- if (nodeId) {
662
- var actionLabel = selectedIndex >= 0 ? "Remove" : "Add";
663
- var actionIcon = selectedIndex >= 0 ? "fa-minus" : "fa-plus";
664
- row.append('<div class="opcua-tree-actions"><a href="#" class="editor-button editor-button-small opcua-client-toggle-tag" data-nodeid="' + escapeHtml(nodeId) + '" data-path="' + path + '"><i class="fa ' + actionIcon + '"></i> ' + actionLabel + '</a></div>');
665
- }
666
- container.append(row);
667
-
668
- if (item.description) {
669
- container.append('<div class="opcua-client-description" style="padding: 0 10px 8px ' + String((depth + 2) * 14) + 'px;">' + escapeHtml(item.description) + '</div>');
670
- }
671
-
672
- if (expanded && hasChildren) {
673
- if (Array.isArray(item.browse)) {
674
- if (!item.browse.length) {
675
- container.append('<div class="opcua-tree-empty">Nenhum filho encontrado.</div>');
676
- } else {
677
- item.browse.forEach(function (child, index) {
678
- if (subtreeVisible || branchHasSearchMatch(child, term)) {
679
- renderBrowseItemFiltered(child, path + ".browse." + index, depth + 1, container, term, subtreeVisible);
680
- }
681
- });
682
- }
683
- } else {
684
- container.append('<div class="opcua-tree-empty">Expandindo...</div>');
685
- }
686
- }
687
- }
688
-
689
- function getItemAtPath(path) {
690
- var tokens = String(path || "").split(".");
691
- var current = browseState;
692
-
693
- for (var index = 0; index < tokens.length; index += 1) {
694
- var token = tokens[index];
695
- if (!token || token === "root") {
696
- continue;
697
- }
698
-
699
- if (!current) {
700
- return null;
701
- }
702
-
703
- if (/^\d+$/.test(token)) {
704
- current = current[Number(token)];
705
- } else {
706
- current = current[token];
707
- }
708
- }
709
-
710
- return current || null;
711
- }
712
-
713
- function addSelectedItem(item) {
714
- var normalized = {
715
- name: item.displayName || item.browseName || item.name || item.nodeID,
716
- nodeID: item.nodeID || item.nodeId,
717
- type: item.dataType || item.type || "",
718
- nodeClass: item.nodeClass || "",
719
- typeDefinition: item.typeDefinition || item.typeDefinitionName || "",
720
- hasTypeDefinition: item.hasTypeDefinition || null,
721
- valueProperty: item.valueProperty || "payload",
722
- valuePropertyType: item.valuePropertyType || "msg"
723
- };
724
- var currentIndex = selectedIndexByNodeId(normalized.nodeID);
725
-
726
- if (currentIndex >= 0) {
727
- selectedItemsState[currentIndex] = normalized;
728
- } else {
729
- selectedItemsState.push(normalized);
730
- }
731
-
732
- syncSelectedItems();
733
- }
734
-
735
- function removeSelectedItemByIndex(index) {
736
- selectedItemsState.splice(index, 1);
737
- syncSelectedItems();
738
- }
739
-
740
- function toggleSelectedNode(path) {
741
- var item = getItemAtPath(path);
742
- var nodeId = nodeIdOf(item);
743
- if (!item || !nodeId) {
744
- return;
745
- }
746
-
747
- var currentIndex = selectedIndexByNodeId(nodeId);
748
- if (currentIndex >= 0) {
749
- selectedItemsState.splice(currentIndex, 1);
750
- } else {
751
- addSelectedItem(item);
752
- return;
753
- }
754
-
755
- syncSelectedItems();
756
- }
757
-
758
- function refreshBrowseRoot() {
759
- var rootNodeId = "i=84";
760
- var container = $("#node-input-browse-tree");
761
- container.html('<div class="opcua-tree-empty">Carregando...</div>');
762
-
763
- loadBrowse(rootNodeId).done(function (payload) {
764
- browseState = payload;
765
- expansionState = { root: true };
766
- saveBrowseSession();
767
- renderBrowseTree();
768
- }).fail(function (xhr) {
769
- var message = xhr && xhr.responseJSON && xhr.responseJSON.error
770
- ? xhr.responseJSON.error
771
- : "Falha ao navegar no servidor OPC UA.";
772
- browseState = null;
773
- container.html('<div class="opcua-tree-empty">' + escapeHtml(message) + '</div>');
774
- RED.notify(message, "error");
775
- });
776
- }
777
-
778
- function hideTreeContextMenu() {
779
- contextMenuPath = "";
780
- $("#node-input-browse-context-menu").hide();
781
- }
782
-
783
- function showTreeContextMenu(x, y, path) {
784
- var menu = $("#node-input-browse-context-menu");
785
- var item = getItemAtPath(path);
786
- contextMenuPath = path || "";
787
- $("#node-input-browse-context-refresh").toggle(!!item && !isVariable(item));
788
- $("#node-input-browse-context-copy-nodeid").toggle(!!nodeIdOf(item));
789
- menu.css({ left: x + "px", top: y + "px" }).show();
790
- }
791
-
792
- function copyNodeIdFromPath(path) {
793
- var item = getItemAtPath(path);
794
- var nodeId = nodeIdOf(item);
795
- if (!nodeId) {
796
- RED.notify("NodeID nao encontrado para o item selecionado.", "warning");
797
- return;
798
- }
799
-
800
- if (navigator.clipboard && typeof navigator.clipboard.writeText === "function") {
801
- navigator.clipboard.writeText(nodeId).then(function () {
802
- RED.notify("NodeID copiado.", "success");
803
- }).catch(function () {
804
- RED.notify("Falha ao copiar NodeID.", "error");
805
- });
806
- return;
807
- }
808
-
809
- var input = $("<textarea readonly></textarea>").val(nodeId).css({
810
- position: "fixed",
811
- left: "-9999px",
812
- top: "0"
813
- });
814
- $("body").append(input);
815
- input[0].select();
816
- try {
817
- document.execCommand("copy");
818
- RED.notify("NodeID copiado.", "success");
819
- } catch (error) {
820
- RED.notify("Falha ao copiar NodeID.", "error");
821
- }
822
- input.remove();
823
- }
824
-
825
- function setBrowseSelectedPath(path) {
826
- browseSelectedPath = path || "";
827
- $(".opcua-tree-row").removeClass("is-selected");
828
- if (browseSelectedPath) {
829
- $('.opcua-tree-row[data-path="' + browseSelectedPath + '"]').addClass("is-selected");
830
- }
831
- }
832
-
833
- function refreshNode(path) {
834
- if (!path) {
835
- return;
836
- }
837
- if (path === "root") {
838
- refreshBrowseRoot();
839
- return;
840
- }
841
-
842
- var item = getItemAtPath(path);
843
- if (!item || isVariable(item)) {
844
- return;
845
- }
846
-
847
- // Force a new discovery using the same lazy-load flow from expand.
848
- item.browse = undefined;
849
- expansionState[path] = true;
850
- saveBrowseSession();
851
- renderBrowseTree();
852
- expandNode(path);
853
- }
854
-
855
- function expandNode(path) {
856
- var item = getItemAtPath(path);
857
- if (!item || !canExpand(item)) {
858
- return;
859
- }
860
-
861
- if (isExpanded(path, false) && !Array.isArray(item.browse)) {
862
- renderBrowseTree();
863
- } else {
864
- expansionState[path] = !isExpanded(path, false);
865
- saveBrowseSession();
866
- if (!expansionState[path]) {
867
- renderBrowseTree();
868
- return;
869
- }
870
- }
871
-
872
- if (Array.isArray(item.browse)) {
873
- renderBrowseTree();
874
- return;
875
- }
876
-
877
- renderBrowseTree();
878
- var browseNodeId = item.nodeID || item.nodeId;
879
- loadBrowse(browseNodeId).done(function (payload) {
880
- item.browse = Array.isArray(payload.browse) ? payload.browse : [];
881
- saveBrowseSession();
882
- renderBrowseTree();
883
- }).fail(function (xhr) {
884
- expansionState[path] = false;
885
- saveBrowseSession();
886
- renderBrowseTree();
887
- var message = xhr && xhr.responseJSON && xhr.responseJSON.error
888
- ? xhr.responseJSON.error
889
- : "Falha ao expandir o node.";
890
- RED.notify(message, "error");
891
- });
892
- }
893
-
894
- function toggleModeFields() {
895
- var isSubscription = $("#node-input-mode").val() === "subscription" || $("#node-input-mode").val() === "events";
896
- var supportsSelection = $("#node-input-mode").val() !== "method" && $("#node-input-mode").val() !== "getSubscriptionId" ;
897
- $(".opcua-client-subscription-row").toggle(isSubscription);
898
- $(".opcua-client-selection-row").toggle(supportsSelection);
899
- renderSelectedTags();
900
- }
901
-
902
- RED.nodes.registerType("opcua-client", {
903
- category: "network",
904
- color: "#d9edf7",
905
- defaults: {
906
- name: { value: "" },
907
- connection: { value: "", type: "opcua-client-config", required: true },
908
- mode: { value: "read", required: true },
909
- selectedItems: {
910
- value: "[]",
911
- validate: function (value) {
912
- try {
913
- return Array.isArray(JSON.parse(value || "[]"));
914
- } catch (error) {
915
- return false;
916
- }
917
- }
918
- },
919
- samplingInterval: {
920
- value: 250,
921
- validate: RED.validators.number()
922
- },
923
- publishingInterval: {
924
- value: 250,
925
- validate: RED.validators.number()
926
- }
927
- },
928
- inputs: 1,
929
- outputs: 1,
930
- icon: "bridge.svg",
931
- label: function () {
932
- return this.name || "opcua-client";
933
- },
934
- oneditprepare: function () {
935
- $("#node-input-selectedItems").typedInput({
936
- type: "json",
937
- types: ["json"]
938
- });
939
-
940
- selectedItemsState = parseSelectedItems(this.selectedItems);
941
-
942
- browseState = null;
943
- expansionState = {};
944
- browseSelectedPath = "";
945
- browseSearchValue = "";
946
- browseSearchTerm = "";
947
- $("#node-input-browse-search").val("");
948
- $("#node-input-browse-search-clear").hide();
949
- loadBrowseSession();
950
- syncSelectedItems();
951
- renderBrowseTree();
952
-
953
- $("#node-input-mode").off("change").on("change", toggleModeFields);
954
- $("#node-input-browse-root").off("click").on("click", function (event) {
955
- event.preventDefault();
956
- refreshBrowseRoot();
957
- });
958
- $("#node-input-open-browse-modal").off("click").on("click", function (event) {
959
- event.preventDefault();
960
- openBrowseModal();
961
- });
962
- $("#node-input-close-browse-modal").off("click").on("click", function (event) {
963
- event.preventDefault();
964
- closeBrowseModal();
965
- });
966
- $("#node-input-browse-modal").off("click").on("click", function (event) {
967
- hideTreeContextMenu();
968
- if (event.target === this) closeBrowseModal();
969
- });
970
- $("#node-input-browse-search").off("input").on("input", function () {
971
- browseSearchValue = $(this).val();
972
- browseSearchTerm = normalizeSearchTerm(browseSearchValue);
973
- $("#node-input-browse-search-clear").toggle(!!browseSearchTerm);
974
- renderBrowseTree();
975
- });
976
- $("#node-input-browse-search-clear").off("click").on("click", function (event) {
977
- event.preventDefault();
978
- browseSearchValue = "";
979
- browseSearchTerm = "";
980
- $("#node-input-browse-search").val("");
981
- $(this).hide();
982
- renderBrowseTree();
983
- });
984
- $("#node-input-connection").off("change.opcuaClientBrowse").on("change.opcuaClientBrowse", function () {
985
- browseState = null;
986
- expansionState = {};
987
- browseSelectedPath = "";
988
- loadBrowseSession();
989
- renderBrowseTree();
990
- });
991
- $(document).off("keydown.opcuaClientBrowseModal").on("keydown.opcuaClientBrowseModal", function (event) {
992
- hideTreeContextMenu();
993
- if (event.key === "Escape") closeBrowseModal();
994
- });
995
-
996
- toggleModeFields();
997
- },
998
- oneditsave: function () {
999
- updateSelectedItemsField();
1000
- saveBrowseSession();
1001
- closeBrowseModal();
1002
- $(document).off("keydown.opcuaClientBrowseModal");
1003
- }
1004
- ,
1005
- oneditcancel: function () {
1006
- closeBrowseModal();
1007
- $(document).off("keydown.opcuaClientBrowseModal");
1008
- }
1009
- });
1010
-
1011
- $(document).on("change", "#node-input-selectedItems", function () {
1012
- selectedItemsState = parseSelectedItems($(this).val());
1013
- renderSelectedTags();
1014
- renderBrowseTree();
1015
- });
1016
-
1017
- $(document).on("click", ".opcua-client-remove-tag", function (event) {
1018
- event.preventDefault();
1019
- removeSelectedItemByIndex(Number($(this).attr("data-index")));
1020
- });
1021
-
1022
- $(document).on("click", ".opcua-client-toggle-tag", function (event) {
1023
- event.preventDefault();
1024
- toggleSelectedNode($(this).attr("data-path"));
1025
- });
1026
-
1027
- $(document).on("click", ".opcua-client-toggle-tree", function (event) {
1028
- event.preventDefault();
1029
- expandNode($(this).attr("data-path"));
1030
- });
1031
- $(document).on("change", ".opcua-client-item-value-type", function () {
1032
- var index = Number($(this).attr("data-index"));
1033
- if (!selectedItemsState[index]) return;
1034
- selectedItemsState[index].valuePropertyType = $(this).val();
1035
- updateSelectedItemsField();
1036
- });
1037
- $(document).on("change input", ".opcua-client-item-value-prop", function () {
1038
- var index = Number($(this).attr("data-index"));
1039
- if (!selectedItemsState[index]) return;
1040
- selectedItemsState[index].valueProperty = $(this).typedInput ? $(this).typedInput("value") : $(this).val();
1041
- var typeField = $("#opcua-client-item-value-type-" + index);
1042
- selectedItemsState[index].valuePropertyType = (typeField.val() || "msg");
1043
- updateSelectedItemsField();
1044
- });
1045
-
1046
- $(document).on("click", ".opcua-tree-row", function (event) {
1047
- if ($(event.target).closest(".opcua-client-toggle-tree, .opcua-client-toggle-tag, .opcua-tree-actions, #node-input-browse-context-menu").length) {
1048
- return;
1049
- }
1050
- setBrowseSelectedPath($(this).attr("data-path"));
1051
- });
1052
-
1053
- $(document).on("contextmenu", ".opcua-tree-row", function (event) {
1054
- var clickedPath = $(this).attr("data-path");
1055
- if (clickedPath) {
1056
- setBrowseSelectedPath(clickedPath);
1057
- }
1058
- var path = browseSelectedPath || clickedPath;
1059
- var item = getItemAtPath(path);
1060
- if (!item) {
1061
- hideTreeContextMenu();
1062
- return;
1063
- }
1064
-
1065
- event.preventDefault();
1066
- showTreeContextMenu(event.clientX, event.clientY, path);
1067
- });
1068
-
1069
- $(document).on("click", "#node-input-browse-context-refresh", function (event) {
1070
- event.preventDefault();
1071
- var highlightedPath = $(".opcua-tree-row.is-selected").first().attr("data-path") || "";
1072
- var path = contextMenuPath || browseSelectedPath || highlightedPath || "";
1073
- hideTreeContextMenu();
1074
- refreshNode(path);
1075
- });
1076
-
1077
- $(document).on("click", "#node-input-browse-context-copy-nodeid", function (event) {
1078
- event.preventDefault();
1079
- var highlightedPath = $(".opcua-tree-row.is-selected").first().attr("data-path") || "";
1080
- var path = contextMenuPath || browseSelectedPath || highlightedPath || "";
1081
- hideTreeContextMenu();
1082
- copyNodeIdFromPath(path);
1083
- });
1084
-
1085
- $(document).on("click", function (event) {
1086
- if (!$(event.target).closest("#node-input-browse-context-menu").length) {
1087
- hideTreeContextMenu();
1088
- }
1089
- });
1090
- })();
1091
- </script>
3
+ <script type="text/javascript" src="opcua-client-resource/script.js"></script>
1092
4
 
1093
5
  <script type="text/html" data-template-name="opcua-client">
1094
6
  <div class="form-row">
@@ -1111,14 +23,11 @@
1111
23
  <option value="events">Events</option>
1112
24
  </select>
1113
25
  </div>
26
+
1114
27
  <div class="form-row opcua-client-selection-row">
1115
28
  <label for="node-input-selectedItems"><i class="fa fa-code"></i> JSON</label>
1116
29
  <input type="text" id="node-input-selectedItems" class="opcua-client-json" style="width: 70%;">
1117
30
  </div>
1118
- <div class="form-row opcua-client-selection-row">
1119
- <label style="width:auto;"><i class="fa fa-sitemap"></i> Browse Tree</label>
1120
- <a href="#" id="node-input-open-browse-modal" class="editor-button"><i class="fa fa-expand"></i> Open browse tree</a>
1121
- </div>
1122
31
  <div id="node-input-browse-modal" class="opcua-tree-modal" style="display:none;">
1123
32
  <div class="opcua-tree-modal__dialog">
1124
33
  <div class="opcua-tree-modal__header">
@@ -1142,6 +51,14 @@
1142
51
  <a href="#" id="node-input-browse-context-copy-nodeid"><i class="fa fa-copy"></i> Copy NodeID</a>
1143
52
  </div>
1144
53
  </div>
54
+
55
+ <div class="form-row opcua-client-selection-row">
56
+ <label style="width:auto;"><i class="fa fa-sitemap"></i> Browse Tree</label>
57
+ <a href="#" id="node-input-open-browse-modal" class="editor-button">
58
+ <i class="fa fa-expand"></i> Open browse tree
59
+ </a>
60
+ </div>
61
+
1145
62
  <div class="form-row opcua-client-subscription-row">
1146
63
  <label for="node-input-samplingInterval"><i class="fa fa-clock-o"></i> Sampling</label>
1147
64
  <input type="text" id="node-input-samplingInterval" placeholder="250">
@@ -1150,6 +67,7 @@
1150
67
  <label for="node-input-publishingInterval"><i class="fa fa-clock-o"></i> Publishing</label>
1151
68
  <input type="text" id="node-input-publishingInterval" placeholder="250">
1152
69
  </div>
70
+
1153
71
  <div class="form-row opcua-client-selection-row" style="margin-bottom:0;">
1154
72
  <label style="width:auto;"><i class="fa fa-tags"></i> Selected items</label>
1155
73
  </div>
@@ -1158,8 +76,6 @@
1158
76
  <div id="node-input-selected-tags" class="opcua-client-tag-box"></div>
1159
77
  </div>
1160
78
 
1161
-
1162
-
1163
79
  </script>
1164
80
 
1165
81
  <script type="text/html" data-help-name="opcua-client">
@@ -1198,7 +114,7 @@
1198
114
  <p>If <code>msg.payload = []</code>, the node browses the OPC UA <code>RootFolder</code>.</p>
1199
115
 
1200
116
  <h3>Method</h3>
1201
- <p><code>methodId</code> and <code>objectId</code> are accepted explicitly. For backward compatibility, <code>nodeID</code> is also accepted as the method id.</p>
117
+ <p><code>nodeID</code> and <code>objectId</code> are accepted explicitly. For backward compatibility, <code>nodeID</code> is also accepted as the method id.</p>
1202
118
  <p><b>Input</b> <code>msg.payload</code>:</p>
1203
119
  <pre><code>[
1204
120
  {
@@ -1222,4 +138,4 @@
1222
138
  ]</code></pre>
1223
139
  <h3>Subscription</h3>
1224
140
  <p>In subscription mode the node emits one message per value change.</p>
1225
- </script>
141
+ </script>