@vitormnm/node-red-simple-opcua 1.7.0 → 1.8.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.
@@ -3,6 +3,7 @@
3
3
  var editorState = { objects: [], folders: [], objectsTypes: [], enumerations: [], nameSpaces: [] };
4
4
  var expansionState = {};
5
5
  var selectedPath = "";
6
+ var draggedPath = null;
6
7
  var pendingCreate = null;
7
8
  var pendingPasswordHashes = 0;
8
9
  var authGroups = [];
@@ -443,6 +444,71 @@
443
444
 
444
445
  function normalizeSearchTerm(value) { return String(value || "").trim().toLowerCase(); }
445
446
  function isExpanded(path, defaultValue) { if (expansionState[path] === undefined) expansionState[path] = !!defaultValue; return expansionState[path]; }
447
+ function isAncestorPath(ancestorPath, descendantPath) {
448
+ if (ancestorPath === descendantPath) return true;
449
+ return descendantPath.indexOf(ancestorPath + ".") === 0;
450
+ }
451
+
452
+ function isValidDropTarget(srcPath, destPath) {
453
+ if (srcPath === destPath) return false;
454
+ if (isAncestorPath(srcPath, destPath)) return false;
455
+ if (srcPath.indexOf("virtual:") === 0 || srcPath.indexOf("nameSpaces.") === 0) return false;
456
+
457
+ var srcClass = nodeClassFromPath(srcPath);
458
+
459
+ if (destPath === "virtual:Objects") {
460
+ return srcClass === "Folder" || srcClass === "Object";
461
+ }
462
+ if (destPath === "virtual:Types.ObjectTypes") {
463
+ var srcTokens = pathToTokens(srcPath);
464
+ return srcClass === "ObjectType" && srcTokens.length === 2 && srcTokens[0] === "objectsTypes";
465
+ }
466
+ if (destPath === "virtual:Types.DataTypes") {
467
+ var srcTokens = pathToTokens(srcPath);
468
+ return srcClass === "Enumeration" && srcTokens.length === 2 && srcTokens[0] === "enumerations";
469
+ }
470
+
471
+ if (destPath.indexOf("virtual:") === 0 || destPath.indexOf("nameSpaces.") === 0) {
472
+ return false;
473
+ }
474
+
475
+ // Both are real paths. Make sure they are in the same scope (Objects vs Types/ObjectTypes)
476
+ var srcIsTemplate = isObjectTypeModelPath(srcPath);
477
+ var destIsTemplate = isObjectTypeModelPath(destPath);
478
+ if (srcIsTemplate !== destIsTemplate) {
479
+ return false;
480
+ }
481
+
482
+ var destClass = nodeClassFromPath(destPath);
483
+ if (destClass === "Folder" || destClass === "Object" || destClass === "ObjectType") {
484
+ return srcClass !== "Enumeration" && srcClass !== "Namespace";
485
+ }
486
+
487
+ return false;
488
+ }
489
+
490
+ function findObjectPath(obj, targetObj, currentPath) {
491
+ if (obj === targetObj) return currentPath;
492
+ if (typeof obj !== "object" || obj === null) return null;
493
+
494
+ if (Array.isArray(obj)) {
495
+ for (var i = 0; i < obj.length; i++) {
496
+ var path = findObjectPath(obj[i], targetObj, currentPath ? currentPath + "." + i : String(i));
497
+ if (path) return path;
498
+ }
499
+ } else {
500
+ var keys = Object.keys(obj);
501
+ for (var i = 0; i < keys.length; i++) {
502
+ var k = keys[i];
503
+ if (k === "folders" || k === "objects" || k === "variables" || k === "methods" || k === "alarms" || k === "objectsTypes" || k === "enumerations") {
504
+ var path = findObjectPath(obj[k], targetObj, currentPath ? currentPath + "." + k : k);
505
+ if (path) return path;
506
+ }
507
+ }
508
+ }
509
+ return null;
510
+ }
511
+
446
512
  function nodeClassFromPath(path) {
447
513
  if (path && path.indexOf("virtual:") === 0) return "VisualFolder";
448
514
  var tokens = pathToTokens(path);
@@ -822,6 +888,9 @@
822
888
  var row = document.createElement("div");
823
889
  row.className = "opcua-tree-row" + (path === selectedPath ? " is-selected" : "");
824
890
  row.setAttribute("data-path", path);
891
+ if (path && path.indexOf("virtual:") !== 0 && path.indexOf("nameSpaces.") !== 0) {
892
+ row.setAttribute("draggable", "true");
893
+ }
825
894
  var label = (path && path.indexOf("virtual:") === 0) ? getVirtualNodeName(path) : (item.name || "(unnamed)");
826
895
  var displayClass = (path && path.indexOf("virtual:") === 0) ? "Folder" : nodeClass;
827
896
  row.innerHTML = indents
@@ -933,7 +1002,7 @@
933
1002
  return;
934
1003
  }
