node-red-contrib-i3x 0.0.2 → 0.0.4

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.
@@ -44,6 +44,13 @@
44
44
  <label for="node-config-input-timeout"><i class="fa fa-clock-o"></i> Timeout (ms)</label>
45
45
  <input type="number" id="node-config-input-timeout" placeholder="10000" min="1000" step="1000">
46
46
  </div>
47
+ <div class="form-row">
48
+ <label>&nbsp;</label>
49
+ <button type="button" id="i3x-test-connection" class="red-ui-button" style="width:calc(100% - 110px)">
50
+ <i class="fa fa-plug"></i> Test Connection
51
+ </button>
52
+ <span id="i3x-test-result" style="margin-left:8px;font-size:12px"></span>
53
+ </div>
47
54
  </script>
48
55
 
49
56
  <script type="text/html" data-help-name="i3x-server">
@@ -93,6 +100,925 @@
93
100
  }
94
101
  authType.on("change", toggleAuth);
95
102
  toggleAuth();
103
+
104
+ var nodeId = this.id;
105
+ $("#i3x-test-connection").on("click", function () {
106
+ var $btn = $(this);
107
+ var $result = $("#i3x-test-result");
108
+ if (!nodeId) {
109
+ $result.html('<span style="color:#d4a017"><i class="fa fa-warning"></i> Save and deploy first</span>');
110
+ return;
111
+ }
112
+ $btn.prop("disabled", true);
113
+ $result.html('<i class="fa fa-spinner fa-spin"></i> Testing...');
114
+ $.getJSON("i3x-server/" + encodeURIComponent(nodeId) + "/status")
115
+ .done(function (data) {
116
+ if (data.connected) {
117
+ $result.html('<span style="color:#22c55e"><i class="fa fa-check"></i> Connected</span>');
118
+ } else {
119
+ $result.html('<span style="color:#ef4444"><i class="fa fa-times"></i> Disconnected</span>');
120
+ }
121
+ })
122
+ .fail(function () {
123
+ $result.html('<span style="color:#ef4444"><i class="fa fa-times"></i> Not deployed</span>');
124
+ })
125
+ .always(function () {
126
+ $btn.prop("disabled", false);
127
+ });
128
+ });
96
129
  },
97
130
  });
