node-red-contrib-i3x 0.0.1 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +14 -0
- package/CHANGELOG.md +51 -2
- package/README.md +19 -2
- package/lib/i3x-client.js +132 -9
- package/lib/node-utils.js +27 -1
- package/nodes/i3x-browse.html +13 -0
- package/nodes/i3x-browse.js +14 -19
- package/nodes/i3x-history.html +14 -0
- package/nodes/i3x-history.js +6 -6
- package/nodes/i3x-read.html +15 -0
- package/nodes/i3x-read.js +6 -6
- package/nodes/i3x-server.html +926 -0
- package/nodes/i3x-server.js +122 -0
- package/nodes/i3x-subscribe.html +14 -1
- package/nodes/i3x-subscribe.js +9 -6
- package/nodes/i3x-write.html +15 -2
- package/nodes/i3x-write.js +3 -3
- package/package.json +3 -3
package/nodes/i3x-server.html
CHANGED
|
@@ -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> </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>
|