@yuneta/gobj-ui 7.23.139 → 7.23.141

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yuneta/gobj-ui",
3
- "version": "7.23.139",
3
+ "version": "7.23.141",
4
4
  "type": "module",
5
5
  "main": "dist/gobj-ui.cjs.js",
6
6
  "module": "dist/gobj-ui.es.js",
@@ -20,7 +20,7 @@
20
20
  "src/",
21
21
  "vite-plugin-yuneta-html.js"
22
22
  ],
23
- "description": "Yuneta UI Library \u2014 the active line: declarative shell (C_YUI_SHELL+NAV+PAGER+WIZARD) + windows, TreeDB views, forms, charts and maps. Consumed by wattyzer and the yunetas JS yunos (gui_agent, gui_treedb). The frozen legacy GClass GUI stack (C_YUI_MAIN/TABS/ROUTING) lives on the v1 branch, npm dist-tag 'legacy' (consumed by estadodelaire and hidraulia).",
23
+ "description": "Yuneta UI Library — the active line: declarative shell (C_YUI_SHELL+NAV+PAGER+WIZARD) + windows, TreeDB views, forms, charts and maps. Consumed by wattyzer and the yunetas JS yunos (gui_agent, gui_treedb). The frozen legacy GClass GUI stack (C_YUI_MAIN/TABS/ROUTING) lives on the v1 branch, npm dist-tag 'legacy' (consumed by estadodelaire and hidraulia).",
24
24
  "scripts": {
25
25
  "build": "vite build",
26
26
  "test": "vitest run",
@@ -13,8 +13,9 @@
13
13
  * The graph opens FOLDED, like a JSON viewer.
14
14
  *
15
15
  * Every record of every topic is fetched, but only the ones on
16
- * screen become G6 nodes: the roots, `expand_depth` levels under
17
- * them, one page (`fold_page_size`) of children per hook. Each
16
+ * screen become G6 nodes: the roots, and the MAIN tree open to
17
+ * the reader's level (`fold_level`, `expand_depth` until they
18
+ * step it), one page (`fold_page_size`) of children per hook. Each
18
19
  * card carries a pill per hook that has children -- `▸ devices
19
20
  * 142` -- which opens or folds that hook; an open hook with more
20
21
  * children than its page ends in a `+N` chip that opens the next
@@ -117,9 +118,10 @@ import {
117
118
  fold_build_model,
118
119
  fold_new_state,
119
120
  fold_visible_set,
120
- fold_expand_to_depth,
121
- fold_expand_all,
122
- fold_collapse_all,
121
+ fold_expand_to_level,
122
+ fold_open_levels,
123
+ fold_close_levels,
124
+ fold_level_clamp,
123
125
  fold_toggle,
124
126
  fold_show_more,
125
127
  fold_reveal,
@@ -434,7 +436,8 @@ SDATA(data_type_t.DTP_STRING, "hook_port_position", 0, "bottom", "Hook po
434
436
  SDATA(data_type_t.DTP_STRING, "fkey_port_position", 0, "top", "Fkey port position"),
435
437
 
436
438
  /*---------------- Folding ----------------*/
437
- SDATA(data_type_t.DTP_INTEGER, "expand_depth", 0, 2, "Levels open when a treedb loads: 1 = the roots alone, 2 = the roots and their children. Every open hook shows its first page"),
439
+ SDATA(data_type_t.DTP_INTEGER, "expand_depth", 0, 2, "Levels open when a treedb loads, until the reader steps (`fold_level`): 1 = the roots alone, 2 = the roots and their children. Every open hook shows its first page"),
440
+ SDATA(data_type_t.DTP_INTEGER, "fold_level", 0, 0, "The level of the MAIN tree the toolbar's stepper stands on: 1 = its roots, N = N-1 levels under them; 0 = `expand_depth`. The host persists it and sends EV_SET_FOLD_LEVEL; it is published back in EV_LEGEND_STATE (`fold`) with the levels the tree has"),
438
441
  SDATA(data_type_t.DTP_INTEGER, "fold_page_size", 0, 24, "Children of one hook shown per page; the `+N` chip after the page opens the next one"),
439
442
  SDATA(data_type_t.DTP_LIST, "hidden_topics", 0, "[]", "Topics left out of the tree: no card, no pill counts them, no edge reaches them"),
440
443
  SDATA(data_type_t.DTP_STRING, "main_topic", 0, "", "The topic the tree hangs from; empty = deduced (the one whose hooks reach the most others). Its parentless records are the roots; a parentless record of a topic linked to it is LOOSE and shown only on request"),
@@ -543,6 +546,7 @@ let PRIVATE_DATA = {
543
546
 
544
547
  /*---------------- folding ----------------*/
545
548
  expand_depth: 2,
549
+ fold_level: 0,
546
550
  fold_page_size: 24,
547
551
  hidden_topics: null,
548
552
  main_topic: "",
@@ -9541,6 +9545,13 @@ function publish_legend_state(gobj, visible)
9541
9545
  main_topic: model.main_topic,
9542
9546
  main_chosen: !!gobj_read_str_attr(gobj, "main_topic"),
9543
9547
  topics: topics,
9548
+ /* The toolbar's stepper: the tree it walks, where it stands,
9549
+ * and how deep it goes. `levels` 0 = nothing to step through. */
9550
+ fold: {
9551
+ topic: model.main_topic,
9552
+ level: current_fold_level(gobj),
9553
+ levels: model.levels,
9554
+ },
9544
9555
  });
9545
9556
  }
