@yuneta/gobj-ui 7.20.0 → 7.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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,17 @@ const MAX_RENDER_ROWS = 5000;
103
125
  */
104
126
  const MAX_TEXT_CHARS = 2000000;
105
127
 
128
+ /*
129
+ * The three views, in switch order. EV_SET_VIEW_MODE with no mode
130
+ * advances along this list, which is what the old two-view toggle did
131
+ * when the list was two long.
132
+ */
133
+ const VIEWS = [
134
+ {mode: "tree", icon: "yi-sitemap", key: "tree view"},
135
+ {mode: "text", icon: "yi-code", key: "text view"},
136
+ {mode: "graph", icon: "yi-hexagon-nodes", key: "graph view"},
137
+ ];
138
+
106
139
  /***************************************************************
107
140
  * Data
108
141
  ***************************************************************/
@@ -113,7 +146,7 @@ SDATA(data_type_t.DTP_POINTER, "subscriber", 0, null, "Subscriber of outpu
113
146
  /*---------------- Config ----------------*/
114
147
  SDATA(data_type_t.DTP_STRING, "title", 0, "", "Optional header title (i18n key)"),
115
148
  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)"),
149
+ SDATA(data_type_t.DTP_STRING, "view_mode", 0, "tree", "View: 'tree' (lazy tree), 'text' (raw dump) or 'graph'"),
117
150
 
118
151
  /*---------------- UI ----------------*/
119
152
  SDATA(data_type_t.DTP_POINTER, "$container", 0, null, "HTMLElement root, mounted by the parent"),
@@ -129,11 +162,13 @@ let PRIVATE_DATA = {
129
162
  $tree: null, // the scrollable tree body element
130
163
  $text: null, // the scrollable text body element
131
164
  $text_body: null, // the <pre> inside it
165
+ $graph: null, // the graph body element
166
+ graph_gobj: null, // hosted C_YUI_JSON_GRAPH child (built on first use)
132
167
  $search: null, // the search input element
133
168
  $search_ctl: null, // the search control (hidden in text view)
134
169
  $expand_btn: null, // "expand loaded" button (hidden in text view)
135
170
  $collapse_btn: null,// "collapse all" button (hidden in text view)
136
- $mode_btn: null, // the tree/text switch
171
+ $mode_btns: null, // Map<mode, HTMLElement> of the view switch
137
172
  };
138
173
 
139
174
  let __gclass__ = null;
@@ -198,6 +233,7 @@ function mt_stop(gobj)
198
233
  ***************************************************************/
199
234
  function mt_destroy(gobj)
200
235
  {
236
+ teardown_graph_child(gobj);
201
237
  destroy_ui(gobj);
202
238
  }
203
239
 
@@ -227,7 +263,12 @@ function build_ui(gobj)
227
263
  ['div', {class: 'JSON_TREE is-flex-grow-1',
228
264
  style: 'flex:1 1 auto; min-height:0; overflow:auto;'}, []],
229
265
  ['div', {class: 'JSON_TEXT is-flex-grow-1 is-hidden',
230
- style: 'flex:1 1 auto; min-height:0; overflow:auto;'}, []]
266
+ style: 'flex:1 1 auto; min-height:0; overflow:auto;'}, []],
267
+ /* The graph body does NOT scroll: G6 owns its viewport and
268
+ * pans inside it. An `overflow:auto` here would give the
269
+ * canvas a second, competing scroller. */
270
+ ['div', {class: 'JSON_GRAPH is-flex-grow-1 is-hidden',
271
+ style: 'flex:1 1 auto; overflow:hidden;'}, []]
231
272
  ]]
232
273
  );
233
274
 
@@ -238,7 +279,14 @@ function build_ui(gobj)
238
279
  priv.$search_ctl = $container.querySelector('.JSON_SEARCH_CONTROL');
239
280
  priv.$expand_btn = $container.querySelector('.EV_EXPAND_ALL');
240
281
  priv.$collapse_btn = $container.querySelector('.EV_COLLAPSE_ALL');
