@yuneta/gobj-ui 7.20.1 → 7.21.1

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/src/c_yui_json.js CHANGED
@@ -24,19 +24,32 @@
24
24
  * Only expanded containers are materialised in the DOM, so the tree
25
25
  * stays bounded no matter how large the source document is.
26
26
  *
27
- * TWO VIEWS over the same working document, chosen by the `view_mode`
28
- * attr ("tree" | "text") and switched from the toolbar: the lazy tree
29
- * above, and the raw `JSON.stringify(…, 4)` dump of what is currently
30
- * loaded. The text view is what you reach for to read a document as
31
- * it is written, to select a slab of it, or to search it with the
32
- * browser's own Ctrl+F; it shows the `__collapsed__` sentinels
33
- * verbatim, because that is honestly what the client holds. The
27
+ * THREE VIEWS over the same working document, chosen by the
28
+ * `view_mode` attr ("tree" | "text" | "graph") and switched from the
29
+ * toolbar. They answer three different questions:
30
+ *
31
+ * tree where is this value, and what is around it the lazy
32
+ * view above, the only one that can drill.
33
+ * text what does this document SAY, verbatim for reading it
34
+ * as written, selecting a slab of it, or searching it with
35
+ * the browser's own Ctrl+F.
36
+ * graph what SHAPE is this — nesting as a hierarchy, drawn by a
37
+ * hosted C_YUI_JSON_GRAPH child (AntV/G6).
38
+ *
39
+ * Neither text nor graph is lazy: both show what the client currently
40
+ * holds, `__collapsed__` sentinels included, because that is honestly
41
+ * what it has. Drill in the tree and they grow with it. The
34
42
  * tree-only controls (search, expand/collapse) hide with the tree.
35
43
  *
44
+ * The graph child is built on FIRST entry into graph mode and not
45
+ * before: G6 measures its container, so a graph created behind
46
+ * `is-hidden` comes up 0x0, and a viewer nobody switches to graph
47
+ * should not pay for a canvas.
48
+ *
36
49
  * DOM is self-describing (UPPER_SNAKE logical classes): JSON_VIEWER /
37
- * JSON_TOOLBAR / JSON_SEARCH / JSON_VIEW_MODE / JSON_TREE / JSON_TEXT /
38
- * JSON_TEXT_BODY / JSON_ROW / JSON_KEY / JSON_VALUE / JSON_SUMMARY /
39
- * JSON_COLLAPSED / JSON_TIME.
50
+ * JSON_TOOLBAR / JSON_SEARCH / JSON_VIEW_SWITCH / JSON_VIEW_MODE /
51
+ * JSON_TREE / JSON_TEXT / JSON_TEXT_BODY / JSON_GRAPH / JSON_ROW /
52
+ * JSON_KEY / JSON_VALUE / JSON_SUMMARY / JSON_COLLAPSED / JSON_TIME.
40
53
  *
41
54
  * Copyright (c) 2026, ArtGins.
42
55
  * All Rights Reserved.