9546
9557
 
@@ -10003,14 +10014,13 @@ function ac_load_data(gobj, event, kw, src)
10003
10014
  }
10004
10015
 
10005
10016
  if(all_loaded && priv.graph) {
10006
- /* The tree of the records, opened `expand_depth` levels: the
10007
- * roots and their children by default. Only that much becomes
10008
- * G6 nodes -- a treedb of a thousand records opens as a
10009
- * handful of cards with a count on each cut. */
10017
+ /* The tree of the records, the main one opened to the reader's
10018
+ * level: the roots and their children by default. Only that
10019
+ * much becomes G6 nodes -- a treedb of a thousand records opens
10020
+ * as a handful of cards with a count on each cut. */
10010
10021
  rebuild_fold_model(gobj);
10011
- fold_expand_to_depth(
10012
- priv._fold_model, priv._fold_state,
10013
- gobj_read_integer_attr(gobj, "expand_depth")
10022
+ fold_expand_to_level(
10023
+ priv._fold_model, priv._fold_state, current_fold_level(gobj)
10014
10024
  );
10015
10025
 
10016
10026
  /* Before the layout runs, and only now: whether anybody has
@@ -10622,21 +10632,62 @@ function ac_show_more(gobj, event, kw, src)
10622
10632
  }
10623
10633
 
10624
10634
  /************************************************************
10625
- * The whole treedb, every hook open on all its children. This
10626
- * is the reader asking for the pile on purpose; the pages are
10627
- * the other way in.
10635
+ * The level of the main tree the toolbar's stepper stands on:
10636
+ * `fold_level` once the reader has stepped, `expand_depth`
10637
+ * until then, clamped to the levels the tree has.
10628
10638
  ************************************************************/
10629
- function ac_expand_all(gobj, event, kw, src)
10639
+ function current_fold_level(gobj)
10630
10640
  {
10631
10641
  let priv = gobj.priv;
10642
+ let level = gobj_read_integer_attr(gobj, "fold_level");
10632
10643
 
10644
+ if(!(level > 0)) {
10645
+ level = gobj_read_integer_attr(gobj, "expand_depth");
10646
+ }
10633
10647
  if(!priv._fold_model) {
10634
- log_error(`${gobj_short_name(gobj)}: expand all with no tree loaded`);
10648
+ return level;
10649
+ }
10650
+ return fold_level_clamp(priv._fold_model, level);
10651
+ }
10652
+
10653
+ /************************************************************
10654
+ * The toolbar's stepper: one level more or one less of the
10655
+ * MAIN tree (the host owns the number and persists it; this
10656
+ * applies it). Deeper opens the hooks above the new level,
10657
+ * shallower folds from it down -- a level is a floor, not a
10658
+ * picture, so what the reader opened by its pill stays (see
10659
+ * treedb_fold_model). Replaces expand all / collapse all
10660
+ * (7.23.141): the whole treedb at once was the pile, and
10661
+ * closing threw away everything the reader had opened.
10662
+ ************************************************************/
10663
+ function ac_set_fold_level(gobj, event, kw, src)
10664
+ {
10665
+ let priv = gobj.priv;
10666
+ let level = Number(kw && kw.level);
10667
+
10668
+ if(!(level > 0)) {
10669
+ log_error(`${gobj_short_name(gobj)}: fold level '${kw && kw.level}' is not a level`);
10635
10670
  return -1;
10636
10671
  }
10672
+ if(!priv._fold_model) {
10673
+ gobj_write_attr(gobj, "fold_level", level);
10674
+ return 0; /* applied when the records arrive */
10675
+ }
10676
+
10677
+ let from = current_fold_level(gobj);
10678
+ level = fold_level_clamp(priv._fold_model, level);
10679
+ gobj_write_attr(gobj, "fold_level", level);
10680
+ if(level === from) {
10681
+ return 0;
10682
+ }
10683
+
10637
10684
  let keep = fold_keep_node(gobj);
10638
10685
  let vp = yui_graph_viewport_of(priv.graph, keep);
10639
- fold_expand_all(priv._fold_model, priv._fold_state);
10686
+ if(level > from) {
10687
+ fold_open_levels(priv._fold_model, priv._fold_state, level);
10688
+ } else {
10689
+ fold_close_levels(priv._fold_model, priv._fold_state, level);
10690
+ }
10640
10691
  reconcile_fold(gobj, {keep: keep, keep_vp: vp});
10641
10692
 
10642
10693
  return 0;
@@ -10669,31 +10720,12 @@ function fold_keep_node(gobj)
10669
10720
  return "";
10670
10721
  }
10671
10722
 
10672
- /************************************************************
10673
- * The roots alone.
10674
- ************************************************************/
10675
- function ac_collapse_all(gobj, event, kw, src)
10676
- {
10677
- let priv = gobj.priv;
10678
-
10679
- if(!priv._fold_model) {
10680
- log_error(`${gobj_short_name(gobj)}: collapse all with no tree loaded`);
10681
- return -1;
10682
- }
10683
- let keep = fold_keep_node(gobj);
10684
- let vp = yui_graph_viewport_of(priv.graph, keep);
10685
- fold_collapse_all(priv._fold_model, priv._fold_state);
10686
- reconcile_fold(gobj, {keep: keep, keep_vp: vp});
10687
-
10688
- return 0;
10689
- }
10690
-
10691
10723
  /************************************************************
10692
10724
  * The legend's three settings, from the host, which persists
10693
10725
  * them. Each rebuilds the tree; the fold state is kept -- it is
10694
10726
  * keyed by group, and a group that is gone is never read -- except
10695
10727
  * for a new MAIN topic, whose roots are other roots: the tree
10696
- * opens again at `expand_depth`.
10728
+ * opens again at the reader's level, now counted from them.
10697
10729
  ************************************************************/
10698
10730
  function ac_set_hidden_topics(gobj, event, kw, src)
10699
10731
  {
@@ -10724,9 +10756,8 @@ function ac_set_main_topic(gobj, event, kw, src)
10724
10756
  let keep = fold_keep_node(gobj);
10725
10757
  let vp = yui_graph_viewport_of(priv.graph, keep);
10726
10758
  rebuild_fold_model(gobj);
10727
- fold_expand_to_depth(
10728
- priv._fold_model, priv._fold_state,
10729
- gobj_read_integer_attr(gobj, "expand_depth")
10759
+ fold_expand_to_level(
10760
+ priv._fold_model, priv._fold_state, current_fold_level(gobj)
10730
10761
  );
10731
10762
  reconcile_fold(gobj, {relayout: true, keep: keep, keep_vp: vp});
10732
10763
  return 0;
@@ -11404,8 +11435,7 @@ function create_gclass(gclass_name)
11404
11435
  ["EV_FIND_NODES", ac_find_nodes, null],
11405
11436
  ["EV_TOGGLE_FOLD", ac_toggle_fold, null],
11406
11437
  ["EV_SHOW_MORE", ac_show_more, null],
11407
- ["EV_EXPAND_ALL", ac_expand_all, null],
11408
- ["EV_COLLAPSE_ALL", ac_collapse_all, null],
11438
+ ["EV_SET_FOLD_LEVEL", ac_set_fold_level, null],
11409
11439
  ["EV_SET_HIDDEN_TOPICS", ac_set_hidden_topics, null],
11410
11440
  ["EV_SET_MAIN_TOPIC", ac_set_main_topic, null],
11411
11441
  ["EV_SET_LOOSE_TOPICS", ac_set_loose_topics, null],
@@ -11472,8 +11502,7 @@ function create_gclass(gclass_name)
11472
11502
  ["EV_CAMERA_CHANGED", event_flag_t.EVF_OUTPUT_EVENT|event_flag_t.EVF_NO_WARN_SUBS],
11473
11503
  ["EV_TOGGLE_FOLD", 0],
11474
11504
  ["EV_SHOW_MORE", 0],
11475
- ["EV_EXPAND_ALL", 0],
11476
- ["EV_COLLAPSE_ALL", 0],
11505
+ ["EV_SET_FOLD_LEVEL", 0],
11477
11506
  ["EV_SET_HIDDEN_TOPICS", 0],
11478
11507
  ["EV_SET_MAIN_TOPIC", 0],
11479
11508
  ["EV_SET_LOOSE_TOPICS", 0],
package/src/c_yui_node.js CHANGED
@@ -1324,6 +1324,12 @@ function ac_add_node(gobj, event, kw, src)
1324
1324
  if(spec.link) {
1325
1325
  kw_child.link = spec.link;
1326
1326
  }
1327
+ /* A declared attr of this gclass that the spec could not reach: a
1328
+ * node whose children are workspaces (a treedb: its topics, its
1329
+ * graph) is declared in the config like every other one. */
1330
+ if(spec.remember_position) {
1331
+ kw_child.remember_position = true;
1332
+ }
1327
1333
  let child = gobj_create_pure_child(spec.id, GCLASS_NAME, kw_child, gobj);
1328
1334
 
1329
1335
  let index = (typeof kw.index === "number") ? kw.index : priv.children.length;
@@ -117,6 +117,10 @@ let PRIVATE_DATA = {
117
117
  /* Section route -> last position visited inside it. Mirrors the
118
118
  * url for the duration of the page; see remember_position(). */
119
119
  section_pos: {},
120
+ /* The routes visited, newest last, bounded -- for "where did the
121
+ * reader leave this subtree" (yui_shell_last_route_under). Same
122
+ * nature as section_pos: a mirror of the url, for the page. */
123
+ route_mru: [],
120
124
  /* Sub-route contributor registry (ROUTING.md): a mounted view
121
125
  * declares the deep, view-owned children of its base route
122
126
  * (topics, /info, /schema, focus topics — subpaths that are NOT
@@ -1465,6 +1469,7 @@ function navigate_to(gobj, route, depth, no_drain)
1465
1469
  stage.active_route = matched_route;
1466
1470
  gobj_write_attr(gobj, "current_route", route);
1467
1471
  remember_position(gobj, route);
1472
+ remember_route(gobj, route);
1468
1473
 
1469
1474
  /* Show/hide secondary navs according to parent item */
1470
1475
  update_secondary_nav_visibility(gobj, entry);
@@ -3187,6 +3192,54 @@ function remembered_position(gobj, route)
3187
3192
  return last;
3188
3193
  }
3189
3194
 
3195
+ /************************************************************
3196
+ * Every route the shell lands on, newest last, a route once.
3197
+ * Bounded: it answers "where was the reader last under X", and
3198
+ * a page that has been open all day has no business growing it.
3199
+ ************************************************************/
3200
+ const ROUTE_MRU_MAX = 64;
3201
+
3202
+ function remember_route(gobj, route)
3203
+ {
3204
+ let priv = gobj.priv;
3205
+ let mru = priv.route_mru;
3206
+ let i = mru.indexOf(route);
3207
+ if(i >= 0) {
3208
+ mru.splice(i, 1);
3209
+ }
3210
+ mru.push(route);
3211
+ if(mru.length > ROUTE_MRU_MAX) {
3212
+ mru.splice(0, mru.length - ROUTE_MRU_MAX);
3213
+ }
3214
+ }
3215
+
3216
+ /************************************************************
3217
+ * Where the reader last was UNDER `route`: the most recent
3218
+ * visited route that is `route` itself or lies below it, or
3219
+ * `route` when none does. A mirror of the url for the page, like
3220
+ * the section memory -- never stored, never applied on its own:
3221
+ * a control that wants to go back to a view AS IT WAS LEFT asks,
3222
+ * and navigates there itself.
3223
+ *
3224
+ * What it is for: a treedb graph's focus lives in the url
3225
+ * (`<graph>/<topic>`), so a button that opened the bare route
3226
+ * took the focus away every time it was used.
3227
+ ************************************************************/
3228
+ function yui_shell_last_route_under(shell_gobj, route)
3229
+ {
3230
+ let priv = shell_gobj && shell_gobj.priv;
3231
+ if(!priv || !is_array(priv.route_mru) || !route) {
3232
+ return route;
3233
+ }
3234
+ for(let i = priv.route_mru.length - 1; i >= 0; i--) {
3235
+ let r = priv.route_mru[i];
3236
+ if(r === route || r.indexOf(route + "/") === 0) {
3237
+ return r;
3238
+ }
3239
+ }
3240
+ return route;
3241
+ }
3242
+
3190
3243
  /************************************************************
3191
3244
  * Resolve the shell that governs `gobj`: the nearest
3192
3245
  * C_YUI_SHELL ancestor, else the last shell created on the
@@ -3609,6 +3662,7 @@ export {
3609
3662
  yui_shell_of,
3610
3663
  yui_shell_zone,
3611
3664
  yui_shell_navigate,
3665
+ yui_shell_last_route_under,
3612
3666
  yui_shell_nav_map,
3613
3667
  yui_shell_set_sub_routes,
3614
3668
  yui_shell_register_event_handler,
@@ -82,7 +82,6 @@ import {
82
82
 
83
83
  import {attach_clear} from "./yui_inputs.js";
84
84
  import {register_c_g6_nodes_tree} from "./c_g6_nodes_tree.js";
85
- import {yui_graph_fold_items} from "./yui_graph_camera.js";
86
85
  import {yui_toolbar, yui_toolbar_icon, YUI_TOOLBAR_ICON_SIZE} from "./yui_toolbar.js";
87
86
  import {
88
87
  removeChildElements,
@@ -137,6 +136,7 @@ SDATA(data_type_t.DTP_LIST, "loose_topics", sdata_flag_t.SDF_PERSIST, "[
137
136
  SDATA(data_type_t.DTP_STRING, "node_mode", sdata_flag_t.SDF_PERSIST, "expanded", "How the records are drawn: `expanded` = the cards with their pills and ports, `compact` = one-line pills with the name, `shape` = coloured figures, the topology alone. User preference"),
138
137
  SDATA(data_type_t.DTP_BOOLEAN, "node_labels", sdata_flag_t.SDF_PERSIST, true, "A `shape` node says its name under its figure. User preference"),
139
138
  SDATA(data_type_t.DTP_DICT, "camera", sdata_flag_t.SDF_PERSIST, "{}", "Where the reader left the viewport: {zoom, x, y}. Handed to the engine on creation and written back when a move settles, so the graph opens where it was left. User preference, per treedb"),
139
+ SDATA(data_type_t.DTP_INTEGER, "fold_level", sdata_flag_t.SDF_PERSIST, 0, "The level of the MAIN tree the toolbar's stepper stands on: 1 = the main topic's roots, N = N-1 levels under them; 0 = never stepped, `expand_depth`. Handed to the engine on creation, so the graph opens at the level it was left. User preference, per treedb"),
140
140
 
141
141
  /*---------------- Remote Connection ----------------*/
142
142
  SDATA(data_type_t.DTP_POINTER, "gobj_remote_yuno", 0, null, "Remote Yuno to request data"),
@@ -269,6 +269,7 @@ function mt_create(gobj)
269
269
  hook_port_position: "bottom",
270
270
  fkey_port_position: "top",
271
271
  expand_depth: gobj_read_integer_attr(gobj, "expand_depth"),
272
+ fold_level: gobj_read_integer_attr(gobj, "fold_level"),
272
273
  fold_page_size: gobj_read_integer_attr(gobj, "fold_page_size"),
273
274
  minimap_min_nodes: gobj_read_integer_attr(gobj, "minimap_min_nodes"),
274
275
  with_gridline: gobj_read_bool_attr(gobj, "with_gridline"),
@@ -671,6 +672,117 @@ function refresh_node_shape_buttons(gobj, $root)
671
672
  }
672
673
  }
673
674
 
675
+ /************************************************************
676
+ * The fold STEPPER: one level less, where the tree stands, one
677
+ * level more.
678
+ *
679
+ * It replaces the fold pair (7.23.141). `expand all` opened every
680
+ * hook of the treedb -- the pile, on purpose, which on a treedb
681
+ * of thousands is a pile nobody reads -- and `collapse all` threw
682
+ * away everything the reader had opened. The stepper walks the
683
+ * MAIN tree a level at a time, the way the frontend view opens
684
+ * one level per press: `+` opens the next level under the main
685
+ * topic's roots, through every topic that is not hidden; `-`
686
+ * folds the deepest one. Pages and pills the reader opened stay.
687
+ *
688
+ * The readout says what the two buttons act on, which no chevron
689
+ * can: the main topic -- its swatch and its name, which is DATA
690
+ * and never translated -- and `level/levels`. On a phone the name
691
+ * goes and the numbers stay. The level is the reader's, per
692
+ * treedb: persisted (`fold_level`) and handed to the engine on
693
+ * creation, so a reload opens where it was left.
694
+ *
695
+ * Both buttons are born disabled: nothing can be stepped until
696
+ * the engine has said how deep the tree goes (EV_LEGEND_STATE).
697
+ ************************************************************/
698
+ function fold_level_items(gobj, wide)
699
+ {
700
+ let step_button = (cls, icon, key, event_name) => {
701
+ return ['button', {class: `${cls} button`, type: 'button',
702
+ style: {height: wide, width: '2.5em'},
703
+ disabled: true,
704
+ title: t(key), 'data-i18n-title': key,
705
+ 'aria-label': t(key), 'data-i18n-aria-label': key},
706
+ yui_toolbar_icon(icon),
707
+ {
708
+ click: (evt) => {
709
+ evt.stopPropagation();
710
+ gobj_send_event(gobj, event_name, {}, gobj);
711
+ }
712
+ }
713
+ ];
714
+ };
715
+
716
+ return [
717
+ ['div', {class: 'GRAPH_FOLD_STEPPER buttons has-addons',
718
+ style: 'margin:0; flex:0 0 auto; flex-wrap:nowrap; align-items:stretch;'}, [
719
+ /* The same two chevrons the pills speak: `▸` folded, `▾` open. */
720
+ step_button('GRAPH_FOLD_COLLAPSE', 'yi-chevron-right',
721
+ 'collapse one level', 'EV_COLLAPSE_LEVEL'),
722
+ /* A `div` with the button skin, like the legend's main
723
+ * chip: a readout between two buttons of one group, not
724
+ * a third button, and at full strength -- `is-static`
725
+ * is Bulma's grey of something switched off. */
726
+ ['div', {class: 'GRAPH_FOLD_LEVEL button',
727
+ style: {height: wide, cursor: 'default', gap: '.35rem'},
728
+ role: 'status',
729
+ title: t('fold level'), 'data-i18n-title': 'fold level'}, [
730
+ ['span', {class: 'GRAPH_FOLD_SWATCH',
731
+ style: 'display:none; width:.8rem; height:.8rem; border-radius:3px; ' +
732
+ 'flex:0 0 auto; border:1px solid rgba(0,0,0,.25);'}],
733
+ ['span', {class: 'GRAPH_FOLD_TOPIC is-hidden-mobile',
734
+ style: 'max-width:10em; overflow:hidden; text-overflow:ellipsis; ' +
735
+ 'white-space:nowrap; font-weight:600;'}],
736
+ /* The WORD, not just the numbers: the legend chip under
737
+ * it says `departments 2/3` too, and there it means
738
+ * records on screen. */
739
+ ['span', {class: 'GRAPH_FOLD_WORD', i18n: 'level'}, 'level'],
740
+ ['span', {class: 'GRAPH_FOLD_COUNT',
741
+ style: 'font-variant-numeric:tabular-nums;'}, '–'],
742
+ ]],
743
+ step_button('GRAPH_FOLD_EXPAND', 'yi-chevron-down',
744
+ 'expand one level', 'EV_EXPAND_LEVEL'),
745
+ ]],
746
+ ];
747
+ }
748
+
749
+ /* Paint the stepper from the engine's last word (EV_LEGEND_STATE). */
750
+ function refresh_fold_level(gobj)
751
+ {
752
+ let priv = gobj.priv;
753
+ let $container = gobj_read_attr(gobj, "$container");
754
+ if(!$container) {
755
+ return;
756
+ }
757
+ let $readout = $container.querySelector('.GRAPH_FOLD_LEVEL');
758
+ if(!$readout) {
759
+ return;
760
+ }
761
+
762
+ let state = priv._legend_state || {};
763
+ let fold = state.fold || {};
764
+ let levels = (fold.levels > 0)? fold.levels : 0;
765
+ let level = (fold.level > 0)? fold.level : 0;
766
+ let topic = fold.topic || "";
767
+ let entry = (is_array(state.topics)? state.topics : []).find((e) => e.topic === topic);
768
+
769
+ let $swatch = $readout.querySelector('.GRAPH_FOLD_SWATCH');
770
+ $swatch.style.display = topic? 'inline-block' : 'none';
771
+ $swatch.style.background = (entry && entry.color) || '#94a3b8';
772
+ $readout.querySelector('.GRAPH_FOLD_TOPIC').textContent = topic;
773
+ $readout.querySelector('.GRAPH_FOLD_COUNT').textContent =
774
+ levels? `${level}/${levels}` : '–';
775
+
776
+ let $collapse = $container.querySelector('.GRAPH_FOLD_COLLAPSE');
777
+ let $expand = $container.querySelector('.GRAPH_FOLD_EXPAND');
778
+ if($collapse) {
779
+ $collapse.disabled = !(levels && level > 1);
780
+ }
781
+ if($expand) {
782
+ $expand.disabled = !(levels && level < levels);
783
+ }
784
+ }
785
+
674
786
  function make_toolbar(gobj)
675
787
  {
676
788
  let priv = gobj.priv;
@@ -741,11 +853,9 @@ function make_toolbar(gobj)
741
853
  * Center: how the graph is SHOWN.
742
854
  */
743
855
  let center_items = [
744
- /* The fold pair, the same two chevrons the JSON graph and the
745
- * lazy tree use: the graph opens folded (see the G6 child), and
746
- * these are the whole thing and the roots alone. Refresh is the
747
- * way back to the default depth. */
748
- ...yui_graph_fold_items(gobj, gobj_read_str_attr(gobj, "wide")),
856
+ /* The fold stepper: one level less or more of the main tree,
857
+ * and between the two, which tree and which level. */
858
+ ...fold_level_items(gobj, gobj_read_str_attr(gobj, "wide")),
749
859
 
750
860
  /* The view of the nodes -- full cards, one-line pills or
751
861
  * figures -- and the name under a figure. */
@@ -2795,6 +2905,7 @@ function ac_legend_state(gobj, event, kw, src)
2795
2905
  let priv = gobj.priv;
2796
2906
  priv._legend_state = kw || null;
2797
2907
  refresh_legend(gobj);
2908
+ refresh_fold_level(gobj);
2798
2909
  return 0;
2799
2910
  }
2800
2911
 
@@ -2964,23 +3075,37 @@ function ac_find_nodes(gobj, event, kw, src)
2964
3075
  }
2965
3076
 
2966
3077
  /************************************************************
2967
- * The fold pair of the toolbar, forwarded to the G6 child,
2968
- * which owns the tree.
3078
+ * One press of the fold stepper (EV_EXPAND_LEVEL /
3079
+ * EV_COLLAPSE_LEVEL): the level moves by one, is SAVED (the
3080
+ * reader's, per treedb) and reaches the G6 child, which owns
3081
+ * the tree.
3082
+ *
3083
+ * The readout moves at once, not when the engine speaks again:
3084
+ * a reconcile is asynchronous, and a second press that came
3085
+ * before it would step from the level already left -- two
3086
+ * presses, one level.
2969
3087
  ************************************************************/
2970
- function ac_expand_all(gobj, event, kw, src)
3088
+ function ac_fold_level_step(gobj, event, kw, src)
2971
3089
  {
2972
3090
  let priv = gobj.priv;
2973
- if(priv.gobj_nodes_tree) {
2974
- gobj_send_event(priv.gobj_nodes_tree, "EV_EXPAND_ALL", {}, gobj);
3091
+ let fold = priv._legend_state && priv._legend_state.fold;
3092
+
3093
+ if(!fold || !(fold.levels > 0)) {
3094
+ log_error(`${gobj_short_name(gobj)}: ${event} with no tree to step through`);
3095
+ return -1;
3096
+ }
3097
+ let level = fold.level + ((event === "EV_EXPAND_LEVEL")? 1 : -1);
3098
+ if(level < 1 || level > fold.levels) {
3099
+ log_error(`${gobj_short_name(gobj)}: ${event}: level ${level} out of 1..${fold.levels}`);
3100
+ return -1;
2975
3101
  }
2976
- return 0;
2977
- }
2978
3102
 
2979
- function ac_collapse_all(gobj, event, kw, src)
2980
- {
2981
- let priv = gobj.priv;
3103
+ gobj_write_attr(gobj, "fold_level", level);
3104
+ gobj_save_persistent_attrs(gobj, "fold_level");
3105
+ fold.level = level;
3106
+ refresh_fold_level(gobj);
2982
3107
  if(priv.gobj_nodes_tree) {
2983
- gobj_send_event(priv.gobj_nodes_tree, "EV_COLLAPSE_ALL", {}, gobj);
3108
+ gobj_send_event(priv.gobj_nodes_tree, "EV_SET_FOLD_LEVEL", {level: level}, gobj);
2984
3109
  }
2985
3110
  return 0;
2986
3111
  }
@@ -3181,8 +3306,8 @@ function create_gclass(gclass_name)
3181
3306
  ["EV_SET_OPERATION_MODE", ac_set_operation_mode, null],
3182
3307
  ["EV_SET_FOCUS_TOPIC", ac_set_focus_topic, null],
3183
3308
  ["EV_FIND_NODES", ac_find_nodes, null],
3184
- ["EV_EXPAND_ALL", ac_expand_all, null],
3185
- ["EV_COLLAPSE_ALL", ac_collapse_all, null],
3309
+ ["EV_EXPAND_LEVEL", ac_fold_level_step, null],
3310
+ ["EV_COLLAPSE_LEVEL", ac_fold_level_step, null],
3186
3311
  ["EV_SET_NODE_MODE", ac_set_node_mode, null],
3187
3312
  ["EV_TOGGLE_NODE_LABELS", ac_toggle_node_labels, null],
3188
3313
  ["EV_LAYOUT_AUTOSET", ac_layout_autoset, null],
@@ -3226,8 +3351,8 @@ function create_gclass(gclass_name)
3226
3351
  ["EV_SET_OPERATION_MODE", 0],
3227
3352
  ["EV_SET_FOCUS_TOPIC", 0],
3228
3353
  ["EV_FIND_NODES", 0],
3229
- ["EV_EXPAND_ALL", 0],
3230
- ["EV_COLLAPSE_ALL", 0],
3354
+ ["EV_EXPAND_LEVEL", 0],
3355
+ ["EV_COLLAPSE_LEVEL", 0],
3231
3356
  ["EV_SET_NODE_MODE", 0],
3232
3357
  ["EV_TOGGLE_NODE_LABELS", 0],
3233
3358
  ["EV_FIND_RESULT", 0],