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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @vitormnm/node-red-simple-opcua
2
2
  OPC UA client and server with a simple graphical interface for Node-RED.
3
- fully parameterized via JSON.
3
+ Fully parameterized in JSON.
4
4
 
5
5
  It supports the following OPC UA items on only 3 nodes.
6
6
 
@@ -9,6 +9,8 @@ It supports the following OPC UA items on only 3 nodes.
9
9
  - events read and write tags in server(See which tags are being written to or read from the client directly on the server in a simple workflow)
10
10
  - methods(write methods in node-red flow)
11
11
  - variables
12
+ - variables arrays
13
+ - description and displayname nodes
12
14
  - objects
13
15
  - simple objectsType
14
16
  - custom namespace
@@ -21,8 +23,9 @@ It supports the following OPC UA items on only 3 nodes.
21
23
  **Client editor**
22
24
  ![node-red-si](/resources/editorClient.PNG)
23
25
 
24
- example json server config
25
26
 
27
+
28
+ example json server config
26
29
  ```
27
30
  {
28
31
  "objects": [],
@@ -124,5 +127,6 @@ example json server config
124
127
  ]
125
128
  }
126
129
  ```
127
-
130
+ Disclaimer
131
+ This node was only used in simulation and testing environments.
128
132
 
@@ -24,17 +24,56 @@ async function browseNode(session, root) {
24
24
  resultMask: 63
25
25
  });
26
26
 
27
- const references = browseResult && Array.isArray(browseResult.references)
28
- ? browseResult.references
29
- : [];
27
+ const references = browseResult?.references ?? [];
28
+ if (!references.length) return result;
29
+
30
+ // Monta lista de todos os atributos de todos os nós de uma vez
31
+ const nodeIds = references.map(ref => normalizeNodeId(ref.nodeId));
32
+ const attributesToRead = nodeIds.flatMap(nodeId => [
33
+ { nodeId, attributeId: AttributeIds.Description },
34
+ { nodeId, attributeId: AttributeIds.DataType },
35
+ { nodeId, attributeId: AttributeIds.Value },
36
+ ]);
37
+
38
+ // UMA única chamada para todos os nós e atributos
39
+ const dataValues = await session.read(attributesToRead);
40
+
41
+ // Distribui os resultados por nó (3 atributos por nó)
42
+ result.browse = await Promise.all(references.map(async (reference, i) => {
43
+ const childNodeId = nodeIds[i];
44
+ const nodeClass = resolveNodeClassName(reference.nodeClass);
45
+ const browseName = extractBrowseName(reference.browseName, childNodeId);
46
+ const displayName = extractDisplayName(reference.displayName, browseName);
47
+
48
+ const descValue = dataValues[i * 3]?.value?.value;
49
+ const description = typeof descValue === "string"
50
+ ? descValue
51
+ : (descValue?.text ?? "");
52
+
53
+ const item = { nodeID: childNodeId, nodeClass, browseName, displayName, description };
54
+
55
+ if (nodeClass === "Variable") {
56
+ const dataTypeValue = dataValues[i * 3 + 1]?.value?.value;
57
+ const rawValue = dataValues[i * 3 + 2]?.value?.value;
58
+
59
+ item.dataType = dataTypeValue?.namespace === 0 && typeof dataTypeValue?.value === "number"
60
+ ? (DataType[dataTypeValue.value] || dataTypeValue.toString())
61
+ : (dataTypeValue?.toString() ?? "");
62
+
63
+ item.value = rawValue ?? "";
64
+ }
30
65
 
31
- for (const reference of references) {
32
- result.browse.push(await mapReference(session, reference));
33
- }
66
+ if (nodeClass === "Method") {
67
+ const definition = await readMethodArguments(session, childNodeId);
68
+ item.inputArguments = definition.inputArguments;
69
+ item.outputArguments = definition.outputArguments;
70
+ }
71
+
72
+ return item;
73
+ }));
34
74
 
35
75
  return result;
36
76
  }