935
1004
  var parentSuffix = path.indexOf("virtual:") === 0 ? "" : buildDefaultNodeIdSuffixFromEditorPath(path);
936
- var defaultName = kind === "variable" ? "newVariable" : kind === "enum-variable" ? "newEnumVariable" : "newObject";
1005
+ var defaultName = kind === "variable" ? "newVariable" : kind === "enum-variable" ? "newEnumVariable" : kind === "folder" ? "newFolder" : kind === "method" ? "newMethod" : kind === "alarm" ? "newAlarm" : "newObject";
937
1006
  var defaultSuffix = parentSuffix ? parentSuffix + "." + defaultName : defaultName;
938
1007
 
939
1008
  pendingCreate = {
@@ -961,7 +1030,7 @@
961
1030
  normalStateValue: 0,
962
1031
  digitalMessage: "Digital alarm",
963
1032
  nodeIdType: "s",
964
- nodeIdValue: (kind === "variable" || kind === "enum-variable") ? defaultSuffix : ""
1033
+ nodeIdValue: defaultSuffix
965
1034
  };
966
1035
  renderDetails();
967
1036
  }
@@ -994,19 +1063,22 @@
994
1063
  }
995
1064
  var target = getAtPath(editorState, branchTargetPath);
996
1065
  if (!Array.isArray(target)) return;
