@yuneta/gobj-ui 7.23.28 → 7.23.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +163 -2
- package/dist/gobj-ui.cjs.js +1278 -202
- package/dist/gobj-ui.es.js +1279 -203
- package/package.json +1 -1
- package/src/c_g6_nodes_tree.js +40 -10
- package/src/c_yui_gobj_tree_js.js +630 -245
- package/src/c_yui_json_graph.js +245 -4
- package/src/gclass_describe.js +234 -0
- package/src/gclass_describe.test.js +125 -0
- package/src/gobj_tree_model.js +436 -0
- package/src/gobj_tree_model.test.js +184 -0
- package/src/lib_graph.css +38 -2
- package/src/yui_gclass_view.js +277 -0
- package/src/yui_graph_camera.js +60 -0
|
@@ -22,24 +22,37 @@ import {
|
|
|
22
22
|
gobj_write_attr,
|
|
23
23
|
gobj_send_event,
|
|
24
24
|
gobj_post_event,
|
|
25
|
+
kw_get_local_storage_value,
|
|
26
|
+
kw_set_local_storage_value,
|
|
25
27
|
gobj_write_str_attr,
|
|
26
28
|
gobj_read_str_attr,
|
|
27
|
-
gobj_publish_event,
|
|
28
29
|
gobj_yuno,
|
|
29
|
-
|
|
30
|
+
gobj_yuno_name,
|
|
30
31
|
gobj_short_name,
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
gobj_is_running,
|
|
34
|
-
gobj_is_playing,
|
|
35
|
-
gobj_is_service,
|
|
36
|
-
gobj_is_pure_child,
|
|
37
|
-
gobj_is_volatil,
|
|
32
|
+
gobj_is_destroying,
|
|
33
|
+
gobj_unsubscribe_event,
|
|
38
34
|
createElement2,
|
|
39
35
|
escapeHtml,
|
|
36
|
+
log_warning,
|
|
40
37
|
refresh_language,
|
|
41
38
|
} from "@yuneta/gobj-js";
|
|
42
39
|
|
|
40
|
+
import {
|
|
41
|
+
describe_js_gobj,
|
|
42
|
+
node_badge_keys,
|
|
43
|
+
node_info_rows,
|
|
44
|
+
node_labels,
|
|
45
|
+
node_role,
|
|
46
|
+
node_status,
|
|
47
|
+
node_status_key,
|
|
48
|
+
} from "./gobj_tree_model.js";
|
|
49
|
+
import {
|
|
50
|
+
close_gclass_view,
|
|
51
|
+
gclass_view_available,
|
|
52
|
+
open_gclass_view,
|
|
53
|
+
} from "./yui_gclass_view.js";
|
|
54
|
+
import {yui_shell_of} from "./c_yui_shell.js";
|
|
55
|
+
|
|
43
56
|
import {yui_toolbar} from "./yui_toolbar.js";
|
|
44
57
|
import {
|
|
45
58
|
yui_graph_camera_items,
|
|
@@ -120,6 +133,123 @@ const ZOOM_CANVAS = {type: 'zoom-canvas', animation: false};
|
|
|
120
133
|
const BEHAVIORS_READING = ['drag-canvas', ZOOM_CANVAS, 'drag-element'];
|
|
121
134
|
const BEHAVIORS_PICKING = ['drag-canvas', ZOOM_CANVAS];
|
|
122
135
|
|
|
136
|
+
/*
|
|
137
|
+
* Where this view is remembered between visits.
|
|
138
|
+
*
|
|
139
|
+
* In localStorage and not in a persistent attr, for the reason the
|
|
140
|
+
* framework gives itself: only a SERVICE can load or save persistent
|
|
141
|
+
* attrs, and this gclass is hosted as a child. It is also the right
|
|
142
|
+
* place on its own terms -- how somebody left THEIR tree arranged is
|
|
143
|
+
* a fact about that browser, not about the yuno.
|
|
144
|
+
*
|
|
145
|
+
* Keyed by the gobj's name so two trees in one app do not overwrite
|
|
146
|
+
* each other's arrangement.
|
|
147
|
+
*/
|
|
148
|
+
const VIEW_STORE_PREFIX = "yui_gobj_tree_view:";
|
|
149
|
+
|
|
150
|
+
/* What is worth carrying back: the layout, where the camera was, and
|
|
151
|
+
* which branches were folded. The anchor goes too -- it is a camera
|
|
152
|
+
* state like the other two, and coming back to a tree still holding
|
|
153
|
+
* the node you left it on is the point of holding it. */
|
|
154
|
+
function view_store_key(gobj)
|
|
155
|
+
{
|
|
156
|
+
return VIEW_STORE_PREFIX + gobj_name(gobj);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function restore_view_shape(gobj)
|
|
160
|
+
{
|
|
161
|
+
let priv = gobj.priv;
|
|
162
|
+
let state = load_view_state(gobj);
|
|
163
|
+
|
|
164
|
+
if(!state) {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
if(state.layout && LAYOUTS[state.layout]) {
|
|
168
|
+
gobj_write_str_attr(gobj, "layout", state.layout);
|
|
169
|
+
/* And the picker with it: the toolbar was built from the
|
|
170
|
+
* attr's default a moment ago, so without this the graph draws
|
|
171
|
+
* the remembered layout while the control names another. */
|
|
172
|
+
if(priv.$layout_select) {
|
|
173
|
+
priv.$layout_select.value = state.layout;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if(state.collapsed && typeof state.collapsed === "object") {
|
|
177
|
+
priv.collapse_state = Object.assign({}, state.collapsed);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function restore_view_state(gobj)
|
|
182
|
+
{
|
|
183
|
+
let priv = gobj.priv;
|
|
184
|
+
let graph = priv.graph;
|
|
185
|
+
let state = load_view_state(gobj);
|
|
186
|
+
|
|
187
|
+
if(!graph || !state) {
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
let done = false;
|
|
192
|
+
priv.restoring = true;
|
|
193
|
+
try {
|
|
194
|
+
if(typeof state.zoom === "number" && state.zoom > 0) {
|
|
195
|
+
graph.zoomTo(state.zoom);
|
|
196
|
+
priv.last_zoom = state.zoom;
|
|
197
|
+
done = true;
|
|
198
|
+
}
|
|
199
|
+
if(Array.isArray(state.position) && state.position.length >= 2) {
|
|
200
|
+
graph.translateTo([state.position[0], state.position[1]]);
|
|
201
|
+
done = true;
|
|
202
|
+
}
|
|
203
|
+
if(state.anchor) {
|
|
204
|
+
/* By NAME, never by node id: ids are generated per build,
|
|
205
|
+
* and this one was written in another session. */
|
|
206
|
+
priv.anchor_name = state.anchor;
|
|
207
|
+
priv.anchor_state = "on";
|
|
208
|
+
reanchor(gobj);
|
|
209
|
+
yui_graph_update_anchor(gobj_read_attr(gobj, "$container"), priv.anchor_state);
|
|
210
|
+
paint_anchor_mark(gobj);
|
|
211
|
+
}
|
|
212
|
+
} catch(e) {
|
|
213
|
+
log_error(`${gobj_short_name(gobj)}: cannot restore the saved view: ${e}`);
|
|
214
|
+
} finally {
|
|
215
|
+
priv.restoring = false;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
yui_graph_update_zoom(gobj_read_attr(gobj, "$container"), priv.graph);
|
|
219
|
+
return done;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function save_view_state(gobj)
|
|
223
|
+
{
|
|
224
|
+
let priv = gobj.priv;
|
|
225
|
+
let graph = priv.graph;
|
|
226
|
+
|
|
227
|
+
if(!graph || priv.restoring) {
|
|
228
|
+
return; /* mid-restore: that is our own writing coming back */
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
let state = {
|
|
232
|
+
layout: gobj_read_str_attr(gobj, "layout"),
|
|
233
|
+
collapsed: priv.collapse_state || {},
|
|
234
|
+
anchor: (priv.anchor_state === "on")? priv.anchor_name: "",
|
|
235
|
+
};
|
|
236
|
+
try {
|
|
237
|
+
state.zoom = graph.getZoom();
|
|
238
|
+
state.position = graph.getPosition();
|
|
239
|
+
} catch(e) {
|
|
240
|
+
/* between renders: keep the rest, the camera is not readable */
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
kw_set_local_storage_value(view_store_key(gobj), state);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function load_view_state(gobj)
|
|
247
|
+
{
|
|
248
|
+
let state = kw_get_local_storage_value(view_store_key(gobj), null, false);
|
|
249
|
+
|
|
250
|
+
return (state && typeof state === "object")? state: null;
|
|
251
|
+
}
|
|
252
|
+
|
|
123
253
|
let PRIVATE_DATA = {
|
|
124
254
|
canvas_id: "",
|
|
125
255
|
graph: null,
|
|
@@ -130,6 +260,9 @@ let PRIVATE_DATA = {
|
|
|
130
260
|
$popover: null,
|
|
131
261
|
$popover_title: null,
|
|
132
262
|
$popover_body: null,
|
|
263
|
+
$popover_gclass: null, // the "gclass" button, or null when unavailable
|
|
264
|
+
popover_node: null, // the descriptor the popover is showing
|
|
265
|
+
gclass_view: null, // handle of the open gclass window (or null)
|
|
133
266
|
node_by_id: null, // map of node_id -> node_data (for popover)
|
|
134
267
|
collapse_state: null, // map of full_name -> "collapsed" | "expanded"
|
|
135
268
|
resize_observer: null, // ResizeObserver on the canvas mount element
|
|
@@ -146,6 +279,8 @@ let PRIVATE_DATA = {
|
|
|
146
279
|
anchor_id: "", // the node the camera keeps in the middle
|
|
147
280
|
anchor_name: "", // its full_name, which survives a rebuild
|
|
148
281
|
anchor_state: "off", // "off" | "arming" | "on"
|
|
282
|
+
restoring: false, // guards the store against its own restore
|
|
283
|
+
restored: false, // the saved arrangement is applied ONCE
|
|
149
284
|
last_zoom: 1, // tells a wheel zoom from a drag in aftertransform
|
|
150
285
|
};
|
|
151
286
|
|
|
@@ -153,20 +288,46 @@ let __gclass__ = null;
|
|
|
153
288
|
let __layout_registered__ = false;
|
|
154
289
|
|
|
155
290
|
/***************************************************************
|
|
156
|
-
* Node
|
|
291
|
+
* Node colours by role.
|
|
292
|
+
*
|
|
293
|
+
* Saturated hues, one family apart from the next, and the same
|
|
294
|
+
* five everywhere the reader meets a gobj. `stroke` draws the
|
|
295
|
+
* border and the edge; `accent` is the brighter sibling used
|
|
296
|
+
* for the bar down the left of the card and the toggle badge,
|
|
297
|
+
* so a card carries its colour twice at two intensities and is
|
|
298
|
+
* readable at the zoom where the text is not.
|
|
157
299
|
***************************************************************/
|
|
158
300
|
const ROLE_COLORS = {
|
|
159
|
-
yuno: {
|
|
160
|
-
service: {
|
|
161
|
-
pure: {
|
|
162
|
-
volatil: {
|
|
163
|
-
|
|
301
|
+
yuno: {stroke: "#0284c7", accent: "#38bdf8"}, // sky
|
|
302
|
+
service: {stroke: "#059669", accent: "#34d399"}, // emerald
|
|
303
|
+
pure: {stroke: "#d97706", accent: "#fbbf24"}, // amber
|
|
304
|
+
volatil: {stroke: "#db2777", accent: "#f472b6"}, // pink
|
|
305
|
+
child: {stroke: "#7c3aed", accent: "#a78bfa"}, // violet
|
|
164
306
|
};
|
|
165
307
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
308
|
+
/***************************************************************
|
|
309
|
+
* Status colours: what the gobj is DOING.
|
|
310
|
+
*
|
|
311
|
+
* A pill and not a word: three states of one gobj are told
|
|
312
|
+
* apart across a whole tree by colour long before anybody
|
|
313
|
+
* reads them, and the dot alone never said which was which.
|
|
314
|
+
* `disabled` is a fourth entry and not a status -- a disabled
|
|
315
|
+
* gobj is still stopped or running -- but it takes the pill,
|
|
316
|
+
* because it is the fact that explains the other one.
|
|
317
|
+
***************************************************************/
|
|
318
|
+
const STATUS_COLORS = {
|
|
319
|
+
light: {
|
|
320
|
+
playing: {fg: "#065f46", bg: "#a7f3d0", dot: "#10b981"},
|
|
321
|
+
running: {fg: "#92400e", bg: "#fde68a", dot: "#f59e0b"},
|
|
322
|
+
stopped: {fg: "#991b1b", bg: "#fecaca", dot: "#ef4444"},
|
|
323
|
+
disabled: {fg: "#334155", bg: "#e2e8f0", dot: "#94a3b8"},
|
|
324
|
+
},
|
|
325
|
+
dark: {
|
|
326
|
+
playing: {fg: "#6ee7b7", bg: "rgba(16,185,129,0.28)", dot: "#34d399"},
|
|
327
|
+
running: {fg: "#fcd34d", bg: "rgba(245,158,11,0.28)", dot: "#fbbf24"},
|
|
328
|
+
stopped: {fg: "#fca5a5", bg: "rgba(239,68,68,0.28)", dot: "#f87171"},
|
|
329
|
+
disabled: {fg: "#cbd5e1", bg: "rgba(148,163,184,0.28)", dot: "#94a3b8"},
|
|
330
|
+
},
|
|
170
331
|
};
|
|
171
332
|
|
|
172
333
|
/***************************************************************
|
|
@@ -176,9 +337,9 @@ const STATE_COLORS = {
|
|
|
176
337
|
***************************************************************/
|
|
177
338
|
const NODE_SIZES = {
|
|
178
339
|
full: {
|
|
179
|
-
yuno: {w:
|
|
180
|
-
service: {w:
|
|
181
|
-
child: {w:
|
|
340
|
+
yuno: {w: 260, h: 96},
|
|
341
|
+
service: {w: 232, h: 92},
|
|
342
|
+
child: {w: 196, h: 86},
|
|
182
343
|
},
|
|
183
344
|
compact: {
|
|
184
345
|
yuno: {w: 210, h: 30},
|
|
@@ -195,45 +356,64 @@ const GT_FONT =
|
|
|
195
356
|
"Helvetica, Arial, sans-serif";
|
|
196
357
|
|
|
197
358
|
/***************************************************************
|
|
198
|
-
*
|
|
199
|
-
*
|
|
200
|
-
*
|
|
359
|
+
* Theme-aware card palette derived from the role colour.
|
|
360
|
+
*
|
|
361
|
+
* Brighter than a tint: at 9% of the role colour every card
|
|
362
|
+
* was the same near-white rectangle and the tree read as a
|
|
363
|
+
* wireframe. The fill carries enough of the hue to be named
|
|
364
|
+
* from across the canvas, and the border and the accent bar
|
|
365
|
+
* carry the rest.
|
|
201
366
|
***************************************************************/
|
|
202
|
-
function role_card_style(
|
|
367
|
+
function role_card_style(colors, dark)
|
|
203
368
|
{
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
369
|
+
let stroke = colors.stroke;
|
|
370
|
+
let accent = colors.accent;
|
|
371
|
+
|
|
207
372
|
return {
|
|
208
373
|
bg: dark
|
|
209
|
-
? `color-mix(in srgb, ${stroke}
|
|
210
|
-
: `color-mix(in srgb, ${stroke}
|
|
211
|
-
border: dark
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
sub: dark ? "#9aa4b2" : "#64748b",
|
|
374
|
+
? `color-mix(in srgb, ${stroke} 34%, #253044)`
|
|
375
|
+
: `color-mix(in srgb, ${stroke} 13%, #ffffff)`,
|
|
376
|
+
border: dark? accent: stroke,
|
|
377
|
+
accent: dark? accent: stroke,
|
|
378
|
+
title: dark ? "#f1f5f9" : "#0f172a",
|
|
379
|
+
sub: dark ? "#a8b3c2" : "#475569",
|
|
216
380
|
tagbg: dark
|
|
217
|
-
? `color-mix(in srgb, ${stroke}
|
|
218
|
-
: `color-mix(in srgb, ${stroke}
|
|
381
|
+
? `color-mix(in srgb, ${stroke} 48%, #253044)`
|
|
382
|
+
: `color-mix(in srgb, ${stroke} 20%, #ffffff)`,
|
|
383
|
+
tagfg: dark? "#f1f5f9": stroke,
|
|
219
384
|
shadow: dark
|
|
220
|
-
? "0
|
|
221
|
-
: "0
|
|
385
|
+
? "0 2px 6px rgba(0,0,0,0.50)"
|
|
386
|
+
: "0 2px 5px rgba(15,23,42,0.16)",
|
|
222
387
|
};
|
|
223
388
|
}
|
|
224
389
|
|
|
225
390
|
/***************************************************************
|
|
226
|
-
*
|
|
391
|
+
* The pill colours of a node, for the theme in force.
|
|
227
392
|
***************************************************************/
|
|
228
|
-
function
|
|
393
|
+
function status_card_style(d, dark)
|
|
229
394
|
{
|
|
230
|
-
|
|
231
|
-
|
|
395
|
+
let table = dark? STATUS_COLORS.dark: STATUS_COLORS.light;
|
|
396
|
+
if(d.disabled) {
|
|
397
|
+
return table.disabled;
|
|
232
398
|
}
|
|
233
|
-
|
|
234
|
-
|
|
399
|
+
return table[node_status(d)] || table.stopped;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/***************************************************************
|
|
403
|
+
* Map a node to its visual size class. Services are drawn
|
|
404
|
+
* bigger than plain children so the service boundary of the
|
|
405
|
+
* hierarchy is visible before anything is read.
|
|
406
|
+
***************************************************************/
|
|
407
|
+
function get_node_category(d)
|
|
408
|
+
{
|
|
409
|
+
switch(node_role(d)) {
|
|
410
|
+
case "yuno":
|
|
411
|
+
return "yuno";
|
|
412
|
+
case "service":
|
|
413
|
+
return "service";
|
|
414
|
+
default:
|
|
415
|
+
return "child";
|
|
235
416
|
}
|
|
236
|
-
return "child";
|
|
237
417
|
}
|
|
238
418
|
|
|
239
419
|
/***************************************************************
|
|
@@ -369,6 +549,25 @@ function mt_create(gobj)
|
|
|
369
549
|
***************************************************************/
|
|
370
550
|
function mt_start(gobj)
|
|
371
551
|
{
|
|
552
|
+
/*
|
|
553
|
+
* The layout and the folds go back BEFORE the first build: they
|
|
554
|
+
* decide what is built, so restoring them afterwards would mean
|
|
555
|
+
* building the tree twice and showing the wrong one first.
|
|
556
|
+
* The camera goes back after, once there is something to point at.
|
|
557
|
+
*/
|
|
558
|
+
restore_view_shape(gobj);
|
|
559
|
+
|
|
560
|
+
/* The cards carry translated text now (the status pill, the
|
|
561
|
+
* badges), and G6 draws them as innerHTML inside its own
|
|
562
|
+
* canvas: refresh_language() cannot reach in there, so a
|
|
563
|
+
* language switch is a REBUILD. Subscribe to the shell
|
|
564
|
+
* directly, like C_YUI_PERIOD -- a host that mounts a bare
|
|
565
|
+
* tree gets the repaint without having to forward anything. */
|
|
566
|
+
let shell = yui_shell_of(gobj);
|
|
567
|
+
if(shell) {
|
|
568
|
+
gobj_subscribe_event(shell, "EV_LANGUAGE_CHANGED", {}, gobj);
|
|
569
|
+
}
|
|
570
|
+
|
|
372
571
|
build_graph(gobj);
|
|
373
572
|
load_tree(gobj);
|
|
374
573
|
}
|
|
@@ -378,6 +577,18 @@ function mt_start(gobj)
|
|
|
378
577
|
***************************************************************/
|
|
379
578
|
function mt_stop(gobj)
|
|
380
579
|
{
|
|
580
|
+
let priv = gobj.priv;
|
|
581
|
+
|
|
582
|
+
let shell = yui_shell_of(gobj);
|
|
583
|
+
if(shell) {
|
|
584
|
+
gobj_unsubscribe_event(shell, "EV_LANGUAGE_CHANGED", {}, gobj);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/* In mt_stop and NOT in mt_destroy: gobj_destroy() destroys the
|
|
588
|
+
* children BEFORE calling mt_destroy, so a hosted gobj retired
|
|
589
|
+
* there is retired after the framework already took it down. */
|
|
590
|
+
close_gclass_view(priv.gclass_view);
|
|
591
|
+
priv.gclass_view = null;
|
|
381
592
|
}
|
|
382
593
|
|
|
383
594
|
/***************************************************************
|
|
@@ -410,6 +621,8 @@ function mt_destroy(gobj)
|
|
|
410
621
|
priv.$popover = null;
|
|
411
622
|
priv.$popover_title = null;
|
|
412
623
|
priv.$popover_body = null;
|
|
624
|
+
priv.$popover_gclass = null;
|
|
625
|
+
priv.popover_node = null;
|
|
413
626
|
priv.$layout_select = null;
|
|
414
627
|
priv.node_by_id = null;
|
|
415
628
|
priv.collapse_state = null;
|
|
@@ -528,10 +741,53 @@ function build_popover(gobj, $container)
|
|
|
528
741
|
`;
|
|
529
742
|
$header.appendChild($title);
|
|
530
743
|
|
|
744
|
+
/* What this gobj IS: its gclass, opened in the gclass viewer.
|
|
745
|
+
*
|
|
746
|
+
* Built only where the app registers C_YUI_JSON -- the viewer
|
|
747
|
+
* the window hosts. An app that does not carry it gets no
|
|
748
|
+
* button rather than a button that fails, and the reason is
|
|
749
|
+
* logged once, at build time. */
|
|
750
|
+
if(gclass_view_available()) {
|
|
751
|
+
let $gclass = document.createElement("button");
|
|
752
|
+
$gclass.type = "button";
|
|
753
|
+
$gclass.className = "GOBJ_TREE_POPOVER_GCLASS button is-small";
|
|
754
|
+
$gclass.style.cssText = `
|
|
755
|
+
margin-right: 6px;
|
|
756
|
+
padding: 0 8px;
|
|
757
|
+
height: 22px;
|
|
758
|
+
font-size: 11px;
|
|
759
|
+
white-space: nowrap;
|
|
760
|
+
`;
|
|
761
|
+
$gclass.textContent = t("gclass");
|
|
762
|
+
$gclass.setAttribute("i18n", "gclass");
|
|
763
|
+
$gclass.title = t("view gclass");
|
|
764
|
+
$gclass.setAttribute("data-i18n-title", "view gclass");
|
|
765
|
+
$gclass.setAttribute("aria-label", t("view gclass"));
|
|
766
|
+
$gclass.setAttribute("data-i18n-aria-label", "view gclass");
|
|
767
|
+
$gclass.addEventListener("click", (evt) => {
|
|
768
|
+
evt.stopPropagation();
|
|
769
|
+
gobj_send_event(gobj, "EV_OPEN_GCLASS", {}, gobj);
|
|
770
|
+
});
|
|
771
|
+
$header.appendChild($gclass);
|
|
772
|
+
priv.$popover_gclass = $gclass;
|
|
773
|
+
} else {
|
|
774
|
+
log_warning(
|
|
775
|
+
`${gobj_short_name(gobj)}: no C_YUI_JSON registered, ` +
|
|
776
|
+
`the gclass viewer is not offered`
|
|
777
|
+
);
|
|
778
|
+
}
|
|
779
|
+
|
|
531
780
|
let $close = document.createElement("button");
|
|
532
781
|
$close.type = "button";
|
|
782
|
+
/* Named so a stylesheet can reach it: the size a FINGER needs is a
|
|
783
|
+
* media query, and a media query cannot beat an inline style
|
|
784
|
+
* without one to hang off. See `lib_graph.css`. */
|
|
785
|
+
$close.className = "GOBJ_TREE_POPOVER_CLOSE";
|
|
533
786
|
$close.textContent = "×";
|
|
534
787
|
$close.title = t("close");
|
|
788
|
+
$close.setAttribute("data-i18n-title", "close");
|
|
789
|
+
$close.setAttribute("aria-label", t("close"));
|
|
790
|
+
$close.setAttribute("data-i18n-aria-label", "close");
|
|
535
791
|
$close.style.cssText = `
|
|
536
792
|
background: transparent;
|
|
537
793
|
border: none;
|
|
@@ -710,6 +966,10 @@ function build_graph(gobj)
|
|
|
710
966
|
if(priv.anchor_state === "on" && priv.anchor_id) {
|
|
711
967
|
yui_graph_center_on(priv.graph, priv.anchor_id);
|
|
712
968
|
}
|
|
969
|
+
/* Where the reader left the camera. One hook covers the wheel,
|
|
970
|
+
* the drag and every zoom button, which is why it is not
|
|
971
|
+
* written into each of them. */
|
|
972
|
+
save_view_state(gobj);
|
|
713
973
|
});
|
|
714
974
|
|
|
715
975
|
graph.on(NodeEvent.CLICK, (evt) => {
|
|
@@ -777,37 +1037,26 @@ function apply_layout(gobj)
|
|
|
777
1037
|
}
|
|
778
1038
|
|
|
779
1039
|
/************************************************************
|
|
780
|
-
*
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
return ROLE_COLORS.yuno;
|
|
786
|
-
}
|
|
787
|
-
if(gobj_is_volatil(target_gobj)) {
|
|
788
|
-
return ROLE_COLORS.volatil;
|
|
789
|
-
}
|
|
790
|
-
if(gobj_is_service(target_gobj)) {
|
|
791
|
-
return ROLE_COLORS.service;
|
|
792
|
-
}
|
|
793
|
-
if(gobj_is_pure_child(target_gobj)) {
|
|
794
|
-
return ROLE_COLORS.pure;
|
|
795
|
-
}
|
|
796
|
-
return ROLE_COLORS.default;
|
|
797
|
-
}
|
|
798
|
-
|
|
799
|
-
/************************************************************
|
|
800
|
-
* Resolve state color
|
|
1040
|
+
* The role colours of a node.
|
|
1041
|
+
*
|
|
1042
|
+
* Note the ORDER: volatil before service. A volatil gobj that
|
|
1043
|
+
* is also a service is drawn volatil, because that is the fact
|
|
1044
|
+
* that decides how long it lives.
|
|
801
1045
|
************************************************************/
|
|
802
|
-
function
|
|
1046
|
+
function get_role_colors(d)
|
|
803
1047
|
{
|
|
804
|
-
|
|
805
|
-
|
|
1048
|
+
switch(node_role(d)) {
|
|
1049
|
+
case "yuno":
|
|
1050
|
+
return ROLE_COLORS.yuno;
|
|
1051
|
+
case "volatil":
|
|
1052
|
+
return ROLE_COLORS.volatil;
|
|
1053
|
+
case "service":
|
|
1054
|
+
return ROLE_COLORS.service;
|
|
1055
|
+
case "pure":
|
|
1056
|
+
return ROLE_COLORS.pure;
|
|
1057
|
+
default:
|
|
1058
|
+
return ROLE_COLORS.child;
|
|
806
1059
|
}
|
|
807
|
-
if(gobj_is_playing(target_gobj)) {
|
|
808
|
-
return STATE_COLORS.running_playing;
|
|
809
|
-
}
|
|
810
|
-
return STATE_COLORS.running;
|
|
811
1060
|
}
|
|
812
1061
|
|
|
813
1062
|
/************************************************************
|
|
@@ -854,27 +1103,27 @@ function is_node_collapsed(gobj, full_name, num_children)
|
|
|
854
1103
|
************************************************************/
|
|
855
1104
|
function render_toggle_html(node_id, collapsed, num_children, cs)
|
|
856
1105
|
{
|
|
857
|
-
let label = collapsed ? ("+" + num_children) : "
|
|
858
|
-
let title = collapsed ? "
|
|
1106
|
+
let label = collapsed ? ("+" + num_children) : "\u2212";
|
|
1107
|
+
let title = collapsed ? t("expand children") : t("collapse children");
|
|
859
1108
|
return `<span
|
|
860
1109
|
data-gobj-toggle="true"
|
|
861
1110
|
data-node-id="${node_id}"
|
|
862
|
-
title="${title}"
|
|
1111
|
+
title="${escapeHtml(title)}"
|
|
863
1112
|
style="
|
|
864
1113
|
flex: 0 0 auto;
|
|
865
1114
|
display: inline-flex;
|
|
866
1115
|
align-items: center;
|
|
867
1116
|
justify-content: center;
|
|
868
|
-
min-width:
|
|
869
|
-
height:
|
|
870
|
-
padding: 0
|
|
1117
|
+
min-width: 24px;
|
|
1118
|
+
height: 18px;
|
|
1119
|
+
padding: 0 6px;
|
|
871
1120
|
margin-left: 4px;
|
|
872
|
-
background: ${cs.
|
|
873
|
-
color:
|
|
874
|
-
border: 1px solid ${cs.
|
|
875
|
-
border-radius:
|
|
1121
|
+
background: ${cs.accent};
|
|
1122
|
+
color: #ffffff;
|
|
1123
|
+
border: 1px solid ${cs.accent};
|
|
1124
|
+
border-radius: 9px;
|
|
876
1125
|
font-size: 10px;
|
|
877
|
-
font-weight:
|
|
1126
|
+
font-weight: 700;
|
|
878
1127
|
line-height: 1;
|
|
879
1128
|
cursor: pointer;
|
|
880
1129
|
user-select: none;
|
|
@@ -882,27 +1131,92 @@ function render_toggle_html(node_id, collapsed, num_children, cs)
|
|
|
882
1131
|
}
|
|
883
1132
|
|
|
884
1133
|
/************************************************************
|
|
885
|
-
*
|
|
1134
|
+
* HTML for the status pill: what the gobj is DOING.
|
|
1135
|
+
*
|
|
1136
|
+
* A dot plus a word, and the word is dropped in the compact
|
|
1137
|
+
* card where there is no room for it -- the colour still
|
|
1138
|
+
* carries it, and the `title` says it in full either way.
|
|
1139
|
+
************************************************************/
|
|
1140
|
+
function render_status_html(d, ss, labels, with_label)
|
|
1141
|
+
{
|
|
1142
|
+
let full = d.disabled?
|
|
1143
|
+
labels.field.disabled:
|
|
1144
|
+
(labels.status[node_status_key(d)] || node_status_key(d));
|
|
1145
|
+
let label = d.disabled?
|
|
1146
|
+
labels.field.disabled:
|
|
1147
|
+
(labels.status_short[node_status(d)] || node_status(d));
|
|
1148
|
+
|
|
1149
|
+
if(!with_label) {
|
|
1150
|
+
return `<span title="${escapeHtml(full)}" style="
|
|
1151
|
+
flex: 0 0 auto;
|
|
1152
|
+
width: 10px; height: 10px; border-radius: 50%;
|
|
1153
|
+
background: ${ss.dot};
|
|
1154
|
+
box-shadow: 0 0 0 2px ${ss.bg};
|
|
1155
|
+
margin-left: 6px;
|
|
1156
|
+
"></span>`;
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
return `<span title="${escapeHtml(full)}" style="
|
|
1160
|
+
flex: 0 0 auto;
|
|
1161
|
+
display: inline-flex;
|
|
1162
|
+
align-items: center;
|
|
1163
|
+
gap: 4px;
|
|
1164
|
+
padding: 1px 7px 1px 5px;
|
|
1165
|
+
background: ${ss.bg};
|
|
1166
|
+
color: ${ss.fg};
|
|
1167
|
+
border-radius: 9px;
|
|
1168
|
+
font-size: 10px;
|
|
1169
|
+
font-weight: 700;
|
|
1170
|
+
line-height: 1.45;
|
|
1171
|
+
white-space: nowrap;
|
|
1172
|
+
"><span style="width:7px; height:7px; border-radius:50%; background:${ss.dot};"></span>${escapeHtml(label)}</span>`;
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
/************************************************************
|
|
1176
|
+
* HTML for the badges: what the gobj IS, besides its colour.
|
|
1177
|
+
************************************************************/
|
|
1178
|
+
function render_badges_html(d, cs, labels)
|
|
1179
|
+
{
|
|
1180
|
+
let html = "";
|
|
1181
|
+
for(let key of node_badge_keys(d)) {
|
|
1182
|
+
let label = labels.badge[key] || key;
|
|
1183
|
+
html += `<span style="
|
|
1184
|
+
font-size: 10px;
|
|
1185
|
+
font-weight: 600;
|
|
1186
|
+
padding: 0 6px;
|
|
1187
|
+
margin-right: 4px;
|
|
1188
|
+
background: ${cs.tagbg};
|
|
1189
|
+
color: ${cs.tagfg};
|
|
1190
|
+
border: 1px solid ${cs.accent};
|
|
1191
|
+
border-radius: 9px;
|
|
1192
|
+
white-space: nowrap;
|
|
1193
|
+
">${escapeHtml(label)}</span>`;
|
|
1194
|
+
}
|
|
1195
|
+
return html;
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
/************************************************************
|
|
1199
|
+
* Recursively build nodes and edges from a NODE DESCRIPTOR
|
|
1200
|
+
* tree (gobj_tree_model.js), never from gobjs: the same
|
|
1201
|
+
* builder draws the browser yuno and a backend one.
|
|
1202
|
+
*
|
|
1203
|
+
* The descriptor is also what the popover reads, so the view
|
|
1204
|
+
* fields it needs are written onto it here (`full_name`,
|
|
1205
|
+
* `short_name`, `num_children`, `is_collapsed`,
|
|
1206
|
+
* `direct_children`) instead of copied into a second object
|
|
1207
|
+
* that can then disagree with the first.
|
|
886
1208
|
************************************************************/
|
|
887
|
-
function build_gobj_nodes(gobj,
|
|
1209
|
+
function build_gobj_nodes(gobj, d, nodes, edges, parent_id, compact, labels)
|
|
888
1210
|
{
|
|
889
1211
|
let priv = gobj.priv;
|
|
890
1212
|
let node_id = gen_node_id(gobj);
|
|
891
1213
|
|
|
892
|
-
let gclass_name = gobj_gclass_name(target_gobj) || "";
|
|
893
|
-
let instance_name = gobj_name(target_gobj) || "";
|
|
894
|
-
let state = gobj_current_state(target_gobj) || "";
|
|
895
|
-
let full_name = gobj_full_name(target_gobj);
|
|
896
|
-
let colors = get_role_colors(target_gobj, is_root);
|
|
897
1214
|
let dark = yui_is_dark();
|
|
898
|
-
let
|
|
899
|
-
let
|
|
1215
|
+
let colors = get_role_colors(d);
|
|
1216
|
+
let cs = role_card_style(colors, dark);
|
|
1217
|
+
let ss = status_card_style(d, dark);
|
|
900
1218
|
|
|
901
|
-
let
|
|
902
|
-
let playing = gobj_is_playing(target_gobj);
|
|
903
|
-
let status_text = !running ? "stopped" : (playing ? "playing" : "running");
|
|
904
|
-
|
|
905
|
-
let category = get_node_category(target_gobj, is_root);
|
|
1219
|
+
let category = get_node_category(d);
|
|
906
1220
|
let size = NODE_SIZES[compact ? "compact" : "full"][category];
|
|
907
1221
|
let width = size.w;
|
|
908
1222
|
let height = size.h;
|
|
@@ -913,10 +1227,15 @@ function build_gobj_nodes(gobj, target_gobj, nodes, edges, parent_id, is_root, c
|
|
|
913
1227
|
*/
|
|
914
1228
|
let border_width = (category === "yuno" || category === "service") ? 2 : 1;
|
|
915
1229
|
|
|
916
|
-
|
|
1230
|
+
/* A disabled gobj is drawn as one: a dashed border says
|
|
1231
|
+
* "this branch is out of the game" at any zoom. */
|
|
1232
|
+
let border_style = d.disabled? "dashed": "solid";
|
|
1233
|
+
|
|
1234
|
+
let children = d.children || [];
|
|
917
1235
|
let num_children = children.length;
|
|
918
1236
|
let has_children = num_children > 0;
|
|
919
|
-
let collapsed = has_children &&
|
|
1237
|
+
let collapsed = has_children &&
|
|
1238
|
+
is_node_collapsed(gobj, d.fullname, num_children);
|
|
920
1239
|
|
|
921
1240
|
/*
|
|
922
1241
|
* Direct-children index (collected even when this node is
|
|
@@ -926,33 +1245,41 @@ function build_gobj_nodes(gobj, target_gobj, nodes, edges, parent_id, is_root, c
|
|
|
926
1245
|
let direct_children = [];
|
|
927
1246
|
for(let child of children) {
|
|
928
1247
|
direct_children.push({
|
|
929
|
-
full_name:
|
|
930
|
-
has_grandchildren: (child.
|
|
1248
|
+
full_name: child.fullname,
|
|
1249
|
+
has_grandchildren: (child.children || []).length > 0,
|
|
931
1250
|
});
|
|
932
1251
|
}
|
|
933
1252
|
|
|
1253
|
+
/* What the VIEW needs on top of the descriptor. Written onto
|
|
1254
|
+
* it, because the popover reads the same object. */
|
|
1255
|
+
d.full_name = d.fullname;
|
|
1256
|
+
d.short_name = d.shortname;
|
|
1257
|
+
d.num_children = num_children;
|
|
1258
|
+
d.is_collapsed = collapsed;
|
|
1259
|
+
d.direct_children = direct_children;
|
|
1260
|
+
d.node_id = node_id;
|
|
1261
|
+
|
|
934
1262
|
let toggle_html = has_children
|
|
935
1263
|
? render_toggle_html(node_id, collapsed, num_children, cs)
|
|
936
1264
|
: "";
|
|
937
1265
|
|
|
1266
|
+
let card_title = d.gclass + (d.name? ("^" + d.name): "");
|
|
938
1267
|
let node_html;
|
|
939
1268
|
|
|
940
1269
|
if(compact) {
|
|
941
1270
|
/*
|
|
942
|
-
* Compact one-line node: "gclass^name" with a left
|
|
943
|
-
*
|
|
1271
|
+
* Compact one-line node: "gclass^name" with a left colour bar
|
|
1272
|
+
* and the status dot. Name truncates with "...".
|
|
944
1273
|
*/
|
|
945
|
-
let compact_label = escapeHtml(
|
|
946
|
-
instance_name ? (gclass_name + "^" + instance_name) : gclass_name
|
|
947
|
-
);
|
|
1274
|
+
let compact_label = escapeHtml(card_title);
|
|
948
1275
|
|
|
949
1276
|
node_html = `
|
|
950
|
-
<div style="
|
|
1277
|
+
<div class="GOBJ_CARD" data-gobj-name="${escapeHtml(d.fullname)}" style="
|
|
951
1278
|
width: ${width}px;
|
|
952
1279
|
height: ${height}px;
|
|
953
1280
|
background: ${cs.bg};
|
|
954
|
-
border: ${border_width}px
|
|
955
|
-
border-left:
|
|
1281
|
+
border: ${border_width}px ${border_style} ${cs.border};
|
|
1282
|
+
border-left: 6px solid ${cs.accent};
|
|
956
1283
|
border-radius: 7px;
|
|
957
1284
|
box-shadow: ${cs.shadow};
|
|
958
1285
|
font-family: ${GT_FONT};
|
|
@@ -961,49 +1288,30 @@ function build_gobj_nodes(gobj, target_gobj, nodes, edges, parent_id, is_root, c
|
|
|
961
1288
|
padding: 0 8px;
|
|
962
1289
|
box-sizing: border-box;
|
|
963
1290
|
overflow: hidden;
|
|
1291
|
+
opacity: ${d.disabled? "0.72": "1"};
|
|
964
1292
|
cursor: pointer;
|
|
965
1293
|
" title="${compact_label}">
|
|
966
1294
|
<span style="flex:1 1 auto; min-width:0; font-size:11px; font-weight:600; color:${cs.title}; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;">${compact_label}</span>
|
|
967
|
-
|
|
968
|
-
flex: 0 0 auto;
|
|
969
|
-
width: 8px; height: 8px; border-radius: 50%;
|
|
970
|
-
background: ${state_color}; margin-left: 6px;
|
|
971
|
-
"></span>
|
|
1295
|
+
${render_status_html(d, ss, labels, false)}
|
|
972
1296
|
${toggle_html}
|
|
973
1297
|
</div>`;
|
|
974
1298
|
} else {
|
|
975
1299
|
/*
|
|
976
|
-
* Full node: header (gclass + optional toggle), name,
|
|
977
|
-
* All rows truncate with "..."
|
|
1300
|
+
* Full node: header (gclass + optional toggle), name, state,
|
|
1301
|
+
* badges + status pill. All rows truncate with "..." -- the
|
|
1302
|
+
* width is fixed.
|
|
978
1303
|
*/
|
|
979
|
-
let header_label = escapeHtml(
|
|
980
|
-
let name_label = escapeHtml(
|
|
981
|
-
let state_label = escapeHtml(state);
|
|
982
|
-
|
|
983
|
-
let badges = [];
|
|
984
|
-
if(is_root) {
|
|
985
|
-
badges.push("yuno");
|
|
986
|
-
} else {
|
|
987
|
-
if(gobj_is_service(target_gobj)) {
|
|
988
|
-
badges.push("service");
|
|
989
|
-
}
|
|
990
|
-
if(gobj_is_pure_child(target_gobj)) {
|
|
991
|
-
badges.push("pure");
|
|
992
|
-
}
|
|
993
|
-
if(gobj_is_volatil(target_gobj)) {
|
|
994
|
-
badges.push("volatil");
|
|
995
|
-
}
|
|
996
|
-
}
|
|
997
|
-
let badges_html = badges.map(b =>
|
|
998
|
-
`<span style="font-size:10px; padding:0 5px; margin-right:4px; background:${cs.tagbg}; color:${cs.border}; border:1px solid ${cs.border}; border-radius:6px; white-space:nowrap;">${b}</span>`
|
|
999
|
-
).join("");
|
|
1304
|
+
let header_label = escapeHtml(d.gclass);
|
|
1305
|
+
let name_label = escapeHtml(d.name);
|
|
1306
|
+
let state_label = escapeHtml(d.state);
|
|
1000
1307
|
|
|
1001
1308
|
node_html = `
|
|
1002
|
-
<div style="
|
|
1309
|
+
<div class="GOBJ_CARD" data-gobj-name="${escapeHtml(d.fullname)}" style="
|
|
1003
1310
|
width: ${width}px;
|
|
1004
1311
|
height: ${height}px;
|
|
1005
1312
|
background: ${cs.bg};
|
|
1006
|
-
border: ${border_width}px
|
|
1313
|
+
border: ${border_width}px ${border_style} ${cs.border};
|
|
1314
|
+
border-left: 6px solid ${cs.accent};
|
|
1007
1315
|
border-radius: 9px;
|
|
1008
1316
|
box-shadow: ${cs.shadow};
|
|
1009
1317
|
overflow: hidden;
|
|
@@ -1012,59 +1320,33 @@ function build_gobj_nodes(gobj, target_gobj, nodes, edges, parent_id, is_root, c
|
|
|
1012
1320
|
display: flex;
|
|
1013
1321
|
flex-direction: column;
|
|
1014
1322
|
justify-content: center;
|
|
1015
|
-
padding:
|
|
1016
|
-
gap:
|
|
1323
|
+
padding: 6px 9px;
|
|
1324
|
+
gap: 2px;
|
|
1325
|
+
opacity: ${d.disabled? "0.72": "1"};
|
|
1017
1326
|
cursor: pointer;
|
|
1018
|
-
" title="${escapeHtml(
|
|
1327
|
+
" title="${escapeHtml(card_title)}">
|
|
1019
1328
|
<div style="flex:0 0 auto; display:flex; align-items:center; gap:6px; overflow:hidden;">
|
|
1020
|
-
<span style="flex:1 1 auto; min-width:0; font-size:12px; line-height:1.25; font-weight:
|
|
1329
|
+
<span style="flex:1 1 auto; min-width:0; font-size:12px; line-height:1.25; font-weight:700; color:${cs.title}; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;">${header_label}</span>
|
|
1021
1330
|
${toggle_html}
|
|
1022
1331
|
</div>
|
|
1023
1332
|
<div style="flex:0 0 auto; font-size:12px; line-height:1.25; color:${cs.sub}; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;">${name_label || " "}</div>
|
|
1024
1333
|
<div style="flex:0 0 auto; display:flex; justify-content:space-between; align-items:center; gap:6px; overflow:hidden;">
|
|
1025
|
-
<span style="flex:1 1 auto; min-width:0;
|
|
1026
|
-
|
|
1027
|
-
<span style="width:7px; height:7px; border-radius:50%; background:${state_color};"></span>${state_label || status_text}
|
|
1028
|
-
</span>
|
|
1334
|
+
<span style="flex:1 1 auto; min-width:0; font-size:10px; line-height:1.25; color:${cs.sub}; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;">${state_label || " "}</span>
|
|
1335
|
+
${render_status_html(d, ss, labels, true)}
|
|
1029
1336
|
</div>
|
|
1337
|
+
<div style="flex:0 0 auto; overflow:hidden; white-space:nowrap; text-overflow:ellipsis;">${render_badges_html(d, cs, labels)}</div>
|
|
1030
1338
|
</div>`;
|
|
1031
1339
|
}
|
|
1032
1340
|
|
|
1033
|
-
let parent_gobj = gobj_parent(target_gobj);
|
|
1034
|
-
|
|
1035
|
-
/*
|
|
1036
|
-
* Node data kept for the popover. Extend this object when
|
|
1037
|
-
* you want to surface more attributes — then add a row in
|
|
1038
|
-
* get_node_info_rows() with the same key.
|
|
1039
|
-
*/
|
|
1040
|
-
let node_data = {
|
|
1041
|
-
gclass: gclass_name,
|
|
1042
|
-
name: instance_name,
|
|
1043
|
-
short_name: gobj_short_name(target_gobj),
|
|
1044
|
-
full_name: full_name,
|
|
1045
|
-
parent_short_name: parent_gobj ? gobj_short_name(parent_gobj) : "",
|
|
1046
|
-
state: state,
|
|
1047
|
-
category: category,
|
|
1048
|
-
is_yuno: is_root,
|
|
1049
|
-
is_service: gobj_is_service(target_gobj),
|
|
1050
|
-
is_pure_child: gobj_is_pure_child(target_gobj),
|
|
1051
|
-
is_volatil: gobj_is_volatil(target_gobj),
|
|
1052
|
-
is_running: running,
|
|
1053
|
-
is_playing: playing,
|
|
1054
|
-
num_children: num_children,
|
|
1055
|
-
is_collapsed: collapsed,
|
|
1056
|
-
direct_children: direct_children,
|
|
1057
|
-
};
|
|
1058
|
-
|
|
1059
1341
|
if(!priv.node_by_id) {
|
|
1060
1342
|
priv.node_by_id = {};
|
|
1061
1343
|
}
|
|
1062
|
-
priv.node_by_id[node_id] =
|
|
1344
|
+
priv.node_by_id[node_id] = d;
|
|
1063
1345
|
|
|
1064
1346
|
let node = {
|
|
1065
1347
|
id: node_id,
|
|
1066
1348
|
type: 'html',
|
|
1067
|
-
data:
|
|
1349
|
+
data: d,
|
|
1068
1350
|
style: {
|
|
1069
1351
|
innerHTML: node_html,
|
|
1070
1352
|
size: [width, height],
|
|
@@ -1079,8 +1361,8 @@ function build_gobj_nodes(gobj, target_gobj, nodes, edges, parent_id, is_root, c
|
|
|
1079
1361
|
source: parent_id,
|
|
1080
1362
|
target: node_id,
|
|
1081
1363
|
style: {
|
|
1082
|
-
stroke: colors.
|
|
1083
|
-
lineWidth: 1,
|
|
1364
|
+
stroke: colors.accent,
|
|
1365
|
+
lineWidth: 1.4,
|
|
1084
1366
|
}
|
|
1085
1367
|
});
|
|
1086
1368
|
}
|
|
@@ -1090,7 +1372,7 @@ function build_gobj_nodes(gobj, target_gobj, nodes, edges, parent_id, is_root, c
|
|
|
1090
1372
|
*/
|
|
1091
1373
|
if(!collapsed) {
|
|
1092
1374
|
for(let child of children) {
|
|
1093
|
-
build_gobj_nodes(gobj, child, nodes, edges, node_id,
|
|
1375
|
+
build_gobj_nodes(gobj, child, nodes, edges, node_id, compact, labels);
|
|
1094
1376
|
}
|
|
1095
1377
|
}
|
|
1096
1378
|
|
|
@@ -1116,12 +1398,23 @@ function load_tree(gobj)
|
|
|
1116
1398
|
return;
|
|
1117
1399
|
}
|
|
1118
1400
|
|
|
1119
|
-
|
|
1120
|
-
|
|
1401
|
+
/*
|
|
1402
|
+
* The tree, as data.
|
|
1403
|
+
*
|
|
1404
|
+
* The graph draws DESCRIPTORS, so this is the one line that
|
|
1405
|
+
* says which yuno is on screen. A backend yuno enters here:
|
|
1406
|
+
* describe_backend_tree(<the `view-gobj-tree` answer>) in
|
|
1407
|
+
* place of describe_js_gobj(gobj_yuno(), true), and nothing
|
|
1408
|
+
* below this point changes.
|
|
1409
|
+
*/
|
|
1410
|
+
let root = describe_js_gobj(gobj_yuno(), true);
|
|
1411
|
+
if(!root) {
|
|
1412
|
+
log_error(`${gobj_short_name(gobj)}: no yuno to draw`);
|
|
1121
1413
|
return;
|
|
1122
1414
|
}
|
|
1123
1415
|
|
|
1124
1416
|
let layout_cfg = get_layout_cfg(gobj_read_str_attr(gobj, "layout"));
|
|
1417
|
+
let labels = node_labels(t);
|
|
1125
1418
|
|
|
1126
1419
|
let anchor = priv.pending_anchor || null;
|
|
1127
1420
|
let preserve_view = !!priv.pending_preserve_view;
|
|
@@ -1134,7 +1427,7 @@ function load_tree(gobj)
|
|
|
1134
1427
|
let nodes = [];
|
|
1135
1428
|
let edges = [];
|
|
1136
1429
|
|
|
1137
|
-
build_gobj_nodes(gobj,
|
|
1430
|
+
build_gobj_nodes(gobj, root, nodes, edges, null, layout_cfg.compact, labels);
|
|
1138
1431
|
|
|
1139
1432
|
if(nodes.length > 0) {
|
|
1140
1433
|
let root_id = nodes[0].id; /* built before its children */
|
|
@@ -1145,6 +1438,7 @@ function load_tree(gobj)
|
|
|
1145
1438
|
* there. (`anchor` below is the OTHER one -- see the
|
|
1146
1439
|
* note in PRIVATE_DATA.) */
|
|
1147
1440
|
reanchor(gobj);
|
|
1441
|
+
paint_anchor_mark(gobj);
|
|
1148
1442
|
|
|
1149
1443
|
if(anchor) {
|
|
1150
1444
|
restore_view_anchored(gobj, anchor);
|
|
@@ -1154,6 +1448,19 @@ function load_tree(gobj)
|
|
|
1154
1448
|
return;
|
|
1155
1449
|
}
|
|
1156
1450
|
|
|
1451
|
+
/*
|
|
1452
|
+
* The arrangement this browser was left in, once: after
|
|
1453
|
+
* that the reader's own camera wins, and a restore on
|
|
1454
|
+
* every rebuild would drag them back every time they
|
|
1455
|
+
* folded something.
|
|
1456
|
+
*/
|
|
1457
|
+
if(!priv.restored) {
|
|
1458
|
+
priv.restored = true;
|
|
1459
|
+
if(restore_view_state(gobj)) {
|
|
1460
|
+
return;
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1157
1464
|
/*
|
|
1158
1465
|
* ACTUAL SIZE, not fit. A tree of anything real does not
|
|
1159
1466
|
* fit at a zoom anybody can read, so fitting answered
|
|
@@ -1280,67 +1587,12 @@ function refresh_tree(gobj, opts)
|
|
|
1280
1587
|
}
|
|
1281
1588
|
}
|
|
1282
1589
|
|
|
1283
|
-
/************************************************************
|
|
1284
|
-
* Rows to show in the popover for a given node data.
|
|
1285
|
-
*
|
|
1286
|
-
* This is the extension point: to surface a new attribute,
|
|
1287
|
-
* collect it into `node_data` in build_gobj_nodes(), then
|
|
1288
|
-
* append a `[label, value]` pair here.
|
|
1289
|
-
************************************************************/
|
|
1290
|
-
function get_node_info_rows(node_data)
|
|
1291
|
-
{
|
|
1292
|
-
let rows = [];
|
|
1293
|
-
|
|
1294
|
-
rows.push([t("gclass"), node_data.gclass || "—"]);
|
|
1295
|
-
rows.push([t("name"), node_data.name || "—"]);
|
|
1296
|
-
|
|
1297
|
-
if(node_data.full_name && node_data.full_name !== node_data.short_name) {
|
|
1298
|
-
rows.push([t("full name"), node_data.full_name]);
|
|
1299
|
-
}
|
|
1300
|
-
|
|
1301
|
-
let role;
|
|
1302
|
-
if(node_data.is_yuno) {
|
|
1303
|
-
role = "yuno";
|
|
1304
|
-
} else if(node_data.is_service) {
|
|
1305
|
-
role = "service";
|
|
1306
|
-
} else if(node_data.is_volatil) {
|
|
1307
|
-
role = "volatil child";
|
|
1308
|
-
} else if(node_data.is_pure_child) {
|
|
1309
|
-
role = "pure child";
|
|
1310
|
-
} else {
|
|
1311
|
-
role = "child";
|
|
1312
|
-
}
|
|
1313
|
-
rows.push([t("role"), role]);
|
|
1314
|
-
|
|
1315
|
-
let status;
|
|
1316
|
-
if(!node_data.is_running) {
|
|
1317
|
-
status = "stopped";
|
|
1318
|
-
} else if(node_data.is_playing) {
|
|
1319
|
-
status = "running + playing";
|
|
1320
|
-
} else {
|
|
1321
|
-
status = "running";
|
|
1322
|
-
}
|
|
1323
|
-
rows.push([t("status"), status]);
|
|
1324
|
-
rows.push([t("state"), node_data.state || "—"]);
|
|
1325
|
-
|
|
1326
|
-
if(node_data.parent_short_name) {
|
|
1327
|
-
rows.push([t("parent"), node_data.parent_short_name]);
|
|
1328
|
-
}
|
|
1329
|
-
|
|
1330
|
-
if(typeof node_data.num_children === "number") {
|
|
1331
|
-
let kids = node_data.num_children;
|
|
1332
|
-
if(kids > 0 && node_data.is_collapsed) {
|
|
1333
|
-
rows.push([t("children"), kids + " " + t("(collapsed)")]);
|
|
1334
|
-
} else if(kids > 0) {
|
|
1335
|
-
rows.push([t("children"), String(kids)]);
|
|
1336
|
-
}
|
|
1337
|
-
}
|
|
1338
|
-
|
|
1339
|
-
return rows;
|
|
1340
|
-
}
|
|
1341
|
-
|
|
1342
1590
|
/************************************************************
|
|
1343
1591
|
* Render popover body from the info rows of a node.
|
|
1592
|
+
*
|
|
1593
|
+
* The rows come from gobj_tree_model.js, so a node of the
|
|
1594
|
+
* browser yuno and a node of a backend one are read out with
|
|
1595
|
+
* the same words in the same order.
|
|
1344
1596
|
************************************************************/
|
|
1345
1597
|
function render_popover_body(gobj, node_data)
|
|
1346
1598
|
{
|
|
@@ -1350,14 +1602,14 @@ function render_popover_body(gobj, node_data)
|
|
|
1350
1602
|
return;
|
|
1351
1603
|
}
|
|
1352
1604
|
|
|
1353
|
-
let rows =
|
|
1605
|
+
let rows = node_info_rows(node_data, t);
|
|
1354
1606
|
|
|
1355
1607
|
/* Bulma scheme vars, like the popover chrome around it (build_popover):
|
|
1356
1608
|
* they flip with <html data-theme>, so the popover follows the theme
|
|
1357
1609
|
* with no redraw. Hardcoding them is what made the value near-black
|
|
1358
1610
|
* (#1A1A1A) on the near-black dark card background — invisible. */
|
|
1359
1611
|
let rows_html = rows.map(([label, value]) => `
|
|
1360
|
-
<div style="display:grid; grid-template-columns:
|
|
1612
|
+
<div style="display:grid; grid-template-columns: 96px 1fr; column-gap:10px; padding: 2px 0;">
|
|
1361
1613
|
<span style="color:var(--bulma-text-weak, #6B7280); font-weight:500;">${escapeHtml(String(label))}</span>
|
|
1362
1614
|
<span style="color:var(--bulma-text-strong, #1A1A1A); word-break:break-all;">${escapeHtml(String(value))}</span>
|
|
1363
1615
|
</div>
|
|
@@ -1381,6 +1633,8 @@ function show_popover(gobj, node_data, client_x, client_y)
|
|
|
1381
1633
|
return;
|
|
1382
1634
|
}
|
|
1383
1635
|
|
|
1636
|
+
priv.popover_node = node_data;
|
|
1637
|
+
|
|
1384
1638
|
$title.textContent = node_data.short_name ||
|
|
1385
1639
|
(node_data.gclass + (node_data.name ? "^" + node_data.name : ""));
|
|
1386
1640
|
$title.title = $title.textContent;
|
|
@@ -1434,6 +1688,7 @@ function hide_popover(gobj)
|
|
|
1434
1688
|
if(priv.$popover_title) {
|
|
1435
1689
|
priv.$popover_title.textContent = "";
|
|
1436
1690
|
}
|
|
1691
|
+
priv.popover_node = null;
|
|
1437
1692
|
}
|
|
1438
1693
|
|
|
1439
1694
|
/************************************************************
|
|
@@ -1795,6 +2050,7 @@ class GobjLanesHLayout extends BaseLayout {
|
|
|
1795
2050
|
************************************************************/
|
|
1796
2051
|
function ac_refresh(gobj, event, kw, src)
|
|
1797
2052
|
{
|
|
2053
|
+
save_view_state(gobj);
|
|
1798
2054
|
refresh_tree(gobj);
|
|
1799
2055
|
return 0;
|
|
1800
2056
|
}
|
|
@@ -1841,10 +2097,12 @@ function ac_change_layout(gobj, event, kw, src)
|
|
|
1841
2097
|
}
|
|
1842
2098
|
|
|
1843
2099
|
apply_layout(gobj);
|
|
2100
|
+
save_view_state(gobj);
|
|
1844
2101
|
|
|
1845
2102
|
/*
|
|
1846
2103
|
* Rebuild nodes because compact/full HTML differs per layout
|
|
1847
2104
|
*/
|
|
2105
|
+
save_view_state(gobj);
|
|
1848
2106
|
refresh_tree(gobj);
|
|
1849
2107
|
|
|
1850
2108
|
return 0;
|
|
@@ -1902,6 +2160,7 @@ function ac_toggle_collapse(gobj, event, kw, src)
|
|
|
1902
2160
|
priv.collapse_state[node_data.full_name] = "collapsed";
|
|
1903
2161
|
}
|
|
1904
2162
|
|
|
2163
|
+
save_view_state(gobj);
|
|
1905
2164
|
refresh_tree(gobj, {anchor: anchor});
|
|
1906
2165
|
return 0;
|
|
1907
2166
|
}
|
|
@@ -2022,12 +2281,46 @@ function ac_center_anchor(gobj, event, kw, src)
|
|
|
2022
2281
|
{
|
|
2023
2282
|
let priv = gobj.priv;
|
|
2024
2283
|
|
|
2284
|
+
paint_anchor_mark(gobj);
|
|
2285
|
+
save_view_state(gobj);
|
|
2025
2286
|
if(priv.anchor_state === "on" && priv.anchor_id) {
|
|
2026
2287
|
yui_graph_center_on(priv.graph, priv.anchor_id);
|
|
2027
2288
|
}
|
|
2028
2289
|
return 0;
|
|
2029
2290
|
}
|
|
2030
2291
|
|
|
2292
|
+
/************************************************************
|
|
2293
|
+
* Mark the card the camera is holding.
|
|
2294
|
+
*
|
|
2295
|
+
* A class on the card's own element and not a G6 node state:
|
|
2296
|
+
* G6 paints no state style on an html node.
|
|
2297
|
+
*
|
|
2298
|
+
* Re-applied after every rebuild, because the cards are new
|
|
2299
|
+
* DOM each time and the mark would otherwise last until the
|
|
2300
|
+
* next expand.
|
|
2301
|
+
************************************************************/
|
|
2302
|
+
function paint_anchor_mark(gobj)
|
|
2303
|
+
{
|
|
2304
|
+
let priv = gobj.priv;
|
|
2305
|
+
let $container = gobj_read_attr(gobj, "$container");
|
|
2306
|
+
|
|
2307
|
+
if(!$container) {
|
|
2308
|
+
return;
|
|
2309
|
+
}
|
|
2310
|
+
for(let $card of $container.querySelectorAll('.GOBJ_CARD_ANCHORED')) {
|
|
2311
|
+
$card.classList.remove('GOBJ_CARD_ANCHORED');
|
|
2312
|
+
}
|
|
2313
|
+
if(priv.anchor_state !== "on" || !priv.anchor_name) {
|
|
2314
|
+
return;
|
|
2315
|
+
}
|
|
2316
|
+
let $it = $container.querySelector(
|
|
2317
|
+
'.GOBJ_CARD[data-gobj-name="' + CSS.escape(priv.anchor_name) + '"]'
|
|
2318
|
+
);
|
|
2319
|
+
if($it) {
|
|
2320
|
+
$it.classList.add('GOBJ_CARD_ANCHORED');
|
|
2321
|
+
}
|
|
2322
|
+
}
|
|
2323
|
+
|
|
2031
2324
|
/************************************************************
|
|
2032
2325
|
* Reading, or picking the node to anchor.
|
|
2033
2326
|
************************************************************/
|
|
@@ -2068,6 +2361,8 @@ function ac_toggle_anchor(gobj, event, kw, src)
|
|
|
2068
2361
|
|
|
2069
2362
|
yui_graph_update_anchor(gobj_read_attr(gobj, "$container"), priv.anchor_state);
|
|
2070
2363
|
set_picking_mode(gobj, priv.anchor_state === "arming");
|
|
2364
|
+
paint_anchor_mark(gobj);
|
|
2365
|
+
save_view_state(gobj);
|
|
2071
2366
|
return 0;
|
|
2072
2367
|
}
|
|
2073
2368
|
|
|
@@ -2202,6 +2497,7 @@ function ac_show(gobj, event, kw, src)
|
|
|
2202
2497
|
/*
|
|
2203
2498
|
* Always refresh on show: the tree may have changed while hidden
|
|
2204
2499
|
*/
|
|
2500
|
+
save_view_state(gobj);
|
|
2205
2501
|
refresh_tree(gobj);
|
|
2206
2502
|
|
|
2207
2503
|
if(graph) {
|
|
@@ -2260,6 +2556,87 @@ function ac_hide(gobj, event, kw, src)
|
|
|
2260
2556
|
return 0;
|
|
2261
2557
|
}
|
|
2262
2558
|
|
|
2559
|
+
/************************************************************
|
|
2560
|
+
* Open the gclass of the node the popover is showing.
|
|
2561
|
+
*
|
|
2562
|
+
* One window at a time: opening a second gclass replaces the
|
|
2563
|
+
* first rather than stacking windows nobody closes.
|
|
2564
|
+
************************************************************/
|
|
2565
|
+
function ac_open_gclass(gobj, event, kw, src)
|
|
2566
|
+
{
|
|
2567
|
+
let priv = gobj.priv;
|
|
2568
|
+
let node_data = priv.popover_node;
|
|
2569
|
+
|
|
2570
|
+
if(!node_data || !node_data.gclass) {
|
|
2571
|
+
log_error(`${gobj_short_name(gobj)}: EV_OPEN_GCLASS with no node`);
|
|
2572
|
+
return -1;
|
|
2573
|
+
}
|
|
2574
|
+
|
|
2575
|
+
close_gclass_view(priv.gclass_view);
|
|
2576
|
+
priv.gclass_view = null;
|
|
2577
|
+
|
|
2578
|
+
priv.gclass_view = open_gclass_view(gobj, node_data.gclass, {
|
|
2579
|
+
title_prefix: gobj_yuno_name() || "",
|
|
2580
|
+
on_close: () => {
|
|
2581
|
+
if(gobj_is_destroying(gobj)) {
|
|
2582
|
+
return;
|
|
2583
|
+
}
|
|
2584
|
+
gobj_send_event(gobj, "EV_GCLASS_CLOSED", {}, gobj);
|
|
2585
|
+
},
|
|
2586
|
+
});
|
|
2587
|
+
|
|
2588
|
+
return 0;
|
|
2589
|
+
}
|
|
2590
|
+
|
|
2591
|
+
/************************************************************
|
|
2592
|
+
* The reader dismissed the gclass window (its own ✕).
|
|
2593
|
+
*
|
|
2594
|
+
* C_YUI_WINDOW.close_window() calls on_close and THEN destroys
|
|
2595
|
+
* itself, so here we only drop our reference -- destroying it
|
|
2596
|
+
* again would destroy it twice.
|
|
2597
|
+
************************************************************/
|
|
2598
|
+
function ac_gclass_closed(gobj, event, kw, src)
|
|
2599
|
+
{
|
|
2600
|
+
gobj.priv.gclass_view = null;
|
|
2601
|
+
return 0;
|
|
2602
|
+
}
|
|
2603
|
+
|
|
2604
|
+
/************************************************************
|
|
2605
|
+
* The gclass viewer asked for a subtree.
|
|
2606
|
+
*
|
|
2607
|
+
* It cannot: the gclass description is a COMPLETE document,
|
|
2608
|
+
* it carries no `__collapsed__` stub, so nothing in it is
|
|
2609
|
+
* fetchable. The event is declared because the viewer is
|
|
2610
|
+
* hosted as a child and subscribes its host to everything it
|
|
2611
|
+
* publishes -- and it is logged, because arriving here means
|
|
2612
|
+
* the document was not the one we built.
|
|
2613
|
+
************************************************************/
|
|
2614
|
+
function ac_gclass_expand_path(gobj, event, kw, src)
|
|
2615
|
+
{
|
|
2616
|
+
log_error(
|
|
2617
|
+
`${gobj_short_name(gobj)}: EV_EXPAND_PATH on a complete document: ` +
|
|
2618
|
+
`${kw && kw.path}`
|
|
2619
|
+
);
|
|
2620
|
+
return -1;
|
|
2621
|
+
}
|
|
2622
|
+
|
|
2623
|
+
/************************************************************
|
|
2624
|
+
* The app switched language.
|
|
2625
|
+
*
|
|
2626
|
+
* A rebuild, not a re-translation: the words live inside the
|
|
2627
|
+
* innerHTML of G6 nodes, where refresh_language() cannot go.
|
|
2628
|
+
************************************************************/
|
|
2629
|
+
function ac_language_changed(gobj, event, kw, src)
|
|
2630
|
+
{
|
|
2631
|
+
let $container = gobj_read_attr(gobj, "$container");
|
|
2632
|
+
if($container) {
|
|
2633
|
+
refresh_language($container, t);
|
|
2634
|
+
}
|
|
2635
|
+
hide_popover(gobj);
|
|
2636
|
+
refresh_tree(gobj);
|
|
2637
|
+
return 0;
|
|
2638
|
+
}
|
|
2639
|
+
|
|
2263
2640
|
|
|
2264
2641
|
|
|
2265
2642
|
|
|
@@ -2311,6 +2688,10 @@ function create_gclass(gclass_name)
|
|
|
2311
2688
|
["EV_RESIZE", ac_resize, null],
|
|
2312
2689
|
["EV_SHOW", ac_show, null],
|
|
2313
2690
|
["EV_HIDE", ac_hide, null],
|
|
2691
|
+
["EV_OPEN_GCLASS", ac_open_gclass, null],
|
|
2692
|
+
["EV_GCLASS_CLOSED", ac_gclass_closed, null],
|
|
2693
|
+
["EV_EXPAND_PATH", ac_gclass_expand_path, null],
|
|
2694
|
+
["EV_LANGUAGE_CHANGED", ac_language_changed, null],
|
|
2314
2695
|
]]
|
|
2315
2696
|
];
|
|
2316
2697
|
|
|
@@ -2334,6 +2715,10 @@ function create_gclass(gclass_name)
|
|
|
2334
2715
|
["EV_RESIZE", 0],
|
|
2335
2716
|
["EV_SHOW", 0],
|
|
2336
2717
|
["EV_HIDE", 0],
|
|
2718
|
+
["EV_OPEN_GCLASS", 0],
|
|
2719
|
+
["EV_GCLASS_CLOSED", 0],
|
|
2720
|
+
["EV_EXPAND_PATH", 0],
|
|
2721
|
+
["EV_LANGUAGE_CHANGED", 0],
|
|
2337
2722
|
];
|
|
2338
2723
|
|
|
2339
2724
|
__gclass__ = gclass_create(
|