@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/README.md CHANGED
@@ -426,42 +426,66 @@ with limits, or any equivalent), and hands the subtree back via
426
426
  the DOM, so the tree stays bounded regardless of document size. With no
427
427
  sentinels present it degrades to a plain client-side collapsible tree.
428
428
 
429
- **Two views, one document.** The toolbar switch (and the `view_mode` attr,
430
- `"tree"` | `"text"`) turns the tree into the **raw text** of the same working
431
- document — `JSON.stringify(…, 4)`, four characters per level like every other
432
- indentation in the house. It is the view for reading a document as it is
433
- written, selecting a slab of it, or searching it with the browser's own Ctrl+F.
434
- Two consequences worth knowing:
435
-
436
- - Nothing there is lazy. The text prints what the client currently holds,
437
- `__collapsed__` sentinels included, because that is honestly what it has.
438
- Expand a branch in the tree and the text grows with it.
439
- - The tree-only controls (search, expand-loaded, collapse-all) hide with the
440
- tree; copy stays. A control that can answer nothing is worse than an absent
441
- one.
442
-
443
- Long lines scroll sideways **inside** the viewer (`white-space: pre` on a
444
- `max-content`-wide `<pre>`), never on the page body: in a raw dump the
445
- indentation *is* the structure, and a wrapped line restarts at column 0 and
446
- lies about the depth of everything under it.
429
+ **Three views, one document.** The toolbar switch (and the `view_mode` attr,
430
+ `"tree"` | `"text"` | `"graph"`) picks which. They answer three different
431
+ questions:
432
+
433
+ | view | question | notes |
434
+ |---|---|---|
435
+ | `tree` | *where is this value, and what is around it* | the lazy view; the only one that can drill |
436
+ | `text` | *what does this document say, verbatim* | `JSON.stringify(…, 4)`, four characters per level |
437
+ | `graph` | *what shape is this* | a hosted `C_YUI_JSON_GRAPH` child (AntV/G6) |
438
+
439
+ - **Neither text nor graph is lazy.** Both show what the client currently
440
+ holds, `__collapsed__` sentinels included, because that is honestly what it
441
+ has. Drill in the tree and they grow with it.
442
+ - **The tree-only controls** (search, expand-loaded, collapse-all) hide with
443
+ the tree; copy stays. A control that can answer nothing is worse than an
444
+ absent one. The graph brings its own zoom/centre toolbar.
445
+ - **The graph child is built on first entry** into graph mode, never in
446
+ `build_ui`: G6 sizes itself from its container, so a graph created behind
447
+ `is-hidden` comes up 0×0. `register_c_yui_json()` auto-registers
448
+ `C_YUI_JSON_GRAPH` (and that register is idempotent, so an app may also
449
+ register it itself, in either order).
450
+
451
+ Two layout facts the browser taught this component, both worth keeping:
452
+
453
+ - In the text view long lines scroll sideways **inside** the viewer
454
+ (`white-space: pre` on a `max-content`-wide `<pre>`), never on the page body:
455
+ in a raw dump the indentation *is* the structure, and a wrapped line restarts
456
+ at column 0 and lies about the depth of everything under it. The `<pre>` must
457
+ be `max-content` wide or the container reports no overflow and the tail of
458
+ every long line is unreachable.
459
+ - The graph body carries a **definite `height`** (`24rem`, with
460
+ `flex: 1 1 auto` so a constrained host still wins). Not `min-height`: a
461
+ percentage height does not resolve against a box sized by a minimum, and the
462
+ tree and the text push their own height while a canvas pushes none — so in an
463
+ unconstrained host the graph came up as a 2px hairline.
447
464
 
448
465
  **Contract:**
449
466
 