@@ -52,6 +65,14 @@ import {
52
65
  gobj_read_pointer_attr,
53
66
  gobj_parent,
54
67
  gobj_subscribe_event,
68
+ gobj_create_pure_child,
69
+ gobj_start,
70
+ gobj_stop,
71
+ gobj_destroy,
72
+ gobj_is_running,
73
+ gobj_short_name,
74
+ clean_name,
75
+ gobj_name,
55
76
  gobj_read_attr,
56
77
  gobj_write_attr,
57
78
  gobj_read_str_attr,
@@ -76,6 +97,7 @@ import {
76
97
  } from "./json_view_helpers.js";
77
98
 
78
99
  import {yui_toolbar} from "./yui_toolbar.js";
100
+ import {register_c_yui_json_graph} from "./c_yui_json_graph.js";
79
101
  import {attach_clear} from "./yui_inputs.js";
80
102
 
81
103
  import {t} from "i18next";
@@ -103,6 +125,18 @@ const MAX_RENDER_ROWS = 5000;
103
125
  */
104
126
  const MAX_TEXT_CHARS = 2000000;
105
127
 
128
+ /*
129
+ * The three views, in switch order — this table IS the order of the
130
+ * buttons, and EV_SET_VIEW_MODE with no mode advances along it.
131
+ * Note the order is not the default: the viewer still OPENS on the
132
+ * tree (the `view_mode` attr default), it just sits second in the row.
133
+ */
134
+ const VIEWS = [
135
+ {mode: "text", icon: "yi-code", key: "text view"},
136
+ {mode: "tree", icon: "yi-sitemap", key: "tree view"},
137
+ {mode: "graph", icon: "yi-hexagon-nodes", key: "graph view"},
138
+ ];
139
+
106
140
  /***************************************************************
107
141
  * Data
108
142
  ***************************************************************/
@@ -113,7 +147,7 @@ SDATA(data_type_t.DTP_POINTER, "subscriber", 0, null, "Subscriber of outpu
113
147
  /*---------------- Config ----------------*/
114
148
  SDATA(data_type_t.DTP_STRING, "title", 0, "", "Optional header title (i18n key)"),
115
149
  SDATA(data_type_t.DTP_JSON, "json_data", 0, null, "Initial JSON to render (usually already collapsed)"),
116
- SDATA(data_type_t.DTP_STRING, "view_mode", 0, "tree", "View: 'tree' (lazy tree) or 'text' (raw JSON dump)"),
150
+ SDATA(data_type_t.DTP_STRING, "view_mode", 0, "tree", "View: 'tree' (lazy tree), 'text' (raw dump) or 'graph'"),
117
151
 
118
152
  /*---------------- UI ----------------*/
119
153
  SDATA(data_type_t.DTP_POINTER, "$container", 0, null, "HTMLElement root, mounted by the parent"),
@@ -129,11 +163,13 @@ let PRIVATE_DATA = {
129
163
  $tree: null, // the scrollable tree body element
130
164
  $text: null, // the scrollable text body element
131
165
  $text_body: null, // the <pre> inside it
166
+ $graph: null, // the graph body element
167
+ graph_gobj: null, // hosted C_YUI_JSON_GRAPH child (built on first use)
132
168
  $search: null, // the search input element
133
169
  $search_ctl: null, // the search control (hidden in text view)
134
170
  $expand_btn: null, // "expand loaded" button (hidden in text view)
135
171
  $collapse_btn: null,// "collapse all" button (hidden in text view)
136
- $mode_btn: null, // the tree/text switch
172
+ $mode_btns: null, // Map<mode, HTMLElement> of the view switch
137
173
  };
138
174
 
139
175
  let __gclass__ = null;
@@ -198,6 +234,7 @@ function mt_stop(gobj)
198
234
  ***************************************************************/
199
235
  function mt_destroy(gobj)
200
236
  {
237
+ teardown_graph_child(gobj);
201
238
  destroy_ui(gobj);
202
239
  }
203
240
 
@@ -227,7 +264,12 @@ function build_ui(gobj)
227
264
  ['div', {class: 'JSON_TREE is-flex-grow-1',
228
265
  style: 'flex:1 1 auto; min-height:0; overflow:auto;'}, []],
229
266
  ['div', {class: 'JSON_TEXT is-flex-grow-1 is-hidden',
230
- style: 'flex:1 1 auto; min-height:0; overflow:auto;'}, []]
267
+ style: 'flex:1 1 auto; min-height:0; overflow:auto;'}, []],
268
+ /* The graph body does NOT scroll: G6 owns its viewport and
269
+ * pans inside it. An `overflow:auto` here would give the
270
+ * canvas a second, competing scroller. */
271
+ ['div', {class: 'JSON_GRAPH is-flex-grow-1 is-hidden',
272
+ style: 'flex:1 1 auto; overflow:hidden;'}, []]
231
273
  ]]
