@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.
@@ -11254,7 +11254,7 @@ function ensure_drag_canvas_patch() {
11254
11254
  * Constants
11255
11255
  ***************************************************************/
11256
11256
  var GCLASS_NAME$13 = "C_YUI_JSON_GRAPH";
11257
- var FOLD_SWALLOWED_EVENTS = [
11257
+ var FOLD_SWALLOWED_EVENTS$1 = [
11258
11258
  "pointerdown",
11259
11259
  "pointerup",
11260
11260
  "mousedown",
@@ -11562,7 +11562,7 @@ function build_ui$12(gobj) {
11562
11562
  keep: $card ? $card.getAttribute("data-json-path") : ""
11563
11563
  }, gobj);
11564
11564
  };
11565
- for (let type of FOLD_SWALLOWED_EVENTS) $mount.addEventListener(type, priv.fold_listener, true);
11565
+ for (let type of FOLD_SWALLOWED_EVENTS$1) $mount.addEventListener(type, priv.fold_listener, true);
11566
11566
  } else (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: no canvas mount, fold handles are dead`);
11567
11567
  priv.$layout_select = $container.querySelector(".JSON_GRAPH_LAYOUT");
11568
11568
  priv.$find_input = $container.querySelector(".JSON_GRAPH_FIND_INPUT");
@@ -11578,7 +11578,7 @@ function destroy_ui$8(gobj) {
11578
11578
  let $container = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "$container");
11579
11579
  if (priv.fold_listener) {
11580
11580
  let $mount = $container ? $container.querySelector("#" + priv.canvas_id) : null;
11581
- if ($mount) for (let type of FOLD_SWALLOWED_EVENTS) $mount.removeEventListener(type, priv.fold_listener, true);
11581
+ if ($mount) for (let type of FOLD_SWALLOWED_EVENTS$1) $mount.removeEventListener(type, priv.fold_listener, true);
11582
11582
  priv.fold_listener = null;
11583
11583
  }
11584
11584
  if ($container) {
@@ -12429,7 +12429,7 @@ function ac_change_layout$2(gobj, event, kw, src) {
12429
12429
  * would throw away where the reader was looking. The toolbar
12430
12430
  * pair, which changes everything at once, does refit.
12431
12431
  ************************************************************/
12432
- function ac_toggle_fold(gobj, event, kw, src) {
12432
+ function ac_toggle_fold$1(gobj, event, kw, src) {
12433
12433
  let priv = gobj.priv;
12434
12434
  let path = kw && kw.path || "";
12435
12435
  if ((0, _yuneta_gobj_js.empty_string)(path)) {
@@ -12468,7 +12468,7 @@ function ac_find_nodes$3(gobj, event, kw, src) {
12468
12468
  /************************************************************
12469
12469
  * EV_EXPAND_ALL
12470
12470
  ************************************************************/
12471
- function ac_expand_all$2(gobj, event, kw, src) {
12471
+ function ac_expand_all$4(gobj, event, kw, src) {
12472
12472
  let priv = gobj.priv;
12473
12473
  if (priv.collapsed.size === 0) return 0;
12474
12474
  priv.collapsed.clear();
@@ -12480,7 +12480,7 @@ function ac_expand_all$2(gobj, event, kw, src) {
12480
12480
  /************************************************************
12481
12481
  * EV_COLLAPSE_ALL — fold every card but the root.
12482
12482
  ************************************************************/
12483
- function ac_collapse_all$2(gobj, event, kw, src) {
12483
+ function ac_collapse_all$4(gobj, event, kw, src) {
12484
12484
  let priv = gobj.priv;
12485
12485
  let json_data = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "json_data");
12486
12486
  if (!json_data) return 0;
@@ -12770,7 +12770,7 @@ function create_gclass$13(gclass_name) {
12770
12770
  ],
12771
12771
  [
12772
12772
  "EV_TOGGLE_FOLD",
12773
- ac_toggle_fold,
12773
+ ac_toggle_fold$1,
12774
12774
  null
12775
12775
  ],
12776
12776
  [
@@ -12780,12 +12780,12 @@ function create_gclass$13(gclass_name) {
12780
12780
  ],
12781
12781
  [
12782
12782
  "EV_EXPAND_ALL",
12783
- ac_expand_all$2,
12783
+ ac_expand_all$4,
12784
12784
  null
12785
12785
  ],
12786
12786
  [
12787
12787
  "EV_COLLAPSE_ALL",
12788
- ac_collapse_all$2,
12788
+ ac_collapse_all$4,
12789
12789
  null
12790
12790
  ],
12791
12791
  [
@@ -13850,7 +13850,7 @@ function ac_search$2(gobj, event, kw, src) {
13850
13850
  /************************************************************
13851
13851
  * EV_EXPAND_ALL — expand every already-loaded container
13852
13852
  ************************************************************/
13853
- function ac_expand_all$1(gobj, event, kw, src) {
13853
+ function ac_expand_all$3(gobj, event, kw, src) {
13854
13854
  let priv = gobj.priv;
13855
13855
  if (priv.root === null || priv.root === void 0) return 0;
13856
13856
  let paths = [];
@@ -13868,7 +13868,7 @@ function ac_expand_all$1(gobj, event, kw, src) {
13868
13868
  /************************************************************
13869
13869
  * EV_COLLAPSE_ALL
13870
13870
  ************************************************************/
13871
- function ac_collapse_all$1(gobj, event, kw, src) {
13871
+ function ac_collapse_all$3(gobj, event, kw, src) {
13872
13872
  gobj.priv.expanded.clear();
13873
13873
  render_view$1(gobj);
13874
13874
  return 0;
@@ -14053,12 +14053,12 @@ function create_gclass$12(gclass_name) {
14053
14053
  ],
14054
14054
  [
14055
14055
  "EV_EXPAND_ALL",
14056
- ac_expand_all$1,
14056
+ ac_expand_all$3,
14057
14057
  null
14058
14058
  ],
14059
14059
  [
14060
14060
  "EV_COLLAPSE_ALL",
14061
- ac_collapse_all$1,
14061
+ ac_collapse_all$3,
14062
14062
  null
14063
14063
  ],
14064
14064
  [
@@ -19061,7 +19061,7 @@ function ac_toggle_collapse(gobj, event, kw, src) {
19061
19061
  /************************************************************
19062
19062
  * Force every node visible (overrides threshold).
19063
19063
  ************************************************************/
19064
- function ac_expand_all(gobj, event, kw, src) {
19064
+ function ac_expand_all$2(gobj, event, kw, src) {
19065
19065
  let priv = gobj.priv;
19066
19066
  if (!priv.collapse_state) priv.collapse_state = {};
19067
19067
  if (priv.node_by_id) for (let id of Object.keys(priv.node_by_id)) {
@@ -19074,7 +19074,7 @@ function ac_expand_all(gobj, event, kw, src) {
19074
19074
  /************************************************************
19075
19075
  * Collapse every node that has children.
19076
19076
  ************************************************************/
19077
- function ac_collapse_all(gobj, event, kw, src) {
19077
+ function ac_collapse_all$2(gobj, event, kw, src) {
19078
19078
  let priv = gobj.priv;
19079
19079
  if (!priv.collapse_state) priv.collapse_state = {};
19080
19080
  if (priv.node_by_id) for (let id of Object.keys(priv.node_by_id)) {
@@ -19426,12 +19426,12 @@ function create_gclass$9(gclass_name) {
19426
19426
  ],
19427
19427
  [
19428
19428
  "EV_EXPAND_ALL",
19429
- ac_expand_all,
19429
+ ac_expand_all$2,
19430
19430
  null
19431
19431
  ],
19432
19432
  [
19433
19433
  "EV_COLLAPSE_ALL",
19434
- ac_collapse_all,
19434
+ ac_collapse_all$2,
19435
19435
  null
19436
19436
  ],
19437
19437
  [
@@ -26800,6 +26800,429 @@ function node_label(desc, record) {
26800
26800
  return id;
26801
26801
  }
26802
26802
  //#endregion
26803
+ //#region src/treedb_fold_model.js
26804
+ /***********************************************************************
26805
+ * treedb_fold_model.js
26806
+ *
26807
+ * A treedb read as a TREE, and which part of it is on screen.
26808
+ *
26809
+ * A treedb is a graph: a record hangs from every parent one of its
26810
+ * fkeys names, and a parent reaches its children through a hook.
26811
+ * Drawn whole it is a pile -- a nave with a hundred and forty
26812
+ * devices is a row of a hundred and forty cards -- so the graph
26813
+ * opens FOLDED, the way a JSON viewer opens: the roots, a level or
26814
+ * two under them, and a count on every cut saying what went with it.
26815
+ *
26816
+ * This module is the arithmetic of that, and nothing else: no G6,
26817
+ * no DOM, no gobj. The graph engine (c_g6_nodes_tree.js) asks it
26818
+ * what is visible and draws exactly that.
26819
+ *
26820
+ * Vocabulary
26821
+ *
26822
+ * node key "topic^id" -- one record
26823
+ * group the children of ONE node through ONE hook,
26824
+ * keyed "topic^id|hook"; the roots of a topic are
26825
+ * a group of their own, keyed "|topic"
26826
+ * shown how many of a group's children are on screen:
26827
+ * absent means folded, a number means the first
26828
+ * N, in record order, a page at a time
26829
+ *
26830
+ * Two rules that are not obvious:
26831
+ *
26832
+ * - A node is visible when SOME visible parent shows it, or it is
26833
+ * a shown root. A record with two parents (a device hangs from
26834
+ * its place AND its controller) is drawn ONCE, under whichever
26835
+ * opened first, and folding one of the two leaves it where the
26836
+ * other one shows it. The union, never the intersection.
26837
+ *
26838
+ * - A parent that the engine does not draw an edge to is not a
26839
+ * parent here either: the `extended` tier (hooks and no fkeys)
26840
+ * gets no edges in draw_link(), so its children are roots. The
26841
+ * tree on screen and the tree in this model have to agree, or a
26842
+ * pill counts children that no line reaches.
26843
+ *
26844
+ * Copyright (c) 2026, ArtGins.
26845
+ * All Rights Reserved.
26846
+ ***********************************************************************/
26847
+ var PAGE_SIZE_DEFAULT = 24;
26848
+ /************************************************************
26849
+ * Keys
26850
+ ************************************************************/
26851
+ function fold_node_key(topic_name, id) {
26852
+ return `${topic_name}^${id}`;
26853
+ }
26854
+ function fold_group_key(node_key, hook_name) {
26855
+ return `${node_key}|${hook_name}`;
26856
+ }
26857
+ function fold_root_group_key(topic_name) {
26858
+ return `|${topic_name}`;
26859
+ }
26860
+ function fold_split_group_key(group_key) {
26861
+ let i = group_key.lastIndexOf("|");
26862
+ if (i < 0) return null;
26863
+ return {
26864
+ node_key: group_key.slice(0, i),
26865
+ hook: group_key.slice(i + 1)
26866
+ };
26867
+ }
26868
+ /************************************************************
26869
+ * Every parent a record names, decoded, in column order.
26870
+ *
26871
+ * Only the parents that will be DRAWN: a topic missing from
26872
+ * `descs` (a system topic such as `__assets__`, never fetched)
26873
+ * and an `extended` parent (no edge is drawn to one, see the
26874
+ * header) are left out, so a child of nothing else is a root.
26875
+ ************************************************************/
26876
+ function parents_of(descs, desc, record) {
26877
+ let out = [];
26878
+ let cols = desc.cols || [];
26879
+ for (let i = 0; i < cols.length; i++) {
26880
+ let col = cols[i];
26881
+ if (!col.fkey) continue;
26882
+ let fkeys = record[col.id];
26883
+ if (!fkeys) continue;
26884
+ let list = (0, _yuneta_gobj_js.is_array)(fkeys) ? fkeys : [fkeys];
26885
+ for (let j = 0; j < list.length; j++) {
26886
+ let ref = (0, _yuneta_gobj_js.treedb_decoder_fkey)(col, list[j]);
26887
+ if (!ref || !ref.topic_name || !ref.hook_name) continue;
26888
+ let parent_desc = descs[ref.topic_name];
26889
+ if (!parent_desc) continue;
26890
+ if (parent_desc.node_treedb_type === "extended") continue;
26891
+ out.push({
26892
+ node_key: fold_node_key(ref.topic_name, String(ref.id)),
26893
+ hook: ref.hook_name,
26894
+ fkey_name: col.id
26895
+ });
26896
+ }
26897
+ }
26898
+ return out;
26899
+ }
26900
+ /************************************************************
26901
+ * Build the model from the topic descriptions and their records.
26902
+ *
26903
+ * descs {topic_name: desc}, system topics included (skipped)
26904
+ * records {topic_name: [record, ...]}, in the order the
26905
+ * backend returned them -- that order is the order of
26906
+ * the children on screen, and of the pages
26907
+ *
26908
+ * Returns
26909
+ * {
26910
+ * nodes: Map<node_key, {key, topic_name, id, record,
26911
+ * parents: [{node_key, hook, fkey_name}],
26912
+ * hooks: {hook: [child_key, ...]}}>
26913
+ * groups: Map<group_key, [child_key, ...]> (roots included)
26914
+ * topics: [topic_name, ...] topics with records, in desc order
26915
+ * }
26916
+ ************************************************************/
26917
+ function fold_build_model(descs, records) {
26918
+ let nodes = /* @__PURE__ */ new Map();
26919
+ let groups = /* @__PURE__ */ new Map();
26920
+ let topics = [];
26921
+ descs = (0, _yuneta_gobj_js.is_object)(descs) ? descs : {};
26922
+ records = (0, _yuneta_gobj_js.is_object)(records) ? records : {};
26923
+ for (let topic_name of Object.keys(descs)) {
26924
+ if (topic_name.substring(0, 2) === "__") continue;
26925
+ descs[topic_name];
26926
+ let list = records[topic_name];
26927
+ if (!(0, _yuneta_gobj_js.is_array)(list)) continue;
26928
+ topics.push(topic_name);
26929
+ for (let i = 0; i < list.length; i++) {
26930
+ let record = list[i];
26931
+ if (!(0, _yuneta_gobj_js.is_object)(record) || record.id === void 0 || record.id === null) continue;
26932
+ let key = fold_node_key(topic_name, String(record.id));
26933
+ nodes.set(key, {
26934
+ key,
26935
+ topic_name,
26936
+ id: String(record.id),
26937
+ record,
26938
+ parents: [],
26939
+ hooks: {}
26940
+ });
26941
+ }
26942
+ }
26943
+ for (let node of nodes.values()) {
26944
+ let desc = descs[node.topic_name];
26945
+ let parents = parents_of(descs, desc, node.record);
26946
+ for (let p of parents) {
26947
+ let parent = nodes.get(p.node_key);
26948
+ if (!parent) continue;
26949
+ node.parents.push(p);
26950
+ if (!parent.hooks[p.hook]) parent.hooks[p.hook] = [];
26951
+ parent.hooks[p.hook].push(node.key);
26952
+ }
26953
+ }
26954
+ 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]);
26955
+ let roots = {};
26956
+ for (let topic_name of topics) roots[topic_name] = [];
26957
+ for (let node of nodes.values()) if (node.parents.length === 0) roots[node.topic_name].push(node.key);
26958
+ let reached = reach_from(nodes, [].concat(...Object.values(roots)));
26959
+ for (let node of nodes.values()) if (!reached.has(node.key)) {
26960
+ roots[node.topic_name].push(node.key);
26961
+ for (let k of reach_from(nodes, [node.key])) reached.add(k);
26962
+ }
26963
+ for (let topic_name of topics) if (roots[topic_name].length > 0) groups.set(fold_root_group_key(topic_name), roots[topic_name]);
26964
+ return {
26965
+ nodes,
26966
+ groups,
26967
+ topics
26968
+ };
26969
+ }
26970
+ /************************************************************
26971
+ * Everything reachable downwards from `start`, cycles included.
26972
+ ************************************************************/
26973
+ function reach_from(nodes, start) {
26974
+ let seen = /* @__PURE__ */ new Set();
26975
+ let queue = start.slice();
26976
+ while (queue.length) {
26977
+ let key = queue.shift();
26978
+ if (seen.has(key)) continue;
26979
+ seen.add(key);
26980
+ let node = nodes.get(key);
26981
+ if (!node) continue;
26982
+ for (let hook of Object.keys(node.hooks)) for (let child of node.hooks[hook]) if (!seen.has(child)) queue.push(child);
26983
+ }
26984
+ return seen;
26985
+ }
26986
+ /************************************************************
26987
+ * Fold state: which groups are open, and how far.
26988
+ ************************************************************/
26989
+ function fold_new_state(page_size) {
26990
+ return {
26991
+ page_size: page_size > 0 ? page_size : PAGE_SIZE_DEFAULT,
26992
+ shown: /* @__PURE__ */ new Map()
26993
+ };
26994
+ }
26995
+ function fold_group_total(model, group_key) {
26996
+ let list = model.groups.get(group_key);
26997
+ return list ? list.length : 0;
26998
+ }
26999
+ function fold_group_shown(model, state, group_key) {
27000
+ let total = fold_group_total(model, group_key);
27001
+ let n = state.shown.get(group_key) || 0;
27002
+ return Math.min(n, total);
27003
+ }
27004
+ function fold_is_open(state, group_key) {
27005
+ return (state.shown.get(group_key) || 0) > 0;
27006
+ }
27007
+ function page_ceiling(state, n) {
27008
+ let p = state.page_size;
27009
+ return Math.ceil(n / p) * p;
27010
+ }
27011
+ /************************************************************
27012
+ * Open a group on its first page, or close it.
27013
+ * Returns true when it ends OPEN.
27014
+ ************************************************************/
27015
+ function fold_toggle(model, state, group_key) {
27016
+ if (fold_is_open(state, group_key)) {
27017
+ state.shown.delete(group_key);
27018
+ return false;
27019
+ }
27020
+ if (fold_group_total(model, group_key) === 0) return false;
27021
+ state.shown.set(group_key, state.page_size);
27022
+ return true;
27023
+ }
27024
+ /************************************************************
27025
+ * One more page of a group. Returns how many are shown now.
27026
+ ************************************************************/
27027
+ function fold_show_more(model, state, group_key) {
27028
+ let total = fold_group_total(model, group_key);
27029
+ let n = fold_group_shown(model, state, group_key) + state.page_size;
27030
+ n = Math.min(n, total);
27031
+ if (n > 0) state.shown.set(group_key, n);
27032
+ return n;
27033
+ }
27034
+ /************************************************************
27035
+ * Every group open on ALL its children. This is the reader
27036
+ * asking for the whole thing; the pages are for the other way in.
27037
+ ************************************************************/
27038
+ function fold_expand_all(model, state) {
27039
+ state.shown.clear();
27040
+ for (let [group_key, list] of model.groups) if (list.length > 0) state.shown.set(group_key, list.length);
27041
+ }
27042
+ /************************************************************
27043
+ * The roots and nothing else -- the first page of each topic's
27044
+ * roots, the way the graph looks before anything is opened.
27045
+ ************************************************************/
27046
+ function fold_collapse_all(model, state) {
27047
+ state.shown.clear();
27048
+ for (let topic_name of model.topics) {
27049
+ let key = fold_root_group_key(topic_name);
27050
+ if (fold_group_total(model, key) > 0) state.shown.set(key, state.page_size);
27051
+ }
27052
+ }
27053
+ /************************************************************
27054
+ * Open the tree down to `depth` levels: the roots are level 1,
27055
+ * their children level 2. A node reached by two paths takes the
27056
+ * shallowest. Each group opens on its first page. `depth` of 1
27057
+ * is the roots alone; anything below that is treated as 1.
27058
+ ************************************************************/
27059
+ function fold_expand_to_depth(model, state, depth) {
27060
+ fold_collapse_all(model, state);
27061
+ if (!(depth > 1)) return;
27062
+ let level = /* @__PURE__ */ new Map();
27063
+ let queue = [];
27064
+ for (let topic_name of model.topics) {
27065
+ let roots = model.groups.get(fold_root_group_key(topic_name)) || [];
27066
+ for (let key of roots) if (!level.has(key)) {
27067
+ level.set(key, 1);
27068
+ queue.push(key);
27069
+ }
27070
+ }
27071
+ while (queue.length) {
27072
+ let key = queue.shift();
27073
+ let lv = level.get(key);
27074
+ if (lv >= depth) continue;
27075
+ let node = model.nodes.get(key);
27076
+ if (!node) continue;
27077
+ for (let hook of Object.keys(node.hooks)) {
27078
+ let children = node.hooks[hook];
27079
+ if (children.length === 0) continue;
27080
+ state.shown.set(fold_group_key(key, hook), state.page_size);
27081
+ for (let child of children) if (!level.has(child)) {
27082
+ level.set(child, lv + 1);
27083
+ queue.push(child);
27084
+ }
27085
+ }
27086
+ }
27087
+ }
27088
+ /************************************************************
27089
+ * The set of node keys on screen, from the roots down through
27090
+ * every open group -- and only through open groups whose parent
27091
+ * is itself on screen: a group remembered open under a folded
27092
+ * ancestor stays remembered, not shown.
27093
+ ************************************************************/
27094
+ function fold_visible_set(model, state) {
27095
+ let visible = /* @__PURE__ */ new Set();
27096
+ let queue = [];
27097
+ let push_group = (group_key, children) => {
27098
+ let n = fold_group_shown(model, state, group_key);
27099
+ for (let i = 0; i < n; i++) {
27100
+ let key = children[i];
27101
+ if (!visible.has(key)) {
27102
+ visible.add(key);
27103
+ queue.push(key);
27104
+ }
27105
+ }
27106
+ };
27107
+ for (let topic_name of model.topics) {
27108
+ let key = fold_root_group_key(topic_name);
27109
+ let roots = model.groups.get(key);
27110
+ if (roots) push_group(key, roots);
27111
+ }
27112
+ while (queue.length) {
27113
+ let key = queue.shift();
27114
+ let node = model.nodes.get(key);
27115
+ if (!node) continue;
27116
+ for (let hook of Object.keys(node.hooks)) push_group(fold_group_key(key, hook), node.hooks[hook]);
27117
+ }
27118
+ return visible;
27119
+ }
27120
+ /************************************************************
27121
+ * The open groups that still have children off screen, with the
27122
+ * parent that owns each -- what the "+N" chips are drawn for.
27123
+ * Only groups whose parent is visible (or root groups).
27124
+ *
27125
+ * Returns [{group_key, node_key, hook, shown, total, rest}]
27126
+ ************************************************************/
27127
+ function fold_pending_groups(model, state, visible) {
27128
+ let out = [];
27129
+ for (let [group_key, list] of model.groups) {
27130
+ let shown = fold_group_shown(model, state, group_key);
27131
+ if (shown === 0 || shown >= list.length) continue;
27132
+ let g = fold_split_group_key(group_key);
27133
+ if (g.node_key && !visible.has(g.node_key)) continue;
27134
+ out.push({
27135
+ group_key,
27136
+ node_key: g.node_key,
27137
+ hook: g.hook,
27138
+ shown,
27139
+ total: list.length,
27140
+ rest: list.length - shown
27141
+ });
27142
+ }
27143
+ return out;
27144
+ }
27145
+ /************************************************************
27146
+ * The pills of one card: one per hook that has children.
27147
+ *
27148
+ * Returns [{hook, group_key, total, shown, open, child_topic}]
27149
+ * `child_topic` is the topic of the first child, which is what
27150
+ * the pill is coloured with (a hook can name several topics in
27151
+ * the schema, but the children it has are what it links).
27152
+ ************************************************************/
27153
+ function fold_pills_of(model, state, node_key) {
27154
+ let node = model.nodes.get(node_key);
27155
+ if (!node) return [];
27156
+ let out = [];
27157
+ for (let hook of Object.keys(node.hooks)) {
27158
+ let children = node.hooks[hook];
27159
+ if (children.length === 0) continue;
27160
+ let group_key = fold_group_key(node_key, hook);
27161
+ let first = model.nodes.get(children[0]);
27162
+ out.push({
27163
+ hook,
27164
+ group_key,
27165
+ total: children.length,
27166
+ shown: fold_group_shown(model, state, group_key),
27167
+ open: fold_is_open(state, group_key),
27168
+ child_topic: first ? first.topic_name : ""
27169
+ });
27170
+ }
27171
+ return out;
27172
+ }
27173
+ /************************************************************
27174
+ * Make ONE node visible, opening the groups on a path from a
27175
+ * root to it, and as many pages of each as it takes for the
27176
+ * node to be inside the shown prefix. Prefers the shortest path
27177
+ * (breadth-first upwards). Returns false if no root reaches it,
27178
+ * which cannot happen with a model built here (see roots) but
27179
+ * is the honest answer for a key the model does not know.
27180
+ ************************************************************/
27181
+ function fold_reveal(model, state, node_key) {
27182
+ if (!model.nodes.get(node_key)) return false;
27183
+ let root_group_of = (key) => {
27184
+ let rg = fold_root_group_key(model.nodes.get(key).topic_name);
27185
+ return (model.groups.get(rg) || []).indexOf(key) >= 0 ? rg : null;
27186
+ };
27187
+ let via = /* @__PURE__ */ new Map();
27188
+ let queue = [node_key];
27189
+ via.set(node_key, null);
27190
+ let root_key = null;
27191
+ while (queue.length) {
27192
+ let key = queue.shift();
27193
+ if (root_group_of(key)) {
27194
+ root_key = key;
27195
+ break;
27196
+ }
27197
+ let n = model.nodes.get(key);
27198
+ for (let p of n.parents) {
27199
+ if (!model.nodes.has(p.node_key) || via.has(p.node_key)) continue;
27200
+ via.set(p.node_key, {
27201
+ child_key: key,
27202
+ hook: p.hook
27203
+ });
27204
+ queue.push(p.node_key);
27205
+ }
27206
+ }
27207
+ if (root_key === null) return false;
27208
+ let open_up_to = (group_key, child_key) => {
27209
+ let list = model.groups.get(group_key) || [];
27210
+ let idx = list.indexOf(child_key);
27211
+ if (idx < 0) return;
27212
+ let need = page_ceiling(state, idx + 1);
27213
+ if (need > (state.shown.get(group_key) || 0)) state.shown.set(group_key, Math.min(need, list.length));
27214
+ };
27215
+ open_up_to(root_group_of(root_key), root_key);
27216
+ let key = root_key;
27217
+ while (true) {
27218
+ let step = via.get(key);
27219
+ if (!step) break;
27220
+ open_up_to(fold_group_key(key, step.hook), step.child_key);
27221
+ key = step.child_key;
27222
+ }
27223
+ return true;
27224
+ }
27225
+ //#endregion
26803
27226
  //#region node_modules/@babel/runtime/helpers/esm/typeof.js
26804
27227
  function _typeof(o) {
26805
27228
  "@babel/helpers - typeof";
@@ -53544,6 +53967,23 @@ function ensure_brush_select_owned() {
53544
53967
  * record: null, // Data of node
53545
53968
  * }
53546
53969
  *
53970
+ * The graph opens FOLDED, like a JSON viewer.
53971
+ *
53972
+ * Every record of every topic is fetched, but only the ones on
53973
+ * screen become G6 nodes: the roots, `expand_depth` levels under
53974
+ * them, one page (`fold_page_size`) of children per hook. Each
53975
+ * card carries a pill per hook that has children -- `▸ devices
53976
+ * 142` -- which opens or folds that hook; an open hook with more
53977
+ * children than its page ends in a `+N` chip that opens the next
53978
+ * page. The arithmetic lives in treedb_fold_model.js; this file
53979
+ * reconciles the G6 data with what that model says is visible
53980
+ * (see reconcile_fold) and lays it out again.
53981
+ *
53982
+ * `dagre` reads LEFT TO RIGHT: the children of a node are a
53983
+ * column beside it, the ports move to the sides with it, and
53984
+ * the edges are horizontal beziers. `antv-dagre` keeps the
53985
+ * top-down reading.
53986
+ *
53547
53987
  * Copyright (c) 2020-2021 Niyamaka.
53548
53988
  * Copyright (c) 2025-2026, ArtGins.
53549
53989
  * All Rights Reserved.
@@ -53603,8 +54043,18 @@ var SELECT_RING = "rgba(59,130,246,0.95)";
53603
54043
  ***************************************************************/
53604
54044
  var _layouts = {
53605
54045
  "manual": { type: "manual" },
53606
- "dagre": { type: "dagre" },
53607
- "antv-dagre": { type: "antv-dagre" },
54046
+ "dagre": {
54047
+ type: "dagre",
54048
+ rankdir: "LR",
54049
+ nodesep: 18,
54050
+ ranksep: 110
54051
+ },
54052
+ "antv-dagre": {
54053
+ type: "antv-dagre",
54054
+ rankdir: "TB",
54055
+ nodesep: 30,
54056
+ ranksep: 80
54057
+ },
53608
54058
  "d3-force": {
53609
54059
  type: "d3-force",
53610
54060
  link: {
@@ -53652,6 +54102,8 @@ var attrs_table$4 = [
53652
54102
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_LIST, "layout_names", _yuneta_gobj_js.sdata_flag_t.SDF_RD, JSON.stringify(Object.keys(_layouts)), "Available layout names (read-only, for parent to query)"),
53653
54103
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_STRING, "hook_port_position", 0, "bottom", "Hook port position"),
53654
54104
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_STRING, "fkey_port_position", 0, "top", "Fkey port position"),
54105
+ (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.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"),
54106
+ (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.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"),
53655
54107
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_BOOLEAN, "confirm_delete_node", 0, true, "Ask confirmation before deleting a node"),
53656
54108
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_BOOLEAN, "confirm_unlink_edge", 0, true, "Ask confirmation before unlinking an edge"),
53657
54109
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_STRING, "wide", 0, "40px", "Height of header"),
@@ -53723,7 +54175,14 @@ var PRIVATE_DATA$4 = {
53723
54175
  _pending_find: null,
53724
54176
  _layout_asked: "",
53725
54177
  _nodes_total: 0,
53726
- _nodes_placed: 0
54178
+ _nodes_placed: 0,
54179
+ expand_depth: 2,
54180
+ fold_page_size: 24,
54181
+ _fold_model: null,
54182
+ _fold_state: null,
54183
+ _fold_listener: null,
54184
+ _pill_sig: null,
54185
+ _fold_busy: false
53727
54186
  };
53728
54187
  var __gclass__$4 = null;
53729
54188
  /******************************
@@ -53743,6 +54202,7 @@ function mt_create$4(gobj) {
53743
54202
  build_ui$4(gobj);
53744
54203
  register_layouts(gobj);
53745
54204
  build_graph$1(gobj);
54205
+ install_fold_listener(gobj);
53746
54206
  if (typeof ResizeObserver !== "undefined" && priv.$container) {
53747
54207
  let ro = new ResizeObserver(() => {
53748
54208
  if (priv._resize_raf) cancelAnimationFrame(priv._resize_raf);
@@ -53782,6 +54242,7 @@ function mt_destroy$4(gobj) {
53782
54242
  i18next.default.off("languageChanged", priv._on_language_changed);
53783
54243
  priv._on_language_changed = null;
53784
54244
  }
54245
+ uninstall_fold_listener(gobj);
53785
54246
  if (priv._on_pointerdown_focus) {
53786
54247
  priv.$container.removeEventListener("pointerdown", priv._on_pointerdown_focus, true);
53787
54248
  priv._on_pointerdown_focus = null;
@@ -53969,6 +54430,7 @@ function configure_events(gobj) {
53969
54430
  });
53970
54431
  priv._on_language_changed = () => {
53971
54432
  update_toolbar(gobj);
54433
+ repaint_more_chips(gobj);
53972
54434
  };
53973
54435
  i18next.default.on("languageChanged", priv._on_language_changed);
53974
54436
  priv._on_pointerdown_focus = (ev) => {
@@ -54466,15 +54928,79 @@ function build_ports(gobj, desc) {
54466
54928
  top_ports.push(port);
54467
54929
  }
54468
54930
  }
54469
- for (let i = 0; i < top_ports.length; i++) top_ports[i].placement = [.5, 0];
54470
- for (let i = 0; i < bottom_ports.length; i++) {
54471
- let point = getPointPosition(bottom_ports.length, i);
54472
- if (top_ports.length === 0) bottom_ports[i].placement = [0, .5];
54473
- else bottom_ports[i].placement = [point, 1];
54474
- }
54931
+ place_ports(gobj, top_ports, bottom_ports);
54475
54932
  return [...top_ports, ...bottom_ports];
54476
54933
  }
54477
54934
  /************************************************************
54935
+ * Which way the graph reads. `dagre` is the left-to-right
54936
+ * layout; everything else keeps the top-down reading, `manual`
54937
+ * included, because the saved arrangements were made with the
54938
+ * ports on the top and bottom edges.
54939
+ ************************************************************/
54940
+ function layout_direction(gobj) {
54941
+ return gobj.priv.layout === "dagre" ? "LR" : "TB";
54942
+ }
54943
+ function edge_type_for(gobj) {
54944
+ return layout_direction(gobj) === "LR" ? "cubic-horizontal" : "cubic";
54945
+ }
54946
+ /************************************************************
54947
+ * Where the ports sit, by direction: fkeys (the way in from a
54948
+ * parent) all on one point of the leading edge, hooks (the way
54949
+ * out to the children) spread along the trailing edge. Left and
54950
+ * right when the graph reads left to right; top and bottom
54951
+ * otherwise.
54952
+ ************************************************************/
54953
+ function place_ports(gobj, fkey_ports, hook_ports) {
54954
+ let lr = layout_direction(gobj) === "LR";
54955
+ for (let i = 0; i < fkey_ports.length; i++) fkey_ports[i].placement = lr ? [0, .5] : [.5, 0];
54956
+ for (let i = 0; i < hook_ports.length; i++) {
54957
+ let point = getPointPosition(hook_ports.length, i);
54958
+ if (fkey_ports.length === 0 && !lr) hook_ports[i].placement = [0, .5];
54959
+ else if (lr) hook_ports[i].placement = [1, point];
54960
+ else hook_ports[i].placement = [point, 1];
54961
+ }
54962
+ }
54963
+ /************************************************************
54964
+ * The layout changed direction: move every port to the edge
54965
+ * the new reading needs, and give every edge the curve that
54966
+ * goes with it. Called before the layout runs, so the lines
54967
+ * are drawn once, in the right place.
54968
+ ************************************************************/
54969
+ function apply_layout_direction(gobj) {
54970
+ let graph = gobj.priv.graph;
54971
+ if (!graph) return;
54972
+ let node_updates = [];
54973
+ for (let nd of graph.getNodeData() || []) {
54974
+ if (!nd || !nd.data || !nd.data.desc || !nd.style || !(0, _yuneta_gobj_js.is_array)(nd.style.ports)) continue;
54975
+ let desc = nd.data.desc;
54976
+ let fkey_ports = [];
54977
+ let hook_ports = [];
54978
+ for (let port of nd.style.ports) {
54979
+ let col = get_col_by_id(desc, port.key);
54980
+ let copy = Object.assign({}, port);
54981
+ if (col && col.fkey) fkey_ports.push(copy);
54982
+ else hook_ports.push(copy);
54983
+ }
54984
+ place_ports(gobj, fkey_ports, hook_ports);
54985
+ node_updates.push({
54986
+ id: nd.id,
54987
+ style: { ports: [...fkey_ports, ...hook_ports] }
54988
+ });
54989
+ }
54990
+ let type = edge_type_for(gobj);
54991
+ let edge_updates = [];
54992
+ for (let ed of graph.getEdgeData() || []) if (ed && ed.type !== type) edge_updates.push({
54993
+ id: ed.id,
54994
+ type
54995
+ });
54996
+ try {
54997
+ if (node_updates.length) graph.updateNodeData(node_updates);
54998
+ if (edge_updates.length) graph.updateEdgeData(edge_updates);
54999
+ } catch (e) {
55000
+ (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: cannot turn the ports: ${e}`);
55001
+ }
55002
+ }
55003
+ /************************************************************
54478
55004
  * Create a topic node in the G6 graph
54479
55005
  ************************************************************/