450
- - Attributes: `subscriber`, `title` (i18n key, optional), `json_data` (initial
451
- JSON, optional), `view_mode` (`"tree"` default | `"text"`), `$container`
452
- (mounted by the parent).
467
+ - Attributes: `subscriber`, `title` (i18n key, optional hidden on mobile,
468
+ where the toolbar cannot hold it as well as the buttons), `json_data`
469
+ (initial JSON, optional), `view_mode` (`"tree"` default | `"text"` |
470
+ `"graph"`), `$container` (mounted by the parent).
453
471
  - Input events: `EV_SET_JSON {json}` (replace the whole document; `ST_EMPTY` →
454
472
  `ST_READY`), `EV_SUBTREE_LOADED {path, json}` (splice a fetched subtree),
455
473
  `EV_SUBTREE_ERROR {path, error}`, `EV_SET_VIEW_MODE {mode}` (`"tree"` /
456
- `"text"`; **no mode = toggle**, which is what the toolbar button sends), plus
457
- `EV_REFRESH` / `EV_SHOW` / `EV_HIDE` / `EV_LANGUAGE_CHANGED`.
458
- - Output event: `EV_EXPAND_PATH {path, size}` (`EVF_OUTPUT_EVENT`) the parent
459
- must declare it in its own FSM (CHILD subscription model).
474
+ `"text"` / `"graph"`; **no mode advances** to the next view, which is what
475
+ the two-view toggle did when the list was two long), plus `EV_REFRESH` /
476
+ `EV_SHOW` / `EV_HIDE` / `EV_LANGUAGE_CHANGED`.
477
+ - Output events: `EV_EXPAND_PATH {path, size}` (`EVF_OUTPUT_EVENT`) the parent
478
+ must declare it in its own FSM (CHILD subscription model) — and
479
+ `EV_JSON_ITEM_CLICKED`, republished from the graph child so the host has one
480
+ contract and never has to know that child exists (`EVF_NO_WARN_SUBS`: most
481
+ hosts do not care).
460
482
  - Internal (DOM → FSM): `EV_TOGGLE_NODE`, `EV_EXPAND_COLLAPSED`, `EV_SEARCH`,
461
483
  `EV_EXPAND_ALL`, `EV_COLLAPSE_ALL`, `EV_COPY_ALL`. Every kw carries only a
462
484
  `path` string — never a DOM node or gobj.
463
- - i18n keys added by the text view: `text view`, `tree view`,
464
- `text truncated; collapse some branches`.
485
+ - i18n keys the switch needs: `tree view`, `text view`, `graph view`,
486
+ `text truncated; collapse some branches`. All spelled out inside `t()` in
487
+ `view_label()` — never `t(VIEWS[i].key)`, which no `validate-locales` can
488
+ see (that shipped once, in `7.20.0`).
465
489
  - Paths use the kernel delimiter (backtick) and index arrays numerically, so a
466
490
  path emitted by the viewer round-trips through `kw_find_path` on the backend.
467
491
 
