@yuneta/gobj-ui 7.22.0 → 7.23.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/README.md CHANGED
@@ -444,7 +444,24 @@ 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
+ (`EV_TOGGLE_FOLD {path}`); a leaf gets a spacer of the same width instead,
450
+ because a handle that does nothing is worse than none and the labels should
451
+ stay on one axis.
452
+
453
+ That handle is the **same filled chip the gobj tree draws** (`+N` folded, `−`
454
+ open — see `render_toggle_html` in `c_yui_gobj_tree_js.js`), and it is a chip
455
+ for a reason: `7.23.0` shipped it as a bare `▾` and on a phone it was
456
+ invisible. At the zoom that fits a document on screen a glyph is a couple of
457
+ pixels of ink; a filled chip is still a visible blob and reads as something you
458
+ press. `+N` also carries the count of what is hidden.
459
+
460
+ The handle is delegated from the canvas mount in the **capture** phase, over
461
+ `pointerdown`/`pointerup`/`mousedown`/`click`: the card is an `innerHTML`
462
+ string so nothing can be bound to it directly, G6 binds on the node element
463
+ below, and G6 builds its click from the POINTER sequence — swallowing `click`
464
+ alone leaves the fold working and the card also reporting an item click. A find
448
465
  does not move the camera — a viewport that jumps on every keystroke is
449
466
  unusable — which is why the count is there.
450
467
 
@@ -491,11 +508,13 @@ Two layout facts the browser taught this component, both worth keeping:
491
508
  `"text"` / `"graph"`; **no mode advances** to the next view, which is what
492
509
  the two-view toggle did when the list was two long), plus `EV_REFRESH` /
493
510
  `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).
511
+ - Output event: `EV_EXPAND_PATH {path, size}` (`EVF_OUTPUT_EVENT`) — the parent
512
+ must declare it in its own FSM (CHILD subscription model). That is the ONLY
513
+ one, on purpose: this viewer is a child of its host and subscribes it to
514
+ everything it publishes, so every output event is a mandatory declaration in
515
+ every host's FSM. The graph child's `EV_JSON_ITEM_CLICKED` stops here
516
+ (`7.21.0` forwarded it and broke exactly that way); a host that wants node
517
+ clicks mounts `C_YUI_JSON_GRAPH` itself.
499
518
  - Internal (DOM → FSM): `EV_TOGGLE_NODE`, `EV_EXPAND_COLLAPSED`, `EV_SEARCH`,
500
519
  `EV_EXPAND_ALL`, `EV_COLLAPSE_ALL`, `EV_COPY_ALL`. Every kw carries only a
501
520
  `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,51 @@ 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