37
-
38
77
  function normalizeBrowseRoots(payload) {
39
78
  if (payload === undefined || payload === null) {
40
79
  return [{ name: "RootFolder", nodeID: ROOT_NODE_ID }];
@@ -1,53 +1,147 @@
1
1
  "use strict";
2
2
 
3
- const { DataType, coerceNodeId } = require("node-opcua");
3
+ const { AttributeIds, DataType, coerceNodeId } = require("node-opcua");
4
4
  const {
5
5
  buildVariantFromItem,
6
- dataValueToItemResult,
7
6
  normalizeTypeName,
8
7
  resolveNodeId,
9
8
  statusCodeToString
10
9
  } = require("../opcua-client-utils");
11
10
 
11
+ // Máximo de tags por chamada session.write (ajuste conforme limite do servidor)
12
+ const WRITE_BATCH_SIZE = 100;
13
+
14
+ // Batches em paralelo simultâneos
15
+ const CONCURRENCY = 5;
16
+
17
+ // Cede o event loop a cada N itens para não travar o Node-RED
18
+ const YIELD_EVERY = 50;
19
+
12
20
  class OpcUaClientWriteService {
13
21
  async execute(node, msg, session, itemsResolver) {
14
22
  const items = itemsResolver.ensureWriteItems(node, msg);
15
- const results = [];
16
-
17
- for (const item of items) {
18
- const nodeId = resolveNodeId(item);
19
-
20
- try {
21
- const explicitType = normalizeTypeName(item.type);
22
- const builtInType = await session.getBuiltInDataType(coerceNodeId(nodeId));
23
- const typeName = explicitType || DataType[builtInType];
24
- const variant = buildVariantFromItem(item, typeName);
25
- const statusCode = await session.writeSingleNode(nodeId, variant);
26
- const dataValue = await session.readVariableValue(nodeId);
27
- const result = dataValueToItemResult(item, dataValue);
28
-
29
- if (statusCode && statusCode.name && statusCode.name !== "Good") {
30
- result.status = statusCodeToString(statusCode);
31
- }
32
-
33
- results.push(result);
34
- } catch (itemError) {
35
- results.push({
36
- name: item.name || nodeId,
37
- nodeID: nodeId,
38
- value: item.value,
39
- type: normalizeTypeName(item.type) || null,
40
- status: itemError.message,
41
- sourceTimestamp: null,
42
- serverTimestamp: null
43
- });
23
+
24
+ // 1. Resolve variantes (tipo + valor) — consulta servidor só para quem não tem tipo explícito
25
+ const variants = await resolveVariants(session, items);
26
+
27
+ // 2. Escreve todos os nós em batches paralelos
28
+ const statusCodes = await writeBatches(session, items, variants);
29
+
30
+ // 3. Monta resultados — statusCode Good já confirma a escrita, sem round-trip extra
31
+ return buildResults(items, variants, statusCodes);
32
+ }
33
+ }
34
+
35
+ // ─── Resolução de tipos ──────────────────────────────────────────────────────
36
+
37
+ async function resolveVariants(session, items) {
38
+ // Separa quais itens precisam consultar o tipo no servidor
39
+ const needsLookup = items
40
+ .map((item, index) => ({ item, index }))
41
+ .filter(({ item }) => !normalizeTypeName(item.type));
42
+
43
+ // Busca tipos desconhecidos em paralelo
44
+ const resolvedTypes = new Map();
45
+
46
+ await mapConcurrent(needsLookup, CONCURRENCY * 2, async ({ item, index }) => {
47
+ try {
48
+ const builtInType = await session.getBuiltInDataType(coerceNodeId(resolveNodeId(item)));
49
+ resolvedTypes.set(index, DataType[builtInType]);
50
+ } catch {
51
+ resolvedTypes.set(index, "String");
52
+ }
53
+ });
54
+
55
+ return items.map((item, index) => {
56
+ const typeName = normalizeTypeName(item.type) || resolvedTypes.get(index) || "String";
57
+ return buildVariantFromItem(item, typeName);
58
+ });
59
+ }
60
+
61
+ // ─── Escrita em batches paralelos ────────────────────────────────────────────
62
+
63
+ async function writeBatches(session, items, variants) {
64
+ const allStatusCodes = new Array(items.length);
65
+
66
+ // Divide em batches de WRITE_BATCH_SIZE
67
+ const batches = [];
68
+ for (let i = 0; i < items.length; i += WRITE_BATCH_SIZE) {
69
+ batches.push({ start: i, end: Math.min(i + WRITE_BATCH_SIZE, items.length) });
70
+ }
71
+
72
+ let processed = 0;
73
+
74
+ await mapConcurrent(batches, CONCURRENCY, async ({ start, end }) => {
75
+ const nodesToWrite = items.slice(start, end).map((item, i) => ({
76
+ nodeId: coerceNodeId(resolveNodeId(item)),
77
+ attributeId: AttributeIds.Value,
78
+ value: { value: variants[start + i] }
79
+ }));
80
+
81
+ try {
82
+ const statusCodes = await session.write(nodesToWrite);
83
+ statusCodes.forEach((sc, i) => {
84
+ allStatusCodes[start + i] = sc;
85
+ });
86
+ } catch (batchError) {
87
+ // Se o batch falhar por completo, marca todos com erro
88
+ for (let i = start; i < end; i++) {
89
+ allStatusCodes[i] = { name: batchError.message, value: -1 };
44
90
  }
45
91
  }
46
92
 
47
- return results;
93
+ // Cede o event loop a cada YIELD_EVERY itens para não travar o Node-RED
94
+ processed += end - start;
95
+ if (processed % YIELD_EVERY === 0) {
96
+ await yieldEventLoop();
97
+ }
98
+ });
99
+
100
+ return allStatusCodes;
101
+ }
102
+
103
+ // ─── Montagem dos resultados ─────────────────────────────────────────────────
104
+
105
+ function buildResults(items, variants, statusCodes) {
106
+ return items.map((item, index) => {
107
+ const nodeId = resolveNodeId(item);
108
+ const sc = statusCodes[index];
109
+ const scName = sc && sc.name ? sc.name : "Good";
110
+ const typeName = DataType[variants[index].dataType] || null;
111
+
112
+ return {
113
+ name: item.name || nodeId,
114
+ nodeID: nodeId,
115
+ value: variants[index].value,
116
+ type: typeName,
117
+ status: scName,
118
+ sourceTimestamp: null,
119
+ serverTimestamp: null
120
+ };
121
+ });
122
+ }
123
+
124
+ // ─── Utilitários ─────────────────────────────────────────────────────────────
125
+
126
+ async function mapConcurrent(items, concurrency, fn) {
127
+ let index = 0;
128
+
129
+ async function worker() {
130
+ while (index < items.length) {
131
+ const i = index++;
132
+ await fn(items[i], i);
133
+ }
48
134
  }
135
+
136
+ await Promise.all(
137
+ Array.from({ length: Math.min(concurrency, items.length) }, worker)
138
+ );
139
+ }
140
+
141
+ function yieldEventLoop() {
142
+ return new Promise(resolve => setImmediate(resolve));
49
143
  }
50
144
 
51
145
  module.exports = {
52
146
  OpcUaClientWriteService
53
- };
147
+ };
@@ -245,12 +245,30 @@
245
245
  <script type="text/javascript">
246
246
  (function () {
247
247
  var selectedItemsState = [];
248
+ var selectedNodeIdSet = {}; // fast O(1) lookup: nodeId → index
248
249
  var browseState = null;
249
250
  var expansionState = {};
250
251
  var browseSearchValue = "";
251
252
  var browseSearchTerm = "";
252
253
  var contextMenuPath = "";
253
254
  var browseSelectedPath = "";
255
+ var renderPending = false; // debounce flag for renderBrowseTree
256
+
257
+ function debounce(fn, delay) {
258
+ var timer;
259
+ return function () {
260
+ var ctx = this, args = arguments;
261
+ clearTimeout(timer);
262
+ timer = setTimeout(function () { fn.apply(ctx, args); }, delay);
263
+ };
264
+ }
265
+
266
+ function rebuildNodeIdIndex() {
267
+ selectedNodeIdSet = {};
268
+ selectedItemsState.forEach(function (item, i) {
269
+ if (item.nodeID) selectedNodeIdSet[item.nodeID] = i;
270
+ });
271
+ }
254
272
 
255
273
  function openBrowseModal() { $("#node-input-browse-modal").show(); $("body").addClass("opcua-tree-modal-open"); }
256
274
  function closeBrowseModal() {
@@ -456,6 +474,7 @@
456
474
  }
457
475
 
458
476
  function syncSelectedItems() {
477
+ rebuildNodeIdIndex();
459
478
  updateSelectedItemsField();
460
479
  renderSelectedTags();
461
480
  renderBrowseTree();
@@ -470,9 +489,8 @@
470
489
  }
471
490
 
472
491
  function selectedIndexByNodeId(nodeId) {
473
- return selectedItemsState.findIndex(function (item) {
474
- return item.nodeID === nodeId;
475
- });
492
+ var idx = selectedNodeIdSet[nodeId];
493
+ return (idx !== undefined) ? idx : -1;
476
494
  }
477
495
 
478
496
  function canExpand(item) {
@@ -501,24 +519,47 @@
501
519
  }
502
520
 
503
521
  function renderBrowseTree() {
522
+ if (renderPending) return;
523
+ renderPending = true;
524
+ setTimeout(function () {
525
+ renderPending = false;
526
+ _doRenderBrowseTree();
527
+ }, 0);
528
+ }
529
+
530
+ function _doRenderBrowseTree() {
504
531
  var container = $("#node-input-browse-tree");
505
- container.empty();
532
+ var frag = document.createDocumentFragment();
506
533
 
507
534
  if (!browseState) {
508
- container.append('<div class="opcua-tree-empty">Click Browse to load the server tree.</div>');
535
+ var empty = document.createElement("div");
536
+ empty.className = "opcua-tree-empty";
537
+ empty.textContent = "Click Browse to load the server tree.";
538
+ frag.appendChild(empty);
539
+ container[0].innerHTML = "";
540
+ container[0].appendChild(frag);
509
541
  return;
510
542
  }
511
543
 
512
544
  if (browseSearchTerm) {
513
545
  if (!branchHasSearchMatch(browseState, browseSearchTerm)) {
514
- container.append('<div class="opcua-tree-empty">No items found in the already explored items.</div>');
546
+ var noMatch = document.createElement("div");
547
+ noMatch.className = "opcua-tree-empty";
548
+ noMatch.textContent = "No items found in the already explored items.";
549
+ frag.appendChild(noMatch);
550
+ container[0].innerHTML = "";
551
+ container[0].appendChild(frag);
515
552
  return;
516
553
  }
517
- renderBrowseRootFiltered(browseState, "root", 0, container, browseSearchTerm);
554
+ renderBrowseRootFiltered(browseState, "root", 0, frag, browseSearchTerm);
555
+ container[0].innerHTML = "";
556
+ container[0].appendChild(frag);
518
557
  return;
519
558
  }
520
559
 
521
- renderBrowseRoot(browseState, "root", 0, container);
560
+ renderBrowseRoot(browseState, "root", 0, frag);
561
+ container[0].innerHTML = "";
562
+ container[0].appendChild(frag);
522
563
  }
523
564
 
524
565
  function isFolderNode(item) {
@@ -549,92 +590,111 @@
549
590
  return "fa-tag";
550
591
  }
551
592
 
552
- function renderBrowseRoot(root, path, depth, container) {
593
+ function makeEl(tag, className, html) {
594
+ var el = document.createElement(tag);
595
+ if (className) el.className = className;
596
+ if (html !== undefined) el.innerHTML = html;
597
+ return el;
598
+ }
599
+
600
+ function makeTreeRow(path, extraClass) {
601
+ var row = document.createElement("div");
602
+ row.className = "opcua-tree-row" + (extraClass ? " " + extraClass : "");
603
+ row.setAttribute("data-path", path);
604
+ return row;
605
+ }
606
+
607
+ function renderBrowseRoot(root, path, depth, frag) {
553
608
  var expanded = isExpanded(path, true);
554
- var row = $('<div class="opcua-tree-row"></div>').attr("data-path", path);
555
- row.append('<span class="opcua-tree-indent"></span>');
556
- row.append('<span class="opcua-tree-twisty opcua-client-toggle-tree" data-path="' + path + '">' + (expanded ? '<i class="fa fa-caret-down"></i>' : '<i class="fa fa-caret-right"></i>') + '</span>');
557
- row.append('<span class="opcua-tree-icon"><i class="fa fa-sitemap"></i></span>');
558
- row.append('<span class="opcua-tree-label">' + escapeHtml(root.name || root.nodeID || "RootFolder") + '</span>');
559
- row.append('<span class="opcua-tree-type">' + escapeHtml(root.nodeID || "") + '</span>');
560
- container.append(row);
609
+ var row = makeTreeRow(path);
610
+ row.innerHTML = '<span class="opcua-tree-indent"></span>'
611
+ + '<span class="opcua-tree-twisty opcua-client-toggle-tree" data-path="' + escapeHtml(path) + '">'
612
+ + (expanded ? '<i class="fa fa-caret-down"></i>' : '<i class="fa fa-caret-right"></i>') + '</span>'
613
+ + '<span class="opcua-tree-icon"><i class="fa fa-sitemap"></i></span>'
614
+ + '<span class="opcua-tree-label">' + escapeHtml(root.name || root.nodeID || "RootFolder") + '</span>'
615
+ + '<span class="opcua-tree-type">' + escapeHtml(root.nodeID || "") + '</span>';
616
+ frag.appendChild(row);
561
617
 
562
618
  if (!expanded) return;
563
619
 
564
620
  if (!Array.isArray(root.browse) || !root.browse.length) {
565
- container.append('<div class="opcua-tree-empty">No items found..</div>');
621
+ frag.appendChild(makeEl("div", "opcua-tree-empty", "No items found.."));
566
622
  } else {
567
623
  root.browse.forEach(function (item, index) {
568
- renderBrowseItem(item, path + ".browse." + index, depth + 1, container);
624
+ renderBrowseItem(item, path + ".browse." + index, depth + 1, frag);
569
625
  });
570
626
  }
571
627
  }
572
628
 
573
- function renderBrowseItem(item, path, depth, container) {
629
+ function renderBrowseItem(item, path, depth, frag) {
574
630
  var expanded = isExpanded(path, false);
575
631
  var nodeId = nodeIdOf(item);
576
632
  var selectedIndex = selectedIndexByNodeId(nodeId);
577
633
  var hasChildren = canExpand(item);
578
- var row = $('<div class="opcua-tree-row"></div>').attr("data-path", path);
579
- if (selectedIndex >= 0) row.addClass("is-selected");
634
+ var row = makeTreeRow(path, selectedIndex >= 0 ? "is-selected" : "");
580
635
 
581
- for (var i = 0; i < depth; i += 1) {
582
- row.append('<span class="opcua-tree-indent"></span>');
583
- }
584
- row.append('<span class="opcua-tree-twisty' + (hasChildren ? ' opcua-client-toggle-tree' : '') + '" data-path="' + path + '">' + (hasChildren ? '<i class="fa ' + (expanded ? 'fa-caret-down' : 'fa-caret-right') + '"></i>' : '') + '</span>');
585
- row.append('<span class="opcua-tree-icon"><i class="fa ' + browseIconFor(item) + '"></i></span>');
586
- row.append('<span class="opcua-tree-label">' + escapeHtml(item.displayName || item.browseName || item.nodeID) + '</span>');
587
- row.append('<span class="opcua-tree-type">' + escapeHtml(item.nodeClass || "") + (item.dataType ? " | " + escapeHtml(item.dataType) : "") + '</span>');
588
- row.append('<span class="opcua-client-nodeid-label">' + escapeHtml(nodeId) + '</span>');
589
-
590
- if (nodeId) {
591
- var actionLabel = selectedIndex >= 0 ? "Remove" : "Add";
592
- var actionIcon = selectedIndex >= 0 ? "fa-minus" : "fa-plus";
593
- row.append('<div class="opcua-tree-actions"><a href="#" class="editor-button editor-button-small opcua-client-toggle-tag" data-nodeid="' + escapeHtml(nodeId) + '" data-path="' + path + '"><i class="fa ' + actionIcon + '"></i> ' + actionLabel + '</a></div>');
594
- }
595
- container.append(row);
636
+ var indents = "";
637
+ for (var i = 0; i < depth; i += 1) indents += '<span class="opcua-tree-indent"></span>';
638
+
639
+ var twisty = '<span class="opcua-tree-twisty' + (hasChildren ? ' opcua-client-toggle-tree' : '') + '" data-path="' + escapeHtml(path) + '">'
640
+ + (hasChildren ? '<i class="fa ' + (expanded ? 'fa-caret-down' : 'fa-caret-right') + '"></i>' : '') + '</span>';
641
+
642
+ var actions = nodeId
643
+ ? '<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>'
644
+ : '';
645
+
646
+ row.innerHTML = indents + twisty
647
+ + '<span class="opcua-tree-icon"><i class="fa ' + browseIconFor(item) + '"></i></span>'
648
+ + '<span class="opcua-tree-label">' + escapeHtml(item.displayName || item.browseName || item.nodeID) + '</span>'
649
+ + '<span class="opcua-tree-type">' + escapeHtml(item.nodeClass || "") + (item.dataType ? " | " + escapeHtml(item.dataType) : "") + '</span>'
650
+ + '<span class="opcua-client-nodeid-label">' + escapeHtml(nodeId) + '</span>'
651
+ + actions;
652
+ frag.appendChild(row);
596
653
 
597
654
  if (item.description) {
598
- container.append('<div class="opcua-client-description" style="padding: 0 10px 8px ' + String((depth + 2) * 14) + 'px;">' + escapeHtml(item.description) + '</div>');
655
+ var desc = makeEl("div", "opcua-client-description");
656
+ desc.style.padding = "0 10px 8px " + String((depth + 2) * 14) + "px";
657
+ desc.textContent = item.description;
658
+ frag.appendChild(desc);
599
659
  }
600
660
 
601
661
  if (expanded && hasChildren) {
602
662
  if (Array.isArray(item.browse)) {
603
663
  if (!item.browse.length) {
604
- container.append('<div class="opcua-tree-empty">No children found..</div>');
664
+ frag.appendChild(makeEl("div", "opcua-tree-empty", "No children found.."));
605
665
  } else {
606
666
  item.browse.forEach(function (child, index) {
607
- renderBrowseItem(child, path + ".browse." + index, depth + 1, container);
667
+ renderBrowseItem(child, path + ".browse." + index, depth + 1, frag);
608
668
  });
609
669
  }
610
670
  } else {
611
- container.append('<div class="opcua-tree-empty">Searching for items...</div>');
671
+ frag.appendChild(makeEl("div", "opcua-tree-empty", "Searching for items..."));
612
672
  }
613
673
  }
614
674
  }
615
675
 
616
- function renderBrowseRootFiltered(root, path, depth, container, term) {
617
- var row = $('<div class="opcua-tree-row"></div>').attr("data-path", path);
618
- row.append('<span class="opcua-tree-indent"></span>');
619
- row.append('<span class="opcua-tree-twisty"><i class="fa fa-caret-down"></i></span>');
620
- row.append('<span class="opcua-tree-icon"><i class="fa fa-sitemap"></i></span>');
621
- row.append('<span class="opcua-tree-label">' + escapeHtml(root.name || root.nodeID || "RootFolder") + '</span>');
622
- row.append('<span class="opcua-tree-type">' + escapeHtml(root.nodeID || "") + '</span>');
623
- container.append(row);
676
+ function renderBrowseRootFiltered(root, path, depth, frag, term) {
677
+ var row = makeTreeRow(path);
678
+ row.innerHTML = '<span class="opcua-tree-indent"></span>'
679
+ + '<span class="opcua-tree-twisty"><i class="fa fa-caret-down"></i></span>'
680
+ + '<span class="opcua-tree-icon"><i class="fa fa-sitemap"></i></span>'
681
+ + '<span class="opcua-tree-label">' + escapeHtml(root.name || root.nodeID || "RootFolder") + '</span>'
682
+ + '<span class="opcua-tree-type">' + escapeHtml(root.nodeID || "") + '</span>';
683
+ frag.appendChild(row);
624
684
 
625
685
  if (!Array.isArray(root.browse) || !root.browse.length) {
626
- container.append('<div class="opcua-tree-empty">Nenhum item encontrado.</div>');
686
+ frag.appendChild(makeEl("div", "opcua-tree-empty", "Nenhum item encontrado."));
627
687
  return;
628
688
  }
629
689
 
630
690
  root.browse.forEach(function (item, index) {
631
691
  if (branchHasSearchMatch(item, term)) {
632
- renderBrowseItemFiltered(item, path + ".browse." + index, depth + 1, container, term, false);
692
+ renderBrowseItemFiltered(item, path + ".browse." + index, depth + 1, frag, term, false);
633
693
  }
634
694
  });
635
695
  }
636
696
 
637
- function renderBrowseItemFiltered(item, path, depth, container, term, ancestorMatched) {
697
+ function renderBrowseItemFiltered(item, path, depth, frag, term, ancestorMatched) {
638
698
  if (!branchHasSearchMatch(item, term)) return;
639
699
 
640
700
  var nodeId = nodeIdOf(item);
@@ -648,40 +708,47 @@
648
708
  var expanded = hasChildren && (hasExplicitExpansion
649
709
  ? !!expansionState[path]
650
710
  : ((subtreeVisible && Array.isArray(item.browse)) || hasMatchingLoadedChild));
651
- var row = $('<div class="opcua-tree-row"></div>').attr("data-path", path);
652
- if (selectedIndex >= 0) row.addClass("is-selected");
653
-
654
- for (var i = 0; i < depth; i += 1) row.append('<span class="opcua-tree-indent"></span>');
655
- row.append('<span class="opcua-tree-twisty' + (hasChildren ? ' opcua-client-toggle-tree' : '') + '" data-path="' + path + '">' + (hasChildren ? '<i class="fa ' + (expanded ? 'fa-caret-down' : 'fa-caret-right') + '"></i>' : '') + '</span>');
656
- row.append('<span class="opcua-tree-icon"><i class="fa ' + browseIconFor(item) + '"></i></span>');
657
- row.append('<span class="opcua-tree-label">' + escapeHtml(item.displayName || item.browseName || item.nodeID) + '</span>');
658
- row.append('<span class="opcua-tree-type">' + escapeHtml(item.nodeClass || "") + (item.dataType ? " | " + escapeHtml(item.dataType) : "") + '</span>');
659
- row.append('<span class="opcua-client-nodeid-label">' + escapeHtml(nodeId) + '</span>');
660
-
661
- if (nodeId) {
662
- var actionLabel = selectedIndex >= 0 ? "Remove" : "Add";
663
- var actionIcon = selectedIndex >= 0 ? "fa-minus" : "fa-plus";
664
- row.append('<div class="opcua-tree-actions"><a href="#" class="editor-button editor-button-small opcua-client-toggle-tag" data-nodeid="' + escapeHtml(nodeId) + '" data-path="' + path + '"><i class="fa ' + actionIcon + '"></i> ' + actionLabel + '</a></div>');
665
- }
666
- container.append(row);
711
+
712
+ var row = makeTreeRow(path, selectedIndex >= 0 ? "is-selected" : "");
713
+
714
+ var indents = "";
715
+ for (var i = 0; i < depth; i += 1) indents += '<span class="opcua-tree-indent"></span>';
716
+
717
+ var twisty = '<span class="opcua-tree-twisty' + (hasChildren ? ' opcua-client-toggle-tree' : '') + '" data-path="' + escapeHtml(path) + '">'
718
+ + (hasChildren ? '<i class="fa ' + (expanded ? 'fa-caret-down' : 'fa-caret-right') + '"></i>' : '') + '</span>';
719
+
720
+ var actions = nodeId
721
+ ? '<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>'
722
+ : '';
723
+
724
+ row.innerHTML = indents + twisty
725
+ + '<span class="opcua-tree-icon"><i class="fa ' + browseIconFor(item) + '"></i></span>'
726
+ + '<span class="opcua-tree-label">' + escapeHtml(item.displayName || item.browseName || item.nodeID) + '</span>'
727
+ + '<span class="opcua-tree-type">' + escapeHtml(item.nodeClass || "") + (item.dataType ? " | " + escapeHtml(item.dataType) : "") + '</span>'
728
+ + '<span class="opcua-client-nodeid-label">' + escapeHtml(nodeId) + '</span>'
729
+ + actions;
730
+ frag.appendChild(row);
667
731
 
668
732
  if (item.description) {
669
- container.append('<div class="opcua-client-description" style="padding: 0 10px 8px ' + String((depth + 2) * 14) + 'px;">' + escapeHtml(item.description) + '</div>');
733
+ var desc = makeEl("div", "opcua-client-description");
734
+ desc.style.padding = "0 10px 8px " + String((depth + 2) * 14) + "px";
735
+ desc.textContent = item.description;
736
+ frag.appendChild(desc);
670
737
  }
671
738
 
672
739
  if (expanded && hasChildren) {
673
740
  if (Array.isArray(item.browse)) {
674
741
  if (!item.browse.length) {
675
- container.append('<div class="opcua-tree-empty">Nenhum filho encontrado.</div>');
742
+ frag.appendChild(makeEl("div", "opcua-tree-empty", "Nenhum filho encontrado."));
676
743
  } else {
677
744
  item.browse.forEach(function (child, index) {
678
745
  if (subtreeVisible || branchHasSearchMatch(child, term)) {
679
- renderBrowseItemFiltered(child, path + ".browse." + index, depth + 1, container, term, subtreeVisible);
746
+ renderBrowseItemFiltered(child, path + ".browse." + index, depth + 1, frag, term, subtreeVisible);
680
747
  }
681
748
  });
682
749
  }
683
750
  } else {
684
- container.append('<div class="opcua-tree-empty">Expandindo...</div>');
751
+ frag.appendChild(makeEl("div", "opcua-tree-empty", "Expandindo..."));
685
752
  }
686
753
  }
687
754
  }
@@ -938,6 +1005,7 @@
938
1005
  });
939
1006
 
940
1007
  selectedItemsState = parseSelectedItems(this.selectedItems);
1008
+ rebuildNodeIdIndex();
941
1009
 
942
1010
  browseState = null;
943
1011
  expansionState = {};
@@ -967,12 +1035,12 @@
967
1035
  hideTreeContextMenu();
968
1036
  if (event.target === this) closeBrowseModal();
969
1037
  });
970
- $("#node-input-browse-search").off("input").on("input", function () {
1038
+ $("#node-input-browse-search").off("input").on("input", debounce(function () {
971
1039
  browseSearchValue = $(this).val();
972
1040
  browseSearchTerm = normalizeSearchTerm(browseSearchValue);
973
1041
  $("#node-input-browse-search-clear").toggle(!!browseSearchTerm);
974
1042
  renderBrowseTree();
975
- });
1043
+ }, 200));
976
1044
  $("#node-input-browse-search-clear").off("click").on("click", function (event) {
977
1045
  event.preventDefault();
978
1046
  browseSearchValue = "";
@@ -1222,4 +1290,4 @@
1222
1290
  ]</code></pre>
1223
1291
  <h3>Subscription</h3>
1224
1292
  <p>In subscription mode the node emits one message per value change.</p>
1225
- </script>
1293
+ </script>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vitormnm/node-red-simple-opcua",
3
- "version": "1.3.2",
3
+ "version": "1.4.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -730,6 +730,30 @@
730
730
  function getNamespaceOptions() {
731
731
  return Array.isArray(editorState.nameSpaces) ? editorState.nameSpaces.slice().sort(function (left, right) { return left.id - right.id; }) : [];
732
732
  }
733
+
734
+ function getDefinedObjectTypeNames() {
735
+ var names = [];
736
+ (editorState.objectsTypes || []).forEach(function (ot) {
737
+ if (ot && ot.name) names.push(String(ot.name));
738
+ });
739
+ return names;
740
+ }
741
+
742
+
743
+ function buildObjectTypeSelect(id, currentValue) {
744
+ var names = getDefinedObjectTypeNames();
745
+ var cv = String(currentValue || "");
746
+ var opts = "";
747
+ var noneSelected = (cv === "") ? " selected" : "";
748
+ opts += "<option value=\"\"" + noneSelected + ">\u2014 none \u2014</option>";
749
+ names.forEach(function (n) {
750
+ var esc = n.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
751
+ var sel = (n === cv) ? " selected" : "";
752
+ opts += "<option value=\"" + esc + "\"" + sel + ">" + esc + "</option>";
753
+ });
754
+ return "<select id=\"" + id + "\">" + opts + "</select>";
755
+ }
756
+
733
757
  function getNamespaceLabel(namespaceId) {
734
758
  var match = getNamespaceOptions().find(function (item) { return item.id === normalizeNamespaceId(namespaceId); });
735
759
  return match ? String(match.id) + " - " + match.name : String(normalizeNamespaceId(namespaceId));
@@ -900,43 +924,72 @@
900
924
  return "fa-tag";
901
925
  }
902
926
 
903
- function createNodeElement(path, depth, ancestorMatched) {
927
+ // ── performance helpers ───────────────────────────────────────────
928
+ var _renderTreePending = false;
929
+
930
+ function debounce(fn, delay) {
931
+ var timer;
932
+ return function () { var ctx = this, args = arguments; clearTimeout(timer); timer = setTimeout(function () { fn.apply(ctx, args); }, delay); };
933
+ }
934
+
935
+ function escapeHtml(v) {
936
+ return String(v || "").replace(/[&<>"']/g, function (c) {
937
+ return { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c];
938
+ });
939
+ }
940
+
941
+ function appendNodeToFrag(frag, path, depth, ancestorMatched) {
904
942
  var item = getAtPath(editorState, path);
905
- if (!item) return $("<div></div>");
943
+ if (!item) return;
906
944
  var nodeClass = nodeClassFromPath(path);
907
945
  var hasChildren = nodeClass !== "Variable" && nodeClass !== "Alarm" && nodeClass !== "Namespace" && getChildrenByPath(path).length > 0;
908
- var expanded = isExpanded(path, depth < 1 || treeSearchTerm);
946
+ var expanded = isExpanded(path, depth < 1 || !!treeSearchTerm);
909
947
  var subtreeVisible = !!ancestorMatched || nodeMatchesSearch(path);
910
- var row = $('<div class="opcua-tree-row"></div>').attr("data-path", path);
911
- if (path === selectedPath) row.addClass("is-selected");
912
- for (var i = 0; i < depth; i += 1) row.append('<span class="opcua-tree-indent"></span>');
913
- row.append('<span class="opcua-tree-twisty">' + (hasChildren ? '<i class="fa ' + (expanded ? 'fa-caret-down' : 'fa-caret-right') + '"></i>' : '') + '</span>');
914
- row.append('<span class="opcua-tree-icon"><i class="fa ' + iconForNodeClass(nodeClass) + '"></i></span>');
915
- row.append('<span class="opcua-tree-label"></span>');
916
- row.find(".opcua-tree-label").text(item.name || "(unnamed)");
917
- row.append('<span class="opcua-tree-type">' + nodeClass + '</span>');
918
- var fragment = $('<div></div>').append(row);
948
+
949
+ var indents = "";
950
+ for (var i = 0; i < depth; i++) indents += '<span class="opcua-tree-indent"></span>';
951
+
952
+ var row = document.createElement("div");
953
+ row.className = "opcua-tree-row" + (path === selectedPath ? " is-selected" : "");
954
+ row.setAttribute("data-path", path);
955
+ row.innerHTML = indents
956
+ + '<span class="opcua-tree-twisty">' + (hasChildren ? '<i class="fa ' + (expanded ? "fa-caret-down" : "fa-caret-right") + '"></i>' : "") + "</span>"
957
+ + '<span class="opcua-tree-icon"><i class="fa ' + iconForNodeClass(nodeClass) + '"></i></span>'
958
+ + '<span class="opcua-tree-label">' + escapeHtml(item.name || "(unnamed)") + "</span>"
959
+ + '<span class="opcua-tree-type">' + escapeHtml(nodeClass) + "</span>";
960
+ frag.appendChild(row);
961
+
919
962
  if (hasChildren && expanded) {
920
963
  getChildrenByPath(path).forEach(function (childPath) {
921
964
  if (!treeSearchTerm || subtreeVisible || branchHasSearchMatch(childPath)) {
922
- fragment.append(createNodeElement(childPath, depth + 1, subtreeVisible));
965
+ appendNodeToFrag(frag, childPath, depth + 1, subtreeVisible);
923
966
  }
924
967
  });
925
968
  }
926
- return fragment;
927
969
  }
928
970
 
929
971
  function renderTree() {
930
- var list = $("#node-input-object-list");
931
- list.empty();
932
- var roots = getTopLevelPaths();
933
- if (!roots.length) {
934
- list.append('<div class="opcua-tree-empty">No OPC UA items. Use Add folder, Add object, or Add namespace.</div>');
935
- return;
936
- }
937
- roots.forEach(function (path) {
938
- if (!treeSearchTerm || branchHasSearchMatch(path)) list.append(createNodeElement(path, 0, false));
939
- });
972
+ if (_renderTreePending) return;
973
+ _renderTreePending = true;
974
+ setTimeout(function () {
975
+ _renderTreePending = false;
976
+ var list = document.getElementById("node-input-object-list");
977
+ if (!list) return;
978
+ var frag = document.createDocumentFragment();
979
+ var roots = getTopLevelPaths();
980
+ if (!roots.length) {
981
+ var empty = document.createElement("div");
982
+ empty.className = "opcua-tree-empty";
983
+ empty.textContent = "No OPC UA items. Use Add folder, Add object, or Add namespace.";
984
+ frag.appendChild(empty);
985
+ } else {
986
+ roots.forEach(function (path) {
987
+ if (!treeSearchTerm || branchHasSearchMatch(path)) appendNodeToFrag(frag, path, 0, false);
988
+ });
989
+ }
990
+ list.innerHTML = "";
991
+ list.appendChild(frag);
992
+ }, 0);
940
993
  }
941
994
 
942
995
  function renderBreadcrumbs() {
@@ -972,8 +1025,10 @@
972
1025
  parentPath: path,
973
1026
  kind: kind,
974
1027
  name: kind === "variable" ? "newVariable" : kind === "folder" ? "newFolder" : kind === "objecttype" ? "newObjectType" : kind === "alarm" ? "newAlarm" : kind === "method" ? "newMethod" : "newObject",
1028
+ displayName: "",
975
1029
  dataType: "Int32",
976
1030
  value: "",
1031
+ access: "readwrite",
977
1032
  objectsType: "",
978
1033
  alarmType: "levelAlarm",
979
1034
  variableNodeId: "",
@@ -1012,16 +1067,19 @@
1012
1067
  if (kind === "variable") {
1013
1068
  target.push(normalizeVariable({
1014
1069
  name: pendingCreate.name,
1070
+ displayName: pendingCreate.displayName || "",
1015
1071
  type: pendingCreate.dataType,
1016
- value: pendingCreate.value
1072
+ value: pendingCreate.value,
1073
+ access: pendingCreate.access || "readwrite"
1017
1074
  }));
1018
1075
  } else if (kind === "folder") {
1019
- target.push(normalizeBranch({ name: pendingCreate.name }));
1076
+ target.push(normalizeBranch({ name: pendingCreate.name, displayName: pendingCreate.displayName || "" }));
1020
1077
  } else if (kind === "objecttype") {
1021
- target.push(normalizeBranch({ name: pendingCreate.name, objectsType: pendingCreate.objectsType || "" }));
1078
+ target.push(normalizeBranch({ name: pendingCreate.name, displayName: pendingCreate.displayName || "", objectsType: pendingCreate.objectsType || "" }));
1022
1079
  target[target.length - 1].nodeId = buildGeneratedNodeIdForPath(branchTargetPath + "." + (target.length - 1));
1023
1080
  } else if (kind === "alarm") {
1024
1081
  target.push(normalizeAlarm({
1082
+ displayName: pendingCreate.displayName || "",
1025
1083
  name: pendingCreate.name,
1026
1084
  type: pendingCreate.alarmType,
1027
1085
  variableNodeId: pendingCreate.variableNodeId,
@@ -1038,9 +1096,9 @@
1038
1096
  digitalMessage: pendingCreate.digitalMessage
1039
1097
  }));
1040
1098
  } else if (kind === "method") {
1041
- target.push(normalizeMethod({ name: pendingCreate.name }));
1099
+ target.push(normalizeMethod({ name: pendingCreate.name, displayName: pendingCreate.displayName || "" }));
1042
1100
  } else {
1043
- target.push(normalizeBranch({ name: pendingCreate.name }));
1101
+ target.push(normalizeBranch({ name: pendingCreate.name, displayName: pendingCreate.displayName || "" }));
1044
1102
  }
1045
1103
  expansionState[parentPath] = true;
1046
1104
  pendingCreate = null;
@@ -1060,12 +1118,14 @@
1060
1118
  panel.append('<div class="form-row"><label>Parent</label><input type="text" id="opcua-create-parent" readonly></div>');
1061
1119
  panel.append('<div class="form-row"><label>Type</label><input type="text" id="opcua-create-kind" readonly></div>');
1062
1120
  panel.append('<div class="form-row"><label>Name</label><input type="text" id="opcua-create-name"></div>');
1121
+ panel.append('<div class="form-row"><label>displayName</label><input type="text" id="opcua-create-displayname" placeholder="Leave blank to use browseName"></div>');
1063
1122
  if (pendingCreate.kind === "variable") {
1064
1123
  panel.append('<div class="form-row"><label>dataType</label><select id="opcua-create-type"><option value="Int16">Int16</option><option value="Int32">Int32</option><option value="Float">Float</option><option value="Boolean">Boolean</option><option value="String">String</option></select></div>');
1065
1124
  panel.append('<div class="form-row"><label>Value</label><input type="text" id="opcua-create-value"></div>');
1125
+ panel.append('<div class="form-row"><label>Access</label><select id="opcua-create-access"><option value="readwrite">readwrite</option><option value="readonly">readonly</option></select></div>');
1066
1126
  }
1067
1127
  if (pendingCreate.kind === "objecttype") {
1068
- panel.append('<div class="form-row"><label>objectsType</label><input type="text" id="opcua-create-objectstype"></div>');
1128
+ panel.append('<div class="form-row"><label>objectsType</label>' + buildObjectTypeSelect("opcua-create-objectstype", pendingCreate.objectsType || "") + '</div>');
1069
1129
  }
1070
1130
  if (pendingCreate.kind === "alarm") {
1071
1131
  panel.append('<div class="form-row"><label>alarmType</label><select id="opcua-create-alarm-type"><option value="levelAlarm">levelAlarm</option><option value="digitalAlarm">digitalAlarm</option></select></div>');
@@ -1089,9 +1149,11 @@
1089
1149
  $("#opcua-create-parent").val(pendingCreate.parentPath);
1090
1150
  $("#opcua-create-kind").val(pendingCreate.kind);
1091
1151
  $("#opcua-create-name").val(pendingCreate.name);
1152
+ $("#opcua-create-displayname").val(pendingCreate.displayName || "");
1092
1153
  $("#opcua-create-type").val(pendingCreate.dataType);
1093
1154
  $("#opcua-create-value").val(pendingCreate.value);
1094
1155
  $("#opcua-create-objectstype").val(pendingCreate.objectsType);
1156
+ $("#opcua-create-access").val(pendingCreate.access || "readwrite");
1095
1157
  $("#opcua-create-alarm-type").val(pendingCreate.alarmType);
1096
1158
  $("#opcua-create-variable-nodeid").val(pendingCreate.variableNodeId);
1097
1159
  $("#opcua-create-severity").val(pendingCreate.severity);
@@ -1130,15 +1192,16 @@
1130
1192
  panel.append('<div class="form-row"><label>namespace</label><select id="opcua-detail-namespace"></select></div>');
1131
1193
  panel.append('<div class="form-row"><label>nodeId</label><div class="opcua-nodeid-field"><span class="opcua-nodeid-prefix">' + getNodeIdPrefix(namespaceId) + '</span><input type="text" id="opcua-detail-nodeid"' + (nodeIdLocked ? ' readonly title="Generated automatically for object type models."' : '') + '><a href="#" id="opcua-detail-copy-nodeid" class="editor-button editor-button-small"><i class="fa fa-copy"></i> Copy</a></div></div>');
1132
1194
  panel.append('<div class="form-row"><label>Description</label><input type="text" id="opcua-detail-description"></div>');
1195
+ panel.append('<div class="form-row"><label>displayName</label><input type="text" id="opcua-detail-displayname" placeholder="Leave blank to use browseName"></div>');
1133
1196
  if (nodeClass === "ObjectType") {
1134
- panel.append('<div class="form-row"><label>objectsType</label><input type="text" id="opcua-detail-objectstype"></div>');
1197
+ panel.append('<div class="form-row"><label>objectsType</label>' + buildObjectTypeSelect("opcua-detail-objectstype", item.objectsType || "") + '</div>');
1135
1198
  }
1136
1199
  if (nodeClass === "Variable") {
1137
1200
  panel.append('<div class="form-row"><label>dataType</label><select id="opcua-detail-type"><option value="Int16">Int16</option><option value="Int32">Int32</option><option value="Float">Float</option><option value="Boolean">Boolean</option><option value="String">String</option></select></div>');
1138
1201
  panel.append('<div class="form-row"><label>Value</label><input type="text" id="opcua-detail-value"></div>');
1202
+ panel.append('<div class="form-row"><label>Access</label><select id="opcua-detail-access"><option value="readwrite">readwrite</option><option value="readonly">readonly</option></select></div>');
1139
1203
  }
1140
1204
  if (nodeClass === "Method") {
1141
- panel.append('<div class="form-row"><label>displayName</label><input type="text" id="opcua-detail-displayname"></div>');
1142
1205
  panel.append('<hr style="margin:8px 0; border-color:#e3e3e3;">');
1143
1206
  panel.append('<div style="font-size:11px;font-weight:700;text-transform:uppercase;color:#666;margin-bottom:4px;">Inputs</div>');
1144
1207
  var inputsDiv = $('<div id="opcua-detail-inputs"></div>').appendTo(panel);
@@ -1203,9 +1266,9 @@
1203
1266
  $("#opcua-detail-namespace").append($("<option></option>").val(option.id).text(getNamespaceLabel(option.id)));
1204
1267
  });
1205
1268
  $("#opcua-detail-namespace").val(String(namespaceId));
1206
- if (nodeClass === "Method") $("#opcua-detail-displayname").val(item.displayName || "");
1207
- if (nodeClass === "ObjectType") $("#opcua-detail-objectstype").val(item.objectsType || "");
1208
- if (nodeClass === "Variable") { $("#opcua-detail-type").val(item.type || "Int32"); $("#opcua-detail-value").val(item.value !== undefined ? item.value : ""); }
1269
+ $("#opcua-detail-displayname").val(item.displayName || "");
1270
+ if (nodeClass === "ObjectType") { $("#opcua-detail-objectstype").val(item.objectsType || ""); }
1271
+ if (nodeClass === "Variable") { $("#opcua-detail-type").val(item.type || "Int32"); $("#opcua-detail-value").val(item.value !== undefined ? item.value : ""); $("#opcua-detail-access").val(item.access || "readwrite"); }
1209
1272
  if (nodeClass === "Alarm") {
1210
1273
  $("#opcua-detail-alarm-type").val(item.type || "levelAlarm");
1211
1274
  $("#opcua-detail-variable-nodeid").val(item.variableNodeId || "");
@@ -1398,8 +1461,9 @@
1398
1461
  }
1399
1462
  });
1400
1463
  $(document).on("input", "#opcua-detail-description", function () { updateNode(selectedPath, { description: $(this).val() }); });
1401
- $(document).on("input", "#opcua-detail-objectstype", function () { updateNode(selectedPath, { objectsType: $(this).val() }); });
1464
+ $(document).on("change", "#opcua-detail-objectstype", function () { updateNode(selectedPath, { objectsType: $(this).val() }); });
1402
1465
  $(document).on("change", "#opcua-detail-type", function () { updateNode(selectedPath, { type: $(this).val() }); });
1466
+ $(document).on("change", "#opcua-detail-access", function () { updateNode(selectedPath, { access: $(this).val() }); });
1403
1467
  $(document).on("input", "#opcua-detail-value", function () { updateNode(selectedPath, { value: $(this).val() }); });
1404
1468
  $(document).on("change", "#opcua-detail-alarm-type", function () {
1405
1469
  updateNode(selectedPath, { type: $(this).val() });
@@ -1418,9 +1482,11 @@
1418
1482
  $(document).on("input", "#opcua-detail-normalstatevalue", function () { updateNode(selectedPath, { normalStateValue: Number($(this).val() || 0) }); });
1419
1483
  $(document).on("input", "#opcua-detail-digitalmessage", function () { updateNode(selectedPath, { digitalMessage: $(this).val() }); });
1420
1484
  $(document).on("input", "#opcua-create-name", function () { if (pendingCreate) pendingCreate.name = $(this).val(); });
1485
+ $(document).on("input", "#opcua-create-displayname", function () { if (pendingCreate) pendingCreate.displayName = $(this).val(); });
1421
1486
  $(document).on("change", "#opcua-create-type", function () { if (pendingCreate) pendingCreate.dataType = $(this).val(); });
1422
1487
  $(document).on("input", "#opcua-create-value", function () { if (pendingCreate) pendingCreate.value = $(this).val(); });
1423
- $(document).on("input", "#opcua-create-objectstype", function () { if (pendingCreate) pendingCreate.objectsType = $(this).val(); });
1488
+ $(document).on("change", "#opcua-create-objectstype", function () { if (pendingCreate) pendingCreate.objectsType = $(this).val(); });
1489
+ $(document).on("change", "#opcua-create-access", function () { if (pendingCreate) pendingCreate.access = $(this).val(); });
1424
1490
  $(document).on("change", "#opcua-create-alarm-type", function () {
1425
1491
  if (pendingCreate) {
1426
1492
  pendingCreate.alarmType = $(this).val();
@@ -1487,10 +1553,10 @@
1487
1553
  $("#node-input-close-tree-modal").off("click").on("click", function (event) { event.preventDefault(); closeTreeModal(); });
1488
1554
  $("#node-input-tree-modal").off("click").on("click", function (event) { if (event.target === this) closeTreeModal(); });
1489
1555
 
1490
- $("#node-input-tree-search").off("input").on("input", function () {
1556
+ $("#node-input-tree-search").off("input").on("input", debounce(function () {
1491
1557
  treeSearchValue = $(this).val(); treeSearchTerm = normalizeSearchTerm(treeSearchValue);
1492
1558
  $("#node-input-tree-search-clear").toggle(!!treeSearchTerm); renderTree();
1493
- });
1559
+ }, 200));
1494
1560
  $("#node-input-tree-search-clear").off("click").on("click", function (event) {
1495
1561
  event.preventDefault(); treeSearchValue = ""; treeSearchTerm = "";
1496
1562
  $("#node-input-tree-search").val(""); $(this).hide(); renderTree();
@@ -1545,4 +1611,4 @@
1545
1611
  <p>Supported variable types: <code>Int32</code>, <code>Float</code>, <code>Boolean</code>, <code>String</code>.</p>
1546
1612
  <p>Supported access modes: <code>readonly</code> and <code>readwrite</code>.</p>
1547
1613
  <p>Authentication can be configured in the editor with a local username list and bcrypt password hashes, and anonymous login can be disabled.</p>
1548
- </script>
1614
+ </script>