@yuneta/gobj-ui 7.23.65 → 7.23.66

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.
@@ -11249,7 +11249,7 @@ function ensure_drag_canvas_patch() {
11249
11249
  * Constants
11250
11250
  ***************************************************************/
11251
11251
  var GCLASS_NAME$13 = "C_YUI_JSON_GRAPH";
11252
- var FOLD_SWALLOWED_EVENTS = [
11252
+ var FOLD_SWALLOWED_EVENTS$1 = [
11253
11253
  "pointerdown",
11254
11254
  "pointerup",
11255
11255
  "mousedown",
@@ -11557,7 +11557,7 @@ function build_ui$12(gobj) {
11557
11557
  keep: $card ? $card.getAttribute("data-json-path") : ""
11558
11558
  }, gobj);
11559
11559
  };
11560
- for (let type of FOLD_SWALLOWED_EVENTS) $mount.addEventListener(type, priv.fold_listener, true);
11560
+ for (let type of FOLD_SWALLOWED_EVENTS$1) $mount.addEventListener(type, priv.fold_listener, true);
11561
11561
  } else log_error(`${gobj_short_name(gobj)}: no canvas mount, fold handles are dead`);
11562
11562
  priv.$layout_select = $container.querySelector(".JSON_GRAPH_LAYOUT");
11563
11563
  priv.$find_input = $container.querySelector(".JSON_GRAPH_FIND_INPUT");
