@yuneta/gobj-ui 7.22.0 → 7.23.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
@@ -444,7 +444,16 @@ same two facilities the tree has: a **find box** (rate-limited, `EV_FIND_NODES`)
444
444
  that highlights matching rows and outlines the cards they are in and says how
445
445
  many matched, and **expand-all / collapse-all** (`EV_EXPAND_ALL` /
446
446
  `EV_COLLAPSE_ALL`) that fold every card but the root, marking each cut with
447
- `▸ N` so the shape stays legible and you can see where the rest went. A find
447
+ `▸ N` so the shape stays legible and you can see where the rest went. Since
448
+ `7.23.0` each card that HAS a branch also carries its own handle in its header
449
+ (`▾` open, `▸ N` folded, `EV_TOGGLE_FOLD {path}`); a leaf gets a spacer instead,
450
+ because a handle that does nothing is worse than none.
451
+
452
+ The handle is delegated from the canvas mount in the **capture** phase, over
453
+ `pointerdown`/`pointerup`/`mousedown`/`click`: the card is an `innerHTML`
454
+ string so nothing can be bound to it directly, G6 binds on the node element
455
+ below, and G6 builds its click from the POINTER sequence — swallowing `click`
456
+ alone leaves the fold working and the card also reporting an item click. A find
448
457
  does not move the camera — a viewport that jumps on every keystroke is
449
458
  unusable — which is why the count is there.
450
459
 
@@ -491,11 +500,13 @@ Two layout facts the browser taught this component, both worth keeping:
491
500
  `"text"` / `"graph"`; **no mode advances** to the next view, which is what
492
501
  the two-view toggle did when the list was two long), plus `EV_REFRESH` /
493
502
  `EV_SHOW` / `EV_HIDE` / `EV_LANGUAGE_CHANGED`.
494
- - Output events: `EV_EXPAND_PATH {path, size}` (`EVF_OUTPUT_EVENT`) — the parent
495
- must declare it in its own FSM (CHILD subscription model) — and
496
- `EV_JSON_ITEM_CLICKED`, republished from the graph child so the host has one
497
- contract and never has to know that child exists (`EVF_NO_WARN_SUBS`: most
498
- hosts do not care).
503
+ - Output event: `EV_EXPAND_PATH {path, size}` (`EVF_OUTPUT_EVENT`) — the parent
504
+ must declare it in its own FSM (CHILD subscription model). That is the ONLY
505
+ one, on purpose: this viewer is a child of its host and subscribes it to
506
+ everything it publishes, so every output event is a mandatory declaration in
507
+ every host's FSM. The graph child's `EV_JSON_ITEM_CLICKED` stops here
508
+ (`7.21.0` forwarded it and broke exactly that way); a host that wants node
509
+ clicks mounts `C_YUI_JSON_GRAPH` itself.
499
510
  - Internal (DOM → FSM): `EV_TOGGLE_NODE`, `EV_EXPAND_COLLAPSED`, `EV_SEARCH`,
500
511
  `EV_EXPAND_ALL`, `EV_COLLAPSE_ALL`, `EV_COPY_ALL`. Every kw carries only a
501
512
  `path` string — never a DOM node or gobj.
@@ -9473,6 +9473,12 @@ function ensure_drag_canvas_patch() {
9473
9473
  * Constants
9474
9474
  ***************************************************************/
9475
9475
  var GCLASS_NAME$11 = "C_YUI_JSON_GRAPH";
9476
+ var FOLD_SWALLOWED_EVENTS = [
9477
+ "pointerdown",
9478
+ "pointerup",
9479
+ "mousedown",
9480
+ "click"
9481
+ ];
9476
9482
  /***************************************************************
9477
9483
  * Data
9478
9484
  ***************************************************************/
@@ -9497,6 +9503,7 @@ var PRIVATE_DATA$11 = {
9497
9503
  collapsed: null,
9498
9504
  match_count: 0,
9499
9505
  find_timer: null,
9506
+ fold_listener: null,
9500
9507
  $find_input: null,
9501
9508
  $find_result: null,
9502
9509
  $find_count: null
@@ -9659,6 +9666,18 @@ function build_ui$10(gobj) {
9659
9666
  ]]
9660
9667
  ]);
9661
9668
  (0, _yuneta_gobj_js.gobj_write_attr)(gobj, "$container", $container);