131
+
132
+ /* ── Shared i3X Browser Widget ──────────────────────────────────── */
133
+ (function () {
134
+ "use strict";
135
+ var CSS_INJECTED = false;
136
+
137
+ /* ── 8. Dark Mode: CSS custom properties ───────────────────────── */
138
+ var CSS_VARS = [
139
+ ":root{",
140
+ " --i3x-accent:#4CAF79;",
141
+ " --i3x-accent-light:rgba(76,175,121,0.08);",
142
+ " --i3x-accent-med:rgba(76,175,121,0.18);",
143
+ " --i3x-bg:#fff;",
144
+ " --i3x-bg-alt:#fafbfc;",
145
+ " --i3x-bg-grad-start:#fafbfc;",
146
+ " --i3x-bg-grad-end:#f3f4f6;",
147
+ " --i3x-bg-hover:#f3f4f6;",
148
+ " --i3x-border:#d0d7de;",
149
+ " --i3x-border-light:#e1e4e8;",
150
+ " --i3x-text:#24292f;",
151
+ " --i3x-text-sec:#57606a;",
152
+ " --i3x-text-muted:#8b949e;",
153
+ " --i3x-text-input:#a0a8b4;",
154
+ " --i3x-chk-off:#c9d1d9;",
155
+ " --i3x-danger:#dc2626;",
156
+ " --i3x-warn:#d4a017;",
157
+ " --i3x-badge-zero-bg:#d0d7de;",
158
+ " --i3x-badge-zero-fg:#57606a;",
159
+ " --i3x-scrollbar:#d0d7de;",
160
+ "}",
161
+ /* Node-RED uses .red-ui-editor.dark for dark mode */
162
+ ".red-ui-editor.dark{",
163
+ " --i3x-bg:#1e1e1e;",
164
+ " --i3x-bg-alt:#252526;",
165
+ " --i3x-bg-grad-start:#252526;",
166
+ " --i3x-bg-grad-end:#2d2d2d;",
167
+ " --i3x-bg-hover:#2a2d2e;",
168
+ " --i3x-border:#444;",
169
+ " --i3x-border-light:#3c3c3c;",
170
+ " --i3x-text:#cccccc;",
171
+ " --i3x-text-sec:#aaa;",
172
+ " --i3x-text-muted:#888;",
173
+ " --i3x-text-input:#777;",
174
+ " --i3x-chk-off:#555;",
175
+ " --i3x-danger:#f87171;",
176
+ " --i3x-warn:#fbbf24;",
177
+ " --i3x-badge-zero-bg:#444;",
178
+ " --i3x-badge-zero-fg:#aaa;",
179
+ " --i3x-scrollbar:#555;",
180
+ "}"
181
+ ].join("\n");
182
+
183
+ /* ── 7. Type-icon mapping ──────────────────────────────────────── */
184
+ var TYPE_ICON_MAP = {
185
+ "sensor": "fa-thermometer-half",
186
+ "temperature": "fa-thermometer-half",
187
+ "pressure": "fa-tachometer",
188
+ "humidity": "fa-tint",
189
+ "flow": "fa-exchange",
190
+ "level": "fa-signal",
191
+ "valve": "fa-cog",
192
+ "pump": "fa-cog",
193
+ "motor": "fa-cog",
194
+ "actuator": "fa-cog",
195
+ "equipment": "fa-wrench",
196
+ "machine": "fa-industry",
197
+ "tank": "fa-database",
198
+ "vessel": "fa-database",
199
+ "pipe": "fa-minus",
200
+ "line": "fa-minus",
201
+ "area": "fa-map-marker",
202
+ "site": "fa-building",
203
+ "plant": "fa-building",
204
+ "enterprise": "fa-globe",
205
+ "alarm": "fa-bell",
206
+ "event": "fa-bolt",
207
+ "setpoint": "fa-sliders",
208
+ "parameter": "fa-sliders",
209
+ "controller": "fa-microchip",
210
+ "plc": "fa-microchip",
211
+ "default": "fa-cube"
212
+ };
213
+
214
+ function guessIcon(typeName) {
215
+ if (!typeName) return TYPE_ICON_MAP["default"];
216
+ var lower = typeName.toLowerCase();
217
+ var keys = Object.keys(TYPE_ICON_MAP);
218
+ for (var i = 0; i < keys.length; i++) {
219
+ if (lower.indexOf(keys[i]) !== -1) return TYPE_ICON_MAP[keys[i]];
220
+ }
221
+ return TYPE_ICON_MAP["default"];
222
+ }
223
+
224
+ function injectCSS() {
225
+ if (CSS_INJECTED) return;
226
+ CSS_INJECTED = true;
227
+ var s = document.createElement("style");
228
+ s.textContent = CSS_VARS + "\n" + [
229
+ /* panel */
230
+ ".i3x-browser{border:1px solid var(--i3x-border);border-radius:8px;margin-top:8px;background:var(--i3x-bg);box-shadow:0 1px 3px rgba(0,0,0,.08);overflow:hidden;transition:all .2s ease;position:relative}",
231
+ /* header */
232
+ ".i3x-browser-hdr{padding:8px 10px;background:linear-gradient(to bottom,var(--i3x-bg-grad-start),var(--i3x-bg-grad-end));border-bottom:1px solid var(--i3x-border-light);display:flex;gap:6px;align-items:center}",
233
+ ".i3x-browser-search-wrap{flex:1;position:relative}",
234
+ ".i3x-browser-search-wrap .fa-search{position:absolute;left:8px;top:50%;transform:translateY(-50%);color:var(--i3x-text-input);font-size:11px;pointer-events:none}",
235
+ ".i3x-browser-search{width:100%;padding:5px 24px 5px 26px;border:1px solid var(--i3x-border);border-radius:6px;font-size:12px;outline:none;background:var(--i3x-bg);color:var(--i3x-text);transition:border-color .15s,box-shadow .15s;box-sizing:border-box}",
236
+ ".i3x-browser-search:focus{border-color:var(--i3x-accent);box-shadow:0 0 0 3px var(--i3x-accent-light)}",
237
+ ".i3x-search-clear{position:absolute;right:6px;top:50%;transform:translateY(-50%);background:none;border:none;color:var(--i3x-text-input);font-size:12px;cursor:pointer;padding:0 2px;display:none;line-height:1}",
238
+ ".i3x-search-clear:hover{color:var(--i3x-text-sec)}",
239
+ ".i3x-hdr-btn{border:1px solid var(--i3x-border);background:var(--i3x-bg);border-radius:6px;width:28px;height:28px;display:flex;align-items:center;justify-content:center;cursor:pointer;color:var(--i3x-text-sec);transition:all .15s;flex-shrink:0}",
240
+ ".i3x-hdr-btn:hover{background:var(--i3x-bg-hover);border-color:var(--i3x-border);color:var(--i3x-text)}",
241
+ /* 6. connection status dot */
242
+ ".i3x-status-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0;transition:background .3s}",
243
+ ".i3x-status-dot.connected{background:#22c55e;box-shadow:0 0 4px rgba(34,197,94,.5)}",
244
+ ".i3x-status-dot.disconnected{background:#ef4444;box-shadow:0 0 4px rgba(239,68,68,.5)}",
245
+ ".i3x-status-dot.unknown{background:#a0a8b4}",
246
+ /* 8. breadcrumb */
247
+ ".i3x-breadcrumb{padding:4px 10px;border-bottom:1px solid var(--i3x-border-light);background:var(--i3x-bg-alt);font-size:11px;color:var(--i3x-text-muted);display:none;align-items:center;gap:4px;overflow-x:auto;white-space:nowrap}",
248
+ ".i3x-breadcrumb .i3x-bc-item{cursor:pointer;color:var(--i3x-accent);text-decoration:none}",
249
+ ".i3x-breadcrumb .i3x-bc-item:hover{text-decoration:underline}",
250
+ ".i3x-breadcrumb .i3x-bc-sep{color:var(--i3x-text-muted);margin:0 2px}",
251
+ ".i3x-breadcrumb .i3x-bc-current{color:var(--i3x-text)}",
252
+ /* search results container */
253
+ ".i3x-search-results{padding:4px 0}",
254
+ ".i3x-search-results .i3x-sr-type{color:var(--i3x-text-muted);font-size:10px;margin-left:auto;flex-shrink:0}",
255
+ /* tree */
256
+ ".i3x-browser-tree{max-height:320px;overflow-y:auto;padding:4px 0;scrollbar-width:thin;scrollbar-color:var(--i3x-scrollbar) transparent}",
257
+ ".i3x-browser-tree::-webkit-scrollbar{width:6px}",
258
+ ".i3x-browser-tree::-webkit-scrollbar-thumb{background:var(--i3x-scrollbar);border-radius:3px}",
259
+ /* messages */
260
+ ".i3x-browser-msg{padding:20px 16px;text-align:center;color:var(--i3x-text-muted);font-size:12px;line-height:1.5}",
261
+ ".i3x-browser-msg .fa{margin-right:4px}",
262
+ ".i3x-browser-msg.i3x-msg-inline{padding:4px 0;text-align:left;font-size:11px}",
263
+ /* nodes */
264
+ ".i3x-bnode{user-select:none}",
265
+ /* rows */
266
+ ".i3x-bnode-row{padding:4px 10px 4px 0;cursor:pointer;font-size:12px;display:flex;align-items:center;gap:5px;border-radius:4px;margin:1px 4px;transition:background .12s;outline:none}",
267
+ ".i3x-bnode-row:hover{background:var(--i3x-bg-hover)}",
268
+ ".i3x-bnode-row.sel{background:var(--i3x-accent-med)}",
269
+ ".i3x-bnode-row.sel:hover{background:var(--i3x-accent-med)}",
270
+ /* 10. keyboard focus ring */
271
+ ".i3x-bnode-row.i3x-kb-focus{outline:2px solid var(--i3x-accent);outline-offset:-2px}",
272
+ /* toggle arrow */
273
+ ".i3x-bnode-toggle{width:16px;text-align:center;flex-shrink:0;color:var(--i3x-text-muted);font-size:10px;transition:transform .15s ease}",
274
+ /* type icon */
275
+ ".i3x-bnode-icon{flex-shrink:0;font-size:12px;width:16px;text-align:center}",
276
+ ".i3x-bnode-row[data-eid] .i3x-bnode-icon{color:var(--i3x-accent)}",
277
+ ".i3x-bnode-row:not([data-eid]) .i3x-bnode-icon{color:var(--i3x-text-muted)}",
278
+ /* checkbox */
279
+ ".i3x-bnode-chk{flex-shrink:0;font-size:13px;color:var(--i3x-chk-off);transition:color .12s}",
280
+ ".i3x-bnode-row.sel .i3x-bnode-chk{color:var(--i3x-accent)}",
281
+ /* name & id */
282
+ ".i3x-bnode-name{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--i3x-text);font-size:12px}",
283
+ ".i3x-bnode-row:not([data-eid]) .i3x-bnode-name{font-weight:600;color:var(--i3x-text-sec);font-size:11px;text-transform:uppercase;letter-spacing:0.3px}",
284
+ ".i3x-bnode-id{color:var(--i3x-text-muted);font-size:10px;flex-shrink:0;max-width:130px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:SFMono-Regular,Consolas,monospace}",
285
+ /* type count badge */
286
+ ".i3x-bnode-count{color:var(--i3x-text-muted);font-size:10px;flex-shrink:0;font-weight:400;text-transform:none;letter-spacing:0}",
287
+ /* children container */
288
+ ".i3x-bnode-children{display:none;border-left:1px solid var(--i3x-border-light);margin-left:18px}",
289
+ ".i3x-bnode-children.open{display:block}",
290
+ /* footer */
291
+ ".i3x-browser-foot{padding:6px 12px;border-top:1px solid var(--i3x-border-light);background:var(--i3x-bg-alt);font-size:11px;color:var(--i3x-text-sec);display:flex;justify-content:space-between;align-items:center;gap:6px}",
292
+ ".i3x-foot-badge{display:inline-flex;align-items:center;gap:4px}",
293
+ ".i3x-foot-count{background:var(--i3x-accent);color:#fff;font-size:10px;font-weight:600;padding:1px 6px;border-radius:10px;min-width:14px;text-align:center}",
294
+ ".i3x-foot-count.zero{background:var(--i3x-badge-zero-bg);color:var(--i3x-badge-zero-fg)}",
295
+ ".i3x-clear-btn{border:none;background:none;color:var(--i3x-text-muted);cursor:pointer;font-size:11px;padding:2px 6px;border-radius:4px;transition:all .12s}",
296
+ ".i3x-clear-btn:hover{background:#fee2e2;color:var(--i3x-danger)}",
297
+ /* 11. export button */
298
+ ".i3x-export-btn{border:1px solid var(--i3x-border);background:var(--i3x-bg);color:var(--i3x-text-sec);cursor:pointer;font-size:11px;padding:2px 8px;border-radius:4px;transition:all .12s;display:inline-flex;align-items:center;gap:3px}",
299
+ ".i3x-export-btn:hover{border-color:var(--i3x-accent);color:var(--i3x-accent)}",
300
+ ".i3x-export-btn.copied{background:var(--i3x-accent);color:#fff;border-color:var(--i3x-accent)}",
301
+ /* browse button */
302
+ ".i3x-browse-btn{display:inline-flex;align-items:center;gap:5px;padding:4px 12px;border:1px solid var(--i3x-border);border-radius:6px;background:linear-gradient(to bottom,var(--i3x-bg),var(--i3x-bg-alt));color:var(--i3x-text);font-size:12px;cursor:pointer;transition:all .15s;font-family:inherit;margin-left:4px}",
303
+ ".i3x-browse-btn:hover{background:linear-gradient(to bottom,var(--i3x-bg-hover),var(--i3x-bg-grad-end));border-color:var(--i3x-border)}",
304
+ ".i3x-browse-btn.active{border-color:var(--i3x-accent);color:var(--i3x-accent);box-shadow:0 0 0 3px var(--i3x-accent-light)}",
305
+ ".i3x-browse-btn .fa{font-size:11px}",
306
+ ".i3x-btn-badge{background:var(--i3x-accent);color:#fff;font-size:9px;font-weight:700;padding:0 5px;border-radius:8px;min-width:12px;text-align:center;line-height:16px}",
307
+ /* 9. resize handle */
308
+ ".i3x-resize-handle{height:6px;cursor:ns-resize;background:transparent;border-top:1px solid var(--i3x-border-light);display:flex;align-items:center;justify-content:center}",
309
+ ".i3x-resize-handle::after{content:'';width:30px;height:3px;border-top:1px solid var(--i3x-border);border-bottom:1px solid var(--i3x-border)}",
310
+ ".i3x-resize-handle:hover::after{border-color:var(--i3x-accent)}",
311
+ /* live value badge */
312
+ ".i3x-bnode-value{font-size:10px;flex-shrink:0;max-width:120px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:SFMono-Regular,Consolas,monospace;padding:1px 5px;border-radius:3px;background:var(--i3x-bg-alt);border:1px solid var(--i3x-border-light);color:var(--i3x-text-sec);cursor:default;transition:all .2s}",
313
+ ".i3x-bnode-value:hover{max-width:300px}",
314
+ ".i3x-bnode-value.i3x-val-loading{color:var(--i3x-text-muted);border-style:dashed;font-style:italic}",
315
+ ".i3x-bnode-value.i3x-val-error{color:var(--i3x-danger);border-color:var(--i3x-danger);background:rgba(220,38,38,0.05)}",
316
+ /* spinner */
317
+ "@keyframes i3x-spin{to{transform:rotate(360deg)}}",
318
+ ".i3x-spinner{display:inline-block;width:14px;height:14px;border:2px solid var(--i3x-border-light);border-top-color:var(--i3x-accent);border-radius:50%;animation:i3x-spin .6s linear infinite;vertical-align:middle;margin-right:6px}"
319
+ ].join("\n");
320
+ document.head.appendChild(s);
321
+ }
322
+
323
+ function esc(t) { return $("<span>").text(t).html(); }
324
+ function spinner() { return '<span class="i3x-spinner"></span>'; }
325
+
326
+ /**
327
+ * I3XBrowser.create(options)
328
+ * container – CSS selector, where to render the widget
329
+ * serverField – selector for the server config dropdown
330
+ * targetField – selector for the element-ID input
331
+ * mode – "single" | "multi"
332
+ */
333
+ window.I3XBrowser = {
334
+ create: function (opts) {
335
+ injectCSS();
336
+
337
+ var $target = $(opts.targetField);
338
+ var $server = $(opts.serverField);
339
+ var mode = opts.mode || "multi";
340
+ var selected = {}; // elementId -> displayName
341
+ var rendered = {}; // tracks rendered nodes to prevent cycles
342
+ var MAX_DEPTH = 8;
343
+ var collapsed = true;
344
+ var typesLoaded = false;
345
+ var searchMode = false; // true when showing server search results
346
+ var searchTimer = null;
347
+ var breadcrumbPath = []; // [{name, eid}]
348
+ var chkOn = mode === "multi" ? "fa-check-square" : "fa-dot-circle-o";
349
+ var chkOff = mode === "multi" ? "fa-square-o" : "fa-circle-o";
350
+
351
+ // parse initial value
352
+ var init = ($target.val() || "").split(",").map(function (s) { return s.trim(); }).filter(Boolean);
353
+ init.forEach(function (id) { selected[id] = id; });
354
+
355
+ // DOM
356
+ var $wrap = $(opts.container);
357
+ var $btnBadge = $('<span class="i3x-btn-badge" style="display:none"></span>');
358
+ var $btn = $('<button class="i3x-browse-btn" type="button"><i class="fa fa-sitemap"></i> Browse Server</button>');
359
+ $btn.append($btnBadge);
360
+ var $panel = $('<div class="i3x-browser" style="display:none"></div>');
361
+
362
+ // Header
363
+ var $hdr = $('<div class="i3x-browser-hdr"></div>');
364
+ /* 6. Connection status indicator */
365
+ var $statusDot = $('<span class="i3x-status-dot unknown" title="Checking connection..."></span>');
366
+ var $searchWrap = $('<div class="i3x-browser-search-wrap"><i class="fa fa-search"></i></div>');
367
+ var $search = $('<input class="i3x-browser-search" placeholder="Search objects... (server-side for 2+ chars)" type="text">');
368
+ var $searchClear = $('<button class="i3x-search-clear" type="button" title="Clear search"><i class="fa fa-times"></i></button>');
369
+ $searchWrap.append($search, $searchClear);
370
+ var $expandAll = $('<button class="i3x-hdr-btn" type="button" title="Expand all"><i class="fa fa-expand"></i></button>');
371
+ var $collapseAll = $('<button class="i3x-hdr-btn" type="button" title="Collapse all"><i class="fa fa-compress"></i></button>');
372
+ var $refresh = $('<button class="i3x-hdr-btn" type="button" title="Refresh"><i class="fa fa-refresh"></i></button>');
373
+ $hdr.append($statusDot, $searchWrap, $expandAll, $collapseAll, $refresh);
374
+
375
+ /* 8. Breadcrumb bar */
376
+ var $breadcrumb = $('<div class="i3x-breadcrumb"></div>');
377
+
378
+ var $tree = $('<div class="i3x-browser-tree" tabindex="0"></div>');
379
+
380
+ /* Footer with export */
381
+ var $foot = $('<div class="i3x-browser-foot">' +
382
+ '<span class="i3x-foot-badge"><span class="i3x-foot-count zero">0</span> <span class="i3x-foot-label">selected</span></span>' +
383
+ '<span style="display:flex;align-items:center;gap:6px">' +
384
+ '<button class="i3x-export-btn" type="button" title="Copy selected IDs as JSON"><i class="fa fa-clipboard"></i> Export</button>' +
385
+ '<button class="i3x-clear-btn" type="button">Clear all</button></span></div>');
386
+
387
+ /* 9. Resize handle */
388
+ var $resizeHandle = $('<div class="i3x-resize-handle" title="Drag to resize"></div>');
389
+
390
+ $panel.append($hdr, $breadcrumb, $tree, $resizeHandle, $foot);
391
+ $wrap.append($btn, $panel);
392
+
393
+ // ── Toggle panel ──
394
+ $btn.on("click", function () {
395
+ collapsed = !collapsed;
396
+ $panel.toggle(!collapsed);
397
+ $btn.toggleClass("active", !collapsed);
398
+ if (!collapsed && !typesLoaded) loadTypes();
399
+ if (!collapsed) {
400
+ $search.focus();
401
+ checkConnectionStatus();
402
+ }
403
+ });
404
+
405
+ function sid() { return $server.val(); }
406
+
407
+ function syncTarget() {
408
+ var ids = Object.keys(selected);
409
+ $target.val(ids.join(", "));
410
+ var n = ids.length;
411
+ $foot.find(".i3x-foot-count").text(n).toggleClass("zero", n === 0);
412
+ if (n > 0) {
413
+ $btnBadge.text(n).show();
414
+ } else {
415
+ $btnBadge.hide();
416
+ }
417
+ }
418
+
419
+ // ── Live value fetching ──
420
+ function formatValue(val) {
421
+ if (val === null || val === undefined) return "null";
422
+ if (typeof val === "object") {
423
+ // Extract .value if it's a VQT structure
424
+ if (val.value !== undefined) return formatValue(val.value);
425
+ return JSON.stringify(val).substring(0, 40);
426
+ }
427
+ if (typeof val === "number") {
428
+ // Round floats for readability
429
+ return Number.isInteger(val) ? String(val) : val.toFixed(3);
430
+ }
431
+ if (typeof val === "boolean") return val ? "true" : "false";
432
+ return String(val).substring(0, 40);
433
+ }
434
+
435
+ function fetchLiveValues(elementIds) {
436
+ if (!elementIds.length || !sid()) return;
437
+ $.ajax({
438
+ url: "i3x-server/" + encodeURIComponent(sid()) + "/browse/values",
439
+ method: "POST",
440
+ contentType: "application/json",
441
+ data: JSON.stringify({ elementIds: elementIds }),
442
+ })
443
+ .done(function (results) {
444
+ // API returns either:
445
+ // { "eid": { data: [{value, quality, timestamp}] } } (object keyed by elementId)
446
+ // or [{elementId, value, ...}] (array)
447
+ var valMap = {};
448
+ if (Array.isArray(results)) {
449
+ results.forEach(function (item) {
450
+ var eid = item.elementId || item.id;
451
+ if (eid) valMap[eid] = item;
452
+ });
453
+ } else if (results && typeof results === "object") {
454
+ Object.keys(results).forEach(function (eid) {
455
+ var entry = results[eid];
456
+ // Unwrap { data: [...] } structure
457
+ if (entry && Array.isArray(entry.data) && entry.data.length > 0) {
458
+ valMap[eid] = entry.data[0]; // latest value
459
+ } else if (entry && entry.value !== undefined) {
460
+ valMap[eid] = entry;
461
+ }
462
+ });
463
+ }
464
+ elementIds.forEach(function (eid) {
465
+ var $val = $tree.find('[data-val-eid="' + eid.replace(/"/g, '\\"') + '"]');
466
+ if (!$val.length) return;
467
+ var item = valMap[eid];
468
+ if (item && item.value !== undefined) {
469
+ var display = formatValue(item.value);
470
+ var parts = [display];
471
+ if (item.quality) parts.push("Q: " + item.quality);
472
+ if (item.timestamp) parts.push(new Date(item.timestamp).toLocaleString());
473
+ $val.removeClass("i3x-val-loading i3x-val-error")
474
+ .text(display)
475
+ .attr("title", parts.join(" | "));
476
+ } else {
477
+ $val.removeClass("i3x-val-loading").text("–").attr("title", "No value");
478
+ }
479
+ });
480
+ })
481
+ .fail(function () {
482
+ elementIds.forEach(function (eid) {
483
+ var $val = $tree.find('[data-val-eid="' + eid.replace(/"/g, '\\"') + '"]');
484
+ $val.removeClass("i3x-val-loading").addClass("i3x-val-error").text("–");
485
+ });
486
+ });
487
+ }
488
+
489
+ // ── 6. Connection status check ──
490
+ function checkConnectionStatus() {
491
+ var s = sid();
492
+ if (!s) {
493
+ $statusDot.attr("class", "i3x-status-dot unknown").attr("title", "No server selected");
494
+ return;
495
+ }
496
+ $.getJSON("i3x-server/" + encodeURIComponent(s) + "/status")
497
+ .done(function (data) {
498
+ if (data.connected) {
499
+ $statusDot.attr("class", "i3x-status-dot connected").attr("title", "Connected");
500
+ } else {
501
+ $statusDot.attr("class", "i3x-status-dot disconnected").attr("title", "Disconnected");
502
+ }
503
+ })
504
+ .fail(function () {
505
+ $statusDot.attr("class", "i3x-status-dot disconnected").attr("title", "Server not deployed");
506
+ });
507
+ }
508
+
509
+ // ── 8. Breadcrumb management ──
510
+ function updateBreadcrumb(path) {
511
+ breadcrumbPath = path || [];
512
+ if (!breadcrumbPath.length) {
513
+ $breadcrumb.hide();
514
+ return;
515
+ }
516
+ $breadcrumb.empty().css("display", "flex");
517
+ var $root = $('<span class="i3x-bc-item">Root</span>');
518
+ $root.on("click", function () {
519
+ updateBreadcrumb([]);
520
+ if (typesLoaded) {
521
+ // re-show the type tree at root level
522
+ $tree.children(".i3x-bnode").show();
523
+ $tree.find(".i3x-bnode-children").removeClass("open");
524
+ $tree.find(".i3x-bnode-toggle .fa").removeClass("fa-caret-down").addClass("fa-caret-right");
525
+ }
526
+ });
527
+ $breadcrumb.append($root);
528
+ breadcrumbPath.forEach(function (item, idx) {
529
+ $breadcrumb.append('<span class="i3x-bc-sep"><i class="fa fa-angle-right"></i></span>');
530
+ if (idx < breadcrumbPath.length - 1) {
531
+ var $link = $('<span class="i3x-bc-item"></span>').text(item.name);
532
+ $link.on("click", function () {
533
+ updateBreadcrumb(breadcrumbPath.slice(0, idx + 1));
534
+ });
535
+ $breadcrumb.append($link);
536
+ } else {
537
+ $breadcrumb.append($('<span class="i3x-bc-current"></span>').text(item.name));
538
+ }
539
+ });
540
+ }
541
+
542
+ // ── Cascade select/deselect on all loaded children ──
543
+ function setNodeSelected($row, eid, name, doSelect) {
544
+ var $chk = $row.find("." + chkOn + ",." + chkOff);
545
+ if (doSelect) {
546
+ selected[eid] = name;
547
+ $row.addClass("sel");
548
+ $chk.removeClass(chkOff).addClass(chkOn);
549
+ } else {
550
+ delete selected[eid];
551
+ $row.removeClass("sel");
552
+ $chk.removeClass(chkOn).addClass(chkOff);
553
+ }
554
+ }
555
+
556
+ function cascadeSelect($parentNode, doSelect) {
557
+ $parentNode.find("> .i3x-bnode-children .i3x-bnode-row[data-eid]").each(function () {
558
+ var $childRow = $(this);
559
+ var ceid = $childRow.attr("data-eid");
560
+ var cname = $childRow.find(".i3x-bnode-name").text();
561
+ setNodeSelected($childRow, ceid, cname, doSelect);
562
+ });
563
+ }
564
+
565
+ // ── Build a tree node ──
566
+ function makeNode(eid, name, depth, icon, typeName) {
567
+ var pad = 10 + depth * 20;
568
+ var isSel = !!selected[eid];
569
+ /* 7. pick icon by type name */
570
+ var nodeIcon = icon || guessIcon(typeName);
571
+ var $node = $('<div class="i3x-bnode"></div>');
572
+ var $row = $('<div class="i3x-bnode-row' + (isSel ? " sel" : "") + '" data-eid="' + esc(eid) + '" style="padding-left:' + pad + 'px" title="' + esc(eid) + '">' +
573
+ '<span class="i3x-bnode-toggle"><i class="fa fa-caret-right"></i></span>' +
574
+ '<i class="fa ' + nodeIcon + ' i3x-bnode-icon"></i>' +
575
+ '<i class="fa ' + (isSel ? chkOn : chkOff) + ' i3x-bnode-chk"></i>' +
576
+ '<span class="i3x-bnode-name">' + esc(name) + '</span>' +
577
+ '<span class="i3x-bnode-value i3x-val-loading" data-val-eid="' + esc(eid) + '"></span>' +
578
+ '<span class="i3x-bnode-id">' + esc(eid) + '</span></div>');
579
+ var $children = $('<div class="i3x-bnode-children"></div>');
580
+ var childrenLoaded = false;
581
+ $node.append($row, $children);
582
+
583
+ // expand/collapse toggle + breadcrumb update
584
+ $row.find(".i3x-bnode-toggle").on("click", function (e) {
585
+ e.stopPropagation();
586
+ var isOpen = $children.hasClass("open");
587
+ $children.toggleClass("open", !isOpen);
588
+ $(this).find(".fa").toggleClass("fa-caret-right", isOpen).toggleClass("fa-caret-down", !isOpen);
589
+ if (!isOpen && !childrenLoaded) {
590
+ childrenLoaded = true;
591
+ loadChildren(eid, $children, depth + 1, typeName);
592
+ }
593
+ // update breadcrumb
594
+ if (!isOpen) {
595
+ var path = [];
596
+ var $cur = $row;
597
+ while ($cur.length) {
598
+ var curEid = $cur.attr("data-eid");
599
+ var curName = $cur.find(".i3x-bnode-name").first().text();
600
+ if (curEid) {
601
+ path.unshift({ name: curName, eid: curEid });
602
+ }
603
+ $cur = $cur.closest(".i3x-bnode").parent().closest(".i3x-bnode").children(".i3x-bnode-row");
604
+ }
605
+ updateBreadcrumb(path);
606
+ }
607
+ });
608
+
609
+ // select/deselect
610
+ $row.on("click", function (e) {
611
+ if ($(e.target).closest(".i3x-bnode-toggle").length) return;
612
+ var willSelect = !selected[eid];
613
+ if (mode === "single") {
614
+ selected = {};
615
+ $tree.find(".i3x-bnode-row").removeClass("sel")
616
+ .find("." + chkOn).removeClass(chkOn).addClass(chkOff);
617
+ }
618
+ setNodeSelected($row, eid, name, willSelect);
619
+ if (mode === "multi") {
620
+ cascadeSelect($node, willSelect);
621
+ }
622
+ syncTarget();
623
+ });
624
+
625
+ return $node;
626
+ }
627
+
628
+ function isParentSelected(parentEid) {
629
+ return !!selected[parentEid];
630
+ }
631
+
632
+ // ── Load children (related objects) ──
633
+ function loadChildren(parentEid, $container, depth, parentTypeName) {
634
+ var pad = 10 + depth * 20;
635
+ if (depth > MAX_DEPTH) {
636
+ $container.html('<div class="i3x-browser-msg i3x-msg-inline" style="padding-left:' + pad + 'px;color:var(--i3x-warn)">Max depth reached</div>');
637
+ return;
638
+ }
639
+ $container.html('<div class="i3x-browser-msg i3x-msg-inline" style="padding-left:' + pad + 'px">' + spinner() + 'Loading...</div>');
640
+ $.getJSON("i3x-server/" + encodeURIComponent(sid()) + "/browse/related/" + encodeURIComponent(parentEid))
641
+ .done(function (items) {
642
+ $container.empty();
643
+ items = Array.isArray(items) ? items : [];
644
+ items = items.filter(function (obj) {
645
+ var eid = obj.elementId || obj.id;
646
+ return eid !== parentEid && !rendered[eid];
647
+ });
648
+ if (!items.length) {
649
+ $container.html('<div class="i3x-browser-msg i3x-msg-inline" style="padding-left:' + pad + 'px">No children</div>');
650
+ return;
651
+ }
652
+ var parentSel = mode === "multi" && isParentSelected(parentEid);
653
+ items.sort(function (a, b) { return (a.displayName || "").localeCompare(b.displayName || ""); });
654
+ var eids = [];
655
+ items.forEach(function (obj) {
656
+ var eid = obj.elementId || obj.id;
657
+ var name = obj.displayName || eid;
658
+ var tName = obj.typeName || parentTypeName || "";
659
+ rendered[eid] = true;
660
+ eids.push(eid);
661
+ if (parentSel) { selected[eid] = name; }
662
+ $container.append(makeNode(eid, name, depth, null, tName));
663
+ });
664
+ if (eids.length) fetchLiveValues(eids);
665
+ if (parentSel) { syncTarget(); }
666
+ })
667
+ .fail(function () {
668
+ $container.html('<div class="i3x-browser-msg i3x-msg-inline" style="padding-left:' + pad + 'px;color:var(--i3x-danger)"><i class="fa fa-exclamation-circle"></i> Failed to load</div>');
669
+ });
670
+ }
671
+
672
+ // ── Load top-level: object types ──
673
+ function loadTypes() {
674
+ var s = sid();
675
+ if (!s) {
676
+ $tree.html('<div class="i3x-browser-msg"><i class="fa fa-info-circle"></i><br>Select and deploy a server configuration first</div>');
677
+ return;
678
+ }
679
+ $tree.html('<div class="i3x-browser-msg">' + spinner() + 'Connecting to server...</div>');
680
+ $.getJSON("i3x-server/" + encodeURIComponent(s) + "/browse/objecttypes")
681
+ .done(function (types) {
682
+ typesLoaded = true;
683
+ searchMode = false;
684
+ renderTypes(Array.isArray(types) ? types : []);
685
+ checkConnectionStatus();
686
+ })
687
+ .fail(function (xhr) {
688
+ var msg = (xhr.responseJSON && xhr.responseJSON.error) || "Connection failed";
689
+ $tree.html('<div class="i3x-browser-msg"><i class="fa fa-exclamation-triangle" style="color:var(--i3x-warn)"></i><br>' + esc(msg) + '</div>');
690
+ checkConnectionStatus();
691
+ });
692
+ }
693
+
694
+ function renderTypes(types) {
695
+ $tree.empty();
696
+ updateBreadcrumb([]);
697
+ if (!types.length) {
698
+ $tree.html('<div class="i3x-browser-msg">No object types found on this server</div>');
699
+ return;
700
+ }
701
+ types.sort(function (a, b) { return (a.displayName || "").localeCompare(b.displayName || ""); });
702
+ types.forEach(function (type) {
703
+ var tid = type.elementId || type.id;
704
+ var tname = type.displayName || tid;
705
+ /* 7. pick icon for type category */
706
+ var typeIcon = guessIcon(tname);
707
+ var $typeNode = $('<div class="i3x-bnode"></div>');
708
+ var $countSpan = $('<span class="i3x-bnode-count"></span>');
709
+ var $trow = $('<div class="i3x-bnode-row" style="padding-left:10px">' +
710
+ '<span class="i3x-bnode-toggle"><i class="fa fa-caret-right"></i></span>' +
711
+ '<i class="fa ' + typeIcon + ' i3x-bnode-icon"></i>' +
712
+ '<span class="i3x-bnode-name">' + esc(tname) + '</span></div>');
713
+ $trow.append($countSpan);
714
+ var $objs = $('<div class="i3x-bnode-children"></div>');
715
+ var loaded = false;
716
+ $typeNode.append($trow, $objs);
717
+
718
+ $trow.on("click", function () {
719
+ var isOpen = $objs.hasClass("open");
720
+ $objs.toggleClass("open", !isOpen);
721
+ $trow.find(".i3x-bnode-toggle .fa")
722
+ .toggleClass("fa-caret-right", isOpen)
723
+ .toggleClass("fa-caret-down", !isOpen);
724
+ if (!isOpen && !loaded) {
725
+ loaded = true;
726
+ loadObjects(tid, tname, $objs, $countSpan);
727
+ }
728
+ });
729
+ $tree.append($typeNode);
730
+ });
731
+ }
732
+
733
+ // ── Load objects for a type ──
734
+ function loadObjects(typeId, typeName, $container, $countSpan) {
735
+ $container.html('<div class="i3x-browser-msg i3x-msg-inline" style="padding-left:30px">' + spinner() + 'Loading...</div>');
736
+ $.getJSON("i3x-server/" + encodeURIComponent(sid()) + "/browse/objects", { typeId: typeId })
737
+ .done(function (objects) {
738
+ $container.empty();
739
+ objects = Array.isArray(objects) ? objects : [];
740
+ if ($countSpan) $countSpan.text("(" + objects.length + ")");
741
+ if (!objects.length) {
742
+ $container.html('<div class="i3x-browser-msg i3x-msg-inline" style="padding-left:30px">No objects</div>');
743
+ return;
744
+ }
745
+ objects.sort(function (a, b) { return (a.displayName || "").localeCompare(b.displayName || ""); });
746
+ var eids = [];
747
+ objects.forEach(function (obj) {
748
+ var eid = obj.elementId || obj.id;
749
+ var name = obj.displayName || eid;
750
+ rendered[eid] = true;
751
+ eids.push(eid);
752
+ $container.append(makeNode(eid, name, 1, null, typeName));
753
+ });
754
+ if (eids.length) fetchLiveValues(eids);
755
+ })
756
+ .fail(function () {
757
+ $container.html('<div class="i3x-browser-msg i3x-msg-inline" style="padding-left:30px;color:var(--i3x-danger)"><i class="fa fa-exclamation-circle"></i> Failed to load</div>');
758
+ });
759
+ }
760
+
761
+ // ── 5. Server-side search with debounce ──
762
+ function serverSearch(q) {
763
+ $tree.html('<div class="i3x-browser-msg">' + spinner() + 'Searching server...</div>');
764
+ $.getJSON("i3x-server/" + encodeURIComponent(sid()) + "/browse/search", { q: q })
765
+ .done(function (results) {
766
+ searchMode = true;
767
+ $tree.empty();
768
+ updateBreadcrumb([{ name: "Search: " + q }]);
769
+ results = Array.isArray(results) ? results : [];
770
+ if (!results.length) {
771
+ $tree.html('<div class="i3x-browser-msg"><i class="fa fa-search"></i><br>No results for "' + esc(q) + '"</div>');
772
+ return;
773
+ }
774
+ var $container = $('<div class="i3x-search-results"></div>');
775
+ var eids = [];
776
+ results.forEach(function (obj) {
777
+ var eid = obj.elementId || obj.id;
778
+ var name = obj.displayName || eid;
779
+ var tName = obj.typeName || "";
780
+ eids.push(eid);
781
+ var $node = makeNode(eid, name, 0, null, tName);
782
+ // add type label
783
+ if (tName) {
784
+ $node.find(".i3x-bnode-row").first().find(".i3x-bnode-id").before(
785
+ '<span class="i3x-sr-type">' + esc(tName) + '</span> '
786
+ );
787
+ }
788
+ $container.append($node);
789
+ });
790
+ $tree.append($container);
791
+ if (eids.length) fetchLiveValues(eids);
792
+ })
793
+ .fail(function () {
794
+ $tree.html('<div class="i3x-browser-msg" style="color:var(--i3x-danger)"><i class="fa fa-exclamation-circle"></i><br>Search failed</div>');
795
+ });
796
+ }
797
+
798
+ // ── Search: local filter for loaded items + server-side for 2+ chars ──
799
+ $search.on("input", function () {
800
+ var q = $search.val().trim();
801
+ var qLower = q.toLowerCase();
802
+ $searchClear.toggle(q.length > 0);
803
+
804
+ if (searchTimer) { clearTimeout(searchTimer); searchTimer = null; }
805
+
806
+ if (!q) {
807
+ // restore tree view
808
+ if (searchMode) {
809
+ searchMode = false;
810
+ typesLoaded = false;
811
+ rendered = {};
812
+ loadTypes();
813
+ } else {
814
+ // just reset local filter
815
+ $tree.find(".i3x-bnode").show();
816
+ $tree.find(".i3x-bnode-row[data-eid]").each(function () {
817
+ $(this).closest(".i3x-bnode").show();
818
+ });
819
+ $tree.children(".i3x-bnode").show();
820
+ }
821
+ updateBreadcrumb([]);
822
+ return;
823
+ }
824
+
825
+ // immediate local filter on loaded items
826
+ $tree.find(".i3x-bnode-row[data-eid]").each(function () {
827
+ var $el = $(this);
828
+ var vis = !qLower ||
829
+ $el.find(".i3x-bnode-name").text().toLowerCase().indexOf(qLower) !== -1 ||
830
+ $el.find(".i3x-bnode-id").text().toLowerCase().indexOf(qLower) !== -1;
831
+ $el.closest(".i3x-bnode").toggle(vis);
832
+ });
833
+ $tree.children(".i3x-bnode").each(function () {
834
+ var $tn = $(this);
835
+ var $childContainer = $tn.children(".i3x-bnode-children");
836
+ if (qLower) {
837
+ $childContainer.addClass("open");
838
+ $tn.find(".i3x-bnode-toggle .fa").removeClass("fa-caret-right").addClass("fa-caret-down");
839
+ var anyVisible = $childContainer.find(".i3x-bnode:visible").length > 0;
840
+ $tn.toggle(anyVisible);
841
+ } else {
842
+ $tn.show();
843
+ }
844
+ });
845
+
846
+ // debounced server-side search for 2+ chars
847
+ if (q.length >= 2) {
848
+ searchTimer = setTimeout(function () {
849
+ serverSearch(q);
850
+ }, 400);
851
+ }
852
+ });
853
+
854
+ $searchClear.on("click", function () {
855
+ $search.val("").trigger("input").focus();
856
+ });
857
+
858
+ // expand all loaded nodes
859
+ $expandAll.on("click", function () {
860
+ $tree.find(".i3x-bnode-children").addClass("open");
861
+ $tree.find(".i3x-bnode-toggle .fa").removeClass("fa-caret-right").addClass("fa-caret-down");
862
+ });
863
+
864
+ // collapse all
865
+ $collapseAll.on("click", function () {
866
+ $tree.find(".i3x-bnode-children").removeClass("open");
867
+ $tree.find(".i3x-bnode-toggle .fa").removeClass("fa-caret-down").addClass("fa-caret-right");
868
+ updateBreadcrumb([]);
869
+ });
870
+
871
+ $refresh.on("click", function () {
872
+ typesLoaded = false;
873
+ searchMode = false;
874
+ rendered = {};
875
+ $search.val("");
876
+ $searchClear.hide();
877
+ loadTypes();
878
+ });
879
+
880
+ $foot.find(".i3x-clear-btn").on("click", function (e) {
881
+ e.preventDefault();
882
+ selected = {};
883
+ $tree.find(".i3x-bnode-row").removeClass("sel")
884
+ .find("." + chkOn).removeClass(chkOn).addClass(chkOff);
885
+ syncTarget();
886
+ });
887
+
888
+ // ── 11. Export selection as JSON ──
889
+ $foot.find(".i3x-export-btn").on("click", function () {
890
+ var ids = Object.keys(selected);
891
+ if (!ids.length) return;
892
+ var json = JSON.stringify(ids, null, 2);
893
+ var $exportBtn = $(this);
894
+ if (navigator.clipboard && navigator.clipboard.writeText) {
895
+ navigator.clipboard.writeText(json).then(function () {
896
+ $exportBtn.addClass("copied").find("i").removeClass("fa-clipboard").addClass("fa-check");
897
+ setTimeout(function () {
898
+ $exportBtn.removeClass("copied").find("i").removeClass("fa-check").addClass("fa-clipboard");
899
+ }, 1500);
900
+ });
901
+ } else {
902
+ // fallback: textarea copy
903
+ var $ta = $('<textarea style="position:fixed;left:-9999px"></textarea>').val(json).appendTo("body");
904
+ $ta[0].select();
905
+ document.execCommand("copy");
906
+ $ta.remove();
907
+ $exportBtn.addClass("copied").find("i").removeClass("fa-clipboard").addClass("fa-check");
908
+ setTimeout(function () {
909
+ $exportBtn.removeClass("copied").find("i").removeClass("fa-check").addClass("fa-clipboard");
910
+ }, 1500);
911
+ }
912
+ });
913
+
914
+ // ── 9. Resize handle: drag to change tree height ──
915
+ (function () {
916
+ var startY, startH;
917
+ $resizeHandle.on("mousedown", function (e) {
918
+ e.preventDefault();
919
+ startY = e.clientY;
920
+ startH = $tree.outerHeight();
921
+ $(document).on("mousemove.i3xresize", function (e2) {
922
+ var newH = Math.max(120, Math.min(800, startH + (e2.clientY - startY)));
923
+ $tree.css("max-height", newH + "px");
924
+ });
925
+ $(document).on("mouseup.i3xresize", function () {
926
+ $(document).off(".i3xresize");
927
+ });
928
+ });
929
+ })();
930
+
931
+ // ── 10. Keyboard navigation ──
932
+ $tree.on("keydown", function (e) {
933
+ var $rows = $tree.find(".i3x-bnode-row:visible");
934
+ if (!$rows.length) return;
935
+ var $focused = $tree.find(".i3x-bnode-row.i3x-kb-focus");
936
+ var idx = $focused.length ? $rows.index($focused) : -1;
937
+
938
+ switch (e.key) {
939
+ case "ArrowDown":
940
+ e.preventDefault();
941
+ $rows.removeClass("i3x-kb-focus");
942
+ idx = Math.min(idx + 1, $rows.length - 1);
943
+ $rows.eq(idx).addClass("i3x-kb-focus");
944
+ scrollIntoViewIfNeeded($rows.eq(idx));
945
+ break;
946
+ case "ArrowUp":
947
+ e.preventDefault();
948
+ $rows.removeClass("i3x-kb-focus");
949
+ idx = Math.max(idx - 1, 0);
950
+ $rows.eq(idx).addClass("i3x-kb-focus");
951
+ scrollIntoViewIfNeeded($rows.eq(idx));
952
+ break;
953
+ case " ":
954
+ e.preventDefault();
955
+ if ($focused.length) $focused.trigger("click");
956
+ break;
957
+ case "Enter":
958
+ e.preventDefault();
959
+ if ($focused.length) {
960
+ $focused.find(".i3x-bnode-toggle").trigger("click");
961
+ }
962
+ break;
963
+ case "ArrowRight":
964
+ e.preventDefault();
965
+ if ($focused.length) {
966
+ var $ch = $focused.closest(".i3x-bnode").children(".i3x-bnode-children");
967
+ if (!$ch.hasClass("open")) {
968
+ $focused.find(".i3x-bnode-toggle").trigger("click");
969
+ }
970
+ }
971
+ break;
972
+ case "ArrowLeft":
973
+ e.preventDefault();
974
+ if ($focused.length) {
975
+ var $ch2 = $focused.closest(".i3x-bnode").children(".i3x-bnode-children");
976
+ if ($ch2.hasClass("open")) {
977
+ $focused.find(".i3x-bnode-toggle").trigger("click");
978
+ } else {
979
+ // jump to parent row
980
+ var $parentRow = $focused.closest(".i3x-bnode").parent().closest(".i3x-bnode").children(".i3x-bnode-row");
981
+ if ($parentRow.length) {
982
+ $rows.removeClass("i3x-kb-focus");
983
+ $parentRow.addClass("i3x-kb-focus");
984
+ scrollIntoViewIfNeeded($parentRow);
985
+ }
986
+ }
987
+ }
988
+ break;
989
+ }
990
+ });
991
+
992
+ function scrollIntoViewIfNeeded($el) {
993
+ if (!$el.length) return;
994
+ var el = $el[0];
995
+ var container = $tree[0];
996
+ var elTop = el.offsetTop;
997
+ var elBot = elTop + el.offsetHeight;
998
+ if (elTop < container.scrollTop) {
999
+ container.scrollTop = elTop;
1000
+ } else if (elBot > container.scrollTop + container.clientHeight) {
1001
+ container.scrollTop = elBot - container.clientHeight;
1002
+ }
1003
+ }
1004
+
1005
+ $server.on("change", function () {
1006
+ typesLoaded = false;
1007
+ searchMode = false;
1008
+ rendered = {};
1009
+ if (!collapsed) {
1010
+ loadTypes();
1011
+ checkConnectionStatus();
1012
+ }
1013
+ });
1014
+
1015
+ syncTarget();
1016
+
1017
+ return {
1018
+ getSelected: function () { return Object.keys(selected); },
1019
+ destroy: function () { $btn.remove(); $panel.remove(); }
1020
+ };
1021
+ }
1022
+ };
1023
+ })();
98
1024
  </script>