241
- priv.$mode_btn = $container.querySelector('.JSON_VIEW_MODE');
282
+ priv.$graph = $container.querySelector('.JSON_GRAPH');
283
+ priv.$mode_btns = new Map();
284
+ VIEWS.forEach(function(v) {
285
+ let $btn = $container.querySelector('.JSON_VIEW_MODE_' + v.mode.toUpperCase());
286
+ if($btn) {
287
+ priv.$mode_btns.set(v.mode, $btn);
288
+ }
289
+ });
242
290
 
243
291
  refresh_language($container, t);
244
292
  }
@@ -259,11 +307,12 @@ function destroy_ui(gobj)
259
307
  priv.$tree = null;
260
308
  priv.$text = null;
261
309
  priv.$text_body = null;
310
+ priv.$graph = null;
262
311
  priv.$search = null;
263
312
  priv.$search_ctl = null;
264
313
  priv.$expand_btn = null;
265
314
  priv.$collapse_btn = null;
266
- priv.$mode_btn = null;
315
+ priv.$mode_btns = null;
267
316
  }
268
317
 
269
318
  /************************************************************
@@ -277,7 +326,11 @@ function make_toolbar(gobj)
277
326
  let left_items = [];
278
327
  if(title) {
279
328
  left_items.push(
280
- ['span', {class: 'JSON_TITLE is-flex is-align-items-center px-2',
329
+ /* is-hidden-mobile: on a phone the toolbar cannot hold the
330
+ * title AND a usable search AND six buttons, and the title
331
+ * is the one thing the host usually repeats — a dialog
332
+ * header, a card heading — so it is the one that goes. */
333
+ ['span', {class: 'JSON_TITLE is-flex is-align-items-center px-2 is-hidden-mobile',
281
334
  style: 'font-weight:600;', 'data-i18n': title}, title]
282
335
  );
283
336
  }
@@ -301,7 +354,7 @@ function make_toolbar(gobj)
301
354
  left_items.push($search_control);
302
355
 
303
356
  let right_items = [
304
- view_mode_button(gobj),
357
+ view_mode_switch(gobj),
305
358
  icon_button(gobj, "yi-chevron-right", "EV_EXPAND_ALL", "expand loaded"),
306
359
  icon_button(gobj, "yi-chevron-right", "EV_COLLAPSE_ALL", "collapse all"),
307
360
  icon_button(gobj, "yi-copy", "EV_COPY_ALL", "copy json"),
@@ -337,34 +390,64 @@ function icon_button(gobj, icon, event_name, label_key)
337
390
  }
338
391
 
339
392
  /************************************************************
340
- * The tree/text switch.
393
+ * The view switch: one button per view, the current one marked.
341
394
  *
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.
395
+ * Three views do not fit a toggle. A button that CYCLES cannot be
396
+ * aimed reaching the graph from the tree would mean passing
397
+ * through the text and rebuilding it on the way so each view gets
398
+ * its own button and says where it goes. apply_view_mode() moves
399
+ * the `is-active` mark.
347
400
  ************************************************************/
348
- function view_mode_button(gobj)
401
+ function view_mode_switch(gobj)
349
402
  {
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
- }];
403
+ let buttons = VIEWS.map(function(v) {
404
+ let label = view_label(v.mode);
405
+ return ['button', {class: `button JSON_VIEW_MODE JSON_VIEW_MODE_${v.mode.toUpperCase()}`,
406
+ type: 'button', style: 'width:2.5em;',
407
+ title: label, 'data-i18n-title': v.key,
408
+ 'aria-label': label, 'data-i18n-aria-label': v.key}, [
409
+ ['span', {class: 'icon'}, [['i', {class: v.icon}]]]
410
+ ], {
411
+ click: function(evt) {
412
+ evt.stopPropagation();
413
+ gobj_send_event(gobj, "EV_SET_VIEW_MODE", {mode: v.mode}, gobj);
414
+ }
415
+ }];
416
+ });
417
+
418
+ return ['div', {class: 'buttons has-addons is-flex-wrap-nowrap mb-0 JSON_VIEW_SWITCH'},
419
+ buttons];
420
+ }
421
+
422
+ /************************************************************
423
+ * The label of a view, with every key SPELLED OUT inside t().
424
+ *
425
+ * Never t(VIEWS[i].key): the apps' validate-locales scans for
426
+ * t("literal"), so a key reached through a variable is a key it
427
+ * cannot demand — and i18next answers an undefined key with the key
428
+ * ITSELF, which renders in lower-case English and never changes
429
+ * language. That shipped once already (7.20.0, `tree view`); this
430
+ * table would have hidden all three.
431
+ ************************************************************/
432
+ function view_label(mode)
433
+ {
434
+ switch(mode) {
435
+ case "text":
436
+ return t("text view");
437
+ case "graph":
438
+ return t("graph view");
439
+ default:
440
+ return t("tree view");
441
+ }
360
442
  }