9669
+ let $mount = $container.querySelector("#" + priv.canvas_id);
9670
+ if ($mount) {
9671
+ priv.fold_listener = function(evt) {
9672
+ let $handle = evt.target && evt.target.closest ? evt.target.closest("[data-fold-path]") : null;
9673
+ if (!$handle) return;
9674
+ evt.stopPropagation();
9675
+ evt.preventDefault();
9676
+ if (evt.type !== "click") return;
9677
+ (0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_TOGGLE_FOLD", { path: $handle.getAttribute("data-fold-path") }, gobj);
9678
+ };
9679
+ for (let type of FOLD_SWALLOWED_EVENTS) $mount.addEventListener(type, priv.fold_listener, true);
9680
+ } else (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: no canvas mount, fold handles are dead`);
9662
9681
  priv.$find_input = $container.querySelector(".JSON_GRAPH_FIND_INPUT");
9663
9682
  priv.$find_result = $container.querySelector(".JSON_GRAPH_FIND_RESULT");
9664
9683
  priv.$find_count = $container.querySelector(".JSON_GRAPH_FIND_COUNT");
@@ -9668,7 +9687,13 @@ function build_ui$10(gobj) {
9668
9687
  * Destroy UI
9669
9688
  ************************************************************/
9670
9689
  function destroy_ui$6(gobj) {
9690
+ let priv = gobj.priv;
9671
9691
  let $container = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "$container");
9692
+ if (priv.fold_listener) {
9693
+ let $mount = $container ? $container.querySelector("#" + priv.canvas_id) : null;
9694
+ if ($mount) for (let type of FOLD_SWALLOWED_EVENTS) $mount.removeEventListener(type, priv.fold_listener, true);
9695
+ priv.fold_listener = null;
9696
+ }
9672
9697
  if ($container) {
9673
9698
  if ($container.parentNode) $container.parentNode.removeChild($container);
9674
9699
  (0, _yuneta_gobj_js.gobj_write_attr)(gobj, "$container", null);
@@ -9906,6 +9931,26 @@ function build_cell_html(key, value, type, dark, matched) {
9906
9931
  return row;
9907
9932
  }
9908
9933
  /************************************************************
9934
+ * The per-node fold handle, drawn in the card's header.
9935
+ *
9936
+ * Only a card with a BRANCH gets one — a leaf has nothing to
9937
+ * fold, and a handle that does nothing is worse than none.
9938
+ * It carries the path in `data-fold-path`, which is what the
9939
+ * delegated listener in build_ui reads; the card is an
9940
+ * innerHTML string, so there is no other way to hang a
9941
+ * handler on it.
9942
+ *
9943
+ * Folded it shows the glyph AND the count of branches hidden
9944
+ * under it; open, just the glyph. No word, no title: a word
9945
+ * inside this string could carry no i18n key.
9946
+ ************************************************************/
9947
+ function fold_toggle_html(path, foldable, folded, branches) {
9948
+ if (!foldable) return `<span style="width:1.1em; flex:0 0 auto;"></span>`;
9949
+ let glyph = folded ? "&#9656;" : "&#9662;";
9950
+ let text = folded ? `${glyph} ${branches}` : glyph;
9951
+ return `<span data-fold-path="${(0, _yuneta_gobj_js.escapeHtml)(path)}" style="flex:0 0 auto; cursor:pointer; user-select:none; padding:0 .2em; min-width:1.1em; text-align:left;">${text}</span>`;
9952
+ }
9953
+ /************************************************************
9909
9954
  * Does this key/value pair match the find term?
9910
9955
  *
9911
9956
  * Scalars are matched on what the card SHOWS. A complex value
@@ -9990,8 +10035,8 @@ function build_json_nodes(gobj, path, kw, nodes, edges, parent_id) {
9990
10035
  });
9991
10036
  }
9992
10037
  let label = get_group_label(path) || (is_dict ? "{}" : "[]");
9993
- let folded = priv.collapsed.has(path) && pending_complex.length > 0;
9994
- if (folded) lines.push(`<span style="color:${type_color("number", dark)}; font-weight:bold;">&#9656; ${pending_complex.length}</span>`);
10038
+ let foldable = pending_complex.length > 0;
10039
+ let folded = foldable && priv.collapsed.has(path);
9995
10040
  let colors = json_card_style(is_dict ? GROUP_COLORS.dict : GROUP_COLORS.list, dark);
9996
10041
  let border_color = card_matched ? dark ? "#ffb300" : "#ff8f00" : colors.border;
9997
10042
  let border_width = card_matched ? 2 : 1;
@@ -10012,8 +10057,10 @@ function build_json_nodes(gobj, path, kw, nodes, edges, parent_id) {
10012
10057
  font-weight: bold;
10013
10058
  font-size: 12px;
10014
10059
  padding: 3px 8px;
10015
- text-align: center;
10016
- ">${(0, _yuneta_gobj_js.escapeHtml)(label)}</div>
10060
+ display: flex;
10061
+ align-items: center;
10062
+ gap: 6px;
10063
+ ">${fold_toggle_html(path, foldable, folded, pending_complex.length)}<span style="flex:1 1 auto; text-align:center;">${(0, _yuneta_gobj_js.escapeHtml)(label)}</span></div>
10017
10064
  ${content_html}
10018
10065
  </div>`;
10019
10066
  let node = {
@@ -10230,6 +10277,27 @@ function ac_refresh$5(gobj, event, kw, src) {
10230
10277
  }
10231
10278
  /************************************************************
10232
10279
  * {theme: "dark"|"light"} — the app switched theme.
10280
+ * EV_TOGGLE_FOLD { path } — fold or unfold ONE card.
10281
+ *
10282
+ * The camera does not move: folding one branch is a local
10283
+ * edit of the picture, and refitting the whole graph for it
10284
+ * would throw away where the reader was looking. The toolbar
10285
+ * pair, which changes everything at once, does refit.
10286
+ ************************************************************/
10287
+ function ac_toggle_fold(gobj, event, kw, src) {
10288
+ let priv = gobj.priv;
10289
+ let path = kw && kw.path || "";
10290
+ if ((0, _yuneta_gobj_js.empty_string)(path)) {
10291
+ (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: EV_TOGGLE_FOLD with no path`);
10292
+ return -1;
10293
+ }
10294
+ if (priv.collapsed.has(path)) priv.collapsed.delete(path);
10295
+ else priv.collapsed.add(path);
10296
+ refresh_json(gobj, { preserve_view: true });
10297
+ update_find_result(gobj);
10298
+ return 0;
10299
+ }
10300
+ /************************************************************
10233
10301
  * EV_FIND_NODES { text }
10234
10302
  *
10235
10303
  * The camera does NOT move: a find is a way of reading what is
@@ -10418,6 +10486,11 @@ function create_gclass$11(gclass_name) {
10418
10486
  ac_find_nodes$2,
10419
10487
  null
10420
10488
  ],
10489
+ [
10490
+ "EV_TOGGLE_FOLD",
10491
+ ac_toggle_fold,
10492
+ null
10493
+ ],
10421
10494
  [
10422
10495
  "EV_EXPAND_ALL",
10423
10496
  ac_expand_all$2,
@@ -10478,6 +10551,7 @@ function create_gclass$11(gclass_name) {
10478
10551
  ["EV_LOAD_DATA", 0],
10479
10552
  ["EV_REFRESH", 0],
10480
10553
  ["EV_FIND_NODES", 0],
10554
+ ["EV_TOGGLE_FOLD", 0],
10481
10555
  ["EV_EXPAND_ALL", 0],
10482
10556
  ["EV_COLLAPSE_ALL", 0],
10483
10557
  ["EV_THEME", 0],
@@ -11681,11 +11755,20 @@ function ac_set_view_mode(gobj, event, kw, src) {
11681
11755
  /************************************************************
11682
11756
  * EV_JSON_ITEM_CLICKED — from the hosted graph child.
11683
11757
  *
11684
- * Republished under this gclass's own name so the host has ONE
11685
- * contract and never has to know the child exists.
11758
+ * It STOPS here, and that is deliberate. 7.21.0 republished
11759
+ * it "so the host has one contract", which got the framework
11760
+ * backwards: this viewer is a CHILD of its host and subscribes
11761
+ * it to everything it publishes, so a new output event is a
11762
+ * new MANDATORY declaration in every host's FSM. None of the
11763
+ * six that mount this viewer asked for node clicks, and every
11764
+ * one of them answered a click on a graph card with
11765
+ * "Event NOT DEFINED in state".
11766
+ *
11767
+ * A host that wants node clicks mounts C_YUI_JSON_GRAPH
11768
+ * itself — that gclass publishes them, and a host that mounts
11769
+ * it has declared them.
11686
11770
  ************************************************************/
