@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
package/src/c_g6_nodes_tree.js
CHANGED
|
@@ -10,6 +10,23 @@
|
|
|
10
10
|
* record: null, // Data of node
|
|
11
11
|
* }
|
|
12
12
|
*
|
|
13
|
+
* The graph opens FOLDED, like a JSON viewer.
|
|
14
|
+
*
|
|
15
|
+
* Every record of every topic is fetched, but only the ones on
|
|
16
|
+
* screen become G6 nodes: the roots, `expand_depth` levels under
|
|
17
|
+
* them, one page (`fold_page_size`) of children per hook. Each
|
|
18
|
+
* card carries a pill per hook that has children -- `▸ devices
|
|
19
|
+
* 142` -- which opens or folds that hook; an open hook with more
|
|
20
|
+
* children than its page ends in a `+N` chip that opens the next
|
|
21
|
+
* page. The arithmetic lives in treedb_fold_model.js; this file
|
|
22
|
+
* reconciles the G6 data with what that model says is visible
|
|
23
|
+
* (see reconcile_fold) and lays it out again.
|
|
24
|
+
*
|
|
25
|
+
* `dagre` reads LEFT TO RIGHT: the children of a node are a
|
|
26
|
+
* column beside it, the ports move to the sides with it, and
|
|
27
|
+
* the edges are horizontal beziers. `antv-dagre` keeps the
|
|
28
|
+
* top-down reading.
|
|
29
|
+
*
|
|
13
30
|
* Copyright (c) 2020-2021 Niyamaka.
|
|
14
31
|
* Copyright (c) 2025-2026, ArtGins.
|
|
15
32
|
* All Rights Reserved.
|
|
@@ -87,7 +104,26 @@ import {
|
|
|
87
104
|
|
|
88
105
|
import {node_label} from "./treedb_node_label.js";
|
|
89
106
|
import {delete_impact} from "./delete_impact.js";
|
|
90
|
-
import {
|
|
107
|
+
import {
|
|
108
|
+
yui_graph_center_on,
|
|
109
|
+
yui_graph_viewport_of,
|
|
110
|
+
yui_graph_place_at,
|
|
111
|
+
} from "./yui_graph_camera.js";
|
|
112
|
+
import {
|
|
113
|
+
fold_build_model,
|
|
114
|
+
fold_new_state,
|
|
115
|
+
fold_visible_set,
|
|
116
|
+
fold_expand_to_depth,
|
|
117
|
+
fold_expand_all,
|
|
118
|
+
fold_collapse_all,
|
|
119
|
+
fold_toggle,
|
|
120
|
+
fold_show_more,
|
|
121
|
+
fold_reveal,
|
|
122
|
+
fold_pills_of,
|
|
123
|
+
fold_pending_groups,
|
|
124
|
+
fold_node_key,
|
|
125
|
+
fold_split_group_key,
|
|
126
|
+
} from "./treedb_fold_model.js";
|
|
91
127
|
|
|
92
128
|
import {
|
|
93
129
|
BaseLayout,
|
|
@@ -218,11 +254,23 @@ const _layouts = {
|
|
|
218
254
|
"manual": {
|
|
219
255
|
type: 'manual',
|
|
220
256
|
},
|
|
257
|
+
/* LEFT TO RIGHT, the house direction for a topology: a parent's
|
|
258
|
+
* children stack in a column beside it and read like a file tree.
|
|
259
|
+
* The separations are explicit because the default ones put the
|
|
260
|
+
* cards edge to edge. `antv-dagre` below keeps the top-down reading
|
|
261
|
+
* for whoever wants it -- the two are the same algorithm read two
|
|
262
|
+
* ways, so the layout picker is also the direction picker. */
|
|
221
263
|
"dagre": {
|
|
222
264
|
type: 'dagre',
|
|
265
|
+
rankdir: 'LR',
|
|
266
|
+
nodesep: 18,
|
|
267
|
+
ranksep: 110,
|
|
223
268
|
},
|
|
224
269
|
"antv-dagre": {
|
|
225
270
|
type: 'antv-dagre',
|
|
271
|
+
rankdir: 'TB',
|
|
272
|
+
nodesep: 30,
|
|
273
|
+
ranksep: 80,
|
|
226
274
|
},
|
|
227
275
|
"d3-force": {
|
|
228
276
|
type: 'd3-force',
|
|
@@ -289,6 +337,10 @@ SDATA(data_type_t.DTP_LIST, "layout_names", sdata_flag_t.SDF_RD,
|
|
|
289
337
|
|
|
290
338
|
SDATA(data_type_t.DTP_STRING, "hook_port_position", 0, "bottom", "Hook port position"),
|
|
291
339
|
SDATA(data_type_t.DTP_STRING, "fkey_port_position", 0, "top", "Fkey port position"),
|
|
340
|
+
|
|
341
|
+
/*---------------- Folding ----------------*/
|
|
342
|
+
SDATA(data_type_t.DTP_INTEGER, "expand_depth", 0, 2, "Levels open when a treedb loads: 1 = the roots alone, 2 = the roots and their children. Every open hook shows its first page"),
|
|
343
|
+
SDATA(data_type_t.DTP_INTEGER, "fold_page_size", 0, 24, "Children of one hook shown per page; the `+N` chip after the page opens the next one"),
|
|
292
344
|
SDATA(data_type_t.DTP_BOOLEAN, "confirm_delete_node", 0, true, "Ask confirmation before deleting a node"),
|
|
293
345
|
SDATA(data_type_t.DTP_BOOLEAN, "confirm_unlink_edge", 0, true, "Ask confirmation before unlinking an edge"),
|
|
294
346
|
|
|
@@ -368,10 +420,20 @@ let PRIVATE_DATA = {
|
|
|
368
420
|
// itself; this is what is on screen, and
|
|
369
421
|
// it exists to diff the repaint)
|
|
370
422
|
_pending_focus_topic: null, // focus requested before data was loaded
|
|
423
|
+
_pending_focus_all: false, // ...and whether it reveals the whole topic
|
|
371
424
|
_pending_find: null, // find requested before data was loaded
|
|
372
425
|
_layout_asked: "", // layout the host asked for at create (see mt_create)
|
|
373
|
-
_nodes_total: 0, //
|
|
374
|
-
_nodes_placed: 0, // ...of which
|
|
426
|
+
_nodes_total: 0, // records of the treedb (auto_layout)
|
|
427
|
+
_nodes_placed: 0, // ...of which carry a saved position
|
|
428
|
+
|
|
429
|
+
/*---------------- folding ----------------*/
|
|
430
|
+
expand_depth: 2,
|
|
431
|
+
fold_page_size: 24,
|
|
432
|
+
_fold_model: null, // treedb_fold_model: the tree of the records
|
|
433
|
+
_fold_state: null, // ...and which groups of it are open
|
|
434
|
+
_fold_listener: null, // delegated click on the card pills
|
|
435
|
+
_pill_sig: null, // Map<node_id, string>: the pills a card was painted with
|
|
436
|
+
_fold_busy: false, // a reconcile is in flight
|
|
375
437
|
};
|
|
376
438
|
|
|
377
439
|
let __gclass__ = null;
|
|
@@ -429,6 +491,7 @@ function mt_create(gobj)
|
|
|
429
491
|
build_ui(gobj);
|
|
430
492
|
register_layouts(gobj);
|
|
431
493
|
build_graph(gobj);
|
|
494
|
+
install_fold_listener(gobj);
|
|
432
495
|
|
|
433
496
|
/* Self-contained resize: the old shell pushed EV_RESIZE from
|
|
434
497
|
* __yui_main__; the new C_YUI_SHELL does not. Observe our own
|
|
@@ -489,6 +552,8 @@ function mt_destroy(gobj)
|
|
|
489
552
|
priv._on_language_changed = null;
|
|
490
553
|
}
|
|
491
554
|
|
|
555
|
+
uninstall_fold_listener(gobj);
|
|
556
|
+
|
|
492
557
|
if(priv._on_pointerdown_focus) {
|
|
493
558
|
priv.$container.removeEventListener(
|
|
494
559
|
"pointerdown", priv._on_pointerdown_focus, true
|
|
@@ -864,9 +929,10 @@ function configure_events(gobj)
|
|
|
864
929
|
}
|
|
865
930
|
});
|
|
866
931
|
|
|
867
|
-
// Re-render G6 toolbars when language changes
|
|
932
|
+
// Re-render G6 toolbars (and the `+N` chips' titles) when language changes
|
|
868
933
|
priv._on_language_changed = () => {
|
|
869
934
|
update_toolbar(gobj);
|
|
935
|
+
repaint_more_chips(gobj);
|
|
870
936
|
};
|
|
871
937
|
i18next.on('languageChanged', priv._on_language_changed);
|
|
872
938
|
|
|
@@ -1681,22 +1747,107 @@ function build_ports(gobj, desc)
|
|
|
1681
1747
|
}
|
|
1682
1748
|
}
|
|
1683
1749
|
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1750
|
+
place_ports(gobj, top_ports, bottom_ports);
|
|
1751
|
+
|
|
1752
|
+
return [...top_ports, ...bottom_ports];
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
/************************************************************
|
|
1756
|
+
* Which way the graph reads. `dagre` is the left-to-right
|
|
1757
|
+
* layout; everything else keeps the top-down reading, `manual`
|
|
1758
|
+
* included, because the saved arrangements were made with the
|
|
1759
|
+
* ports on the top and bottom edges.
|
|
1760
|
+
************************************************************/
|
|
1761
|
+
function layout_direction(gobj)
|
|
1762
|
+
{
|
|
1763
|
+
return (gobj.priv.layout === "dagre")? "LR" : "TB";
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
function edge_type_for(gobj)
|
|
1767
|
+
{
|
|
1768
|
+
return (layout_direction(gobj) === "LR")? "cubic-horizontal" : "cubic";
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
/************************************************************
|
|
1772
|
+
* Where the ports sit, by direction: fkeys (the way in from a
|
|
1773
|
+
* parent) all on one point of the leading edge, hooks (the way
|
|
1774
|
+
* out to the children) spread along the trailing edge. Left and
|
|
1775
|
+
* right when the graph reads left to right; top and bottom
|
|
1776
|
+
* otherwise.
|
|
1777
|
+
************************************************************/
|
|
1778
|
+
function place_ports(gobj, fkey_ports, hook_ports)
|
|
1779
|
+
{
|
|
1780
|
+
let lr = (layout_direction(gobj) === "LR");
|
|
1781
|
+
|
|
1782
|
+
for(let i = 0; i < fkey_ports.length; i++) {
|
|
1783
|
+
fkey_ports[i].placement = lr? [0, 0.5] : [0.5, 0];
|
|
1689
1784
|
}
|
|
1690
|
-
for(let i=0; i<
|
|
1691
|
-
let point = getPointPosition(
|
|
1692
|
-
if(
|
|
1693
|
-
|
|
1785
|
+
for(let i = 0; i < hook_ports.length; i++) {
|
|
1786
|
+
let point = getPointPosition(hook_ports.length, i);
|
|
1787
|
+
if(fkey_ports.length === 0 && !lr) {
|
|
1788
|
+
hook_ports[i].placement = [0, 0.5];
|
|
1789
|
+
} else if(lr) {
|
|
1790
|
+
hook_ports[i].placement = [1, point];
|
|
1694
1791
|
} else {
|
|
1695
|
-
|
|
1792
|
+
hook_ports[i].placement = [point, 1];
|
|
1696
1793
|
}
|
|
1697
1794
|
}
|
|
1795
|
+
}
|
|
1698
1796
|
|
|
1699
|
-
|
|
1797
|
+
/************************************************************
|
|
1798
|
+
* The layout changed direction: move every port to the edge
|
|
1799
|
+
* the new reading needs, and give every edge the curve that
|
|
1800
|
+
* goes with it. Called before the layout runs, so the lines
|
|
1801
|
+
* are drawn once, in the right place.
|
|
1802
|
+
************************************************************/
|
|
1803
|
+
function apply_layout_direction(gobj)
|
|
1804
|
+
{
|
|
1805
|
+
let priv = gobj.priv;
|
|
1806
|
+
let graph = priv.graph;
|
|
1807
|
+
|
|
1808
|
+
if(!graph) {
|
|
1809
|
+
return;
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
let node_updates = [];
|
|
1813
|
+
for(let nd of (graph.getNodeData() || [])) {
|
|
1814
|
+
if(!nd || !nd.data || !nd.data.desc || !nd.style || !is_array(nd.style.ports)) {
|
|
1815
|
+
continue;
|
|
1816
|
+
}
|
|
1817
|
+
let desc = nd.data.desc;
|
|
1818
|
+
let fkey_ports = [];
|
|
1819
|
+
let hook_ports = [];
|
|
1820
|
+
for(let port of nd.style.ports) {
|
|
1821
|
+
let col = get_col_by_id(desc, port.key);
|
|
1822
|
+
let copy = Object.assign({}, port);
|
|
1823
|
+
if(col && col.fkey) {
|
|
1824
|
+
fkey_ports.push(copy);
|
|
1825
|
+
} else {
|
|
1826
|
+
hook_ports.push(copy);
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
place_ports(gobj, fkey_ports, hook_ports);
|
|
1830
|
+
node_updates.push({id: nd.id, style: {ports: [...fkey_ports, ...hook_ports]}});
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1833
|
+
let type = edge_type_for(gobj);
|
|
1834
|
+
let edge_updates = [];
|
|
1835
|
+
for(let ed of (graph.getEdgeData() || [])) {
|
|
1836
|
+
if(ed && ed.type !== type) {
|
|
1837
|
+
edge_updates.push({id: ed.id, type: type});
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
|
|
1841
|
+
try {
|
|
1842
|
+
if(node_updates.length) {
|
|
1843
|
+
graph.updateNodeData(node_updates);
|
|
1844
|
+
}
|
|
1845
|
+
if(edge_updates.length) {
|
|
1846
|
+
graph.updateEdgeData(edge_updates);
|
|
1847
|
+
}
|
|
1848
|
+
} catch(e) {
|
|
1849
|
+
log_error(`${gobj_short_name(gobj)}: cannot turn the ports: ${e}`);
|
|
1850
|
+
}
|
|
1700
1851
|
}
|
|
1701
1852
|
|
|
1702
1853
|
/************************************************************
|
|
@@ -1714,18 +1865,14 @@ function create_topic_node(gobj, desc, record)
|
|
|
1714
1865
|
let xy = get_default_ne_xy(gobj);
|
|
1715
1866
|
let geometry = get_node_graph_props(gobj, desc.topic_name, record.id, record);
|
|
1716
1867
|
|
|
1717
|
-
/*
|
|
1718
|
-
*
|
|
1719
|
-
* `
|
|
1720
|
-
*
|
|
1721
|
-
*
|
|
1722
|
-
*
|
|
1723
|
-
*
|
|
1724
|
-
|
|
1725
|
-
if(geometry_has_position(geometry)) {
|
|
1726
|
-
priv._nodes_placed++;
|
|
1727
|
-
}
|
|
1728
|
-
|
|
1868
|
+
/* The two kw_get_int() below carry KW_CREATE: `get_node_graph_props()`
|
|
1869
|
+
* hands back the record's own `_geometry` object when it has one — and
|
|
1870
|
+
* `{}` is one — so they WRITE the invented cascade coordinates into
|
|
1871
|
+
* it. That is why the placed/total count auto_layout decides on is
|
|
1872
|
+
* taken over the records BEFORE any card is built
|
|
1873
|
+
* (count_placed_records): asked afterwards, every node of every
|
|
1874
|
+
* treedb answers "placed" -- 126 of 126, all of them positions the
|
|
1875
|
+
* app had just made up. */
|
|
1729
1876
|
let x = kw_get_int(gobj, geometry, "x", xy, kw_flag_t.KW_CREATE);
|
|
1730
1877
|
let y = kw_get_int(gobj, geometry, "y", xy, kw_flag_t.KW_CREATE);
|
|
1731
1878
|
|
|
@@ -1745,13 +1892,18 @@ function create_topic_node(gobj, desc, record)
|
|
|
1745
1892
|
//labelText: desc.topic_name + "^" + record.id,
|
|
1746
1893
|
};
|
|
1747
1894
|
|
|
1895
|
+
/* The pills are part of the card, so the card is taller when it
|
|
1896
|
+
* has any: a row of them on a 96px card sat on the subtitle. */
|
|
1897
|
+
let pills_html = pills_html_of_key(gobj, fold_node_key(desc.topic_name, String(record.id)));
|
|
1898
|
+
let size = card_size_for(node_treedb_type, !!pills_html);
|
|
1899
|
+
|
|
1748
1900
|
if(node_treedb_type === 'child') {
|
|
1749
1901
|
// Pure child (LEAF): smallest tier. Rounded-rect chip,
|
|
1750
1902
|
// same card language as entities, lighter.
|
|
1751
1903
|
node_graph_type = 'html';
|
|
1752
|
-
style.size =
|
|
1753
|
-
style.dx = -
|
|
1754
|
-
style.dy = -
|
|
1904
|
+
style.size = size;
|
|
1905
|
+
style.dx = -size[0] / 2;
|
|
1906
|
+
style.dy = -size[1] / 2;
|
|
1755
1907
|
style.innerHTML = build_chip_innerHTML(
|
|
1756
1908
|
desc.color, priv.theme, record.icon,
|
|
1757
1909
|
node_label(desc, record), record.id
|
|
@@ -1762,23 +1914,23 @@ function create_topic_node(gobj, desc, record)
|
|
|
1762
1914
|
// with name; "structural" style (neutral grey, dashed
|
|
1763
1915
|
// border) to read as a container/junction.
|
|
1764
1916
|
node_graph_type = 'html';
|
|
1765
|
-
style.size =
|
|
1766
|
-
style.dx = -
|
|
1767
|
-
style.dy = -
|
|
1917
|
+
style.size = size;
|
|
1918
|
+
style.dx = -size[0] / 2;
|
|
1919
|
+
style.dy = -size[1] / 2;
|
|
1768
1920
|
style.innerHTML = build_node_innerHTML(
|
|
1769
1921
|
desc.color, priv.theme, record.icon, node_label(desc, record),
|
|
1770
|
-
desc.topic_name, true, record.id
|
|
1922
|
+
desc.topic_name, true, record.id, false, false, false, pills_html
|
|
1771
1923
|
);
|
|
1772
1924
|
|
|
1773
1925
|
} else {
|
|
1774
1926
|
// Hierarchical entity (ROOT / container): largest tier.
|
|
1775
1927
|
node_graph_type = 'html';
|
|
1776
|
-
style.size =
|
|
1777
|
-
style.dx = -
|
|
1778
|
-
style.dy = -
|
|
1928
|
+
style.size = size;
|
|
1929
|
+
style.dx = -size[0] / 2;
|
|
1930
|
+
style.dy = -size[1] / 2;
|
|
1779
1931
|
style.innerHTML = build_node_innerHTML(
|
|
1780
1932
|
desc.color, priv.theme, record.icon, node_label(desc, record),
|
|
1781
|
-
desc.topic_name, false, record.id
|
|
1933
|
+
desc.topic_name, false, record.id, false, false, false, pills_html
|
|
1782
1934
|
);
|
|
1783
1935
|
}
|
|
1784
1936
|
|
|
@@ -1875,40 +2027,9 @@ function update_topic_node(gobj, desc, node_name, record)
|
|
|
1875
2027
|
// Update record data
|
|
1876
2028
|
nodedata.data.record = record;
|
|
1877
2029
|
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
id: node_name,
|
|
1882
|
-
style: {
|
|
1883
|
-
innerHTML: build_chip_innerHTML(
|
|
1884
|
-
desc.color, priv.theme, record.icon,
|
|
1885
|
-
node_label(desc, record), record.id
|
|
1886
|
-
),
|
|
1887
|
-
}
|
|
1888
|
-
}]);
|
|
1889
|
-
} else if(desc.node_treedb_type === 'extended') {
|
|
1890
|
-
graph.updateNodeData([{
|
|
1891
|
-
id: node_name,
|
|
1892
|
-
style: {
|
|
1893
|
-
innerHTML: build_node_innerHTML(
|
|
1894
|
-
desc.color, priv.theme, record.icon,
|
|
1895
|
-
node_label(desc, record),
|
|
1896
|
-
desc.topic_name, true, record.id
|
|
1897
|
-
),
|
|
1898
|
-
}
|
|
1899
|
-
}]);
|
|
1900
|
-
} else if(desc.node_treedb_type === 'hierarchical') {
|
|
1901
|
-
graph.updateNodeData([{
|
|
1902
|
-
id: node_name,
|
|
1903
|
-
style: {
|
|
1904
|
-
innerHTML: build_node_innerHTML(
|
|
1905
|
-
desc.color, priv.theme, record.icon,
|
|
1906
|
-
node_label(desc, record),
|
|
1907
|
-
desc.topic_name, false, record.id
|
|
1908
|
-
),
|
|
1909
|
-
}
|
|
1910
|
-
}]);
|
|
1911
|
-
}
|
|
2030
|
+
/* Regenerate the card for the new record, with the flags it
|
|
2031
|
+
* carries right now (repaint_cards reads them where they live). */
|
|
2032
|
+
repaint_cards(gobj, new Set([node_name]));
|
|
1912
2033
|
} catch(e) {
|
|
1913
2034
|
log_error(e.message);
|
|
1914
2035
|
}
|
|
@@ -1972,22 +2093,6 @@ function remove_local_node(gobj, topic_name, node)
|
|
|
1972
2093
|
}
|
|
1973
2094
|
}
|
|
1974
2095
|
|
|
1975
|
-
/************************************************************
|
|
1976
|
-
* Create all links from records, initial load from parent
|
|
1977
|
-
************************************************************/
|
|
1978
|
-
function create_links(gobj)
|
|
1979
|
-
{
|
|
1980
|
-
let priv = gobj.priv;
|
|
1981
|
-
|
|
1982
|
-
for(const [topic_name, records] of Object.entries(priv.records)) {
|
|
1983
|
-
let desc = priv.descs[topic_name];
|
|
1984
|
-
for(let i = 0; i < records.length; i++) {
|
|
1985
|
-
let record = records[i];
|
|
1986
|
-
draw_links(gobj, desc, record, true);
|
|
1987
|
-
}
|
|
1988
|
-
}
|
|
1989
|
-
}
|
|
1990
|
-
|
|
1991
2096
|
/************************************************************
|
|
1992
2097
|
* Collect all fkey references from a record as a Set.
|
|
1993
2098
|
*
|
|
@@ -2115,9 +2220,30 @@ function draw_link(
|
|
|
2115
2220
|
}
|
|
2116
2221
|
|
|
2117
2222
|
/*
|
|
2118
|
-
* Child node (me)
|
|
2223
|
+
* Child node (me) -- on screen too, or there is nothing to draw
|
|
2224
|
+
* to: a folded child's parent is visible and the child is not.
|
|
2119
2225
|
*/
|
|
2120
2226
|
let child_node = build_node_name(gobj, child_topic, child_id);
|
|
2227
|
+
let child_cell = null;
|
|
2228
|
+
try {
|
|
2229
|
+
child_cell = graph.getNodeData(child_node);
|
|
2230
|
+
} catch(e) {
|
|
2231
|
+
child_cell = null;
|
|
2232
|
+
}
|
|
2233
|
+
if(!child_cell) {
|
|
2234
|
+
if(verbose) {
|
|
2235
|
+
log_error(`${gobj_short_name(gobj)}: child node NOT FOUND: ${child_node}`);
|
|
2236
|
+
}
|
|
2237
|
+
return;
|
|
2238
|
+
}
|
|
2239
|
+
|
|
2240
|
+
/* Once. The reconcile after a fold redraws the links of every
|
|
2241
|
+
* visible node, and an edge id is a counter: without this every
|
|
2242
|
+
* reconcile laid a second line over the first. */
|
|
2243
|
+
if(get_edge_by_link(gobj, parent_topic, parent_id, hook_name,
|
|
2244
|
+
child_topic, child_id, fkey_name)) {
|
|
2245
|
+
return;
|
|
2246
|
+
}
|
|
2121
2247
|
|
|
2122
2248
|
/*
|
|
2123
2249
|
* Create the edge with independent id and semantic data
|
|
@@ -2177,7 +2303,7 @@ function draw_link(
|
|
|
2177
2303
|
|
|
2178
2304
|
let edge = {
|
|
2179
2305
|
id: build_edge_id(gobj),
|
|
2180
|
-
type:
|
|
2306
|
+
type: edge_type_for(gobj),
|
|
2181
2307
|
source: parent_node,
|
|
2182
2308
|
target: child_node,
|
|
2183
2309
|
style: {
|
|
@@ -2389,9 +2515,14 @@ function save_geometry(gobj)
|
|
|
2389
2515
|
let priv = gobj.priv;
|
|
2390
2516
|
let graph = priv.graph;
|
|
2391
2517
|
|
|
2392
|
-
|
|
2518
|
+
/* Collect geometry for the nodes ON SCREEN into _graph_properties.
|
|
2519
|
+
* A folded node keeps the entry it had: nothing here touches it.
|
|
2520
|
+
* A `+N` chip is not a record and has no entry to keep. */
|
|
2393
2521
|
const nodes = graph.getData().nodes;
|
|
2394
2522
|
for(let i = 0; i < nodes.length; i++) {
|
|
2523
|
+
if(!nodes[i].data || !nodes[i].data.desc) {
|
|
2524
|
+
continue;
|
|
2525
|
+
}
|
|
2395
2526
|
update_geometry(gobj, nodes[i].id);
|
|
2396
2527
|
}
|
|
2397
2528
|
|
|
@@ -2720,60 +2851,117 @@ async function graph_fit_selection(gobj, ids)
|
|
|
2720
2851
|
* once the data loads (see ac_load_data). Guarded end-to-end — a
|
|
2721
2852
|
* missing G6 API or an unknown topic logs and no-ops, never throws.
|
|
2722
2853
|
************************************************************/
|
|
2723
|
-
function graph_focus_topic(gobj, topic)
|
|
2854
|
+
function graph_focus_topic(gobj, topic, reveal_all)
|
|
2724
2855
|
{
|
|
2725
2856
|
let priv = gobj.priv;
|
|
2726
2857
|
let graph = priv.graph;
|
|
2727
|
-
if(!graph || !priv.graph_rendered) {
|
|
2858
|
+
if(!graph || !priv.graph_rendered || !priv._fold_model) {
|
|
2728
2859
|
priv._pending_focus_topic = topic;
|
|
2860
|
+
priv._pending_focus_all = !!reveal_all;
|
|
2729
2861
|
return;
|
|
2730
2862
|
}
|
|
2731
2863
|
|
|
2732
|
-
let
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2864
|
+
let model = priv._fold_model;
|
|
2865
|
+
let state = priv._fold_state;
|
|
2866
|
+
|
|
2867
|
+
/* "Show me these": the ones on screen are highlighted, and the
|
|
2868
|
+
* hidden ones are opened up to. How many depends on who asks.
|
|
2869
|
+
* A click on the LEGEND (`reveal_all`) means every record of the
|
|
2870
|
+
* topic, however deep it is folded away. The graph's per-topic
|
|
2871
|
+
* ROUTE lands here on every load (`.../graph/devices` is a focus
|
|
2872
|
+
* on `devices`) and gets ONE PAGE, the same cap the find has:
|
|
2873
|
+
* revealing a whole topic on landing opened 563 groups of a
|
|
2874
|
+
* 6400-record treedb -- the pile, back, before anybody touched
|
|
2875
|
+
* anything. */
|
|
2876
|
+
let keys = [];
|
|
2877
|
+
if(!empty_string(topic)) {
|
|
2878
|
+
for(let node of model.nodes.values()) {
|
|
2879
|
+
if(node.topic_name === topic) {
|
|
2880
|
+
keys.push(node.key);
|
|
2881
|
+
}
|
|
2882
|
+
}
|
|
2883
|
+
if(keys.length === 0) {
|
|
2884
|
+
log_warning(`${gobj_short_name(gobj)}: focus topic '${topic}' has no nodes`);
|
|
2885
|
+
}
|
|
2886
|
+
let visible = fold_visible_set(model, state);
|
|
2887
|
+
let revealed = 0;
|
|
2888
|
+
for(let key of keys) {
|
|
2889
|
+
if(visible.has(key)) {
|
|
2890
|
+
continue;
|
|
2891
|
+
}
|
|
2892
|
+
if(!reveal_all && revealed >= state.page_size) {
|
|
2893
|
+
break;
|
|
2894
|
+
}
|
|
2895
|
+
fold_reveal(model, state, key);
|
|
2896
|
+
revealed++;
|
|
2897
|
+
}
|
|
2898
|
+
}
|
|
2899
|
+
priv._focus_topic = topic || null;
|
|
2900
|
+
|
|
2901
|
+
reconcile_fold(gobj, {}).then(() => {
|
|
2902
|
+
apply_focus_ids(gobj, keys, "focus topic");
|
|
2903
|
+
});
|
|
2904
|
+
}
|
|
2905
|
+
|
|
2906
|
+
/************************************************************
|
|
2907
|
+
* Paint the highlight on these records (by model key) and put
|
|
2908
|
+
* the first of them in the middle. Shared by the find and the
|
|
2909
|
+
* topic focus, after the reconcile that brought them on screen.
|
|
2910
|
+
* Keys that are still off screen are skipped: a match beyond the
|
|
2911
|
+
* page a reveal opened is counted, not painted.
|
|
2912
|
+
************************************************************/
|
|
2913
|
+
function apply_focus_ids(gobj, keys, what)
|
|
2914
|
+
{
|
|
2915
|
+
let priv = gobj.priv;
|
|
2916
|
+
let graph = priv.graph;
|
|
2917
|
+
if(!graph) {
|
|
2737
2918
|
return;
|
|
2738
2919
|
}
|
|
2739
|
-
let nodes = is_array(data.nodes) ? data.nodes : [];
|
|
2740
2920
|
|
|
2741
|
-
/* Clear the previous focus highlight, then set the new one. */
|
|
2742
2921
|
let states = {};
|
|
2743
2922
|
for(let id of (priv._focus_ids || [])) {
|
|
2744
2923
|
states[id] = [];
|
|
2745
2924
|
}
|
|
2746
2925
|
let ids = [];
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
}
|
|
2753
|
-
|
|
2754
|
-
log_warning(`${gobj_short_name(gobj)}: focus topic '${topic}' has no nodes`);
|
|
2926
|
+
for(let key of keys) {
|
|
2927
|
+
let id = node_id_of_key(gobj, key);
|
|
2928
|
+
let nd = null;
|
|
2929
|
+
try {
|
|
2930
|
+
nd = graph.getNodeData(id);
|
|
2931
|
+
} catch(e) {
|
|
2932
|
+
nd = null;
|
|
2755
2933
|
}
|
|
2756
|
-
|
|
2934
|
+
if(nd) {
|
|
2935
|
+
ids.push(id);
|
|
2757
2936
|
states[id] = ['active'];
|
|
2758
2937
|
}
|
|
2759
2938
|
}
|
|
2760
2939
|
let prev_ids = priv._focus_ids || [];
|
|
2761
2940
|
priv._focus_ids = ids;
|
|
2762
|
-
priv._focus_topic = topic || null;
|
|
2763
2941
|
|
|
2764
2942
|
try {
|
|
2765
2943
|
if(typeof graph.setElementState === "function") {
|
|
2766
2944
|
graph.setElementState(states);
|
|
2767
2945
|
}
|
|
2768
2946
|
apply_node_highlight(gobj, prev_ids, ids);
|
|
2769
|
-
if(ids.length
|
|
2770
|
-
graph
|
|
2947
|
+
if(ids.length) {
|
|
2948
|
+
yui_graph_center_on(graph, ids[0]);
|
|
2771
2949
|
}
|
|
2772
2950
|
} catch(e) {
|
|
2773
|
-
log_error(`${gobj_short_name(gobj)}:
|
|
2951
|
+
log_error(`${gobj_short_name(gobj)}: ${what} apply failed: ${e}`);
|
|
2774
2952
|
}
|
|
2775
2953
|
}
|
|
2776
2954
|
|
|
2955
|
+
/* "topic^id" -> the G6 node id. */
|
|
2956
|
+
function node_id_of_key(gobj, key)
|
|
2957
|
+
{
|
|
2958
|
+
let i = key.indexOf("^");
|
|
2959
|
+
if(i < 0) {
|
|
2960
|
+
return "";
|
|
2961
|
+
}
|
|
2962
|
+
return build_node_name(gobj, key.slice(0, i), key.slice(i + 1));
|
|
2963
|
+
}
|
|
2964
|
+
|
|
2777
2965
|
/************************************************************
|
|
2778
2966
|
* Find the nodes whose name, id or topic contains `term`, highlight
|
|
2779
2967
|
* them with the same amber 'active' state the topic focus uses, and
|
|
@@ -2792,71 +2980,64 @@ function graph_find_nodes(gobj, term)
|
|
|
2792
2980
|
{
|
|
2793
2981
|
let priv = gobj.priv;
|
|
2794
2982
|
let graph = priv.graph;
|
|
2795
|
-
if(!graph || !priv.graph_rendered) {
|
|
2983
|
+
if(!graph || !priv.graph_rendered || !priv._fold_model) {
|
|
2796
2984
|
priv._pending_find = term;
|
|
2797
2985
|
return 0;
|
|
2798
2986
|
}
|
|
2799
2987
|
|
|
2800
|
-
let
|
|
2801
|
-
|
|
2802
|
-
data = graph.getData() || {};
|
|
2803
|
-
} catch(e) {
|
|
2804
|
-
log_error(`${gobj_short_name(gobj)}: graph.getData() failed: ${e}`);
|
|
2805
|
-
return 0;
|
|
2806
|
-
}
|
|
2807
|
-
let nodes = is_array(data.nodes) ? data.nodes : [];
|
|
2808
|
-
|
|
2809
|
-
let states = {};
|
|
2810
|
-
for(let id of (priv._focus_ids || [])) {
|
|
2811
|
-
states[id] = [];
|
|
2812
|
-
}
|
|
2988
|
+
let model = priv._fold_model;
|
|
2989
|
+
let state = priv._fold_state;
|
|
2813
2990
|
|
|
2814
|
-
|
|
2991
|
+
/* Searched over the RECORDS, not the cards: most of the treedb is
|
|
2992
|
+
* folded away, and a find that only read what was on screen would
|
|
2993
|
+
* answer "nothing" for a device three levels down. */
|
|
2994
|
+
let keys = [];
|
|
2815
2995
|
if(!empty_string(term)) {
|
|
2816
2996
|
let needle = String(term).toLowerCase();
|
|
2817
|
-
for(let
|
|
2818
|
-
|
|
2819
|
-
continue;
|
|
2820
|
-
}
|
|
2821
|
-
let record = n.data.record || {};
|
|
2997
|
+
for(let node of model.nodes.values()) {
|
|
2998
|
+
let record = node.record || {};
|
|
2822
2999
|
let label = "";
|
|
2823
3000
|
try {
|
|
2824
|
-
label = node_label(
|
|
3001
|
+
label = node_label(priv.descs[node.topic_name] || {}, record) || "";
|
|
2825
3002
|
} catch(e) {
|
|
2826
3003
|
label = "";
|
|
2827
3004
|
}
|
|
2828
3005
|
let haystack = [
|
|
2829
3006
|
label,
|
|
2830
3007
|
record.id,
|
|
2831
|
-
|
|
3008
|
+
node.topic_name
|
|
2832
3009
|
].filter((v) => typeof v === "string").join(" ").toLowerCase();
|
|
2833
3010
|
|
|
2834
3011
|
if(haystack.includes(needle)) {
|
|
2835
|
-
|
|
3012
|
+
keys.push(node.key);
|
|
2836
3013
|
}
|
|
2837
3014
|
}
|
|
2838
|
-
for(let id of ids) {
|
|
2839
|
-
states[id] = ['active'];
|
|
2840
|
-
}
|
|
2841
3015
|
}
|
|
2842
3016
|
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
3017
|
+
/* Hidden matches are opened up to, one page of them at most: a
|
|
3018
|
+
* single letter matches half the treedb, and unfolding half the
|
|
3019
|
+
* treedb on a keystroke is the pile this whole thing exists to
|
|
3020
|
+
* avoid. The count reported is of all the matches. */
|
|
3021
|
+
let visible = fold_visible_set(model, state);
|
|
3022
|
+
let revealed = 0;
|
|
3023
|
+
for(let key of keys) {
|
|
3024
|
+
if(visible.has(key)) {
|
|
3025
|
+
continue;
|
|
2850
3026
|
}
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
graph.focusElement(ids);
|
|
3027
|
+
if(revealed >= state.page_size) {
|
|
3028
|
+
break;
|
|
2854
3029
|
}
|
|
2855
|
-
|
|
2856
|
-
|
|
3030
|
+
fold_reveal(model, state, key);
|
|
3031
|
+
revealed++;
|
|
2857
3032
|
}
|
|
2858
3033
|
|
|
2859
|
-
|
|
3034
|
+
priv._focus_topic = null; /* a find takes over the highlight */
|
|
3035
|
+
|
|
3036
|
+
reconcile_fold(gobj, {}).then(() => {
|
|
3037
|
+
apply_focus_ids(gobj, keys, "find");
|
|
3038
|
+
});
|
|
3039
|
+
|
|
3040
|
+
return keys.length;
|
|
2860
3041
|
}
|
|
2861
3042
|
|
|
2862
3043
|
async function graph_zoom_in(gobj)
|
|
@@ -3067,21 +3248,39 @@ class LightNode extends Circle
|
|
|
3067
3248
|
class ManualLayout extends BaseLayout
|
|
3068
3249
|
{
|
|
3069
3250
|
async execute(data, options) {
|
|
3070
|
-
const { nodes = [] } = data;
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3251
|
+
const { nodes = [], edges = [] } = data;
|
|
3252
|
+
|
|
3253
|
+
let placed = {};
|
|
3254
|
+
let out = [];
|
|
3255
|
+
let chips = [];
|
|
3256
|
+
for(let node of nodes) {
|
|
3257
|
+
let d = node.data || {};
|
|
3258
|
+
if(d.more) {
|
|
3259
|
+
chips.push(node); /* a `+N` chip: placed by its parent below */
|
|
3260
|
+
continue;
|
|
3261
|
+
}
|
|
3262
|
+
let gp = d.graph_props;
|
|
3263
|
+
let geo = d.record? d.record._geometry : null;
|
|
3264
|
+
let x = (gp && gp.x != null) ? gp.x :
|
|
3265
|
+
(geo && geo.x != null) ? geo.x : 0;
|
|
3266
|
+
let y = (gp && gp.y != null) ? gp.y :
|
|
3267
|
+
(geo && geo.y != null) ? geo.y : 0;
|
|
3268
|
+
placed[node.id] = [x, y];
|
|
3269
|
+
out.push({id: node.id, style: {x: x, y: y}});
|
|
3270
|
+
}
|
|
3271
|
+
|
|
3272
|
+
/* A chip has no saved place of its own and never will: it sits
|
|
3273
|
+
* under the card it continues, or where it was born if it is
|
|
3274
|
+
* a root group's. */
|
|
3275
|
+
for(let node of chips) {
|
|
3276
|
+
let edge = edges.find((e) => e.target === node.id);
|
|
3277
|
+
let p = edge? placed[edge.source] : null;
|
|
3278
|
+
let x = p? p[0] : ((node.style && node.style.x) || 0);
|
|
3279
|
+
let y = p? p[1] + 80 : ((node.style && node.style.y) || 0);
|
|
3280
|
+
out.push({id: node.id, style: {x: x, y: y}});
|
|
3281
|
+
}
|
|
3282
|
+
|
|
3283
|
+
return {nodes: out};
|
|
3085
3284
|
}
|
|
3086
3285
|
}
|
|
3087
3286
|
|
|
@@ -5386,7 +5585,8 @@ function show_node_popover(gobj)
|
|
|
5386
5585
|
updateStyle.innerHTML = build_node_innerHTML(
|
|
5387
5586
|
fill, priv.theme, record.icon,
|
|
5388
5587
|
node_label(nodeData.data.desc, record),
|
|
5389
|
-
nodeData.data.desc.topic_name, false, record.id
|
|
5588
|
+
nodeData.data.desc.topic_name, false, record.id,
|
|
5589
|
+
false, false, false, pills_html_of(gobj, nodeData)
|
|
5390
5590
|
);
|
|
5391
5591
|
}
|
|
5392
5592
|
graph.updateNodeData([{ id: node_id, style: updateStyle }]);
|
|
@@ -5424,7 +5624,8 @@ function show_node_popover(gobj)
|
|
|
5424
5624
|
restoreStyle.innerHTML = build_node_innerHTML(
|
|
5425
5625
|
origFill, priv.theme, record.icon,
|
|
5426
5626
|
node_label(nodeData.data.desc, record),
|
|
5427
|
-
nodeData.data.desc.topic_name, false, record.id
|
|
5627
|
+
nodeData.data.desc.topic_name, false, record.id,
|
|
5628
|
+
false, false, false, pills_html_of(gobj, nodeData)
|
|
5428
5629
|
);
|
|
5429
5630
|
}
|
|
5430
5631
|
graph.updateNodeData([{ id: node_id, style: restoreStyle }]);
|
|
@@ -5746,6 +5947,11 @@ function refresh_minimap(gobj)
|
|
|
5746
5947
|
let size = (nd && nd.style && nd.style.size) || [120, 60];
|
|
5747
5948
|
let color = (nd && nd.data && nd.data.desc && nd.data.desc.color)
|
|
5748
5949
|
|| "#94a3b8";
|
|
5950
|
+
if(is_more_node(nd)) {
|
|
5951
|
+
/* A `+N` chip is a cut, not a card: a faint
|
|
5952
|
+
* mark at that scale. */
|
|
5953
|
+
color = "rgba(148, 163, 184, 0.35)";
|
|
5954
|
+
}
|
|
5749
5955
|
return new RectGeometry({
|
|
5750
5956
|
style: {
|
|
5751
5957
|
x: -size[0] / 2,
|
|
@@ -5772,7 +5978,7 @@ function refresh_minimap(gobj)
|
|
|
5772
5978
|
* same three-way choice — it lived inline in the theme refresh and is
|
|
5773
5979
|
* shared now. Returns null for a node that carries no desc.
|
|
5774
5980
|
************************************************************/
|
|
5775
|
-
function node_innerHTML_of(nd, theme, highlight, selected, anchored)
|
|
5981
|
+
function node_innerHTML_of(nd, theme, highlight, selected, anchored, pills_html)
|
|
5776
5982
|
{
|
|
5777
5983
|
if(!nd || !nd.data || !nd.data.desc) {
|
|
5778
5984
|
return null;
|
|
@@ -5790,12 +5996,14 @@ function node_innerHTML_of(nd, theme, highlight, selected, anchored)
|
|
|
5790
5996
|
case 'extended':
|
|
5791
5997
|
return build_node_innerHTML(
|
|
5792
5998
|
desc.color, theme, record.icon, label,
|
|
5793
|
-
desc.topic_name, true, record.id, highlight, selected, anchored
|
|
5999
|
+
desc.topic_name, true, record.id, highlight, selected, anchored,
|
|
6000
|
+
pills_html
|
|
5794
6001
|
);
|
|
5795
6002
|
case 'hierarchical':
|
|
5796
6003
|
return build_node_innerHTML(
|
|
5797
6004
|
desc.color, theme, record.icon, label,
|
|
5798
|
-
desc.topic_name, false, record.id, highlight, selected, anchored
|
|
6005
|
+
desc.topic_name, false, record.id, highlight, selected, anchored,
|
|
6006
|
+
pills_html
|
|
5799
6007
|
);
|
|
5800
6008
|
}
|
|
5801
6009
|
return null;
|
|
@@ -5824,9 +6032,16 @@ function repaint_cards(gobj, ids)
|
|
|
5824
6032
|
|
|
5825
6033
|
let updates = [];
|
|
5826
6034
|
for(let id of ids) {
|
|
6035
|
+
let nd;
|
|
6036
|
+
try {
|
|
6037
|
+
nd = graph.getNodeData(id);
|
|
6038
|
+
} catch(e) {
|
|
6039
|
+
continue; /* gone since it was listed */
|
|
6040
|
+
}
|
|
5827
6041
|
let html = node_innerHTML_of(
|
|
5828
|
-
|
|
5829
|
-
priv.anchor_state === "on" && id === priv.anchor_id
|
|
6042
|
+
nd, priv.theme, focus.has(id), selected.has(id),
|
|
6043
|
+
priv.anchor_state === "on" && id === priv.anchor_id,
|
|
6044
|
+
pills_html_of(gobj, nd)
|
|
5830
6045
|
);
|
|
5831
6046
|
if(html !== null) {
|
|
5832
6047
|
updates.push({id: id, style: {innerHTML: html}});
|
|
@@ -5882,9 +6097,15 @@ function refresh_html_nodes_theme(gobj, theme)
|
|
|
5882
6097
|
let updates = [];
|
|
5883
6098
|
for(let i = 0; i < nodes.length; i++) {
|
|
5884
6099
|
let id = nodes[i].id;
|
|
6100
|
+
let nd = graph.getNodeData(id);
|
|
5885
6101
|
let html = node_innerHTML_of(
|
|
5886
|
-
|
|
6102
|
+
nd, theme, highlighted.has(id), selected.has(id),
|
|
6103
|
+
priv.anchor_state === "on" && id === priv.anchor_id,
|
|
6104
|
+
pills_html_of(gobj, nd)
|
|
5887
6105
|
);
|
|
6106
|
+
if(html === null) {
|
|
6107
|
+
html = more_innerHTML_of(gobj, nd, theme); /* a `+N` chip, or null */
|
|
6108
|
+
}
|
|
5888
6109
|
if(html !== null) {
|
|
5889
6110
|
updates.push({id: id, style: {innerHTML: html}});
|
|
5890
6111
|
}
|
|
@@ -6036,7 +6257,7 @@ function build_chip_innerHTML(color, theme, icon, label, key, highlight, selecte
|
|
|
6036
6257
|
* colour is kept (per-topic differentiation) but softened via
|
|
6037
6258
|
* color-mix instead of a harsh saturated fill. Theme-aware.
|
|
6038
6259
|
************************************************************/
|
|
6039
|
-
function build_node_innerHTML(color, theme, icon, label, topic_name, structural, key, highlight, selected, anchored)
|
|
6260
|
+
function build_node_innerHTML(color, theme, icon, label, topic_name, structural, key, highlight, selected, anchored, pills_html)
|
|
6040
6261
|
{
|
|
6041
6262
|
let title = key || label;
|
|
6042
6263
|
let dark = (theme === "dark");
|
|
@@ -6091,6 +6312,18 @@ function build_node_innerHTML(color, theme, icon, label, topic_name, structural,
|
|
|
6091
6312
|
">${escapeHtml(topic_name)}</div>`;
|
|
6092
6313
|
}
|
|
6093
6314
|
|
|
6315
|
+
/* The fold pills, one per hook with children: a row under the
|
|
6316
|
+
* subtitle, built by pills_html_of(). Empty for a card with
|
|
6317
|
+
* nothing to fold. */
|
|
6318
|
+
let pills_row = "";
|
|
6319
|
+
if(pills_html) {
|
|
6320
|
+
pills_row = `
|
|
6321
|
+
<div class="TREEDB_PILLS" style="
|
|
6322
|
+
display: flex; flex-wrap: nowrap; gap: 4px; justify-content: center;
|
|
6323
|
+
margin-top: 6px; max-width: 100%; overflow: hidden;
|
|
6324
|
+
">${pills_html}</div>`;
|
|
6325
|
+
}
|
|
6326
|
+
|
|
6094
6327
|
return `
|
|
6095
6328
|
<div class="TREEDB_CARD" title="${escapeHtml(title)}" style="
|
|
6096
6329
|
box-sizing: border-box;
|
|
@@ -6116,7 +6349,7 @@ function build_node_innerHTML(color, theme, icon, label, topic_name, structural,
|
|
|
6116
6349
|
max-width: 100%; overflow: hidden; text-overflow: ellipsis;
|
|
6117
6350
|
display: -webkit-box; -webkit-line-clamp: 2;
|
|
6118
6351
|
-webkit-box-orient: vertical; word-break: break-word;
|
|
6119
|
-
">${escapeHtml(label)}</div>${sub_html}
|
|
6352
|
+
">${escapeHtml(label)}</div>${sub_html}${pills_row}
|
|
6120
6353
|
</div>
|
|
6121
6354
|
`;
|
|
6122
6355
|
}
|
|
@@ -6172,7 +6405,7 @@ function select_all_nodes(gobj)
|
|
|
6172
6405
|
}
|
|
6173
6406
|
|
|
6174
6407
|
let nodes = (graph.getData() || {}).nodes || [];
|
|
6175
|
-
set_selection(gobj, nodes.map((nd) => nd.id));
|
|
6408
|
+
set_selection(gobj, nodes.filter((nd) => nd.data && nd.data.desc).map((nd) => nd.id));
|
|
6176
6409
|
}
|
|
6177
6410
|
|
|
6178
6411
|
/************************************************************
|
|
@@ -6612,7 +6845,8 @@ function apply_node_properties(gobj, node_id, fill, stroke, lineWidth, scope)
|
|
|
6612
6845
|
updateStyle.innerHTML = build_node_innerHTML(
|
|
6613
6846
|
fill, priv.theme, record.icon,
|
|
6614
6847
|
node_label(nd.data.desc, record),
|
|
6615
|
-
nd.data.desc.topic_name, false, record.id
|
|
6848
|
+
nd.data.desc.topic_name, false, record.id,
|
|
6849
|
+
false, false, false, pills_html_of(gobj, nd)
|
|
6616
6850
|
);
|
|
6617
6851
|
}
|
|
6618
6852
|
updates.push({ id: nodes[i].id, style: updateStyle });
|
|
@@ -6706,8 +6940,8 @@ function update_edge_geometry(gobj, edge_id)
|
|
|
6706
6940
|
let graph = priv.graph;
|
|
6707
6941
|
|
|
6708
6942
|
let edgeData = graph.getEdgeData(edge_id);
|
|
6709
|
-
if(!edgeData || !edgeData.data) {
|
|
6710
|
-
return;
|
|
6943
|
+
if(!edgeData || !edgeData.data || !edgeData.data.parent_topic) {
|
|
6944
|
+
return; /* a `+N` chip's line: not a link, nothing to save */
|
|
6711
6945
|
}
|
|
6712
6946
|
|
|
6713
6947
|
let style = edgeData.style || {};
|
|
@@ -6931,8 +7165,8 @@ function copy_size_to_nodes(gobj, same_topic_only)
|
|
|
6931
7165
|
const nodes = graph.getData().nodes;
|
|
6932
7166
|
for(let i = 0; i < nodes.length; i++) {
|
|
6933
7167
|
let nd = graph.getNodeData(nodes[i].id);
|
|
6934
|
-
if(!nd || !nd.data) {
|
|
6935
|
-
continue;
|
|
7168
|
+
if(!nd || !nd.data || !nd.data.desc) {
|
|
7169
|
+
continue; /* a `+N` chip keeps its own size */
|
|
6936
7170
|
}
|
|
6937
7171
|
if(same_topic_only && nd.data.topic_name !== source_topic) {
|
|
6938
7172
|
continue;
|
|
@@ -7063,6 +7297,581 @@ function copy_size_to_ports(gobj, same_topic_only)
|
|
|
7063
7297
|
}
|
|
7064
7298
|
|
|
7065
7299
|
|
|
7300
|
+
/***************************
|
|
7301
|
+
* Folding
|
|
7302
|
+
***************************/
|
|
7303
|
+
|
|
7304
|
+
|
|
7305
|
+
|
|
7306
|
+
|
|
7307
|
+
/* A card with pills is this much taller; the row sits under the subtitle. */
|
|
7308
|
+
const PILL_ROW_HEIGHT = 24;
|
|
7309
|
+
const MORE_CHIP_SIZE = [76, 26];
|
|
7310
|
+
|
|
7311
|
+
/************************************************************
|
|
7312
|
+
* The three card sizes, with room for the pill row when there
|
|
7313
|
+
* is one. A chip (pure child) never has pills: it has no hooks.
|
|
7314
|
+
************************************************************/
|
|
7315
|
+
function card_size_for(node_treedb_type, has_pills)
|
|
7316
|
+
{
|
|
7317
|
+
let extra = has_pills? PILL_ROW_HEIGHT : 0;
|
|
7318
|
+
switch(node_treedb_type) {
|
|
7319
|
+
case 'child':
|
|
7320
|
+
return [116, 40];
|
|
7321
|
+
case 'extended':
|
|
7322
|
+
return [144, 66 + extra];
|
|
7323
|
+
default:
|
|
7324
|
+
return [172, 96 + extra];
|
|
7325
|
+
}
|
|
7326
|
+
}
|
|
7327
|
+
|
|
7328
|
+
/************************************************************
|
|
7329
|
+
* The model's key of a G6 record node, or "" for anything else.
|
|
7330
|
+
************************************************************/
|
|
7331
|
+
function node_key_of(nd)
|
|
7332
|
+
{
|
|
7333
|
+
if(!nd || !nd.data || !nd.data.desc || !nd.data.record) {
|
|
7334
|
+
return "";
|
|
7335
|
+
}
|
|
7336
|
+
return fold_node_key(nd.data.desc.topic_name, String(nd.data.record.id));
|
|
7337
|
+
}
|
|
7338
|
+
|
|
7339
|
+
function is_more_node(nd)
|
|
7340
|
+
{
|
|
7341
|
+
return !!(nd && nd.data && nd.data.more);
|
|
7342
|
+
}
|
|
7343
|
+
|
|
7344
|
+
function more_node_id(gobj, group_key)
|
|
7345
|
+
{
|
|
7346
|
+
return sprintf("more-%s-%s", gobj.priv.treedb_name, group_key);
|
|
7347
|
+
}
|
|
7348
|
+
|
|
7349
|
+
function more_edge_id(gobj, group_key)
|
|
7350
|
+
{
|
|
7351
|
+
return sprintf("more-edge-%s-%s", gobj.priv.treedb_name, group_key);
|
|
7352
|
+
}
|
|
7353
|
+
|
|
7354
|
+
/************************************************************
|
|
7355
|
+
* The colours of a pill or a chip, from the topic colour it
|
|
7356
|
+
* stands for -- the CHILD topic, the same colour its hook port
|
|
7357
|
+
* wears, so the pill and the port say the same thing.
|
|
7358
|
+
************************************************************/
|
|
7359
|
+
function pill_colors(color, theme)
|
|
7360
|
+
{
|
|
7361
|
+
let dark = (theme === "dark");
|
|
7362
|
+
return {
|
|
7363
|
+
bg: dark
|
|
7364
|
+
? `color-mix(in srgb, ${color} 45%, #1b2230)`
|
|
7365
|
+
: `color-mix(in srgb, ${color} 22%, #ffffff)`,
|
|
7366
|
+
fg: dark
|
|
7367
|
+
? `color-mix(in srgb, ${color} 35%, #ffffff)`
|
|
7368
|
+
: `color-mix(in srgb, ${color} 55%, #0f172a)`,
|
|
7369
|
+
border: dark
|
|
7370
|
+
? `color-mix(in srgb, ${color} 80%, #ffffff)`
|
|
7371
|
+
: color,
|
|
7372
|
+
};
|
|
7373
|
+
}
|
|
7374
|
+
|
|
7375
|
+
/************************************************************
|
|
7376
|
+
* One pill: `▸ devices 142` folded, `▾ devices 24/142` open on a
|
|
7377
|
+
* page, `▾ devices 142` open whole. A glyph and numbers on
|
|
7378
|
+
* purpose: the card is an innerHTML string where a WORD could
|
|
7379
|
+
* carry no i18n key, and the hook name is a schema identifier,
|
|
7380
|
+
* like the topic subtitle above it.
|
|
7381
|
+
************************************************************/
|
|
7382
|
+
function build_pill_html(pill, color, theme)
|
|
7383
|
+
{
|
|
7384
|
+
let c = pill_colors(color, theme);
|
|
7385
|
+
let glyph = pill.open? "▾" : "▸"; /* ▾ / ▸ */
|
|
7386
|
+
let count = (pill.open && pill.shown < pill.total)
|
|
7387
|
+
? `${pill.shown}/${pill.total}`
|
|
7388
|
+
: `${pill.total}`;
|
|
7389
|
+
|
|
7390
|
+
return `<span class="TREEDB_PILL" data-fold-group="${escapeHtml(pill.group_key)}"
|
|
7391
|
+
title="${escapeHtml(pill.hook)}" style="
|
|
7392
|
+
display: inline-flex; align-items: center; gap: 3px; flex: 0 1 auto;
|
|
7393
|
+
height: 16px; padding: 0 6px; border-radius: 8px;
|
|
7394
|
+
background: ${c.bg}; color: ${c.fg}; border: 1px solid ${c.border};
|
|
7395
|
+
font-size: 10px; font-weight: 700; line-height: 1; white-space: nowrap;
|
|
7396
|
+
overflow: hidden; text-overflow: ellipsis;
|
|
7397
|
+
cursor: pointer; user-select: none;
|
|
7398
|
+
"><span style="font-size: 9px;">${glyph}</span>${escapeHtml(pill.hook)}<span style="opacity: .75; font-weight: 600;">${count}</span></span>`;
|
|
7399
|
+
}
|
|
7400
|
+
|
|
7401
|
+
/************************************************************
|
|
7402
|
+
* The pill row of one node, as html; "" when it has nothing to
|
|
7403
|
+
* fold or the model is not built yet.
|
|
7404
|
+
************************************************************/
|
|
7405
|
+
function pills_html_of_key(gobj, node_key)
|
|
7406
|
+
{
|
|
7407
|
+
let priv = gobj.priv;
|
|
7408
|
+
|
|
7409
|
+
if(!priv._fold_model || !priv._fold_state || !node_key) {
|
|
7410
|
+
return "";
|
|
7411
|
+
}
|
|
7412
|
+
let pills = fold_pills_of(priv._fold_model, priv._fold_state, node_key);
|
|
7413
|
+
if(!pills.length) {
|
|
7414
|
+
return "";
|
|
7415
|
+
}
|
|
7416
|
+
let out = [];
|
|
7417
|
+
for(let pill of pills) {
|
|
7418
|
+
let child_desc = priv.descs? priv.descs[pill.child_topic] : null;
|
|
7419
|
+
let color = (child_desc && child_desc.color) || "#94a3b8";
|
|
7420
|
+
out.push(build_pill_html(pill, color, priv.theme));
|
|
7421
|
+
}
|
|
7422
|
+
return out.join("");
|
|
7423
|
+
}
|
|
7424
|
+
|
|
7425
|
+
function pills_html_of(gobj, nd)
|
|
7426
|
+
{
|
|
7427
|
+
return pills_html_of_key(gobj, node_key_of(nd));
|
|
7428
|
+
}
|
|
7429
|
+
|
|
7430
|
+
/* What a card's pills say, as one string -- to repaint only the
|
|
7431
|
+
* cards whose pills changed. */
|
|
7432
|
+
function pills_signature(gobj, node_key)
|
|
7433
|
+
{
|
|
7434
|
+
let priv = gobj.priv;
|
|
7435
|
+
|
|
7436
|
+
if(!priv._fold_model || !priv._fold_state) {
|
|
7437
|
+
return "";
|
|
7438
|
+
}
|
|
7439
|
+
let pills = fold_pills_of(priv._fold_model, priv._fold_state, node_key);
|
|
7440
|
+
return pills.map((p) => `${p.hook}:${p.open? p.shown : "-"}/${p.total}`).join(",");
|
|
7441
|
+
}
|
|
7442
|
+
|
|
7443
|
+
/************************************************************
|
|
7444
|
+
* The `+N` chip at the end of a page: the rest of the group,
|
|
7445
|
+
* in the child topic's colour, dashed like a cut. Its title is
|
|
7446
|
+
* the one WORD of the folding, and it is translated at build
|
|
7447
|
+
* time: the chips are rebuilt on a language change.
|
|
7448
|
+
************************************************************/
|
|
7449
|
+
function build_more_innerHTML(color, theme, rest)
|
|
7450
|
+
{
|
|
7451
|
+
let c = pill_colors(color, theme);
|
|
7452
|
+
return `
|
|
7453
|
+
<div class="TREEDB_MORE" title="${escapeHtml(t('show more'))}" style="
|
|
7454
|
+
box-sizing: border-box; width: 100%; height: 100%;
|
|
7455
|
+
display: flex; align-items: center; justify-content: center; gap: 4px;
|
|
7456
|
+
background: ${c.bg}; color: ${c.fg}; border: 1px dashed ${c.border};
|
|
7457
|
+
border-radius: 13px;
|
|
7458
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
|
7459
|
+
font-size: 12px; font-weight: 700; line-height: 1;
|
|
7460
|
+
cursor: pointer; user-select: none;
|
|
7461
|
+
">+${rest}<span style="font-size: 10px; opacity: .8;">…</span></div>
|
|
7462
|
+
`;
|
|
7463
|
+
}
|
|
7464
|
+
|
|
7465
|
+
function more_innerHTML_of(gobj, nd, theme)
|
|
7466
|
+
{
|
|
7467
|
+
if(!is_more_node(nd)) {
|
|
7468
|
+
return null;
|
|
7469
|
+
}
|
|
7470
|
+
let priv = gobj.priv;
|
|
7471
|
+
let child_desc = priv.descs? priv.descs[nd.data.child_topic] : null;
|
|
7472
|
+
let color = (child_desc && child_desc.color) || "#94a3b8";
|
|
7473
|
+
return build_more_innerHTML(color, theme, nd.data.rest);
|
|
7474
|
+
}
|
|
7475
|
+
|
|
7476
|
+
/************************************************************
|
|
7477
|
+
* The pills live inside an `innerHTML` string, so no handler can
|
|
7478
|
+
* be attached to them: the click is delegated from the container
|
|
7479
|
+
* in the CAPTURE phase over four event types -- the same door the
|
|
7480
|
+
* JSON graph opens for its fold handles, for the same reasons: G6
|
|
7481
|
+
* binds on the html node element, a descendant, so only capture
|
|
7482
|
+
* runs first; and G6 never reads the DOM `click`, it builds its
|
|
7483
|
+
* own from the pointer pair, so `pointerdown`/`pointerup` are the
|
|
7484
|
+
* ones that have to be cut or the card is ALSO clicked.
|
|
7485
|
+
************************************************************/
|
|
7486
|
+
const FOLD_SWALLOWED_EVENTS = ["pointerdown", "pointerup", "mousedown", "click"];
|
|
7487
|
+
|
|
7488
|
+
function install_fold_listener(gobj)
|
|
7489
|
+
{
|
|
7490
|
+
let priv = gobj.priv;
|
|
7491
|
+
|
|
7492
|
+
if(!priv.$container) {
|
|
7493
|
+
log_error(`${gobj_short_name(gobj)}: no container, the fold pills are dead`);
|
|
7494
|
+
return;
|
|
7495
|
+
}
|
|
7496
|
+
priv._fold_listener = function(evt) {
|
|
7497
|
+
let $pill = (evt.target && evt.target.closest)
|
|
7498
|
+
? evt.target.closest('[data-fold-group]')
|
|
7499
|
+
: null;
|
|
7500
|
+
if(!$pill) {
|
|
7501
|
+
return;
|
|
7502
|
+
}
|
|
7503
|
+
evt.stopPropagation();
|
|
7504
|
+
evt.preventDefault();
|
|
7505
|
+
if(evt.type !== "click") {
|
|
7506
|
+
return; /* swallowed so G6 never sees a node click */
|
|
7507
|
+
}
|
|
7508
|
+
gobj_send_event(gobj, "EV_TOGGLE_FOLD", {
|
|
7509
|
+
group: $pill.getAttribute('data-fold-group'),
|
|
7510
|
+
}, gobj);
|
|
7511
|
+
};
|
|
7512
|
+
for(let type of FOLD_SWALLOWED_EVENTS) {
|
|
7513
|
+
priv.$container.addEventListener(type, priv._fold_listener, true);
|
|
7514
|
+
}
|
|
7515
|
+
}
|
|
7516
|
+
|
|
7517
|
+
function uninstall_fold_listener(gobj)
|
|
7518
|
+
{
|
|
7519
|
+
let priv = gobj.priv;
|
|
7520
|
+
|
|
7521
|
+
if(priv._fold_listener && priv.$container) {
|
|
7522
|
+
for(let type of FOLD_SWALLOWED_EVENTS) {
|
|
7523
|
+
priv.$container.removeEventListener(type, priv._fold_listener, true);
|
|
7524
|
+
}
|
|
7525
|
+
}
|
|
7526
|
+
priv._fold_listener = null;
|
|
7527
|
+
}
|
|
7528
|
+
|
|
7529
|
+
/************************************************************
|
|
7530
|
+
* (Re)build the tree of the records. Cheap -- one pass over the
|
|
7531
|
+
* records -- so it runs again on every create/update/delete
|
|
7532
|
+
* rather than being patched. The fold state survives: it is
|
|
7533
|
+
* keyed by group, and a group that is gone is simply never read.
|
|
7534
|
+
************************************************************/
|
|
7535
|
+
function rebuild_fold_model(gobj)
|
|
7536
|
+
{
|
|
7537
|
+
let priv = gobj.priv;
|
|
7538
|
+
|
|
7539
|
+
priv._fold_model = fold_build_model(priv.descs || {}, priv.records || {});
|
|
7540
|
+
if(!priv._fold_state) {
|
|
7541
|
+
priv._fold_state = fold_new_state(gobj_read_integer_attr(gobj, "fold_page_size"));
|
|
7542
|
+
}
|
|
7543
|
+
if(!priv._pill_sig) {
|
|
7544
|
+
priv._pill_sig = new Map();
|
|
7545
|
+
}
|
|
7546
|
+
}
|
|
7547
|
+
|
|
7548
|
+
/************************************************************
|
|
7549
|
+
* How many records there are, and how many carry a saved
|
|
7550
|
+
* position -- what auto_layout decides on. Counted over the
|
|
7551
|
+
* RECORDS, because the nodes on screen are a fraction of them.
|
|
7552
|
+
************************************************************/
|
|
7553
|
+
function count_placed_records(gobj)
|
|
7554
|
+
{
|
|
7555
|
+
let priv = gobj.priv;
|
|
7556
|
+
|
|
7557
|
+
priv._nodes_total = 0;
|
|
7558
|
+
priv._nodes_placed = 0;
|
|
7559
|
+
for(const [topic_name, records] of Object.entries(priv.records || {})) {
|
|
7560
|
+
if(topic_name.substring(0, 2) === "__" || !is_array(records)) {
|
|
7561
|
+
continue;
|
|
7562
|
+
}
|
|
7563
|
+
for(let record of records) {
|
|
7564
|
+
priv._nodes_total++;
|
|
7565
|
+
let geometry = get_node_graph_props(gobj, topic_name, record.id, record);
|
|
7566
|
+
if(geometry_has_position(geometry)) {
|
|
7567
|
+
priv._nodes_placed++;
|
|
7568
|
+
}
|
|
7569
|
+
}
|
|
7570
|
+
}
|
|
7571
|
+
}
|
|
7572
|
+
|
|
7573
|
+
/************************************************************
|
|
7574
|
+
* Make the G6 data say what the fold model says is visible.
|
|
7575
|
+
*
|
|
7576
|
+
* Record nodes not visible any more are removed (their edges go
|
|
7577
|
+
* with them), the visible ones missing are created, the links
|
|
7578
|
+
* between visible nodes are drawn, the `+N` chips are placed or
|
|
7579
|
+
* taken away, and every card whose pills changed is repainted.
|
|
7580
|
+
* Then a draw, a layout if the set of nodes changed, and the
|
|
7581
|
+
* camera: fit the whole thing on the first load, or put `keep`
|
|
7582
|
+
* back where it was on screen before the change.
|
|
7583
|
+
*
|
|
7584
|
+
* Serialised: one reconcile at a time, in order. Two pills tapped
|
|
7585
|
+
* quickly would otherwise interleave their draws.
|
|
7586
|
+
*
|
|
7587
|
+
* opts.relayout run the layout even if no node came or went
|
|
7588
|
+
* opts.fit fit the viewport afterwards
|
|
7589
|
+
* opts.keep node id to hold still on screen
|
|
7590
|
+
* opts.keep_vp where it was ([x, y] viewport px)
|
|
7591
|
+
************************************************************/
|
|
7592
|
+
function reconcile_fold(gobj, opts)
|
|
7593
|
+
{
|
|
7594
|
+
let priv = gobj.priv;
|
|
7595
|
+
|
|
7596
|
+
let run = () => reconcile_fold_now(gobj, opts || {}).catch((e) => {
|
|
7597
|
+
log_error(`${gobj_short_name(gobj)}: reconcile failed: ${e}`);
|
|
7598
|
+
});
|
|
7599
|
+
priv._fold_chain = (priv._fold_chain || Promise.resolve()).then(run, run);
|
|
7600
|
+
return priv._fold_chain;
|
|
7601
|
+
}
|
|
7602
|
+
|
|
7603
|
+
async function reconcile_fold_now(gobj, opts)
|
|
7604
|
+
{
|
|
7605
|
+
let priv = gobj.priv;
|
|
7606
|
+
let graph = priv.graph;
|
|
7607
|
+
let model = priv._fold_model;
|
|
7608
|
+
let state = priv._fold_state;
|
|
7609
|
+
|
|
7610
|
+
if(!graph || !model || !state || gobj_is_destroying(gobj)) {
|
|
7611
|
+
return;
|
|
7612
|
+
}
|
|
7613
|
+
|
|
7614
|
+
let visible = fold_visible_set(model, state);
|
|
7615
|
+
let changed = false;
|
|
7616
|
+
|
|
7617
|
+
/* Paused for the whole of it: every add and remove below is the
|
|
7618
|
+
* fold's, not the user's, and none of it is a command to undo. */
|
|
7619
|
+
history_pause(gobj);
|
|
7620
|
+
try {
|
|
7621
|
+
changed = await reconcile_fold_apply(gobj, opts, visible);
|
|
7622
|
+
} finally {
|
|
7623
|
+
history_resume(gobj);
|
|
7624
|
+
}
|
|
7625
|
+
return changed;
|
|
7626
|
+
}
|
|
7627
|
+
|
|
7628
|
+
async function reconcile_fold_apply(gobj, opts, visible)
|
|
7629
|
+
{
|
|
7630
|
+
let priv = gobj.priv;
|
|
7631
|
+
let graph = priv.graph;
|
|
7632
|
+
let model = priv._fold_model;
|
|
7633
|
+
let state = priv._fold_state;
|
|
7634
|
+
let changed = false;
|
|
7635
|
+
|
|
7636
|
+
/* What is on screen now. */
|
|
7637
|
+
let present = new Map(); /* node_key -> node id */
|
|
7638
|
+
let more_present = new Map(); /* group_key -> node id */
|
|
7639
|
+
for(let nd of (graph.getNodeData() || [])) {
|
|
7640
|
+
if(is_more_node(nd)) {
|
|
7641
|
+
more_present.set(nd.data.more, nd.id);
|
|
7642
|
+
continue;
|
|
7643
|
+
}
|
|
7644
|
+
let key = node_key_of(nd);
|
|
7645
|
+
if(key) {
|
|
7646
|
+
present.set(key, nd.id);
|
|
7647
|
+
}
|
|
7648
|
+
}
|
|
7649
|
+
|
|
7650
|
+
/* Gone. */
|
|
7651
|
+
let to_remove = [];
|
|
7652
|
+
for(let [key, id] of present) {
|
|
7653
|
+
if(!visible.has(key)) {
|
|
7654
|
+
to_remove.push(id);
|
|
7655
|
+
}
|
|
7656
|
+
}
|
|
7657
|
+
if(to_remove.length) {
|
|
7658
|
+
let gone = new Set(to_remove);
|
|
7659
|
+
if(priv._selected_node_id && gone.has(priv._selected_node_id)) {
|
|
7660
|
+
deselect_node(gobj);
|
|
7661
|
+
}
|
|
7662
|
+
if(priv._detail_node_id && gone.has(priv._detail_node_id)) {
|
|
7663
|
+
hide_node_detail(gobj);
|
|
7664
|
+
}
|
|
7665
|
+
priv._focus_ids = (priv._focus_ids || []).filter((id) => !gone.has(id));
|
|
7666
|
+
priv._selected_paint_ids = (priv._selected_paint_ids || []).filter((id) => !gone.has(id));
|
|
7667
|
+
if(priv.anchor_state === "on" && gone.has(priv.anchor_id)) {
|
|
7668
|
+
priv.anchor_id = "";
|
|
7669
|
+
priv.anchor_state = "off";
|
|
7670
|
+
update_toolbar(gobj);
|
|
7671
|
+
}
|
|
7672
|
+
for(let id of to_remove) {
|
|
7673
|
+
priv._pill_sig.delete(id);
|
|
7674
|
+
}
|
|
7675
|
+
graph.removeNodeData(to_remove);
|
|
7676
|
+
changed = true;
|
|
7677
|
+
}
|
|
7678
|
+
|
|
7679
|
+
/* New. */
|
|
7680
|
+
for(let key of visible) {
|
|
7681
|
+
if(present.has(key)) {
|
|
7682
|
+
continue;
|
|
7683
|
+
}
|
|
7684
|
+
let node = model.nodes.get(key);
|
|
7685
|
+
let desc = priv.descs[node.topic_name];
|
|
7686
|
+
if(!desc) {
|
|
7687
|
+
log_error(`${gobj_short_name(gobj)}: no desc for ${key}`);
|
|
7688
|
+
continue;
|
|
7689
|
+
}
|
|
7690
|
+
create_topic_node(gobj, desc, node.record);
|
|
7691
|
+
priv._pill_sig.set(
|
|
7692
|
+
build_node_name(gobj, node.topic_name, node.id),
|
|
7693
|
+
pills_signature(gobj, key)
|
|
7694
|
+
);
|
|
7695
|
+
changed = true;
|
|
7696
|
+
}
|
|
7697
|
+
|
|
7698
|
+
/* The links between what is visible. draw_link draws each once
|
|
7699
|
+
* and skips the ones whose other end is off screen. */
|
|
7700
|
+
for(let key of visible) {
|
|
7701
|
+
let node = model.nodes.get(key);
|
|
7702
|
+
let desc = priv.descs[node.topic_name];
|
|
7703
|
+
if(desc) {
|
|
7704
|
+
draw_links(gobj, desc, node.record, false);
|
|
7705
|
+
}
|
|
7706
|
+
}
|
|
7707
|
+
|
|
7708
|
+
/* The `+N` chips. */
|
|
7709
|
+
let pending = fold_pending_groups(model, state, visible);
|
|
7710
|
+
let wanted = new Map();
|
|
7711
|
+
for(let p of pending) {
|
|
7712
|
+
wanted.set(p.group_key, p);
|
|
7713
|
+
}
|
|
7714
|
+
let more_gone = [];
|
|
7715
|
+
for(let [group_key, id] of more_present) {
|
|
7716
|
+
if(!wanted.has(group_key)) {
|
|
7717
|
+
more_gone.push(id);
|
|
7718
|
+
}
|
|
7719
|
+
}
|
|
7720
|
+
if(more_gone.length) {
|
|
7721
|
+
graph.removeNodeData(more_gone);
|
|
7722
|
+
changed = true;
|
|
7723
|
+
}
|
|
7724
|
+
for(let p of pending) {
|
|
7725
|
+
let first = model.nodes.get((model.groups.get(p.group_key) || [])[0]);
|
|
7726
|
+
let child_topic = first? first.topic_name : "";
|
|
7727
|
+
let child_desc = priv.descs[child_topic];
|
|
7728
|
+
let color = (child_desc && child_desc.color) || "#94a3b8";
|
|
7729
|
+
let id = more_node_id(gobj, p.group_key);
|
|
7730
|
+
if(more_present.has(p.group_key)) {
|
|
7731
|
+
let nd = graph.getNodeData(id);
|
|
7732
|
+
if(nd && nd.data.rest !== p.rest) {
|
|
7733
|
+
nd.data.rest = p.rest;
|
|
7734
|
+
graph.updateNodeData([{
|
|
7735
|
+
id: id,
|
|
7736
|
+
style: {innerHTML: build_more_innerHTML(color, priv.theme, p.rest)},
|
|
7737
|
+
}]);
|
|
7738
|
+
}
|
|
7739
|
+
continue;
|
|
7740
|
+
}
|
|
7741
|
+
let xy = get_default_ne_xy(gobj);
|
|
7742
|
+
graph.addNodeData([{
|
|
7743
|
+
id: id,
|
|
7744
|
+
type: 'html',
|
|
7745
|
+
style: {
|
|
7746
|
+
x: xy, y: xy,
|
|
7747
|
+
size: [...MORE_CHIP_SIZE],
|
|
7748
|
+
dx: -MORE_CHIP_SIZE[0] / 2,
|
|
7749
|
+
dy: -MORE_CHIP_SIZE[1] / 2,
|
|
7750
|
+
innerHTML: build_more_innerHTML(color, priv.theme, p.rest),
|
|
7751
|
+
},
|
|
7752
|
+
data: {
|
|
7753
|
+
more: p.group_key,
|
|
7754
|
+
child_topic: child_topic,
|
|
7755
|
+
rest: p.rest,
|
|
7756
|
+
},
|
|
7757
|
+
}]);
|
|
7758
|
+
if(p.node_key) {
|
|
7759
|
+
let parent = model.nodes.get(p.node_key);
|
|
7760
|
+
let dark = (priv.theme === "dark");
|
|
7761
|
+
graph.addEdgeData([{
|
|
7762
|
+
id: more_edge_id(gobj, p.group_key),
|
|
7763
|
+
type: edge_type_for(gobj),
|
|
7764
|
+
source: build_node_name(gobj, parent.topic_name, parent.id),
|
|
7765
|
+
target: id,
|
|
7766
|
+
style: {
|
|
7767
|
+
sourcePort: p.hook,
|
|
7768
|
+
lineWidth: 1,
|
|
7769
|
+
lineDash: [4, 3],
|
|
7770
|
+
stroke: dark? '#8b94a3' : '#94a3b8',
|
|
7771
|
+
startArrow: false,
|
|
7772
|
+
endArrow: false,
|
|
7773
|
+
},
|
|
7774
|
+
data: {more: p.group_key},
|
|
7775
|
+
}]);
|
|
7776
|
+
}
|
|
7777
|
+
changed = true;
|
|
7778
|
+
}
|
|
7779
|
+
|
|
7780
|
+
/* The cards whose pills changed. */
|
|
7781
|
+
let repaint = new Set();
|
|
7782
|
+
let resize = [];
|
|
7783
|
+
for(let key of visible) {
|
|
7784
|
+
let node = model.nodes.get(key);
|
|
7785
|
+
let id = build_node_name(gobj, node.topic_name, node.id);
|
|
7786
|
+
let sig = pills_signature(gobj, key);
|
|
7787
|
+
if(priv._pill_sig.get(id) === sig) {
|
|
7788
|
+
continue;
|
|
7789
|
+
}
|
|
7790
|
+
let had = !!priv._pill_sig.get(id);
|
|
7791
|
+
priv._pill_sig.set(id, sig);
|
|
7792
|
+
repaint.add(id);
|
|
7793
|
+
if(had !== !!sig) {
|
|
7794
|
+
let nd = graph.getNodeData(id);
|
|
7795
|
+
let desc = nd && nd.data && nd.data.desc;
|
|
7796
|
+
let geometry = (nd && nd.data && nd.data.graph_props) || {};
|
|
7797
|
+
/* A saved size is the owner's: only a card on the default
|
|
7798
|
+
* size grows and shrinks with its pills. */
|
|
7799
|
+
if(desc && !is_array(geometry.size)) {
|
|
7800
|
+
let size = card_size_for(desc.node_treedb_type, !!sig);
|
|
7801
|
+
resize.push({id: id, style: {
|
|
7802
|
+
size: size, dx: -size[0] / 2, dy: -size[1] / 2,
|
|
7803
|
+
}});
|
|
7804
|
+
}
|
|
7805
|
+
}
|
|
7806
|
+
}
|
|
7807
|
+
if(resize.length) {
|
|
7808
|
+
graph.updateNodeData(resize);
|
|
7809
|
+
}
|
|
7810
|
+
if(repaint.size) {
|
|
7811
|
+
repaint_cards(gobj, repaint);
|
|
7812
|
+
}
|
|
7813
|
+
|
|
7814
|
+
/* Decided BEFORE the drawing, not after it: the minimap builds its
|
|
7815
|
+
* canvas on its FIRST render, off the graph's draw events, so one
|
|
7816
|
+
* added after the last draw is bound and never painted. The count
|
|
7817
|
+
* it decides on is the nodes just reconciled. */
|
|
7818
|
+
try {
|
|
7819
|
+
refresh_minimap(gobj);
|
|
7820
|
+
} catch(e) {
|
|
7821
|
+
log_error(`${gobj_short_name(gobj)}: refresh_minimap failed: ${e}`);
|
|
7822
|
+
}
|
|
7823
|
+
|
|
7824
|
+
await graph_draw(gobj);
|
|
7825
|
+
if(changed || opts.relayout) {
|
|
7826
|
+
await graph_layout(gobj);
|
|
7827
|
+
}
|
|
7828
|
+
|
|
7829
|
+
if(opts.fit) {
|
|
7830
|
+
await graph_fit_readable(gobj);
|
|
7831
|
+
} else if(opts.keep && opts.keep_vp) {
|
|
7832
|
+
await yui_graph_place_at(graph, opts.keep, opts.keep_vp);
|
|
7833
|
+
}
|
|
7834
|
+
|
|
7835
|
+
update_resize_handles_position(gobj);
|
|
7836
|
+
update_port_resize_handles_position(gobj);
|
|
7837
|
+
update_edge_icon_position(gobj);
|
|
7838
|
+
update_node_icon_position(gobj);
|
|
7839
|
+
update_link_icon_position(gobj);
|
|
7840
|
+
|
|
7841
|
+
return changed;
|
|
7842
|
+
}
|
|
7843
|
+
|
|
7844
|
+
/************************************************************
|
|
7845
|
+
* Repaint the `+N` chips (theme or language changed).
|
|
7846
|
+
************************************************************/
|
|
7847
|
+
function repaint_more_chips(gobj)
|
|
7848
|
+
{
|
|
7849
|
+
let priv = gobj.priv;
|
|
7850
|
+
let graph = priv.graph;
|
|
7851
|
+
|
|
7852
|
+
if(!graph) {
|
|
7853
|
+
return;
|
|
7854
|
+
}
|
|
7855
|
+
let updates = [];
|
|
7856
|
+
for(let nd of (graph.getNodeData() || [])) {
|
|
7857
|
+
let html = more_innerHTML_of(gobj, nd, priv.theme);
|
|
7858
|
+
if(html !== null) {
|
|
7859
|
+
updates.push({id: nd.id, style: {innerHTML: html}});
|
|
7860
|
+
}
|
|
7861
|
+
}
|
|
7862
|
+
if(updates.length) {
|
|
7863
|
+
try {
|
|
7864
|
+
graph.updateNodeData(updates);
|
|
7865
|
+
graph.draw();
|
|
7866
|
+
} catch(e) {
|
|
7867
|
+
log_error(`${gobj_short_name(gobj)}: cannot repaint the chips: ${e}`);
|
|
7868
|
+
}
|
|
7869
|
+
}
|
|
7870
|
+
}
|
|
7871
|
+
|
|
7872
|
+
|
|
7873
|
+
|
|
7874
|
+
|
|
7066
7875
|
/***************************
|
|
7067
7876
|
* Actions
|
|
7068
7877
|
***************************/
|
|
@@ -7116,6 +7925,11 @@ function ac_clear_data(gobj, event, kw, src)
|
|
|
7116
7925
|
priv._nodes_total = 0;
|
|
7117
7926
|
priv._nodes_placed = 0;
|
|
7118
7927
|
|
|
7928
|
+
/* So does the fold: a reload opens at `expand_depth` again. */
|
|
7929
|
+
priv._fold_model = null;
|
|
7930
|
+
priv._fold_state = null;
|
|
7931
|
+
priv._pill_sig = null;
|
|
7932
|
+
|
|
7119
7933
|
graph_remove_plugin(gobj, "history");
|
|
7120
7934
|
update_history_buttons(gobj);
|
|
7121
7935
|
graph_clear(gobj);
|
|
@@ -7164,81 +7978,67 @@ function ac_load_data(gobj, event, kw, src)
|
|
|
7164
7978
|
priv.records[topic_name] = data;
|
|
7165
7979
|
priv.descs[topic_name].loaded = true;
|
|
7166
7980
|
|
|
7167
|
-
/*--------------------------------------------------*
|
|
7168
|
-
* Creating and loading topic cells from backend
|
|
7169
|
-
*--------------------------------------------------*/
|
|
7170
|
-
for(let i=0; i<data.length; i++) {
|
|
7171
|
-
let record = data[i];
|
|
7172
|
-
create_topic_node(gobj, desc, record);
|
|
7173
|
-
}
|
|
7174
|
-
|
|
7175
7981
|
/*----------------------------------------------------*
|
|
7176
|
-
*
|
|
7982
|
+
* Nothing is drawn until every topic is in: the tree
|
|
7983
|
+
* needs the parents to exist before it can hang the
|
|
7984
|
+
* children from them.
|
|
7177
7985
|
*----------------------------------------------------*/
|
|
7178
|
-
let
|
|
7986
|
+
let all_loaded = true;
|
|
7179
7987
|
for (const [topic_name, desc] of Object.entries(priv.descs)) {
|
|
7180
7988
|
if (topic_name.substring(0, 2) === "__") {
|
|
7181
7989
|
continue; // ignore system topics
|
|
7182
7990
|
}
|
|
7183
7991
|
if(!desc.loaded) {
|
|
7184
|
-
|
|
7992
|
+
all_loaded = false;
|
|
7185
7993
|
break;
|
|
7186
7994
|
}
|
|
7187
7995
|
}
|
|
7188
7996
|
|
|
7189
|
-
if(
|
|
7190
|
-
/*
|
|
7191
|
-
*
|
|
7192
|
-
*
|
|
7193
|
-
*
|
|
7194
|
-
|
|
7195
|
-
|
|
7196
|
-
|
|
7197
|
-
|
|
7997
|
+
if(all_loaded && priv.graph) {
|
|
7998
|
+
/* The tree of the records, opened `expand_depth` levels: the
|
|
7999
|
+
* roots and their children by default. Only that much becomes
|
|
8000
|
+
* G6 nodes -- a treedb of a thousand records opens as a
|
|
8001
|
+
* handful of cards with a count on each cut. */
|
|
8002
|
+
rebuild_fold_model(gobj);
|
|
8003
|
+
fold_expand_to_depth(
|
|
8004
|
+
priv._fold_model, priv._fold_state,
|
|
8005
|
+
gobj_read_integer_attr(gobj, "expand_depth")
|
|
8006
|
+
);
|
|
8007
|
+
|
|
8008
|
+
/* Before the layout runs, and only now: whether anybody has
|
|
8009
|
+
* ever placed a node of this treedb is not known until its
|
|
8010
|
+
* records and __graphs__ are in. Counted over the records,
|
|
8011
|
+
* not the cards: most of them are folded away. */
|
|
8012
|
+
count_placed_records(gobj);
|
|
8013
|
+
let auto_laid = false;
|
|
7198
8014
|
try {
|
|
7199
|
-
|
|
8015
|
+
auto_laid = auto_layout(gobj);
|
|
7200
8016
|
} catch(e) {
|
|
7201
|
-
log_error(`${gobj_short_name(gobj)}:
|
|
7202
|
-
}
|
|
7203
|
-
|
|
7204
|
-
|
|
7205
|
-
|
|
7206
|
-
|
|
7207
|
-
|
|
7208
|
-
|
|
7209
|
-
|
|
7210
|
-
|
|
7211
|
-
|
|
7212
|
-
|
|
7213
|
-
|
|
7214
|
-
|
|
7215
|
-
|
|
7216
|
-
|
|
7217
|
-
|
|
7218
|
-
|
|
7219
|
-
|
|
7220
|
-
|
|
7221
|
-
|
|
7222
|
-
|
|
7223
|
-
|
|
7224
|
-
|
|
7225
|
-
|
|
7226
|
-
}
|
|
7227
|
-
sync_history_plugin(gobj);
|
|
7228
|
-
/* Nodes exist and are positioned now: apply a focus
|
|
7229
|
-
* requested before the data was ready (deep link). */
|
|
7230
|
-
if(priv._pending_focus_topic !== null) {
|
|
7231
|
-
let ft = priv._pending_focus_topic;
|
|
7232
|
-
priv._pending_focus_topic = null;
|
|
7233
|
-
graph_focus_topic(gobj, ft);
|
|
7234
|
-
}
|
|
7235
|
-
if(priv._pending_find !== null) {
|
|
7236
|
-
let term = priv._pending_find;
|
|
7237
|
-
priv._pending_find = null;
|
|
7238
|
-
publish_find_result(gobj, term, graph_find_nodes(gobj, term));
|
|
7239
|
-
}
|
|
7240
|
-
});
|
|
7241
|
-
});
|
|
8017
|
+
log_error(`${gobj_short_name(gobj)}: auto_layout failed: ${e}`);
|
|
8018
|
+
}
|
|
8019
|
+
/* The ports and edges follow the layout's direction, and the
|
|
8020
|
+
* nodes are about to be built: settle it first. */
|
|
8021
|
+
apply_layout_direction(gobj);
|
|
8022
|
+
|
|
8023
|
+
/* A graph laid out by us opens WHOLE (fit). Only when WE laid
|
|
8024
|
+
* it out — a saved arrangement includes where its owner was
|
|
8025
|
+
* looking. */
|
|
8026
|
+
reconcile_fold(gobj, {relayout: true, fit: auto_laid}).then(() => {
|
|
8027
|
+
sync_history_plugin(gobj);
|
|
8028
|
+
/* Nodes exist and are positioned now: apply a focus
|
|
8029
|
+
* requested before the data was ready (deep link). */
|
|
8030
|
+
if(priv._pending_focus_topic !== null) {
|
|
8031
|
+
let ft = priv._pending_focus_topic;
|
|
8032
|
+
let all = priv._pending_focus_all;
|
|
8033
|
+
priv._pending_focus_topic = null;
|
|
8034
|
+
priv._pending_focus_all = false;
|
|
8035
|
+
graph_focus_topic(gobj, ft, all);
|
|
8036
|
+
}
|
|
8037
|
+
if(priv._pending_find !== null) {
|
|
8038
|
+
let term = priv._pending_find;
|
|
8039
|
+
priv._pending_find = null;
|
|
8040
|
+
publish_find_result(gobj, term, graph_find_nodes(gobj, term));
|
|
8041
|
+
}
|
|
7242
8042
|
});
|
|
7243
8043
|
}
|
|
7244
8044
|
|
|
@@ -7306,14 +8106,12 @@ function ac_node_created(gobj, event, kw, src)
|
|
|
7306
8106
|
priv.records[topic_name].push(node);
|
|
7307
8107
|
|
|
7308
8108
|
/*
|
|
7309
|
-
*
|
|
8109
|
+
* Into the tree, and on screen: whoever created it wants to see
|
|
8110
|
+
* it, so the path down to it is opened.
|
|
7310
8111
|
*/
|
|
7311
|
-
|
|
7312
|
-
|
|
7313
|
-
|
|
7314
|
-
graph_draw(gobj).then(() => {
|
|
7315
|
-
history_resume(gobj);
|
|
7316
|
-
});
|
|
8112
|
+
rebuild_fold_model(gobj);
|
|
8113
|
+
fold_reveal(priv._fold_model, priv._fold_state, fold_node_key(topic_name, String(node.id)));
|
|
8114
|
+
reconcile_fold(gobj, {});
|
|
7317
8115
|
|
|
7318
8116
|
return 0;
|
|
7319
8117
|
}
|
|
@@ -7394,12 +8192,22 @@ function ac_node_updated(gobj, event, kw, src)
|
|
|
7394
8192
|
}
|
|
7395
8193
|
|
|
7396
8194
|
/*
|
|
7397
|
-
* Update node data and local record
|
|
8195
|
+
* Update node data and local record. The card may be off screen
|
|
8196
|
+
* (folded): then only the record moves, and the reconcile below
|
|
8197
|
+
* repaints the count on whatever pill reaches it.
|
|
7398
8198
|
*/
|
|
7399
|
-
update_topic_node(gobj, desc, node_name, node);
|
|
7400
8199
|
update_local_node(gobj, topic_name, node);
|
|
8200
|
+
let on_screen = false;
|
|
8201
|
+
try {
|
|
8202
|
+
on_screen = !!graph.getNodeData(node_name);
|
|
8203
|
+
} catch(e) {
|
|
8204
|
+
on_screen = false;
|
|
8205
|
+
}
|
|
8206
|
+
if(on_screen) {
|
|
8207
|
+
update_topic_node(gobj, desc, node_name, node);
|
|
8208
|
+
}
|
|
7401
8209
|
|
|
7402
|
-
// Draw edges for refs that appeared
|
|
8210
|
+
// Draw edges for refs that appeared (draw_link skips an end off screen)
|
|
7403
8211
|
for(let ref of new_refs) {
|
|
7404
8212
|
if(!old_refs.has(ref)) {
|
|
7405
8213
|
let [col_idx, fkey_value] = ref.split("\t");
|
|
@@ -7411,6 +8219,11 @@ function ac_node_updated(gobj, event, kw, src)
|
|
|
7411
8219
|
history_resume(gobj);
|
|
7412
8220
|
});
|
|
7413
8221
|
|
|
8222
|
+
/* The links are the tree: a moved fkey moves the record from one
|
|
8223
|
+
* parent's pill to another's. */
|
|
8224
|
+
rebuild_fold_model(gobj);
|
|
8225
|
+
reconcile_fold(gobj, {});
|
|
8226
|
+
|
|
7414
8227
|
return 0;
|
|
7415
8228
|
}
|
|
7416
8229
|
|
|
@@ -7450,11 +8263,17 @@ function ac_node_deleted(gobj, event, kw, src)
|
|
|
7450
8263
|
clear_links(gobj, desc, node, false);
|
|
7451
8264
|
remove_topic_node(gobj, node_name);
|
|
7452
8265
|
remove_local_node(gobj, topic_name, node);
|
|
8266
|
+
if(priv._pill_sig) {
|
|
8267
|
+
priv._pill_sig.delete(node_name);
|
|
8268
|
+
}
|
|
7453
8269
|
|
|
7454
8270
|
graph_draw(gobj).then(() => {
|
|
7455
8271
|
history_resume(gobj);
|
|
7456
8272
|
});
|
|
7457
8273
|
|
|
8274
|
+
rebuild_fold_model(gobj);
|
|
8275
|
+
reconcile_fold(gobj, {});
|
|
8276
|
+
|
|
7458
8277
|
return 0;
|
|
7459
8278
|
}
|
|
7460
8279
|
|
|
@@ -7544,6 +8363,7 @@ function ac_theme(gobj, event, kw, src)
|
|
|
7544
8363
|
|
|
7545
8364
|
refresh_html_nodes_theme(gobj, theme);
|
|
7546
8365
|
refresh_default_edges_theme(gobj, theme);
|
|
8366
|
+
repaint_more_chips(gobj);
|
|
7547
8367
|
|
|
7548
8368
|
graph_draw(gobj).then(() => {
|
|
7549
8369
|
// Restore toolbar icon states lost when G6 re-renders the DOM
|
|
@@ -7712,7 +8532,98 @@ function ac_zoom_selection(gobj, event, kw, src)
|
|
|
7712
8532
|
|
|
7713
8533
|
function ac_focus_topic(gobj, event, kw, src)
|
|
7714
8534
|
{
|
|
7715
|
-
graph_focus_topic(gobj, kw && kw.topic);
|
|
8535
|
+
graph_focus_topic(gobj, kw && kw.topic, !!(kw && kw.reveal === "all"));
|
|
8536
|
+
return 0;
|
|
8537
|
+
}
|
|
8538
|
+
|
|
8539
|
+
/************************************************************
|
|
8540
|
+
* A pill was tapped: open that hook on its first page, or fold
|
|
8541
|
+
* it. The card the pill is on stays where it was on screen --
|
|
8542
|
+
* the layout moves everything else around it.
|
|
8543
|
+
************************************************************/
|
|
8544
|
+
function ac_toggle_fold(gobj, event, kw, src)
|
|
8545
|
+
{
|
|
8546
|
+
let priv = gobj.priv;
|
|
8547
|
+
let group = (kw && kw.group) || "";
|
|
8548
|
+
|
|
8549
|
+
if(!priv._fold_model || !group) {
|
|
8550
|
+
log_error(`${gobj_short_name(gobj)}: fold of '${group}' with no tree loaded`);
|
|
8551
|
+
return -1;
|
|
8552
|
+
}
|
|
8553
|
+
let g = fold_split_group_key(group);
|
|
8554
|
+
if(!g) {
|
|
8555
|
+
log_error(`${gobj_short_name(gobj)}: bad fold group '${group}'`);
|
|
8556
|
+
return -1;
|
|
8557
|
+
}
|
|
8558
|
+
let node_id = g.node_key? node_id_of_key(gobj, g.node_key) : "";
|
|
8559
|
+
let vp = node_id? yui_graph_viewport_of(priv.graph, node_id) : null;
|
|
8560
|
+
|
|
8561
|
+
fold_toggle(priv._fold_model, priv._fold_state, group);
|
|
8562
|
+
reconcile_fold(gobj, {keep: node_id, keep_vp: vp});
|
|
8563
|
+
|
|
8564
|
+
return 0;
|
|
8565
|
+
}
|
|
8566
|
+
|
|
8567
|
+
/************************************************************
|
|
8568
|
+
* A `+N` chip was tapped: the next page of its group. The
|
|
8569
|
+
* parent holds still; the chip itself moves down the column.
|
|
8570
|
+
************************************************************/
|
|
8571
|
+
function ac_show_more(gobj, event, kw, src)
|
|
8572
|
+
{
|
|
8573
|
+
let priv = gobj.priv;
|
|
8574
|
+
let group = (kw && kw.group) || "";
|
|
8575
|
+
|
|
8576
|
+
if(!priv._fold_model || !group) {
|
|
8577
|
+
log_error(`${gobj_short_name(gobj)}: show more of '${group}' with no tree loaded`);
|
|
8578
|
+
return -1;
|
|
8579
|
+
}
|
|
8580
|
+
let g = fold_split_group_key(group);
|
|
8581
|
+
if(!g) {
|
|
8582
|
+
log_error(`${gobj_short_name(gobj)}: bad fold group '${group}'`);
|
|
8583
|
+
return -1;
|
|
8584
|
+
}
|
|
8585
|
+
let node_id = g.node_key? node_id_of_key(gobj, g.node_key) : "";
|
|
8586
|
+
let vp = node_id? yui_graph_viewport_of(priv.graph, node_id) : null;
|
|
8587
|
+
|
|
8588
|
+
fold_show_more(priv._fold_model, priv._fold_state, group);
|
|
8589
|
+
reconcile_fold(gobj, {keep: node_id, keep_vp: vp});
|
|
8590
|
+
|
|
8591
|
+
return 0;
|
|
8592
|
+
}
|
|
8593
|
+
|
|
8594
|
+
/************************************************************
|
|
8595
|
+
* The whole treedb, every hook open on all its children. This
|
|
8596
|
+
* is the reader asking for the pile on purpose; the pages are
|
|
8597
|
+
* the other way in.
|
|
8598
|
+
************************************************************/
|
|
8599
|
+
function ac_expand_all(gobj, event, kw, src)
|
|
8600
|
+
{
|
|
8601
|
+
let priv = gobj.priv;
|
|
8602
|
+
|
|
8603
|
+
if(!priv._fold_model) {
|
|
8604
|
+
log_error(`${gobj_short_name(gobj)}: expand all with no tree loaded`);
|
|
8605
|
+
return -1;
|
|
8606
|
+
}
|
|
8607
|
+
fold_expand_all(priv._fold_model, priv._fold_state);
|
|
8608
|
+
reconcile_fold(gobj, {fit: true});
|
|
8609
|
+
|
|
8610
|
+
return 0;
|
|
8611
|
+
}
|
|
8612
|
+
|
|
8613
|
+
/************************************************************
|
|
8614
|
+
* The roots alone.
|
|
8615
|
+
************************************************************/
|
|
8616
|
+
function ac_collapse_all(gobj, event, kw, src)
|
|
8617
|
+
{
|
|
8618
|
+
let priv = gobj.priv;
|
|
8619
|
+
|
|
8620
|
+
if(!priv._fold_model) {
|
|
8621
|
+
log_error(`${gobj_short_name(gobj)}: collapse all with no tree loaded`);
|
|
8622
|
+
return -1;
|
|
8623
|
+
}
|
|
8624
|
+
fold_collapse_all(priv._fold_model, priv._fold_state);
|
|
8625
|
+
reconcile_fold(gobj, {fit: true});
|
|
8626
|
+
|
|
7716
8627
|
return 0;
|
|
7717
8628
|
}
|
|
7718
8629
|
|
|
@@ -7755,6 +8666,9 @@ function ac_set_layout(gobj, event, kw, src)
|
|
|
7755
8666
|
{
|
|
7756
8667
|
let priv = gobj.priv;
|
|
7757
8668
|
let layout = select_layout(gobj, kw.layout);
|
|
8669
|
+
/* The ports turn with the reading direction, before the lines
|
|
8670
|
+
* are laid out from them. */
|
|
8671
|
+
apply_layout_direction(gobj);
|
|
7758
8672
|
graph_set_layout(gobj, layout).then(() => {
|
|
7759
8673
|
configure_behaviour(gobj);
|
|
7760
8674
|
update_toolbar(gobj);
|
|
@@ -7913,6 +8827,21 @@ function ac_node_click(gobj, event, kw, src)
|
|
|
7913
8827
|
let graph = priv.graph;
|
|
7914
8828
|
let node_id = kw.evt.target.id;
|
|
7915
8829
|
|
|
8830
|
+
/*
|
|
8831
|
+
* A `+N` chip is one thing only: the next page of its group. It
|
|
8832
|
+
* is not a record -- nothing to select, anchor, detail or link.
|
|
8833
|
+
*/
|
|
8834
|
+
let more_nd = null;
|
|
8835
|
+
try {
|
|
8836
|
+
more_nd = graph.getNodeData(node_id);
|
|
8837
|
+
} catch(e) {
|
|
8838
|
+
more_nd = null;
|
|
8839
|
+
}
|
|
8840
|
+
if(is_more_node(more_nd)) {
|
|
8841
|
+
gobj_send_event(gobj, "EV_SHOW_MORE", {group: more_nd.data.more}, gobj);
|
|
8842
|
+
return 0;
|
|
8843
|
+
}
|
|
8844
|
+
|
|
7916
8845
|
/*
|
|
7917
8846
|
* A waiting anchor TAKES the click, before selection, before
|
|
7918
8847
|
* ports, before linking: it is what the reader armed the button
|
|
@@ -8251,6 +9180,10 @@ function create_gclass(gclass_name)
|
|
|
8251
9180
|
["EV_ZOOM_SELECTION", ac_zoom_selection, null],
|
|
8252
9181
|
["EV_FOCUS_TOPIC", ac_focus_topic, null],
|
|
8253
9182
|
["EV_FIND_NODES", ac_find_nodes, null],
|
|
9183
|
+
["EV_TOGGLE_FOLD", ac_toggle_fold, null],
|
|
9184
|
+
["EV_SHOW_MORE", ac_show_more, null],
|
|
9185
|
+
["EV_EXPAND_ALL", ac_expand_all, null],
|
|
9186
|
+
["EV_COLLAPSE_ALL", ac_collapse_all, null],
|
|
8254
9187
|
["EV_CENTER", ac_center, null],
|
|
8255
9188
|
["EV_FULLSCREEN", ac_fullscreen, null],
|
|
8256
9189
|
["EV_SET_LAYOUT", ac_set_layout, null],
|
|
@@ -8304,6 +9237,10 @@ function create_gclass(gclass_name)
|
|
|
8304
9237
|
["EV_FIND_NODES", 0],
|
|
8305
9238
|
["EV_FIND_RESULT", event_flag_t.EVF_OUTPUT_EVENT],
|
|
8306
9239
|
["EV_LAYOUT_AUTOSET", event_flag_t.EVF_OUTPUT_EVENT],
|
|
9240
|
+
["EV_TOGGLE_FOLD", 0],
|
|
9241
|
+
["EV_SHOW_MORE", 0],
|
|
9242
|
+
["EV_EXPAND_ALL", 0],
|
|
9243
|
+
["EV_COLLAPSE_ALL", 0],
|
|
8307
9244
|
["EV_CENTER", 0],
|
|
8308
9245
|
["EV_FULLSCREEN", 0],
|
|
8309
9246
|
["EV_SET_LAYOUT", 0],
|