232
274
  );
233
275
 
@@ -238,7 +280,14 @@ function build_ui(gobj)
238
280
  priv.$search_ctl = $container.querySelector('.JSON_SEARCH_CONTROL');
239
281
  priv.$expand_btn = $container.querySelector('.EV_EXPAND_ALL');
240
282
  priv.$collapse_btn = $container.querySelector('.EV_COLLAPSE_ALL');
241
- priv.$mode_btn = $container.querySelector('.JSON_VIEW_MODE');
283
+ priv.$graph = $container.querySelector('.JSON_GRAPH');
284
+ priv.$mode_btns = new Map();
285
+ VIEWS.forEach(function(v) {
286
+ let $btn = $container.querySelector('.JSON_VIEW_MODE_' + v.mode.toUpperCase());
287
+ if($btn) {
288
+ priv.$mode_btns.set(v.mode, $btn);
289
+ }
290
+ });
242
291
 
243
292
  refresh_language($container, t);
244
293
  }
@@ -259,11 +308,12 @@ function destroy_ui(gobj)
259
308
  priv.$tree = null;
260
309
  priv.$text = null;
261
310
  priv.$text_body = null;
311
+ priv.$graph = null;
262
312
  priv.$search = null;
263
313
  priv.$search_ctl = null;
264
314
  priv.$expand_btn = null;
265
315
  priv.$collapse_btn = null;
266
- priv.$mode_btn = null;
316
+ priv.$mode_btns = null;
267
317
  }
268
318
 
269
319
  /************************************************************
@@ -277,7 +327,11 @@ function make_toolbar(gobj)
277
327
  let left_items = [];
278
328
  if(title) {
279
329
  left_items.push(
280
- ['span', {class: 'JSON_TITLE is-flex is-align-items-center px-2',
330
+ /* is-hidden-mobile: on a phone the toolbar cannot hold the
331
+ * title AND a usable search AND six buttons, and the title
332
+ * is the one thing the host usually repeats — a dialog
333
+ * header, a card heading — so it is the one that goes. */
334
+ ['span', {class: 'JSON_TITLE is-flex is-align-items-center px-2 is-hidden-mobile',
281
335
  style: 'font-weight:600;', 'data-i18n': title}, title]
282
336
  );
283
337
  }
@@ -301,7 +355,7 @@ function make_toolbar(gobj)
301
355
  left_items.push($search_control);
302
356
 
303
357
  let right_items = [
304
- view_mode_button(gobj),
358
+ view_mode_switch(gobj),
305
359
  icon_button(gobj, "yi-chevron-right", "EV_EXPAND_ALL", "expand loaded"),
306
360
  icon_button(gobj, "yi-chevron-right", "EV_COLLAPSE_ALL", "collapse all"),
307
361
  icon_button(gobj, "yi-copy", "EV_COPY_ALL", "copy json"),
@@ -337,34 +391,64 @@ function icon_button(gobj, icon, event_name, label_key)
337
391
  }
338
392
 
339
393
  /************************************************************
340
- * The tree/text switch.
394
+ * The view switch: one button per view, the current one marked.
341
395
  *
342
- * It shows the view it takes you TO the `code` glyph while the
343
- * tree is on screen, the `sitemap` glyph while the text is and
344
- * sends EV_SET_VIEW_MODE with no mode, which the action reads as
345
- * "toggle". apply_view_mode() keeps the icon and its i18n keys in
346
- * step with the attr.
396
+ * Three views do not fit a toggle. A button that CYCLES cannot be
397
+ * aimed reaching the graph from the tree would mean passing
398
+ * through the text and rebuilding it on the way so each view gets
399
+ * its own button and says where it goes. apply_view_mode() moves
400
+ * the `is-active` mark.
347
401
  ************************************************************/