11687
11771
  function ac_json_item_clicked(gobj, event, kw, src) {
11688
- (0, _yuneta_gobj_js.gobj_publish_event)(gobj, "EV_JSON_ITEM_CLICKED", kw);
11689
11772
  return 0;
11690
11773
  }
11691
11774
  /************************************************************
@@ -11858,7 +11941,7 @@ function create_gclass$10(gclass_name) {
11858
11941
  ["EV_COLLAPSE_ALL", 0],
11859
11942
  ["EV_COPY_ALL", 0],
11860
11943
  ["EV_SET_VIEW_MODE", 0],
11861
- ["EV_JSON_ITEM_CLICKED", _yuneta_gobj_js.event_flag_t.EVF_OUTPUT_EVENT | _yuneta_gobj_js.event_flag_t.EVF_NO_WARN_SUBS],
11944
+ ["EV_JSON_ITEM_CLICKED", 0],
11862
11945
  ["EV_LANGUAGE_CHANGED", 0],
11863
11946
  ["EV_REFRESH", 0],
11864
11947
  ["EV_SHOW", 0],
@@ -9468,6 +9468,12 @@ function ensure_drag_canvas_patch() {
9468
9468
  * Constants
9469
9469
  ***************************************************************/
9470
9470
  var GCLASS_NAME$11 = "C_YUI_JSON_GRAPH";