@@ -11573,7 +11573,7 @@ function destroy_ui$8(gobj) {
11573
11573
  let $container = gobj_read_attr(gobj, "$container");
11574
11574
  if (priv.fold_listener) {
11575
11575
  let $mount = $container ? $container.querySelector("#" + priv.canvas_id) : null;
11576
- if ($mount) for (let type of FOLD_SWALLOWED_EVENTS) $mount.removeEventListener(type, priv.fold_listener, true);
11576
+ if ($mount) for (let type of FOLD_SWALLOWED_EVENTS$1) $mount.removeEventListener(type, priv.fold_listener, true);
11577
11577
  priv.fold_listener = null;
11578
11578
  }
11579
11579
  if ($container) {
@@ -12424,7 +12424,7 @@ function ac_change_layout$2(gobj, event, kw, src) {
12424
12424
  * would throw away where the reader was looking. The toolbar
12425
12425
  * pair, which changes everything at once, does refit.
12426
12426
  ************************************************************/
12427
- function ac_toggle_fold(gobj, event, kw, src) {
12427
+ function ac_toggle_fold$1(gobj, event, kw, src) {
12428
12428
  let priv = gobj.priv;
12429
12429
  let path = kw && kw.path || "";
12430
12430
  if (empty_string(path)) {
@@ -12463,7 +12463,7 @@ function ac_find_nodes$3(gobj, event, kw, src) {
12463
12463
  /************************************************************
12464
12464
  * EV_EXPAND_ALL
12465
12465
  ************************************************************/
12466
- function ac_expand_all$2(gobj, event, kw, src) {
12466
+ function ac_expand_all$4(gobj, event, kw, src) {
12467
12467
  let priv = gobj.priv;
12468
12468
  if (priv.collapsed.size === 0) return 0;
12469
12469
  priv.collapsed.clear();
@@ -12475,7 +12475,7 @@ function ac_expand_all$2(gobj, event, kw, src) {
12475
12475
  /************************************************************
12476
12476
  * EV_COLLAPSE_ALL — fold every card but the root.
12477
12477
  ************************************************************/
12478
- function ac_collapse_all$2(gobj, event, kw, src) {
12478
+ function ac_collapse_all$4(gobj, event, kw, src) {
12479
12479
  let priv = gobj.priv;
12480
12480
  let json_data = gobj_read_attr(gobj, "json_data");
12481
12481
  if (!json_data) return 0;
@@ -12765,7 +12765,7 @@ function create_gclass$13(gclass_name) {
12765
12765
  ],
12766
12766
  [
12767
12767
  "EV_TOGGLE_FOLD",
12768
- ac_toggle_fold,
12768
+ ac_toggle_fold$1,
12769
12769
  null
12770
12770
  ],
12771
12771
  [
@@ -12775,12 +12775,12 @@ function create_gclass$13(gclass_name) {
12775
12775
  ],
12776
12776
  [
12777
12777
  "EV_EXPAND_ALL",
12778
- ac_expand_all$2,
12778
+ ac_expand_all$4,
12779
12779
  null
12780
12780
  ],
12781
12781
  [
12782
12782
  "EV_COLLAPSE_ALL",
12783
- ac_collapse_all$2,
12783
+ ac_collapse_all$4,
12784
12784
  null
12785
12785
  ],
12786
12786
  [
@@ -13845,7 +13845,7 @@ function ac_search$2(gobj, event, kw, src) {
13845
13845
  /************************************************************
13846
13846
  * EV_EXPAND_ALL — expand every already-loaded container
13847
13847
  ************************************************************/
13848
- function ac_expand_all$1(gobj, event, kw, src) {
13848
+ function ac_expand_all$3(gobj, event, kw, src) {
13849
13849
  let priv = gobj.priv;
13850
13850
  if (priv.root === null || priv.root === void 0) return 0;
13851
13851
  let paths = [];
@@ -13863,7 +13863,7 @@ function ac_expand_all$1(gobj, event, kw, src) {
13863
13863
  /************************************************************
13864
13864
  * EV_COLLAPSE_ALL
13865
13865
  ************************************************************/
13866
- function ac_collapse_all$1(gobj, event, kw, src) {
13866
+ function ac_collapse_all$3(gobj, event, kw, src) {
13867
13867
  gobj.priv.expanded.clear();
13868
13868
  render_view$1(gobj);
13869
13869
  return 0;
@@ -14048,12 +14048,12 @@ function create_gclass$12(gclass_name) {
14048
14048
  ],
14049
14049
  [
14050
14050
  "EV_EXPAND_ALL",
14051
- ac_expand_all$1,
14051
+ ac_expand_all$3,
14052
14052
  null
14053
14053
  ],
14054
14054
  [
14055
14055
  "EV_COLLAPSE_ALL",
14056
- ac_collapse_all$1,
14056
+ ac_collapse_all$3,
14057
14057
  null
14058
14058
  ],
14059
14059
  [
@@ -19056,7 +19056,7 @@ function ac_toggle_collapse(gobj, event, kw, src) {
19056
19056
  /************************************************************
19057
19057
  * Force every node visible (overrides threshold).
19058
19058
  ************************************************************/
19059
- function ac_expand_all(gobj, event, kw, src) {
19059
+ function ac_expand_all$2(gobj, event, kw, src) {
19060
19060
  let priv = gobj.priv;
19061
19061
  if (!priv.collapse_state) priv.collapse_state = {};
19062
19062
  if (priv.node_by_id) for (let id of Object.keys(priv.node_by_id)) {
@@ -19069,7 +19069,7 @@ function ac_expand_all(gobj, event, kw, src) {
19069
19069
  /************************************************************
19070
19070
  * Collapse every node that has children.
19071
19071
  ************************************************************/
19072
- function ac_collapse_all(gobj, event, kw, src) {
19072
+ function ac_collapse_all$2(gobj, event, kw, src) {
19073
19073
  let priv = gobj.priv;
19074
19074
  if (!priv.collapse_state) priv.collapse_state = {};
19075
19075
  if (priv.node_by_id) for (let id of Object.keys(priv.node_by_id)) {
@@ -19421,12 +19421,12 @@ function create_gclass$9(gclass_name) {
19421
19421
  ],
19422
19422
  [
19423
19423
  "EV_EXPAND_ALL",
19424
- ac_expand_all,
19424
+ ac_expand_all$2,
19425
19425
  null
19426
19426
  ],
19427
19427
  [
19428
19428
  "EV_COLLAPSE_ALL",
19429
- ac_collapse_all,
19429
+ ac_collapse_all$2,
19430
19430
  null
19431
19431
  ],
19432
19432
  [
@@ -26795,6 +26795,429 @@ function node_label(desc, record) {
26795
26795
  return id;
26796
26796
  }
26797
26797
  //#endregion
26798
+ //#region src/treedb_fold_model.js
26799
+ /***********************************************************************
26800
+ * treedb_fold_model.js
26801
+ *
26802
+ * A treedb read as a TREE, and which part of it is on screen.
26803
+ *
26804
+ * A treedb is a graph: a record hangs from every parent one of its
26805
+ * fkeys names, and a parent reaches its children through a hook.
26806
+ * Drawn whole it is a pile -- a nave with a hundred and forty
26807
+ * devices is a row of a hundred and forty cards -- so the graph
26808
+ * opens FOLDED, the way a JSON viewer opens: the roots, a level or
26809
+ * two under them, and a count on every cut saying what went with it.
26810
+ *
26811
+ * This module is the arithmetic of that, and nothing else: no G6,
26812
+ * no DOM, no gobj. The graph engine (c_g6_nodes_tree.js) asks it
26813
+ * what is visible and draws exactly that.
26814
+ *
26815
+ * Vocabulary
26816
+ *
26817
+ * node key "topic^id" -- one record
26818
+ * group the children of ONE node through ONE hook,
26819
+ * keyed "topic^id|hook"; the roots of a topic are
26820
+ * a group of their own, keyed "|topic"
26821
+ * shown how many of a group's children are on screen:
26822
+ * absent means folded, a number means the first
26823
+ * N, in record order, a page at a time
26824
+ *
26825
+ * Two rules that are not obvious:
26826
+ *
26827
+ * - A node is visible when SOME visible parent shows it, or it is
26828
+ * a shown root. A record with two parents (a device hangs from
26829
+ * its place AND its controller) is drawn ONCE, under whichever
26830
+ * opened first, and folding one of the two leaves it where the
26831
+ * other one shows it. The union, never the intersection.
26832
+ *
26833
+ * - A parent that the engine does not draw an edge to is not a
26834
+ * parent here either: the `extended` tier (hooks and no fkeys)
26835
+ * gets no edges in draw_link(), so its children are roots. The
26836
+ * tree on screen and the tree in this model have to agree, or a
26837
+ * pill counts children that no line reaches.
26838
+ *
26839
+ * Copyright (c) 2026, ArtGins.
26840
+ * All Rights Reserved.
26841
+ ***********************************************************************/
26842
+ var PAGE_SIZE_DEFAULT = 24;
26843
+ /************************************************************
26844
+ * Keys
26845
+ ************************************************************/
26846
+ function fold_node_key(topic_name, id) {
26847
+ return `${topic_name}^${id}`;
26848
+ }
26849
+ function fold_group_key(node_key, hook_name) {
26850
+ return `${node_key}|${hook_name}`;
26851
+ }
26852
+ function fold_root_group_key(topic_name) {
26853
+ return `|${topic_name}`;
26854
+ }
26855
+ function fold_split_group_key(group_key) {
26856
+ let i = group_key.lastIndexOf("|");
26857
+ if (i < 0) return null;
26858
+ return {
26859
+ node_key: group_key.slice(0, i),
26860
+ hook: group_key.slice(i + 1)
26861
+ };
26862
+ }
26863
+ /************************************************************
26864
+ * Every parent a record names, decoded, in column order.
26865
+ *
26866
+ * Only the parents that will be DRAWN: a topic missing from
26867
+ * `descs` (a system topic such as `__assets__`, never fetched)
26868
+ * and an `extended` parent (no edge is drawn to one, see the
26869
+ * header) are left out, so a child of nothing else is a root.
26870
+ ************************************************************/
26871
+ function parents_of(descs, desc, record) {
26872
+ let out = [];
26873
+ let cols = desc.cols || [];
26874
+ for (let i = 0; i < cols.length; i++) {
26875
+ let col = cols[i];
26876
+ if (!col.fkey) continue;
26877
+ let fkeys = record[col.id];
26878
+ if (!fkeys) continue;
26879
+ let list = is_array(fkeys) ? fkeys : [fkeys];
26880
+ for (let j = 0; j < list.length; j++) {
26881
+ let ref = treedb_decoder_fkey(col, list[j]);
26882
+ if (!ref || !ref.topic_name || !ref.hook_name) continue;
26883
+ let parent_desc = descs[ref.topic_name];
26884
+ if (!parent_desc) continue;
26885
+ if (parent_desc.node_treedb_type === "extended") continue;
26886
+ out.push({
26887
+ node_key: fold_node_key(ref.topic_name, String(ref.id)),
26888
+ hook: ref.hook_name,
26889
+ fkey_name: col.id
26890
+ });
26891
+ }
26892
+ }
26893
+ return out;
26894
+ }
26895
+ /************************************************************
26896
+ * Build the model from the topic descriptions and their records.
26897
+ *
26898
+ * descs {topic_name: desc}, system topics included (skipped)
26899
+ * records {topic_name: [record, ...]}, in the order the
26900
+ * backend returned them -- that order is the order of
26901
+ * the children on screen, and of the pages
26902
+ *
26903
+ * Returns
26904
+ * {
26905
+ * nodes: Map<node_key, {key, topic_name, id, record,
26906
+ * parents: [{node_key, hook, fkey_name}],
26907
+ * hooks: {hook: [child_key, ...]}}>
26908
+ * groups: Map<group_key, [child_key, ...]> (roots included)
26909
+ * topics: [topic_name, ...] topics with records, in desc order
26910
+ * }
26911
+ ************************************************************/
26912
+ function fold_build_model(descs, records) {
26913
+ let nodes = /* @__PURE__ */ new Map();
26914
+ let groups = /* @__PURE__ */ new Map();
26915
+ let topics = [];
26916
+ descs = is_object(descs) ? descs : {};
26917
+ records = is_object(records) ? records : {};
26918
+ for (let topic_name of Object.keys(descs)) {
26919
+ if (topic_name.substring(0, 2) === "__") continue;
26920
+ descs[topic_name];
26921
+ let list = records[topic_name];
26922
+ if (!is_array(list)) continue;
26923
+ topics.push(topic_name);
26924
+ for (let i = 0; i < list.length; i++) {
26925
+ let record = list[i];
26926
+ if (!is_object(record) || record.id === void 0 || record.id === null) continue;
26927
+ let key = fold_node_key(topic_name, String(record.id));
26928
+ nodes.set(key, {
26929
+ key,
26930
+ topic_name,
26931
+ id: String(record.id),
26932
+ record,
26933
+ parents: [],
26934
+ hooks: {}
26935
+ });
26936
+ }
26937
+ }
26938
+ for (let node of nodes.values()) {
26939
+ let desc = descs[node.topic_name];
26940
+ let parents = parents_of(descs, desc, node.record);
26941
+ for (let p of parents) {
26942
+ let parent = nodes.get(p.node_key);
26943
+ if (!parent) continue;
26944
+ node.parents.push(p);
26945
+ if (!parent.hooks[p.hook]) parent.hooks[p.hook] = [];
26946
+ parent.hooks[p.hook].push(node.key);
26947
+ }
26948
+ }
26949
+ for (let node of nodes.values()) for (let hook of Object.keys(node.hooks)) groups.set(fold_group_key(node.key, hook), node.hooks[hook]);
26950
+ let roots = {};
26951
+ for (let topic_name of topics) roots[topic_name] = [];
26952
+ for (let node of nodes.values()) if (node.parents.length === 0) roots[node.topic_name].push(node.key);
26953
+ let reached = reach_from(nodes, [].concat(...Object.values(roots)));
26954
+ for (let node of nodes.values()) if (!reached.has(node.key)) {
26955
+ roots[node.topic_name].push(node.key);
26956
+ for (let k of reach_from(nodes, [node.key])) reached.add(k);
26957
+ }
26958
+ for (let topic_name of topics) if (roots[topic_name].length > 0) groups.set(fold_root_group_key(topic_name), roots[topic_name]);
26959
+ return {
26960
+ nodes,
26961
+ groups,
26962
+ topics
26963
+ };
26964
+ }
26965
+ /************************************************************
26966
+ * Everything reachable downwards from `start`, cycles included.
26967
+ ************************************************************/
26968
+ function reach_from(nodes, start) {
26969
+ let seen = /* @__PURE__ */ new Set();
26970
+ let queue = start.slice();
26971
+ while (queue.length) {
26972
+ let key = queue.shift();
26973
+ if (seen.has(key)) continue;
26974
+ seen.add(key);
26975
+ let node = nodes.get(key);
26976
+ if (!node) continue;
26977
+ for (let hook of Object.keys(node.hooks)) for (let child of node.hooks[hook]) if (!seen.has(child)) queue.push(child);
26978
+ }
26979
+ return seen;
26980
+ }
26981
+ /************************************************************
26982
+ * Fold state: which groups are open, and how far.
26983
+ ************************************************************/
26984
+ function fold_new_state(page_size) {
26985
+ return {
26986
+ page_size: page_size > 0 ? page_size : PAGE_SIZE_DEFAULT,
26987
+ shown: /* @__PURE__ */ new Map()
26988
+ };
26989
+ }
26990
+ function fold_group_total(model, group_key) {
26991
+ let list = model.groups.get(group_key);
26992
+ return list ? list.length : 0;
26993
+ }
26994
+ function fold_group_shown(model, state, group_key) {
26995
+ let total = fold_group_total(model, group_key);
26996
+ let n = state.shown.get(group_key) || 0;
26997
+ return Math.min(n, total);
26998
+ }
26999
+ function fold_is_open(state, group_key) {
27000
+ return (state.shown.get(group_key) || 0) > 0;
27001
+ }
27002
+ function page_ceiling(state, n) {
27003
+ let p = state.page_size;
27004
+ return Math.ceil(n / p) * p;
27005
+ }
27006
+ /************************************************************
27007
+ * Open a group on its first page, or close it.
27008
+ * Returns true when it ends OPEN.
27009
+ ************************************************************/
27010
+ function fold_toggle(model, state, group_key) {
27011
+ if (fold_is_open(state, group_key)) {
27012
+ state.shown.delete(group_key);
27013
+ return false;
27014
+ }
27015
+ if (fold_group_total(model, group_key) === 0) return false;
27016
+ state.shown.set(group_key, state.page_size);
27017
+ return true;
27018
+ }
27019
+ /************************************************************
27020
+ * One more page of a group. Returns how many are shown now.
27021
+ ************************************************************/
27022
+ function fold_show_more(model, state, group_key) {
27023
+ let total = fold_group_total(model, group_key);
27024
+ let n = fold_group_shown(model, state, group_key) + state.page_size;
27025
+ n = Math.min(n, total);
27026
+ if (n > 0) state.shown.set(group_key, n);
27027
+ return n;
27028
+ }
27029
+ /************************************************************
27030
+ * Every group open on ALL its children. This is the reader
27031
+ * asking for the whole thing; the pages are for the other way in.
27032
+ ************************************************************/
27033
+ function fold_expand_all(model, state) {
27034
+ state.shown.clear();
27035
+ for (let [group_key, list] of model.groups) if (list.length > 0) state.shown.set(group_key, list.length);
27036
+ }
27037
+ /************************************************************
27038
+ * The roots and nothing else -- the first page of each topic's
27039
+ * roots, the way the graph looks before anything is opened.
27040
+ ************************************************************/
27041
+ function fold_collapse_all(model, state) {
27042
+ state.shown.clear();
27043
+ for (let topic_name of model.topics) {
27044
+ let key = fold_root_group_key(topic_name);
27045
+ if (fold_group_total(model, key) > 0) state.shown.set(key, state.page_size);
27046
+ }
27047
+ }
27048
+ /************************************************************
27049
+ * Open the tree down to `depth` levels: the roots are level 1,
27050
+ * their children level 2. A node reached by two paths takes the
27051
+ * shallowest. Each group opens on its first page. `depth` of 1
27052
+ * is the roots alone; anything below that is treated as 1.
27053
+ ************************************************************/
27054
+ function fold_expand_to_depth(model, state, depth) {
27055
+ fold_collapse_all(model, state);
27056
+ if (!(depth > 1)) return;
27057
+ let level = /* @__PURE__ */ new Map();
27058
+ let queue = [];
27059
+ for (let topic_name of model.topics) {
27060
+ let roots = model.groups.get(fold_root_group_key(topic_name)) || [];
27061
+ for (let key of roots) if (!level.has(key)) {
27062
+ level.set(key, 1);
27063
+ queue.push(key);
27064
+ }
27065
+ }
27066
+ while (queue.length) {
27067
+ let key = queue.shift();
27068
+ let lv = level.get(key);
27069
+ if (lv >= depth) continue;
27070
+ let node = model.nodes.get(key);
27071
+ if (!node) continue;
27072
+ for (let hook of Object.keys(node.hooks)) {
27073
+ let children = node.hooks[hook];
27074
+ if (children.length === 0) continue;
27075
+ state.shown.set(fold_group_key(key, hook), state.page_size);
27076
+ for (let child of children) if (!level.has(child)) {
27077
+ level.set(child, lv + 1);
27078
+ queue.push(child);
27079
+ }
27080
+ }
27081
+ }
27082
+ }
27083
+ /************************************************************
27084
+ * The set of node keys on screen, from the roots down through
27085
+ * every open group -- and only through open groups whose parent
27086
+ * is itself on screen: a group remembered open under a folded
27087
+ * ancestor stays remembered, not shown.
27088
+ ************************************************************/
27089
+ function fold_visible_set(model, state) {
27090
+ let visible = /* @__PURE__ */ new Set();
27091
+ let queue = [];
27092
+ let push_group = (group_key, children) => {
27093
+ let n = fold_group_shown(model, state, group_key);
27094
+ for (let i = 0; i < n; i++) {
27095
+ let key = children[i];
27096
+ if (!visible.has(key)) {
27097
+ visible.add(key);
27098
+ queue.push(key);
27099
+ }
27100
+ }
27101
+ };
27102
+ for (let topic_name of model.topics) {
27103
+ let key = fold_root_group_key(topic_name);
27104
+ let roots = model.groups.get(key);
27105
+ if (roots) push_group(key, roots);
27106
+ }
27107
+ while (queue.length) {
27108
+ let key = queue.shift();
27109
+ let node = model.nodes.get(key);
27110
+ if (!node) continue;
27111
+ for (let hook of Object.keys(node.hooks)) push_group(fold_group_key(key, hook), node.hooks[hook]);
27112
+ }
27113
+ return visible;
27114
+ }
27115
+ /************************************************************
27116
+ * The open groups that still have children off screen, with the
27117
+ * parent that owns each -- what the "+N" chips are drawn for.
27118
+ * Only groups whose parent is visible (or root groups).
27119
+ *
27120
+ * Returns [{group_key, node_key, hook, shown, total, rest}]
27121
+ ************************************************************/
27122
+ function fold_pending_groups(model, state, visible) {
27123
+ let out = [];
27124
+ for (let [group_key, list] of model.groups) {
27125
+ let shown = fold_group_shown(model, state, group_key);
27126
+ if (shown === 0 || shown >= list.length) continue;
27127
+ let g = fold_split_group_key(group_key);
27128
+ if (g.node_key && !visible.has(g.node_key)) continue;
27129
+ out.push({
27130
+ group_key,
27131
+ node_key: g.node_key,
27132
+ hook: g.hook,
27133
+ shown,
27134
+ total: list.length,
27135
+ rest: list.length - shown
27136
+ });
27137
+ }
27138
+ return out;
27139
+ }
27140
+ /************************************************************
27141
+ * The pills of one card: one per hook that has children.
27142
+ *
27143
+ * Returns [{hook, group_key, total, shown, open, child_topic}]
27144
+ * `child_topic` is the topic of the first child, which is what
27145
+ * the pill is coloured with (a hook can name several topics in
27146
+ * the schema, but the children it has are what it links).
27147
+ ************************************************************/
27148
+ function fold_pills_of(model, state, node_key) {
27149
+ let node = model.nodes.get(node_key);
27150
+ if (!node) return [];
27151
+ let out = [];
27152
+ for (let hook of Object.keys(node.hooks)) {
27153
+ let children = node.hooks[hook];
27154
+ if (children.length === 0) continue;
27155
+ let group_key = fold_group_key(node_key, hook);
27156
+ let first = model.nodes.get(children[0]);
27157
+ out.push({
27158
+ hook,
27159
+ group_key,
27160
+ total: children.length,
27161
+ shown: fold_group_shown(model, state, group_key),
27162
+ open: fold_is_open(state, group_key),
27163
+ child_topic: first ? first.topic_name : ""
27164
+ });
27165
+ }
27166
+ return out;
27167
+ }
27168
+ /************************************************************
27169
+ * Make ONE node visible, opening the groups on a path from a
27170
+ * root to it, and as many pages of each as it takes for the
27171
+ * node to be inside the shown prefix. Prefers the shortest path
27172
+ * (breadth-first upwards). Returns false if no root reaches it,
27173
+ * which cannot happen with a model built here (see roots) but
27174
+ * is the honest answer for a key the model does not know.
27175
+ ************************************************************/
27176
+ function fold_reveal(model, state, node_key) {
27177
+ if (!model.nodes.get(node_key)) return false;
27178
+ let root_group_of = (key) => {
27179
+ let rg = fold_root_group_key(model.nodes.get(key).topic_name);
27180
+ return (model.groups.get(rg) || []).indexOf(key) >= 0 ? rg : null;
27181
+ };
27182
+ let via = /* @__PURE__ */ new Map();
27183
+ let queue = [node_key];
27184
+ via.set(node_key, null);
27185
+ let root_key = null;
27186
+ while (queue.length) {
27187
+ let key = queue.shift();
27188
+ if (root_group_of(key)) {
27189
+ root_key = key;
27190
+ break;
27191
+ }
27192
+ let n = model.nodes.get(key);
27193
+ for (let p of n.parents) {
27194
+ if (!model.nodes.has(p.node_key) || via.has(p.node_key)) continue;
27195
+ via.set(p.node_key, {
27196
+ child_key: key,
27197
+ hook: p.hook
27198
+ });
27199
+ queue.push(p.node_key);
27200
+ }
27201
+ }
27202
+ if (root_key === null) return false;
27203
+ let open_up_to = (group_key, child_key) => {
27204
+ let list = model.groups.get(group_key) || [];
27205
+ let idx = list.indexOf(child_key);
27206
+ if (idx < 0) return;
27207
+ let need = page_ceiling(state, idx + 1);
27208
+ if (need > (state.shown.get(group_key) || 0)) state.shown.set(group_key, Math.min(need, list.length));
27209
+ };
27210
+ open_up_to(root_group_of(root_key), root_key);
27211
+ let key = root_key;
27212
+ while (true) {
27213
+ let step = via.get(key);
27214
+ if (!step) break;
27215
+ open_up_to(fold_group_key(key, step.hook), step.child_key);
27216
+ key = step.child_key;
27217
+ }
27218
+ return true;
27219
+ }
27220
+ //#endregion
26798
27221
  //#region node_modules/@babel/runtime/helpers/esm/typeof.js
26799
27222
  function _typeof(o) {
26800
27223
  "@babel/helpers - typeof";
@@ -53539,6 +53962,23 @@ function ensure_brush_select_owned() {
53539
53962
  * record: null, // Data of node
53540
53963
  * }
53541
53964
  *
53965
+ * The graph opens FOLDED, like a JSON viewer.
53966
+ *
53967
+ * Every record of every topic is fetched, but only the ones on
53968
+ * screen become G6 nodes: the roots, `expand_depth` levels under
53969
+ * them, one page (`fold_page_size`) of children per hook. Each
53970
+ * card carries a pill per hook that has children -- `▸ devices
53971
+ * 142` -- which opens or folds that hook; an open hook with more
53972
+ * children than its page ends in a `+N` chip that opens the next
53973
+ * page. The arithmetic lives in treedb_fold_model.js; this file
53974
+ * reconciles the G6 data with what that model says is visible
53975
+ * (see reconcile_fold) and lays it out again.
53976
+ *
53977
+ * `dagre` reads LEFT TO RIGHT: the children of a node are a
53978
+ * column beside it, the ports move to the sides with it, and
53979
+ * the edges are horizontal beziers. `antv-dagre` keeps the
53980
+ * top-down reading.
53981
+ *
53542
53982
  * Copyright (c) 2020-2021 Niyamaka.
53543
53983
  * Copyright (c) 2025-2026, ArtGins.
53544
53984
  * All Rights Reserved.
@@ -53598,8 +54038,18 @@ var SELECT_RING = "rgba(59,130,246,0.95)";
53598
54038
  ***************************************************************/
53599
54039
  var _layouts = {
53600
54040
  "manual": { type: "manual" },
53601
- "dagre": { type: "dagre" },
53602
- "antv-dagre": { type: "antv-dagre" },
54041
+ "dagre": {
54042
+ type: "dagre",
54043
+ rankdir: "LR",
54044
+ nodesep: 18,
54045
+ ranksep: 110
54046
+ },
54047
+ "antv-dagre": {
54048
+ type: "antv-dagre",
54049
+ rankdir: "TB",
54050
+ nodesep: 30,
54051
+ ranksep: 80
54052
+ },
53603
54053
  "d3-force": {
53604
54054
  type: "d3-force",
53605
54055
  link: {
@@ -53647,6 +54097,8 @@ var attrs_table$4 = [
53647
54097
  SDATA(data_type_t.DTP_LIST, "layout_names", sdata_flag_t.SDF_RD, JSON.stringify(Object.keys(_layouts)), "Available layout names (read-only, for parent to query)"),
53648
54098
  SDATA(data_type_t.DTP_STRING, "hook_port_position", 0, "bottom", "Hook port position"),
53649
54099
  SDATA(data_type_t.DTP_STRING, "fkey_port_position", 0, "top", "Fkey port position"),
54100
+ 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"),
54101
+ 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"),
53650
54102
  SDATA(data_type_t.DTP_BOOLEAN, "confirm_delete_node", 0, true, "Ask confirmation before deleting a node"),
53651
54103
  SDATA(data_type_t.DTP_BOOLEAN, "confirm_unlink_edge", 0, true, "Ask confirmation before unlinking an edge"),
53652
54104
  SDATA(data_type_t.DTP_STRING, "wide", 0, "40px", "Height of header"),
@@ -53718,7 +54170,14 @@ var PRIVATE_DATA$4 = {
53718
54170
  _pending_find: null,
53719
54171
  _layout_asked: "",
53720
54172
  _nodes_total: 0,
53721
- _nodes_placed: 0
54173
+ _nodes_placed: 0,
54174
+ expand_depth: 2,
54175
+ fold_page_size: 24,
54176
+ _fold_model: null,
54177
+ _fold_state: null,
54178
+ _fold_listener: null,
54179
+ _pill_sig: null,
54180
+ _fold_busy: false
53722
54181
  };
53723
54182
  var __gclass__$4 = null;
53724
54183
  /******************************
@@ -53738,6 +54197,7 @@ function mt_create$4(gobj) {
53738
54197
  build_ui$4(gobj);
53739
54198
  register_layouts(gobj);
53740
54199
  build_graph$1(gobj);
54200
+ install_fold_listener(gobj);
53741
54201
  if (typeof ResizeObserver !== "undefined" && priv.$container) {
53742
54202
  let ro = new ResizeObserver(() => {
53743
54203
  if (priv._resize_raf) cancelAnimationFrame(priv._resize_raf);
@@ -53777,6 +54237,7 @@ function mt_destroy$4(gobj) {
53777
54237
  i18next.off("languageChanged", priv._on_language_changed);
53778
54238
  priv._on_language_changed = null;
53779
54239
  }
54240
+ uninstall_fold_listener(gobj);
53780
54241
  if (priv._on_pointerdown_focus) {
53781
54242
  priv.$container.removeEventListener("pointerdown", priv._on_pointerdown_focus, true);
53782
54243
  priv._on_pointerdown_focus = null;
@@ -53964,6 +54425,7 @@ function configure_events(gobj) {
53964
54425
  });
53965
54426
  priv._on_language_changed = () => {
53966
54427
  update_toolbar(gobj);
54428
+ repaint_more_chips(gobj);
53967
54429
  };
53968
54430
  i18next.on("languageChanged", priv._on_language_changed);
53969
54431
  priv._on_pointerdown_focus = (ev) => {
@@ -54461,15 +54923,79 @@ function build_ports(gobj, desc) {
54461
54923
  top_ports.push(port);
54462
54924
  }
54463
54925
  }
54464
- for (let i = 0; i < top_ports.length; i++) top_ports[i].placement = [.5, 0];
54465
- for (let i = 0; i < bottom_ports.length; i++) {
54466
- let point = getPointPosition(bottom_ports.length, i);
54467
- if (top_ports.length === 0) bottom_ports[i].placement = [0, .5];
54468
- else bottom_ports[i].placement = [point, 1];
54469
- }
54926
+ place_ports(gobj, top_ports, bottom_ports);
54470
54927
  return [...top_ports, ...bottom_ports];
54471
54928
  }
54472
54929
  /************************************************************
54930
+ * Which way the graph reads. `dagre` is the left-to-right
54931
+ * layout; everything else keeps the top-down reading, `manual`
54932
+ * included, because the saved arrangements were made with the
54933
+ * ports on the top and bottom edges.
54934
+ ************************************************************/
54935
+ function layout_direction(gobj) {
54936
+ return gobj.priv.layout === "dagre" ? "LR" : "TB";
54937
+ }
54938
+ function edge_type_for(gobj) {
54939
+ return layout_direction(gobj) === "LR" ? "cubic-horizontal" : "cubic";
54940
+ }
54941
+ /************************************************************
54942
+ * Where the ports sit, by direction: fkeys (the way in from a
54943
+ * parent) all on one point of the leading edge, hooks (the way
54944
+ * out to the children) spread along the trailing edge. Left and
54945
+ * right when the graph reads left to right; top and bottom
54946
+ * otherwise.
54947
+ ************************************************************/
54948
+ function place_ports(gobj, fkey_ports, hook_ports) {
54949
+ let lr = layout_direction(gobj) === "LR";
54950
+ for (let i = 0; i < fkey_ports.length; i++) fkey_ports[i].placement = lr ? [0, .5] : [.5, 0];
54951
+ for (let i = 0; i < hook_ports.length; i++) {
54952
+ let point = getPointPosition(hook_ports.length, i);
54953
+ if (fkey_ports.length === 0 && !lr) hook_ports[i].placement = [0, .5];
54954
+ else if (lr) hook_ports[i].placement = [1, point];
54955
+ else hook_ports[i].placement = [point, 1];
54956
+ }
54957
+ }
54958
+ /************************************************************
54959
+ * The layout changed direction: move every port to the edge
54960
+ * the new reading needs, and give every edge the curve that
54961
+ * goes with it. Called before the layout runs, so the lines
54962
+ * are drawn once, in the right place.
54963
+ ************************************************************/
54964
+ function apply_layout_direction(gobj) {
54965
+ let graph = gobj.priv.graph;
54966
+ if (!graph) return;
54967
+ let node_updates = [];
54968
+ for (let nd of graph.getNodeData() || []) {
54969
+ if (!nd || !nd.data || !nd.data.desc || !nd.style || !is_array(nd.style.ports)) continue;
54970
+ let desc = nd.data.desc;
54971
+ let fkey_ports = [];
54972
+ let hook_ports = [];
54973
+ for (let port of nd.style.ports) {
54974
+ let col = get_col_by_id(desc, port.key);
54975
+ let copy = Object.assign({}, port);
54976
+ if (col && col.fkey) fkey_ports.push(copy);
54977
+ else hook_ports.push(copy);
54978
+ }
54979
+ place_ports(gobj, fkey_ports, hook_ports);
54980
+ node_updates.push({
54981
+ id: nd.id,
54982
+ style: { ports: [...fkey_ports, ...hook_ports] }
54983
+ });
54984
+ }
54985
+ let type = edge_type_for(gobj);
54986
+ let edge_updates = [];
54987
+ for (let ed of graph.getEdgeData() || []) if (ed && ed.type !== type) edge_updates.push({
54988
+ id: ed.id,
54989
+ type
54990
+ });
54991
+ try {
54992
+ if (node_updates.length) graph.updateNodeData(node_updates);
54993
+ if (edge_updates.length) graph.updateEdgeData(edge_updates);
54994
+ } catch (e) {
54995
+ log_error(`${gobj_short_name(gobj)}: cannot turn the ports: ${e}`);
54996
+ }
54997
+ }
54998
+ /************************************************************
54473
54999
  * Create a topic node in the G6 graph
54474
55000
  ************************************************************/
54475
55001
  function create_topic_node(gobj, desc, record) {
@@ -54478,8 +55004,6 @@ function create_topic_node(gobj, desc, record) {
54478
55004
  let node_name = build_node_name(gobj, desc.topic_name, record.id);
54479
55005
  let xy = get_default_ne_xy(gobj);
54480
55006
  let geometry = get_node_graph_props(gobj, desc.topic_name, record.id, record);
54481
- priv._nodes_total++;
54482
- if (geometry_has_position(geometry)) priv._nodes_placed++;
54483
55007
  let x = kw_get_int(gobj, geometry, "x", xy, kw_flag_t.KW_CREATE);
54484
55008
  let y = kw_get_int(gobj, geometry, "y", xy, kw_flag_t.KW_CREATE);
54485
55009
  let ports = build_ports(gobj, desc);
@@ -54492,24 +55016,26 @@ function create_topic_node(gobj, desc, record) {
54492
55016
  stroke: getStrokeColor(desc.color),
54493
55017
  lineWidth: 1
54494
55018
  };
55019
+ let pills_html = pills_html_of_key(gobj, fold_node_key(desc.topic_name, String(record.id)));
55020
+ let size = card_size_for(node_treedb_type, !!pills_html);
54495
55021
  if (node_treedb_type === "child") {
54496
55022
  node_graph_type = "html";
54497
- style.size = [116, 40];
54498
- style.dx = -58;
54499
- style.dy = -20;
55023
+ style.size = size;
55024
+ style.dx = -size[0] / 2;
55025
+ style.dy = -size[1] / 2;
54500
55026
  style.innerHTML = build_chip_innerHTML(desc.color, priv.theme, record.icon, node_label(desc, record), record.id);
54501
55027
  } else if (node_treedb_type === "extended") {
54502
55028
  node_graph_type = "html";
54503
- style.size = [144, 66];
54504
- style.dx = -72;
54505
- style.dy = -33;
54506
- style.innerHTML = build_node_innerHTML(desc.color, priv.theme, record.icon, node_label(desc, record), desc.topic_name, true, record.id);
55029
+ style.size = size;
55030
+ style.dx = -size[0] / 2;
55031
+ style.dy = -size[1] / 2;
55032
+ style.innerHTML = build_node_innerHTML(desc.color, priv.theme, record.icon, node_label(desc, record), desc.topic_name, true, record.id, false, false, false, pills_html);
54507
55033
  } else {
54508
55034
  node_graph_type = "html";
54509
- style.size = [172, 96];
54510
- style.dx = -86;
54511
- style.dy = -48;
54512
- style.innerHTML = build_node_innerHTML(desc.color, priv.theme, record.icon, node_label(desc, record), desc.topic_name, false, record.id);
55035
+ style.size = size;
55036
+ style.dx = -size[0] / 2;
55037
+ style.dy = -size[1] / 2;
55038
+ style.innerHTML = build_node_innerHTML(desc.color, priv.theme, record.icon, node_label(desc, record), desc.topic_name, false, record.id, false, false, false, pills_html);
54513
55039
  }
54514
55040
  let topic_props = priv._graph_properties[desc.topic_name];
54515
55041
  let topic_defaults = is_object(topic_props) && is_object(topic_props.defaults) ? topic_props.defaults : null;
@@ -54566,8 +55092,7 @@ function create_topic_node(gobj, desc, record) {
54566
55092
  * Update topic node data
54567
55093
  ************************************************************/
54568
55094
  function update_topic_node(gobj, desc, node_name, record) {
54569
- let priv = gobj.priv;
54570
- let graph = priv.graph;
55095
+ let graph = gobj.priv.graph;
54571
55096
  try {
54572
55097
  let nodedata = graph.getNodeData(node_name);
54573
55098
  if (!nodedata) {
@@ -54575,18 +55100,7 @@ function update_topic_node(gobj, desc, node_name, record) {
54575
55100
  return;
54576
55101
  }
54577
55102
  nodedata.data.record = record;
54578
- if (desc.node_treedb_type === "child") graph.updateNodeData([{
54579
- id: node_name,
54580
- style: { innerHTML: build_chip_innerHTML(desc.color, priv.theme, record.icon, node_label(desc, record), record.id) }
54581
- }]);
54582
- else if (desc.node_treedb_type === "extended") graph.updateNodeData([{
54583
- id: node_name,
54584
- style: { innerHTML: build_node_innerHTML(desc.color, priv.theme, record.icon, node_label(desc, record), desc.topic_name, true, record.id) }
54585
- }]);
54586
- else if (desc.node_treedb_type === "hierarchical") graph.updateNodeData([{
54587
- id: node_name,
54588
- style: { innerHTML: build_node_innerHTML(desc.color, priv.theme, record.icon, node_label(desc, record), desc.topic_name, false, record.id) }
54589
- }]);
55103
+ repaint_cards(gobj, /* @__PURE__ */ new Set([node_name]));
54590
55104
  } catch (e) {
54591
55105
  log_error(e.message);
54592
55106
  }
@@ -54625,19 +55139,6 @@ function remove_local_node(gobj, topic_name, node) {
54625
55139
  }
54626
55140
  }
54627
55141
  /************************************************************
54628
- * Create all links from records, initial load from parent
54629
- ************************************************************/
54630
- function create_links(gobj) {
54631
- let priv = gobj.priv;
54632
- for (const [topic_name, records] of Object.entries(priv.records)) {
54633
- let desc = priv.descs[topic_name];
54634
- for (let i = 0; i < records.length; i++) {
54635
- let record = records[i];
54636
- draw_links(gobj, desc, record, true);
54637
- }
54638
- }
54639
- }
54640
- /************************************************************
54641
55142
  * Collect all fkey references from a record as a Set.
54642
55143
  *
54643
55144
  * Each entry is a string: "col_id\tcol_idx\tfkey_value"
@@ -54717,6 +55218,17 @@ function draw_link(gobj, child_topic, child_id, source_col, fkey, verbose) {
54717
55218
  return;
54718
55219
  }
54719
55220
  let child_node = build_node_name(gobj, child_topic, child_id);
55221
+ let child_cell = null;
55222
+ try {
55223
+ child_cell = graph.getNodeData(child_node);
55224
+ } catch (e) {
55225
+ child_cell = null;
55226
+ }
55227
+ if (!child_cell) {
55228
+ if (verbose) log_error(`${gobj_short_name(gobj)}: child node NOT FOUND: ${child_node}`);
55229
+ return;
55230
+ }
55231
+ if (get_edge_by_link(gobj, parent_topic, parent_id, hook_name, child_topic, child_id, fkey_name)) return;
54720
55232
  let dark = priv.theme === "dark";
54721
55233
  let is_tree = parent_topic === child_topic;
54722
55234
  let saved_lineWidth = is_tree ? 2 : 1.6;
@@ -54755,7 +55267,7 @@ function draw_link(gobj, child_topic, child_id, source_col, fkey, verbose) {
54755
55267
  }
54756
55268
  let edge = {
54757
55269
  id: build_edge_id(gobj),
54758
- type: "cubic",
55270
+ type: edge_type_for(gobj),
54759
55271
  source: parent_node,
54760
55272
  target: child_node,
54761
55273
  style: {
@@ -54886,7 +55398,10 @@ function save_geometry(gobj) {
54886
55398
  let priv = gobj.priv;
54887
55399
  let graph = priv.graph;
54888
55400
  const nodes = graph.getData().nodes;
54889
- for (let i = 0; i < nodes.length; i++) update_geometry(gobj, nodes[i].id);
55401
+ for (let i = 0; i < nodes.length; i++) {
55402
+ if (!nodes[i].data || !nodes[i].data.desc) continue;
55403
+ update_geometry(gobj, nodes[i].id);
55404
+ }
54890
55405
  const edges = graph.getData().edges;
54891
55406
  for (let i = 0; i < edges.length; i++) update_edge_geometry(gobj, edges[i].id);
54892
55407
  let origin = gobj_read_str_attr(gobj_yuno(), "node_uuid");
@@ -55143,38 +55658,72 @@ async function graph_fit_selection(gobj, ids) {
55143
55658
  ************************************************************/
55144
55659
  function graph_focus_topic(gobj, topic) {
55145
55660
  let priv = gobj.priv;
55146
- let graph = priv.graph;
55147
- if (!graph || !priv.graph_rendered) {
55661
+ if (!priv.graph || !priv.graph_rendered || !priv._fold_model) {
55148
55662
  priv._pending_focus_topic = topic;
55149
55663
  return;
55150
55664
  }
55151
- let data = {};
55152
- try {
55153
- data = graph.getData() || {};
55154
- } catch (e) {
55155
- log_error(`${gobj_short_name(gobj)}: graph.getData() failed: ${e}`);
55156
- return;
55665
+ let model = priv._fold_model;
55666
+ let state = priv._fold_state;
55667
+ let keys = [];
55668
+ if (!empty_string(topic)) {
55669
+ for (let node of model.nodes.values()) if (node.topic_name === topic) keys.push(node.key);
55670
+ if (keys.length === 0) log_warning(`${gobj_short_name(gobj)}: focus topic '${topic}' has no nodes`);
55671
+ let visible = fold_visible_set(model, state);
55672
+ let revealed = 0;
55673
+ for (let key of keys) {
55674
+ if (visible.has(key)) continue;
55675
+ if (revealed >= state.page_size) break;
55676
+ fold_reveal(model, state, key);
55677
+ revealed++;
55678
+ }
55157
55679
  }
55158
- let nodes = is_array(data.nodes) ? data.nodes : [];
55680
+ priv._focus_topic = topic || null;
55681
+ reconcile_fold(gobj, {}).then(() => {
55682
+ apply_focus_ids(gobj, keys, "focus topic");
55683
+ });
55684
+ }
55685
+ /************************************************************
55686
+ * Paint the highlight on these records (by model key) and put
55687
+ * the first of them in the middle. Shared by the find and the
55688
+ * topic focus, after the reconcile that brought them on screen.
55689
+ * Keys that are still off screen are skipped: a match beyond the
55690
+ * page a reveal opened is counted, not painted.
55691
+ ************************************************************/
55692
+ function apply_focus_ids(gobj, keys, what) {
55693
+ let priv = gobj.priv;
55694
+ let graph = priv.graph;
55695
+ if (!graph) return;
55159
55696
  let states = {};
55160
55697
  for (let id of priv._focus_ids || []) states[id] = [];
55161
55698
  let ids = [];
55162
- if (!empty_string(topic)) {
55163
- for (let n of nodes) if (n && n.data && n.data.topic_name === topic) ids.push(n.id);
55164
- if (ids.length === 0) log_warning(`${gobj_short_name(gobj)}: focus topic '${topic}' has no nodes`);
55165
- for (let id of ids) states[id] = ["active"];
55699
+ for (let key of keys) {
55700
+ let id = node_id_of_key(gobj, key);
55701
+ let nd = null;
55702
+ try {
55703
+ nd = graph.getNodeData(id);
55704
+ } catch (e) {
55705
+ nd = null;
55706
+ }
55707
+ if (nd) {
55708
+ ids.push(id);
55709
+ states[id] = ["active"];
55710
+ }
55166
55711
  }
55167
55712
  let prev_ids = priv._focus_ids || [];
55168
55713
  priv._focus_ids = ids;
55169
- priv._focus_topic = topic || null;
55170
55714
  try {
55171
55715
  if (typeof graph.setElementState === "function") graph.setElementState(states);
55172
55716
  apply_node_highlight(gobj, prev_ids, ids);
55173
- if (ids.length && typeof graph.focusElement === "function") graph.focusElement(ids);
55717
+ if (ids.length) yui_graph_center_on(graph, ids[0]);
55174
55718
  } catch (e) {
55175
- log_error(`${gobj_short_name(gobj)}: focus topic apply failed: ${e}`);
55719
+ log_error(`${gobj_short_name(gobj)}: ${what} apply failed: ${e}`);
55176
55720
  }
55177
55721
  }
55722
+ function node_id_of_key(gobj, key) {
55723
+ let i = key.indexOf("^");
55724
+ if (i < 0) return "";
55725
+ return build_node_name(gobj, key.slice(0, i), key.slice(i + 1));
55726
+ }
55178
55727
  /************************************************************
55179
55728
  * Find the nodes whose name, id or topic contains `term`, highlight
55180
55729
  * them with the same amber 'active' state the topic focus uses, and
@@ -55191,52 +55740,43 @@ function graph_focus_topic(gobj, topic) {
55191
55740
  ************************************************************/
55192
55741
  function graph_find_nodes(gobj, term) {
55193
55742
  let priv = gobj.priv;
55194
- let graph = priv.graph;
55195
- if (!graph || !priv.graph_rendered) {
55743
+ if (!priv.graph || !priv.graph_rendered || !priv._fold_model) {
55196
55744
  priv._pending_find = term;
55197
55745
  return 0;
55198
55746
  }
55199
- let data = {};
55200
- try {
55201
- data = graph.getData() || {};
55202
- } catch (e) {
55203
- log_error(`${gobj_short_name(gobj)}: graph.getData() failed: ${e}`);
55204
- return 0;
55205
- }
55206
- let nodes = is_array(data.nodes) ? data.nodes : [];
55207
- let states = {};
55208
- for (let id of priv._focus_ids || []) states[id] = [];
55209
- let ids = [];
55747
+ let model = priv._fold_model;
55748
+ let state = priv._fold_state;
55749
+ let keys = [];
55210
55750
  if (!empty_string(term)) {
55211
55751
  let needle = String(term).toLowerCase();
55212
- for (let n of nodes) {
55213
- if (!n || !n.data) continue;
55214
- let record = n.data.record || {};
55752
+ for (let node of model.nodes.values()) {
55753
+ let record = node.record || {};
55215
55754
  let label = "";
55216
55755
  try {
55217
- label = node_label(n.data.desc || {}, record) || "";
55756
+ label = node_label(priv.descs[node.topic_name] || {}, record) || "";
55218
55757
  } catch (e) {
55219
55758
  label = "";
55220
55759
  }
55221
55760
  if ([
55222
55761
  label,
55223
55762
  record.id,
55224
- n.data.topic_name
55225
- ].filter((v) => typeof v === "string").join(" ").toLowerCase().includes(needle)) ids.push(n.id);
55763
+ node.topic_name
55764
+ ].filter((v) => typeof v === "string").join(" ").toLowerCase().includes(needle)) keys.push(node.key);
55226
55765
  }
55227
- for (let id of ids) states[id] = ["active"];
55228
55766
  }
55229
- let prev_ids = priv._focus_ids || [];
55230
- priv._focus_ids = ids;
55231
- priv._focus_topic = null;
55232
- try {
55233
- if (typeof graph.setElementState === "function") graph.setElementState(states);
55234
- apply_node_highlight(gobj, prev_ids, ids);
55235
- if (ids.length && typeof graph.focusElement === "function") graph.focusElement(ids);
55236
- } catch (e) {
55237
- log_error(`${gobj_short_name(gobj)}: find apply failed: ${e}`);
55767
+ let visible = fold_visible_set(model, state);
55768
+ let revealed = 0;
55769
+ for (let key of keys) {
55770
+ if (visible.has(key)) continue;
55771
+ if (revealed >= state.page_size) break;
55772
+ fold_reveal(model, state, key);
55773
+ revealed++;
55238
55774
  }
55239
- return ids.length;
55775
+ priv._focus_topic = null;
55776
+ reconcile_fold(gobj, {}).then(() => {
55777
+ apply_focus_ids(gobj, keys, "find");
55778
+ });
55779
+ return keys.length;
55240
55780
  }
55241
55781
  async function graph_zoom_in(gobj) {
55242
55782
  let graph = gobj.priv.graph;
@@ -55349,20 +55889,43 @@ var LightNode = class extends Circle {
55349
55889
  ************************************************************/
55350
55890
  var ManualLayout = class extends BaseLayout {
55351
55891
  async execute(data, options) {
55352
- const { nodes = [] } = data;
55353
- return { nodes: nodes.map((node) => {
55354
- let gp = node.data.graph_props;
55355
- let geo = node.data.record._geometry;
55892
+ const { nodes = [], edges = [] } = data;
55893
+ let placed = {};
55894
+ let out = [];
55895
+ let chips = [];
55896
+ for (let node of nodes) {
55897
+ let d = node.data || {};
55898
+ if (d.more) {
55899
+ chips.push(node);
55900
+ continue;
55901
+ }
55902
+ let gp = d.graph_props;
55903
+ let geo = d.record ? d.record._geometry : null;
55356
55904
  let x = gp && gp.x != null ? gp.x : geo && geo.x != null ? geo.x : 0;
55357
55905
  let y = gp && gp.y != null ? gp.y : geo && geo.y != null ? geo.y : 0;
55358
- return {
55906
+ placed[node.id] = [x, y];
55907
+ out.push({
55359
55908
  id: node.id,
55360
55909
  style: {
55361
55910
  x,
55362
55911
  y
55363
55912
  }
55364
- };
55365
- }) };
55913
+ });
55914
+ }
55915
+ for (let node of chips) {
55916
+ let edge = edges.find((e) => e.target === node.id);
55917
+ let p = edge ? placed[edge.source] : null;
55918
+ let x = p ? p[0] : node.style && node.style.x || 0;
55919
+ let y = p ? p[1] + 80 : node.style && node.style.y || 0;
55920
+ out.push({
55921
+ id: node.id,
55922
+ style: {
55923
+ x,
55924
+ y
55925
+ }
55926
+ });
55927
+ }
55928
+ return { nodes: out };
55366
55929
  }
55367
55930
  };
55368
55931
  /************************************************************
@@ -56866,7 +57429,7 @@ function show_node_popover(gobj) {
56866
57429
  };
56867
57430
  if (node_graph_type === "hierarchical") {
56868
57431
  let record = nodeData.data.record || {};
56869
- updateStyle.innerHTML = build_node_innerHTML(fill, priv.theme, record.icon, node_label(nodeData.data.desc, record), nodeData.data.desc.topic_name, false, record.id);
57432
+ updateStyle.innerHTML = build_node_innerHTML(fill, priv.theme, record.icon, node_label(nodeData.data.desc, record), nodeData.data.desc.topic_name, false, record.id, false, false, false, pills_html_of(gobj, nodeData));
56870
57433
  }
56871
57434
  graph.updateNodeData([{
56872
57435
  id: node_id,
@@ -56906,7 +57469,7 @@ function show_node_popover(gobj) {
56906
57469
  };
56907
57470
  if (node_graph_type === "hierarchical") {
56908
57471
  let record = nodeData.data.record || {};
56909
- restoreStyle.innerHTML = build_node_innerHTML(origFill, priv.theme, record.icon, node_label(nodeData.data.desc, record), nodeData.data.desc.topic_name, false, record.id);
57472
+ restoreStyle.innerHTML = build_node_innerHTML(origFill, priv.theme, record.icon, node_label(nodeData.data.desc, record), nodeData.data.desc.topic_name, false, record.id, false, false, false, pills_html_of(gobj, nodeData));
56910
57473
  }
56911
57474
  graph.updateNodeData([{
56912
57475
  id: node_id,
@@ -57091,6 +57654,7 @@ function refresh_minimap(gobj) {
57091
57654
  let nd = graph.getNodeData(id);
57092
57655
  let size = nd && nd.style && nd.style.size || [120, 60];
57093
57656
  let color = nd && nd.data && nd.data.desc && nd.data.desc.color || "#94a3b8";
57657
+ if (is_more_node(nd)) color = "rgba(148, 163, 184, 0.35)";
57094
57658
  return new Rect({ style: {
57095
57659
  x: -size[0] / 2,
57096
57660
  y: -size[1] / 2,
@@ -57113,15 +57677,15 @@ function refresh_minimap(gobj) {
57113
57677
  * same three-way choice — it lived inline in the theme refresh and is
57114
57678
  * shared now. Returns null for a node that carries no desc.
57115
57679
  ************************************************************/
57116
- function node_innerHTML_of(nd, theme, highlight, selected, anchored) {
57680
+ function node_innerHTML_of(nd, theme, highlight, selected, anchored, pills_html) {
57117
57681
  if (!nd || !nd.data || !nd.data.desc) return null;
57118
57682
  let desc = nd.data.desc;
57119
57683
  let record = nd.data.record || {};
57120
57684
  let label = node_label(desc, record);
57121
57685
  switch (desc.node_treedb_type) {
57122
57686
  case "child": return build_chip_innerHTML(desc.color, theme, record.icon, label, record.id, highlight, selected, anchored);
57123
- case "extended": return build_node_innerHTML(desc.color, theme, record.icon, label, desc.topic_name, true, record.id, highlight, selected, anchored);
57124
- case "hierarchical": return build_node_innerHTML(desc.color, theme, record.icon, label, desc.topic_name, false, record.id, highlight, selected, anchored);
57687
+ case "extended": return build_node_innerHTML(desc.color, theme, record.icon, label, desc.topic_name, true, record.id, highlight, selected, anchored, pills_html);
57688
+ case "hierarchical": return build_node_innerHTML(desc.color, theme, record.icon, label, desc.topic_name, false, record.id, highlight, selected, anchored, pills_html);
57125
57689
  }
57126
57690
  return null;
57127
57691
  }
@@ -57142,7 +57706,13 @@ function repaint_cards(gobj, ids) {
57142
57706
  let selected = new Set(priv._selected_paint_ids || []);
57143
57707
  let updates = [];
57144
57708
  for (let id of ids) {
57145
- let html = node_innerHTML_of(graph.getNodeData(id), priv.theme, focus.has(id), selected.has(id), priv.anchor_state === "on" && id === priv.anchor_id);
57709
+ let nd;
57710
+ try {
57711
+ nd = graph.getNodeData(id);
57712
+ } catch (e) {
57713
+ continue;
57714
+ }
57715
+ let html = node_innerHTML_of(nd, priv.theme, focus.has(id), selected.has(id), priv.anchor_state === "on" && id === priv.anchor_id, pills_html_of(gobj, nd));
57146
57716
  if (html !== null) updates.push({
57147
57717
  id,
57148
57718
  style: { innerHTML: html }
@@ -57178,7 +57748,9 @@ function refresh_html_nodes_theme(gobj, theme) {
57178
57748
  let updates = [];
57179
57749
  for (let i = 0; i < nodes.length; i++) {
57180
57750
  let id = nodes[i].id;
57181
- let html = node_innerHTML_of(graph.getNodeData(id), theme, highlighted.has(id), selected.has(id));
57751
+ let nd = graph.getNodeData(id);
57752
+ let html = node_innerHTML_of(nd, theme, highlighted.has(id), selected.has(id), priv.anchor_state === "on" && id === priv.anchor_id, pills_html_of(gobj, nd));
57753
+ if (html === null) html = more_innerHTML_of(gobj, nd, theme);
57182
57754
  if (html !== null) updates.push({
57183
57755
  id,
57184
57756
  style: { innerHTML: html }
@@ -57293,7 +57865,7 @@ function build_chip_innerHTML(color, theme, icon, label, key, highlight, selecte
57293
57865
  * colour is kept (per-topic differentiation) but softened via
57294
57866
  * color-mix instead of a harsh saturated fill. Theme-aware.
57295
57867
  ************************************************************/
57296
- function build_node_innerHTML(color, theme, icon, label, topic_name, structural, key, highlight, selected, anchored) {
57868
+ function build_node_innerHTML(color, theme, icon, label, topic_name, structural, key, highlight, selected, anchored, pills_html) {
57297
57869
  let title = key || label;
57298
57870
  let dark = theme === "dark";
57299
57871
  let surface = dark ? "#1b2230" : "#ffffff";
@@ -57327,6 +57899,12 @@ function build_node_innerHTML(color, theme, icon, label, topic_name, structural,
57327
57899
  margin-top: 2px; max-width: 100%;
57328
57900
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
57329
57901
  ">${escapeHtml(topic_name)}</div>`;
57902
+ let pills_row = "";
57903
+ if (pills_html) pills_row = `
57904
+ <div class="TREEDB_PILLS" style="
57905
+ display: flex; flex-wrap: nowrap; gap: 4px; justify-content: center;
57906
+ margin-top: 6px; max-width: 100%; overflow: hidden;
57907
+ ">${pills_html}</div>`;
57330
57908
  return `
57331
57909
  <div class="TREEDB_CARD" title="${escapeHtml(title)}" style="
57332
57910
  box-sizing: border-box;
@@ -57352,7 +57930,7 @@ function build_node_innerHTML(color, theme, icon, label, topic_name, structural,
57352
57930
  max-width: 100%; overflow: hidden; text-overflow: ellipsis;
57353
57931
  display: -webkit-box; -webkit-line-clamp: 2;
57354
57932
  -webkit-box-orient: vertical; word-break: break-word;
57355
- ">${escapeHtml(label)}</div>${sub_html}
57933
+ ">${escapeHtml(label)}</div>${sub_html}${pills_row}
57356
57934
  </div>
57357
57935
  `;
57358
57936
  }
@@ -57385,7 +57963,7 @@ function execute_delete_node(gobj, nodeData) {
57385
57963
  function select_all_nodes(gobj) {
57386
57964
  let graph = gobj.priv.graph;
57387
57965
  if (!graph) return;
57388
- set_selection(gobj, ((graph.getData() || {}).nodes || []).map((nd) => nd.id));
57966
+ set_selection(gobj, ((graph.getData() || {}).nodes || []).filter((nd) => nd.data && nd.data.desc).map((nd) => nd.id));
57389
57967
  }
57390
57968
  /************************************************************
57391
57969
  * The nodes currently selected, as their node data.
@@ -57649,7 +58227,7 @@ function apply_node_properties(gobj, node_id, fill, stroke, lineWidth, scope) {
57649
58227
  };
57650
58228
  if (nd.data.desc.node_treedb_type === "hierarchical") {
57651
58229
  let record = nd.data.record || {};
57652
- updateStyle.innerHTML = build_node_innerHTML(fill, priv.theme, record.icon, node_label(nd.data.desc, record), nd.data.desc.topic_name, false, record.id);
58230
+ updateStyle.innerHTML = build_node_innerHTML(fill, priv.theme, record.icon, node_label(nd.data.desc, record), nd.data.desc.topic_name, false, record.id, false, false, false, pills_html_of(gobj, nd));
57653
58231
  }
57654
58232
  updates.push({
57655
58233
  id: nodes[i].id,
@@ -57725,7 +58303,7 @@ function apply_edge_properties(gobj, edge_id, lineWidth, stroke, scope) {
57725
58303
  function update_edge_geometry(gobj, edge_id) {
57726
58304
  let priv = gobj.priv;
57727
58305
  let edgeData = priv.graph.getEdgeData(edge_id);
57728
- if (!edgeData || !edgeData.data) return;
58306
+ if (!edgeData || !edgeData.data || !edgeData.data.parent_topic) return;
57729
58307
  let style = edgeData.style || {};
57730
58308
  let lineWidth = style.lineWidth;
57731
58309
  let stroke = style.stroke;
@@ -57866,7 +58444,7 @@ function copy_size_to_nodes(gobj, same_topic_only) {
57866
58444
  const nodes = graph.getData().nodes;
57867
58445
  for (let i = 0; i < nodes.length; i++) {
57868
58446
  let nd = graph.getNodeData(nodes[i].id);
57869
- if (!nd || !nd.data) continue;
58447
+ if (!nd || !nd.data || !nd.data.desc) continue;
57870
58448
  if (same_topic_only && nd.data.topic_name !== source_topic) continue;
57871
58449
  if (nodes[i].id === node_id) continue;
57872
58450
  let updateStyle = { size: [...source_size] };
@@ -57954,6 +58532,415 @@ function copy_size_to_ports(gobj, same_topic_only) {
57954
58532
  }
57955
58533
  }
57956
58534
  /***************************
58535
+ * Folding
58536
+ ***************************/
58537
+ var PILL_ROW_HEIGHT = 24;
58538
+ var MORE_CHIP_SIZE = [76, 26];
58539
+ /************************************************************
58540
+ * The three card sizes, with room for the pill row when there
58541
+ * is one. A chip (pure child) never has pills: it has no hooks.
58542
+ ************************************************************/
58543
+ function card_size_for(node_treedb_type, has_pills) {
58544
+ let extra = has_pills ? PILL_ROW_HEIGHT : 0;
58545
+ switch (node_treedb_type) {
58546
+ case "child": return [116, 40];
58547
+ case "extended": return [144, 66 + extra];
58548
+ default: return [172, 96 + extra];
58549
+ }
58550
+ }
58551
+ /************************************************************
58552
+ * The model's key of a G6 record node, or "" for anything else.
58553
+ ************************************************************/
58554
+ function node_key_of(nd) {
58555
+ if (!nd || !nd.data || !nd.data.desc || !nd.data.record) return "";
58556
+ return fold_node_key(nd.data.desc.topic_name, String(nd.data.record.id));
58557
+ }
58558
+ function is_more_node(nd) {
58559
+ return !!(nd && nd.data && nd.data.more);
58560
+ }
58561
+ function more_node_id(gobj, group_key) {
58562
+ return sprintf("more-%s-%s", gobj.priv.treedb_name, group_key);
58563
+ }
58564
+ function more_edge_id(gobj, group_key) {
58565
+ return sprintf("more-edge-%s-%s", gobj.priv.treedb_name, group_key);
58566
+ }
58567
+ /************************************************************
58568
+ * The colours of a pill or a chip, from the topic colour it
58569
+ * stands for -- the CHILD topic, the same colour its hook port
58570
+ * wears, so the pill and the port say the same thing.
58571
+ ************************************************************/
58572
+ function pill_colors(color, theme) {
58573
+ let dark = theme === "dark";
58574
+ return {
58575
+ bg: dark ? `color-mix(in srgb, ${color} 45%, #1b2230)` : `color-mix(in srgb, ${color} 22%, #ffffff)`,
58576
+ fg: dark ? `color-mix(in srgb, ${color} 35%, #ffffff)` : `color-mix(in srgb, ${color} 55%, #0f172a)`,
58577
+ border: dark ? `color-mix(in srgb, ${color} 80%, #ffffff)` : color
58578
+ };
58579
+ }
58580
+ /************************************************************
58581
+ * One pill: `▸ devices 142` folded, `▾ devices 24/142` open on a
58582
+ * page, `▾ devices 142` open whole. A glyph and numbers on
58583
+ * purpose: the card is an innerHTML string where a WORD could
58584
+ * carry no i18n key, and the hook name is a schema identifier,
58585
+ * like the topic subtitle above it.
58586
+ ************************************************************/
58587
+ function build_pill_html(pill, color, theme) {
58588
+ let c = pill_colors(color, theme);
58589
+ let glyph = pill.open ? "&#9662;" : "&#9656;";
58590
+ let count = pill.open && pill.shown < pill.total ? `${pill.shown}/${pill.total}` : `${pill.total}`;
58591
+ return `<span class="TREEDB_PILL" data-fold-group="${escapeHtml(pill.group_key)}"
58592
+ title="${escapeHtml(pill.hook)}" style="
58593
+ display: inline-flex; align-items: center; gap: 3px; flex: 0 1 auto;
58594
+ height: 16px; padding: 0 6px; border-radius: 8px;
58595
+ background: ${c.bg}; color: ${c.fg}; border: 1px solid ${c.border};
58596
+ font-size: 10px; font-weight: 700; line-height: 1; white-space: nowrap;
58597
+ overflow: hidden; text-overflow: ellipsis;
58598
+ cursor: pointer; user-select: none;
58599
+ "><span style="font-size: 9px;">${glyph}</span>${escapeHtml(pill.hook)}<span style="opacity: .75; font-weight: 600;">${count}</span></span>`;
58600
+ }
58601
+ /************************************************************
58602
+ * The pill row of one node, as html; "" when it has nothing to
58603
+ * fold or the model is not built yet.
58604
+ ************************************************************/
58605
+ function pills_html_of_key(gobj, node_key) {
58606
+ let priv = gobj.priv;
58607
+ if (!priv._fold_model || !priv._fold_state || !node_key) return "";
58608
+ let pills = fold_pills_of(priv._fold_model, priv._fold_state, node_key);
58609
+ if (!pills.length) return "";
58610
+ let out = [];
58611
+ for (let pill of pills) {
58612
+ let child_desc = priv.descs ? priv.descs[pill.child_topic] : null;
58613
+ let color = child_desc && child_desc.color || "#94a3b8";
58614
+ out.push(build_pill_html(pill, color, priv.theme));
58615
+ }
58616
+ return out.join("");
58617
+ }
58618
+ function pills_html_of(gobj, nd) {
58619
+ return pills_html_of_key(gobj, node_key_of(nd));
58620
+ }
58621
+ function pills_signature(gobj, node_key) {
58622
+ let priv = gobj.priv;
58623
+ if (!priv._fold_model || !priv._fold_state) return "";
58624
+ return fold_pills_of(priv._fold_model, priv._fold_state, node_key).map((p) => `${p.hook}:${p.open ? p.shown : "-"}/${p.total}`).join(",");
58625
+ }
58626
+ /************************************************************
58627
+ * The `+N` chip at the end of a page: the rest of the group,
58628
+ * in the child topic's colour, dashed like a cut. Its title is
58629
+ * the one WORD of the folding, and it is translated at build
58630
+ * time: the chips are rebuilt on a language change.
58631
+ ************************************************************/
58632
+ function build_more_innerHTML(color, theme, rest) {
58633
+ let c = pill_colors(color, theme);
58634
+ return `
58635
+ <div class="TREEDB_MORE" title="${escapeHtml(t("show more"))}" style="
58636
+ box-sizing: border-box; width: 100%; height: 100%;
58637
+ display: flex; align-items: center; justify-content: center; gap: 4px;
58638
+ background: ${c.bg}; color: ${c.fg}; border: 1px dashed ${c.border};
58639
+ border-radius: 13px;
58640
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
58641
+ font-size: 12px; font-weight: 700; line-height: 1;
58642
+ cursor: pointer; user-select: none;
58643
+ ">+${rest}<span style="font-size: 10px; opacity: .8;">&#8230;</span></div>
58644
+ `;
58645
+ }
58646
+ function more_innerHTML_of(gobj, nd, theme) {
58647
+ if (!is_more_node(nd)) return null;
58648
+ let priv = gobj.priv;
58649
+ let child_desc = priv.descs ? priv.descs[nd.data.child_topic] : null;
58650
+ return build_more_innerHTML(child_desc && child_desc.color || "#94a3b8", theme, nd.data.rest);
58651
+ }
58652
+ /************************************************************
58653
+ * The pills live inside an `innerHTML` string, so no handler can
58654
+ * be attached to them: the click is delegated from the container
58655
+ * in the CAPTURE phase over four event types -- the same door the
58656
+ * JSON graph opens for its fold handles, for the same reasons: G6
58657
+ * binds on the html node element, a descendant, so only capture
58658
+ * runs first; and G6 never reads the DOM `click`, it builds its
58659
+ * own from the pointer pair, so `pointerdown`/`pointerup` are the
58660
+ * ones that have to be cut or the card is ALSO clicked.
58661
+ ************************************************************/
58662
+ var FOLD_SWALLOWED_EVENTS = [
58663
+ "pointerdown",
58664
+ "pointerup",
58665
+ "mousedown",
58666
+ "click"
58667
+ ];
58668
+ function install_fold_listener(gobj) {
58669
+ let priv = gobj.priv;
58670
+ if (!priv.$container) {
58671
+ log_error(`${gobj_short_name(gobj)}: no container, the fold pills are dead`);
58672
+ return;
58673
+ }
58674
+ priv._fold_listener = function(evt) {
58675
+ let $pill = evt.target && evt.target.closest ? evt.target.closest("[data-fold-group]") : null;
58676
+ if (!$pill) return;
58677
+ evt.stopPropagation();
58678
+ evt.preventDefault();
58679
+ if (evt.type !== "click") return;
58680
+ gobj_send_event(gobj, "EV_TOGGLE_FOLD", { group: $pill.getAttribute("data-fold-group") }, gobj);
58681
+ };
58682
+ for (let type of FOLD_SWALLOWED_EVENTS) priv.$container.addEventListener(type, priv._fold_listener, true);
58683
+ }
58684
+ function uninstall_fold_listener(gobj) {
58685
+ let priv = gobj.priv;
58686
+ if (priv._fold_listener && priv.$container) for (let type of FOLD_SWALLOWED_EVENTS) priv.$container.removeEventListener(type, priv._fold_listener, true);
58687
+ priv._fold_listener = null;
58688
+ }
58689
+ /************************************************************
58690
+ * (Re)build the tree of the records. Cheap -- one pass over the
58691
+ * records -- so it runs again on every create/update/delete
58692
+ * rather than being patched. The fold state survives: it is
58693
+ * keyed by group, and a group that is gone is simply never read.
58694
+ ************************************************************/
58695
+ function rebuild_fold_model(gobj) {
58696
+ let priv = gobj.priv;
58697
+ priv._fold_model = fold_build_model(priv.descs || {}, priv.records || {});
58698
+ if (!priv._fold_state) priv._fold_state = fold_new_state(gobj_read_integer_attr(gobj, "fold_page_size"));
58699
+ if (!priv._pill_sig) priv._pill_sig = /* @__PURE__ */ new Map();
58700
+ }
58701
+ /************************************************************
58702
+ * How many records there are, and how many carry a saved
58703
+ * position -- what auto_layout decides on. Counted over the
58704
+ * RECORDS, because the nodes on screen are a fraction of them.
58705
+ ************************************************************/
58706
+ function count_placed_records(gobj) {
58707
+ let priv = gobj.priv;
58708
+ priv._nodes_total = 0;
58709
+ priv._nodes_placed = 0;
58710
+ for (const [topic_name, records] of Object.entries(priv.records || {})) {
58711
+ if (topic_name.substring(0, 2) === "__" || !is_array(records)) continue;
58712
+ for (let record of records) {
58713
+ priv._nodes_total++;
58714
+ if (geometry_has_position(get_node_graph_props(gobj, topic_name, record.id, record))) priv._nodes_placed++;
58715
+ }
58716
+ }
58717
+ }
58718
+ /************************************************************
58719
+ * Make the G6 data say what the fold model says is visible.
58720
+ *
58721
+ * Record nodes not visible any more are removed (their edges go
58722
+ * with them), the visible ones missing are created, the links
58723
+ * between visible nodes are drawn, the `+N` chips are placed or
58724
+ * taken away, and every card whose pills changed is repainted.
58725
+ * Then a draw, a layout if the set of nodes changed, and the
58726
+ * camera: fit the whole thing on the first load, or put `keep`
58727
+ * back where it was on screen before the change.
58728
+ *
58729
+ * Serialised: one reconcile at a time, in order. Two pills tapped
58730
+ * quickly would otherwise interleave their draws.
58731
+ *
58732
+ * opts.relayout run the layout even if no node came or went
58733
+ * opts.fit fit the viewport afterwards
58734
+ * opts.keep node id to hold still on screen
58735
+ * opts.keep_vp where it was ([x, y] viewport px)
58736
+ ************************************************************/
58737
+ function reconcile_fold(gobj, opts) {
58738
+ let priv = gobj.priv;
58739
+ let run = () => reconcile_fold_now(gobj, opts || {}).catch((e) => {
58740
+ log_error(`${gobj_short_name(gobj)}: reconcile failed: ${e}`);
58741
+ });
58742
+ priv._fold_chain = (priv._fold_chain || Promise.resolve()).then(run, run);
58743
+ return priv._fold_chain;
58744
+ }
58745
+ async function reconcile_fold_now(gobj, opts) {
58746
+ let priv = gobj.priv;
58747
+ let graph = priv.graph;
58748
+ let model = priv._fold_model;
58749
+ let state = priv._fold_state;
58750
+ if (!graph || !model || !state || gobj_is_destroying(gobj)) return;
58751
+ let visible = fold_visible_set(model, state);
58752
+ let changed = false;
58753
+ history_pause(gobj);
58754
+ try {
58755
+ changed = await reconcile_fold_apply(gobj, opts, visible);
58756
+ } finally {
58757
+ history_resume(gobj);
58758
+ }
58759
+ return changed;
58760
+ }
58761
+ async function reconcile_fold_apply(gobj, opts, visible) {
58762
+ let priv = gobj.priv;
58763
+ let graph = priv.graph;
58764
+ let model = priv._fold_model;
58765
+ let state = priv._fold_state;
58766
+ let changed = false;
58767
+ let present = /* @__PURE__ */ new Map();
58768
+ let more_present = /* @__PURE__ */ new Map();
58769
+ for (let nd of graph.getNodeData() || []) {
58770
+ if (is_more_node(nd)) {
58771
+ more_present.set(nd.data.more, nd.id);
58772
+ continue;
58773
+ }
58774
+ let key = node_key_of(nd);
58775
+ if (key) present.set(key, nd.id);
58776
+ }
58777
+ let to_remove = [];
58778
+ for (let [key, id] of present) if (!visible.has(key)) to_remove.push(id);
58779
+ if (to_remove.length) {
58780
+ let gone = new Set(to_remove);
58781
+ if (priv._selected_node_id && gone.has(priv._selected_node_id)) deselect_node(gobj);
58782
+ if (priv._detail_node_id && gone.has(priv._detail_node_id)) hide_node_detail(gobj);
58783
+ priv._focus_ids = (priv._focus_ids || []).filter((id) => !gone.has(id));
58784
+ priv._selected_paint_ids = (priv._selected_paint_ids || []).filter((id) => !gone.has(id));
58785
+ if (priv.anchor_state === "on" && gone.has(priv.anchor_id)) {
58786
+ priv.anchor_id = "";
58787
+ priv.anchor_state = "off";
58788
+ update_toolbar(gobj);
58789
+ }
58790
+ for (let id of to_remove) priv._pill_sig.delete(id);
58791
+ graph.removeNodeData(to_remove);
58792
+ changed = true;
58793
+ }
58794
+ for (let key of visible) {
58795
+ if (present.has(key)) continue;
58796
+ let node = model.nodes.get(key);
58797
+ let desc = priv.descs[node.topic_name];
58798
+ if (!desc) {
58799
+ log_error(`${gobj_short_name(gobj)}: no desc for ${key}`);
58800
+ continue;
58801
+ }
58802
+ create_topic_node(gobj, desc, node.record);
58803
+ priv._pill_sig.set(build_node_name(gobj, node.topic_name, node.id), pills_signature(gobj, key));
58804
+ changed = true;
58805
+ }
58806
+ for (let key of visible) {
58807
+ let node = model.nodes.get(key);
58808
+ let desc = priv.descs[node.topic_name];
58809
+ if (desc) draw_links(gobj, desc, node.record, false);
58810
+ }
58811
+ let pending = fold_pending_groups(model, state, visible);
58812
+ let wanted = /* @__PURE__ */ new Map();
58813
+ for (let p of pending) wanted.set(p.group_key, p);
58814
+ let more_gone = [];
58815
+ for (let [group_key, id] of more_present) if (!wanted.has(group_key)) more_gone.push(id);
58816
+ if (more_gone.length) {
58817
+ graph.removeNodeData(more_gone);
58818
+ changed = true;
58819
+ }
58820
+ for (let p of pending) {
58821
+ let first = model.nodes.get((model.groups.get(p.group_key) || [])[0]);
58822
+ let child_topic = first ? first.topic_name : "";
58823
+ let child_desc = priv.descs[child_topic];
58824
+ let color = child_desc && child_desc.color || "#94a3b8";
58825
+ let id = more_node_id(gobj, p.group_key);
58826
+ if (more_present.has(p.group_key)) {
58827
+ let nd = graph.getNodeData(id);
58828
+ if (nd && nd.data.rest !== p.rest) {
58829
+ nd.data.rest = p.rest;
58830
+ graph.updateNodeData([{
58831
+ id,
58832
+ style: { innerHTML: build_more_innerHTML(color, priv.theme, p.rest) }
58833
+ }]);
58834
+ }
58835
+ continue;
58836
+ }
58837
+ let xy = get_default_ne_xy(gobj);
58838
+ graph.addNodeData([{
58839
+ id,
58840
+ type: "html",
58841
+ style: {
58842
+ x: xy,
58843
+ y: xy,
58844
+ size: [...MORE_CHIP_SIZE],
58845
+ dx: -MORE_CHIP_SIZE[0] / 2,
58846
+ dy: -MORE_CHIP_SIZE[1] / 2,
58847
+ innerHTML: build_more_innerHTML(color, priv.theme, p.rest)
58848
+ },
58849
+ data: {
58850
+ more: p.group_key,
58851
+ child_topic,
58852
+ rest: p.rest
58853
+ }
58854
+ }]);
58855
+ if (p.node_key) {
58856
+ let parent = model.nodes.get(p.node_key);
58857
+ let dark = priv.theme === "dark";
58858
+ graph.addEdgeData([{
58859
+ id: more_edge_id(gobj, p.group_key),
58860
+ type: edge_type_for(gobj),
58861
+ source: build_node_name(gobj, parent.topic_name, parent.id),
58862
+ target: id,
58863
+ style: {
58864
+ sourcePort: p.hook,
58865
+ lineWidth: 1,
58866
+ lineDash: [4, 3],
58867
+ stroke: dark ? "#8b94a3" : "#94a3b8",
58868
+ startArrow: false,
58869
+ endArrow: false
58870
+ },
58871
+ data: { more: p.group_key }
58872
+ }]);
58873
+ }
58874
+ changed = true;
58875
+ }
58876
+ let repaint = /* @__PURE__ */ new Set();
58877
+ let resize = [];
58878
+ for (let key of visible) {
58879
+ let node = model.nodes.get(key);
58880
+ let id = build_node_name(gobj, node.topic_name, node.id);
58881
+ let sig = pills_signature(gobj, key);
58882
+ if (priv._pill_sig.get(id) === sig) continue;
58883
+ let had = !!priv._pill_sig.get(id);
58884
+ priv._pill_sig.set(id, sig);
58885
+ repaint.add(id);
58886
+ if (had !== !!sig) {
58887
+ let nd = graph.getNodeData(id);
58888
+ let desc = nd && nd.data && nd.data.desc;
58889
+ let geometry = nd && nd.data && nd.data.graph_props || {};
58890
+ if (desc && !is_array(geometry.size)) {
58891
+ let size = card_size_for(desc.node_treedb_type, !!sig);
58892
+ resize.push({
58893
+ id,
58894
+ style: {
58895
+ size,
58896
+ dx: -size[0] / 2,
58897
+ dy: -size[1] / 2
58898
+ }
58899
+ });
58900
+ }
58901
+ }
58902
+ }
58903
+ if (resize.length) graph.updateNodeData(resize);
58904
+ if (repaint.size) repaint_cards(gobj, repaint);
58905
+ try {
58906
+ refresh_minimap(gobj);
58907
+ } catch (e) {
58908
+ log_error(`${gobj_short_name(gobj)}: refresh_minimap failed: ${e}`);
58909
+ }
58910
+ await graph_draw(gobj);
58911
+ if (changed || opts.relayout) await graph_layout(gobj);
58912
+ if (opts.fit) await graph_fit_readable(gobj);
58913
+ else if (opts.keep && opts.keep_vp) await yui_graph_place_at(graph, opts.keep, opts.keep_vp);
58914
+ update_resize_handles_position(gobj);
58915
+ update_port_resize_handles_position(gobj);
58916
+ update_edge_icon_position(gobj);
58917
+ update_node_icon_position(gobj);
58918
+ update_link_icon_position(gobj);
58919
+ return changed;
58920
+ }
58921
+ /************************************************************
58922
+ * Repaint the `+N` chips (theme or language changed).
58923
+ ************************************************************/
58924
+ function repaint_more_chips(gobj) {
58925
+ let priv = gobj.priv;
58926
+ let graph = priv.graph;
58927
+ if (!graph) return;
58928
+ let updates = [];
58929
+ for (let nd of graph.getNodeData() || []) {
58930
+ let html = more_innerHTML_of(gobj, nd, priv.theme);
58931
+ if (html !== null) updates.push({
58932
+ id: nd.id,
58933
+ style: { innerHTML: html }
58934
+ });
58935
+ }
58936
+ if (updates.length) try {
58937
+ graph.updateNodeData(updates);
58938
+ graph.draw();
58939
+ } catch (e) {
58940
+ log_error(`${gobj_short_name(gobj)}: cannot repaint the chips: ${e}`);
58941
+ }
58942
+ }
58943
+ /***************************
57957
58944
  * Actions
57958
58945
  ***************************/
57959
58946
  /************************************************************
@@ -57982,6 +58969,9 @@ function ac_clear_data(gobj, event, kw, src) {
57982
58969
  gobj_write_attr(gobj, "records", {});
57983
58970
  priv._nodes_total = 0;
57984
58971
  priv._nodes_placed = 0;
58972
+ priv._fold_model = null;
58973
+ priv._fold_state = null;
58974
+ priv._pill_sig = null;
57985
58975
  graph_remove_plugin(gobj, "history");
57986
58976
  update_history_buttons(gobj);
57987
58977
  graph_clear(gobj);
@@ -58012,53 +59002,43 @@ function ac_load_data(gobj, event, kw, src) {
58012
59002
  }
58013
59003
  return 0;
58014
59004
  }
58015
- let desc = priv.descs[topic_name];
59005
+ priv.descs[topic_name];
58016
59006
  priv.records[topic_name] = data;
58017
59007
  priv.descs[topic_name].loaded = true;
58018
- for (let i = 0; i < data.length; i++) {
58019
- let record = data[i];
58020
- create_topic_node(gobj, desc, record);
58021
- }
58022
- let do_links = true;
59008
+ let all_loaded = true;
58023
59009
  for (const [topic_name, desc] of Object.entries(priv.descs)) {
58024
59010
  if (topic_name.substring(0, 2) === "__") continue;
58025
59011
  if (!desc.loaded) {
58026
- do_links = false;
59012
+ all_loaded = false;
58027
59013
  break;
58028
59014
  }
58029
59015
  }
58030
- if (do_links && priv.graph) {
59016
+ if (all_loaded && priv.graph) {
59017
+ rebuild_fold_model(gobj);
59018
+ fold_expand_to_depth(priv._fold_model, priv._fold_state, gobj_read_integer_attr(gobj, "expand_depth"));
59019
+ count_placed_records(gobj);
59020
+ let auto_laid = false;
58031
59021
  try {
58032
- refresh_minimap(gobj);
59022
+ auto_laid = auto_layout(gobj);
58033
59023
  } catch (e) {
58034
- log_error(`${gobj_short_name(gobj)}: refresh_minimap failed: ${e}`);
58035
- }
58036
- graph_draw(gobj).then(() => {
58037
- create_links(gobj);
58038
- graph_draw(gobj).then(() => {
58039
- let auto_laid = false;
58040
- try {
58041
- auto_laid = auto_layout(gobj);
58042
- } catch (e) {
58043
- log_error(`${gobj_short_name(gobj)}: auto_layout failed: ${e}`);
58044
- }
58045
- graph_layout(gobj).then(() => {
58046
- if (auto_laid) graph_fit_readable(gobj).catch((e) => {
58047
- log_error(`${gobj_short_name(gobj)}: cannot fit the graph: ${e}`);
58048
- });
58049
- sync_history_plugin(gobj);
58050
- if (priv._pending_focus_topic !== null) {
58051
- let ft = priv._pending_focus_topic;
58052
- priv._pending_focus_topic = null;
58053
- graph_focus_topic(gobj, ft);
58054
- }
58055
- if (priv._pending_find !== null) {
58056
- let term = priv._pending_find;
58057
- priv._pending_find = null;
58058
- publish_find_result(gobj, term, graph_find_nodes(gobj, term));
58059
- }
58060
- });
58061
- });
59024
+ log_error(`${gobj_short_name(gobj)}: auto_layout failed: ${e}`);
59025
+ }
59026
+ apply_layout_direction(gobj);
59027
+ reconcile_fold(gobj, {
59028
+ relayout: true,
59029
+ fit: auto_laid
59030
+ }).then(() => {
59031
+ sync_history_plugin(gobj);
59032
+ if (priv._pending_focus_topic !== null) {
59033
+ let ft = priv._pending_focus_topic;
59034
+ priv._pending_focus_topic = null;
59035
+ graph_focus_topic(gobj, ft);
59036
+ }
59037
+ if (priv._pending_find !== null) {
59038
+ let term = priv._pending_find;
59039
+ priv._pending_find = null;
59040
+ publish_find_result(gobj, term, graph_find_nodes(gobj, term));
59041
+ }
58062
59042
  });
58063
59043
  }
58064
59044
  return 0;
@@ -58094,19 +59074,15 @@ function ac_node_created(gobj, event, kw, src) {
58094
59074
  build_graph_properties(gobj);
58095
59075
  return 0;
58096
59076
  }
58097
- let desc = priv.descs[topic_name];
58098
- if (!desc) {
59077
+ if (!priv.descs[topic_name]) {
58099
59078
  log_error(`ac_node_created: unknown topic: ${topic_name}`);
58100
59079
  return 0;
58101
59080
  }
58102
59081
  if (!priv.records[topic_name]) priv.records[topic_name] = [];
58103
59082
  priv.records[topic_name].push(node);
58104
- history_pause(gobj);
58105
- create_topic_node(gobj, desc, node);
58106
- draw_links(gobj, desc, node, false);
58107
- graph_draw(gobj).then(() => {
58108
- history_resume(gobj);
58109
- });
59083
+ rebuild_fold_model(gobj);
59084
+ fold_reveal(priv._fold_model, priv._fold_state, fold_node_key(topic_name, String(node.id)));
59085
+ reconcile_fold(gobj, {});
58110
59086
  return 0;
58111
59087
  }
58112
59088
  /************************************************************
@@ -58155,8 +59131,14 @@ function ac_node_updated(gobj, event, kw, src) {
58155
59131
  let [col_idx, fkey_value] = ref.split(" ");
58156
59132
  clear_link(gobj, topic_name, node.id, cols[parseInt(col_idx)], fkey_value, false);
58157
59133
  }
58158
- update_topic_node(gobj, desc, node_name, node);
58159
59134
  update_local_node(gobj, topic_name, node);
59135
+ let on_screen = false;
59136
+ try {
59137
+ on_screen = !!graph.getNodeData(node_name);
59138
+ } catch (e) {
59139
+ on_screen = false;
59140
+ }
59141
+ if (on_screen) update_topic_node(gobj, desc, node_name, node);
58160
59142
  for (let ref of new_refs) if (!old_refs.has(ref)) {
58161
59143
  let [col_idx, fkey_value] = ref.split(" ");
58162
59144
  draw_link(gobj, topic_name, node.id, cols[parseInt(col_idx)], fkey_value, false);
@@ -58164,6 +59146,8 @@ function ac_node_updated(gobj, event, kw, src) {
58164
59146
  graph_draw(gobj).then(() => {
58165
59147
  history_resume(gobj);
58166
59148
  });
59149
+ rebuild_fold_model(gobj);
59150
+ reconcile_fold(gobj, {});
58167
59151
  return 0;
58168
59152
  }
58169
59153
  /************************************************************
@@ -58189,9 +59173,12 @@ function ac_node_deleted(gobj, event, kw, src) {
58189
59173
  clear_links(gobj, desc, node, false);
58190
59174
  remove_topic_node(gobj, node_name);
58191
59175
  remove_local_node(gobj, topic_name, node);
59176
+ if (priv._pill_sig) priv._pill_sig.delete(node_name);
58192
59177
  graph_draw(gobj).then(() => {
58193
59178
  history_resume(gobj);
58194
59179
  });
59180
+ rebuild_fold_model(gobj);
59181
+ reconcile_fold(gobj, {});
58195
59182
  return 0;
58196
59183
  }
58197
59184
  /************************************************************
@@ -58250,6 +59237,7 @@ function ac_theme$1(gobj, event, kw, src) {
58250
59237
  });
58251
59238
  refresh_html_nodes_theme(gobj, theme);
58252
59239
  refresh_default_edges_theme(gobj, theme);
59240
+ repaint_more_chips(gobj);
58253
59241
  graph_draw(gobj).then(() => {
58254
59242
  update_history_buttons(gobj);
58255
59243
  });
@@ -58364,6 +59352,85 @@ function ac_focus_topic(gobj, event, kw, src) {
58364
59352
  return 0;
58365
59353
  }
58366
59354
  /************************************************************
59355
+ * A pill was tapped: open that hook on its first page, or fold
59356
+ * it. The card the pill is on stays where it was on screen --
59357
+ * the layout moves everything else around it.
59358
+ ************************************************************/
59359
+ function ac_toggle_fold(gobj, event, kw, src) {
59360
+ let priv = gobj.priv;
59361
+ let group = kw && kw.group || "";
59362
+ if (!priv._fold_model || !group) {
59363
+ log_error(`${gobj_short_name(gobj)}: fold of '${group}' with no tree loaded`);
59364
+ return -1;
59365
+ }
59366
+ let g = fold_split_group_key(group);
59367
+ if (!g) {
59368
+ log_error(`${gobj_short_name(gobj)}: bad fold group '${group}'`);
59369
+ return -1;
59370
+ }
59371
+ let node_id = g.node_key ? node_id_of_key(gobj, g.node_key) : "";
59372
+ let vp = node_id ? yui_graph_viewport_of(priv.graph, node_id) : null;
59373
+ fold_toggle(priv._fold_model, priv._fold_state, group);
59374
+ reconcile_fold(gobj, {
59375
+ keep: node_id,
59376
+ keep_vp: vp
59377
+ });
59378
+ return 0;
59379
+ }
59380
+ /************************************************************
59381
+ * A `+N` chip was tapped: the next page of its group. The
59382
+ * parent holds still; the chip itself moves down the column.
59383
+ ************************************************************/
59384
+ function ac_show_more(gobj, event, kw, src) {
59385
+ let priv = gobj.priv;
59386
+ let group = kw && kw.group || "";
59387
+ if (!priv._fold_model || !group) {
59388
+ log_error(`${gobj_short_name(gobj)}: show more of '${group}' with no tree loaded`);
59389
+ return -1;
59390
+ }
59391
+ let g = fold_split_group_key(group);
59392
+ if (!g) {
59393
+ log_error(`${gobj_short_name(gobj)}: bad fold group '${group}'`);
59394
+ return -1;
59395
+ }
59396
+ let node_id = g.node_key ? node_id_of_key(gobj, g.node_key) : "";
59397
+ let vp = node_id ? yui_graph_viewport_of(priv.graph, node_id) : null;
59398
+ fold_show_more(priv._fold_model, priv._fold_state, group);
59399
+ reconcile_fold(gobj, {
59400
+ keep: node_id,
59401
+ keep_vp: vp
59402
+ });
59403
+ return 0;
59404
+ }
59405
+ /************************************************************
59406
+ * The whole treedb, every hook open on all its children. This
59407
+ * is the reader asking for the pile on purpose; the pages are
59408
+ * the other way in.
59409
+ ************************************************************/
59410
+ function ac_expand_all$1(gobj, event, kw, src) {
59411
+ let priv = gobj.priv;
59412
+ if (!priv._fold_model) {
59413
+ log_error(`${gobj_short_name(gobj)}: expand all with no tree loaded`);
59414
+ return -1;
59415
+ }
59416
+ fold_expand_all(priv._fold_model, priv._fold_state);
59417
+ reconcile_fold(gobj, { fit: true });
59418
+ return 0;
59419
+ }
59420
+ /************************************************************
59421
+ * The roots alone.
59422
+ ************************************************************/
59423
+ function ac_collapse_all$1(gobj, event, kw, src) {
59424
+ let priv = gobj.priv;
59425
+ if (!priv._fold_model) {
59426
+ log_error(`${gobj_short_name(gobj)}: collapse all with no tree loaded`);
59427
+ return -1;
59428
+ }
59429
+ fold_collapse_all(priv._fold_model, priv._fold_state);
59430
+ reconcile_fold(gobj, { fit: true });
59431
+ return 0;
59432
+ }
59433
+ /************************************************************
58367
59434
  * Tell whoever asked how many nodes the term matched. The count is
58368
59435
  * the answer to the question the box asks; without it "nothing moved"
58369
59436
  * and "nothing matched" look the same.
@@ -58394,7 +59461,9 @@ function ac_fullscreen(gobj, event, kw, src) {
58394
59461
  ************************************************************/
58395
59462
  function ac_set_layout$1(gobj, event, kw, src) {
58396
59463
  gobj.priv;
58397
- graph_set_layout(gobj, select_layout(gobj, kw.layout)).then(() => {
59464
+ let layout = select_layout(gobj, kw.layout);
59465
+ apply_layout_direction(gobj);
59466
+ graph_set_layout(gobj, layout).then(() => {
58398
59467
  configure_behaviour(gobj);
58399
59468
  update_toolbar(gobj);
58400
59469
  });
@@ -58501,6 +59570,16 @@ function ac_node_click$2(gobj, event, kw, src) {
58501
59570
  let priv = gobj.priv;
58502
59571
  let graph = priv.graph;
58503
59572
  let node_id = kw.evt.target.id;
59573
+ let more_nd = null;
59574
+ try {
59575
+ more_nd = graph.getNodeData(node_id);
59576
+ } catch (e) {
59577
+ more_nd = null;
59578
+ }
59579
+ if (is_more_node(more_nd)) {
59580
+ gobj_send_event(gobj, "EV_SHOW_MORE", { group: more_nd.data.more }, gobj);
59581
+ return 0;
59582
+ }
58504
59583
  if (priv.anchor_state === "arming") {
58505
59584
  priv.anchor_id = node_id;
58506
59585
  priv.anchor_state = "on";
@@ -58805,6 +59884,26 @@ function create_gclass$4(gclass_name) {
58805
59884
  ac_find_nodes$1,
58806
59885
  null
58807
59886
  ],
59887
+ [
59888
+ "EV_TOGGLE_FOLD",
59889
+ ac_toggle_fold,
59890
+ null
59891
+ ],
59892
+ [
59893
+ "EV_SHOW_MORE",
59894
+ ac_show_more,
59895
+ null
59896
+ ],
59897
+ [
59898
+ "EV_EXPAND_ALL",
59899
+ ac_expand_all$1,
59900
+ null
59901
+ ],
59902
+ [
59903
+ "EV_COLLAPSE_ALL",
59904
+ ac_collapse_all$1,
59905
+ null
59906
+ ],
58808
59907
  [
58809
59908
  "EV_CENTER",
58810
59909
  ac_center,
@@ -58902,6 +60001,10 @@ function create_gclass$4(gclass_name) {
58902
60001
  ["EV_FIND_NODES", 0],
58903
60002
  ["EV_FIND_RESULT", event_flag_t.EVF_OUTPUT_EVENT],
58904
60003
  ["EV_LAYOUT_AUTOSET", event_flag_t.EVF_OUTPUT_EVENT],
60004
+ ["EV_TOGGLE_FOLD", 0],
60005
+ ["EV_SHOW_MORE", 0],
60006
+ ["EV_EXPAND_ALL", 0],
60007
+ ["EV_COLLAPSE_ALL", 0],
58905
60008
  ["EV_CENTER", 0],
58906
60009
  ["EV_FULLSCREEN", 0],
58907
60010
  ["EV_SET_LAYOUT", 0],
@@ -58959,6 +60062,8 @@ var attrs_table$3 = [
58959
60062
  SDATA(data_type_t.DTP_POINTER, "subscriber", 0, null, "Subscriber of output events"),
58960
60063
  SDATA(data_type_t.DTP_LIST, "operation_modes", 0, "[\"reading\", \"operation\", \"writing\", \"edition\"]", "Available **permission** or behaviour modes. These operation modes are required to be accomplish by the graph handler (G6 child). TODO permissions must match treedb permissions."),
58961
60064
  SDATA(data_type_t.DTP_BOOLEAN, "with_treedb_tables", 0, false, "Include treedb tables"),
60065
+ SDATA(data_type_t.DTP_INTEGER, "expand_depth", 0, 2, "Levels of the treedb open when it loads: 1 = the roots alone, 2 = the roots and their children. Forwarded to the G6 child"),
60066
+ SDATA(data_type_t.DTP_INTEGER, "fold_page_size", 0, 24, "Children of one hook shown per page in the graph; a `+N` chip opens the next page. Forwarded to the G6 child"),
58962
60067
  SDATA(data_type_t.DTP_STRING, "operation_mode", sdata_flag_t.SDF_PERSIST, "reading", "Current operation mode (internal behaviour or role). Changed by the user trough the gui."),
58963
60068
  SDATA(data_type_t.DTP_STRING, "layout", sdata_flag_t.SDF_PERSIST, "", "Current graph layout. User preference. Changed by the user through the gui."),
58964
60069
  SDATA(data_type_t.DTP_POINTER, "gobj_remote_yuno", 0, null, "Remote Yuno to request data"),
@@ -59032,7 +60137,9 @@ function mt_create$3(gobj) {
59032
60137
  layout: priv.layout,
59033
60138
  with_treedb_tables: priv.with_treedb_tables,
59034
60139
  hook_port_position: "bottom",
59035
- fkey_port_position: "top"
60140
+ fkey_port_position: "top",
60141
+ expand_depth: gobj_read_integer_attr(gobj, "expand_depth"),
60142
+ fold_page_size: gobj_read_integer_attr(gobj, "fold_page_size")
59036
60143
  }, gobj);
59037
60144
  populate_nodes_tree_options(gobj);
59038
60145
  if (priv.with_treedb_tables) {}
@@ -59290,6 +60397,7 @@ function make_toolbar(gobj) {
59290
60397
  gobj_send_event(gobj, "EV_REFRESH_TREEDB", { evt }, gobj);
59291
60398
  } }
59292
60399
  ],
60400
+ ...yui_graph_fold_items(gobj, gobj_read_str_attr(gobj, "wide")),
59293
60401
  [
59294
60402
  "button",
59295
60403
  {
@@ -60431,6 +61539,20 @@ function ac_find_nodes(gobj, event, kw, src) {
60431
61539
  return 0;
60432
61540
  }
60433
61541
  /************************************************************
61542
+ * The fold pair of the toolbar, forwarded to the G6 child,
61543
+ * which owns the tree.
61544
+ ************************************************************/
61545
+ function ac_expand_all(gobj, event, kw, src) {
61546
+ let priv = gobj.priv;
61547
+ if (priv.gobj_nodes_tree) gobj_send_event(priv.gobj_nodes_tree, "EV_EXPAND_ALL", {}, gobj);
61548
+ return 0;
61549
+ }
61550
+ function ac_collapse_all(gobj, event, kw, src) {
61551
+ let priv = gobj.priv;
61552
+ if (priv.gobj_nodes_tree) gobj_send_event(priv.gobj_nodes_tree, "EV_COLLAPSE_ALL", {}, gobj);
61553
+ return 0;
61554
+ }
61555
+ /************************************************************
60434
61556
  * How many nodes the term matched, from the graph child. Zero with a
60435
61557
  * term typed is an ANSWER and is shown; an empty box shows nothing.
60436
61558
  ************************************************************/
@@ -60623,6 +61745,16 @@ function create_gclass$3(gclass_name) {
60623
61745
  ac_find_nodes,
60624
61746
  null
60625
61747
  ],
61748
+ [
61749
+ "EV_EXPAND_ALL",
61750
+ ac_expand_all,
61751
+ null
61752
+ ],
61753
+ [
61754
+ "EV_COLLAPSE_ALL",
61755
+ ac_collapse_all,
61756
+ null
61757
+ ],
60626
61758
  [
60627
61759
  "EV_LAYOUT_AUTOSET",
60628
61760
  ac_layout_autoset,
@@ -60685,6 +61817,8 @@ function create_gclass$3(gclass_name) {
60685
61817
  ["EV_SET_OPERATION_MODE", 0],
60686
61818
  ["EV_SET_FOCUS_TOPIC", 0],
60687
61819
  ["EV_FIND_NODES", 0],
61820
+ ["EV_EXPAND_ALL", 0],
61821
+ ["EV_COLLAPSE_ALL", 0],
60688
61822
  ["EV_FIND_RESULT", 0],
60689
61823
  ["EV_LAYOUT_AUTOSET", 0],
60690
61824
  ["EV_TOGGLE_LEGEND", 0],