348
- function view_mode_button(gobj)
402
+ function view_mode_switch(gobj)
349
403
  {
350
- return ['button', {class: 'button JSON_VIEW_MODE', style: 'width:2.5em;',
351
- title: t("text view"), 'data-i18n-title': "text view",
352
- 'aria-label': t("text view"), 'data-i18n-aria-label': "text view"}, [
353
- ['span', {class: 'icon'}, [['i', {class: 'yi-code'}]]]
354
- ], {
355
- click: function(evt) {
356
- evt.stopPropagation();
357
- gobj_send_event(gobj, "EV_SET_VIEW_MODE", {}, gobj);
358
- }
359
- }];
404
+ let buttons = VIEWS.map(function(v) {
405
+ let label = view_label(v.mode);
406
+ return ['button', {class: `button JSON_VIEW_MODE JSON_VIEW_MODE_${v.mode.toUpperCase()}`,
407
+ type: 'button', style: 'width:2.5em;',
408
+ title: label, 'data-i18n-title': v.key,
409
+ 'aria-label': label, 'data-i18n-aria-label': v.key}, [
410
+ ['span', {class: 'icon'}, [['i', {class: v.icon}]]]
411
+ ], {
412
+ click: function(evt) {
413
+ evt.stopPropagation();
414
+ gobj_send_event(gobj, "EV_SET_VIEW_MODE", {mode: v.mode}, gobj);
415
+ }
416
+ }];
417
+ });
418
+
419
+ return ['div', {class: 'buttons has-addons is-flex-wrap-nowrap mb-0 JSON_VIEW_SWITCH'},
420
+ buttons];
421
+ }
422
+
423
+ /************************************************************
424
+ * The label of a view, with every key SPELLED OUT inside t().
425
+ *
426
+ * Never t(VIEWS[i].key): the apps' validate-locales scans for
427
+ * t("literal"), so a key reached through a variable is a key it
428
+ * cannot demand — and i18next answers an undefined key with the key
429
+ * ITSELF, which renders in lower-case English and never changes
430
+ * language. That shipped once already (7.20.0, `tree view`); this
431
+ * table would have hidden all three.
432
+ ************************************************************/
433
+ function view_label(mode)
434
+ {
435
+ switch(mode) {
436
+ case "text":
437
+ return t("text view");
438
+ case "graph":
439
+ return t("graph view");
440
+ default:
441
+ return t("tree view");
442
+ }
360
443
  }
361
444
 
362
445
  /************************************************************
363
- * The current view, normalised. Anything but "text" is the tree.
446
+ * The current view, normalised. Anything unknown is the tree.
364
447
  ************************************************************/
365
448
  function current_view_mode(gobj)
366
449
  {
367
- return (gobj_read_str_attr(gobj, "view_mode") === "text")? "text": "tree";
450
+ let mode = gobj_read_str_attr(gobj, "view_mode");
451
+ return VIEWS.some((v) => v.mode === mode)? mode: "tree";
368
452
  }
369
453
 
370
454
  /************************************************************
@@ -373,45 +457,32 @@ function current_view_mode(gobj)
373
457
  function apply_view_mode(gobj)
374
458
  {
375
459
  let priv = gobj.priv;
376
- let text_mode = current_view_mode(gobj) === "text";
460
+ let mode = current_view_mode(gobj);
377
461
 
378
- if(priv.$tree) {
379
- priv.$tree.classList.toggle('is-hidden', text_mode);
380
- }
381
- if(priv.$text) {
382
- priv.$text.classList.toggle('is-hidden', !text_mode);
462
+ let bodies = {tree: priv.$tree, text: priv.$text, graph: priv.$graph};
463
+ for(let [m, $el] of Object.entries(bodies)) {
464
+ if($el) {
465
+ $el.classList.toggle('is-hidden', m !== mode);
466
+ }
383
467
  }
384
468
 
385
469
  /*
386
- * Search and expand/collapse act on TREE rows; in the text view
387
- * they have nothing to act on, and a control that answers nothing
388
- * is worse than an absent one. A <pre> is searched with the
389
- * browser's own Ctrl+F.
470
+ * Search and expand/collapse act on TREE rows; the other two views
471
+ * have no rows to act on, and a control that answers nothing is
472
+ * worse than an absent one. A <pre> is searched with the browser's
473
+ * own Ctrl+F, and the graph carries its own controls.
390
474
  */