@@ -10277,6 +10277,7 @@ function create_gclass$11(gclass_name) {
10277
10277
  * Register GClass
10278
10278
  ***************************************************************/
10279
10279
  function register_c_yui_json_graph() {
10280
+ if ((0, _yuneta_gobj_js.gclass_find_by_name)(GCLASS_NAME$11, false)) return 0;
10280
10281
  return create_gclass$11(GCLASS_NAME$11);
10281
10282
  }
10282
10283
  /************************************************************
@@ -10477,19 +10478,32 @@ function json_text_dump(root, max_chars) {
10477
10478
  * Only expanded containers are materialised in the DOM, so the tree
10478
10479
  * stays bounded no matter how large the source document is.
10479
10480
  *
10480
- * TWO VIEWS over the same working document, chosen by the `view_mode`
10481
- * attr ("tree" | "text") and switched from the toolbar: the lazy tree
10482
- * above, and the raw `JSON.stringify(…, 4)` dump of what is currently
10483
- * loaded. The text view is what you reach for to read a document as
10484
- * it is written, to select a slab of it, or to search it with the
10485
- * browser's own Ctrl+F; it shows the `__collapsed__` sentinels
10486
- * verbatim, because that is honestly what the client holds. The
10481
+ * THREE VIEWS over the same working document, chosen by the
10482
+ * `view_mode` attr ("tree" | "text" | "graph") and switched from the
10483
+ * toolbar. They answer three different questions:
10484
+ *
10485
+ * tree where is this value, and what is around it the lazy
10486
+ * view above, the only one that can drill.
10487
+ * text what does this document SAY, verbatim for reading it
10488
+ * as written, selecting a slab of it, or searching it with
10489
+ * the browser's own Ctrl+F.
10490
+ * graph what SHAPE is this — nesting as a hierarchy, drawn by a
10491
+ * hosted C_YUI_JSON_GRAPH child (AntV/G6).
10492
+ *
10493
+ * Neither text nor graph is lazy: both show what the client currently
10494
+ * holds, `__collapsed__` sentinels included, because that is honestly
10495
+ * what it has. Drill in the tree and they grow with it. The
10487
10496
  * tree-only controls (search, expand/collapse) hide with the tree.
10488
10497
  *
10498
+ * The graph child is built on FIRST entry into graph mode and not
10499
+ * before: G6 measures its container, so a graph created behind
10500
+ * `is-hidden` comes up 0x0, and a viewer nobody switches to graph
10501
+ * should not pay for a canvas.
10502
+ *
10489
10503
  * DOM is self-describing (UPPER_SNAKE logical classes): JSON_VIEWER /
10490
- * JSON_TOOLBAR / JSON_SEARCH / JSON_VIEW_MODE / JSON_TREE / JSON_TEXT /
10491
- * JSON_TEXT_BODY / JSON_ROW / JSON_KEY / JSON_VALUE / JSON_SUMMARY /
10492
- * JSON_COLLAPSED / JSON_TIME.
10504
+ * JSON_TOOLBAR / JSON_SEARCH / JSON_VIEW_SWITCH / JSON_VIEW_MODE /
10505
+ * JSON_TREE / JSON_TEXT / JSON_TEXT_BODY / JSON_GRAPH / JSON_ROW /
10506
+ * JSON_KEY / JSON_VALUE / JSON_SUMMARY / JSON_COLLAPSED / JSON_TIME.
10493
10507
  *
10494
10508
  * Copyright (c) 2026, ArtGins.
10495
10509
  * All Rights Reserved.
@@ -10500,6 +10514,23 @@ function json_text_dump(root, max_chars) {
10500
10514
  var GCLASS_NAME$10 = "C_YUI_JSON";
10501
10515
  var MAX_RENDER_ROWS = 5e3;
10502
10516
  var MAX_TEXT_CHARS = 2e6;
10517
+ var VIEWS = [
10518
+ {
10519
+ mode: "tree",
10520
+ icon: "yi-sitemap",
10521
+ key: "tree view"
10522
+ },
10523
+ {
10524
+ mode: "text",
10525
+ icon: "yi-code",
10526
+ key: "text view"
10527
+ },
10528
+ {
10529
+ mode: "graph",
10530
+ icon: "yi-hexagon-nodes",
10531
+ key: "graph view"
10532
+ }
10533
+ ];
10503
10534
  /***************************************************************
10504
10535
  * Data
10505
10536
  ***************************************************************/
@@ -10507,7 +10538,7 @@ var attrs_table$10 = [
10507
10538
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_POINTER, "subscriber", 0, null, "Subscriber of output events"),
10508
10539
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_STRING, "title", 0, "", "Optional header title (i18n key)"),
10509
10540
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_JSON, "json_data", 0, null, "Initial JSON to render (usually already collapsed)"),
10510
- (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_STRING, "view_mode", 0, "tree", "View: 'tree' (lazy tree) or 'text' (raw JSON dump)"),
10541
+ (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_STRING, "view_mode", 0, "tree", "View: 'tree' (lazy tree), 'text' (raw dump) or 'graph'"),
10511
10542
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_POINTER, "$container", 0, null, "HTMLElement root, mounted by the parent"),
10512
10543
  (0, _yuneta_gobj_js.SDATA_END)()
