@yuneta/gobj-ui 7.2.5 → 7.4.0
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 +9 -0
- package/dist/gobj-ui.cjs.js +291 -40
- package/dist/gobj-ui.es.js +291 -40
- package/package.json +1 -1
- package/src/c_g6_nodes_tree.js +187 -44
- package/src/c_yui_treedb_graph.js +171 -7
package/src/c_g6_nodes_tree.js
CHANGED
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
gobj_read_attr,
|
|
32
32
|
gobj_read_str_attr,
|
|
33
33
|
gobj_read_bool_attr,
|
|
34
|
+
gobj_read_integer_attr,
|
|
34
35
|
gobj_write_attr,
|
|
35
36
|
gobj_write_str_attr,
|
|
36
37
|
gobj_parent,
|
|
@@ -93,7 +94,7 @@ import {
|
|
|
93
94
|
register,
|
|
94
95
|
} from '@antv/g6';
|
|
95
96
|
|
|
96
|
-
import {Circle as CircleGeometry} from '@antv/g';
|
|
97
|
+
import {Circle as CircleGeometry, Rect as RectGeometry} from '@antv/g';
|
|
97
98
|
import i18next, {t} from "i18next";
|
|
98
99
|
|
|
99
100
|
import {inject_svg_icons} from "./lib_icons.js";
|
|
@@ -140,6 +141,14 @@ ensure_drag_canvas_patch();
|
|
|
140
141
|
***************************************************************/
|
|
141
142
|
const GCLASS_NAME = "C_G6_NODES_TREE";
|
|
142
143
|
|
|
144
|
+
/* The highlight of a focused topic / a find match. It is drawn INTO the
|
|
145
|
+
* node's own html, not with G6's `active` element state: every node here
|
|
146
|
+
* is an `html` node, whose key shape is a DOM element — the state's
|
|
147
|
+
* `stroke` / `halo` have nothing to paint on and the amber never
|
|
148
|
+
* appeared, for the topic focus either. */
|
|
149
|
+
const HIGHLIGHT_COLOR = "#f0a020";
|
|
150
|
+
const HIGHLIGHT_HALO = "rgba(240,160,32,0.35)";
|
|
151
|
+
|
|
143
152
|
/***************************************************************
|
|
144
153
|
* Internal layout and operation mode definitions
|
|
145
154
|
***************************************************************/
|
|
@@ -210,6 +219,7 @@ SDATA(data_type_t.DTP_BOOLEAN, "with_treedb_tables", 0, false, "Include tre
|
|
|
210
219
|
SDATA(data_type_t.DTP_BOOLEAN, "with_gridline", 0, true, "Use gridline plugin"),
|
|
211
220
|
SDATA(data_type_t.DTP_BOOLEAN, "with_fullscreen", 0, true, "Use fullscreen plugin"),
|
|
212
221
|
SDATA(data_type_t.DTP_BOOLEAN, "with_toolbar", 0, true, "Use toolbar plugin"),
|
|
222
|
+
SDATA(data_type_t.DTP_INTEGER, "minimap_min_nodes", 0, 30, "Show the minimap from this many nodes on (0 = never). A minimap of a graph that fits on screen is decoration; one of two hundred cards is the only way to know where you are"),
|
|
213
223
|
SDATA(data_type_t.DTP_STRING, "toolbar_position", 0, "right-top",
|
|
214
224
|
"Toolbar position: top-left, top-right, bottom-left, bottom-right, left-top, right-top"),
|
|
215
225
|
SDATA(data_type_t.DTP_LIST, "layout_names", sdata_flag_t.SDF_RD,
|
|
@@ -2113,6 +2123,7 @@ function graph_focus_topic(gobj, topic)
|
|
|
2113
2123
|
states[id] = ['active'];
|
|
2114
2124
|
}
|
|
2115
2125
|
}
|
|
2126
|
+
let prev_ids = priv._focus_ids || [];
|
|
2116
2127
|
priv._focus_ids = ids;
|
|
2117
2128
|
priv._focus_topic = topic || null;
|
|
2118
2129
|
|
|
@@ -2120,6 +2131,7 @@ function graph_focus_topic(gobj, topic)
|
|
|
2120
2131
|
if(typeof graph.setElementState === "function") {
|
|
2121
2132
|
graph.setElementState(states);
|
|
2122
2133
|
}
|
|
2134
|
+
apply_node_highlight(gobj, prev_ids, ids);
|
|
2123
2135
|
if(ids.length && typeof graph.focusElement === "function") {
|
|
2124
2136
|
graph.focusElement(ids);
|
|
2125
2137
|
}
|
|
@@ -2194,6 +2206,7 @@ function graph_find_nodes(gobj, term)
|
|
|
2194
2206
|
}
|
|
2195
2207
|
}
|
|
2196
2208
|
|
|
2209
|
+
let prev_ids = priv._focus_ids || [];
|
|
2197
2210
|
priv._focus_ids = ids;
|
|
2198
2211
|
priv._focus_topic = null; /* a find takes over the highlight */
|
|
2199
2212
|
|
|
@@ -2201,6 +2214,7 @@ function graph_find_nodes(gobj, term)
|
|
|
2201
2214
|
if(typeof graph.setElementState === "function") {
|
|
2202
2215
|
graph.setElementState(states);
|
|
2203
2216
|
}
|
|
2217
|
+
apply_node_highlight(gobj, prev_ids, ids);
|
|
2204
2218
|
if(ids.length && typeof graph.focusElement === "function") {
|
|
2205
2219
|
graph.focusElement(ids);
|
|
2206
2220
|
}
|
|
@@ -4469,6 +4483,154 @@ function show_node_detail_popover(gobj, node_id)
|
|
|
4469
4483
|
* not re-themed by setTheme()) are regenerated; structural
|
|
4470
4484
|
* junction diamonds get their neutral fill/stroke re-themed.
|
|
4471
4485
|
************************************************************/
|
|
4486
|
+
/************************************************************
|
|
4487
|
+
* Show or hide the minimap, deciding by the SIZE of the graph.
|
|
4488
|
+
*
|
|
4489
|
+
* A minimap of a graph that already fits on screen is decoration; one
|
|
4490
|
+
* of two hundred cards is the only way to know where you are. So it is
|
|
4491
|
+
* not a preference the reader has to find and set — it appears when
|
|
4492
|
+
* there is something to be lost in, from `minimap_min_nodes` on.
|
|
4493
|
+
*
|
|
4494
|
+
* Its shapes are drawn by hand. G6's minimap clones each element's key
|
|
4495
|
+
* shape into its own canvas, and every node here is an `html` node
|
|
4496
|
+
* whose key shape is a DOM element — the same reason the `active`
|
|
4497
|
+
* state never painted (see HIGHLIGHT_COLOR). A block in the topic's
|
|
4498
|
+
* colour is also what a minimap of cards SHOULD show: at that scale a
|
|
4499
|
+
* card is a rectangle anyway.
|
|
4500
|
+
************************************************************/
|
|
4501
|
+
function refresh_minimap(gobj)
|
|
4502
|
+
{
|
|
4503
|
+
let priv = gobj.priv;
|
|
4504
|
+
let graph = priv.graph;
|
|
4505
|
+
if(!graph) {
|
|
4506
|
+
return;
|
|
4507
|
+
}
|
|
4508
|
+
|
|
4509
|
+
let threshold = gobj_read_integer_attr(gobj, "minimap_min_nodes");
|
|
4510
|
+
let n = 0;
|
|
4511
|
+
try {
|
|
4512
|
+
let data = graph.getData() || {};
|
|
4513
|
+
n = is_array(data.nodes)? data.nodes.length : 0;
|
|
4514
|
+
} catch(e) {
|
|
4515
|
+
n = 0;
|
|
4516
|
+
}
|
|
4517
|
+
|
|
4518
|
+
if(!threshold || n < threshold) {
|
|
4519
|
+
graph_remove_plugin(gobj, "minimap");
|
|
4520
|
+
return;
|
|
4521
|
+
}
|
|
4522
|
+
|
|
4523
|
+
if(graph_get_plugin(gobj, "minimap")) {
|
|
4524
|
+
return; /* already up */
|
|
4525
|
+
}
|
|
4526
|
+
|
|
4527
|
+
graph_add_plugin(
|
|
4528
|
+
gobj,
|
|
4529
|
+
"minimap",
|
|
4530
|
+
{
|
|
4531
|
+
size: [200, 140],
|
|
4532
|
+
position: "bottom-left",
|
|
4533
|
+
shape: (id, element_type, element) => {
|
|
4534
|
+
if(element_type !== "node") {
|
|
4535
|
+
return element; /* edges clone themselves fine */
|
|
4536
|
+
}
|
|
4537
|
+
try {
|
|
4538
|
+
let nd = graph.getNodeData(id);
|
|
4539
|
+
let size = (nd && nd.style && nd.style.size) || [120, 60];
|
|
4540
|
+
let color = (nd && nd.data && nd.data.desc && nd.data.desc.color)
|
|
4541
|
+
|| "#94a3b8";
|
|
4542
|
+
return new RectGeometry({
|
|
4543
|
+
style: {
|
|
4544
|
+
x: -size[0] / 2,
|
|
4545
|
+
y: -size[1] / 2,
|
|
4546
|
+
width: size[0],
|
|
4547
|
+
height: size[1],
|
|
4548
|
+
fill: color,
|
|
4549
|
+
radius: 4
|
|
4550
|
+
}
|
|
4551
|
+
});
|
|
4552
|
+
} catch(e) {
|
|
4553
|
+
log_error(`${gobj_short_name(gobj)}: minimap shape failed: ${e}`);
|
|
4554
|
+
return element;
|
|
4555
|
+
}
|
|
4556
|
+
}
|
|
4557
|
+
}
|
|
4558
|
+
);
|
|
4559
|
+
}
|
|
4560
|
+
|
|
4561
|
+
/************************************************************
|
|
4562
|
+
* The innerHTML of ONE node, in the given theme and highlight state.
|
|
4563
|
+
* The three treedb tiers (hierarchical / extended / child) each have
|
|
4564
|
+
* their own card, and both the theme refresh and the highlight need the
|
|
4565
|
+
* same three-way choice — it lived inline in the theme refresh and is
|
|
4566
|
+
* shared now. Returns null for a node that carries no desc.
|
|
4567
|
+
************************************************************/
|
|
4568
|
+
function node_innerHTML_of(nd, theme, highlight)
|
|
4569
|
+
{
|
|
4570
|
+
if(!nd || !nd.data || !nd.data.desc) {
|
|
4571
|
+
return null;
|
|
4572
|
+
}
|
|
4573
|
+
let desc = nd.data.desc;
|
|
4574
|
+
let record = nd.data.record || {};
|
|
4575
|
+
let label = node_label(desc, record);
|
|
4576
|
+
|
|
4577
|
+
switch(desc.node_treedb_type) {
|
|
4578
|
+
case 'child':
|
|
4579
|
+
return build_chip_innerHTML(
|
|
4580
|
+
desc.color, theme, record.icon, label, record.id, highlight
|
|
4581
|
+
);
|
|
4582
|
+
case 'extended':
|
|
4583
|
+
return build_node_innerHTML(
|
|
4584
|
+
desc.color, theme, record.icon, label,
|
|
4585
|
+
desc.topic_name, true, record.id, highlight
|
|
4586
|
+
);
|
|
4587
|
+
case 'hierarchical':
|
|
4588
|
+
return build_node_innerHTML(
|
|
4589
|
+
desc.color, theme, record.icon, label,
|
|
4590
|
+
desc.topic_name, false, record.id, highlight
|
|
4591
|
+
);
|
|
4592
|
+
}
|
|
4593
|
+
return null;
|
|
4594
|
+
}
|
|
4595
|
+
|
|
4596
|
+
/************************************************************
|
|
4597
|
+
* Repaint the cards whose highlight state CHANGES: the ones that had it
|
|
4598
|
+
* and lose it, plus the ones that gain it. Only those — a treedb graph
|
|
4599
|
+
* is redrawn per keystroke of the find box otherwise.
|
|
4600
|
+
************************************************************/
|
|
4601
|
+
function apply_node_highlight(gobj, prev_ids, next_ids)
|
|
4602
|
+
{
|
|
4603
|
+
let priv = gobj.priv;
|
|
4604
|
+
let graph = priv.graph;
|
|
4605
|
+
if(!graph) {
|
|
4606
|
+
return;
|
|
4607
|
+
}
|
|
4608
|
+
|
|
4609
|
+
let next = new Set(next_ids || []);
|
|
4610
|
+
let touched = new Set([...(prev_ids || []), ...next]);
|
|
4611
|
+
if(touched.size === 0) {
|
|
4612
|
+
return;
|
|
4613
|
+
}
|
|
4614
|
+
|
|
4615
|
+
let updates = [];
|
|
4616
|
+
for(let id of touched) {
|
|
4617
|
+
let nd = graph.getNodeData(id);
|
|
4618
|
+
let html = node_innerHTML_of(nd, priv.theme, next.has(id));
|
|
4619
|
+
if(html !== null) {
|
|
4620
|
+
updates.push({id: id, style: {innerHTML: html}});
|
|
4621
|
+
}
|
|
4622
|
+
}
|
|
4623
|
+
if(!updates.length) {
|
|
4624
|
+
return;
|
|
4625
|
+
}
|
|
4626
|
+
try {
|
|
4627
|
+
graph.updateNodeData(updates);
|
|
4628
|
+
graph.draw();
|
|
4629
|
+
} catch(e) {
|
|
4630
|
+
log_error(`${gobj_short_name(gobj)}: cannot repaint the highlight: ${e}`);
|
|
4631
|
+
}
|
|
4632
|
+
}
|
|
4633
|
+
|
|
4472
4634
|
function refresh_html_nodes_theme(gobj, theme)
|
|
4473
4635
|
{
|
|
4474
4636
|
let priv = gobj.priv;
|
|
@@ -4477,46 +4639,16 @@ function refresh_html_nodes_theme(gobj, theme)
|
|
|
4477
4639
|
return;
|
|
4478
4640
|
}
|
|
4479
4641
|
let nodes = graph.getData().nodes || [];
|
|
4642
|
+
/* The highlight is part of the card's html, so a theme switch that
|
|
4643
|
+
* rebuilt every card without it would silently CLEAR the focus or the
|
|
4644
|
+
* find that is on screen. Carry it across. */
|
|
4645
|
+
let highlighted = new Set(priv._focus_ids || []);
|
|
4480
4646
|
let updates = [];
|
|
4481
4647
|
for(let i = 0; i < nodes.length; i++) {
|
|
4482
|
-
let
|
|
4483
|
-
|
|
4484
|
-
|
|
4485
|
-
|
|
4486
|
-
let tt = nd.data.desc.node_treedb_type;
|
|
4487
|
-
let record = nd.data.record || {};
|
|
4488
|
-
if(tt === 'hierarchical') {
|
|
4489
|
-
updates.push({
|
|
4490
|
-
id: nodes[i].id,
|
|
4491
|
-
style: {
|
|
4492
|
-
innerHTML: build_node_innerHTML(
|
|
4493
|
-
nd.data.desc.color, theme, record.icon,
|
|
4494
|
-
node_label(nd.data.desc, record),
|
|
4495
|
-
nd.data.desc.topic_name, false, record.id
|
|
4496
|
-
)
|
|
4497
|
-
}
|
|
4498
|
-
});
|
|
4499
|
-
} else if(tt === 'child') {
|
|
4500
|
-
updates.push({
|
|
4501
|
-
id: nodes[i].id,
|
|
4502
|
-
style: {
|
|
4503
|
-
innerHTML: build_chip_innerHTML(
|
|
4504
|
-
nd.data.desc.color, theme, record.icon,
|
|
4505
|
-
node_label(nd.data.desc, record), record.id
|
|
4506
|
-
)
|
|
4507
|
-
}
|
|
4508
|
-
});
|
|
4509
|
-
} else if(tt === 'extended') {
|
|
4510
|
-
updates.push({
|
|
4511
|
-
id: nodes[i].id,
|
|
4512
|
-
style: {
|
|
4513
|
-
innerHTML: build_node_innerHTML(
|
|
4514
|
-
nd.data.desc.color, theme, record.icon,
|
|
4515
|
-
node_label(nd.data.desc, record),
|
|
4516
|
-
nd.data.desc.topic_name, true, record.id
|
|
4517
|
-
)
|
|
4518
|
-
}
|
|
4519
|
-
});
|
|
4648
|
+
let id = nodes[i].id;
|
|
4649
|
+
let html = node_innerHTML_of(graph.getNodeData(id), theme, highlighted.has(id));
|
|
4650
|
+
if(html !== null) {
|
|
4651
|
+
updates.push({id: id, style: {innerHTML: html}});
|
|
4520
4652
|
}
|
|
4521
4653
|
}
|
|
4522
4654
|
if(updates.length > 0) {
|
|
@@ -4559,7 +4691,7 @@ function refresh_default_edges_theme(gobj, theme)
|
|
|
4559
4691
|
* but lighter (1px border, no shadow, single line). The name is
|
|
4560
4692
|
* always legible (ellipsis + native title tooltip on overflow).
|
|
4561
4693
|
************************************************************/
|
|
4562
|
-
function build_chip_innerHTML(color, theme, icon, label, key)
|
|
4694
|
+
function build_chip_innerHTML(color, theme, icon, label, key, highlight)
|
|
4563
4695
|
{
|
|
4564
4696
|
let title = key || label;
|
|
4565
4697
|
let dark = (theme === "dark");
|
|
@@ -4571,6 +4703,9 @@ function build_chip_innerHTML(color, theme, icon, label, key)
|
|
|
4571
4703
|
? `color-mix(in srgb, ${color} 85%, #ffffff)`
|
|
4572
4704
|
: color;
|
|
4573
4705
|
let text_color = dark ? "#e8eaed" : "#0f172a";
|
|
4706
|
+
if(highlight) {
|
|
4707
|
+
border = HIGHLIGHT_COLOR;
|
|
4708
|
+
}
|
|
4574
4709
|
|
|
4575
4710
|
let icon_html = "";
|
|
4576
4711
|
if(icon) {
|
|
@@ -4586,7 +4721,8 @@ function build_chip_innerHTML(color, theme, icon, label, key)
|
|
|
4586
4721
|
width: 100%;
|
|
4587
4722
|
height: 100%;
|
|
4588
4723
|
background: ${bg};
|
|
4589
|
-
border: 1px solid ${border};
|
|
4724
|
+
border: ${highlight? "3px" : "1px"} solid ${border};
|
|
4725
|
+
${highlight? `box-shadow: 0 0 0 4px ${HIGHLIGHT_HALO};` : ""}
|
|
4590
4726
|
border-radius: 8px;
|
|
4591
4727
|
color: ${text_color};
|
|
4592
4728
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
|
@@ -4613,7 +4749,7 @@ function build_chip_innerHTML(color, theme, icon, label, key)
|
|
|
4613
4749
|
* colour is kept (per-topic differentiation) but softened via
|
|
4614
4750
|
* color-mix instead of a harsh saturated fill. Theme-aware.
|
|
4615
4751
|
************************************************************/
|
|
4616
|
-
function build_node_innerHTML(color, theme, icon, label, topic_name, structural, key)
|
|
4752
|
+
function build_node_innerHTML(color, theme, icon, label, topic_name, structural, key, highlight)
|
|
4617
4753
|
{
|
|
4618
4754
|
let title = key || label;
|
|
4619
4755
|
let dark = (theme === "dark");
|
|
@@ -4639,6 +4775,10 @@ function build_node_innerHTML(color, theme, icon, label, topic_name, structural,
|
|
|
4639
4775
|
: color;
|
|
4640
4776
|
border_style = "solid";
|
|
4641
4777
|
}
|
|
4778
|
+
if(highlight) {
|
|
4779
|
+
border = HIGHLIGHT_COLOR;
|
|
4780
|
+
border_style = "solid";
|
|
4781
|
+
}
|
|
4642
4782
|
let title_color = dark ? "#e8eaed" : "#0f172a";
|
|
4643
4783
|
let sub_color = dark ? "#9aa4b2" : "#64748b";
|
|
4644
4784
|
let shadow = dark
|
|
@@ -4670,9 +4810,9 @@ function build_node_innerHTML(color, theme, icon, label, topic_name, structural,
|
|
|
4670
4810
|
width: 100%;
|
|
4671
4811
|
height: 100%;
|
|
4672
4812
|
background: ${bg};
|
|
4673
|
-
border: 1.5px ${border_style} ${border};
|
|
4813
|
+
border: ${highlight? "3px" : "1.5px"} ${border_style} ${border};
|
|
4674
4814
|
border-radius: 10px;
|
|
4675
|
-
box-shadow: ${shadow};
|
|
4815
|
+
box-shadow: ${highlight? `0 0 0 4px ${HIGHLIGHT_HALO}, ${shadow}` : shadow};
|
|
4676
4816
|
color: ${title_color};
|
|
4677
4817
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
|
4678
4818
|
display: flex;
|
|
@@ -5623,6 +5763,9 @@ function ac_load_data(gobj, event, kw, src)
|
|
|
5623
5763
|
} else {
|
|
5624
5764
|
graph_remove_plugin(gobj, "history");
|
|
5625
5765
|
}
|
|
5766
|
+
/* The graph knows its size now: a minimap appears only
|
|
5767
|
+
* when there is something to be lost in. */
|
|
5768
|
+
refresh_minimap(gobj);
|
|
5626
5769
|
/* Nodes exist and are positioned now: apply a focus
|
|
5627
5770
|
* requested before the data was ready (deep link). */
|
|
5628
5771
|
if(priv._pending_focus_topic !== null) {
|
|
@@ -147,6 +147,8 @@ let PRIVATE_DATA = {
|
|
|
147
147
|
topics: [],
|
|
148
148
|
records: {},
|
|
149
149
|
gobj_nodes_tree: null,
|
|
150
|
+
find_timer: null, // rate-limits the find box (see make_toolbar)
|
|
151
|
+
focus_topic: "", // topic the graph is focused on (marks the legend)
|
|
150
152
|
gobj_treedb_tables: null,
|
|
151
153
|
hook_data_viewer: null,
|
|
152
154
|
json_gobj: null, /* C_YUI_JSON viewer of the raw tranger (or null) */
|
|
@@ -310,6 +312,14 @@ function mt_stop(gobj)
|
|
|
310
312
|
***************************************************************/
|
|
311
313
|
function mt_destroy(gobj)
|
|
312
314
|
{
|
|
315
|
+
let priv = gobj.priv;
|
|
316
|
+
/* The find box rate-limits itself with a timer; a view torn down
|
|
317
|
+
* between the keystroke and the send would fire an event into a
|
|
318
|
+
* destroyed gobj. */
|
|
319
|
+
if(priv.find_timer) {
|
|
320
|
+
clearTimeout(priv.find_timer);
|
|
321
|
+
priv.find_timer = null;
|
|
322
|
+
}
|
|
313
323
|
destroy_ui(gobj);
|
|
314
324
|
}
|
|
315
325
|
|
|
@@ -345,6 +355,13 @@ function build_ui(gobj)
|
|
|
345
355
|
// Don't use is-flex, don't work well with is-hidden
|
|
346
356
|
['div', {class: 'C_YUI_TREEDB_GRAPH', style: `height:100%; display:flex; flex-direction:column;`}, [
|
|
347
357
|
['div', {class: 'GRAPH_TOOLBAR_ROW is-flex-grow-0 is-flex is-align-items-center'}, row_items],
|
|
358
|
+
/* Topic colour legend: a strip, not an overlay. It is opened to
|
|
359
|
+
* be READ against the graph, and an overlay would cover the
|
|
360
|
+
* thing it explains. Built on first open (the colours are the
|
|
361
|
+
* child's, assigned when the schema arrives). */
|
|
362
|
+
['div', {class: 'GRAPH_LEGEND is-hidden is-flex-grow-0',
|
|
363
|
+
style: 'display:flex; flex-wrap:wrap; align-items:center; ' +
|
|
364
|
+
'gap:.4rem; padding:.35rem .5rem;'}, []],
|
|
348
365
|
['div', {class: `GRAPH_BODY is-flex-grow-1 ${padding}`, style: 'height:100%; min-height:0; overflow:hidden;'}, [
|
|
349
366
|
['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;'}, [
|
|
350
367
|
]]
|
|
@@ -487,6 +504,23 @@ function make_toolbar(gobj)
|
|
|
487
504
|
}
|
|
488
505
|
}],
|
|
489
506
|
|
|
507
|
+
/* Which colour is which topic. The port colour of a node encodes
|
|
508
|
+
* the topic it links to, which is the whole point of the graph and
|
|
509
|
+
* which nothing on screen explained. Clicking an entry focuses that
|
|
510
|
+
* topic, so the legend is also the way to say "show me these" —
|
|
511
|
+
* and clicking the focused one again clears it. */
|
|
512
|
+
['button', {class: 'GRAPH_LEGEND_BTN button ml-2',
|
|
513
|
+
title: t('legend'), 'aria-label': t('legend'),
|
|
514
|
+
'data-i18n-title': 'legend', 'data-i18n-aria-label': 'legend'}, [
|
|
515
|
+
['i', {class: 'yi-square'}],
|
|
516
|
+
['span', {class: 'is-hidden-mobile', style: 'padding-left:5px;', i18n: 'legend'}, 'legend']
|
|
517
|
+
], {
|
|
518
|
+
click: (evt) => {
|
|
519
|
+
evt.stopPropagation();
|
|
520
|
+
gobj_send_event(gobj, "EV_TOGGLE_LEGEND", {}, gobj);
|
|
521
|
+
}
|
|
522
|
+
}],
|
|
523
|
+
|
|
490
524
|
/* Inspect the treedb's raw tranger json in the lazy tree viewer
|
|
491
525
|
* (print-tranger on the C_NODE service). A treedb can be huge, so
|
|
492
526
|
* the viewer drills in on demand — see open_json_viewer. */
|
|
@@ -530,14 +564,21 @@ function make_toolbar(gobj)
|
|
|
530
564
|
['i', {class: 'yi-magnifying-glass'}]
|
|
531
565
|
]]
|
|
532
566
|
], {
|
|
567
|
+
/* Rate-limited, not delayed for effect: a match repaints the
|
|
568
|
+
* cards it lands on, and on a large treedb the first letter
|
|
569
|
+
* typed can match hundreds. This is input plumbing — the
|
|
570
|
+
* event still carries every action to the FSM, just not one
|
|
571
|
+
* per keystroke. */
|
|
533
572
|
input: (evt) => {
|
|
534
573
|
evt.stopPropagation();
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
574
|
+
let text = evt.target.value.trim();
|
|
575
|
+
if(priv.find_timer) {
|
|
576
|
+
clearTimeout(priv.find_timer);
|
|
577
|
+
}
|
|
578
|
+
priv.find_timer = setTimeout(function() {
|
|
579
|
+
priv.find_timer = null;
|
|
580
|
+
gobj_send_event(gobj, "EV_FIND_NODES", {text: text}, gobj);
|
|
581
|
+
}, 250);
|
|
541
582
|
}
|
|
542
583
|
}],
|
|
543
584
|
/* Two spans, not one string: the number is DATA and "matches" is
|
|
@@ -2178,6 +2219,115 @@ function ac_set_operation_mode(gobj, event, kw, src)
|
|
|
2178
2219
|
* tree, which centres + highlights that topic's nodes (deferring
|
|
2179
2220
|
* until its data is loaded).
|
|
2180
2221
|
************************************************************/
|
|
2222
|
+
/************************************************************
|
|
2223
|
+
* Fill the legend strip with one entry per topic: a swatch in the
|
|
2224
|
+
* topic's colour and its name. The colours belong to the graph child —
|
|
2225
|
+
* it assigns them from its palette when the schema arrives — so they
|
|
2226
|
+
* are read from there and never guessed twice.
|
|
2227
|
+
*
|
|
2228
|
+
* An entry is a BUTTON: clicking it focuses that topic, clicking the
|
|
2229
|
+
* focused one clears the focus. The focus travels the same way a topic
|
|
2230
|
+
* card's graph icon travels — as EV_TOPIC_SELECTED, which the host turns
|
|
2231
|
+
* into the URL, so what you are looking at stays linkable.
|
|
2232
|
+
************************************************************/
|
|
2233
|
+
function refresh_legend(gobj)
|
|
2234
|
+
{
|
|
2235
|
+
let priv = gobj.priv;
|
|
2236
|
+
let $container = gobj_read_attr(gobj, "$container");
|
|
2237
|
+
if(!$container) {
|
|
2238
|
+
return;
|
|
2239
|
+
}
|
|
2240
|
+
let $legend = $container.querySelector(".GRAPH_LEGEND");
|
|
2241
|
+
if(!$legend) {
|
|
2242
|
+
return;
|
|
2243
|
+
}
|
|
2244
|
+
|
|
2245
|
+
let descs = priv.gobj_nodes_tree?
|
|
2246
|
+
gobj_read_attr(priv.gobj_nodes_tree, "descs") : null;
|
|
2247
|
+
$legend.textContent = "";
|
|
2248
|
+
if(!descs) {
|
|
2249
|
+
return;
|
|
2250
|
+
}
|
|
2251
|
+
|
|
2252
|
+
for(const [topic_name, desc] of Object.entries(descs)) {
|
|
2253
|
+
if(topic_name.substring(0, 2) === "__") {
|
|
2254
|
+
continue; /* the internal topics are not drawn */
|
|
2255
|
+
}
|
|
2256
|
+
if(!desc || !desc.color) {
|
|
2257
|
+
continue;
|
|
2258
|
+
}
|
|
2259
|
+
let focused = (priv.focus_topic === topic_name);
|
|
2260
|
+
let $item = createElement2(
|
|
2261
|
+
['button', {
|
|
2262
|
+
class: 'GRAPH_LEGEND_ITEM button is-small' +
|
|
2263
|
+
(focused? ' is-primary' : ''),
|
|
2264
|
+
title: topic_name,
|
|
2265
|
+
'aria-label': topic_name,
|
|
2266
|
+
'aria-pressed': focused? 'true' : 'false'
|
|
2267
|
+
}, [
|
|
2268
|
+
['span', {class: 'GRAPH_LEGEND_SWATCH',
|
|
2269
|
+
style: `display:inline-block; width:.8rem; height:.8rem; ` +
|
|
2270
|
+
`border-radius:3px; margin-right:.4rem; ` +
|
|
2271
|
+
`background:${desc.color}; ` +
|
|
2272
|
+
`border:1px solid rgba(0,0,0,.25);`}],
|
|
2273
|
+
/* The topic name is DATA: it carries no i18n key and is
|
|
2274
|
+
* never translated. */
|
|
2275
|
+
['span', {}, topic_name]
|
|
2276
|
+
], {
|
|
2277
|
+
click: (evt) => {
|
|
2278
|
+
evt.stopPropagation();
|
|
2279
|
+
gobj_send_event(gobj, "EV_LEGEND_TOPIC", {topic: topic_name}, gobj);
|
|
2280
|
+
}
|
|
2281
|
+
}]
|
|
2282
|
+
);
|
|
2283
|
+
$legend.appendChild($item);
|
|
2284
|
+
}
|
|
2285
|
+
}
|
|
2286
|
+
|
|
2287
|
+
/************************************************************
|
|
2288
|
+
*
|
|
2289
|
+
************************************************************/
|
|
2290
|
+
function ac_toggle_legend(gobj, event, kw, src)
|
|
2291
|
+
{
|
|
2292
|
+
let $container = gobj_read_attr(gobj, "$container");
|
|
2293
|
+
if(!$container) {
|
|
2294
|
+
return 0;
|
|
2295
|
+
}
|
|
2296
|
+
let $legend = $container.querySelector(".GRAPH_LEGEND");
|
|
2297
|
+
if(!$legend) {
|
|
2298
|
+
log_error(`${gobj_short_name(gobj)}: no legend strip to toggle`);
|
|
2299
|
+
return -1;
|
|
2300
|
+
}
|
|
2301
|
+
if($legend.classList.contains("is-hidden")) {
|
|
2302
|
+
refresh_legend(gobj);
|
|
2303
|
+
$legend.classList.remove("is-hidden");
|
|
2304
|
+
} else {
|
|
2305
|
+
$legend.classList.add("is-hidden");
|
|
2306
|
+
}
|
|
2307
|
+
return 0;
|
|
2308
|
+
}
|
|
2309
|
+
|
|
2310
|
+
/************************************************************
|
|
2311
|
+
* A legend entry was clicked: focus that topic, or clear the focus if
|
|
2312
|
+
* it was already the focused one. Published UP rather than applied
|
|
2313
|
+
* here, so the URL follows — the host owns the route.
|
|
2314
|
+
************************************************************/
|
|
2315
|
+
function ac_legend_topic(gobj, event, kw, src)
|
|
2316
|
+
{
|
|
2317
|
+
let priv = gobj.priv;
|
|
2318
|
+
let topic = (kw && kw.topic) || "";
|
|
2319
|
+
if(!topic) {
|
|
2320
|
+
log_error(`${gobj_short_name(gobj)}: legend click with no topic`);
|
|
2321
|
+
return -1;
|
|
2322
|
+
}
|
|
2323
|
+
gobj_publish_event(
|
|
2324
|
+
gobj,
|
|
2325
|
+
"EV_TOPIC_SELECTED",
|
|
2326
|
+
{topic: (priv.focus_topic === topic)? "" : topic}
|
|
2327
|
+
);
|
|
2328
|
+
return 0;
|
|
2329
|
+
}
|
|
2330
|
+
|
|
2181
2331
|
/************************************************************
|
|
2182
2332
|
* Forward the find down to the graph child.
|
|
2183
2333
|
************************************************************/
|
|
@@ -2226,14 +2376,22 @@ function ac_find_result(gobj, event, kw, src)
|
|
|
2226
2376
|
function ac_set_focus_topic(gobj, event, kw, src)
|
|
2227
2377
|
{
|
|
2228
2378
|
let priv = gobj.priv;
|
|
2379
|
+
priv.focus_topic = (kw && kw.topic) || "";
|
|
2229
2380
|
if(priv.gobj_nodes_tree) {
|
|
2230
2381
|
gobj_send_event(
|
|
2231
2382
|
priv.gobj_nodes_tree,
|
|
2232
2383
|
"EV_FOCUS_TOPIC",
|
|
2233
|
-
{topic:
|
|
2384
|
+
{topic: priv.focus_topic},
|
|
2234
2385
|
gobj
|
|
2235
2386
|
);
|
|
2236
2387
|
}
|
|
2388
|
+
/* Keep the legend's mark on the topic that is actually focused —
|
|
2389
|
+
* including when the focus arrives from the URL and not from a click. */
|
|
2390
|
+
let $container = gobj_read_attr(gobj, "$container");
|
|
2391
|
+
let $legend = $container? $container.querySelector(".GRAPH_LEGEND") : null;
|
|
2392
|
+
if($legend && !$legend.classList.contains("is-hidden")) {
|
|
2393
|
+
refresh_legend(gobj);
|
|
2394
|
+
}
|
|
2237
2395
|
return 0;
|
|
2238
2396
|
}
|
|
2239
2397
|
|
|
@@ -2323,6 +2481,8 @@ function create_gclass(gclass_name)
|
|
|
2323
2481
|
["EV_SET_OPERATION_MODE", ac_set_operation_mode, null],
|
|
2324
2482
|
["EV_SET_FOCUS_TOPIC", ac_set_focus_topic, null],
|
|
2325
2483
|
["EV_FIND_NODES", ac_find_nodes, null],
|
|
2484
|
+
["EV_TOGGLE_LEGEND", ac_toggle_legend, null],
|
|
2485
|
+
["EV_LEGEND_TOPIC", ac_legend_topic, null],
|
|
2326
2486
|
["EV_FIND_RESULT", ac_find_result, null],
|
|
2327
2487
|
["EV_SHOW", ac_show, null],
|
|
2328
2488
|
["EV_HIDE", ac_hide, null],
|
|
@@ -2360,6 +2520,10 @@ function create_gclass(gclass_name)
|
|
|
2360
2520
|
["EV_SET_FOCUS_TOPIC", 0],
|
|
2361
2521
|
["EV_FIND_NODES", 0],
|
|
2362
2522
|
["EV_FIND_RESULT", 0],
|
|
2523
|
+
["EV_TOGGLE_LEGEND", 0],
|
|
2524
|
+
["EV_LEGEND_TOPIC", 0],
|
|
2525
|
+
["EV_TOPIC_SELECTED",
|
|
2526
|
+
event_flag_t.EVF_OUTPUT_EVENT | event_flag_t.EVF_NO_WARN_SUBS],
|
|
2363
2527
|
["EV_OPERATION_MODE_CHANGED",
|
|
2364
2528
|
event_flag_t.EVF_OUTPUT_EVENT | event_flag_t.EVF_NO_WARN_SUBS],
|
|
2365
2529
|
["EV_RECORD_WRITTEN",
|