@yuneta/gobj-ui 7.23.139 → 7.23.141
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +99 -26
- package/dist/gobj-ui.cjs.js +384 -112
- package/dist/gobj-ui.es.js +384 -112
- package/package.json +2 -2
- package/src/c_g6_nodes_tree.js +75 -46
- package/src/c_yui_node.js +6 -0
- package/src/c_yui_shell.js +54 -0
- package/src/c_yui_treedb_graph.js +146 -21
- package/src/c_yui_treedb_topics.js +62 -16
- package/src/treedb_fold_model.js +133 -1
- package/src/treedb_fold_model.test.js +80 -0
|
@@ -58,7 +58,12 @@ import {
|
|
|
58
58
|
import "./c_yui_treedb_topics.css";
|
|
59
59
|
|
|
60
60
|
import {yui_shell_show_error, yui_shell_show_modal, yui_shell_popup_layer} from "./shell_modals.js";
|
|
61
|
-
import {
|
|
61
|
+
import {
|
|
62
|
+
yui_shell_of,
|
|
63
|
+
yui_shell_set_sub_routes,
|
|
64
|
+
yui_shell_navigate,
|
|
65
|
+
yui_shell_last_route_under,
|
|
66
|
+
} from "./c_yui_shell.js";
|
|
62
67
|
import {nodes_answer} from "./nodes_answer.js";
|
|
63
68
|
import {yui_toolbar_icon} from "./yui_toolbar.js";
|
|
64
69
|
|
|
@@ -254,26 +259,28 @@ function build_ui(gobj)
|
|
|
254
259
|
{
|
|
255
260
|
let source_url = gobj_read_str_attr(gobj, "source_url") || "";
|
|
256
261
|
|
|
257
|
-
/* The
|
|
258
|
-
*
|
|
259
|
-
*
|
|
260
|
-
*
|
|
261
|
-
*
|
|
262
|
-
*
|
|
263
|
-
let
|
|
264
|
-
let graph_tpl = is_object(routes) && typeof routes.graph === "string"? routes.graph : "";
|
|
265
|
-
let graph_href = /\/\{topic\}$/.test(graph_tpl)? graph_tpl.replace(/\/\{topic\}$/, "") : "";
|
|
262
|
+
/* The treedb as a graph, AS THE READER LEFT IT. The route is the
|
|
263
|
+
* card template without its `/{topic}`; a host whose template does
|
|
264
|
+
* not end in it, or gives none, gets no button. Not a fixed link:
|
|
265
|
+
* the graph's focus lives in its url (`<graph>/<topic>`), so the
|
|
266
|
+
* bare route took it away every time -- the click asks the shell
|
|
267
|
+
* where the reader last was under that route (ac_open_graph). */
|
|
268
|
+
let graph_href = graph_route_of(gobj);
|
|
266
269
|
let graph_items = [];
|
|
267
270
|
if(graph_href) {
|
|
268
271
|
graph_items.push(
|
|
269
|
-
['
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
'data-i18n-title': 'graph', 'data-i18n-aria-label': 'graph'}, [
|
|
272
|
+
['button', {class: 'button TREEDB_GRAPH_BTN',
|
|
273
|
+
style: 'margin-left:auto;',
|
|
274
|
+
title: t('graph'), 'aria-label': t('graph'),
|
|
275
|
+
'data-i18n-title': 'graph', 'data-i18n-aria-label': 'graph'}, [
|
|
274
276
|
['span', {class: 'icon'}, [yui_toolbar_icon('yi-hexagon-nodes')]],
|
|
275
277
|
['span', {i18n: 'graph'}, 'graph']
|
|
276
|
-
]
|
|
278
|
+
], {
|
|
279
|
+
click: (evt) => {
|
|
280
|
+
evt.stopPropagation();
|
|
281
|
+
gobj_send_event(gobj, "EV_OPEN_GRAPH", {}, gobj);
|
|
282
|
+
}
|
|
283
|
+
}]
|
|
277
284
|
);
|
|
278
285
|
}
|
|
279
286
|
|
|
@@ -389,6 +396,21 @@ function build_ui(gobj)
|
|
|
389
396
|
}
|
|
390
397
|
}
|
|
391
398
|
|
|
399
|
+
/************************************************************
|
|
400
|
+
* The graph's route with no topic: the host's card template
|
|
401
|
+
* `card_action_routes.graph` without its trailing `/{topic}`,
|
|
402
|
+
* as a ROUTE (no `#`). "" when the host gives no such template.
|
|
403
|
+
************************************************************/
|
|
404
|
+
function graph_route_of(gobj)
|
|
405
|
+
{
|
|
406
|
+
let routes = gobj_read_attr(gobj, "card_action_routes");
|
|
407
|
+
let tpl = (is_object(routes) && typeof routes.graph === "string")? routes.graph : "";
|
|
408
|
+
if(!/\/\{topic\}$/.test(tpl)) {
|
|
409
|
+
return "";
|
|
410
|
+
}
|
|
411
|
+
return tpl.replace(/\/\{topic\}$/, "").replace(/^#/, "");
|
|
412
|
+
}
|
|
413
|
+
|
|
392
414
|
/************************************************************
|
|
393
415
|
* Is the backend session live? The Raw JSON viewer issues a remote
|
|
394
416
|
* print-tranger, so it only makes sense with a session — the remote
|
|
@@ -2476,6 +2498,28 @@ function ac_open_json(gobj, event, kw, src)
|
|
|
2476
2498
|
return 0;
|
|
2477
2499
|
}
|
|
2478
2500
|
|
|
2501
|
+
/********************************************
|
|
2502
|
+
* Open the graph where the reader left it: its last visited
|
|
2503
|
+
* route under the graph's base -- `<graph>/<topic>` when a topic
|
|
2504
|
+
* was focused there -- or the bare base when the graph was never
|
|
2505
|
+
* opened in this page. A user move: pushed, so Back returns.
|
|
2506
|
+
********************************************/
|
|
2507
|
+
function ac_open_graph(gobj, event, kw, src)
|
|
2508
|
+
{
|
|
2509
|
+
let base = graph_route_of(gobj);
|
|
2510
|
+
if(!base) {
|
|
2511
|
+
log_error(`${gobj_short_name(gobj)}: open graph with no graph route`);
|
|
2512
|
+
return -1;
|
|
2513
|
+
}
|
|
2514
|
+
let shell = yui_shell_of(gobj);
|
|
2515
|
+
if(!shell) {
|
|
2516
|
+
log_error(`${gobj_short_name(gobj)}: open graph with no shell`);
|
|
2517
|
+
return -1;
|
|
2518
|
+
}
|
|
2519
|
+
yui_shell_navigate(shell, yui_shell_last_route_under(shell, base), {push: true});
|
|
2520
|
+
return 0;
|
|
2521
|
+
}
|
|
2522
|
+
|
|
2479
2523
|
/********************************************
|
|
2480
2524
|
* The viewer asked to load a collapsed subtree (tranger mode only):
|
|
2481
2525
|
* re-issue print-tranger for that path.
|
|
@@ -2556,6 +2600,7 @@ function create_gclass(gclass_name)
|
|
|
2556
2600
|
["EV_DELETE_RECORD", ac_delete_record, null],
|
|
2557
2601
|
["EV_REFRESH_TOPIC", ac_refresh_topic, null],
|
|
2558
2602
|
["EV_OPEN_JSON", ac_open_json, null],
|
|
2603
|
+
["EV_OPEN_GRAPH", ac_open_graph, null],
|
|
2559
2604
|
["EV_EXPAND_PATH", ac_json_expand_path, null],
|
|
2560
2605
|
["EV_JSON_CLOSED", ac_json_closed, null],
|
|
2561
2606
|
["EV_SHOW", ac_show, null],
|
|
@@ -2586,6 +2631,7 @@ function create_gclass(gclass_name)
|
|
|
2586
2631
|
event_flag_t.EVF_NO_WARN_SUBS],
|
|
2587
2632
|
["EV_REFRESH_TOPIC", 0],
|
|
2588
2633
|
["EV_OPEN_JSON", 0],
|
|
2634
|
+
["EV_OPEN_GRAPH", 0],
|
|
2589
2635
|
["EV_EXPAND_PATH", 0],
|
|
2590
2636
|
["EV_JSON_CLOSED", 0],
|
|
2591
2637
|
["EV_SHOW", 0],
|
package/src/treedb_fold_model.js
CHANGED
|
@@ -281,7 +281,7 @@ export function fold_build_model(descs, records, opts)
|
|
|
281
281
|
}
|
|
282
282
|
}
|
|
283
283
|
|
|
284
|
-
|
|
284
|
+
let model = {
|
|
285
285
|
nodes: nodes,
|
|
286
286
|
groups: groups,
|
|
287
287
|
topics: topics,
|
|
@@ -291,7 +291,67 @@ export function fold_build_model(descs, records, opts)
|
|
|
291
291
|
linked: linked, /* Set of topics the schema hangs from main */
|
|
292
292
|
loose: loose, /* {topic: [key, ...]} records that should hang and do not */
|
|
293
293
|
loose_shown: loose_shown,
|
|
294
|
+
level_of: null, /* Map<node_key, level> of the MAIN tree, see below */
|
|
295
|
+
levels: 0, /* its deepest level (0 = no tree to step through) */
|
|
294
296
|
};
|
|
297
|
+
count_levels(model);
|
|
298
|
+
return model;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/************************************************************
|
|
302
|
+
* The LEVELS of the main tree: the roots of the main topic --
|
|
303
|
+
* and of the topics hung from it, whose roots are the loose
|
|
304
|
+
* records the reader asked for -- are level 1, their children
|
|
305
|
+
* level 2, and so on down every hook of every topic that is not
|
|
306
|
+
* hidden. A node reached by two paths takes the shallowest.
|
|
307
|
+
*
|
|
308
|
+
* With no main topic every topic is a tree of its own and all
|
|
309
|
+
* of them count. A topic the schema does not tie to the main
|
|
310
|
+
* one is a separate tree, and has no level: the stepper of the
|
|
311
|
+
* toolbar speaks for the main tree and nothing else, those
|
|
312
|
+
* trees are opened by their own pills.
|
|
313
|
+
************************************************************/
|
|
314
|
+
function count_levels(model)
|
|
315
|
+
{
|
|
316
|
+
let level_of = new Map();
|
|
317
|
+
let queue = [];
|
|
318
|
+
let deepest = 0;
|
|
319
|
+
|
|
320
|
+
for(let topic_name of model.topics) {
|
|
321
|
+
if(model.main_topic && !model.linked.has(topic_name)) {
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
let roots = model.groups.get(fold_root_group_key(topic_name)) || [];
|
|
325
|
+
for(let key of roots) {
|
|
326
|
+
if(!level_of.has(key)) {
|
|
327
|
+
level_of.set(key, 1);
|
|
328
|
+
queue.push(key);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
let head = 0;
|
|
333
|
+
while(head < queue.length) {
|
|
334
|
+
let key = queue[head++];
|
|
335
|
+
let lv = level_of.get(key);
|
|
336
|
+
if(lv > deepest) {
|
|
337
|
+
deepest = lv;
|
|
338
|
+
}
|
|
339
|
+
let node = model.nodes.get(key);
|
|
340
|
+
if(!node) {
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
for(let hook of Object.keys(node.hooks)) {
|
|
344
|
+
for(let child of node.hooks[hook]) {
|
|
345
|
+
if(!level_of.has(child)) {
|
|
346
|
+
level_of.set(child, lv + 1);
|
|
347
|
+
queue.push(child);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
model.level_of = level_of;
|
|
354
|
+
model.levels = deepest;
|
|
295
355
|
}
|
|
296
356
|
|
|
297
357
|
/************************************************************
|
|
@@ -565,6 +625,78 @@ export function fold_expand_to_depth(model, state, depth)
|
|
|
565
625
|
}
|
|
566
626
|
}
|
|
567
627
|
|
|
628
|
+
/************************************************************
|
|
629
|
+
* The toolbar's STEPPER over the levels of the main tree (see
|
|
630
|
+
* count_levels): level N shows the roots and N-1 levels under
|
|
631
|
+
* them. A level is a floor of what is open, not a picture that
|
|
632
|
+
* replaces it:
|
|
633
|
+
*
|
|
634
|
+
* opening to N opens, on its first page, every hook of
|
|
635
|
+
* every node above level N that is folded;
|
|
636
|
+
* what the reader opened, and every page
|
|
637
|
+
* already shown, stays
|
|
638
|
+
* closing to N folds every hook of every node at level N
|
|
639
|
+
* or below, and touches nothing above it
|
|
640
|
+
*
|
|
641
|
+
* A separate tree (a topic not tied to the main one) has no
|
|
642
|
+
* level, so neither ever touches it.
|
|
643
|
+
************************************************************/
|
|
644
|
+
export function fold_level_clamp(model, level)
|
|
645
|
+
{
|
|
646
|
+
let top = (model.levels > 0)? model.levels : 1;
|
|
647
|
+
level = Math.floor(Number(level)) || 1;
|
|
648
|
+
if(level < 1) {
|
|
649
|
+
return 1;
|
|
650
|
+
}
|
|
651
|
+
if(level > top) {
|
|
652
|
+
return top;
|
|
653
|
+
}
|
|
654
|
+
return level;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
export function fold_open_levels(model, state, level)
|
|
658
|
+
{
|
|
659
|
+
for(let [key, lv] of model.level_of) {
|
|
660
|
+
if(lv >= level) {
|
|
661
|
+
continue;
|
|
662
|
+
}
|
|
663
|
+
let node = model.nodes.get(key);
|
|
664
|
+
if(!node) {
|
|
665
|
+
continue;
|
|
666
|
+
}
|
|
667
|
+
for(let hook of Object.keys(node.hooks)) {
|
|
668
|
+
let group_key = fold_group_key(key, hook);
|
|
669
|
+
if(node.hooks[hook].length > 0 && !fold_is_open(state, group_key)) {
|
|
670
|
+
state.shown.set(group_key, state.page_size);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
export function fold_close_levels(model, state, level)
|
|
677
|
+
{
|
|
678
|
+
for(let [key, lv] of model.level_of) {
|
|
679
|
+
if(lv < level) {
|
|
680
|
+
continue;
|
|
681
|
+
}
|
|
682
|
+
let node = model.nodes.get(key);
|
|
683
|
+
if(!node) {
|
|
684
|
+
continue;
|
|
685
|
+
}
|
|
686
|
+
for(let hook of Object.keys(node.hooks)) {
|
|
687
|
+
state.shown.delete(fold_group_key(key, hook));
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
/* From scratch: the roots of every tree on their first page, and
|
|
693
|
+
* the main tree open down to `level`. What a load opens. */
|
|
694
|
+
export function fold_expand_to_level(model, state, level)
|
|
695
|
+
{
|
|
696
|
+
fold_collapse_all(model, state);
|
|
697
|
+
fold_open_levels(model, state, fold_level_clamp(model, level));
|
|
698
|
+
}
|
|
699
|
+
|
|
568
700
|
/************************************************************
|
|
569
701
|
* The set of node keys on screen, from the roots down through
|
|
570
702
|
* every open group -- and only through open groups whose parent
|
|
@@ -26,6 +26,11 @@ import {
|
|
|
26
26
|
fold_split_group_key,
|
|
27
27
|
fold_main_topic,
|
|
28
28
|
fold_group_shown,
|
|
29
|
+
fold_is_open,
|
|
30
|
+
fold_level_clamp,
|
|
31
|
+
fold_open_levels,
|
|
32
|
+
fold_close_levels,
|
|
33
|
+
fold_expand_to_level,
|
|
29
34
|
} from "./treedb_fold_model.js";
|
|
30
35
|
|
|
31
36
|
const places_desc = {
|
|
@@ -323,6 +328,81 @@ describe("depth", () => {
|
|
|
323
328
|
});
|
|
324
329
|
});
|
|
325
330
|
|
|
331
|
+
describe("levels of the main tree (the toolbar's stepper)", () => {
|
|
332
|
+
test("levels count from the main topic's roots, down every hook", () => {
|
|
333
|
+
let m = fold_build_model(descs, make_records(30));
|
|
334
|
+
/* es 1, norte/sur 2, nave1 3, its devices and c1 4 */
|
|
335
|
+
expect(m.levels).toBe(4);
|
|
336
|
+
expect(m.level_of.get(K("places", "es"))).toBe(1);
|
|
337
|
+
expect(m.level_of.get(K("places", "nave1"))).toBe(3);
|
|
338
|
+
/* d0 hangs from nave1 (3) and c1 (4): the shallowest wins */
|
|
339
|
+
expect(m.level_of.get(K("devices", "d0"))).toBe(4);
|
|
340
|
+
/* device_groups is not tied to places: a tree of its own, no level */
|
|
341
|
+
expect(m.level_of.has(K("device_groups", "g1"))).toBe(false);
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
test("a hidden topic takes its levels with it", () => {
|
|
345
|
+
let m = fold_build_model(descs, make_records(30), {hidden_topics: ["devices", "controllers"]});
|
|
346
|
+
expect(m.levels).toBe(3);
|
|
347
|
+
let m0 = fold_build_model({}, {});
|
|
348
|
+
expect(m0.levels).toBe(0);
|
|
349
|
+
expect(fold_level_clamp(m0, 5)).toBe(1);
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
test("clamp keeps a level between 1 and the deepest one", () => {
|
|
353
|
+
let m = fold_build_model(descs, make_records(3));
|
|
354
|
+
expect(fold_level_clamp(m, 0)).toBe(1);
|
|
355
|
+
expect(fold_level_clamp(m, 3)).toBe(3);
|
|
356
|
+
expect(fold_level_clamp(m, 9)).toBe(4);
|
|
357
|
+
expect(fold_level_clamp(m, "2")).toBe(2);
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
test("level 1 is the roots of every tree, and each step opens one more", () => {
|
|
361
|
+
let m = fold_build_model(descs, make_records(30));
|
|
362
|
+
let st = fold_new_state(24);
|
|
363
|
+
fold_expand_to_level(m, st, 1);
|
|
364
|
+
expect([...fold_visible_set(m, st)].sort())
|
|
365
|
+
.toEqual([K("device_groups", "g1"), K("places", "es")]);
|
|
366
|
+
fold_open_levels(m, st, 2);
|
|
367
|
+
let v = fold_visible_set(m, st);
|
|
368
|
+
expect(v.has(K("places", "norte"))).toBe(true);
|
|
369
|
+
expect(v.has(K("places", "nave1"))).toBe(false);
|
|
370
|
+
fold_open_levels(m, st, 3);
|
|
371
|
+
expect(fold_visible_set(m, st).has(K("places", "nave1"))).toBe(true);
|
|
372
|
+
expect(fold_visible_set(m, st).has(K("devices", "d0"))).toBe(false);
|
|
373
|
+
fold_open_levels(m, st, 4);
|
|
374
|
+
v = fold_visible_set(m, st);
|
|
375
|
+
expect(v.has(K("devices", "d23"))).toBe(true);
|
|
376
|
+
expect(v.has(K("devices", "d24"))).toBe(false); /* one page */
|
|
377
|
+
expect(v.has(K("controllers", "c1"))).toBe(true);
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
test("opening keeps the pages already shown; closing folds only from its level down", () => {
|
|
381
|
+
let m = fold_build_model(descs, make_records(30));
|
|
382
|
+
let st = fold_new_state(24);
|
|
383
|
+
fold_expand_to_level(m, st, 4);
|
|
384
|
+
let g = fold_group_key(K("places", "nave1"), "devices");
|
|
385
|
+
fold_show_more(m, st, g); /* 30 */
|
|
386
|
+
fold_open_levels(m, st, 4);
|
|
387
|
+
expect(fold_group_shown(m, st, g)).toBe(30);
|
|
388
|
+
fold_close_levels(m, st, 3);
|
|
389
|
+
let v = fold_visible_set(m, st);
|
|
390
|
+
expect(v.has(K("places", "nave1"))).toBe(true); /* level 3 stays */
|
|
391
|
+
expect(v.has(K("devices", "d0"))).toBe(false); /* its hooks folded */
|
|
392
|
+
expect(v.has(K("controllers", "c1"))).toBe(false);
|
|
393
|
+
expect(fold_is_open(st, fold_group_key(K("places", "norte"), "places"))).toBe(true);
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
test("closing does not reopen what the reader folded above the level", () => {
|
|
397
|
+
let m = fold_build_model(descs, make_records(3));
|
|
398
|
+
let st = fold_new_state(24);
|
|
399
|
+
fold_expand_to_level(m, st, 4);
|
|
400
|
+
fold_toggle(m, st, fold_group_key(K("places", "es"), "places")); /* fold es */
|
|
401
|
+
fold_close_levels(m, st, 3);
|
|
402
|
+
expect(fold_is_open(st, fold_group_key(K("places", "es"), "places"))).toBe(false);
|
|
403
|
+
});
|
|
404
|
+
});
|
|
405
|
+
|
|
326
406
|
describe("toggle and pages", () => {
|
|
327
407
|
test("a folded group opens on its first page and closes again", () => {
|
|
328
408
|
let m = fold_build_model(descs, make_records(30));
|