361
443
 
362
444
  /************************************************************
363
- * The current view, normalised. Anything but "text" is the tree.
445
+ * The current view, normalised. Anything unknown is the tree.
364
446
  ************************************************************/
365
447
  function current_view_mode(gobj)
366
448
  {
367
- return (gobj_read_str_attr(gobj, "view_mode") === "text")? "text": "tree";
449
+ let mode = gobj_read_str_attr(gobj, "view_mode");
450
+ return VIEWS.some((v) => v.mode === mode)? mode: "tree";
368
451
  }
369
452
 
370
453
  /************************************************************
@@ -373,38 +456,32 @@ function current_view_mode(gobj)
373
456
  function apply_view_mode(gobj)
374
457
  {
375
458
  let priv = gobj.priv;
376
- let text_mode = current_view_mode(gobj) === "text";
459
+ let mode = current_view_mode(gobj);
377
460
 
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);
461
+ let bodies = {tree: priv.$tree, text: priv.$text, graph: priv.$graph};
462
+ for(let [m, $el] of Object.entries(bodies)) {
463
+ if($el) {
464
+ $el.classList.toggle('is-hidden', m !== mode);
465
+ }
383
466
  }
384
467
 
385
468
  /*
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.
469
+ * Search and expand/collapse act on TREE rows; the other two views
470
+ * have no rows to act on, and a control that answers nothing is
471
+ * worse than an absent one. A <pre> is searched with the browser's
472
+ * own Ctrl+F, and the graph carries its own controls.
390
473
  */
391
474
  [priv.$search_ctl, priv.$expand_btn, priv.$collapse_btn].forEach(function($el) {
392
475
  if($el) {
393
- $el.classList.toggle('is-hidden', text_mode);
476
+ $el.classList.toggle('is-hidden', mode !== "tree");
394
477
  }
395
478
  });
396
479
 