9471
+ var FOLD_SWALLOWED_EVENTS = [
9472
+ "pointerdown",
9473
+ "pointerup",
9474
+ "mousedown",
9475
+ "click"
9476
+ ];
9471
9477
  /***************************************************************
9472
9478
  * Data
9473
9479
  ***************************************************************/
@@ -9492,6 +9498,7 @@ var PRIVATE_DATA$11 = {
9492
9498
  collapsed: null,
9493
9499
  match_count: 0,
9494
9500
  find_timer: null,
9501
+ fold_listener: null,
9495
9502
  $find_input: null,
9496
9503
  $find_result: null,
9497
9504
  $find_count: null
@@ -9654,6 +9661,18 @@ function build_ui$10(gobj) {
9654
9661
  ]]
9655
9662
  ]);
9656
9663
  gobj_write_attr(gobj, "$container", $container);
9664
+ let $mount = $container.querySelector("#" + priv.canvas_id);
9665
+ if ($mount) {
9666
+ priv.fold_listener = function(evt) {
9667
+ let $handle = evt.target && evt.target.closest ? evt.target.closest("[data-fold-path]") : null;
9668
+ if (!$handle) return;
9669
+ evt.stopPropagation();
9670
+ evt.preventDefault();
9671
+ if (evt.type !== "click") return;
9672
+ gobj_send_event(gobj, "EV_TOGGLE_FOLD", { path: $handle.getAttribute("data-fold-path") }, gobj);
9673
+ };
9674
+ for (let type of FOLD_SWALLOWED_EVENTS) $mount.addEventListener(type, priv.fold_listener, true);
9675
+ } else log_error(`${gobj_short_name(gobj)}: no canvas mount, fold handles are dead`);
9657
9676
  priv.$find_input = $container.querySelector(".JSON_GRAPH_FIND_INPUT");
9658
9677
  priv.$find_result = $container.querySelector(".JSON_GRAPH_FIND_RESULT");
9659
9678
  priv.$find_count = $container.querySelector(".JSON_GRAPH_FIND_COUNT");
