@yuneta/gobj-ui 7.23.66 → 7.23.68
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 +66 -6
- package/dist/gobj-ui.cjs.js +454 -90
- package/dist/gobj-ui.es.js +455 -91
- package/package.json +1 -1
- package/src/c_g6_nodes_tree.js +160 -16
- package/src/c_yui_treedb_graph.js +268 -82
- package/src/treedb_fold_model.js +171 -15
- package/src/treedb_fold_model.test.js +120 -2
package/src/treedb_fold_model.js
CHANGED
|
@@ -140,7 +140,7 @@ function parents_of(descs, desc, record)
|
|
|
140
140
|
* topics: [topic_name, ...] topics with records, in desc order
|
|
141
141
|
* }
|
|
142
142
|
************************************************************/
|
|
143
|
-
export function fold_build_model(descs, records)
|
|
143
|
+
export function fold_build_model(descs, records, opts)
|
|
144
144
|
{
|
|
145
145
|
let nodes = new Map();
|
|
146
146
|
let groups = new Map();
|
|
@@ -148,8 +148,21 @@ export function fold_build_model(descs, records)
|
|
|
148
148
|
|
|
149
149
|
descs = is_object(descs)? descs : {};
|
|
150
150
|
records = is_object(records)? records : {};
|
|
151
|
+
opts = is_object(opts)? opts : {};
|
|
151
152
|
|
|
152
|
-
|
|
153
|
+
let hidden = new Set(is_array(opts.hidden_topics)? opts.hidden_topics : []);
|
|
154
|
+
let loose_shown = new Set(is_array(opts.loose_topics)? opts.loose_topics : []);
|
|
155
|
+
let main_topic = opts.main_topic || fold_main_topic(descs);
|
|
156
|
+
if(main_topic && (!descs[main_topic] || hidden.has(main_topic))) {
|
|
157
|
+
main_topic = ""; /* a main topic that is not drawn governs nothing */
|
|
158
|
+
}
|
|
159
|
+
let linked = fold_linked_topics(descs, main_topic);
|
|
160
|
+
let totals = {};
|
|
161
|
+
|
|
162
|
+
/* Pass 1: every record is a node -- of the topics that are SHOWN.
|
|
163
|
+
* A hidden topic leaves the model whole: no pills count it, no
|
|
164
|
+
* edge reaches it, and the records that hung from it alone become
|
|
165
|
+
* roots, the way any record with no drawable parent does. */
|
|
153
166
|
for(let topic_name of Object.keys(descs)) {
|
|
154
167
|
if(topic_name.substring(0, 2) === "__") {
|
|
155
168
|
continue;
|
|
@@ -159,6 +172,10 @@ export function fold_build_model(descs, records)
|
|
|
159
172
|
if(!is_array(list)) {
|
|
160
173
|
continue;
|
|
161
174
|
}
|
|
175
|
+
totals[topic_name] = list.length;
|
|
176
|
+
if(hidden.has(topic_name)) {
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
162
179
|
topics.push(topic_name);
|
|
163
180
|
for(let i = 0; i < list.length; i++) {
|
|
164
181
|
let record = list[i];
|
|
@@ -203,28 +220,60 @@ export function fold_build_model(descs, records)
|
|
|
203
220
|
}
|
|
204
221
|
}
|
|
205
222
|
|
|
206
|
-
/* Roots: no drawable parent
|
|
207
|
-
*
|
|
208
|
-
*
|
|
223
|
+
/* Roots: no drawable parent -- with one distinction when there is
|
|
224
|
+
* a MAIN topic. A record of a topic the schema hangs from the main
|
|
225
|
+
* one (`linked`) that has no parent is LOOSE: a device with no
|
|
226
|
+
* place. It should hang and does not, so it is not a root of the
|
|
227
|
+
* tree; it is counted per topic and shown only on request
|
|
228
|
+
* (`loose_topics`). A topic the schema does not tie to the main
|
|
229
|
+
* one at all is a tree of its own, and its parentless records are
|
|
230
|
+
* its roots, as before.
|
|
231
|
+
*
|
|
232
|
+
* Then whatever the roots cannot reach -- a cycle with no way in
|
|
233
|
+
* -- becomes a root too, so that every record has a place on
|
|
234
|
+
* screen to be opened from. */
|
|
209
235
|
let roots = {};
|
|
236
|
+
let loose = {};
|
|
210
237
|
for(let topic_name of topics) {
|
|
211
238
|
roots[topic_name] = [];
|
|
239
|
+
loose[topic_name] = [];
|
|
212
240
|
}
|
|
213
241
|
for(let node of nodes.values()) {
|
|
214
|
-
if(node.parents.length
|
|
215
|
-
|
|
242
|
+
if(node.parents.length > 0) {
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
let t = node.topic_name;
|
|
246
|
+
if(main_topic && t !== main_topic && linked.has(t)) {
|
|
247
|
+
loose[t].push(node.key);
|
|
248
|
+
if(loose_shown.has(t)) {
|
|
249
|
+
roots[t].push(node.key);
|
|
250
|
+
}
|
|
251
|
+
} else {
|
|
252
|
+
roots[t].push(node.key);
|
|
216
253
|
}
|
|
217
254
|
}
|
|
218
255
|
let reached = reach_from(nodes, [].concat(...Object.values(roots)));
|
|
219
256
|
for(let node of nodes.values()) {
|
|
220
|
-
if(
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
257
|
+
if(reached.has(node.key)) {
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
let t = node.topic_name;
|
|
261
|
+
if(main_topic && t !== main_topic && linked.has(t)) {
|
|
262
|
+
/* Reachable through a loose record that is not shown, or
|
|
263
|
+
* through a hidden topic: loose as well, not a root. */
|
|
264
|
+
if(node.parents.length > 0) {
|
|
265
|
+
loose[t].push(node.key);
|
|
266
|
+
}
|
|
267
|
+
if(!loose_shown.has(t)) {
|
|
268
|
+
continue;
|
|
226
269
|
}
|
|
227
270
|
}
|
|
271
|
+
roots[t].push(node.key);
|
|
272
|
+
/* Its own subtree is reachable now; mark it so a whole
|
|
273
|
+
* cycle does not become a row of roots. */
|
|
274
|
+
for(let k of reach_from(nodes, [node.key])) {
|
|
275
|
+
reached.add(k);
|
|
276
|
+
}
|
|
228
277
|
}
|
|
229
278
|
for(let topic_name of topics) {
|
|
230
279
|
if(roots[topic_name].length > 0) {
|
|
@@ -232,7 +281,103 @@ export function fold_build_model(descs, records)
|
|
|
232
281
|
}
|
|
233
282
|
}
|
|
234
283
|
|
|
235
|
-
return {
|
|
284
|
+
return {
|
|
285
|
+
nodes: nodes,
|
|
286
|
+
groups: groups,
|
|
287
|
+
topics: topics,
|
|
288
|
+
totals: totals, /* {topic: records}, hidden topics included */
|
|
289
|
+
hidden: hidden, /* Set of hidden topics */
|
|
290
|
+
main_topic: main_topic, /* "" when nothing governs the tree */
|
|
291
|
+
linked: linked, /* Set of topics the schema hangs from main */
|
|
292
|
+
loose: loose, /* {topic: [key, ...]} records that should hang and do not */
|
|
293
|
+
loose_shown: loose_shown,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/************************************************************
|
|
298
|
+
* The topic the schema hangs the others from: the one whose hooks
|
|
299
|
+
* reach the most OTHER topics, a self-referent hook breaking a tie
|
|
300
|
+
* (a tree of places over a flat list of groups), schema order
|
|
301
|
+
* breaking the rest. "" when no topic reaches another -- a treedb of
|
|
302
|
+
* unrelated lists has no trunk, and every topic is a tree of its own.
|
|
303
|
+
************************************************************/
|
|
304
|
+
export function fold_main_topic(descs)
|
|
305
|
+
{
|
|
306
|
+
descs = is_object(descs)? descs : {};
|
|
307
|
+
let best = "";
|
|
308
|
+
let best_score = 0;
|
|
309
|
+
let best_self = false;
|
|
310
|
+
|
|
311
|
+
for(let topic_name of Object.keys(descs)) {
|
|
312
|
+
if(topic_name.substring(0, 2) === "__") {
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
let reach = hook_targets(descs, topic_name);
|
|
316
|
+
let self = reach.has(topic_name);
|
|
317
|
+
reach.delete(topic_name);
|
|
318
|
+
let score = reach.size;
|
|
319
|
+
if(score === 0) {
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
if(score > best_score || (score === best_score && self && !best_self)) {
|
|
323
|
+
best = topic_name;
|
|
324
|
+
best_score = score;
|
|
325
|
+
best_self = self;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return best;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/* The topics a topic's hooks name (drawn topics only). An `extended`
|
|
332
|
+
* topic names nothing: the engine draws no edge from one, so its
|
|
333
|
+
* records hang nothing and it can govern no tree. */
|
|
334
|
+
function hook_targets(descs, topic_name)
|
|
335
|
+
{
|
|
336
|
+
let out = new Set();
|
|
337
|
+
let desc = descs[topic_name];
|
|
338
|
+
if(!desc || desc.node_treedb_type === "extended") {
|
|
339
|
+
return out;
|
|
340
|
+
}
|
|
341
|
+
let cols = is_array(desc.cols)? desc.cols : [];
|
|
342
|
+
for(let col of cols) {
|
|
343
|
+
if(!is_object(col.hook) || !is_array(col.flag) || col.flag.indexOf("hook") < 0) {
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
for(let child of Object.keys(col.hook)) {
|
|
347
|
+
if(child.substring(0, 2) !== "__" && descs[child]) {
|
|
348
|
+
out.add(child);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return out;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/************************************************************
|
|
356
|
+
* The topics the schema hangs from `main_topic`, main included:
|
|
357
|
+
* reached by following hooks from it. A record of one of these
|
|
358
|
+
* with no parent is LOOSE; a topic outside the set is its own tree.
|
|
359
|
+
* Empty when there is no main topic.
|
|
360
|
+
************************************************************/
|
|
361
|
+
export function fold_linked_topics(descs, main_topic)
|
|
362
|
+
{
|
|
363
|
+
let linked = new Set();
|
|
364
|
+
if(!main_topic || !is_object(descs) || !descs[main_topic]) {
|
|
365
|
+
return linked;
|
|
366
|
+
}
|
|
367
|
+
let queue = [main_topic];
|
|
368
|
+
while(queue.length) {
|
|
369
|
+
let t = queue.shift();
|
|
370
|
+
if(linked.has(t)) {
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
linked.add(t);
|
|
374
|
+
for(let child of hook_targets(descs, t)) {
|
|
375
|
+
if(!linked.has(child)) {
|
|
376
|
+
queue.push(child);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
return linked;
|
|
236
381
|
}
|
|
237
382
|
|
|
238
383
|
/************************************************************
|
|
@@ -283,7 +428,18 @@ export function fold_group_total(model, group_key)
|
|
|
283
428
|
export function fold_group_shown(model, state, group_key)
|
|
284
429
|
{
|
|
285
430
|
let total = fold_group_total(model, group_key);
|
|
286
|
-
let n
|
|
431
|
+
let n;
|
|
432
|
+
if(state.shown.has(group_key)) {
|
|
433
|
+
n = state.shown.get(group_key);
|
|
434
|
+
} else if(group_key.charAt(0) === "|") {
|
|
435
|
+
/* A ROOT group nobody has set is open on its first page: no
|
|
436
|
+
* pill folds the roots, so "unset" cannot mean "folded" -- a
|
|
437
|
+
* topic shown again after being hidden, or one that appears
|
|
438
|
+
* when the main topic moves, would otherwise draw nothing. */
|
|
439
|
+
n = state.page_size;
|
|
440
|
+
} else {
|
|
441
|
+
n = 0;
|
|
442
|
+
}
|
|
287
443
|
return Math.min(n, total);
|
|
288
444
|
}
|
|
289
445
|
|
|
@@ -24,6 +24,8 @@ import {
|
|
|
24
24
|
fold_group_key,
|
|
25
25
|
fold_root_group_key,
|
|
26
26
|
fold_split_group_key,
|
|
27
|
+
fold_main_topic,
|
|
28
|
+
fold_group_shown,
|
|
27
29
|
} from "./treedb_fold_model.js";
|
|
28
30
|
|
|
29
31
|
const places_desc = {
|
|
@@ -133,12 +135,21 @@ describe("the model", () => {
|
|
|
133
135
|
expect(m.groups.get(fold_root_group_key("device_groups"))).toEqual([K("device_groups", "g1")]);
|
|
134
136
|
});
|
|
135
137
|
|
|
136
|
-
test("a child of an `extended` parent only
|
|
138
|
+
test("a child of an `extended` parent only has no parent: the engine draws no edge to one", () => {
|
|
137
139
|
let recs = make_records(1);
|
|
138
140
|
recs.devices[0] = {id: "d0", group: "device_groups^g1^members"};
|
|
139
141
|
let m = fold_build_model(descs, recs);
|
|
140
|
-
expect(m.groups.get(fold_root_group_key("devices"))).toEqual([K("devices", "d0")]);
|
|
141
142
|
expect(m.nodes.get(K("device_groups", "g1")).hooks).toEqual({});
|
|
143
|
+
/* devices hang from places (the main topic), so a device with
|
|
144
|
+
* no parent is LOOSE, not a root -- see the "main topic" block */
|
|
145
|
+
expect(m.loose.devices).toEqual([K("devices", "d0")]);
|
|
146
|
+
expect(m.groups.get(fold_root_group_key("devices"))).toBeUndefined();
|
|
147
|
+
/* and with no main topic it is a plain root */
|
|
148
|
+
let m2 = fold_build_model({places: places_desc, devices: devices_desc, device_groups: groups_desc}, recs, {main_topic: ""});
|
|
149
|
+
expect(m2.main_topic).toBe("places"); /* "" means "deduce", and places reaches devices */
|
|
150
|
+
let m3 = fold_build_model({devices: devices_desc, device_groups: groups_desc}, recs);
|
|
151
|
+
expect(m3.main_topic).toBe("");
|
|
152
|
+
expect(m3.groups.get(fold_root_group_key("devices"))).toEqual([K("devices", "d0")]);
|
|
142
153
|
});
|
|
143
154
|
|
|
144
155
|
test("a cycle with no way in gets one root, not a row of them", () => {
|
|
@@ -164,6 +175,113 @@ describe("the model", () => {
|
|
|
164
175
|
});
|
|
165
176
|
});
|
|
166
177
|
|
|
178
|
+
describe("main topic, hidden topics, loose records", () => {
|
|
179
|
+
test("the main topic is the one whose hooks reach the most others; places, not device_groups", () => {
|
|
180
|
+
expect(fold_main_topic(descs)).toBe("places");
|
|
181
|
+
let m = fold_build_model(descs, make_records(3));
|
|
182
|
+
expect(m.main_topic).toBe("places");
|
|
183
|
+
expect([...m.linked].sort()).toEqual(["controllers", "devices", "places"]);
|
|
184
|
+
expect(m.linked.has("device_groups")).toBe(false);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test("a self-referent hook breaks a tie", () => {
|
|
188
|
+
let a = {topic_name: "a", node_treedb_type: "extended",
|
|
189
|
+
cols: [{id: "id", flag: []}, {id: "xs", flag: ["hook"], hook: {x: "a"}}]};
|
|
190
|
+
let b = {topic_name: "b", node_treedb_type: "hierarchical",
|
|
191
|
+
cols: [{id: "id", flag: []}, {id: "xs", flag: ["hook"], hook: {x: "b"}},
|
|
192
|
+
{id: "bs", flag: ["hook"], hook: {b: "parent"}},
|
|
193
|
+
{id: "parent", flag: ["fkey"], fkey: {b: "bs"}}]};
|
|
194
|
+
let x = {topic_name: "x", node_treedb_type: "child", cols: [{id: "id", flag: []}]};
|
|
195
|
+
expect(fold_main_topic({a: a, b: b, x: x})).toBe("b");
|
|
196
|
+
expect(fold_main_topic({x: x})).toBe("");
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test("an explicit main topic wins over the deduced one", () => {
|
|
200
|
+
let m = fold_build_model(descs, make_records(3), {main_topic: "controllers"});
|
|
201
|
+
expect(m.main_topic).toBe("controllers");
|
|
202
|
+
expect([...m.linked].sort()).toEqual(["controllers", "devices"]);
|
|
203
|
+
/* places is not linked to controllers, so its roots are roots */
|
|
204
|
+
expect(m.groups.get(fold_root_group_key("places"))).toEqual([K("places", "es")]);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test("a device with no place is LOOSE: counted, not a root, unless asked for", () => {
|
|
208
|
+
let recs = make_records(3);
|
|
209
|
+
recs.devices.push({id: "d_loose"});
|
|
210
|
+
let m = fold_build_model(descs, recs);
|
|
211
|
+
expect(m.loose.devices).toEqual([K("devices", "d_loose")]);
|
|
212
|
+
expect(m.groups.get(fold_root_group_key("devices"))).toBeUndefined();
|
|
213
|
+
expect(m.totals.devices).toBe(4);
|
|
214
|
+
let m2 = fold_build_model(descs, recs, {loose_topics: ["devices"]});
|
|
215
|
+
expect(m2.groups.get(fold_root_group_key("devices"))).toEqual([K("devices", "d_loose")]);
|
|
216
|
+
expect(m2.loose.devices).toEqual([K("devices", "d_loose")]);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test("a device hanging from a loose controller only is loose too, and follows it", () => {
|
|
220
|
+
let recs = make_records(0);
|
|
221
|
+
recs.controllers.push({id: "c_loose"});
|
|
222
|
+
recs.devices.push({id: "d_under", controller: ["controllers^c_loose^devices"]});
|
|
223
|
+
let m = fold_build_model(descs, recs);
|
|
224
|
+
expect(m.loose.controllers).toEqual([K("controllers", "c_loose")]);
|
|
225
|
+
expect(m.loose.devices).toEqual([K("devices", "d_under")]);
|
|
226
|
+
let st = fold_new_state(24);
|
|
227
|
+
fold_expand_all(m, st);
|
|
228
|
+
expect(fold_visible_set(m, st).has(K("devices", "d_under"))).toBe(false);
|
|
229
|
+
let m2 = fold_build_model(descs, recs, {loose_topics: ["controllers"]});
|
|
230
|
+
fold_expand_all(m2, st);
|
|
231
|
+
expect(fold_visible_set(m2, st).has(K("devices", "d_under"))).toBe(true);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
test("a hidden topic leaves the model whole: no node, no pill, no edge", () => {
|
|
235
|
+
let m = fold_build_model(descs, make_records(30), {hidden_topics: ["devices"]});
|
|
236
|
+
expect(m.topics).toEqual(["places", "controllers", "device_groups"]);
|
|
237
|
+
expect(m.totals.devices).toBe(30);
|
|
238
|
+
expect(m.nodes.get(K("places", "nave1")).hooks.devices).toBeUndefined();
|
|
239
|
+
expect(fold_pills_of(m, fold_new_state(24), K("places", "nave1")).map((p) => p.hook))
|
|
240
|
+
.toEqual(["controllers"]);
|
|
241
|
+
expect(m.hidden.has("devices")).toBe(true);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
test("hiding a topic in the middle: what hung from it alone becomes loose", () => {
|
|
245
|
+
let m = fold_build_model(descs, make_records(12), {hidden_topics: ["controllers"]});
|
|
246
|
+
/* d0..d9 hang from nave1 AND c1: still under nave1 */
|
|
247
|
+
expect(m.nodes.get(K("devices", "d0")).parents.length).toBe(1);
|
|
248
|
+
expect(m.loose.devices).toEqual([]);
|
|
249
|
+
/* places hidden = the main topic gone: nothing governs, and
|
|
250
|
+
* whatever has no parent left is a root -- c1, with d0 and d1
|
|
251
|
+
* still hanging from it */
|
|
252
|
+
let m2 = fold_build_model(descs, make_records(2), {hidden_topics: ["places"]});
|
|
253
|
+
expect(m2.main_topic).toBe("");
|
|
254
|
+
expect(m2.groups.get(fold_root_group_key("controllers"))).toEqual([K("controllers", "c1")]);
|
|
255
|
+
expect(m2.groups.get(fold_root_group_key("devices"))).toBeUndefined();
|
|
256
|
+
let m3 = fold_build_model(descs, {devices: [{id: "d9"}]}, {hidden_topics: ["places"]});
|
|
257
|
+
expect(m3.groups.get(fold_root_group_key("devices"))).toEqual([K("devices", "d9")]);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
test("a topic shown again opens on its roots' first page: nobody folds the roots", () => {
|
|
261
|
+
let recs = make_records(3);
|
|
262
|
+
let st = fold_new_state(24);
|
|
263
|
+
let m = fold_build_model(descs, recs, {hidden_topics: ["device_groups"]});
|
|
264
|
+
fold_expand_to_depth(m, st, 2);
|
|
265
|
+
expect(fold_visible_set(m, st).has(K("device_groups", "g1"))).toBe(false);
|
|
266
|
+
let m2 = fold_build_model(descs, recs);
|
|
267
|
+
expect(fold_visible_set(m2, st).has(K("device_groups", "g1"))).toBe(true);
|
|
268
|
+
expect(fold_pending_groups(m2, st, fold_visible_set(m2, st))).toEqual([]);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
test("the fold state survives a hide and a show", () => {
|
|
272
|
+
let recs = make_records(30);
|
|
273
|
+
let st = fold_new_state(24);
|
|
274
|
+
let m = fold_build_model(descs, recs);
|
|
275
|
+
fold_expand_to_depth(m, st, 4);
|
|
276
|
+
let g = fold_group_key(K("places", "nave1"), "devices");
|
|
277
|
+
expect(fold_group_shown(m, st, g)).toBe(24);
|
|
278
|
+
let m_hidden = fold_build_model(descs, recs, {hidden_topics: ["devices"]});
|
|
279
|
+
expect(fold_group_shown(m_hidden, st, g)).toBe(0); /* no group: nothing shown */
|
|
280
|
+
let m_back = fold_build_model(descs, recs);
|
|
281
|
+
expect(fold_group_shown(m_back, st, g)).toBe(24); /* remembered */
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
|
|
167
285
|
describe("depth", () => {
|
|
168
286
|
test("depth 1 is the roots alone", () => {
|
|
169
287
|
let m = fold_build_model(descs, make_records(30));
|