10513
10544
  ];
@@ -10520,11 +10551,13 @@ var PRIVATE_DATA$10 = {
10520
10551
  $tree: null,
10521
10552
  $text: null,
10522
10553
  $text_body: null,
10554
+ $graph: null,
10555
+ graph_gobj: null,
10523
10556
  $search: null,
10524
10557
  $search_ctl: null,
10525
10558
  $expand_btn: null,
10526
10559
  $collapse_btn: null,
10527
- $mode_btn: null
10560
+ $mode_btns: null
10528
10561
  };
10529
10562
  var __gclass__$10 = null;
10530
10563
  /******************************
@@ -10561,6 +10594,7 @@ function mt_stop$10(gobj) {}
10561
10594
  * Framework Method: Destroy
10562
10595
  ***************************************************************/
10563
10596
  function mt_destroy$10(gobj) {
10597
+ teardown_graph_child(gobj);
10564
10598
  destroy_ui$5(gobj);
10565
10599
  }
10566
10600
  /***************************
@@ -10599,6 +10633,14 @@ function build_ui$9(gobj) {
10599
10633
  style: "flex:1 1 auto; min-height:0; overflow:auto;"
10600
10634
  },
10601
10635
  []
10636
+ ],
10637
+ [
10638
+ "div",
10639
+ {
10640
+ class: "JSON_GRAPH is-flex-grow-1 is-hidden",
10641
+ style: "flex:1 1 auto; overflow:hidden;"
10642
+ },
10643
+ []
10602
10644
  ]
10603
10645
  ]
10604
10646
  ]);
@@ -10609,7 +10651,12 @@ function build_ui$9(gobj) {
10609
10651
  priv.$search_ctl = $container.querySelector(".JSON_SEARCH_CONTROL");
10610
10652
  priv.$expand_btn = $container.querySelector(".EV_EXPAND_ALL");
10611
10653
  priv.$collapse_btn = $container.querySelector(".EV_COLLAPSE_ALL");
10612
- priv.$mode_btn = $container.querySelector(".JSON_VIEW_MODE");
10654
+ priv.$graph = $container.querySelector(".JSON_GRAPH");
10655
+ priv.$mode_btns = /* @__PURE__ */ new Map();
10656
+ VIEWS.forEach(function(v) {
10657
+ let $btn = $container.querySelector(".JSON_VIEW_MODE_" + v.mode.toUpperCase());
10658
+ if ($btn) priv.$mode_btns.set(v.mode, $btn);
10659
+ });
10613
10660
  (0, _yuneta_gobj_js.refresh_language)($container, i18next.t);
10614
10661
  }