54480
55006
  function create_topic_node(gobj, desc, record) {
@@ -54483,8 +55009,6 @@ function create_topic_node(gobj, desc, record) {
54483
55009
  let node_name = build_node_name(gobj, desc.topic_name, record.id);
54484
55010
  let xy = get_default_ne_xy(gobj);
54485
55011
  let geometry = get_node_graph_props(gobj, desc.topic_name, record.id, record);
54486
- priv._nodes_total++;
54487
- if (geometry_has_position(geometry)) priv._nodes_placed++;
54488
55012
  let x = (0, _yuneta_gobj_js.kw_get_int)(gobj, geometry, "x", xy, _yuneta_gobj_js.kw_flag_t.KW_CREATE);
54489
55013
  let y = (0, _yuneta_gobj_js.kw_get_int)(gobj, geometry, "y", xy, _yuneta_gobj_js.kw_flag_t.KW_CREATE);
54490
55014
  let ports = build_ports(gobj, desc);
@@ -54497,24 +55021,26 @@ function create_topic_node(gobj, desc, record) {
54497
55021
  stroke: getStrokeColor(desc.color),
54498
55022
  lineWidth: 1
54499
55023
  };
55024
+ let pills_html = pills_html_of_key(gobj, fold_node_key(desc.topic_name, String(record.id)));
55025
+ let size = card_size_for(node_treedb_type, !!pills_html);
54500
55026
  if (node_treedb_type === "child") {
54501
55027
  node_graph_type = "html";
54502
- style.size = [116, 40];
54503
- style.dx = -58;
54504
- style.dy = -20;
55028
+ style.size = size;
55029
+ style.dx = -size[0] / 2;
55030
+ style.dy = -size[1] / 2;
54505
55031
  style.innerHTML = build_chip_innerHTML(desc.color, priv.theme, record.icon, node_label(desc, record), record.id);
54506
55032
  } else if (node_treedb_type === "extended") {
54507
55033
  node_graph_type = "html";
54508
- style.size = [144, 66];
54509
- style.dx = -72;
54510
- style.dy = -33;
54511
- style.innerHTML = build_node_innerHTML(desc.color, priv.theme, record.icon, node_label(desc, record), desc.topic_name, true, record.id);
55034
+ style.size = size;
55035
+ style.dx = -size[0] / 2;
55036
+ style.dy = -size[1] / 2;
55037
+ 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);
54512
55038
  } else {
54513
55039
  node_graph_type = "html";
54514
- style.size = [172, 96];
54515
- style.dx = -86;
54516
- style.dy = -48;
54517
- style.innerHTML = build_node_innerHTML(desc.color, priv.theme, record.icon, node_label(desc, record), desc.topic_name, false, record.id);
55040
+ style.size = size;
55041
+ style.dx = -size[0] / 2;
55042
+ style.dy = -size[1] / 2;
55043
+ 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);
54518
55044
  }