397
- if(priv.$mode_btn) {
398
- let icon = text_mode? "yi-sitemap": "yi-code";
399
- let key = text_mode? "tree view": "text view";
400
- let $i = priv.$mode_btn.querySelector('i');
401
- if($i) {
402
- $i.className = icon;
480
+ if(priv.$mode_btns) {
481
+ for(let [m, $btn] of priv.$mode_btns) {
482
+ $btn.classList.toggle('is-active', m === mode);
483
+ $btn.setAttribute("aria-pressed", (m === mode)? "true": "false");
403
484
  }
404
- priv.$mode_btn.setAttribute("data-i18n-title", key);
405
- priv.$mode_btn.setAttribute("data-i18n-aria-label", key);
406
- priv.$mode_btn.setAttribute("title", t(key));
407
- priv.$mode_btn.setAttribute("aria-label", t(key));
408
485
  }
409
486
  }
410
487
 
@@ -413,10 +490,105 @@ function apply_view_mode(gobj)
413
490
  ************************************************************/
414
491
  function render_view(gobj)
415
492
  {
416
- if(current_view_mode(gobj) === "text") {
417
- render_text(gobj);
418
- } else {
419
- render_tree(gobj);
493
+ switch(current_view_mode(gobj)) {
494
+ case "text":
495
+ render_text(gobj);
496
+ break;
497
+ case "graph":
498
+ render_graph(gobj);
499
+ break;
500
+ default:
501
+ render_tree(gobj);
502
+ break;
503
+ }
504
+ }
505
+
506
+ /************************************************************
507
+ * Render the graph view.
508
+ *
509
+ * The C_YUI_JSON_GRAPH child is built HERE, on first entry, and
510
+ * never in build_ui: G6 sizes itself from its container, and a
511
+ * container behind `is-hidden` measures 0x0. By the time this
512
+ * runs apply_view_mode() has already revealed the body.
513
+ ************************************************************/
514
+ function render_graph(gobj)
515
+ {
516
+ let priv = gobj.priv;
517
+ let $graph = priv.$graph;
518
+ if(!$graph) {
519
+ return;
520
+ }
521
+
522
+ if(priv.root === null || priv.root === undefined) {
523
+ teardown_graph_child(gobj);
524
+ $graph.textContent = "";
525
+ $graph.appendChild(createElement2(
526
+ ['div', {class: 'JSON_EMPTY has-text-grey p-3', 'data-i18n': 'no data'}, 'no data']
527
+ ));
528
+ refresh_language($graph, t);
529
+ return;
530
+ }
531
+
532
+ if(!priv.graph_gobj) {
533
+ $graph.textContent = "";
534
+ if(!build_graph_child(gobj)) {
535
+ return; // Error already logged
536
+ }
537
+ }
538
+
539
+ /* The document, and then the size: the container may have changed
540
+ * while this view was hidden, and G6 does not watch it. */
541
+ gobj_send_event(priv.graph_gobj, "EV_LOAD_DATA",
542
+ {data: priv.root, path: gobj_read_str_attr(gobj, "title") || ""}, gobj);
543
+ gobj_send_event(priv.graph_gobj, "EV_RESIZE", {}, gobj);
544
+ }
545
+
546
+ /************************************************************
547
+ * Build and mount the hosted graph child. Returns true on
548
+ * success; every failure path logs.
549
+ ************************************************************/
550
+ function build_graph_child(gobj)
551
+ {
552
+ let priv = gobj.priv;
553
+
554
+ let graph = gobj_create_pure_child(
555
+ "graph_" + clean_name(gobj_name(gobj)),
556
+ "C_YUI_JSON_GRAPH",
557
+ {},
558
+ gobj
559
+ );
560
+ if(!graph) {
561
+ log_error(`${gobj_short_name(gobj)}: cannot create the graph viewer`);
562
+ return false;
563
+ }
564
+
565
+ let $box = gobj_read_attr(graph, "$container");
566
+ if(!$box) {
567
+ log_error(`${gobj_short_name(gobj)}: the graph viewer built no $container`);
568
+ gobj_destroy(graph);
569
+ return false;
570
+ }
571
+
572
+ /* Mounted BEFORE gobj_start: mt_start builds the G6 graph and
573
+ * measures the canvas, which is 0x0 while it is still detached. */
574
+ priv.$graph.appendChild($box);
575
+ priv.graph_gobj = graph;
576
+ gobj_start(graph);
577
+ return true;
578
+ }
579
+
580
+ /************************************************************
581
+ * Destroy the hosted graph child, if any.
582
+ ************************************************************/
583
+ function teardown_graph_child(gobj)
584
+ {
585
+ let priv = gobj.priv;
586
+ if(priv.graph_gobj) {
587
+ if(gobj_is_running(priv.graph_gobj)) {
588
+ gobj_stop(priv.graph_gobj);
589
+ }
590
+ gobj_destroy(priv.graph_gobj);
591
+ priv.graph_gobj = null;
420
592
  }
421
593
  }
422
594
 
@@ -970,21 +1142,28 @@ function ac_copy_all(gobj, event, kw, src)
970
1142
  }
971
1143
 
972
1144
  /************************************************************
973
- * EV_SET_VIEW_MODE { mode } — "tree" | "text".
1145
+ * EV_SET_VIEW_MODE { mode } — "tree" | "text" | "graph".
974
1146
  *
975
- * With no mode it toggles, which is what the toolbar button sends.
1147
+ * With no mode it ADVANCES to the next view in VIEWS order, which
1148
+ * is what the two-view toggle did when the list was two long. The
1149
+ * toolbar buttons always name their mode.
976
1150
  ************************************************************/
977
1151
  function ac_set_view_mode(gobj, event, kw, src)
978
1152
  {
979
1153
  let mode = kw.mode;
980
1154
 
981
1155
  if(mode === undefined || mode === null || mode === "") {
982
- mode = (current_view_mode(gobj) === "text")? "tree": "text";
983
- } else if(mode !== "tree" && mode !== "text") {
1156
+ let i = VIEWS.findIndex((v) => v.mode === current_view_mode(gobj));
1157
+ mode = VIEWS[(i + 1) % VIEWS.length].mode;
1158
+ } else if(!VIEWS.some((v) => v.mode === mode)) {
984
1159
  log_error(`${GCLASS_NAME}: unknown view_mode '${mode}'`);
985
1160
  return -1;
986
1161
  }
987
1162
 
1163
+ if(mode === current_view_mode(gobj)) {
1164
+ return 0; // already there; re-rendering the graph would relayout it
1165
+ }
1166
+
988
1167
  gobj_write_attr(gobj, "view_mode", mode);
989
1168
 
990
1169
  apply_view_mode(gobj);
@@ -992,6 +1171,18 @@ function ac_set_view_mode(gobj, event, kw, src)
992
1171
  return 0;
993
1172
  }
994
1173
 
1174
+ /************************************************************
1175
+ * EV_JSON_ITEM_CLICKED — from the hosted graph child.
1176
+ *
1177
+ * Republished under this gclass's own name so the host has ONE
1178
+ * contract and never has to know the child exists.
1179
+ ************************************************************/
1180
+ function ac_json_item_clicked(gobj, event, kw, src)
1181
+ {
1182
+ gobj_publish_event(gobj, "EV_JSON_ITEM_CLICKED", kw);
1183
+ return 0;
1184
+ }
1185
+
995
1186
  /************************************************************
996
1187
  * EV_LANGUAGE_CHANGED — re-translate chrome + re-render
997
1188
  ************************************************************/
@@ -1084,6 +1275,7 @@ function create_gclass(gclass_name)
1084
1275
  ]],
