@yuneta/gobj-ui 7.23.65 → 7.23.67
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +58 -0
- package/dist/gobj-ui.cjs.js +1330 -186
- package/dist/gobj-ui.es.js +1330 -186
- package/package.json +1 -1
- package/src/c_g6_nodes_tree.js +1193 -256
- package/src/c_yui_treedb_graph.js +45 -2
- package/src/treedb_fold_model.js +592 -0
- package/src/treedb_fold_model.test.js +308 -0
|
@@ -37,6 +37,7 @@ import {
|
|
|
37
37
|
kw_get_dict_value,
|
|
38
38
|
gobj_short_name,
|
|
39
39
|
gobj_read_str_attr,
|
|
40
|
+
gobj_read_integer_attr,
|
|
40
41
|
gobj_destroy,
|
|
41
42
|
json_object_update,
|
|
42
43
|
is_object,
|
|
@@ -80,6 +81,7 @@ import {
|
|
|
80
81
|
import {yui_toolbar} from "./yui_toolbar.js";
|
|
81
82
|
import {attach_clear} from "./yui_inputs.js";
|
|
82
83
|
import {register_c_g6_nodes_tree} from "./c_g6_nodes_tree.js";
|
|
84
|
+
import {yui_graph_fold_items} from "./yui_graph_camera.js";
|
|
83
85
|
import {
|
|
84
86
|
removeChildElements,
|
|
85
87
|
disableElements,
|
|
@@ -107,6 +109,8 @@ SDATA(data_type_t.DTP_LIST, "operation_modes", 0,
|
|
|
107
109
|
'["reading", "operation", "writing", "edition"]',
|
|
108
110
|
"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."),
|
|
109
111
|
SDATA(data_type_t.DTP_BOOLEAN, "with_treedb_tables",0, false, "Include treedb tables"),
|
|
112
|
+
SDATA(data_type_t.DTP_INTEGER, "expand_depth", 0, 2, "Levels of the treedb open when it loads: 1 = the roots alone, 2 = the roots and their children. Forwarded to the G6 child"),
|
|
113
|
+
SDATA(data_type_t.DTP_INTEGER, "fold_page_size", 0, 24, "Children of one hook shown per page in the graph; a `+N` chip opens the next page. Forwarded to the G6 child"),
|
|
110
114
|
|
|
111
115
|
/*---------------- User last selections ----------------*/
|
|
112
116
|
SDATA(data_type_t.DTP_STRING, "operation_mode", sdata_flag_t.SDF_PERSIST, "reading", "Current operation mode (internal behaviour or role). Changed by the user trough the gui."),
|
|
@@ -240,6 +244,8 @@ function mt_create(gobj)
|
|
|
240
244
|
with_treedb_tables: priv.with_treedb_tables,
|
|
241
245
|
hook_port_position: "bottom",
|
|
242
246
|
fkey_port_position: "top",
|
|
247
|
+
expand_depth: gobj_read_integer_attr(gobj, "expand_depth"),
|
|
248
|
+
fold_page_size: gobj_read_integer_attr(gobj, "fold_page_size"),
|
|
243
249
|
},
|
|
244
250
|
gobj
|
|
245
251
|
);
|
|
@@ -563,6 +569,12 @@ function make_toolbar(gobj)
|
|
|
563
569
|
}
|
|
564
570
|
}],
|
|
565
571
|
|
|
572
|
+
/* The fold pair, the same two chevrons the JSON graph and the
|
|
573
|
+
* lazy tree use: the graph opens folded (see the G6 child), and
|
|
574
|
+
* these are the whole thing and the roots alone. Refresh is the
|
|
575
|
+
* way back to the default depth. */
|
|
576
|
+
...yui_graph_fold_items(gobj, gobj_read_str_attr(gobj, "wide")),
|
|
577
|
+
|
|
566
578
|
/* Which colour is which topic. The port colour of a node encodes
|
|
567
579
|
* the topic it links to, which is the whole point of the graph and
|
|
568
580
|
* which nothing on screen explained. Clicking an entry focuses that
|
|
@@ -2423,7 +2435,12 @@ function ac_legend_topic(gobj, event, kw, src)
|
|
|
2423
2435
|
* DEDUPED: a legend that only announced moved the URL and focused
|
|
2424
2436
|
* nothing. This is the same contract the topics view keeps, where
|
|
2425
2437
|
* clicking a topic shows it and says so. */
|
|
2426
|
-
|
|
2438
|
+
/* `reveal: "all"`: a click on the legend is "show me these", and
|
|
2439
|
+
* these means every record of the topic, however deep it is folded
|
|
2440
|
+
* away. The route that lands on a topic sends no `reveal` and gets
|
|
2441
|
+
* one page -- it fires on every load, and a whole topic there is
|
|
2442
|
+
* the pile back before anybody touched anything. */
|
|
2443
|
+
gobj_send_event(gobj, "EV_SET_FOCUS_TOPIC", {topic: next, reveal: "all"}, gobj);
|
|
2427
2444
|
gobj_publish_event(gobj, "EV_TOPIC_SELECTED", {topic: next});
|
|
2428
2445
|
return 0;
|
|
2429
2446
|
}
|
|
@@ -2466,6 +2483,28 @@ function ac_find_nodes(gobj, event, kw, src)
|
|
|
2466
2483
|
return 0;
|
|
2467
2484
|
}
|
|
2468
2485
|
|
|
2486
|
+
/************************************************************
|
|
2487
|
+
* The fold pair of the toolbar, forwarded to the G6 child,
|
|
2488
|
+
* which owns the tree.
|
|
2489
|
+
************************************************************/
|
|
2490
|
+
function ac_expand_all(gobj, event, kw, src)
|
|
2491
|
+
{
|
|
2492
|
+
let priv = gobj.priv;
|
|
2493
|
+
if(priv.gobj_nodes_tree) {
|
|
2494
|
+
gobj_send_event(priv.gobj_nodes_tree, "EV_EXPAND_ALL", {}, gobj);
|
|
2495
|
+
}
|
|
2496
|
+
return 0;
|
|
2497
|
+
}
|
|
2498
|
+
|
|
2499
|
+
function ac_collapse_all(gobj, event, kw, src)
|
|
2500
|
+
{
|
|
2501
|
+
let priv = gobj.priv;
|
|
2502
|
+
if(priv.gobj_nodes_tree) {
|
|
2503
|
+
gobj_send_event(priv.gobj_nodes_tree, "EV_COLLAPSE_ALL", {}, gobj);
|
|
2504
|
+
}
|
|
2505
|
+
return 0;
|
|
2506
|
+
}
|
|
2507
|
+
|
|
2469
2508
|
/************************************************************
|
|
2470
2509
|
* How many nodes the term matched, from the graph child. Zero with a
|
|
2471
2510
|
* term typed is an ANSWER and is shown; an empty box shows nothing.
|
|
@@ -2502,7 +2541,7 @@ function ac_set_focus_topic(gobj, event, kw, src)
|
|
|
2502
2541
|
gobj_send_event(
|
|
2503
2542
|
priv.gobj_nodes_tree,
|
|
2504
2543
|
"EV_FOCUS_TOPIC",
|
|
2505
|
-
{topic: priv.focus_topic},
|
|
2544
|
+
{topic: priv.focus_topic, reveal: (kw && kw.reveal) || "page"},
|
|
2506
2545
|
gobj
|
|
2507
2546
|
);
|
|
2508
2547
|
}
|
|
@@ -2602,6 +2641,8 @@ function create_gclass(gclass_name)
|
|
|
2602
2641
|
["EV_SET_OPERATION_MODE", ac_set_operation_mode, null],
|
|
2603
2642
|
["EV_SET_FOCUS_TOPIC", ac_set_focus_topic, null],
|
|
2604
2643
|
["EV_FIND_NODES", ac_find_nodes, null],
|
|
2644
|
+
["EV_EXPAND_ALL", ac_expand_all, null],
|
|
2645
|
+
["EV_COLLAPSE_ALL", ac_collapse_all, null],
|
|
2605
2646
|
["EV_LAYOUT_AUTOSET", ac_layout_autoset, null],
|
|
2606
2647
|
["EV_TOGGLE_LEGEND", ac_toggle_legend, null],
|
|
2607
2648
|
["EV_LEGEND_TOPIC", ac_legend_topic, null],
|
|
@@ -2641,6 +2682,8 @@ function create_gclass(gclass_name)
|
|
|
2641
2682
|
["EV_SET_OPERATION_MODE", 0],
|
|
2642
2683
|
["EV_SET_FOCUS_TOPIC", 0],
|
|
2643
2684
|
["EV_FIND_NODES", 0],
|
|
2685
|
+
["EV_EXPAND_ALL", 0],
|
|
2686
|
+
["EV_COLLAPSE_ALL", 0],
|
|
2644
2687
|
["EV_FIND_RESULT", 0],
|
|
2645
2688
|
["EV_LAYOUT_AUTOSET", 0],
|
|
2646
2689
|
["EV_TOGGLE_LEGEND", 0],
|
|
@@ -0,0 +1,592 @@
|
|
|
1
|
+
/***********************************************************************
|
|
2
|
+
* treedb_fold_model.js
|
|
3
|
+
*
|
|
4
|
+
* A treedb read as a TREE, and which part of it is on screen.
|
|
5
|
+
*
|
|
6
|
+
* A treedb is a graph: a record hangs from every parent one of its
|
|
7
|
+
* fkeys names, and a parent reaches its children through a hook.
|
|
8
|
+
* Drawn whole it is a pile -- a nave with a hundred and forty
|
|
9
|
+
* devices is a row of a hundred and forty cards -- so the graph
|
|
10
|
+
* opens FOLDED, the way a JSON viewer opens: the roots, a level or
|
|
11
|
+
* two under them, and a count on every cut saying what went with it.
|
|
12
|
+
*
|
|
13
|
+
* This module is the arithmetic of that, and nothing else: no G6,
|
|
14
|
+
* no DOM, no gobj. The graph engine (c_g6_nodes_tree.js) asks it
|
|
15
|
+
* what is visible and draws exactly that.
|
|
16
|
+
*
|
|
17
|
+
* Vocabulary
|
|
18
|
+
*
|
|
19
|
+
* node key "topic^id" -- one record
|
|
20
|
+
* group the children of ONE node through ONE hook,
|
|
21
|
+
* keyed "topic^id|hook"; the roots of a topic are
|
|
22
|
+
* a group of their own, keyed "|topic"
|
|
23
|
+
* shown how many of a group's children are on screen:
|
|
24
|
+
* absent means folded, a number means the first
|
|
25
|
+
* N, in record order, a page at a time
|
|
26
|
+
*
|
|
27
|
+
* Two rules that are not obvious:
|
|
28
|
+
*
|
|
29
|
+
* - A node is visible when SOME visible parent shows it, or it is
|
|
30
|
+
* a shown root. A record with two parents (a device hangs from
|
|
31
|
+
* its place AND its controller) is drawn ONCE, under whichever
|
|
32
|
+
* opened first, and folding one of the two leaves it where the
|
|
33
|
+
* other one shows it. The union, never the intersection.
|
|
34
|
+
*
|
|
35
|
+
* - A parent that the engine does not draw an edge to is not a
|
|
36
|
+
* parent here either: the `extended` tier (hooks and no fkeys)
|
|
37
|
+
* gets no edges in draw_link(), so its children are roots. The
|
|
38
|
+
* tree on screen and the tree in this model have to agree, or a
|
|
39
|
+
* pill counts children that no line reaches.
|
|
40
|
+
*
|
|
41
|
+
* Copyright (c) 2026, ArtGins.
|
|
42
|
+
* All Rights Reserved.
|
|
43
|
+
***********************************************************************/
|
|
44
|
+
import {
|
|
45
|
+
is_array,
|
|
46
|
+
is_object,
|
|
47
|
+
treedb_decoder_fkey,
|
|
48
|
+
} from "@yuneta/gobj-js";
|
|
49
|
+
|
|
50
|
+
const PAGE_SIZE_DEFAULT = 24;
|
|
51
|
+
|
|
52
|
+
/************************************************************
|
|
53
|
+
* Keys
|
|
54
|
+
************************************************************/
|
|
55
|
+
export function fold_node_key(topic_name, id)
|
|
56
|
+
{
|
|
57
|
+
return `${topic_name}^${id}`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function fold_group_key(node_key, hook_name)
|
|
61
|
+
{
|
|
62
|
+
return `${node_key}|${hook_name}`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function fold_root_group_key(topic_name)
|
|
66
|
+
{
|
|
67
|
+
return `|${topic_name}`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/* "topic^id|hook" -> {node_key, hook}; "|topic" -> {node_key: "", hook: topic} */
|
|
71
|
+
export function fold_split_group_key(group_key)
|
|
72
|
+
{
|
|
73
|
+
let i = group_key.lastIndexOf("|");
|
|
74
|
+
if(i < 0) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
return {node_key: group_key.slice(0, i), hook: group_key.slice(i + 1)};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/************************************************************
|
|
81
|
+
* Every parent a record names, decoded, in column order.
|
|
82
|
+
*
|
|
83
|
+
* Only the parents that will be DRAWN: a topic missing from
|
|
84
|
+
* `descs` (a system topic such as `__assets__`, never fetched)
|
|
85
|
+
* and an `extended` parent (no edge is drawn to one, see the
|
|
86
|
+
* header) are left out, so a child of nothing else is a root.
|
|
87
|
+
************************************************************/
|
|
88
|
+
function parents_of(descs, desc, record)
|
|
89
|
+
{
|
|
90
|
+
let out = [];
|
|
91
|
+
let cols = desc.cols || [];
|
|
92
|
+
|
|
93
|
+
for(let i = 0; i < cols.length; i++) {
|
|
94
|
+
let col = cols[i];
|
|
95
|
+
if(!col.fkey) {
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
let fkeys = record[col.id];
|
|
99
|
+
if(!fkeys) {
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
let list = is_array(fkeys)? fkeys : [fkeys];
|
|
103
|
+
for(let j = 0; j < list.length; j++) {
|
|
104
|
+
let ref = treedb_decoder_fkey(col, list[j]);
|
|
105
|
+
if(!ref || !ref.topic_name || !ref.hook_name) {
|
|
106
|
+
continue; /* Error already logged by the decoder */
|
|
107
|
+
}
|
|
108
|
+
let parent_desc = descs[ref.topic_name];
|
|
109
|
+
if(!parent_desc) {
|
|
110
|
+
continue; /* a topic that is not drawn: no parent here */
|
|
111
|
+
}
|
|
112
|
+
if(parent_desc.node_treedb_type === "extended") {
|
|
113
|
+
continue; /* the engine draws no edge to it, see draw_link */
|
|
114
|
+
}
|
|
115
|
+
out.push({
|
|
116
|
+
node_key: fold_node_key(ref.topic_name, String(ref.id)),
|
|
117
|
+
hook: ref.hook_name,
|
|
118
|
+
fkey_name: col.id,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return out;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/************************************************************
|
|
127
|
+
* Build the model from the topic descriptions and their records.
|
|
128
|
+
*
|
|
129
|
+
* descs {topic_name: desc}, system topics included (skipped)
|
|
130
|
+
* records {topic_name: [record, ...]}, in the order the
|
|
131
|
+
* backend returned them -- that order is the order of
|
|
132
|
+
* the children on screen, and of the pages
|
|
133
|
+
*
|
|
134
|
+
* Returns
|
|
135
|
+
* {
|
|
136
|
+
* nodes: Map<node_key, {key, topic_name, id, record,
|
|
137
|
+
* parents: [{node_key, hook, fkey_name}],
|
|
138
|
+
* hooks: {hook: [child_key, ...]}}>
|
|
139
|
+
* groups: Map<group_key, [child_key, ...]> (roots included)
|
|
140
|
+
* topics: [topic_name, ...] topics with records, in desc order
|
|
141
|
+
* }
|
|
142
|
+
************************************************************/
|
|
143
|
+
export function fold_build_model(descs, records)
|
|
144
|
+
{
|
|
145
|
+
let nodes = new Map();
|
|
146
|
+
let groups = new Map();
|
|
147
|
+
let topics = [];
|
|
148
|
+
|
|
149
|
+
descs = is_object(descs)? descs : {};
|
|
150
|
+
records = is_object(records)? records : {};
|
|
151
|
+
|
|
152
|
+
/* Pass 1: every record is a node. */
|
|
153
|
+
for(let topic_name of Object.keys(descs)) {
|
|
154
|
+
if(topic_name.substring(0, 2) === "__") {
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
let desc = descs[topic_name];
|
|
158
|
+
let list = records[topic_name];
|
|
159
|
+
if(!is_array(list)) {
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
topics.push(topic_name);
|
|
163
|
+
for(let i = 0; i < list.length; i++) {
|
|
164
|
+
let record = list[i];
|
|
165
|
+
if(!is_object(record) || record.id === undefined || record.id === null) {
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
let key = fold_node_key(topic_name, String(record.id));
|
|
169
|
+
nodes.set(key, {
|
|
170
|
+
key: key,
|
|
171
|
+
topic_name: topic_name,
|
|
172
|
+
id: String(record.id),
|
|
173
|
+
record: record,
|
|
174
|
+
parents: [],
|
|
175
|
+
hooks: {},
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/* Pass 2: the parents, and through them the children groups. A
|
|
181
|
+
* parent that names a record nobody fetched is not a parent. */
|
|
182
|
+
for(let node of nodes.values()) {
|
|
183
|
+
let desc = descs[node.topic_name];
|
|
184
|
+
let parents = parents_of(descs, desc, node.record);
|
|
185
|
+
for(let p of parents) {
|
|
186
|
+
let parent = nodes.get(p.node_key);
|
|
187
|
+
if(!parent) {
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
node.parents.push(p);
|
|
191
|
+
if(!parent.hooks[p.hook]) {
|
|
192
|
+
parent.hooks[p.hook] = [];
|
|
193
|
+
}
|
|
194
|
+
parent.hooks[p.hook].push(node.key);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/* Children groups, in parent order (Map order = insertion = record
|
|
199
|
+
* order within a topic, topics in desc order). */
|
|
200
|
+
for(let node of nodes.values()) {
|
|
201
|
+
for(let hook of Object.keys(node.hooks)) {
|
|
202
|
+
groups.set(fold_group_key(node.key, hook), node.hooks[hook]);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/* Roots: no drawable parent. Then whatever the roots cannot reach
|
|
207
|
+
* -- a cycle with no way in -- becomes a root too, so that every
|
|
208
|
+
* record has a place on screen to be opened from. */
|
|
209
|
+
let roots = {};
|
|
210
|
+
for(let topic_name of topics) {
|
|
211
|
+
roots[topic_name] = [];
|
|
212
|
+
}
|
|
213
|
+
for(let node of nodes.values()) {
|
|
214
|
+
if(node.parents.length === 0) {
|
|
215
|
+
roots[node.topic_name].push(node.key);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
let reached = reach_from(nodes, [].concat(...Object.values(roots)));
|
|
219
|
+
for(let node of nodes.values()) {
|
|
220
|
+
if(!reached.has(node.key)) {
|
|
221
|
+
roots[node.topic_name].push(node.key);
|
|
222
|
+
/* Its own subtree is reachable now; mark it so a whole
|
|
223
|
+
* cycle does not become a row of roots. */
|
|
224
|
+
for(let k of reach_from(nodes, [node.key])) {
|
|
225
|
+
reached.add(k);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
for(let topic_name of topics) {
|
|
230
|
+
if(roots[topic_name].length > 0) {
|
|
231
|
+
groups.set(fold_root_group_key(topic_name), roots[topic_name]);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return {nodes: nodes, groups: groups, topics: topics};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/************************************************************
|
|
239
|
+
* Everything reachable downwards from `start`, cycles included.
|
|
240
|
+
************************************************************/
|
|
241
|
+
function reach_from(nodes, start)
|
|
242
|
+
{
|
|
243
|
+
let seen = new Set();
|
|
244
|
+
let queue = start.slice();
|
|
245
|
+
while(queue.length) {
|
|
246
|
+
let key = queue.shift();
|
|
247
|
+
if(seen.has(key)) {
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
seen.add(key);
|
|
251
|
+
let node = nodes.get(key);
|
|
252
|
+
if(!node) {
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
for(let hook of Object.keys(node.hooks)) {
|
|
256
|
+
for(let child of node.hooks[hook]) {
|
|
257
|
+
if(!seen.has(child)) {
|
|
258
|
+
queue.push(child);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return seen;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/************************************************************
|
|
267
|
+
* Fold state: which groups are open, and how far.
|
|
268
|
+
************************************************************/
|
|
269
|
+
export function fold_new_state(page_size)
|
|
270
|
+
{
|
|
271
|
+
return {
|
|
272
|
+
page_size: (page_size > 0)? page_size : PAGE_SIZE_DEFAULT,
|
|
273
|
+
shown: new Map(), /* group_key -> number of children shown */
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export function fold_group_total(model, group_key)
|
|
278
|
+
{
|
|
279
|
+
let list = model.groups.get(group_key);
|
|
280
|
+
return list? list.length : 0;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export function fold_group_shown(model, state, group_key)
|
|
284
|
+
{
|
|
285
|
+
let total = fold_group_total(model, group_key);
|
|
286
|
+
let n = state.shown.get(group_key) || 0;
|
|
287
|
+
return Math.min(n, total);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export function fold_is_open(state, group_key)
|
|
291
|
+
{
|
|
292
|
+
return (state.shown.get(group_key) || 0) > 0;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/* The next page boundary at or above `n`: 25 -> 48 with pages of 24. */
|
|
296
|
+
function page_ceiling(state, n)
|
|
297
|
+
{
|
|
298
|
+
let p = state.page_size;
|
|
299
|
+
return Math.ceil(n / p) * p;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/************************************************************
|
|
303
|
+
* Open a group on its first page, or close it.
|
|
304
|
+
* Returns true when it ends OPEN.
|
|
305
|
+
************************************************************/
|
|
306
|
+
export function fold_toggle(model, state, group_key)
|
|
307
|
+
{
|
|
308
|
+
if(fold_is_open(state, group_key)) {
|
|
309
|
+
state.shown.delete(group_key);
|
|
310
|
+
return false;
|
|
311
|
+
}
|
|
312
|
+
if(fold_group_total(model, group_key) === 0) {
|
|
313
|
+
return false; /* nothing to open */
|
|
314
|
+
}
|
|
315
|
+
state.shown.set(group_key, state.page_size);
|
|
316
|
+
return true;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/************************************************************
|
|
320
|
+
* One more page of a group. Returns how many are shown now.
|
|
321
|
+
************************************************************/
|
|
322
|
+
export function fold_show_more(model, state, group_key)
|
|
323
|
+
{
|
|
324
|
+
let total = fold_group_total(model, group_key);
|
|
325
|
+
let n = fold_group_shown(model, state, group_key) + state.page_size;
|
|
326
|
+
n = Math.min(n, total);
|
|
327
|
+
if(n > 0) {
|
|
328
|
+
state.shown.set(group_key, n);
|
|
329
|
+
}
|
|
330
|
+
return n;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/************************************************************
|
|
334
|
+
* Every group open on ALL its children. This is the reader
|
|
335
|
+
* asking for the whole thing; the pages are for the other way in.
|
|
336
|
+
************************************************************/
|
|
337
|
+
export function fold_expand_all(model, state)
|
|
338
|
+
{
|
|
339
|
+
state.shown.clear();
|
|
340
|
+
for(let [group_key, list] of model.groups) {
|
|
341
|
+
if(list.length > 0) {
|
|
342
|
+
state.shown.set(group_key, list.length);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/************************************************************
|
|
348
|
+
* The roots and nothing else -- the first page of each topic's
|
|
349
|
+
* roots, the way the graph looks before anything is opened.
|
|
350
|
+
************************************************************/
|
|
351
|
+
export function fold_collapse_all(model, state)
|
|
352
|
+
{
|
|
353
|
+
state.shown.clear();
|
|
354
|
+
for(let topic_name of model.topics) {
|
|
355
|
+
let key = fold_root_group_key(topic_name);
|
|
356
|
+
if(fold_group_total(model, key) > 0) {
|
|
357
|
+
state.shown.set(key, state.page_size);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/************************************************************
|
|
363
|
+
* Open the tree down to `depth` levels: the roots are level 1,
|
|
364
|
+
* their children level 2. A node reached by two paths takes the
|
|
365
|
+
* shallowest. Each group opens on its first page. `depth` of 1
|
|
366
|
+
* is the roots alone; anything below that is treated as 1.
|
|
367
|
+
************************************************************/
|
|
368
|
+
export function fold_expand_to_depth(model, state, depth)
|
|
369
|
+
{
|
|
370
|
+
fold_collapse_all(model, state);
|
|
371
|
+
if(!(depth > 1)) {
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
let level = new Map();
|
|
376
|
+
let queue = [];
|
|
377
|
+
for(let topic_name of model.topics) {
|
|
378
|
+
let roots = model.groups.get(fold_root_group_key(topic_name)) || [];
|
|
379
|
+
for(let key of roots) {
|
|
380
|
+
if(!level.has(key)) {
|
|
381
|
+
level.set(key, 1);
|
|
382
|
+
queue.push(key);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
while(queue.length) {
|
|
387
|
+
let key = queue.shift();
|
|
388
|
+
let lv = level.get(key);
|
|
389
|
+
if(lv >= depth) {
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
let node = model.nodes.get(key);
|
|
393
|
+
if(!node) {
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
for(let hook of Object.keys(node.hooks)) {
|
|
397
|
+
let children = node.hooks[hook];
|
|
398
|
+
if(children.length === 0) {
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
state.shown.set(fold_group_key(key, hook), state.page_size);
|
|
402
|
+
for(let child of children) {
|
|
403
|
+
if(!level.has(child)) {
|
|
404
|
+
level.set(child, lv + 1);
|
|
405
|
+
queue.push(child);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/************************************************************
|
|
413
|
+
* The set of node keys on screen, from the roots down through
|
|
414
|
+
* every open group -- and only through open groups whose parent
|
|
415
|
+
* is itself on screen: a group remembered open under a folded
|
|
416
|
+
* ancestor stays remembered, not shown.
|
|
417
|
+
************************************************************/
|
|
418
|
+
export function fold_visible_set(model, state)
|
|
419
|
+
{
|
|
420
|
+
let visible = new Set();
|
|
421
|
+
let queue = [];
|
|
422
|
+
|
|
423
|
+
let push_group = (group_key, children) => {
|
|
424
|
+
let n = fold_group_shown(model, state, group_key);
|
|
425
|
+
for(let i = 0; i < n; i++) {
|
|
426
|
+
let key = children[i];
|
|
427
|
+
if(!visible.has(key)) {
|
|
428
|
+
visible.add(key);
|
|
429
|
+
queue.push(key);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
for(let topic_name of model.topics) {
|
|
435
|
+
let key = fold_root_group_key(topic_name);
|
|
436
|
+
let roots = model.groups.get(key);
|
|
437
|
+
if(roots) {
|
|
438
|
+
push_group(key, roots);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
while(queue.length) {
|
|
442
|
+
let key = queue.shift();
|
|
443
|
+
let node = model.nodes.get(key);
|
|
444
|
+
if(!node) {
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
447
|
+
for(let hook of Object.keys(node.hooks)) {
|
|
448
|
+
push_group(fold_group_key(key, hook), node.hooks[hook]);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
return visible;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/************************************************************
|
|
456
|
+
* The open groups that still have children off screen, with the
|
|
457
|
+
* parent that owns each -- what the "+N" chips are drawn for.
|
|
458
|
+
* Only groups whose parent is visible (or root groups).
|
|
459
|
+
*
|
|
460
|
+
* Returns [{group_key, node_key, hook, shown, total, rest}]
|
|
461
|
+
************************************************************/
|
|
462
|
+
export function fold_pending_groups(model, state, visible)
|
|
463
|
+
{
|
|
464
|
+
let out = [];
|
|
465
|
+
for(let [group_key, list] of model.groups) {
|
|
466
|
+
let shown = fold_group_shown(model, state, group_key);
|
|
467
|
+
if(shown === 0 || shown >= list.length) {
|
|
468
|
+
continue;
|
|
469
|
+
}
|
|
470
|
+
let g = fold_split_group_key(group_key);
|
|
471
|
+
if(g.node_key && !visible.has(g.node_key)) {
|
|
472
|
+
continue;
|
|
473
|
+
}
|
|
474
|
+
out.push({
|
|
475
|
+
group_key: group_key,
|
|
476
|
+
node_key: g.node_key,
|
|
477
|
+
hook: g.hook,
|
|
478
|
+
shown: shown,
|
|
479
|
+
total: list.length,
|
|
480
|
+
rest: list.length - shown,
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
return out;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/************************************************************
|
|
487
|
+
* The pills of one card: one per hook that has children.
|
|
488
|
+
*
|
|
489
|
+
* Returns [{hook, group_key, total, shown, open, child_topic}]
|
|
490
|
+
* `child_topic` is the topic of the first child, which is what
|
|
491
|
+
* the pill is coloured with (a hook can name several topics in
|
|
492
|
+
* the schema, but the children it has are what it links).
|
|
493
|
+
************************************************************/
|
|
494
|
+
export function fold_pills_of(model, state, node_key)
|
|
495
|
+
{
|
|
496
|
+
let node = model.nodes.get(node_key);
|
|
497
|
+
if(!node) {
|
|
498
|
+
return [];
|
|
499
|
+
}
|
|
500
|
+
let out = [];
|
|
501
|
+
for(let hook of Object.keys(node.hooks)) {
|
|
502
|
+
let children = node.hooks[hook];
|
|
503
|
+
if(children.length === 0) {
|
|
504
|
+
continue;
|
|
505
|
+
}
|
|
506
|
+
let group_key = fold_group_key(node_key, hook);
|
|
507
|
+
let first = model.nodes.get(children[0]);
|
|
508
|
+
out.push({
|
|
509
|
+
hook: hook,
|
|
510
|
+
group_key: group_key,
|
|
511
|
+
total: children.length,
|
|
512
|
+
shown: fold_group_shown(model, state, group_key),
|
|
513
|
+
open: fold_is_open(state, group_key),
|
|
514
|
+
child_topic: first? first.topic_name : "",
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
return out;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/************************************************************
|
|
521
|
+
* Make ONE node visible, opening the groups on a path from a
|
|
522
|
+
* root to it, and as many pages of each as it takes for the
|
|
523
|
+
* node to be inside the shown prefix. Prefers the shortest path
|
|
524
|
+
* (breadth-first upwards). Returns false if no root reaches it,
|
|
525
|
+
* which cannot happen with a model built here (see roots) but
|
|
526
|
+
* is the honest answer for a key the model does not know.
|
|
527
|
+
************************************************************/
|
|
528
|
+
export function fold_reveal(model, state, node_key)
|
|
529
|
+
{
|
|
530
|
+
let node = model.nodes.get(node_key);
|
|
531
|
+
if(!node) {
|
|
532
|
+
return false;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/* Upwards BFS: from the node to the first ancestor that is a
|
|
536
|
+
* root, remembering how each step was reached. */
|
|
537
|
+
let root_group_of = (key) => {
|
|
538
|
+
let n = model.nodes.get(key);
|
|
539
|
+
let rg = fold_root_group_key(n.topic_name);
|
|
540
|
+
let roots = model.groups.get(rg) || [];
|
|
541
|
+
return roots.indexOf(key) >= 0? rg : null;
|
|
542
|
+
};
|
|
543
|
+
|
|
544
|
+
let via = new Map(); /* key -> {parent_key, hook} */
|
|
545
|
+
let queue = [node_key];
|
|
546
|
+
via.set(node_key, null);
|
|
547
|
+
let root_key = null;
|
|
548
|
+
while(queue.length) {
|
|
549
|
+
let key = queue.shift();
|
|
550
|
+
if(root_group_of(key)) {
|
|
551
|
+
root_key = key;
|
|
552
|
+
break;
|
|
553
|
+
}
|
|
554
|
+
let n = model.nodes.get(key);
|
|
555
|
+
for(let p of n.parents) {
|
|
556
|
+
if(!model.nodes.has(p.node_key) || via.has(p.node_key)) {
|
|
557
|
+
continue;
|
|
558
|
+
}
|
|
559
|
+
via.set(p.node_key, {child_key: key, hook: p.hook});
|
|
560
|
+
queue.push(p.node_key);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
if(root_key === null) {
|
|
564
|
+
return false;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/* Walk back down, opening each group far enough. */
|
|
568
|
+
let open_up_to = (group_key, child_key) => {
|
|
569
|
+
let list = model.groups.get(group_key) || [];
|
|
570
|
+
let idx = list.indexOf(child_key);
|
|
571
|
+
if(idx < 0) {
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
let need = page_ceiling(state, idx + 1);
|
|
575
|
+
let have = state.shown.get(group_key) || 0;
|
|
576
|
+
if(need > have) {
|
|
577
|
+
state.shown.set(group_key, Math.min(need, list.length));
|
|
578
|
+
}
|
|
579
|
+
};
|
|
580
|
+
|
|
581
|
+
open_up_to(root_group_of(root_key), root_key);
|
|
582
|
+
let key = root_key;
|
|
583
|
+
while(true) {
|
|
584
|
+
let step = via.get(key);
|
|
585
|
+
if(!step) {
|
|
586
|
+
break;
|
|
587
|
+
}
|
|
588
|
+
open_up_to(fold_group_key(key, step.hook), step.child_key);
|
|
589
|
+
key = step.child_key;
|
|
590
|
+
}
|
|
591
|
+
return true;
|
|
592
|
+
}
|