391
475
  [priv.$search_ctl, priv.$expand_btn, priv.$collapse_btn].forEach(function($el) {
392
476
  if($el) {
393
- $el.classList.toggle('is-hidden', text_mode);
477
+ $el.classList.toggle('is-hidden', mode !== "tree");
394
478
  }
395
479
  });
396
480
 
397
- if(priv.$mode_btn) {
398
- let icon = text_mode? "yi-sitemap": "yi-code";
399
- /* Both keys spelled out inside t(), never t(variable): the apps'
400
- * validate-locales scans for t("literal"), and a key it cannot
401
- * see is a key it cannot demand — i18next then answers an
402
- * undefined key with the key ITSELF, so the button renders in
403
- * lower-case English and never changes language. `key` still
404
- * carries the data-i18n-* attrs refresh_language() reads. */
405
- let key = text_mode? "tree view": "text view";
406
- let label = text_mode? t("tree view"): t("text view");
407
- let $i = priv.$mode_btn.querySelector('i');
408
- if($i) {
409
- $i.className = icon;
481
+ if(priv.$mode_btns) {
482
+ for(let [m, $btn] of priv.$mode_btns) {
483
+ $btn.classList.toggle('is-active', m === mode);
484
+ $btn.setAttribute("aria-pressed", (m === mode)? "true": "false");
410
485
  }
411
- priv.$mode_btn.setAttribute("data-i18n-title", key);
412
- priv.$mode_btn.setAttribute("data-i18n-aria-label", key);
413
- priv.$mode_btn.setAttribute("title", label);
414
- priv.$mode_btn.setAttribute("aria-label", label);
415
486
  }
416
487
  }
417
488
 
@@ -420,10 +491,105 @@ function apply_view_mode(gobj)
420
491
  ************************************************************/
421
492
  function render_view(gobj)
422
493
  {
423
- if(current_view_mode(gobj) === "text") {
424
- render_text(gobj);
425
- } else {
426
- render_tree(gobj);
494
+ switch(current_view_mode(gobj)) {
495
+ case "text":
496
+ render_text(gobj);
497
+ break;
498
+ case "graph":
499
+ render_graph(gobj);
500
+ break;
501
+ default:
502
+ render_tree(gobj);
503
+ break;
504
+ }
505
+ }
506
+
507
+ /************************************************************
508
+ * Render the graph view.
509
+ *
510
+ * The C_YUI_JSON_GRAPH child is built HERE, on first entry, and
511
+ * never in build_ui: G6 sizes itself from its container, and a
512
+ * container behind `is-hidden` measures 0x0. By the time this
513
+ * runs apply_view_mode() has already revealed the body.
514
+ ************************************************************/
515
+ function render_graph(gobj)
516
+ {
517
+ let priv = gobj.priv;
518
+ let $graph = priv.$graph;
519
+ if(!$graph) {
520
+ return;
521
+ }
522
+
523
+ if(priv.root === null || priv.root === undefined) {
524
+ teardown_graph_child(gobj);
525
+ $graph.textContent = "";
526
+ $graph.appendChild(createElement2(
527
+ ['div', {class: 'JSON_EMPTY has-text-grey p-3', 'data-i18n': 'no data'}, 'no data']
528
+ ));
529
+ refresh_language($graph, t);
530
+ return;
531
+ }
532
+
533
+ if(!priv.graph_gobj) {
534
+ $graph.textContent = "";
535
+ if(!build_graph_child(gobj)) {
536
+ return; // Error already logged
537
+ }
538
+ }
539
+
540
+ /* The document, and then the size: the container may have changed
541
+ * while this view was hidden, and G6 does not watch it. */
542
+ gobj_send_event(priv.graph_gobj, "EV_LOAD_DATA",
543
+ {data: priv.root, path: gobj_read_str_attr(gobj, "title") || ""}, gobj);
544
+ gobj_send_event(priv.graph_gobj, "EV_RESIZE", {}, gobj);
545
+ }
546
+
547
+ /************************************************************
548
+ * Build and mount the hosted graph child. Returns true on
549
+ * success; every failure path logs.
550
+ ************************************************************/
551
+ function build_graph_child(gobj)
552
+ {
553
+ let priv = gobj.priv;
554
+
555
+ let graph = gobj_create_pure_child(
556
+ "graph_" + clean_name(gobj_name(gobj)),
557
+ "C_YUI_JSON_GRAPH",
558
+ {},
559
+ gobj
560
+ );
561
+ if(!graph) {
562
+ log_error(`${gobj_short_name(gobj)}: cannot create the graph viewer`);
563
+ return false;
564
+ }
565
+
566
+ let $box = gobj_read_attr(graph, "$container");
567
+ if(!$box) {
568
+ log_error(`${gobj_short_name(gobj)}: the graph viewer built no $container`);
569
+ gobj_destroy(graph);
570
+ return false;
571
+ }
572
+
573
+ /* Mounted BEFORE gobj_start: mt_start builds the G6 graph and
574
+ * measures the canvas, which is 0x0 while it is still detached. */
575
+ priv.$graph.appendChild($box);
576
+ priv.graph_gobj = graph;
577
+ gobj_start(graph);
578
+ return true;
579
+ }
580
+
581
+ /************************************************************
582
+ * Destroy the hosted graph child, if any.
583
+ ************************************************************/
584
+ function teardown_graph_child(gobj)
585
+ {
586
+ let priv = gobj.priv;
587
+ if(priv.graph_gobj) {
588
+ if(gobj_is_running(priv.graph_gobj)) {
589
+ gobj_stop(priv.graph_gobj);
590
+ }
591
+ gobj_destroy(priv.graph_gobj);
592
+ priv.graph_gobj = null;
427
593
  }
428
594
  }
429
595
 
@@ -977,21 +1143,28 @@ function ac_copy_all(gobj, event, kw, src)
977
1143
  }
978
1144
 
979
1145
  /************************************************************
980
- * EV_SET_VIEW_MODE { mode } — "tree" | "text".
1146
+ * EV_SET_VIEW_MODE { mode } — "tree" | "text" | "graph".
981
1147
  *
982
- * With no mode it toggles, which is what the toolbar button sends.
1148
+ * With no mode it ADVANCES to the next view in VIEWS order, which
1149
+ * is what the two-view toggle did when the list was two long. The
1150
+ * toolbar buttons always name their mode.
983
1151
  ************************************************************/
984
1152
  function ac_set_view_mode(gobj, event, kw, src)
985
1153
  {
986
1154
  let mode = kw.mode;
987
1155
 
988
1156
  if(mode === undefined || mode === null || mode === "") {
989
- mode = (current_view_mode(gobj) === "text")? "tree": "text";
990
- } else if(mode !== "tree" && mode !== "text") {
1157
+ let i = VIEWS.findIndex((v) => v.mode === current_view_mode(gobj));
1158
+ mode = VIEWS[(i + 1) % VIEWS.length].mode;
1159
+ } else if(!VIEWS.some((v) => v.mode === mode)) {
991
1160
  log_error(`${GCLASS_NAME}: unknown view_mode '${mode}'`);
992
1161
  return -1;
993
1162
  }
994
1163
 
1164
+ if(mode === current_view_mode(gobj)) {
1165
+ return 0; // already there; re-rendering the graph would relayout it
1166
+ }
1167
+
995
1168
  gobj_write_attr(gobj, "view_mode", mode);
996
1169
 
997
1170
  apply_view_mode(gobj);
@@ -999,6 +1172,18 @@ function ac_set_view_mode(gobj, event, kw, src)
999
1172
  return 0;
1000
1173
  }
1001
1174
 
1175
+ /************************************************************
1176
+ * EV_JSON_ITEM_CLICKED — from the hosted graph child.
1177
+ *
1178
+ * Republished under this gclass's own name so the host has ONE
1179
+ * contract and never has to know the child exists.
1180
+ ************************************************************/
1181
+ function ac_json_item_clicked(gobj, event, kw, src)
1182
+ {
1183
+ gobj_publish_event(gobj, "EV_JSON_ITEM_CLICKED", kw);
1184
+ return 0;
1185
+ }
1186
+
1002
1187
  /************************************************************
1003
1188
  * EV_LANGUAGE_CHANGED — re-translate chrome + re-render
1004
1189
  ************************************************************/
@@ -1091,6 +1276,7 @@ function create_gclass(gclass_name)
1091
1276
  ]],