10615
10662
  /************************************************************
@@ -10625,11 +10672,12 @@ function destroy_ui$5(gobj) {
10625
10672
  priv.$tree = null;
10626
10673
  priv.$text = null;
10627
10674
  priv.$text_body = null;
10675
+ priv.$graph = null;
10628
10676
  priv.$search = null;
10629
10677
  priv.$search_ctl = null;
10630
10678
  priv.$expand_btn = null;
10631
10679
  priv.$collapse_btn = null;
10632
- priv.$mode_btn = null;
10680
+ priv.$mode_btns = null;
10633
10681
  }
10634
10682
  /************************************************************
10635
10683
  * Toolbar: search + tree/text switch + expand-all /
@@ -10641,7 +10689,7 @@ function make_toolbar$2(gobj) {
10641
10689
  if (title) left_items.push([
10642
10690
  "span",
10643
10691
  {
10644
- class: "JSON_TITLE is-flex is-align-items-center px-2",
10692
+ class: "JSON_TITLE is-flex is-align-items-center px-2 is-hidden-mobile",
10645
10693
  style: "font-weight:600;",
10646
10694
  "data-i18n": title
10647
10695
  },
@@ -10674,7 +10722,7 @@ function make_toolbar$2(gobj) {
10674
10722
  attach_clear($search_control, $search_input);
10675
10723
  left_items.push($search_control);
10676
10724
  let right_items = [
10677
- view_mode_button(gobj),
10725
+ view_mode_switch(gobj),
10678
10726
  icon_button(gobj, "yi-chevron-right", "EV_EXPAND_ALL", "expand loaded"),
10679
10727
  icon_button(gobj, "yi-chevron-right", "EV_COLLAPSE_ALL", "collapse all"),
10680
10728
  icon_button(gobj, "yi-copy", "EV_COPY_ALL", "copy json")
@@ -10731,74 +10779,174 @@ function icon_button(gobj, icon, event_name, label_key) {
10731
10779
  ];
10732
10780
  }
10733
10781
  /************************************************************
10734
- * The tree/text switch.
10782
+ * The view switch: one button per view, the current one marked.
10735
10783
  *
10736
- * It shows the view it takes you TO the `code` glyph while the
10737
- * tree is on screen, the `sitemap` glyph while the text is and
10738
- * sends EV_SET_VIEW_MODE with no mode, which the action reads as
10739
- * "toggle". apply_view_mode() keeps the icon and its i18n keys in
10740
- * step with the attr.
10784
+ * Three views do not fit a toggle. A button that CYCLES cannot be
10785
+ * aimed reaching the graph from the tree would mean passing
10786
+ * through the text and rebuilding it on the way so each view gets
10787
+ * its own button and says where it goes. apply_view_mode() moves
10788
+ * the `is-active` mark.
10741
10789
  ************************************************************/
