@yuneta/gobj-ui 7.23.67 → 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.
@@ -115,6 +115,9 @@ SDATA(data_type_t.DTP_INTEGER, "fold_page_size", 0, 24, "Children of one
115
115
  /*---------------- User last selections ----------------*/
116
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."),
117
117
  SDATA(data_type_t.DTP_STRING, "layout", sdata_flag_t.SDF_PERSIST, "", "Current graph layout. User preference. Changed by the user through the gui."),
118
+ SDATA(data_type_t.DTP_LIST, "hidden_topics", sdata_flag_t.SDF_PERSIST, "[]", "Topics hidden from the graph (legend). User preference, per treedb"),
119
+ SDATA(data_type_t.DTP_STRING, "main_topic", sdata_flag_t.SDF_PERSIST, "", "The topic the tree hangs from; empty = deduced by the graph. User preference, per treedb"),
120
+ SDATA(data_type_t.DTP_LIST, "loose_topics", sdata_flag_t.SDF_PERSIST, "[]", "Topics whose LOOSE records (no parent) are shown. User preference, per treedb"),
118
121
 
119
122
  /*---------------- Remote Connection ----------------*/
120
123
  SDATA(data_type_t.DTP_POINTER, "gobj_remote_yuno", 0, null, "Remote Yuno to request data"),
@@ -165,6 +168,7 @@ let PRIVATE_DATA = {
165
168
  operation_mode: null,
166
169
  layout: null,
167
170
  _topics_subscribed: {},
171
+ _legend_state: null, /* the engine's last EV_LEGEND_STATE */
168
172
  _links_subscribed: false, /* EV_TREEDB_NODE_LINKED/UNLINKED (treedb-wide) */
169
173
 
170
174
  is_pinhold_window: false, // inherited of v6, todo review
@@ -246,6 +250,9 @@ function mt_create(gobj)
246
250
  fkey_port_position: "top",
247
251
  expand_depth: gobj_read_integer_attr(gobj, "expand_depth"),
248
252
  fold_page_size: gobj_read_integer_attr(gobj, "fold_page_size"),
253
+ hidden_topics: gobj_read_attr(gobj, "hidden_topics") || [],
254
+ main_topic: gobj_read_str_attr(gobj, "main_topic") || "",
255
+ loose_topics: gobj_read_attr(gobj, "loose_topics") || [],
249
256
  },
250
257
  gobj
251
258
  );
@@ -363,13 +370,18 @@ function build_ui(gobj)
363
370
  // Don't use is-flex, don't work well with is-hidden