@@ -9663,7 +9682,13 @@ function build_ui$10(gobj) {
9663
9682
  * Destroy UI
9664
9683
  ************************************************************/
9665
9684
  function destroy_ui$6(gobj) {
9685
+ let priv = gobj.priv;
9666
9686
  let $container = gobj_read_attr(gobj, "$container");
9687
+ if (priv.fold_listener) {
9688
+ let $mount = $container ? $container.querySelector("#" + priv.canvas_id) : null;
9689
+ if ($mount) for (let type of FOLD_SWALLOWED_EVENTS) $mount.removeEventListener(type, priv.fold_listener, true);
9690
+ priv.fold_listener = null;
9691
+ }
9667
9692
  if ($container) {
9668
9693
  if ($container.parentNode) $container.parentNode.removeChild($container);
9669
9694
  gobj_write_attr(gobj, "$container", null);
@@ -9901,6 +9926,26 @@ function build_cell_html(key, value, type, dark, matched) {
9901
9926
  return row;
9902
9927
  }
9903
9928
  /************************************************************
9929
+ * The per-node fold handle, drawn in the card's header.
9930
+ *
9931
+ * Only a card with a BRANCH gets one — a leaf has nothing to
9932
+ * fold, and a handle that does nothing is worse than none.
9933
+ * It carries the path in `data-fold-path`, which is what the
9934
+ * delegated listener in build_ui reads; the card is an
9935
+ * innerHTML string, so there is no other way to hang a
9936
+ * handler on it.
9937
+ *
9938
+ * Folded it shows the glyph AND the count of branches hidden
9939
+ * under it; open, just the glyph. No word, no title: a word
9940
+ * inside this string could carry no i18n key.
9941
+ ************************************************************/
9942
+ function fold_toggle_html(path, foldable, folded, branches) {
9943
+ if (!foldable) return `<span style="width:1.1em; flex:0 0 auto;"></span>`;
9944
+ let glyph = folded ? "&#9656;" : "&#9662;";
9945
+ let text = folded ? `${glyph} ${branches}` : glyph;
9946
+ return `<span data-fold-path="${escapeHtml(path)}" style="flex:0 0 auto; cursor:pointer; user-select:none; padding:0 .2em; min-width:1.1em; text-align:left;">${text}</span>`;
9947
+ }
9948
+ /************************************************************
9904
9949
  * Does this key/value pair match the find term?
9905
9950
  *
9906
9951
  * Scalars are matched on what the card SHOWS. A complex value
@@ -9985,8 +10030,8 @@ function build_json_nodes(gobj, path, kw, nodes, edges, parent_id) {
9985
10030
  });
9986
10031
  }
9987
10032
  let label = get_group_label(path) || (is_dict ? "{}" : "[]");
9988
- let folded = priv.collapsed.has(path) && pending_complex.length > 0;
9989
- if (folded) lines.push(`<span style="color:${type_color("number", dark)}; font-weight:bold;">&#9656; ${pending_complex.length}</span>`);
10033
+ let foldable = pending_complex.length > 0;
10034
+ let folded = foldable && priv.collapsed.has(path);
9990
10035
  let colors = json_card_style(is_dict ? GROUP_COLORS.dict : GROUP_COLORS.list, dark);
9991
10036
  let border_color = card_matched ? dark ? "#ffb300" : "#ff8f00" : colors.border;
9992
10037
  let border_width = card_matched ? 2 : 1;
@@ -10007,8 +10052,10 @@ function build_json_nodes(gobj, path, kw, nodes, edges, parent_id) {
10007
10052
  font-weight: bold;
10008
10053
  font-size: 12px;
10009
10054
  padding: 3px 8px;
10010
- text-align: center;
10011
- ">${escapeHtml(label)}</div>
10055
+ display: flex;
10056
+ align-items: center;
10057
+ gap: 6px;
10058
+ ">${fold_toggle_html(path, foldable, folded, pending_complex.length)}<span style="flex:1 1 auto; text-align:center;">${escapeHtml(label)}</span></div>
10012
10059
  ${content_html}
10013
10060
  </div>`;
10014
10061
  let node = {
@@ -10225,6 +10272,27 @@ function ac_refresh$5(gobj, event, kw, src) {
10225
10272
  }
10226
10273
  /************************************************************
10227
10274
  * {theme: "dark"|"light"} — the app switched theme.
10275
+ * EV_TOGGLE_FOLD { path } — fold or unfold ONE card.
10276
+ *
10277
+ * The camera does not move: folding one branch is a local
10278
+ * edit of the picture, and refitting the whole graph for it
10279
+ * would throw away where the reader was looking. The toolbar
10280
+ * pair, which changes everything at once, does refit.
10281
+ ************************************************************/
10282
+ function ac_toggle_fold(gobj, event, kw, src) {
10283
+ let priv = gobj.priv;
10284
+ let path = kw && kw.path || "";
10285
+ if (empty_string(path)) {
10286
+ log_error(`${gobj_short_name(gobj)}: EV_TOGGLE_FOLD with no path`);
10287
+ return -1;
10288
+ }
10289
+ if (priv.collapsed.has(path)) priv.collapsed.delete(path);
10290
+ else priv.collapsed.add(path);
10291
+ refresh_json(gobj, { preserve_view: true });
10292
+ update_find_result(gobj);
10293
+ return 0;
10294
+ }
10295
+ /************************************************************
10228
10296
  * EV_FIND_NODES { text }
10229
10297
  *
10230
10298
  * The camera does NOT move: a find is a way of reading what is
@@ -10413,6 +10481,11 @@ function create_gclass$11(gclass_name) {
10413
10481
  ac_find_nodes$2,
10414
10482
  null
10415
10483
  ],
10484
+ [
10485
+ "EV_TOGGLE_FOLD",
10486
+ ac_toggle_fold,
10487
+ null
10488
+ ],
10416
10489
  [
10417
10490
  "EV_EXPAND_ALL",
10418
10491
  ac_expand_all$2,
@@ -10473,6 +10546,7 @@ function create_gclass$11(gclass_name) {
10473
10546
  ["EV_LOAD_DATA", 0],
10474
10547
  ["EV_REFRESH", 0],
10475
10548
  ["EV_FIND_NODES", 0],
10549
+ ["EV_TOGGLE_FOLD", 0],
10476
10550
  ["EV_EXPAND_ALL", 0],
10477
10551
  ["EV_COLLAPSE_ALL", 0],
10478
10552
  ["EV_THEME", 0],
@@ -11676,11 +11750,20 @@ function ac_set_view_mode(gobj, event, kw, src) {
11676
11750
  /************************************************************
11677
11751
  * EV_JSON_ITEM_CLICKED — from the hosted graph child.
11678
11752
  *
11679
- * Republished under this gclass's own name so the host has ONE
11680
- * contract and never has to know the child exists.
11753
+ * It STOPS here, and that is deliberate. 7.21.0 republished
11754
+ * it "so the host has one contract", which got the framework
11755
+ * backwards: this viewer is a CHILD of its host and subscribes
11756
+ * it to everything it publishes, so a new output event is a
11757
+ * new MANDATORY declaration in every host's FSM. None of the
11758
+ * six that mount this viewer asked for node clicks, and every
11759
+ * one of them answered a click on a graph card with
11760
+ * "Event NOT DEFINED in state".
11761
+ *
11762
+ * A host that wants node clicks mounts C_YUI_JSON_GRAPH
11763
+ * itself — that gclass publishes them, and a host that mounts
11764
+ * it has declared them.
11681
11765
  ************************************************************/
11682
11766
  function ac_json_item_clicked(gobj, event, kw, src) {
11683
- gobj_publish_event(gobj, "EV_JSON_ITEM_CLICKED", kw);
11684
11767
  return 0;
11685
11768
  }
11686
11769
  /************************************************************
@@ -11853,7 +11936,7 @@ function create_gclass$10(gclass_name) {
11853
11936
  ["EV_COLLAPSE_ALL", 0],
11854
11937
  ["EV_COPY_ALL", 0],
11855
11938
  ["EV_SET_VIEW_MODE", 0],
11856
- ["EV_JSON_ITEM_CLICKED", event_flag_t.EVF_OUTPUT_EVENT | event_flag_t.EVF_NO_WARN_SUBS],
11939
+ ["EV_JSON_ITEM_CLICKED", 0],
11857
11940
  ["EV_LANGUAGE_CHANGED", 0],
11858
11941
  ["EV_REFRESH", 0],
11859
11942
  ["EV_SHOW", 0],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yuneta/gobj-ui",
3
- "version": "7.22.0",
3
+ "version": "7.23.0",
4
4
  "type": "module",
5
5
  "main": "dist/gobj-ui.cjs.js",
6
6
  "module": "dist/gobj-ui.es.js",
package/src/c_yui_json.js CHANGED
@@ -1175,12 +1175,21 @@ function ac_set_view_mode(gobj, event, kw, src)
1175
1175
  /************************************************************
1176
1176
  * EV_JSON_ITEM_CLICKED — from the hosted graph child.
1177
1177
  *
1178
- * Republished under this gclass's own name so the host has ONE
1179
- * contract and never has to know the child exists.
1178
+ * It STOPS here, and that is deliberate. 7.21.0 republished
1179
+ * it "so the host has one contract", which got the framework
1180
+ * backwards: this viewer is a CHILD of its host and subscribes
1181
+ * it to everything it publishes, so a new output event is a
1182
+ * new MANDATORY declaration in every host's FSM. None of the
1183
+ * six that mount this viewer asked for node clicks, and every
1184
+ * one of them answered a click on a graph card with
1185
+ * "Event NOT DEFINED in state".
1186
+ *
1187
+ * A host that wants node clicks mounts C_YUI_JSON_GRAPH
1188
+ * itself — that gclass publishes them, and a host that mounts
1189
+ * it has declared them.
1180
1190
  ************************************************************/
1181
1191
  function ac_json_item_clicked(gobj, event, kw, src)
1182
1192
  {
1183
- gobj_publish_event(gobj, "EV_JSON_ITEM_CLICKED", kw);
1184
1193
  return 0;
1185
1194
  }
1186
1195
 
@@ -1307,7 +1316,7 @@ function create_gclass(gclass_name)
1307
1316
  ["EV_COLLAPSE_ALL", 0],
1308
1317
  ["EV_COPY_ALL", 0],
1309
1318
  ["EV_SET_VIEW_MODE", 0],
1310
- ["EV_JSON_ITEM_CLICKED", event_flag_t.EVF_OUTPUT_EVENT|event_flag_t.EVF_NO_WARN_SUBS],
1319
+ ["EV_JSON_ITEM_CLICKED", 0],
1311
1320
  ["EV_LANGUAGE_CHANGED", 0],
1312
1321
  ["EV_REFRESH", 0],
1313
1322
  ["EV_SHOW", 0],
@@ -67,6 +67,13 @@ import { yui_is_dark, yui_theme_now, yui_watch_theme } from "./yui_theme.js";
67
67
  ***************************************************************/
68
68
  const GCLASS_NAME = "C_YUI_JSON_GRAPH";
69
69
 
70
+ /*
71
+ * What a click on a fold handle has to swallow so G6 never turns it
72
+ * into a node click. `click` is the one that ACTS; the rest are cut
73
+ * because G6 assembles its own click from the pointer sequence.
74
+ */
75
+ const FOLD_SWALLOWED_EVENTS = ["pointerdown", "pointerup", "mousedown", "click"];
76
+
70
77
  /***************************************************************
71
78
  * Data
72
79
  ***************************************************************/
@@ -102,6 +109,7 @@ let PRIVATE_DATA = {
102
109
  collapsed: null, // Set<string> of paths whose children are hidden
103
110
  match_count: 0, // matches of the last find, for the count chip
104
111
  find_timer: null, // rate-limits the find box
112
+ fold_listener: null, // delegated click on the card fold handles
105
113
  $find_input: null,
106
114
  $find_result: null,
107
115
  $find_count: null,
@@ -303,6 +311,46 @@ function build_ui(gobj)
303
311
 
304
312
  gobj_write_attr(gobj, "$container", $container);
305
313
 
314
+ /*
315
+ * The fold handles live inside an `innerHTML` string, so no handler
316
+ * can be attached to them: the click is delegated from the canvas
317
+ * mount, in the CAPTURE phase, over FOUR event types.
318
+ *
319
+ * Capture because G6 binds on the html node ELEMENT, a descendant
320
+ * of this one: in the bubble phase G6 has already run by the time
321
+ * this listener sees the event, so stopping it there stops nothing.
322
+ *
323
+ * Four types because G6 does not use the DOM `click` at all — it
324
+ * builds its own from the POINTER sequence. Swallowing `click`
325
+ * alone left the fold working and the card ALSO reporting an item
326
+ * click; `pointerdown`/`pointerup` are the pair that has to be cut,
327
+ * and `mousedown` goes with them for a browser that still sends it.
328
+ * Measured by instrumenting the mount, not guessed.
329
+ */
330
+ let $mount = $container.querySelector('#' + priv.canvas_id);
331
+ if($mount) {
332
+ priv.fold_listener = function(evt) {
333
+ let $handle = evt.target && evt.target.closest
334
+ ? evt.target.closest('[data-fold-path]')
335
+ : null;
336
+ if(!$handle) {
337
+ return;
338
+ }
339
+ evt.stopPropagation();
340
+ evt.preventDefault();
341
+ if(evt.type !== "click") {
342
+ return; /* swallowed so G6 never sees a node click */
343
+ }
344
+ gobj_send_event(gobj, "EV_TOGGLE_FOLD",
345
+ {path: $handle.getAttribute('data-fold-path')}, gobj);
346
+ };
347
+ for(let type of FOLD_SWALLOWED_EVENTS) {
348
+ $mount.addEventListener(type, priv.fold_listener, true);
349
+ }
350
+ } else {
351
+ log_error(`${gobj_short_name(gobj)}: no canvas mount, fold handles are dead`);
352
+ }
353
+
306
354
  priv.$find_input = $container.querySelector('.JSON_GRAPH_FIND_INPUT');
307
355
  priv.$find_result = $container.querySelector('.JSON_GRAPH_FIND_RESULT');
308
356
  priv.$find_count = $container.querySelector('.JSON_GRAPH_FIND_COUNT');
@@ -315,7 +363,19 @@ function build_ui(gobj)
315
363
  ************************************************************/
316
364
  function destroy_ui(gobj)
317
365
  {
366
+ let priv = gobj.priv;
318
367
  let $container = gobj_read_attr(gobj, "$container");
368
+
369
+ if(priv.fold_listener) {
370
+ let $mount = $container? $container.querySelector('#' + priv.canvas_id): null;
371
+ if($mount) {
372
+ for(let type of FOLD_SWALLOWED_EVENTS) {
373
+ $mount.removeEventListener(type, priv.fold_listener, true);
374
+ }
375
+ }
376
+ priv.fold_listener = null;
377
+ }
378
+
319
379
  if($container) {
320
380
  if($container.parentNode) {
321
381
  $container.parentNode.removeChild($container);
@@ -598,6 +658,36 @@ function build_cell_html(key, value, type, dark, matched)
598
658
  return row;
599
659
  }
600
660
 
661
+ /************************************************************
662
+ * The per-node fold handle, drawn in the card's header.
663
+ *
664
+ * Only a card with a BRANCH gets one — a leaf has nothing to
665
+ * fold, and a handle that does nothing is worse than none.
666
+ * It carries the path in `data-fold-path`, which is what the
667
+ * delegated listener in build_ui reads; the card is an
668
+ * innerHTML string, so there is no other way to hang a
669
+ * handler on it.
670
+ *
671
+ * Folded it shows the glyph AND the count of branches hidden
672
+ * under it; open, just the glyph. No word, no title: a word
673
+ * inside this string could carry no i18n key.
674
+ ************************************************************/
675
+ function fold_toggle_html(path, foldable, folded, branches)
676
+ {
677
+ if(!foldable) {
678
+ /* A spacer, so the label of a leaf card stays centred on the
679
+ * same axis as the label of a foldable one. */
680
+ return `<span style="width:1.1em; flex:0 0 auto;"></span>`;
681
+ }
682
+
683
+ let glyph = folded? "&#9656;": "&#9662;"; /* right / down */
684
+ let text = folded? `${glyph} ${branches}`: glyph;
685
+
686
+ return `<span data-fold-path="${escapeHtml(path)}" ` +
687
+ `style="flex:0 0 auto; cursor:pointer; user-select:none; ` +
688
+ `padding:0 .2em; min-width:1.1em; text-align:left;">${text}</span>`;
689
+ }
690
+
601
691
  /************************************************************
602
692
  * Does this key/value pair match the find term?
603
693
  *
@@ -725,17 +815,12 @@ function build_json_nodes(gobj, path, kw, nodes, edges, parent_id)
725
815
  * Folded: keep the card, drop the branch, and SAY how much went
726
816
  * with it. A cut that is not marked reads as a document that ends
727
817
  * there — the same rule the lazy tree follows for a `__collapsed__`
728
- * sentinel. The badge is a glyph and a number on purpose: the card
818
+ * sentinel. The mark is a glyph and a number on purpose: the card
729
819
  * is an innerHTML string, where a word could carry no i18n key and
730
820
  * would sit there in English forever.
731
821
  */
732
- let folded = priv.collapsed.has(path) && pending_complex.length > 0;
733
- if(folded) {
734
- lines.push(
735
- `<span style="color:${type_color("number", dark)}; font-weight:bold;">` +
736
- `&#9656; ${pending_complex.length}</span>`
737
- );
738
- }
822
+ let foldable = pending_complex.length > 0;
823
+ let folded = foldable && priv.collapsed.has(path);
739
824
  let colors = json_card_style(
740
825
  is_dict ? GROUP_COLORS.dict : GROUP_COLORS.list, dark
741
826
  );
@@ -766,8 +851,10 @@ function build_json_nodes(gobj, path, kw, nodes, edges, parent_id)
766
851
  font-weight: bold;
767
852
  font-size: 12px;
768
853
  padding: 3px 8px;
769
- text-align: center;
770
- ">${escapeHtml(label)}</div>
854
+ display: flex;
855
+ align-items: center;
856
+ gap: 6px;
857
+ ">${fold_toggle_html(path, foldable, folded, pending_complex.length)}<span style="flex:1 1 auto; text-align:center;">${escapeHtml(label)}</span></div>
771
858
  ${content_html}
772
859
  </div>`;
773
860
 
@@ -1097,6 +1184,35 @@ function ac_refresh(gobj, event, kw, src)
1097
1184
 
1098
1185
  /************************************************************
1099
1186
  * {theme: "dark"|"light"} — the app switched theme.
1187
+ * EV_TOGGLE_FOLD { path } — fold or unfold ONE card.
1188
+ *
1189
+ * The camera does not move: folding one branch is a local
1190
+ * edit of the picture, and refitting the whole graph for it
1191
+ * would throw away where the reader was looking. The toolbar
1192
+ * pair, which changes everything at once, does refit.
1193
+ ************************************************************/
1194
+ function ac_toggle_fold(gobj, event, kw, src)
1195
+ {
1196
+ let priv = gobj.priv;
1197
+ let path = (kw && kw.path) || "";
1198
+
1199
+ if(empty_string(path)) {
1200
+ log_error(`${gobj_short_name(gobj)}: EV_TOGGLE_FOLD with no path`);
1201
+ return -1;
1202
+ }
1203
+
1204
+ if(priv.collapsed.has(path)) {
1205
+ priv.collapsed.delete(path);
1206
+ } else {
1207
+ priv.collapsed.add(path);
1208
+ }
1209
+
1210
+ refresh_json(gobj, {preserve_view: true});
1211
+ update_find_result(gobj);
1212
+ return 0;
1213
+ }
1214
+
1215
+ /************************************************************
1100
1216
  * EV_FIND_NODES { text }
1101
1217
  *
1102
1218
  * The camera does NOT move: a find is a way of reading what is
@@ -1349,6 +1465,7 @@ function create_gclass(gclass_name)
1349
1465
  ["EV_LOAD_DATA", ac_load_data, null],
1350
1466
  ["EV_REFRESH", ac_refresh, null],
1351
1467
  ["EV_FIND_NODES", ac_find_nodes, null],
1468
+ ["EV_TOGGLE_FOLD", ac_toggle_fold, null],
1352
1469
  ["EV_EXPAND_ALL", ac_expand_all, null],
1353
1470
  ["EV_COLLAPSE_ALL", ac_collapse_all, null],
1354
1471
  ["EV_THEME", ac_theme, null],
@@ -1370,6 +1487,7 @@ function create_gclass(gclass_name)
1370
1487
  ["EV_LOAD_DATA", 0],
1371
1488
  ["EV_REFRESH", 0],
1372
1489
  ["EV_FIND_NODES", 0],
1490
+ ["EV_TOGGLE_FOLD", 0],
1373
1491
  ["EV_EXPAND_ALL", 0],
1374
1492
  ["EV_COLLAPSE_ALL", 0],
1375
1493
  ["EV_THEME", 0],