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