364
371
  ['div', {class: 'C_YUI_TREEDB_GRAPH', style: `height:100%; display:flex; flex-direction:column;`}, [
365
372
  ['div', {class: 'GRAPH_TOOLBAR_ROW is-flex-grow-0 is-flex is-align-items-center'}, row_items],
366
- /* Topic colour legend: a strip, not an overlay. It is opened to
367
- * be READ against the graph, and an overlay would cover the
368
- * thing it explains. Built on first open (the colours are the
369
- * child's, assigned when the schema arrives). */
370
- ['div', {class: 'GRAPH_LEGEND is-hidden is-flex-grow-0',
371
- style: 'display:flex; flex-wrap:wrap; align-items:center; ' +
372
- 'gap:.4rem; padding:.35rem .5rem;'}, []],
373
+ /* The legend: a strip under the toolbar, ALWAYS there, one
374
+ * chip per topic. It is the graph's layer control, not a
375
+ * colour key: a chip shows or hides its topic, marks the main
376
+ * one, shows the loose records, highlights. Painted from the
377
+ * engine's EV_LEGEND_STATE, which is the truth about what is
378
+ * drawn. A strip and not an overlay because it is read
379
+ * AGAINST the graph, and an overlay would cover the thing it
380
+ * explains. Scrolls sideways on a narrow screen. */
381
+ ['div', {class: 'GRAPH_LEGEND is-flex-grow-0',
382
+ style: 'display:flex; flex-wrap:nowrap; align-items:center; ' +
383
+ 'gap:.35rem; padding:.3rem .5rem; overflow-x:auto; ' +
384
+ 'min-height:2.2rem;'}, []],
373
385
  ['div', {class: `GRAPH_BODY is-flex-grow-1 ${padding}`, style: 'height:100%; min-height:0; overflow:hidden;'}, [
374
386
  ['div', {id: priv.canvas_id, class: `GRAPH_CANVAS graph-container`, style: 'height:100%; min-height:0;border: 1px solid var(--bulma-border-weak);border-radius:0.2rem;'}, [
375
387
  ]]
@@ -575,23 +587,6 @@ function make_toolbar(gobj)
575
587
  * way back to the default depth. */
576
588
  ...yui_graph_fold_items(gobj, gobj_read_str_attr(gobj, "wide")),
577
589
 
578
- /* Which colour is which topic. The port colour of a node encodes
579
- * the topic it links to, which is the whole point of the graph and
580
- * which nothing on screen explained. Clicking an entry focuses that
581
- * topic, so the legend is also the way to say "show me these" —
582
- * and clicking the focused one again clears it. */
583
- ['button', {class: 'GRAPH_LEGEND_BTN button ml-2',
584
- title: t('legend'), 'aria-label': t('legend'),
585
- 'data-i18n-title': 'legend', 'data-i18n-aria-label': 'legend'}, [
586
- ['i', {class: 'yi-square'}],
587
- ['span', {class: 'is-hidden-mobile', style: 'padding-left:5px;', i18n: 'legend'}, 'legend']
588
- ], {
589
- click: (evt) => {
590
- evt.stopPropagation();
591
- gobj_send_event(gobj, "EV_TOGGLE_LEGEND", {}, gobj);
592
- }
593
- }],
594
-
595
590
  /* Inspect the treedb's raw tranger json in the lazy tree viewer
596
591
  * (print-tranger on the C_NODE service). A treedb can be huge, so
597
592
  * the viewer drills in on demand — see open_json_viewer. */
@@ -2345,72 +2340,194 @@ function refresh_legend(gobj)
2345
2340
  return;
2346
2341
  }
2347
2342
 
2348
- let descs = priv.gobj_nodes_tree?
2349
- gobj_read_attr(priv.gobj_nodes_tree, "descs") : null;
2343
+ let state = priv._legend_state;
2350
2344
  $legend.textContent = "";
2351
- if(!descs) {
2352
- return;
2345
+ if(!state || !is_array(state.topics)) {
2346
+ return; /* nothing drawn yet: the engine has not spoken */
2353
2347
  }
2354
2348
 
2355
- for(const [topic_name, desc] of Object.entries(descs)) {
2356
- if(topic_name.substring(0, 2) === "__") {
2357
- continue; /* the internal topics are not drawn */
2349
+ /* The main topic leads the strip: it is the trunk everything else
2350
+ * hangs from, and the eye reads the strip left to right. */
2351
+ let topics = state.topics.slice().sort((a, b) => {
2352
+ let am = (a.topic === state.main_topic)? 0 : 1;
2353
+ let bm = (b.topic === state.main_topic)? 0 : 1;
2354
+ return am - bm;
2355
+ });
2356
+
2357
+ for(let entry of topics) {
2358
+ let topic_name = entry.topic;
2359
+ let is_main = (topic_name === state.main_topic);
2360
+ let focused = (priv.focus_topic === topic_name);
2361
+ let hidden = !!entry.hidden;
2362
+
2363
+ /* A control with a small, named target for each of the things
2364
+ * the strip can do, so a finger lands on ONE of them:
2365
+ * the body show / hide (the main topic cannot be hidden)
2366
+ * ★ make it the main topic
2367
+ * +N show / hide its loose records
2368
+ * ⌖ highlight (the focus)
2369
+ * The names are DATA: a topic name carries no i18n key and is
2370
+ * never translated; the counts are numbers. */
2371
+ let controls = [];
2372
+
2373
+ controls.push(
2374
+ ['span', {class: 'GRAPH_LEGEND_SWATCH',
2375
+ style: `display:inline-block; width:.8rem; height:.8rem; ` +
2376
+ `border-radius:3px; margin-right:.35rem; flex:0 0 auto; ` +
2377
+ `background:${entry.color || "#94a3b8"}; ` +
2378
+ `border:1px solid rgba(0,0,0,.25);` +
2379
+ (hidden? 'opacity:.35;' : '')}],
2380
+ ['span', {class: 'GRAPH_LEGEND_NAME',
2381
+ style: hidden? 'text-decoration:line-through; opacity:.6;' : ''},
2382
+ topic_name],
2383
+ ['span', {class: 'GRAPH_LEGEND_COUNT has-text-grey',
2384
+ style: 'margin-left:.35rem; font-size:.8em;'},
2385
+ hidden? `${entry.total}` : `${entry.visible}/${entry.total}`]
2386
+ );
2387
+
2388
+ /* Built as an object first: an attribute that is `undefined`
2389
+ * is still SET by createElement2 (to the string "undefined"),
2390
+ * and a `disabled="undefined"` button is a disabled button --
2391
+ * every chip was dead except its star. */
2392
+ let body_attrs = {
2393
+ class: 'GRAPH_LEGEND_ITEM button is-small' +
2394
+ (focused? ' is-primary is-light' : '') +
2395
+ (is_main? ' GRAPH_LEGEND_MAIN' : ''),
2396
+ type: 'button',
2397
+ style: 'gap:0; flex:0 0 auto;',
2398
+ title: is_main? t('main topic') : (hidden? t('show topic') : t('hide topic')),
2399
+ 'aria-label': topic_name,
2400
+ 'aria-pressed': hidden? 'false' : 'true',
2401
+ };
2402
+ if(is_main) {
2403
+ body_attrs.disabled = 'disabled';
2358
2404
  }
2359
- if(!desc || !desc.color) {
2360
- continue;
2405
+ let $body = createElement2(
2406
+ ['button', body_attrs, controls, {
2407
+ click: (evt) => {
2408
+ evt.stopPropagation();
2409
+ gobj_send_event(gobj, "EV_LEGEND_TOPIC",
2410
+ {topic: topic_name, action: "toggle"}, gobj);
2411
+ }
2412
+ }]
2413
+ );
2414
+
2415
+ let extras = [];
2416
+
2417
+ /* The star: filled on the main one, hollow on the others,
2418
+ * where it moves the star. On a main topic the READER chose it
2419
+ * is a button too: pressing it hands the choice back to the
2420
+ * graph (deduced again). On a deduced one it is a mark. */
2421
+ if(is_main && state.main_chosen) {
2422
+ extras.push(['button', {class: 'GRAPH_LEGEND_STAR button is-small is-warning is-light',
2423
+ type: 'button', style: 'padding:0 .4rem;',
2424
+ title: t('main topic'), 'data-i18n-title': 'main topic',
2425
+ 'aria-label': t('main topic'), 'aria-pressed': 'true'},
2426
+ '★', {
2427
+ click: (evt) => {
2428
+ evt.stopPropagation();
2429
+ gobj_send_event(gobj, "EV_LEGEND_TOPIC",
2430
+ {topic: "", action: "main"}, gobj);
2431
+ }
2432
+ }]);
2433
+ } else if(is_main) {
2434
+ extras.push(['span', {class: 'GRAPH_LEGEND_STAR button is-small is-static',
2435
+ style: 'padding:0 .4rem;',
2436
+ title: t('main topic'), 'data-i18n-title': 'main topic'},
2437
+ '★']);
2438
+ } else if(!hidden) {
2439
+ extras.push(['button', {class: 'GRAPH_LEGEND_STAR button is-small',
2440
+ type: 'button', style: 'padding:0 .4rem;',
2441
+ title: t('main topic'), 'data-i18n-title': 'main topic',
2442
+ 'aria-label': t('main topic')},
2443
+ '☆', {
2444
+ click: (evt) => {
2445
+ evt.stopPropagation();
2446
+ gobj_send_event(gobj, "EV_LEGEND_TOPIC",
2447
+ {topic: topic_name, action: "main"}, gobj);
2448
+ }
2449
+ }]);
2361
2450
  }
2362
- let focused = (priv.focus_topic === topic_name);
2363
- let $item = createElement2(
2364
- ['button', {
2365
- /* `is-light` on the focused one, not solid `is-primary`:
2366
- * the solid fill sat right behind the swatch and swallowed
2367
- * the one colour the entry exists to show. */
2368
- class: 'GRAPH_LEGEND_ITEM button is-small' +
2451
+
2452
+ /* Loose records: the ones that should hang from the main tree
2453
+ * and do not. Only where there are any, and only for a topic
2454
+ * the schema hangs from the main one. */
2455
+ if(!hidden && entry.linked && entry.loose > 0) {
2456
+ extras.push(['button', {
2457
+ class: 'GRAPH_LEGEND_LOOSE button is-small' +
2458
+ (entry.loose_shown? ' is-warning is-light' : ''),
2459
+ type: 'button', style: 'padding:0 .45rem;',
2460
+ title: t('loose records'), 'data-i18n-title': 'loose records',
2461
+ 'aria-label': t('loose records'),
2462
+ 'aria-pressed': entry.loose_shown? 'true' : 'false'
2463
+ }, `+${entry.loose}`, {
2464
+ click: (evt) => {
2465
+ evt.stopPropagation();
2466
+ gobj_send_event(gobj, "EV_LEGEND_TOPIC",
2467
+ {topic: topic_name, action: "loose"}, gobj);
2468
+ }
2469
+ }]);
2470
+ }
2471
+
2472
+ if(!hidden) {
2473
+ extras.push(['button', {
2474
+ class: 'GRAPH_LEGEND_FOCUS button is-small' +
2369
2475
  (focused? ' is-primary is-light' : ''),
2370
- title: topic_name,
2371
- 'aria-label': topic_name,
2476
+ type: 'button', style: 'padding:0 .4rem;',
2477
+ title: t('highlight topic'), 'data-i18n-title': 'highlight topic',
2478
+ 'aria-label': t('highlight topic'),
2372
2479
  'aria-pressed': focused? 'true' : 'false'
2373
- }, [
2374
- ['span', {class: 'GRAPH_LEGEND_SWATCH',
2375
- style: `display:inline-block; width:.8rem; height:.8rem; ` +
2376
- `border-radius:3px; margin-right:.4rem; ` +
2377
- `background:${desc.color}; ` +
2378
- `border:1px solid rgba(0,0,0,.25);`}],
2379
- /* The topic name is DATA: it carries no i18n key and is
2380
- * never translated. */
2381
- ['span', {}, topic_name]
2382
- ], {
2480
+ }, [['i', {class: 'yi-location-crosshairs'}]], {
2383
2481
  click: (evt) => {
2384
2482
  evt.stopPropagation();
2385
- gobj_send_event(gobj, "EV_LEGEND_TOPIC", {topic: topic_name}, gobj);
2483
+ gobj_send_event(gobj, "EV_LEGEND_TOPIC",
2484
+ {topic: topic_name, action: "focus"}, gobj);
2386
2485
  }
2387
- }]
2486
+ }]);
2487
+ }
2488
+
2489
+ let $chip = createElement2(
2490
+ ['div', {class: 'GRAPH_LEGEND_CHIP buttons has-addons',
2491
+ style: 'margin:0; flex:0 0 auto; flex-wrap:nowrap;'},
2492
+ [$body].concat(extras.map((spec) => createElement2(spec)))]
2388
2493
  );
2389
- $legend.appendChild($item);
2494
+ $legend.appendChild($chip);
2390
2495
  }
2391
2496
  }
2392
2497
 
2498
+ /************************************************************
2499
+ * The engine said what is drawn: repaint the strip.
2500
+ ************************************************************/
2501
+ function ac_legend_state(gobj, event, kw, src)
2502
+ {
2503
+ let priv = gobj.priv;
2504
+ priv._legend_state = kw || null;
2505
+ refresh_legend(gobj);
2506
+ return 0;
2507
+ }
2508
+
2393
2509
  /************************************************************
2394
2510
  *
2395
2511
  ************************************************************/
2396
- function ac_toggle_legend(gobj, event, kw, src)
2512
+ /************************************************************
2513
+ * One of the legend's settings changed: write it, SAVE it (they
2514
+ * are the reader's, per treedb) and tell the engine.
2515
+ ************************************************************/
2516
+ function set_legend_list_attr(gobj, name, event_name, topics)
2397
2517
  {
2398
- let $container = gobj_read_attr(gobj, "$container");
2399
- if(!$container) {
2400
- return 0;
2401
- }
2402
- let $legend = $container.querySelector(".GRAPH_LEGEND");
2403
- if(!$legend) {
2404
- log_error(`${gobj_short_name(gobj)}: no legend strip to toggle`);
2405
- return -1;
2406
- }
2407
- if($legend.classList.contains("is-hidden")) {
2408
- refresh_legend(gobj);
2409
- $legend.classList.remove("is-hidden");
2410
- } else {
2411
- $legend.classList.add("is-hidden");
2518
+ let priv = gobj.priv;
2519
+
2520
+ gobj_write_attr(gobj, name, topics);
2521
+ gobj_save_persistent_attrs(gobj, name);
2522
+ if(priv.gobj_nodes_tree) {
2523
+ gobj_send_event(priv.gobj_nodes_tree, event_name, {topics: topics}, gobj);
2412
2524
  }
2413
- return 0;
2525
+ }
2526
+
2527
+ function legend_list_attr(gobj, name)
2528
+ {
2529
+ let list = gobj_read_attr(gobj, name);
2530
+ return is_array(list)? list.slice() : [];
2414
2531
  }
2415
2532
 
2416
2533
  /************************************************************
@@ -2422,11 +2539,60 @@ function ac_legend_topic(gobj, event, kw, src)
2422
2539
  {
2423
2540
  let priv = gobj.priv;
2424
2541
  let topic = (kw && kw.topic) || "";
2425
- if(!topic) {
2542
+ let action = (kw && kw.action) || "focus";
2543
+ if(!topic && action !== "main") {
2426
2544
  log_error(`${gobj_short_name(gobj)}: legend click with no topic`);
2427
2545
  return -1;
2428
2546
  }
2429
2547
 
2548
+ let state = priv._legend_state || {};
2549
+
2550
+ switch(action) {
2551
+ case "toggle": {
2552
+ /* Show or hide the topic. Never the main one: hiding the
2553
+ * trunk turns everything into roots, which is the pile. */
2554
+ if(topic === state.main_topic) {
2555
+ log_error(`${gobj_short_name(gobj)}: the main topic '${topic}' cannot be hidden`);
2556
+ return -1;
2557
+ }
2558
+ let hidden = legend_list_attr(gobj, "hidden_topics");
2559
+ if(str_in_list(hidden, topic)) {
2560
+ delete_from_list(hidden, topic);
2561
+ } else {
2562
+ hidden.push(topic);
2563
+ }
2564
+ set_legend_list_attr(gobj, "hidden_topics", "EV_SET_HIDDEN_TOPICS", hidden);
2565
+ return 0;
2566
+ }
2567
+
2568
+ case "loose": {
2569
+ let loose = legend_list_attr(gobj, "loose_topics");
2570
+ if(str_in_list(loose, topic)) {
2571
+ delete_from_list(loose, topic);
2572
+ } else {
2573
+ loose.push(topic);
2574
+ }
2575
+ set_legend_list_attr(gobj, "loose_topics", "EV_SET_LOOSE_TOPICS", loose);
2576
+ return 0;
2577
+ }
2578
+
2579
+ case "main": {
2580
+ gobj_write_str_attr(gobj, "main_topic", topic);
2581
+ gobj_save_persistent_attrs(gobj, "main_topic");
2582
+ if(priv.gobj_nodes_tree) {
2583
+ gobj_send_event(priv.gobj_nodes_tree, "EV_SET_MAIN_TOPIC", {topic: topic}, gobj);
2584
+ }
2585
+ return 0;
2586
+ }
2587
+
2588
+ case "focus":
2589
+ break;
2590
+
2591
+ default:
2592
+ log_error(`${gobj_short_name(gobj)}: legend action unknown: ${action}`);
2593
+ return -1;
2594
+ }
2595
+
2430
2596
  let next = (priv.focus_topic === topic)? "" : topic;
2431
2597
 
2432
2598
  /* APPLY, then announce — in that order, and both halves are needed.
@@ -2528,7 +2694,14 @@ function ac_find_result(gobj, event, kw, src)
2528
2694
  return 0;
2529
2695
  }
2530
2696
 
2531
- $count.textContent = t("matches", {count: (kw && kw.matches) || 0});
2697
+ let text = t("matches", {count: (kw && kw.matches) || 0});
2698
+ /* Matches in HIDDEN topics, counted apart: "0" alone reads as
2699
+ * "does not exist" when it means "is hidden". */
2700
+ let hidden = (kw && kw.hidden_matches) || 0;
2701
+ if(hidden > 0) {
2702
+ text += ` (+${hidden} ${t("hidden topics")})`;
2703
+ }
2704
+ $count.textContent = text;
2532
2705
  $result.classList.remove("is-hidden");
2533
2706
  return 0;
2534
2707
  }
@@ -2537,6 +2710,18 @@ function ac_set_focus_topic(gobj, event, kw, src)
2537
2710
  {
2538
2711
  let priv = gobj.priv;
2539
2712
  priv.focus_topic = (kw && kw.topic) || "";
2713
+
2714
+ /* The URL wins over the strip: a route that lands on a hidden
2715
+ * topic shows it first, or it would highlight nothing and say
2716
+ * nothing about why. */
2717
+ if(priv.focus_topic) {
2718
+ let hidden = legend_list_attr(gobj, "hidden_topics");
2719
+ if(str_in_list(hidden, priv.focus_topic)) {
2720
+ delete_from_list(hidden, priv.focus_topic);
2721
+ set_legend_list_attr(gobj, "hidden_topics", "EV_SET_HIDDEN_TOPICS", hidden);
2722
+ }
2723
+ }
2724
+
2540
2725
  if(priv.gobj_nodes_tree) {
2541
2726
  gobj_send_event(
2542
2727
  priv.gobj_nodes_tree,
@@ -2547,11 +2732,7 @@ function ac_set_focus_topic(gobj, event, kw, src)
2547
2732
  }
2548
2733
  /* Keep the legend's mark on the topic that is actually focused —
2549
2734
  * including when the focus arrives from the URL and not from a click. */
2550
- let $container = gobj_read_attr(gobj, "$container");
2551
- let $legend = $container? $container.querySelector(".GRAPH_LEGEND") : null;
2552
- if($legend && !$legend.classList.contains("is-hidden")) {
2553
- refresh_legend(gobj);
2554
- }
2735
+ refresh_legend(gobj);
2555
2736
  return 0;
2556
2737
  }
2557
2738
 
@@ -2644,8 +2825,8 @@ function create_gclass(gclass_name)
2644
2825
  ["EV_EXPAND_ALL", ac_expand_all, null],
2645
2826
  ["EV_COLLAPSE_ALL", ac_collapse_all, null],
2646
2827
  ["EV_LAYOUT_AUTOSET", ac_layout_autoset, null],
2647
- ["EV_TOGGLE_LEGEND", ac_toggle_legend, null],
2648
2828
  ["EV_LEGEND_TOPIC", ac_legend_topic, null],
2829
+ ["EV_LEGEND_STATE", ac_legend_state, null],
2649
2830
  ["EV_FIND_RESULT", ac_find_result, null],
2650
2831
  ["EV_SHOW", ac_show, null],
2651
2832
  ["EV_HIDE", ac_hide, null],
@@ -2686,8 +2867,8 @@ function create_gclass(gclass_name)
2686
2867
  ["EV_COLLAPSE_ALL", 0],
2687
2868
  ["EV_FIND_RESULT", 0],
2688
2869
  ["EV_LAYOUT_AUTOSET", 0],
2689
- ["EV_TOGGLE_LEGEND", 0],
2690
2870
  ["EV_LEGEND_TOPIC", 0],
2871
+ ["EV_LEGEND_STATE", 0],
2691
2872
  ["EV_TOPIC_SELECTED",
2692
2873
  event_flag_t.EVF_OUTPUT_EVENT | event_flag_t.EVF_NO_WARN_SUBS],
2693
2874
  ["EV_OPERATION_MODE_CHANGED",