1092
1277
  ["ST_READY", [
1093
1278
  ["EV_SET_JSON", ac_set_json, null],
1279
+ ["EV_JSON_ITEM_CLICKED", ac_json_item_clicked, null],
1094
1280
  ["EV_SUBTREE_LOADED", ac_subtree_loaded, null],
1095
1281
  ["EV_SUBTREE_ERROR", ac_subtree_error, null],
1096
1282
  ["EV_TOGGLE_NODE", ac_toggle_node, null],
@@ -1121,6 +1307,7 @@ function create_gclass(gclass_name)
1121
1307
  ["EV_COLLAPSE_ALL", 0],
1122
1308
  ["EV_COPY_ALL", 0],
1123
1309
  ["EV_SET_VIEW_MODE", 0],
1310
+ ["EV_JSON_ITEM_CLICKED", event_flag_t.EVF_OUTPUT_EVENT|event_flag_t.EVF_NO_WARN_SUBS],
1124
1311
  ["EV_LANGUAGE_CHANGED", 0],
1125
1312
  ["EV_REFRESH", 0],
1126
1313
  ["EV_SHOW", 0],
@@ -1161,6 +1348,12 @@ function register_c_yui_json()
1161
1348
  if(gclass_find_by_name(GCLASS_NAME, false)) {
1162
1349
  return 0;
1163
1350
  }
1351
+ /* The graph view hosts a C_YUI_JSON_GRAPH child: make sure its
1352
+ * gclass exists even if the app never registered it (same
1353
+ * arrangement C_YUI_TREEDB_TOPIC_WITH_FORM makes for this one). */
1354
+ if(!gclass_find_by_name("C_YUI_JSON_GRAPH", false)) {
1355
+ register_c_yui_json_graph();
1356
+ }
1164
1357
  return create_gclass(GCLASS_NAME);
1165
1358
  }
1166
1359
 
@@ -15,6 +15,7 @@ import {
15
15
  sdata_flag_t,
16
16
  event_flag_t,
17
17
  gclass_create,
18
+ gclass_find_by_name,
18
19
  log_error,
19
20
  gobj_read_pointer_attr,
20
21
  gobj_parent,
@@ -1109,6 +1110,13 @@ function create_gclass(gclass_name)
1109
1110
  ***************************************************************/
1110
1111
  function register_c_yui_json_graph()
1111
1112
  {
1113
+ /* Idempotent: C_YUI_JSON auto-registers this gclass for its graph
1114
+ * view, so an app that also registers it explicitly (in either
1115
+ * order) must not trip "GClass ALREADY created". Same arrangement
1116
+ * C_YUI_JSON itself makes for C_YUI_FORM's hosts. */
1117
+ if(gclass_find_by_name(GCLASS_NAME, false)) {
1118
+ return 0;
1119
+ }
1112
1120
  return create_gclass(GCLASS_NAME);
1113
1121
  }
1114
1122