54519
55045
  let topic_props = priv._graph_properties[desc.topic_name];
54520
55046
  let topic_defaults = (0, _yuneta_gobj_js.is_object)(topic_props) && (0, _yuneta_gobj_js.is_object)(topic_props.defaults) ? topic_props.defaults : null;
@@ -54571,8 +55097,7 @@ function create_topic_node(gobj, desc, record) {
54571
55097
  * Update topic node data
54572
55098
  ************************************************************/
54573
55099
  function update_topic_node(gobj, desc, node_name, record) {
54574
- let priv = gobj.priv;
54575
- let graph = priv.graph;
55100
+ let graph = gobj.priv.graph;
54576
55101
  try {
54577
55102
  let nodedata = graph.getNodeData(node_name);
54578
55103
  if (!nodedata) {
@@ -54580,18 +55105,7 @@ function update_topic_node(gobj, desc, node_name, record) {
54580
55105
  return;
54581
55106
  }
54582
55107
  nodedata.data.record = record;
54583
- if (desc.node_treedb_type === "child") graph.updateNodeData([{
54584
- id: node_name,
54585
- style: { innerHTML: build_chip_innerHTML(desc.color, priv.theme, record.icon, node_label(desc, record), record.id) }
54586
- }]);
54587
- else if (desc.node_treedb_type === "extended") graph.updateNodeData([{
54588
- id: node_name,
54589
- style: { innerHTML: build_node_innerHTML(desc.color, priv.theme, record.icon, node_label(desc, record), desc.topic_name, true, record.id) }
54590
- }]);
54591
- else if (desc.node_treedb_type === "hierarchical") graph.updateNodeData([{
54592
- id: node_name,
54593
- style: { innerHTML: build_node_innerHTML(desc.color, priv.theme, record.icon, node_label(desc, record), desc.topic_name, false, record.id) }
54594
- }]);
55108
+ repaint_cards(gobj, /* @__PURE__ */ new Set([node_name]));
54595
55109
  } catch (e) {
54596
55110
  (0, _yuneta_gobj_js.log_error)(e.message);
54597
55111
  }
@@ -54630,19 +55144,6 @@ function remove_local_node(gobj, topic_name, node) {
54630
55144
  }
54631
55145
  }
54632
55146
  /************************************************************
54633
- * Create all links from records, initial load from parent
54634
- ************************************************************/
54635
- function create_links(gobj) {
54636
- let priv = gobj.priv;
54637
- for (const [topic_name, records] of Object.entries(priv.records)) {
54638
- let desc = priv.descs[topic_name];
54639
- for (let i = 0; i < records.length; i++) {
54640
- let record = records[i];
54641
- draw_links(gobj, desc, record, true);
54642
- }
54643
- }
54644
- }
54645
- /************************************************************
54646
55147
  * Collect all fkey references from a record as a Set.
54647
55148
  *
54648
55149
  * Each entry is a string: "col_id\tcol_idx\tfkey_value"
@@ -54722,6 +55223,17 @@ function draw_link(gobj, child_topic, child_id, source_col, fkey, verbose) {
54722
55223
  return;
54723
55224
  }
54724
55225
  let child_node = build_node_name(gobj, child_topic, child_id);
55226
+ let child_cell = null;
55227
+ try {
55228
+ child_cell = graph.getNodeData(child_node);
55229
+ } catch (e) {
55230
+ child_cell = null;
55231
+ }
55232
+ if (!child_cell) {
55233
+ if (verbose) (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: child node NOT FOUND: ${child_node}`);
55234
+ return;
55235
+ }
55236
+ if (get_edge_by_link(gobj, parent_topic, parent_id, hook_name, child_topic, child_id, fkey_name)) return;
54725
55237
  let dark = priv.theme === "dark";
54726
55238
  let is_tree = parent_topic === child_topic;
54727
55239
  let saved_lineWidth = is_tree ? 2 : 1.6;
@@ -54760,7 +55272,7 @@ function draw_link(gobj, child_topic, child_id, source_col, fkey, verbose) {
54760
55272
  }
54761
55273
  let edge = {
54762
55274
  id: build_edge_id(gobj),
54763
- type: "cubic",
55275
+ type: edge_type_for(gobj),
54764
55276
  source: parent_node,
54765
55277
  target: child_node,
54766
55278
  style: {
@@ -54891,7 +55403,10 @@ function save_geometry(gobj) {
54891
55403
  let priv = gobj.priv;
54892
55404
  let graph = priv.graph;
54893
55405
  const nodes = graph.getData().nodes;
54894
- for (let i = 0; i < nodes.length; i++) update_geometry(gobj, nodes[i].id);
55406
+ for (let i = 0; i < nodes.length; i++) {
55407
+ if (!nodes[i].data || !nodes[i].data.desc) continue;
55408
+ update_geometry(gobj, nodes[i].id);
55409
+ }
54895
55410
  const edges = graph.getData().edges;
54896
55411
  for (let i = 0; i < edges.length; i++) update_edge_geometry(gobj, edges[i].id);
54897
55412
  let origin = (0, _yuneta_gobj_js.gobj_read_str_attr)((0, _yuneta_gobj_js.gobj_yuno)(), "node_uuid");
@@ -55148,38 +55663,72 @@ async function graph_fit_selection(gobj, ids) {
55148
55663
  ************************************************************/
55149
55664
  function graph_focus_topic(gobj, topic) {
55150
55665
  let priv = gobj.priv;
55151
- let graph = priv.graph;
55152
- if (!graph || !priv.graph_rendered) {
55666
+ if (!priv.graph || !priv.graph_rendered || !priv._fold_model) {
55153
55667
  priv._pending_focus_topic = topic;
55154
55668
  return;
55155
55669
  }
55156
- let data = {};
55157
- try {
55158
- data = graph.getData() || {};
55159
- } catch (e) {
55160
- (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: graph.getData() failed: ${e}`);
55161
- return;
55670
+ let model = priv._fold_model;
55671
+ let state = priv._fold_state;
55672
+ let keys = [];
55673
+ if (!(0, _yuneta_gobj_js.empty_string)(topic)) {
55674
+ for (let node of model.nodes.values()) if (node.topic_name === topic) keys.push(node.key);
55675
+ if (keys.length === 0) (0, _yuneta_gobj_js.log_warning)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: focus topic '${topic}' has no nodes`);
55676
+ let visible = fold_visible_set(model, state);
55677
+ let revealed = 0;
55678
+ for (let key of keys) {
55679
+ if (visible.has(key)) continue;
55680
+ if (revealed >= state.page_size) break;
55681
+ fold_reveal(model, state, key);
55682
+ revealed++;
55683
+ }
55162
55684
  }
55163
- let nodes = (0, _yuneta_gobj_js.is_array)(data.nodes) ? data.nodes : [];
55685
+ priv._focus_topic = topic || null;
55686
+ reconcile_fold(gobj, {}).then(() => {
55687
+ apply_focus_ids(gobj, keys, "focus topic");
55688
+ });
55689
+ }
55690
+ /************************************************************
55691
+ * Paint the highlight on these records (by model key) and put
55692
+ * the first of them in the middle. Shared by the find and the
55693
+ * topic focus, after the reconcile that brought them on screen.
55694
+ * Keys that are still off screen are skipped: a match beyond the
55695
+ * page a reveal opened is counted, not painted.
55696
+ ************************************************************/
55697
+ function apply_focus_ids(gobj, keys, what) {
55698
+ let priv = gobj.priv;
55699
+ let graph = priv.graph;
55700
+ if (!graph) return;
55164
55701
  let states = {};
55165
55702
  for (let id of priv._focus_ids || []) states[id] = [];
55166
55703
  let ids = [];
55167
- if (!(0, _yuneta_gobj_js.empty_string)(topic)) {
55168
- for (let n of nodes) if (n && n.data && n.data.topic_name === topic) ids.push(n.id);
55169
- if (ids.length === 0) (0, _yuneta_gobj_js.log_warning)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: focus topic '${topic}' has no nodes`);
55170
- for (let id of ids) states[id] = ["active"];
55704
+ for (let key of keys) {
55705
+ let id = node_id_of_key(gobj, key);
55706
+ let nd = null;
55707
+ try {
55708
+ nd = graph.getNodeData(id);
55709
+ } catch (e) {
55710
+ nd = null;
55711
+ }
55712
+ if (nd) {
55713
+ ids.push(id);
55714
+ states[id] = ["active"];
55715
+ }
55171
55716
  }
55172
55717
  let prev_ids = priv._focus_ids || [];
55173
55718
  priv._focus_ids = ids;
55174
- priv._focus_topic = topic || null;
55175
55719
  try {
55176
55720
  if (typeof graph.setElementState === "function") graph.setElementState(states);
55177
55721
  apply_node_highlight(gobj, prev_ids, ids);
55178
- if (ids.length && typeof graph.focusElement === "function") graph.focusElement(ids);
55722
+ if (ids.length) yui_graph_center_on(graph, ids[0]);
55179
55723
  } catch (e) {
55180
- (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: focus topic apply failed: ${e}`);
55724
+ (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: ${what} apply failed: ${e}`);
55181
55725
  }
55182
55726
  }
55727
+ function node_id_of_key(gobj, key) {
55728
+ let i = key.indexOf("^");
55729
+ if (i < 0) return "";
55730
+ return build_node_name(gobj, key.slice(0, i), key.slice(i + 1));
55731
+ }
55183
55732
  /************************************************************
55184
55733
  * Find the nodes whose name, id or topic contains `term`, highlight
55185
55734
  * them with the same amber 'active' state the topic focus uses, and
@@ -55196,52 +55745,43 @@ function graph_focus_topic(gobj, topic) {
55196
55745
  ************************************************************/
55197
55746
  function graph_find_nodes(gobj, term) {
55198
55747
  let priv = gobj.priv;
55199
- let graph = priv.graph;
55200
- if (!graph || !priv.graph_rendered) {
55748
+ if (!priv.graph || !priv.graph_rendered || !priv._fold_model) {
55201
55749
  priv._pending_find = term;
55202
55750
  return 0;
55203
55751
  }
55204
- let data = {};
55205
- try {
55206
- data = graph.getData() || {};
55207
- } catch (e) {
55208
- (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: graph.getData() failed: ${e}`);
55209
- return 0;
55210
- }
55211
- let nodes = (0, _yuneta_gobj_js.is_array)(data.nodes) ? data.nodes : [];
55212
- let states = {};
55213
- for (let id of priv._focus_ids || []) states[id] = [];
55214
- let ids = [];
55752
+ let model = priv._fold_model;
55753
+ let state = priv._fold_state;
55754
+ let keys = [];
55215
55755
  if (!(0, _yuneta_gobj_js.empty_string)(term)) {
55216
55756
  let needle = String(term).toLowerCase();
55217
- for (let n of nodes) {
55218
- if (!n || !n.data) continue;
55219
- let record = n.data.record || {};
55757
+ for (let node of model.nodes.values()) {
55758
+ let record = node.record || {};
55220
55759
  let label = "";
55221
55760
  try {
55222
- label = node_label(n.data.desc || {}, record) || "";
55761
+ label = node_label(priv.descs[node.topic_name] || {}, record) || "";
55223
55762
  } catch (e) {
55224
55763
  label = "";
55225
55764
  }
55226
55765
  if ([
55227
55766
  label,
55228
55767
  record.id,
55229
- n.data.topic_name
55230
- ].filter((v) => typeof v === "string").join(" ").toLowerCase().includes(needle)) ids.push(n.id);
55768
+ node.topic_name
55769
+ ].filter((v) => typeof v === "string").join(" ").toLowerCase().includes(needle)) keys.push(node.key);
55231
55770
  }
55232
- for (let id of ids) states[id] = ["active"];
55233
55771
  }
55234
- let prev_ids = priv._focus_ids || [];
55235
- priv._focus_ids = ids;
55236
- priv._focus_topic = null;
55237
- try {
55238
- if (typeof graph.setElementState === "function") graph.setElementState(states);
55239
- apply_node_highlight(gobj, prev_ids, ids);
55240
- if (ids.length && typeof graph.focusElement === "function") graph.focusElement(ids);
55241
- } catch (e) {
55242
- (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: find apply failed: ${e}`);
55772
+ let visible = fold_visible_set(model, state);
55773
+ let revealed = 0;
55774
+ for (let key of keys) {
55775
+ if (visible.has(key)) continue;
55776
+ if (revealed >= state.page_size) break;
55777
+ fold_reveal(model, state, key);
55778
+ revealed++;
55243
55779
  }
55244
- return ids.length;
55780
+ priv._focus_topic = null;
55781
+ reconcile_fold(gobj, {}).then(() => {
55782
+ apply_focus_ids(gobj, keys, "find");
55783
+ });
55784
+ return keys.length;
55245
55785
  }
55246
55786
  async function graph_zoom_in(gobj) {
55247
55787
  let graph = gobj.priv.graph;
@@ -55354,20 +55894,43 @@ var LightNode = class extends _antv_g6.Circle {
55354
55894
  ************************************************************/
55355
55895
  var ManualLayout = class extends _antv_g6.BaseLayout {
55356
55896
  async execute(data, options) {
55357
- const { nodes = [] } = data;
55358
- return { nodes: nodes.map((node) => {
55359
- let gp = node.data.graph_props;
55360
- let geo = node.data.record._geometry;
55897
+ const { nodes = [], edges = [] } = data;
55898
+ let placed = {};
55899
+ let out = [];
55900
+ let chips = [];
55901
+ for (let node of nodes) {
55902
+ let d = node.data || {};
55903
+ if (d.more) {
55904
+ chips.push(node);
55905
+ continue;
55906
+ }
55907
+ let gp = d.graph_props;
55908
+ let geo = d.record ? d.record._geometry : null;
55361
55909
  let x = gp && gp.x != null ? gp.x : geo && geo.x != null ? geo.x : 0;
55362
55910
  let y = gp && gp.y != null ? gp.y : geo && geo.y != null ? geo.y : 0;
55363
- return {
55911
+ placed[node.id] = [x, y];
55912
+ out.push({
55364
55913
  id: node.id,
55365
55914
  style: {
55366
55915
  x,
55367
55916
  y
55368
55917
  }
55369
- };
55370
- }) };
55918
+ });
55919
+ }
55920
+ for (let node of chips) {
55921
+ let edge = edges.find((e) => e.target === node.id);
55922
+ let p = edge ? placed[edge.source] : null;
55923
+ let x = p ? p[0] : node.style && node.style.x || 0;
55924
+ let y = p ? p[1] + 80 : node.style && node.style.y || 0;
55925
+ out.push({
55926
+ id: node.id,
55927
+ style: {
55928
+ x,
55929
+ y
55930
+ }
55931
+ });
55932
+ }
55933
+ return { nodes: out };
55371
55934
  }
55372
55935
  };
55373
55936
  /************************************************************
@@ -56871,7 +57434,7 @@ function show_node_popover(gobj) {
56871
57434
  };
56872
57435
  if (node_graph_type === "hierarchical") {
56873
57436
  let record = nodeData.data.record || {};
56874
- updateStyle.innerHTML = build_node_innerHTML(fill, priv.theme, record.icon, node_label(nodeData.data.desc, record), nodeData.data.desc.topic_name, false, record.id);
57437
+ 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));
56875
57438
  }
56876
57439
  graph.updateNodeData([{
56877
57440
  id: node_id,
@@ -56911,7 +57474,7 @@ function show_node_popover(gobj) {
56911
57474
  };
56912
57475
  if (node_graph_type === "hierarchical") {
56913
57476
  let record = nodeData.data.record || {};
56914
- restoreStyle.innerHTML = build_node_innerHTML(origFill, priv.theme, record.icon, node_label(nodeData.data.desc, record), nodeData.data.desc.topic_name, false, record.id);
57477
+ 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));
56915
57478
  }
56916
57479
  graph.updateNodeData([{
56917
57480
  id: node_id,
@@ -57096,6 +57659,7 @@ function refresh_minimap(gobj) {
57096
57659
  let nd = graph.getNodeData(id);
57097
57660
  let size = nd && nd.style && nd.style.size || [120, 60];
57098
57661
  let color = nd && nd.data && nd.data.desc && nd.data.desc.color || "#94a3b8";
57662
+ if (is_more_node(nd)) color = "rgba(148, 163, 184, 0.35)";
57099
57663
  return new Rect({ style: {
57100
57664
  x: -size[0] / 2,
57101
57665
  y: -size[1] / 2,
@@ -57118,15 +57682,15 @@ function refresh_minimap(gobj) {
57118
57682
  * same three-way choice — it lived inline in the theme refresh and is
57119
57683
  * shared now. Returns null for a node that carries no desc.
57120
57684
  ************************************************************/
57121
- function node_innerHTML_of(nd, theme, highlight, selected, anchored) {
57685
+ function node_innerHTML_of(nd, theme, highlight, selected, anchored, pills_html) {
57122
57686
  if (!nd || !nd.data || !nd.data.desc) return null;
57123
57687
  let desc = nd.data.desc;
57124
57688
  let record = nd.data.record || {};
57125
57689
  let label = node_label(desc, record);
57126
57690
  switch (desc.node_treedb_type) {
57127
57691
  case "child": return build_chip_innerHTML(desc.color, theme, record.icon, label, record.id, highlight, selected, anchored);
57128
- case "extended": return build_node_innerHTML(desc.color, theme, record.icon, label, desc.topic_name, true, record.id, highlight, selected, anchored);
57129
- case "hierarchical": return build_node_innerHTML(desc.color, theme, record.icon, label, desc.topic_name, false, record.id, highlight, selected, anchored);
57692
+ case "extended": return build_node_innerHTML(desc.color, theme, record.icon, label, desc.topic_name, true, record.id, highlight, selected, anchored, pills_html);
57693
+ case "hierarchical": return build_node_innerHTML(desc.color, theme, record.icon, label, desc.topic_name, false, record.id, highlight, selected, anchored, pills_html);
57130
57694
  }
57131
57695
  return null;
57132
57696
  }
@@ -57147,7 +57711,13 @@ function repaint_cards(gobj, ids) {
57147
57711
  let selected = new Set(priv._selected_paint_ids || []);
57148
57712
  let updates = [];
57149
57713
  for (let id of ids) {
57150
- let html = node_innerHTML_of(graph.getNodeData(id), priv.theme, focus.has(id), selected.has(id), priv.anchor_state === "on" && id === priv.anchor_id);
57714
+ let nd;
57715
+ try {
57716
+ nd = graph.getNodeData(id);
57717
+ } catch (e) {
57718
+ continue;
57719
+ }
57720
+ 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));
57151
57721
  if (html !== null) updates.push({
57152
57722
  id,
57153
57723
  style: { innerHTML: html }
@@ -57183,7 +57753,9 @@ function refresh_html_nodes_theme(gobj, theme) {
57183
57753
  let updates = [];
57184
57754
  for (let i = 0; i < nodes.length; i++) {
57185
57755
  let id = nodes[i].id;
57186
- let html = node_innerHTML_of(graph.getNodeData(id), theme, highlighted.has(id), selected.has(id));
57756
+ let nd = graph.getNodeData(id);
57757
+ 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));
57758
+ if (html === null) html = more_innerHTML_of(gobj, nd, theme);
57187
57759
  if (html !== null) updates.push({
57188
57760
  id,
57189
57761
  style: { innerHTML: html }
@@ -57298,7 +57870,7 @@ function build_chip_innerHTML(color, theme, icon, label, key, highlight, selecte
57298
57870
  * colour is kept (per-topic differentiation) but softened via
57299
57871
  * color-mix instead of a harsh saturated fill. Theme-aware.
57300
57872
  ************************************************************/
57301
- function build_node_innerHTML(color, theme, icon, label, topic_name, structural, key, highlight, selected, anchored) {
57873
+ function build_node_innerHTML(color, theme, icon, label, topic_name, structural, key, highlight, selected, anchored, pills_html) {
57302
57874
  let title = key || label;
57303
57875
  let dark = theme === "dark";
57304
57876
  let surface = dark ? "#1b2230" : "#ffffff";
@@ -57332,6 +57904,12 @@ function build_node_innerHTML(color, theme, icon, label, topic_name, structural,
57332
57904
  margin-top: 2px; max-width: 100%;
57333
57905
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
57334
57906
  ">${(0, _yuneta_gobj_js.escapeHtml)(topic_name)}</div>`;
57907
+ let pills_row = "";
57908
+ if (pills_html) pills_row = `
57909
+ <div class="TREEDB_PILLS" style="
57910
+ display: flex; flex-wrap: nowrap; gap: 4px; justify-content: center;
57911
+ margin-top: 6px; max-width: 100%; overflow: hidden;
57912
+ ">${pills_html}</div>`;
57335
57913
  return `
57336
57914
  <div class="TREEDB_CARD" title="${(0, _yuneta_gobj_js.escapeHtml)(title)}" style="
57337
57915
  box-sizing: border-box;
@@ -57357,7 +57935,7 @@ function build_node_innerHTML(color, theme, icon, label, topic_name, structural,
57357
57935
  max-width: 100%; overflow: hidden; text-overflow: ellipsis;
57358
57936
  display: -webkit-box; -webkit-line-clamp: 2;
57359
57937
  -webkit-box-orient: vertical; word-break: break-word;
57360
- ">${(0, _yuneta_gobj_js.escapeHtml)(label)}</div>${sub_html}
57938
+ ">${(0, _yuneta_gobj_js.escapeHtml)(label)}</div>${sub_html}${pills_row}
57361
57939
  </div>
57362
57940
  `;
57363
57941
  }
@@ -57390,7 +57968,7 @@ function execute_delete_node(gobj, nodeData) {
57390
57968
  function select_all_nodes(gobj) {
57391
57969
  let graph = gobj.priv.graph;
57392
57970
  if (!graph) return;
57393
- set_selection(gobj, ((graph.getData() || {}).nodes || []).map((nd) => nd.id));
57971
+ set_selection(gobj, ((graph.getData() || {}).nodes || []).filter((nd) => nd.data && nd.data.desc).map((nd) => nd.id));
57394
57972
  }
57395
57973
  /************************************************************
57396
57974
  * The nodes currently selected, as their node data.
@@ -57654,7 +58232,7 @@ function apply_node_properties(gobj, node_id, fill, stroke, lineWidth, scope) {
57654
58232
  };
57655
58233
  if (nd.data.desc.node_treedb_type === "hierarchical") {
57656
58234
  let record = nd.data.record || {};
57657
- updateStyle.innerHTML = build_node_innerHTML(fill, priv.theme, record.icon, node_label(nd.data.desc, record), nd.data.desc.topic_name, false, record.id);
58235
+ 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));
57658
58236
  }
57659
58237
  updates.push({
57660
58238
  id: nodes[i].id,
@@ -57730,7 +58308,7 @@ function apply_edge_properties(gobj, edge_id, lineWidth, stroke, scope) {
57730
58308
  function update_edge_geometry(gobj, edge_id) {
57731
58309
  let priv = gobj.priv;
57732
58310
  let edgeData = priv.graph.getEdgeData(edge_id);
57733
- if (!edgeData || !edgeData.data) return;
58311
+ if (!edgeData || !edgeData.data || !edgeData.data.parent_topic) return;
57734
58312
  let style = edgeData.style || {};
57735
58313
  let lineWidth = style.lineWidth;
57736
58314
  let stroke = style.stroke;
@@ -57871,7 +58449,7 @@ function copy_size_to_nodes(gobj, same_topic_only) {
57871
58449
  const nodes = graph.getData().nodes;
57872
58450
  for (let i = 0; i < nodes.length; i++) {
57873
58451
  let nd = graph.getNodeData(nodes[i].id);
57874
- if (!nd || !nd.data) continue;
58452
+ if (!nd || !nd.data || !nd.data.desc) continue;
57875
58453
  if (same_topic_only && nd.data.topic_name !== source_topic) continue;
57876
58454
  if (nodes[i].id === node_id) continue;
57877
58455
  let updateStyle = { size: [...source_size] };
@@ -57959,6 +58537,415 @@ function copy_size_to_ports(gobj, same_topic_only) {
57959
58537
  }
57960
58538
  }
57961
58539
  /***************************
58540
+ * Folding
58541
+ ***************************/
58542
+ var PILL_ROW_HEIGHT = 24;
58543
+ var MORE_CHIP_SIZE = [76, 26];
58544
+ /************************************************************
58545
+ * The three card sizes, with room for the pill row when there
58546
+ * is one. A chip (pure child) never has pills: it has no hooks.
58547
+ ************************************************************/
58548
+ function card_size_for(node_treedb_type, has_pills) {
58549
+ let extra = has_pills ? PILL_ROW_HEIGHT : 0;
58550
+ switch (node_treedb_type) {
58551
+ case "child": return [116, 40];
58552
+ case "extended": return [144, 66 + extra];
58553
+ default: return [172, 96 + extra];
58554
+ }
58555
+ }
58556
+ /************************************************************
58557
+ * The model's key of a G6 record node, or "" for anything else.
58558
+ ************************************************************/
58559
+ function node_key_of(nd) {
58560
+ if (!nd || !nd.data || !nd.data.desc || !nd.data.record) return "";
58561
+ return fold_node_key(nd.data.desc.topic_name, String(nd.data.record.id));
58562
+ }
58563
+ function is_more_node(nd) {
58564
+ return !!(nd && nd.data && nd.data.more);
58565
+ }
58566
+ function more_node_id(gobj, group_key) {
58567
+ return (0, _yuneta_gobj_js.sprintf)("more-%s-%s", gobj.priv.treedb_name, group_key);
58568
+ }
58569
+ function more_edge_id(gobj, group_key) {
58570
+ return (0, _yuneta_gobj_js.sprintf)("more-edge-%s-%s", gobj.priv.treedb_name, group_key);
58571
+ }
58572
+ /************************************************************
58573
+ * The colours of a pill or a chip, from the topic colour it
58574
+ * stands for -- the CHILD topic, the same colour its hook port
58575
+ * wears, so the pill and the port say the same thing.
58576
+ ************************************************************/
58577
+ function pill_colors(color, theme) {
58578
+ let dark = theme === "dark";
58579
+ return {
58580
+ bg: dark ? `color-mix(in srgb, ${color} 45%, #1b2230)` : `color-mix(in srgb, ${color} 22%, #ffffff)`,
58581
+ fg: dark ? `color-mix(in srgb, ${color} 35%, #ffffff)` : `color-mix(in srgb, ${color} 55%, #0f172a)`,
58582
+ border: dark ? `color-mix(in srgb, ${color} 80%, #ffffff)` : color
58583
+ };
58584
+ }
58585
+ /************************************************************
58586
+ * One pill: `▸ devices 142` folded, `▾ devices 24/142` open on a
58587
+ * page, `▾ devices 142` open whole. A glyph and numbers on
58588
+ * purpose: the card is an innerHTML string where a WORD could
58589
+ * carry no i18n key, and the hook name is a schema identifier,
58590
+ * like the topic subtitle above it.
58591
+ ************************************************************/
58592
+ function build_pill_html(pill, color, theme) {
58593
+ let c = pill_colors(color, theme);
58594
+ let glyph = pill.open ? "&#9662;" : "&#9656;";
58595
+ let count = pill.open && pill.shown < pill.total ? `${pill.shown}/${pill.total}` : `${pill.total}`;
58596
+ return `<span class="TREEDB_PILL" data-fold-group="${(0, _yuneta_gobj_js.escapeHtml)(pill.group_key)}"
58597
+ title="${(0, _yuneta_gobj_js.escapeHtml)(pill.hook)}" style="
58598
+ display: inline-flex; align-items: center; gap: 3px; flex: 0 1 auto;
58599
+ height: 16px; padding: 0 6px; border-radius: 8px;
58600
+ background: ${c.bg}; color: ${c.fg}; border: 1px solid ${c.border};
58601
+ font-size: 10px; font-weight: 700; line-height: 1; white-space: nowrap;
58602
+ overflow: hidden; text-overflow: ellipsis;
58603
+ cursor: pointer; user-select: none;
58604
+ "><span style="font-size: 9px;">${glyph}</span>${(0, _yuneta_gobj_js.escapeHtml)(pill.hook)}<span style="opacity: .75; font-weight: 600;">${count}</span></span>`;
58605
+ }
58606
+ /************************************************************
58607
+ * The pill row of one node, as html; "" when it has nothing to
58608
+ * fold or the model is not built yet.
58609
+ ************************************************************/
58610
+ function pills_html_of_key(gobj, node_key) {
58611
+ let priv = gobj.priv;
58612
+ if (!priv._fold_model || !priv._fold_state || !node_key) return "";
58613
+ let pills = fold_pills_of(priv._fold_model, priv._fold_state, node_key);
58614
+ if (!pills.length) return "";
58615
+ let out = [];
58616
+ for (let pill of pills) {
58617
+ let child_desc = priv.descs ? priv.descs[pill.child_topic] : null;
58618
+ let color = child_desc && child_desc.color || "#94a3b8";
58619
+ out.push(build_pill_html(pill, color, priv.theme));
58620
+ }
58621
+ return out.join("");
58622
+ }
58623
+ function pills_html_of(gobj, nd) {
58624
+ return pills_html_of_key(gobj, node_key_of(nd));
58625
+ }
58626
+ function pills_signature(gobj, node_key) {
58627
+ let priv = gobj.priv;
58628
+ if (!priv._fold_model || !priv._fold_state) return "";
58629
+ return fold_pills_of(priv._fold_model, priv._fold_state, node_key).map((p) => `${p.hook}:${p.open ? p.shown : "-"}/${p.total}`).join(",");
58630
+ }
58631
+ /************************************************************
58632
+ * The `+N` chip at the end of a page: the rest of the group,
58633
+ * in the child topic's colour, dashed like a cut. Its title is
58634
+ * the one WORD of the folding, and it is translated at build
58635
+ * time: the chips are rebuilt on a language change.
58636
+ ************************************************************/
58637
+ function build_more_innerHTML(color, theme, rest) {
58638
+ let c = pill_colors(color, theme);
58639
+ return `
58640
+ <div class="TREEDB_MORE" title="${(0, _yuneta_gobj_js.escapeHtml)((0, i18next.t)("show more"))}" style="
58641
+ box-sizing: border-box; width: 100%; height: 100%;
58642
+ display: flex; align-items: center; justify-content: center; gap: 4px;
58643
+ background: ${c.bg}; color: ${c.fg}; border: 1px dashed ${c.border};
58644
+ border-radius: 13px;
58645
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
58646
+ font-size: 12px; font-weight: 700; line-height: 1;
58647
+ cursor: pointer; user-select: none;
58648
+ ">+${rest}<span style="font-size: 10px; opacity: .8;">&#8230;</span></div>
58649
+ `;
58650
+ }
58651
+ function more_innerHTML_of(gobj, nd, theme) {
58652
+ if (!is_more_node(nd)) return null;
58653
+ let priv = gobj.priv;
58654
+ let child_desc = priv.descs ? priv.descs[nd.data.child_topic] : null;
58655
+ return build_more_innerHTML(child_desc && child_desc.color || "#94a3b8", theme, nd.data.rest);
58656
+ }
58657
+ /************************************************************
58658
+ * The pills live inside an `innerHTML` string, so no handler can
58659
+ * be attached to them: the click is delegated from the container
58660
+ * in the CAPTURE phase over four event types -- the same door the
58661
+ * JSON graph opens for its fold handles, for the same reasons: G6
58662
+ * binds on the html node element, a descendant, so only capture
58663
+ * runs first; and G6 never reads the DOM `click`, it builds its
58664
+ * own from the pointer pair, so `pointerdown`/`pointerup` are the
58665
+ * ones that have to be cut or the card is ALSO clicked.
58666
+ ************************************************************/
58667
+ var FOLD_SWALLOWED_EVENTS = [
58668
+ "pointerdown",
58669
+ "pointerup",
58670
+ "mousedown",
58671
+ "click"
58672
+ ];
58673
+ function install_fold_listener(gobj) {
58674
+ let priv = gobj.priv;
58675
+ if (!priv.$container) {
58676
+ (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: no container, the fold pills are dead`);
58677
+ return;
58678
+ }
58679
+ priv._fold_listener = function(evt) {
58680
+ let $pill = evt.target && evt.target.closest ? evt.target.closest("[data-fold-group]") : null;
58681
+ if (!$pill) return;
58682
+ evt.stopPropagation();
58683
+ evt.preventDefault();
58684
+ if (evt.type !== "click") return;
58685
+ (0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_TOGGLE_FOLD", { group: $pill.getAttribute("data-fold-group") }, gobj);
58686
+ };
58687
+ for (let type of FOLD_SWALLOWED_EVENTS) priv.$container.addEventListener(type, priv._fold_listener, true);
58688
+ }
58689
+ function uninstall_fold_listener(gobj) {
58690
+ let priv = gobj.priv;
58691
+ if (priv._fold_listener && priv.$container) for (let type of FOLD_SWALLOWED_EVENTS) priv.$container.removeEventListener(type, priv._fold_listener, true);
58692
+ priv._fold_listener = null;
58693
+ }
58694
+ /************************************************************
58695
+ * (Re)build the tree of the records. Cheap -- one pass over the
58696
+ * records -- so it runs again on every create/update/delete
58697
+ * rather than being patched. The fold state survives: it is
58698
+ * keyed by group, and a group that is gone is simply never read.
58699
+ ************************************************************/
58700
+ function rebuild_fold_model(gobj) {
58701
+ let priv = gobj.priv;
58702
+ priv._fold_model = fold_build_model(priv.descs || {}, priv.records || {});
58703
+ if (!priv._fold_state) priv._fold_state = fold_new_state((0, _yuneta_gobj_js.gobj_read_integer_attr)(gobj, "fold_page_size"));
58704
+ if (!priv._pill_sig) priv._pill_sig = /* @__PURE__ */ new Map();
58705
+ }
58706
+ /************************************************************
58707
+ * How many records there are, and how many carry a saved
58708
+ * position -- what auto_layout decides on. Counted over the
58709
+ * RECORDS, because the nodes on screen are a fraction of them.
58710
+ ************************************************************/
58711
+ function count_placed_records(gobj) {
58712
+ let priv = gobj.priv;
58713
+ priv._nodes_total = 0;
58714
+ priv._nodes_placed = 0;
58715
+ for (const [topic_name, records] of Object.entries(priv.records || {})) {
58716
+ if (topic_name.substring(0, 2) === "__" || !(0, _yuneta_gobj_js.is_array)(records)) continue;
58717
+ for (let record of records) {
58718
+ priv._nodes_total++;
58719
+ if (geometry_has_position(get_node_graph_props(gobj, topic_name, record.id, record))) priv._nodes_placed++;
58720
+ }
58721
+ }
58722
+ }
58723
+ /************************************************************
58724
+ * Make the G6 data say what the fold model says is visible.
58725
+ *
58726
+ * Record nodes not visible any more are removed (their edges go
58727
+ * with them), the visible ones missing are created, the links
58728
+ * between visible nodes are drawn, the `+N` chips are placed or
58729
+ * taken away, and every card whose pills changed is repainted.
58730
+ * Then a draw, a layout if the set of nodes changed, and the
58731
+ * camera: fit the whole thing on the first load, or put `keep`
58732
+ * back where it was on screen before the change.
58733
+ *
58734
+ * Serialised: one reconcile at a time, in order. Two pills tapped
58735
+ * quickly would otherwise interleave their draws.
58736
+ *
58737
+ * opts.relayout run the layout even if no node came or went
58738
+ * opts.fit fit the viewport afterwards
58739
+ * opts.keep node id to hold still on screen
58740
+ * opts.keep_vp where it was ([x, y] viewport px)
58741
+ ************************************************************/
58742
+ function reconcile_fold(gobj, opts) {
58743
+ let priv = gobj.priv;
58744
+ let run = () => reconcile_fold_now(gobj, opts || {}).catch((e) => {
58745
+ (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: reconcile failed: ${e}`);
58746
+ });
58747
+ priv._fold_chain = (priv._fold_chain || Promise.resolve()).then(run, run);
58748
+ return priv._fold_chain;
58749
+ }
58750
+ async function reconcile_fold_now(gobj, opts) {
58751
+ let priv = gobj.priv;
58752
+ let graph = priv.graph;
58753
+ let model = priv._fold_model;
58754
+ let state = priv._fold_state;
58755
+ if (!graph || !model || !state || (0, _yuneta_gobj_js.gobj_is_destroying)(gobj)) return;
58756
+ let visible = fold_visible_set(model, state);
58757
+ let changed = false;
58758
+ history_pause(gobj);
58759
+ try {
58760
+ changed = await reconcile_fold_apply(gobj, opts, visible);
58761
+ } finally {
58762
+ history_resume(gobj);
58763
+ }
58764
+ return changed;
58765
+ }
58766
+ async function reconcile_fold_apply(gobj, opts, visible) {
58767
+ let priv = gobj.priv;
58768
+ let graph = priv.graph;
58769
+ let model = priv._fold_model;
58770
+ let state = priv._fold_state;
58771
+ let changed = false;
58772
+ let present = /* @__PURE__ */ new Map();
58773
+ let more_present = /* @__PURE__ */ new Map();
58774
+ for (let nd of graph.getNodeData() || []) {
58775
+ if (is_more_node(nd)) {
58776
+ more_present.set(nd.data.more, nd.id);
58777
+ continue;
58778
+ }
58779
+ let key = node_key_of(nd);
58780
+ if (key) present.set(key, nd.id);
58781
+ }
58782
+ let to_remove = [];
58783
+ for (let [key, id] of present) if (!visible.has(key)) to_remove.push(id);
58784
+ if (to_remove.length) {
58785
+ let gone = new Set(to_remove);
58786
+ if (priv._selected_node_id && gone.has(priv._selected_node_id)) deselect_node(gobj);
58787
+ if (priv._detail_node_id && gone.has(priv._detail_node_id)) hide_node_detail(gobj);
58788
+ priv._focus_ids = (priv._focus_ids || []).filter((id) => !gone.has(id));
58789
+ priv._selected_paint_ids = (priv._selected_paint_ids || []).filter((id) => !gone.has(id));
58790
+ if (priv.anchor_state === "on" && gone.has(priv.anchor_id)) {
58791
+ priv.anchor_id = "";
58792
+ priv.anchor_state = "off";
58793
+ update_toolbar(gobj);
58794
+ }
58795
+ for (let id of to_remove) priv._pill_sig.delete(id);
58796
+ graph.removeNodeData(to_remove);
58797
+ changed = true;
58798
+ }
58799
+ for (let key of visible) {
58800
+ if (present.has(key)) continue;
58801
+ let node = model.nodes.get(key);
58802
+ let desc = priv.descs[node.topic_name];
58803
+ if (!desc) {
58804
+ (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: no desc for ${key}`);
58805
+ continue;
58806
+ }
58807
+ create_topic_node(gobj, desc, node.record);
58808
+ priv._pill_sig.set(build_node_name(gobj, node.topic_name, node.id), pills_signature(gobj, key));
58809
+ changed = true;
58810
+ }
58811
+ for (let key of visible) {
58812
+ let node = model.nodes.get(key);
58813
+ let desc = priv.descs[node.topic_name];
58814
+ if (desc) draw_links(gobj, desc, node.record, false);
58815
+ }
58816
+ let pending = fold_pending_groups(model, state, visible);
58817
+ let wanted = /* @__PURE__ */ new Map();
58818
+ for (let p of pending) wanted.set(p.group_key, p);
58819
+ let more_gone = [];
58820
+ for (let [group_key, id] of more_present) if (!wanted.has(group_key)) more_gone.push(id);
58821
+ if (more_gone.length) {
58822
+ graph.removeNodeData(more_gone);
58823
+ changed = true;
58824
+ }
58825
+ for (let p of pending) {
58826
+ let first = model.nodes.get((model.groups.get(p.group_key) || [])[0]);
58827
+ let child_topic = first ? first.topic_name : "";
58828
+ let child_desc = priv.descs[child_topic];
58829
+ let color = child_desc && child_desc.color || "#94a3b8";
58830
+ let id = more_node_id(gobj, p.group_key);
58831
+ if (more_present.has(p.group_key)) {
58832
+ let nd = graph.getNodeData(id);
58833
+ if (nd && nd.data.rest !== p.rest) {
58834
+ nd.data.rest = p.rest;
58835
+ graph.updateNodeData([{
58836
+ id,
58837
+ style: { innerHTML: build_more_innerHTML(color, priv.theme, p.rest) }
58838
+ }]);
58839
+ }
58840
+ continue;
58841
+ }
58842
+ let xy = get_default_ne_xy(gobj);
58843
+ graph.addNodeData([{
58844
+ id,
58845
+ type: "html",
58846
+ style: {
58847
+ x: xy,
58848
+ y: xy,
58849
+ size: [...MORE_CHIP_SIZE],
58850
+ dx: -MORE_CHIP_SIZE[0] / 2,
58851
+ dy: -MORE_CHIP_SIZE[1] / 2,
58852
+ innerHTML: build_more_innerHTML(color, priv.theme, p.rest)
58853
+ },
58854
+ data: {
58855
+ more: p.group_key,
58856
+ child_topic,
58857
+ rest: p.rest
58858
+ }
58859
+ }]);
58860
+ if (p.node_key) {
58861
+ let parent = model.nodes.get(p.node_key);
58862
+ let dark = priv.theme === "dark";
58863
+ graph.addEdgeData([{
58864
+ id: more_edge_id(gobj, p.group_key),
58865
+ type: edge_type_for(gobj),
58866
+ source: build_node_name(gobj, parent.topic_name, parent.id),
58867
+ target: id,
58868
+ style: {
58869
+ sourcePort: p.hook,
58870
+ lineWidth: 1,
58871
+ lineDash: [4, 3],
58872
+ stroke: dark ? "#8b94a3" : "#94a3b8",
58873
+ startArrow: false,
58874
+ endArrow: false
58875
+ },
58876
+ data: { more: p.group_key }
58877
+ }]);
58878
+ }
58879
+ changed = true;
58880
+ }
58881
+ let repaint = /* @__PURE__ */ new Set();
58882
+ let resize = [];
58883
+ for (let key of visible) {
58884
+ let node = model.nodes.get(key);
58885
+ let id = build_node_name(gobj, node.topic_name, node.id);
58886
+ let sig = pills_signature(gobj, key);
58887
+ if (priv._pill_sig.get(id) === sig) continue;
58888
+ let had = !!priv._pill_sig.get(id);
58889
+ priv._pill_sig.set(id, sig);
58890
+ repaint.add(id);
58891
+ if (had !== !!sig) {
58892
+ let nd = graph.getNodeData(id);
58893
+ let desc = nd && nd.data && nd.data.desc;
58894
+ let geometry = nd && nd.data && nd.data.graph_props || {};
58895
+ if (desc && !(0, _yuneta_gobj_js.is_array)(geometry.size)) {
58896
+ let size = card_size_for(desc.node_treedb_type, !!sig);
58897
+ resize.push({
58898
+ id,
58899
+ style: {
58900
+ size,
58901
+ dx: -size[0] / 2,
58902
+ dy: -size[1] / 2
58903
+ }
58904
+ });
58905
+ }
58906
+ }
58907
+ }
58908
+ if (resize.length) graph.updateNodeData(resize);
58909
+ if (repaint.size) repaint_cards(gobj, repaint);
58910
+ try {
58911
+ refresh_minimap(gobj);
58912
+ } catch (e) {
58913
+ (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: refresh_minimap failed: ${e}`);
58914
+ }
58915
+ await graph_draw(gobj);
58916
+ if (changed || opts.relayout) await graph_layout(gobj);
58917
+ if (opts.fit) await graph_fit_readable(gobj);
58918
+ else if (opts.keep && opts.keep_vp) await yui_graph_place_at(graph, opts.keep, opts.keep_vp);
58919
+ update_resize_handles_position(gobj);
58920
+ update_port_resize_handles_position(gobj);
58921
+ update_edge_icon_position(gobj);
58922
+ update_node_icon_position(gobj);
58923
+ update_link_icon_position(gobj);
58924
+ return changed;
58925
+ }
58926
+ /************************************************************
58927
+ * Repaint the `+N` chips (theme or language changed).
58928
+ ************************************************************/
58929
+ function repaint_more_chips(gobj) {
58930
+ let priv = gobj.priv;
58931
+ let graph = priv.graph;
58932
+ if (!graph) return;
58933
+ let updates = [];
58934
+ for (let nd of graph.getNodeData() || []) {
58935
+ let html = more_innerHTML_of(gobj, nd, priv.theme);
58936
+ if (html !== null) updates.push({
58937
+ id: nd.id,
58938
+ style: { innerHTML: html }
58939
+ });
58940
+ }
58941
+ if (updates.length) try {
58942
+ graph.updateNodeData(updates);
58943
+ graph.draw();
58944
+ } catch (e) {
58945
+ (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: cannot repaint the chips: ${e}`);
58946
+ }
58947
+ }
58948
+ /***************************
57962
58949
  * Actions
57963
58950
  ***************************/
57964
58951
  /************************************************************
@@ -57987,6 +58974,9 @@ function ac_clear_data(gobj, event, kw, src) {
57987
58974
  (0, _yuneta_gobj_js.gobj_write_attr)(gobj, "records", {});
57988
58975
  priv._nodes_total = 0;
57989
58976
  priv._nodes_placed = 0;
58977
+ priv._fold_model = null;
58978
+ priv._fold_state = null;
58979
+ priv._pill_sig = null;
57990
58980
  graph_remove_plugin(gobj, "history");
57991
58981
  update_history_buttons(gobj);
57992
58982
  graph_clear(gobj);
@@ -58017,53 +59007,43 @@ function ac_load_data(gobj, event, kw, src) {
58017
59007
  }
58018
59008
  return 0;
58019
59009
  }
58020
- let desc = priv.descs[topic_name];
59010
+ priv.descs[topic_name];
58021
59011
  priv.records[topic_name] = data;
58022
59012
  priv.descs[topic_name].loaded = true;
58023
- for (let i = 0; i < data.length; i++) {
58024
- let record = data[i];
58025
- create_topic_node(gobj, desc, record);
58026
- }
58027
- let do_links = true;
59013
+ let all_loaded = true;
58028
59014
  for (const [topic_name, desc] of Object.entries(priv.descs)) {
58029
59015
  if (topic_name.substring(0, 2) === "__") continue;
58030
59016
  if (!desc.loaded) {
58031
- do_links = false;
59017
+ all_loaded = false;
58032
59018
  break;
58033
59019
  }
58034
59020
  }
58035
- if (do_links && priv.graph) {
59021
+ if (all_loaded && priv.graph) {
59022
+ rebuild_fold_model(gobj);
59023
+ fold_expand_to_depth(priv._fold_model, priv._fold_state, (0, _yuneta_gobj_js.gobj_read_integer_attr)(gobj, "expand_depth"));
59024
+ count_placed_records(gobj);
59025
+ let auto_laid = false;
58036
59026
  try {
58037
- refresh_minimap(gobj);
59027
+ auto_laid = auto_layout(gobj);
58038
59028
  } catch (e) {
58039
- (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: refresh_minimap failed: ${e}`);
58040
- }
58041
- graph_draw(gobj).then(() => {
58042
- create_links(gobj);
58043
- graph_draw(gobj).then(() => {
58044
- let auto_laid = false;
58045
- try {
58046
- auto_laid = auto_layout(gobj);
58047
- } catch (e) {
58048
- (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: auto_layout failed: ${e}`);
58049
- }
58050
- graph_layout(gobj).then(() => {
58051
- if (auto_laid) graph_fit_readable(gobj).catch((e) => {
58052
- (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: cannot fit the graph: ${e}`);
58053
- });
58054
- sync_history_plugin(gobj);
58055
- if (priv._pending_focus_topic !== null) {
58056
- let ft = priv._pending_focus_topic;
58057
- priv._pending_focus_topic = null;
58058
- graph_focus_topic(gobj, ft);
58059
- }
58060
- if (priv._pending_find !== null) {
58061
- let term = priv._pending_find;
58062
- priv._pending_find = null;
58063
- publish_find_result(gobj, term, graph_find_nodes(gobj, term));
58064
- }
58065
- });
58066
- });
59029
+ (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: auto_layout failed: ${e}`);
59030
+ }
59031
+ apply_layout_direction(gobj);
59032
+ reconcile_fold(gobj, {
59033
+ relayout: true,
59034
+ fit: auto_laid
59035
+ }).then(() => {
59036
+ sync_history_plugin(gobj);
59037
+ if (priv._pending_focus_topic !== null) {
59038
+ let ft = priv._pending_focus_topic;
59039
+ priv._pending_focus_topic = null;
59040
+ graph_focus_topic(gobj, ft);
59041
+ }
59042
+ if (priv._pending_find !== null) {
59043
+ let term = priv._pending_find;
59044
+ priv._pending_find = null;
59045
+ publish_find_result(gobj, term, graph_find_nodes(gobj, term));
59046
+ }
58067
59047
  });
58068
59048
  }
58069
59049
  return 0;
@@ -58099,19 +59079,15 @@ function ac_node_created(gobj, event, kw, src) {
58099
59079
  build_graph_properties(gobj);
58100
59080
  return 0;
58101
59081
  }
58102
- let desc = priv.descs[topic_name];
58103
- if (!desc) {
59082
+ if (!priv.descs[topic_name]) {
58104
59083
  (0, _yuneta_gobj_js.log_error)(`ac_node_created: unknown topic: ${topic_name}`);
58105
59084
  return 0;
58106
59085
  }
58107
59086
  if (!priv.records[topic_name]) priv.records[topic_name] = [];
58108
59087
  priv.records[topic_name].push(node);
58109
- history_pause(gobj);
58110
- create_topic_node(gobj, desc, node);
58111
- draw_links(gobj, desc, node, false);
58112
- graph_draw(gobj).then(() => {
58113
- history_resume(gobj);
58114
- });
59088
+ rebuild_fold_model(gobj);
59089
+ fold_reveal(priv._fold_model, priv._fold_state, fold_node_key(topic_name, String(node.id)));
59090
+ reconcile_fold(gobj, {});
58115
59091
  return 0;
58116
59092
  }
58117
59093
  /************************************************************
@@ -58160,8 +59136,14 @@ function ac_node_updated(gobj, event, kw, src) {
58160
59136
  let [col_idx, fkey_value] = ref.split(" ");
58161
59137
  clear_link(gobj, topic_name, node.id, cols[parseInt(col_idx)], fkey_value, false);
58162
59138
  }
58163
- update_topic_node(gobj, desc, node_name, node);
58164
59139
  update_local_node(gobj, topic_name, node);
59140
+ let on_screen = false;
59141
+ try {
59142
+ on_screen = !!graph.getNodeData(node_name);
59143
+ } catch (e) {
59144
+ on_screen = false;
59145
+ }
59146
+ if (on_screen) update_topic_node(gobj, desc, node_name, node);
58165
59147
  for (let ref of new_refs) if (!old_refs.has(ref)) {
58166
59148
  let [col_idx, fkey_value] = ref.split(" ");
58167
59149
  draw_link(gobj, topic_name, node.id, cols[parseInt(col_idx)], fkey_value, false);
@@ -58169,6 +59151,8 @@ function ac_node_updated(gobj, event, kw, src) {
58169
59151
  graph_draw(gobj).then(() => {
58170
59152
  history_resume(gobj);
58171
59153
  });
59154
+ rebuild_fold_model(gobj);
59155
+ reconcile_fold(gobj, {});
58172
59156
  return 0;
58173
59157
  }
58174
59158
  /************************************************************
@@ -58194,9 +59178,12 @@ function ac_node_deleted(gobj, event, kw, src) {
58194
59178
  clear_links(gobj, desc, node, false);
58195
59179
  remove_topic_node(gobj, node_name);
58196
59180
  remove_local_node(gobj, topic_name, node);
59181
+ if (priv._pill_sig) priv._pill_sig.delete(node_name);
58197
59182
  graph_draw(gobj).then(() => {
58198
59183
  history_resume(gobj);
58199
59184
  });
59185
+ rebuild_fold_model(gobj);
59186
+ reconcile_fold(gobj, {});
58200
59187
  return 0;
58201
59188
  }
58202
59189
  /************************************************************
@@ -58255,6 +59242,7 @@ function ac_theme$1(gobj, event, kw, src) {
58255
59242
  });
58256
59243
  refresh_html_nodes_theme(gobj, theme);
58257
59244
  refresh_default_edges_theme(gobj, theme);
59245
+ repaint_more_chips(gobj);
58258
59246
  graph_draw(gobj).then(() => {
58259
59247
  update_history_buttons(gobj);
58260
59248
  });
@@ -58369,6 +59357,85 @@ function ac_focus_topic(gobj, event, kw, src) {
58369
59357
  return 0;
58370
59358
  }
58371
59359
  /************************************************************
59360
+ * A pill was tapped: open that hook on its first page, or fold
59361
+ * it. The card the pill is on stays where it was on screen --
59362
+ * the layout moves everything else around it.
59363
+ ************************************************************/
59364
+ function ac_toggle_fold(gobj, event, kw, src) {
59365
+ let priv = gobj.priv;
59366
+ let group = kw && kw.group || "";
59367
+ if (!priv._fold_model || !group) {
59368
+ (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: fold of '${group}' with no tree loaded`);
59369
+ return -1;
59370
+ }
59371
+ let g = fold_split_group_key(group);
59372
+ if (!g) {
59373
+ (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: bad fold group '${group}'`);
59374
+ return -1;
59375
+ }
59376
+ let node_id = g.node_key ? node_id_of_key(gobj, g.node_key) : "";
59377
+ let vp = node_id ? yui_graph_viewport_of(priv.graph, node_id) : null;
59378
+ fold_toggle(priv._fold_model, priv._fold_state, group);
59379
+ reconcile_fold(gobj, {
59380
+ keep: node_id,
59381
+ keep_vp: vp
59382
+ });
59383
+ return 0;
59384
+ }
59385
+ /************************************************************
59386
+ * A `+N` chip was tapped: the next page of its group. The
59387
+ * parent holds still; the chip itself moves down the column.
59388
+ ************************************************************/
59389
+ function ac_show_more(gobj, event, kw, src) {
59390
+ let priv = gobj.priv;
59391
+ let group = kw && kw.group || "";
59392
+ if (!priv._fold_model || !group) {
59393
+ (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: show more of '${group}' with no tree loaded`);
59394
+ return -1;
59395
+ }
59396
+ let g = fold_split_group_key(group);
59397
+ if (!g) {
59398
+ (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: bad fold group '${group}'`);
59399
+ return -1;
59400
+ }
59401
+ let node_id = g.node_key ? node_id_of_key(gobj, g.node_key) : "";
59402
+ let vp = node_id ? yui_graph_viewport_of(priv.graph, node_id) : null;
59403
+ fold_show_more(priv._fold_model, priv._fold_state, group);
59404
+ reconcile_fold(gobj, {
59405
+ keep: node_id,
59406
+ keep_vp: vp
59407
+ });
59408
+ return 0;
59409
+ }
59410
+ /************************************************************
59411
+ * The whole treedb, every hook open on all its children. This
59412
+ * is the reader asking for the pile on purpose; the pages are
59413
+ * the other way in.
59414
+ ************************************************************/
59415
+ function ac_expand_all$1(gobj, event, kw, src) {
59416
+ let priv = gobj.priv;
59417
+ if (!priv._fold_model) {
59418
+ (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: expand all with no tree loaded`);
59419
+ return -1;
59420
+ }
59421
+ fold_expand_all(priv._fold_model, priv._fold_state);
59422
+ reconcile_fold(gobj, { fit: true });
59423
+ return 0;
59424
+ }
59425
+ /************************************************************
59426
+ * The roots alone.
59427
+ ************************************************************/
59428
+ function ac_collapse_all$1(gobj, event, kw, src) {
59429
+ let priv = gobj.priv;
59430
+ if (!priv._fold_model) {
59431
+ (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: collapse all with no tree loaded`);
59432
+ return -1;
59433
+ }
59434
+ fold_collapse_all(priv._fold_model, priv._fold_state);
59435
+ reconcile_fold(gobj, { fit: true });
59436
+ return 0;
59437
+ }
59438
+ /************************************************************
58372
59439
  * Tell whoever asked how many nodes the term matched. The count is
58373
59440
  * the answer to the question the box asks; without it "nothing moved"
58374
59441
  * and "nothing matched" look the same.
@@ -58399,7 +59466,9 @@ function ac_fullscreen(gobj, event, kw, src) {
58399
59466
  ************************************************************/
58400
59467
  function ac_set_layout$1(gobj, event, kw, src) {
58401
59468
  gobj.priv;
58402
- graph_set_layout(gobj, select_layout(gobj, kw.layout)).then(() => {
59469
+ let layout = select_layout(gobj, kw.layout);
59470
+ apply_layout_direction(gobj);
59471
+ graph_set_layout(gobj, layout).then(() => {
58403
59472
  configure_behaviour(gobj);
58404
59473
  update_toolbar(gobj);
58405
59474
  });
@@ -58506,6 +59575,16 @@ function ac_node_click$2(gobj, event, kw, src) {
58506
59575
  let priv = gobj.priv;
58507
59576
  let graph = priv.graph;
58508
59577
  let node_id = kw.evt.target.id;
59578
+ let more_nd = null;
59579
+ try {
59580
+ more_nd = graph.getNodeData(node_id);
59581
+ } catch (e) {
59582
+ more_nd = null;
59583
+ }
59584
+ if (is_more_node(more_nd)) {
59585
+ (0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_SHOW_MORE", { group: more_nd.data.more }, gobj);
59586
+ return 0;
59587
+ }
58509
59588
  if (priv.anchor_state === "arming") {
58510
59589
  priv.anchor_id = node_id;
58511
59590
  priv.anchor_state = "on";
@@ -58810,6 +59889,26 @@ function create_gclass$4(gclass_name) {
58810
59889
  ac_find_nodes$1,
58811
59890
  null
58812
59891
  ],
59892
+ [
59893
+ "EV_TOGGLE_FOLD",
59894
+ ac_toggle_fold,
59895
+ null
59896
+ ],
59897
+ [
59898
+ "EV_SHOW_MORE",
59899
+ ac_show_more,
59900
+ null
59901
+ ],
59902
+ [
59903
+ "EV_EXPAND_ALL",
59904
+ ac_expand_all$1,
59905
+ null
59906
+ ],
59907
+ [
59908
+ "EV_COLLAPSE_ALL",
59909
+ ac_collapse_all$1,
59910
+ null
59911
+ ],
58813
59912
  [
58814
59913
  "EV_CENTER",
58815
59914
  ac_center,
@@ -58907,6 +60006,10 @@ function create_gclass$4(gclass_name) {
58907
60006
  ["EV_FIND_NODES", 0],
58908
60007
  ["EV_FIND_RESULT", _yuneta_gobj_js.event_flag_t.EVF_OUTPUT_EVENT],
58909
60008
  ["EV_LAYOUT_AUTOSET", _yuneta_gobj_js.event_flag_t.EVF_OUTPUT_EVENT],
60009
+ ["EV_TOGGLE_FOLD", 0],
60010
+ ["EV_SHOW_MORE", 0],
60011
+ ["EV_EXPAND_ALL", 0],
60012
+ ["EV_COLLAPSE_ALL", 0],
58910
60013
  ["EV_CENTER", 0],
58911
60014
  ["EV_FULLSCREEN", 0],
58912
60015
  ["EV_SET_LAYOUT", 0],
@@ -58964,6 +60067,8 @@ var attrs_table$3 = [
58964
60067
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_POINTER, "subscriber", 0, null, "Subscriber of output events"),
58965
60068
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.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."),
58966
60069
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_BOOLEAN, "with_treedb_tables", 0, false, "Include treedb tables"),
60070
+ (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.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"),
60071
+ (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.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"),
58967
60072
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_STRING, "operation_mode", _yuneta_gobj_js.sdata_flag_t.SDF_PERSIST, "reading", "Current operation mode (internal behaviour or role). Changed by the user trough the gui."),
58968
60073
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_STRING, "layout", _yuneta_gobj_js.sdata_flag_t.SDF_PERSIST, "", "Current graph layout. User preference. Changed by the user through the gui."),
58969
60074
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_POINTER, "gobj_remote_yuno", 0, null, "Remote Yuno to request data"),
@@ -59037,7 +60142,9 @@ function mt_create$3(gobj) {
59037
60142
  layout: priv.layout,
59038
60143
  with_treedb_tables: priv.with_treedb_tables,
59039
60144
  hook_port_position: "bottom",
59040
- fkey_port_position: "top"
60145
+ fkey_port_position: "top",
60146
+ expand_depth: (0, _yuneta_gobj_js.gobj_read_integer_attr)(gobj, "expand_depth"),
60147
+ fold_page_size: (0, _yuneta_gobj_js.gobj_read_integer_attr)(gobj, "fold_page_size")
59041
60148
  }, gobj);
59042
60149
  populate_nodes_tree_options(gobj);
59043
60150
  if (priv.with_treedb_tables) {}
@@ -59295,6 +60402,7 @@ function make_toolbar(gobj) {
59295
60402
  (0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_REFRESH_TREEDB", { evt }, gobj);
59296
60403
  } }
59297
60404
  ],
60405
+ ...yui_graph_fold_items(gobj, (0, _yuneta_gobj_js.gobj_read_str_attr)(gobj, "wide")),
59298
60406
  [
59299
60407
  "button",
59300
60408
  {
@@ -60436,6 +61544,20 @@ function ac_find_nodes(gobj, event, kw, src) {
60436
61544
  return 0;
60437
61545
  }
60438
61546
  /************************************************************
61547
+ * The fold pair of the toolbar, forwarded to the G6 child,
61548
+ * which owns the tree.
61549
+ ************************************************************/
61550
+ function ac_expand_all(gobj, event, kw, src) {
61551
+ let priv = gobj.priv;
61552
+ if (priv.gobj_nodes_tree) (0, _yuneta_gobj_js.gobj_send_event)(priv.gobj_nodes_tree, "EV_EXPAND_ALL", {}, gobj);
61553
+ return 0;
61554
+ }
61555
+ function ac_collapse_all(gobj, event, kw, src) {
61556
+ let priv = gobj.priv;
61557
+ if (priv.gobj_nodes_tree) (0, _yuneta_gobj_js.gobj_send_event)(priv.gobj_nodes_tree, "EV_COLLAPSE_ALL", {}, gobj);
61558
+ return 0;
61559
+ }
61560
+ /************************************************************
60439
61561
  * How many nodes the term matched, from the graph child. Zero with a
60440
61562
  * term typed is an ANSWER and is shown; an empty box shows nothing.
60441
61563
  ************************************************************/
@@ -60628,6 +61750,16 @@ function create_gclass$3(gclass_name) {
60628
61750
  ac_find_nodes,
60629
61751
  null
60630
61752
  ],
61753
+ [
61754
+ "EV_EXPAND_ALL",
61755
+ ac_expand_all,
61756
+ null
61757
+ ],
61758
+ [
61759
+ "EV_COLLAPSE_ALL",
61760
+ ac_collapse_all,
61761
+ null
61762
+ ],
60631
61763
  [
60632
61764
  "EV_LAYOUT_AUTOSET",
60633
61765
  ac_layout_autoset,
@@ -60690,6 +61822,8 @@ function create_gclass$3(gclass_name) {
60690
61822
  ["EV_SET_OPERATION_MODE", 0],
60691
61823
  ["EV_SET_FOCUS_TOPIC", 0],
60692
61824
  ["EV_FIND_NODES", 0],
61825
+ ["EV_EXPAND_ALL", 0],
61826
+ ["EV_COLLAPSE_ALL", 0],
60693
61827
  ["EV_FIND_RESULT", 0],
60694
61828
  ["EV_LAYOUT_AUTOSET", 0],
60695
61829
  ["EV_TOGGLE_LEGEND", 0],