1085
1276
  ["ST_READY", [
1086
1277
  ["EV_SET_JSON", ac_set_json, null],
1278
+ ["EV_JSON_ITEM_CLICKED", ac_json_item_clicked, null],
1087
1279
  ["EV_SUBTREE_LOADED", ac_subtree_loaded, null],
1088
1280
  ["EV_SUBTREE_ERROR", ac_subtree_error, null],
1089
1281
  ["EV_TOGGLE_NODE", ac_toggle_node, null],
@@ -1114,6 +1306,7 @@ function create_gclass(gclass_name)
1114
1306
  ["EV_COLLAPSE_ALL", 0],
1115
1307
  ["EV_COPY_ALL", 0],
1116
1308
  ["EV_SET_VIEW_MODE", 0],
1309
+ ["EV_JSON_ITEM_CLICKED", event_flag_t.EVF_OUTPUT_EVENT|event_flag_t.EVF_NO_WARN_SUBS],
1117
1310
  ["EV_LANGUAGE_CHANGED", 0],
1118
1311
  ["EV_REFRESH", 0],
1119
1312
  ["EV_SHOW", 0],
@@ -1154,6 +1347,12 @@ function register_c_yui_json()
1154
1347
  if(gclass_find_by_name(GCLASS_NAME, false)) {
1155
1348
  return 0;
1156
1349
  }
1350
+ /* The graph view hosts a C_YUI_JSON_GRAPH child: make sure its
1351
+ * gclass exists even if the app never registered it (same
1352
+ * arrangement C_YUI_TREEDB_TOPIC_WITH_FORM makes for this one). */
1353
+ if(!gclass_find_by_name("C_YUI_JSON_GRAPH", false)) {
1354
+ register_c_yui_json_graph();
1355
+ }
1157
1356
  return create_gclass(GCLASS_NAME);
1158
1357
  }
1159
1358
 
@@ -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