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

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