10742
- function view_mode_button(gobj) {
10790
+ function view_mode_switch(gobj) {
10743
10791
  return [
10744
- "button",
10745
- {
10746
- class: "button JSON_VIEW_MODE",
10747
- style: "width:2.5em;",
10748
- title: (0, i18next.t)("text view"),
10749
- "data-i18n-title": "text view",
10750
- "aria-label": (0, i18next.t)("text view"),
10751
- "data-i18n-aria-label": "text view"
10752
- },
10753
- [[
10754
- "span",
10755
- { class: "icon" },
10756
- [["i", { class: "yi-code" }]]
10757
- ]],
10758
- { click: function(evt) {
10759
- evt.stopPropagation();
10760
- (0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_SET_VIEW_MODE", {}, gobj);
10761
- } }
10792
+ "div",
10793
+ { class: "buttons has-addons is-flex-wrap-nowrap mb-0 JSON_VIEW_SWITCH" },
10794
+ VIEWS.map(function(v) {
10795
+ let label = view_label(v.mode);
10796
+ return [
10797
+ "button",
10798
+ {
10799
+ class: `button JSON_VIEW_MODE JSON_VIEW_MODE_${v.mode.toUpperCase()}`,
10800
+ type: "button",
10801
+ style: "width:2.5em;",
10802
+ title: label,
10803
+ "data-i18n-title": v.key,
10804
+ "aria-label": label,
10805
+ "data-i18n-aria-label": v.key
10806
+ },
10807
+ [[
10808
+ "span",
10809
+ { class: "icon" },
10810
+ [["i", { class: v.icon }]]
10811
+ ]],
10812
+ { click: function(evt) {
10813
+ evt.stopPropagation();
10814
+ (0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_SET_VIEW_MODE", { mode: v.mode }, gobj);
10815
+ } }
10816
+ ];
10817
+ })
10762
10818
  ];
10763
10819
  }
10764
10820
  /************************************************************
10765
- * The current view, normalised. Anything but "text" is the tree.
10821
+ * The label of a view, with every key SPELLED OUT inside t().
10822
+ *
10823
+ * Never t(VIEWS[i].key): the apps' validate-locales scans for
10824
+ * t("literal"), so a key reached through a variable is a key it
10825
+ * cannot demand — and i18next answers an undefined key with the key
10826
+ * ITSELF, which renders in lower-case English and never changes
10827
+ * language. That shipped once already (7.20.0, `tree view`); this
10828
+ * table would have hidden all three.
10829
+ ************************************************************/
10830
+ function view_label(mode) {
10831
+ switch (mode) {
10832
+ case "text": return (0, i18next.t)("text view");
10833
+ case "graph": return (0, i18next.t)("graph view");
10834
+ default: return (0, i18next.t)("tree view");
10835
+ }
10836
+ }
10837
+ /************************************************************
10838
+ * The current view, normalised. Anything unknown is the tree.
10766
10839
  ************************************************************/
10767
10840
  function current_view_mode(gobj) {
10768
- return (0, _yuneta_gobj_js.gobj_read_str_attr)(gobj, "view_mode") === "text" ? "text" : "tree";
10841
+ let mode = (0, _yuneta_gobj_js.gobj_read_str_attr)(gobj, "view_mode");
10842
+ return VIEWS.some((v) => v.mode === mode) ? mode : "tree";
10769
10843
  }
10770
10844
  /************************************************************
10771
10845
  * Show the chrome of the current view and hide the other's.
10772
10846
  ************************************************************/
10773
10847
  function apply_view_mode(gobj) {
10774
10848
  let priv = gobj.priv;
10775
- let text_mode = current_view_mode(gobj) === "text";
10776
- if (priv.$tree) priv.$tree.classList.toggle("is-hidden", text_mode);
10777
- if (priv.$text) priv.$text.classList.toggle("is-hidden", !text_mode);
10849
+ let mode = current_view_mode(gobj);
10850
+ let bodies = {
10851
+ tree: priv.$tree,
10852
+ text: priv.$text,
10853
+ graph: priv.$graph
10854
+ };
10855
+ for (let [m, $el] of Object.entries(bodies)) if ($el) $el.classList.toggle("is-hidden", m !== mode);
10778
10856
  [
10779
10857
  priv.$search_ctl,
10780
10858
  priv.$expand_btn,
10781
10859
  priv.$collapse_btn
10782
10860
  ].forEach(function($el) {
10783
- if ($el) $el.classList.toggle("is-hidden", text_mode);
10861
+ if ($el) $el.classList.toggle("is-hidden", mode !== "tree");
10784
10862
  });
10785
- if (priv.$mode_btn) {
10786
- let icon = text_mode ? "yi-sitemap" : "yi-code";
10787
- let key = text_mode ? "tree view" : "text view";
10788
- let $i = priv.$mode_btn.querySelector("i");
10789
- if ($i) $i.className = icon;
10790
- priv.$mode_btn.setAttribute("data-i18n-title", key);
10791
- priv.$mode_btn.setAttribute("data-i18n-aria-label", key);
10792
- priv.$mode_btn.setAttribute("title", (0, i18next.t)(key));
10793
- priv.$mode_btn.setAttribute("aria-label", (0, i18next.t)(key));
10863
+ if (priv.$mode_btns) for (let [m, $btn] of priv.$mode_btns) {
10864
+ $btn.classList.toggle("is-active", m === mode);
10865
+ $btn.setAttribute("aria-pressed", m === mode ? "true" : "false");
10794
10866
  }
10795
10867
  }
10796
10868
  /************************************************************
10797
10869
  * Render whichever view is current.
10798
10870
  ************************************************************/
10799
10871
  function render_view(gobj) {
10800
- if (current_view_mode(gobj) === "text") render_text(gobj);
10801
- else render_tree(gobj);
10872
+ switch (current_view_mode(gobj)) {
10873
+ case "text":
10874
+ render_text(gobj);
10875
+ break;
10876
+ case "graph":
10877
+ render_graph(gobj);
10878
+ break;
10879
+ default: render_tree(gobj);
10880
+ }
10881
+ }
10882
+ /************************************************************
10883
+ * Render the graph view.
10884
+ *
10885
+ * The C_YUI_JSON_GRAPH child is built HERE, on first entry, and
10886
+ * never in build_ui: G6 sizes itself from its container, and a
10887
+ * container behind `is-hidden` measures 0x0. By the time this
10888
+ * runs apply_view_mode() has already revealed the body.
10889
+ ************************************************************/
10890
+ function render_graph(gobj) {
10891
+ let priv = gobj.priv;
10892
+ let $graph = priv.$graph;
10893
+ if (!$graph) return;
10894
+ if (priv.root === null || priv.root === void 0) {
10895
+ teardown_graph_child(gobj);
10896
+ $graph.textContent = "";
10897
+ $graph.appendChild((0, _yuneta_gobj_js.createElement2)([
10898
+ "div",
10899
+ {
10900
+ class: "JSON_EMPTY has-text-grey p-3",
10901
+ "data-i18n": "no data"
10902
+ },
10903
+ "no data"
10904
+ ]));
10905
+ (0, _yuneta_gobj_js.refresh_language)($graph, i18next.t);
10906
+ return;
10907
+ }
10908
+ if (!priv.graph_gobj) {
10909
+ $graph.textContent = "";
10910
+ if (!build_graph_child(gobj)) return;
10911
+ }
10912
+ (0, _yuneta_gobj_js.gobj_send_event)(priv.graph_gobj, "EV_LOAD_DATA", {
10913
+ data: priv.root,
10914
+ path: (0, _yuneta_gobj_js.gobj_read_str_attr)(gobj, "title") || ""
10915
+ }, gobj);
10916
+ (0, _yuneta_gobj_js.gobj_send_event)(priv.graph_gobj, "EV_RESIZE", {}, gobj);
10917
+ }
10918
+ /************************************************************
10919
+ * Build and mount the hosted graph child. Returns true on
10920
+ * success; every failure path logs.
10921
+ ************************************************************/
10922
+ function build_graph_child(gobj) {
10923
+ let priv = gobj.priv;
10924
+ let graph = (0, _yuneta_gobj_js.gobj_create_pure_child)("graph_" + (0, _yuneta_gobj_js.clean_name)((0, _yuneta_gobj_js.gobj_name)(gobj)), "C_YUI_JSON_GRAPH", {}, gobj);
10925
+ if (!graph) {
10926
+ (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: cannot create the graph viewer`);
10927
+ return false;
10928
+ }
10929
+ let $box = (0, _yuneta_gobj_js.gobj_read_attr)(graph, "$container");
10930
+ if (!$box) {
10931
+ (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: the graph viewer built no $container`);
10932
+ (0, _yuneta_gobj_js.gobj_destroy)(graph);
10933
+ return false;
10934
+ }
10935
+ priv.$graph.appendChild($box);
10936
+ priv.graph_gobj = graph;
10937
+ (0, _yuneta_gobj_js.gobj_start)(graph);
10938
+ return true;
10939
+ }
10940
+ /************************************************************
10941
+ * Destroy the hosted graph child, if any.
10942
+ ************************************************************/
10943
+ function teardown_graph_child(gobj) {
10944
+ let priv = gobj.priv;
10945
+ if (priv.graph_gobj) {
10946
+ if ((0, _yuneta_gobj_js.gobj_is_running)(priv.graph_gobj)) (0, _yuneta_gobj_js.gobj_stop)(priv.graph_gobj);
10947
+ (0, _yuneta_gobj_js.gobj_destroy)(priv.graph_gobj);
10948
+ priv.graph_gobj = null;
10949
+ }
10802
10950
  }
10803
10951
  /************************************************************
10804
10952
  * Render the text view: the whole loaded document, verbatim.
@@ -11289,23 +11437,36 @@ function ac_copy_all(gobj, event, kw, src) {
11289
11437
  return 0;
11290
11438
  }
11291
11439
  /************************************************************
11292
- * EV_SET_VIEW_MODE { mode } — "tree" | "text".
11440
+ * EV_SET_VIEW_MODE { mode } — "tree" | "text" | "graph".
11293
11441
  *
11294
- * With no mode it toggles, which is what the toolbar button sends.
11442
+ * With no mode it ADVANCES to the next view in VIEWS order, which
11443
+ * is what the two-view toggle did when the list was two long. The
11444
+ * toolbar buttons always name their mode.
11295
11445
  ************************************************************/
11296
11446
  function ac_set_view_mode(gobj, event, kw, src) {
11297
11447
  let mode = kw.mode;
11298
- if (mode === void 0 || mode === null || mode === "") mode = current_view_mode(gobj) === "text" ? "tree" : "text";
11299
- else if (mode !== "tree" && mode !== "text") {
11448
+ if (mode === void 0 || mode === null || mode === "") mode = VIEWS[(VIEWS.findIndex((v) => v.mode === current_view_mode(gobj)) + 1) % VIEWS.length].mode;
11449
+ else if (!VIEWS.some((v) => v.mode === mode)) {
11300
11450
  (0, _yuneta_gobj_js.log_error)(`${GCLASS_NAME$10}: unknown view_mode '${mode}'`);
11301
11451
  return -1;
11302
11452
  }
11453
+ if (mode === current_view_mode(gobj)) return 0;
11303
11454
  (0, _yuneta_gobj_js.gobj_write_attr)(gobj, "view_mode", mode);
11304
11455
  apply_view_mode(gobj);
11305
11456
  render_view(gobj);
11306
11457
  return 0;
11307
11458
  }
11308
11459
  /************************************************************
11460
+ * EV_JSON_ITEM_CLICKED — from the hosted graph child.
11461
+ *
11462
+ * Republished under this gclass's own name so the host has ONE
11463
+ * contract and never has to know the child exists.
11464
+ ************************************************************/
11465
+ function ac_json_item_clicked(gobj, event, kw, src) {
11466
+ (0, _yuneta_gobj_js.gobj_publish_event)(gobj, "EV_JSON_ITEM_CLICKED", kw);
11467
+ return 0;
11468
+ }
11469
+ /************************************************************
11309
11470
  * EV_LANGUAGE_CHANGED — re-translate chrome + re-render
11310
11471
  ************************************************************/
11311
11472
  function ac_language_changed$3(gobj, event, kw, src) {
@@ -11393,6 +11554,11 @@ function create_gclass$10(gclass_name) {
11393
11554
  ac_set_json,
11394
11555
  null
11395
11556
  ],
11557
+ [
11558
+ "EV_JSON_ITEM_CLICKED",
11559
+ ac_json_item_clicked,
11560
+ null
11561
+ ],
11396
11562
  [
11397
11563
  "EV_SUBTREE_LOADED",
11398
11564
  ac_subtree_loaded,
@@ -11470,6 +11636,7 @@ function create_gclass$10(gclass_name) {
11470
11636
  ["EV_COLLAPSE_ALL", 0],
11471
11637
  ["EV_COPY_ALL", 0],
11472
11638
  ["EV_SET_VIEW_MODE", 0],
11639
+ ["EV_JSON_ITEM_CLICKED", _yuneta_gobj_js.event_flag_t.EVF_OUTPUT_EVENT | _yuneta_gobj_js.event_flag_t.EVF_NO_WARN_SUBS],
11473
11640
  ["EV_LANGUAGE_CHANGED", 0],
11474
11641
  ["EV_REFRESH", 0],
11475
11642
  ["EV_SHOW", 0],
@@ -11485,6 +11652,7 @@ function create_gclass$10(gclass_name) {
11485
11652
  ***************************************************************/
11486
11653
  function register_c_yui_json() {
11487
11654
  if ((0, _yuneta_gobj_js.gclass_find_by_name)(GCLASS_NAME$10, false)) return 0;
11655
+ if (!(0, _yuneta_gobj_js.gclass_find_by_name)("C_YUI_JSON_GRAPH", false)) register_c_yui_json_graph();
11488
11656
  return create_gclass$10(GCLASS_NAME$10);
11489
11657
  }
11490
11658
  //#endregion