+ * A filled CHIP, the same one the gobj tree draws
9944
+ * (`c_yui_gobj_tree_js.js`, render_toggle_html): background,
9945
+ * border, radius, and `+N` / `−` rather than a triangle. It
9946
+ * shipped once as a bare `▾` and on a phone it was invisible —
9947
+ * a glyph is a couple of pixels of ink at the zoom that fits a
9948
+ * document on screen, while a filled chip stays a visible blob
9949
+ * and reads as something you press. `+N` also carries the
9950
+ * count of what is hidden, which the open state does not need.
9951
+ *
9952
+ * No `title`: it would be a word inside an innerHTML string,
9953
+ * where it could carry no i18n key and would sit in English
9954
+ * forever. `+3` and `−` need no language.
9955
+ ************************************************************/
9956
+ function fold_toggle_html(path, foldable, folded, branches, colors) {
9957
+ if (!foldable) return `<span style="width:24px; flex:0 0 auto;"></span>`;
9958
+ let label = folded ? "+" + branches : "&#8722;";
9959
+ return `<span data-fold-path="${(0, _yuneta_gobj_js.escapeHtml)(path)}" style="
9960
+ flex: 0 0 auto;
9961
+ display: inline-flex;
9962
+ align-items: center;
9963
+ justify-content: center;
9964
+ min-width: 22px;
9965
+ height: 16px;
9966
+ padding: 0 5px;
9967
+ background: ${colors.bg};
9968
+ color: ${colors.border};
9969
+ border: 1px solid ${colors.bg};
9970
+ border-radius: 6px;
9971
+ font-size: 11px;
9972
+ font-weight: 700;
9973
+ line-height: 1;
9974
+ cursor: pointer;
9975
+ user-select: none;
9976
+ ">${label}</span>`;
9977
+ }
9978
+ /************************************************************
9909
9979
  * Does this key/value pair match the find term?
9910
9980
  *
9911
9981
  * Scalars are matched on what the card SHOWS. A complex value
@@ -9990,8 +10060,8 @@ function build_json_nodes(gobj, path, kw, nodes, edges, parent_id) {
9990
10060
  });
9991
10061
  }
9992
10062
  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>`);
10063
+ let foldable = pending_complex.length > 0;
10064
+ let folded = foldable && priv.collapsed.has(path);
9995
10065
  let colors = json_card_style(is_dict ? GROUP_COLORS.dict : GROUP_COLORS.list, dark);
9996
10066
  let border_color = card_matched ? dark ? "#ffb300" : "#ff8f00" : colors.border;
9997
10067
  let border_width = card_matched ? 2 : 1;
@@ -10012,8 +10082,10 @@ function build_json_nodes(gobj, path, kw, nodes, edges, parent_id) {
10012
10082
  font-weight: bold;
10013
10083
  font-size: 12px;
10014
10084
  padding: 3px 8px;
10015
- text-align: center;
10016
- ">${(0, _yuneta_gobj_js.escapeHtml)(label)}</div>
10085
+ display: flex;
10086
+ align-items: center;
10087
+ gap: 6px;
10088
+ ">${fold_toggle_html(path, foldable, folded, pending_complex.length, colors)}<span style="flex:1 1 auto; text-align:center;">${(0, _yuneta_gobj_js.escapeHtml)(label)}</span></div>
10017
10089
  ${content_html}
10018
10090
  </div>`;
10019
10091
  let node = {
@@ -10230,6 +10302,27 @@ function ac_refresh$5(gobj, event, kw, src) {
10230
10302
  }
10231
10303
  /************************************************************
10232
10304
  * {theme: "dark"|"light"} — the app switched theme.
10305
+ * EV_TOGGLE_FOLD { path } — fold or unfold ONE card.
10306
+ *
10307
+ * The camera does not move: folding one branch is a local
10308
+ * edit of the picture, and refitting the whole graph for it
10309
+ * would throw away where the reader was looking. The toolbar
10310
+ * pair, which changes everything at once, does refit.
10311
+ ************************************************************/
10312
+ function ac_toggle_fold(gobj, event, kw, src) {
10313
+ let priv = gobj.priv;
10314
+ let path = kw && kw.path || "";
10315
+ if ((0, _yuneta_gobj_js.empty_string)(path)) {
10316
+ (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: EV_TOGGLE_FOLD with no path`);
10317
+ return -1;
10318
+ }
10319
+ if (priv.collapsed.has(path)) priv.collapsed.delete(path);
10320
+ else priv.collapsed.add(path);
10321
+ refresh_json(gobj, { preserve_view: true });
10322
+ update_find_result(gobj);
10323
+ return 0;
10324
+ }
10325
+ /************************************************************
10233
10326
  * EV_FIND_NODES { text }
10234
10327
  *
10235
10328
  * The camera does NOT move: a find is a way of reading what is
@@ -10418,6 +10511,11 @@ function create_gclass$11(gclass_name) {
10418
10511
  ac_find_nodes$2,
10419
10512
  null
10420
10513
  ],
10514
+ [
10515
+ "EV_TOGGLE_FOLD",
10516
+ ac_toggle_fold,
10517
+ null
10518
+ ],
10421
10519
  [
10422
10520
  "EV_EXPAND_ALL",
10423
10521
  ac_expand_all$2,
@@ -10478,6 +10576,7 @@ function create_gclass$11(gclass_name) {
10478
10576
  ["EV_LOAD_DATA", 0],
10479
10577
  ["EV_REFRESH", 0],
10480
10578
  ["EV_FIND_NODES", 0],
10579
+ ["EV_TOGGLE_FOLD", 0],
10481
10580
  ["EV_EXPAND_ALL", 0],
10482
10581
  ["EV_COLLAPSE_ALL", 0],
10483
10582
  ["EV_THEME", 0],
@@ -11681,11 +11780,20 @@ function ac_set_view_mode(gobj, event, kw, src) {
11681
11780
  /************************************************************
11682
11781
  * EV_JSON_ITEM_CLICKED — from the hosted graph child.
11683
11782
  *
11684
- * Republished under this gclass's own name so the host has ONE
11685
- * contract and never has to know the child exists.
11783
+ * It STOPS here, and that is deliberate. 7.21.0 republished
11784
+ * it "so the host has one contract", which got the framework
11785
+ * backwards: this viewer is a CHILD of its host and subscribes
11786
+ * it to everything it publishes, so a new output event is a
11787
+ * new MANDATORY declaration in every host's FSM. None of the
11788
+ * six that mount this viewer asked for node clicks, and every
11789
+ * one of them answered a click on a graph card with
11790
+ * "Event NOT DEFINED in state".
11791
+ *
11792
+ * A host that wants node clicks mounts C_YUI_JSON_GRAPH
11793
+ * itself — that gclass publishes them, and a host that mounts
11794
+ * it has declared them.
11686
11795
  ************************************************************/
11687
11796
  function ac_json_item_clicked(gobj, event, kw, src) {
11688
- (0, _yuneta_gobj_js.gobj_publish_event)(gobj, "EV_JSON_ITEM_CLICKED", kw);
11689
11797
  return 0;
11690
11798
  }
11691
11799
  /************************************************************
@@ -11858,7 +11966,7 @@ function create_gclass$10(gclass_name) {
11858
11966
  ["EV_COLLAPSE_ALL", 0],
11859
11967
  ["EV_COPY_ALL", 0],
11860
11968
  ["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],
11969
+ ["EV_JSON_ITEM_CLICKED", 0],
11862
11970
  ["EV_LANGUAGE_CHANGED", 0],
11863
11971
  ["EV_REFRESH", 0],
11864
11972
  ["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,51 @@ 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
+ * A filled CHIP, the same one the gobj tree draws
9939
+ * (`c_yui_gobj_tree_js.js`, render_toggle_html): background,
9940
+ * border, radius, and `+N` / `−` rather than a triangle. It
9941
+ * shipped once as a bare `▾` and on a phone it was invisible —
9942
+ * a glyph is a couple of pixels of ink at the zoom that fits a
9943
+ * document on screen, while a filled chip stays a visible blob
9944
+ * and reads as something you press. `+N` also carries the
9945
+ * count of what is hidden, which the open state does not need.
9946
+ *
9947
+ * No `title`: it would be a word inside an innerHTML string,
9948
+ * where it could carry no i18n key and would sit in English
9949
+ * forever. `+3` and `−` need no language.
9950
+ ************************************************************/
9951
+ function fold_toggle_html(path, foldable, folded, branches, colors) {
9952
+ if (!foldable) return `<span style="width:24px; flex:0 0 auto;"></span>`;
9953
+ let label = folded ? "+" + branches : "&#8722;";
9954
+ return `<span data-fold-path="${escapeHtml(path)}" style="
9955
+ flex: 0 0 auto;
9956
+ display: inline-flex;
9957
+ align-items: center;
9958
+ justify-content: center;
9959
+ min-width: 22px;
9960
+ height: 16px;
9961
+ padding: 0 5px;
9962
+ background: ${colors.bg};
9963
+ color: ${colors.border};
9964
+ border: 1px solid ${colors.bg};
9965
+ border-radius: 6px;
9966
+ font-size: 11px;
9967
+ font-weight: 700;
9968
+ line-height: 1;
9969
+ cursor: pointer;
9970
+ user-select: none;
9971
+ ">${label}</span>`;
9972
+ }
9973
+ /************************************************************
9904
9974
  * Does this key/value pair match the find term?
9905
9975
  *
9906
9976
  * Scalars are matched on what the card SHOWS. A complex value
@@ -9985,8 +10055,8 @@ function build_json_nodes(gobj, path, kw, nodes, edges, parent_id) {
9985
10055
  });
9986
10056
  }
9987
10057
  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>`);
10058
+ let foldable = pending_complex.length > 0;
10059
+ let folded = foldable && priv.collapsed.has(path);
9990
10060
  let colors = json_card_style(is_dict ? GROUP_COLORS.dict : GROUP_COLORS.list, dark);
9991
10061
  let border_color = card_matched ? dark ? "#ffb300" : "#ff8f00" : colors.border;
9992
10062
  let border_width = card_matched ? 2 : 1;
@@ -10007,8 +10077,10 @@ function build_json_nodes(gobj, path, kw, nodes, edges, parent_id) {
10007
10077
  font-weight: bold;
10008
10078
  font-size: 12px;
10009
10079
  padding: 3px 8px;
10010
- text-align: center;
10011
- ">${escapeHtml(label)}</div>
10080
+ display: flex;
10081
+ align-items: center;
10082
+ gap: 6px;
10083
+ ">${fold_toggle_html(path, foldable, folded, pending_complex.length, colors)}<span style="flex:1 1 auto; text-align:center;">${escapeHtml(label)}</span></div>
10012
10084
  ${content_html}
10013
10085
  </div>`;
10014
10086
  let node = {
@@ -10225,6 +10297,27 @@ function ac_refresh$5(gobj, event, kw, src) {
10225
10297
  }
10226
10298
  /************************************************************
10227
10299
  * {theme: "dark"|"light"} — the app switched theme.
10300
+ * EV_TOGGLE_FOLD { path } — fold or unfold ONE card.
10301
+ *
10302
+ * The camera does not move: folding one branch is a local
10303
+ * edit of the picture, and refitting the whole graph for it
10304
+ * would throw away where the reader was looking. The toolbar
10305
+ * pair, which changes everything at once, does refit.
10306
+ ************************************************************/
10307
+ function ac_toggle_fold(gobj, event, kw, src) {
10308
+ let priv = gobj.priv;
10309
+ let path = kw && kw.path || "";
10310
+ if (empty_string(path)) {
10311
+ log_error(`${gobj_short_name(gobj)}: EV_TOGGLE_FOLD with no path`);
10312
+ return -1;
10313
+ }
10314
+ if (priv.collapsed.has(path)) priv.collapsed.delete(path);
10315
+ else priv.collapsed.add(path);
10316
+ refresh_json(gobj, { preserve_view: true });
10317
+ update_find_result(gobj);
10318
+ return 0;
10319
+ }
10320
+ /************************************************************
10228
10321
  * EV_FIND_NODES { text }
10229
10322
  *
10230
10323
  * The camera does NOT move: a find is a way of reading what is
@@ -10413,6 +10506,11 @@ function create_gclass$11(gclass_name) {
10413
10506
  ac_find_nodes$2,
10414
10507
  null
10415
10508
  ],
10509
+ [
10510
+ "EV_TOGGLE_FOLD",
10511
+ ac_toggle_fold,
10512
+ null
10513
+ ],
10416
10514
  [
10417
10515
  "EV_EXPAND_ALL",
10418
10516
  ac_expand_all$2,
@@ -10473,6 +10571,7 @@ function create_gclass$11(gclass_name) {
10473
10571
  ["EV_LOAD_DATA", 0],
10474
10572
  ["EV_REFRESH", 0],
10475
10573
  ["EV_FIND_NODES", 0],
10574
+ ["EV_TOGGLE_FOLD", 0],
10476
10575
  ["EV_EXPAND_ALL", 0],
10477
10576
  ["EV_COLLAPSE_ALL", 0],
10478
10577
  ["EV_THEME", 0],
@@ -11676,11 +11775,20 @@ function ac_set_view_mode(gobj, event, kw, src) {
11676
11775
  /************************************************************
11677
11776
  * EV_JSON_ITEM_CLICKED — from the hosted graph child.
11678
11777
  *
11679
- * Republished under this gclass's own name so the host has ONE
11680
- * contract and never has to know the child exists.
11778
+ * It STOPS here, and that is deliberate. 7.21.0 republished
11779
+ * it "so the host has one contract", which got the framework
11780
+ * backwards: this viewer is a CHILD of its host and subscribes
11781
+ * it to everything it publishes, so a new output event is a
11782
+ * new MANDATORY declaration in every host's FSM. None of the
11783
+ * six that mount this viewer asked for node clicks, and every
11784
+ * one of them answered a click on a graph card with
11785
+ * "Event NOT DEFINED in state".
11786
+ *
11787
+ * A host that wants node clicks mounts C_YUI_JSON_GRAPH
11788
+ * itself — that gclass publishes them, and a host that mounts
11789
+ * it has declared them.
11681
11790
  ************************************************************/
11682
11791
  function ac_json_item_clicked(gobj, event, kw, src) {
11683
- gobj_publish_event(gobj, "EV_JSON_ITEM_CLICKED", kw);
11684
11792
  return 0;
11685
11793
  }
11686
11794
  /************************************************************
@@ -11853,7 +11961,7 @@ function create_gclass$10(gclass_name) {
11853
11961
  ["EV_COLLAPSE_ALL", 0],
11854
11962
  ["EV_COPY_ALL", 0],
11855
11963
  ["EV_SET_VIEW_MODE", 0],
11856
- ["EV_JSON_ITEM_CLICKED", event_flag_t.EVF_OUTPUT_EVENT | event_flag_t.EVF_NO_WARN_SUBS],
11964
+ ["EV_JSON_ITEM_CLICKED", 0],
11857
11965
  ["EV_LANGUAGE_CHANGED", 0],
11858
11966
  ["EV_REFRESH", 0],
11859
11967
  ["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.1",
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,59 @@ 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
+ * A filled CHIP, the same one the gobj tree draws
672
+ * (`c_yui_gobj_tree_js.js`, render_toggle_html): background,
673
+ * border, radius, and `+N` / `−` rather than a triangle. It
674
+ * shipped once as a bare `▾` and on a phone it was invisible —
675
+ * a glyph is a couple of pixels of ink at the zoom that fits a
676
+ * document on screen, while a filled chip stays a visible blob
677
+ * and reads as something you press. `+N` also carries the
678
+ * count of what is hidden, which the open state does not need.
679
+ *
680
+ * No `title`: it would be a word inside an innerHTML string,
681
+ * where it could carry no i18n key and would sit in English
682
+ * forever. `+3` and `−` need no language.
683
+ ************************************************************/
684
+ function fold_toggle_html(path, foldable, folded, branches, colors)
685
+ {
686
+ if(!foldable) {
687
+ /* A spacer the width of the chip, so the label of a leaf card
688
+ * stays on the same axis as the label of a foldable one. */
689
+ return `<span style="width:24px; flex:0 0 auto;"></span>`;
690
+ }
691
+
692
+ let label = folded? ("+" + branches): "&#8722;"; /* +N / − */
693
+
694
+ return `<span data-fold-path="${escapeHtml(path)}" style="
695
+ flex: 0 0 auto;
696
+ display: inline-flex;
697
+ align-items: center;
698
+ justify-content: center;
699
+ min-width: 22px;
700
+ height: 16px;
701
+ padding: 0 5px;
702
+ background: ${colors.bg};
703
+ color: ${colors.border};
704
+ border: 1px solid ${colors.bg};
705
+ border-radius: 6px;
706
+ font-size: 11px;
707
+ font-weight: 700;
708
+ line-height: 1;
709
+ cursor: pointer;
710
+ user-select: none;
711
+ ">${label}</span>`;
712
+ }
713
+
601
714
  /************************************************************
602
715
  * Does this key/value pair match the find term?
603
716
  *
@@ -725,17 +838,12 @@ function build_json_nodes(gobj, path, kw, nodes, edges, parent_id)
725
838
  * Folded: keep the card, drop the branch, and SAY how much went
726
839
  * with it. A cut that is not marked reads as a document that ends
727
840
  * 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
841
+ * sentinel. The mark is a glyph and a number on purpose: the card
729
842
  * is an innerHTML string, where a word could carry no i18n key and
730
843
  * would sit there in English forever.
731
844
  */
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
- }
845
+ let foldable = pending_complex.length > 0;
846
+ let folded = foldable && priv.collapsed.has(path);
739
847
  let colors = json_card_style(
740
848
  is_dict ? GROUP_COLORS.dict : GROUP_COLORS.list, dark
741
849
  );
@@ -766,8 +874,10 @@ function build_json_nodes(gobj, path, kw, nodes, edges, parent_id)
766
874
  font-weight: bold;
767
875
  font-size: 12px;
768
876
  padding: 3px 8px;
769
- text-align: center;
770
- ">${escapeHtml(label)}</div>
877
+ display: flex;
878
+ align-items: center;
879
+ gap: 6px;
880
+ ">${fold_toggle_html(path, foldable, folded, pending_complex.length, colors)}<span style="flex:1 1 auto; text-align:center;">${escapeHtml(label)}</span></div>
771
881
  ${content_html}
772
882
  </div>`;
773
883
 
@@ -1097,6 +1207,35 @@ function ac_refresh(gobj, event, kw, src)
1097
1207
 
1098
1208
  /************************************************************
1099
1209
  * {theme: "dark"|"light"} — the app switched theme.
1210
+ * EV_TOGGLE_FOLD { path } — fold or unfold ONE card.
1211
+ *
1212
+ * The camera does not move: folding one branch is a local
1213
+ * edit of the picture, and refitting the whole graph for it
1214
+ * would throw away where the reader was looking. The toolbar
1215
+ * pair, which changes everything at once, does refit.
1216
+ ************************************************************/
1217
+ function ac_toggle_fold(gobj, event, kw, src)
1218
+ {
1219
+ let priv = gobj.priv;
1220
+ let path = (kw && kw.path) || "";
1221
+
1222
+ if(empty_string(path)) {
1223
+ log_error(`${gobj_short_name(gobj)}: EV_TOGGLE_FOLD with no path`);
1224
+ return -1;
1225
+ }
1226
+
1227
+ if(priv.collapsed.has(path)) {
1228
+ priv.collapsed.delete(path);
1229
+ } else {
1230
+ priv.collapsed.add(path);
1231
+ }
1232
+
1233
+ refresh_json(gobj, {preserve_view: true});
1234
+ update_find_result(gobj);
1235
+ return 0;
1236
+ }
1237
+
1238
+ /************************************************************
1100
1239
  * EV_FIND_NODES { text }
1101
1240
  *
1102
1241
  * The camera does NOT move: a find is a way of reading what is
@@ -1349,6 +1488,7 @@ function create_gclass(gclass_name)
1349
1488
  ["EV_LOAD_DATA", ac_load_data, null],
1350
1489
  ["EV_REFRESH", ac_refresh, null],
1351
1490
  ["EV_FIND_NODES", ac_find_nodes, null],
1491
+ ["EV_TOGGLE_FOLD", ac_toggle_fold, null],
1352
1492
  ["EV_EXPAND_ALL", ac_expand_all, null],
1353
1493
  ["EV_COLLAPSE_ALL", ac_collapse_all, null],
1354
1494
  ["EV_THEME", ac_theme, null],
@@ -1370,6 +1510,7 @@ function create_gclass(gclass_name)
1370
1510
  ["EV_LOAD_DATA", 0],
1371
1511
  ["EV_REFRESH", 0],
1372
1512
  ["EV_FIND_NODES", 0],
1513
+ ["EV_TOGGLE_FOLD", 0],
1373
1514
  ["EV_EXPAND_ALL", 0],
1374
1515
  ["EV_COLLAPSE_ALL", 0],
1375
1516
  ["EV_THEME", 0],