997
- if (kind === "variable" || kind === "enum-variable") {
998
- var customNodeId = "";
1066
+
1067
+ var customNodeId = "";
1068
+ var parentIsObjectTypeModel = isObjectTypeModelPath(parentPath);
1069
+ if (!parentIsObjectTypeModel && pendingCreate.nodeIdValue) {
999
1070
  var nsId = getNodeNamespaceId(parentPath);
1000
- if (pendingCreate.nodeIdValue) {
1001
- if (pendingCreate.nodeIdType === "i") {
1002
- var numVal = parseInt(pendingCreate.nodeIdValue, 10);
1003
- if (!isNaN(numVal)) {
1004
- customNodeId = "ns=" + nsId + ";i=" + numVal;
1005
- }
1006
- } else {
1007
- customNodeId = "ns=" + nsId + ";s=" + pendingCreate.nodeIdValue.trim();
1071
+ if (pendingCreate.nodeIdType === "i") {
1072
+ var numVal = parseInt(pendingCreate.nodeIdValue, 10);
1073
+ if (!isNaN(numVal)) {
1074
+ customNodeId = "ns=" + nsId + ";i=" + numVal;
1008
1075
  }
1076
+ } else {
1077
+ customNodeId = "ns=" + nsId + ";s=" + pendingCreate.nodeIdValue.trim();
1009
1078
  }
1079
+ }
1080
+
1081
+ if (kind === "variable" || kind === "enum-variable") {
1010
1082
  target.push(normalizeVariable({
1011
1083
  name: pendingCreate.name,
1012
1084
  displayName: pendingCreate.displayName || "",
@@ -1017,10 +1089,12 @@
1017
1089
  nodeId: customNodeId
1018
1090
  }));
1019
1091
  } else if (kind === "folder") {
1020
- target.push(normalizeBranch({ name: pendingCreate.name, displayName: pendingCreate.displayName || "", accessPermission: pendingCreate.accessPermission }));
1092
+ target.push(normalizeBranch({ name: pendingCreate.name, displayName: pendingCreate.displayName || "", accessPermission: pendingCreate.accessPermission, nodeId: customNodeId }));
1021
1093
  } else if (kind === "objecttype") {
1022
- target.push(normalizeBranch({ name: pendingCreate.name, displayName: pendingCreate.displayName || "", objectsType: pendingCreate.objectsType || "", accessPermission: pendingCreate.accessPermission }));
1023
- target[target.length - 1].nodeId = buildGeneratedNodeIdForPath(branchTargetPath + "." + (target.length - 1));
1094
+ target.push(normalizeBranch({ name: pendingCreate.name, displayName: pendingCreate.displayName || "", objectsType: pendingCreate.objectsType || "", accessPermission: pendingCreate.accessPermission, nodeId: customNodeId }));
1095
+ if (!customNodeId) {
1096
+ target[target.length - 1].nodeId = buildGeneratedNodeIdForPath(branchTargetPath + "." + (target.length - 1));
1097
+ }
1024
1098
  } else if (kind === "alarm") {
1025
1099
  target.push(normalizeAlarm({
1026
1100
  displayName: pendingCreate.displayName || "",
@@ -1039,12 +1113,13 @@
1039
1113
  lowLowLimit: pendingCreate.lowLowLimit,
1040
1114
  lowLowMessage: pendingCreate.lowLowMessage,
1041
1115
  normalStateValue: pendingCreate.normalStateValue,
1042
- digitalMessage: pendingCreate.digitalMessage
1116
+ digitalMessage: pendingCreate.digitalMessage,
1117
+ nodeId: customNodeId
1043
1118
  }));
1044
1119
  } else if (kind === "method") {
1045
- target.push(normalizeMethod({ name: pendingCreate.name, displayName: pendingCreate.displayName || "", accessPermission: pendingCreate.accessPermission }));
1120
+ target.push(normalizeMethod({ name: pendingCreate.name, displayName: pendingCreate.displayName || "", accessPermission: pendingCreate.accessPermission, nodeId: customNodeId }));
1046
1121
  } else {
1047
- target.push(normalizeBranch({ name: pendingCreate.name, displayName: pendingCreate.displayName || "", accessPermission: pendingCreate.accessPermission }));
1122
+ target.push(normalizeBranch({ name: pendingCreate.name, displayName: pendingCreate.displayName || "", accessPermission: pendingCreate.accessPermission, nodeId: customNodeId }));
1048
1123
  }
1049
1124
  expansionState[parentPath] = true;
1050
1125
  pendingCreate = null;
@@ -1066,14 +1141,17 @@
1066
1141
  panel.append('<div class="form-row"><label>Name</label><input type="text" id="opcua-create-name"></div>');
1067
1142
  panel.append('<div class="form-row"><label>displayName</label><input type="text" id="opcua-create-displayname" placeholder="Leave blank to use browseName"></div>');
1068
1143
  panel.append('<div class="form-row"><label>accessPermission</label>' + buildAccessPermissionSelect("opcua-create-accesspermission", pendingCreate.accessPermission || ["public"]) + '</div>');
1144
+ var parentIsObjectTypeModel = isObjectTypeModelPath(pendingCreate.parentPath);
1145
+ if (!parentIsObjectTypeModel) {
1146
+ panel.append('<div class="form-row"><label>nodeId Type</label><select id="opcua-create-nodeid-type"><option value="s">s (String)</option><option value="i">i (Numeric)</option></select></div>');
1147
+ panel.append('<div class="form-row" id="opcua-create-nodeid-value-row"><label id="opcua-create-nodeid-value-label">nodeId Value</label><input type="text" id="opcua-create-nodeid-value" placeholder="Leave blank for default (s)"></div>');
1148
+ }
1069
1149
  if (pendingCreate.kind === "variable" || pendingCreate.kind === "enum-variable") {
1070
1150
  var isEnum = pendingCreate.kind === "enum-variable";
1071
1151
  var typeHtml = isEnum ? buildEnumerationSelect("opcua-create-type", pendingCreate.dataType) : '<select id="opcua-create-type"><option value="Int16">Int16</option><option value="Int32">Int32</option><option value="Int64">Int64</option><option value="Float">Float</option><option value="Boolean">Boolean</option><option value="String">String</option></select>';
1072
1152
  panel.append('<div class="form-row"><label>dataType</label>' + typeHtml + '</div>');
1073
1153
  panel.append('<div class="form-row"><label>Value</label><input type="text" id="opcua-create-value"></div>');
1074
1154
  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>');
1075
- panel.append('<div class="form-row"><label>nodeId Type</label><select id="opcua-create-nodeid-type"><option value="s">s (String)</option><option value="i">i (Numeric)</option></select></div>');
1076
- panel.append('<div class="form-row" id="opcua-create-nodeid-value-row"><label id="opcua-create-nodeid-value-label">nodeId Value</label><input type="text" id="opcua-create-nodeid-value" placeholder="Leave blank for default (s)"></div>');
1077
1155
  }
1078
1156
  if (pendingCreate.kind === "objecttype") {
1079
1157
  panel.append('<div class="form-row"><label>objectsType</label>' + buildObjectTypeSelect("opcua-create-objectstype", pendingCreate.objectsType || "") + '</div>');
@@ -1103,7 +1181,7 @@
1103
1181
  $("#opcua-create-name").val(pendingCreate.name);
1104
1182
  $("#opcua-create-displayname").val(pendingCreate.displayName || "");
1105
1183
  $("#opcua-create-accesspermission").val(normalizeAccessPermissionValues(pendingCreate.accessPermission));
1106
- if (pendingCreate.kind === "variable" || pendingCreate.kind === "enum-variable") {
1184
+ if (!parentIsObjectTypeModel) {
1107
1185
  $("#opcua-create-nodeid-type").val(pendingCreate.nodeIdType || "s");
1108
1186
  $("#opcua-create-nodeid-value").val(pendingCreate.nodeIdValue || "");
1109
1187
  updateNodeIdValueInputState("create", pendingCreate.nodeIdType || "s");
@@ -1185,7 +1263,7 @@
1185
1263
  panel.append('<div class="form-row"><label>browseName</label><input type="text" id="opcua-detail-name"></div>');
1186
1264
  panel.append('<div class="form-row"><label>nodeClass</label><input type="text" id="opcua-detail-class" readonly></div>');
1187
1265
  panel.append('<div class="form-row"><label>namespace</label><select id="opcua-detail-namespace"></select></div>');
1188
- if (nodeClass === "Variable" && !nodeIdLocked) {
1266
+ if (!nodeIdLocked) {
1189
1267
  panel.append('<div class="form-row"><label>nodeId</label>' +
1190
1268
  '<div class="opcua-nodeid-field">' +
1191
1269
  '<span class="opcua-nodeid-prefix">ns=' + namespaceId + ';</span>' +
@@ -1273,7 +1351,7 @@
1273
1351
  panel.append('<div class="form-row"><label style="width:90px;">Actions</label><div><a href="#" id="opcua-detail-edit" class="editor-button editor-button-small"><i class="fa fa-pencil"></i> Edit</a> <a href="#" id="opcua-detail-remove" class="editor-button editor-button-small"><i class="fa fa-trash"></i> Remove</a></div></div>');
1274
1352
  $("#opcua-detail-name").val(item.name || "");
1275
1353
  $("#opcua-detail-class").val(nodeClass);
1276
- if (nodeClass === "Variable" && !nodeIdLocked) {
1354
+ if (!nodeIdLocked) {
1277
1355
  var parsed = parseNodeId(item.nodeId, buildDefaultNodeIdSuffixFromEditorPath(selectedPath));
1278
1356
  $("#opcua-detail-nodeid-type").val(parsed.type);
1279
1357
  $("#opcua-detail-nodeid-value").val(parsed.value);
@@ -1330,8 +1408,72 @@
1330
1408
  if (selectedPath === path) selectedPath = "";
1331
1409
  syncStateToJson(true);
1332
1410
  renderVisualEditor();
1411
+ }
1412
+
1413
+ function moveNode(srcPath, destPath) {
1414
+ var item = getAtPath(editorState, srcPath);
1415
+ if (!item) return;
1416
+
1417
+ var srcClass = nodeClassFromPath(srcPath);
1418
+ var targetCollection = null;
1419
+
1420
+ if (destPath === "virtual:Objects") {
1421
+ if (srcClass === "Folder") {
1422
+ editorState.folders = editorState.folders || [];
1423
+ targetCollection = editorState.folders;
1424
+ } else if (srcClass === "Object") {
1425
+ editorState.objects = editorState.objects || [];
1426
+ targetCollection = editorState.objects;
1427
+ }
1428
+ } else if (destPath === "virtual:Types.ObjectTypes") {
1429
+ editorState.objectsTypes = editorState.objectsTypes || [];
1430
+ targetCollection = editorState.objectsTypes;
1431
+ } else if (destPath === "virtual:Types.DataTypes") {
1432
+ editorState.enumerations = editorState.enumerations || [];
1433
+ targetCollection = editorState.enumerations;
1434
+ } else {
1435
+ var destParent = getAtPath(editorState, destPath);
1436
+ if (!destParent) return;
1437
+
1438
+ if (srcClass === "Folder") {
1439
+ destParent.folders = destParent.folders || [];
1440
+ targetCollection = destParent.folders;
1441
+ } else if (srcClass === "Object") {
1442
+ destParent.objects = destParent.objects || [];
1443
+ targetCollection = destParent.objects;
1444
+ } else if (srcClass === "Variable") {
1445
+ destParent.variables = destParent.variables || [];
1446
+ targetCollection = destParent.variables;
1447
+ } else if (srcClass === "Method") {
1448
+ destParent.methods = destParent.methods || [];
1449
+ targetCollection = destParent.methods;
1450
+ } else if (srcClass === "Alarm") {
1451
+ destParent.alarms = destParent.alarms || [];
1452
+ targetCollection = destParent.alarms;
1453
+ } else if (srcClass === "ObjectType") {
1454
+ destParent.objectsTypes = destParent.objectsTypes || [];
1455
+ targetCollection = destParent.objectsTypes;
1456
+ }
1457
+ }
1458
+
1459
+ if (targetCollection) {
1460
+ removeAtPath(editorState, srcPath);
1461
+ targetCollection.push(item);
1462
+
1463
+ var newPath = findObjectPath(editorState, item, "");
1464
+ if (newPath) {
1465
+ selectedPath = newPath;
1466
+ expansionState[destPath] = true;
1467
+ } else {
1468
+ selectedPath = "";
1469
+ }
1470
+
1471
+ syncStateToJson(true);
1472
+ renderVisualEditor();
1473
+ }
1333
1474
  }
1334
1475
 
1476
+
1335
1477
  function buildGroupOptions(selectedGroup) {
1336
1478
  var currentGroup = String(selectedGroup || "");
1337
1479
  var options = authGroups.map(function (groupName) {
@@ -1560,6 +1702,56 @@
1560
1702
  } catch (error) { RED.notify("Invalid JSON: " + error.message, "error"); }
1561
1703
  });
1562
1704
 
1705
+ $(document).on("dragstart", ".opcua-tree-row", function (event) {
1706
+ var path = $(this).attr("data-path");
1707
+ if (path && path.indexOf("virtual:") !== 0 && path.indexOf("nameSpaces.") !== 0) {
1708
+ draggedPath = path;
1709
+ if (event.originalEvent.dataTransfer) {
1710
+ event.originalEvent.dataTransfer.effectAllowed = "move";
1711
+ event.originalEvent.dataTransfer.setData("text/plain", path);
1712
+ }
1713
+ $(this).addClass("is-dragging");
1714
+ } else {
1715
+ event.preventDefault();
1716
+ }
1717
+ });
1718
+
1719
+ $(document).on("dragend", ".opcua-tree-row", function (event) {
1720
+ $(this).removeClass("is-dragging");
1721
+ draggedPath = null;
1722
+ $(".opcua-tree-row").removeClass("drag-over");
1723
+ });
1724
+
1725
+ $(document).on("dragover", ".opcua-tree-row", function (event) {
1726
+ if (!draggedPath) return;
1727
+ event.preventDefault();
1728
+
1729
+ var row = $(this);
1730
+ var targetPath = row.attr("data-path");
1731
+ if (isValidDropTarget(draggedPath, targetPath)) {
1732
+ if (event.originalEvent.dataTransfer) {
1733
+ event.originalEvent.dataTransfer.dropEffect = "move";
1734
+ }
1735
+ $(".opcua-tree-row").not(row).removeClass("drag-over");
1736
+ row.addClass("drag-over");
1737
+ }
1738
+ });
1739
+
1740
+ $(document).on("dragleave", ".opcua-tree-row", function (event) {
1741
+ $(this).removeClass("drag-over");
1742
+ });
1743
+
1744
+ $(document).on("drop", ".opcua-tree-row", function (event) {
1745
+ event.preventDefault();
1746
+ $(".opcua-tree-row").removeClass("drag-over");
1747
+
1748
+ if (!draggedPath) return;
1749
+ var targetPath = $(this).attr("data-path");
1750
+ if (isValidDropTarget(draggedPath, targetPath)) {
1751
+ moveNode(draggedPath, targetPath);
1752
+ }
1753
+ });
1754
+
1563
1755
  $(document).on("click", ".opcua-tree-row", function (event) {
1564
1756
  var path = $(this).attr("data-path");
1565
1757
  if ($(event.target).closest(".opcua-tree-twisty").length) {
@@ -1582,10 +1774,16 @@
1582
1774
  if (path === "virtual:Objects") {
1583
1775
  contextMenu.find('[data-action="add-folder"]').show();
1584
1776
  contextMenu.find('[data-action="add-object"]').show();
1777
+ contextMenu.find('[data-action="expand-all-below"]').show();
1778
+ contextMenu.find('[data-action="collapse-all-below"]').show();
1585
1779
  } else if (path === "virtual:Types.ObjectTypes") {
1586
1780
  contextMenu.find('[data-action="add-objecttype"]').show();
1781
+ contextMenu.find('[data-action="expand-all-below"]').show();
1782
+ contextMenu.find('[data-action="collapse-all-below"]').show();
1587
1783
  } else if (path === "virtual:Types.DataTypes") {
1588
1784
  contextMenu.find('[data-action="add-enumeration"]').show();
1785
+ contextMenu.find('[data-action="expand-all-below"]').show();
1786
+ contextMenu.find('[data-action="collapse-all-below"]').show();
1589
1787
  }
1590
1788
  } else {
1591
1789
  var nodeClass = nodeClassFromPath(path);
@@ -1593,11 +1791,14 @@
1593
1791
  contextMenu.find('[data-action="add-folder"]').show();
1594
1792
  contextMenu.find('[data-action="add-object"]').show();
1595
1793
  contextMenu.find('[data-action="add-variable"]').show();
1794
+ contextMenu.find('[data-action="add-objecttype"]').show();
1596
1795
  contextMenu.find('[data-action="add-enum-variable"]').show();
1597
1796
  contextMenu.find('[data-action="add-alarm"]').show();
1598
1797
  contextMenu.find('[data-action="add-method"]').show();
1599
1798
  contextMenu.find('[data-action="edit"]').show();
1600
1799
  contextMenu.find('[data-action="remove"]').show();
1800
+ contextMenu.find('[data-action="expand-all-below"]').show();
1801
+ contextMenu.find('[data-action="collapse-all-below"]').show();
1601
1802
  } else if (nodeClass === "Enumeration") {
1602
1803
  contextMenu.find('[data-action="edit"]').show();
1603
1804
  contextMenu.find('[data-action="remove"]').show();
@@ -1631,6 +1832,22 @@
1631
1832
  if (action === "add-method") addNode(selectedPath, "method");
1632
1833
  if (action === "remove") removeNode(selectedPath);
1633
1834
  if (action === "edit") renderDetails();
1835
+ if (action === "expand-all-below") {
1836
+ function walk(p) {
1837
+ expansionState[p] = true;
1838
+ getChildrenByPath(p).forEach(walk);
1839
+ }
1840
+ walk(selectedPath);
1841
+ renderTree();
1842
+ }
1843
+ if (action === "collapse-all-below") {
1844
+ function walk(p) {
1845
+ expansionState[p] = false;
1846
+ getChildrenByPath(p).forEach(walk);
1847
+ }
1848
+ walk(selectedPath);
1849
+ renderTree();
1850
+ }
1634
1851
  $("#opcua-tree-context-menu").hide();
1635
1852
  });
1636
1853