@yuneta/gobj-ui 7.23.74 → 7.23.75
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 +66 -12
- package/dist/gobj-ui.cjs.js +493 -135
- package/dist/gobj-ui.es.js +493 -135
- package/package.json +1 -1
- package/src/c_g6_nodes_tree.js +433 -94
- package/src/c_yui_treedb_graph.js +104 -0
- package/src/treedb_layout.js +40 -56
- package/src/treedb_layout.test.js +45 -24
- package/src/yui_icons.css +6 -0
package/src/c_g6_nodes_tree.js
CHANGED
|
@@ -52,6 +52,7 @@ import {
|
|
|
52
52
|
gobj_read_integer_attr,
|
|
53
53
|
gobj_write_attr,
|
|
54
54
|
gobj_write_str_attr,
|
|
55
|
+
gobj_write_bool_attr,
|
|
55
56
|
gobj_parent,
|
|
56
57
|
gobj_name,
|
|
57
58
|
gobj_short_name,
|
|
@@ -124,7 +125,7 @@ import {
|
|
|
124
125
|
fold_node_key,
|
|
125
126
|
fold_split_group_key,
|
|
126
127
|
} from "./treedb_fold_model.js";
|
|
127
|
-
import {layout_tree,
|
|
128
|
+
import {layout_tree, layout_radial} from "./treedb_layout.js";
|
|
128
129
|
|
|
129
130
|
import {
|
|
130
131
|
BaseLayout,
|
|
@@ -247,6 +248,28 @@ const HIGHLIGHT_HALO = "rgba(240,160,32,0.35)";
|
|
|
247
248
|
*/
|
|
248
249
|
const SELECT_RING = "rgba(59,130,246,0.95)";
|
|
249
250
|
|
|
251
|
+
/*
|
|
252
|
+
* The ports of an open card. They are what a link is DRAWN from
|
|
253
|
+
* and what a resize takes hold of, and at a radius of 6 (2 on a
|
|
254
|
+
* chip) with a hairline stroke nobody could tell they were either:
|
|
255
|
+
* a port has to look like a handle.
|
|
256
|
+
*/
|
|
257
|
+
const PORT_R_ENTITY = 10;
|
|
258
|
+
const PORT_R_CHILD = 5;
|
|
259
|
+
const PORT_LINE_WIDTH = 2;
|
|
260
|
+
|
|
261
|
+
/*
|
|
262
|
+
* The CLOSED shape of a record: a rounded square of the topic's
|
|
263
|
+
* colour, no ports, no text -- topology and nothing else. The
|
|
264
|
+
* three tiers keep their order of size, so a root is still bigger
|
|
265
|
+
* than a leaf when nothing says which is which.
|
|
266
|
+
*/
|
|
267
|
+
const COMPACT_SIZE = {
|
|
268
|
+
hierarchical: 32,
|
|
269
|
+
extended: 28,
|
|
270
|
+
child: 22,
|
|
271
|
+
};
|
|
272
|
+
|
|
250
273
|
/***************************************************************
|
|
251
274
|
* Internal layout and operation mode definitions
|
|
252
275
|
***************************************************************/
|
|
@@ -262,21 +285,20 @@ const _layouts = {
|
|
|
262
285
|
* for whoever wants it -- the two are the same algorithm read two
|
|
263
286
|
* ways, so the layout picker is also the direction picker. */
|
|
264
287
|
/* OURS, made for a treedb (treedb_layout.js): a tidy tree read
|
|
265
|
-
*
|
|
266
|
-
* the
|
|
267
|
-
*
|
|
268
|
-
*
|
|
269
|
-
*
|
|
270
|
-
*
|
|
288
|
+
* TOP TO BOTTOM. It takes the spanning tree of what is on screen
|
|
289
|
+
* -- the roots, the first parent that reaches a node, the hooks
|
|
290
|
+
* in schema order, the records in their order -- so opening a
|
|
291
|
+
* hook moves nothing that is not under or beside it. Down and not
|
|
292
|
+
* right, because down is where a tree has room: a hall with a
|
|
293
|
+
* hundred devices is a wide row, not a column beside a card --
|
|
294
|
+
* and read right it was `dagre` with the siblings held still,
|
|
295
|
+
* which nobody could tell apart. The default for a treedb nobody
|
|
296
|
+
* has arranged (see auto_layout). */
|
|
271
297
|
"treedb-tree": {
|
|
272
298
|
type: 'treedb-tree',
|
|
299
|
+
direction: 'TB',
|
|
273
300
|
nodesep: 18,
|
|
274
|
-
ranksep:
|
|
275
|
-
},
|
|
276
|
-
"treedb-outline": {
|
|
277
|
-
type: 'treedb-outline',
|
|
278
|
-
nodesep: 12,
|
|
279
|
-
indent: 48,
|
|
301
|
+
ranksep: 90,
|
|
280
302
|
},
|
|
281
303
|
"dagre": {
|
|
282
304
|
type: 'dagre',
|
|
@@ -370,6 +392,10 @@ SDATA(data_type_t.DTP_INTEGER, "fold_page_size", 0, 24, "Children of
|
|
|
370
392
|
SDATA(data_type_t.DTP_LIST, "hidden_topics", 0, "[]", "Topics left out of the tree: no card, no pill counts them, no edge reaches them"),
|
|
371
393
|
SDATA(data_type_t.DTP_STRING, "main_topic", 0, "", "The topic the tree hangs from; empty = deduced (the one whose hooks reach the most others). Its parentless records are the roots; a parentless record of a topic linked to it is LOOSE and shown only on request"),
|
|
372
394
|
SDATA(data_type_t.DTP_LIST, "loose_topics", 0, "[]", "Topics whose loose records (no parent, in a topic the schema hangs from the main one) are shown as roots"),
|
|
395
|
+
|
|
396
|
+
/*---------------- Node shape ----------------*/
|
|
397
|
+
SDATA(data_type_t.DTP_STRING, "node_mode", 0, "expanded", "How a record is drawn: `expanded` = the card with its ports (the only shape a link can be edited on); `compact` = a rounded square of the topic's colour, no ports, no text -- the topology alone. One node can be toggled against it (EV_TOGGLE_NODE_MODE)"),
|
|
398
|
+
SDATA(data_type_t.DTP_BOOLEAN, "node_labels", 0, true, "A compact node says its name under the square. Off, it is a coloured square and nothing else"),
|
|
373
399
|
SDATA(data_type_t.DTP_BOOLEAN, "confirm_delete_node", 0, true, "Ask confirmation before deleting a node"),
|
|
374
400
|
SDATA(data_type_t.DTP_BOOLEAN, "confirm_unlink_edge", 0, true, "Ask confirmation before unlinking an edge"),
|
|
375
401
|
|
|
@@ -466,6 +492,7 @@ let PRIVATE_DATA = {
|
|
|
466
492
|
_fold_state: null, // ...and which groups of it are open
|
|
467
493
|
_fold_listener: null, // delegated click on the card pills
|
|
468
494
|
_pill_sig: null, // Map<node_id, string>: the pills a card was painted with
|
|
495
|
+
_open_nodes: null, // Set<node_id>: the nodes toggled AGAINST `node_mode`
|
|
469
496
|
_fold_busy: false, // a reconcile is in flight
|
|
470
497
|
};
|
|
471
498
|
|
|
@@ -807,7 +834,6 @@ function register_layouts(gobj)
|
|
|
807
834
|
_g6_extensions_registered = true;
|
|
808
835
|
register(ExtensionCategory.LAYOUT, 'manual', ManualLayout);
|
|
809
836
|
register(ExtensionCategory.LAYOUT, 'treedb-tree', TreedbTreeLayout);
|
|
810
|
-
register(ExtensionCategory.LAYOUT, 'treedb-outline', TreedbOutlineLayout);
|
|
811
837
|
register(ExtensionCategory.LAYOUT, 'treedb-radial', TreedbRadialLayout);
|
|
812
838
|
register(ExtensionCategory.NODE, 'light', LightNode);
|
|
813
839
|
}
|
|
@@ -928,6 +954,10 @@ function configure_events(gobj)
|
|
|
928
954
|
gobj_send_event(gobj, "EV_NODE_CONTEXT_MENU", {evt: evt}, gobj);
|
|
929
955
|
});
|
|
930
956
|
|
|
957
|
+
graph.on(NodeEvent.DBLCLICK, (evt) => {
|
|
958
|
+
gobj_send_event(gobj, "EV_NODE_DBLCLICK", {evt: evt}, gobj);
|
|
959
|
+
});
|
|
960
|
+
|
|
931
961
|
graph.on(EdgeEvent.CLICK, (evt) => {
|
|
932
962
|
if(consume_long_press_click(graph)) {
|
|
933
963
|
return;
|
|
@@ -1598,7 +1628,7 @@ function configure_behaviour(gobj)
|
|
|
1598
1628
|
priv.edit_mode = false;
|
|
1599
1629
|
behaviors = [
|
|
1600
1630
|
"drag-canvas",
|
|
1601
|
-
|
|
1631
|
+
...camera_behaviors(),
|
|
1602
1632
|
];
|
|
1603
1633
|
break;
|
|
1604
1634
|
case "edition":
|
|
@@ -1630,7 +1660,7 @@ function configure_behaviour(gobj)
|
|
|
1630
1660
|
return true; /* a key, not a pointer */
|
|
1631
1661
|
},
|
|
1632
1662
|
},
|
|
1633
|
-
|
|
1663
|
+
...camera_behaviors(),
|
|
1634
1664
|
/* Moves EVERY node in the `selected` state, not just
|
|
1635
1665
|
* the one under the pointer, and wraps the whole move
|
|
1636
1666
|
* in one history batch -- so a group move is one drag
|
|
@@ -1689,7 +1719,7 @@ function configure_behaviour(gobj)
|
|
|
1689
1719
|
* where the gestures ARE the camera, the graph froze. */
|
|
1690
1720
|
behaviors = [
|
|
1691
1721
|
"drag-canvas",
|
|
1692
|
-
|
|
1722
|
+
...camera_behaviors(),
|
|
1693
1723
|
];
|
|
1694
1724
|
break;
|
|
1695
1725
|
default:
|
|
@@ -1792,9 +1822,38 @@ function build_ports(gobj, desc)
|
|
|
1792
1822
|
* Which way the graph reads. `dagre` is the left-to-right
|
|
1793
1823
|
* layout; everything else keeps the top-down reading, `manual`
|
|
1794
1824
|
* included, because the saved arrangements were made with the
|
|
1795
|
-
* ports on the top and bottom edges.
|
|
1825
|
+
* ports on the top and bottom edges. The treedb tree reads down
|
|
1826
|
+
* too, since 7.23.75: read right it was dagre's twin.
|
|
1796
1827
|
************************************************************/
|
|
1797
|
-
const LR_LAYOUTS = ["dagre"
|
|
1828
|
+
const LR_LAYOUTS = ["dagre"];
|
|
1829
|
+
|
|
1830
|
+
/************************************************************
|
|
1831
|
+
* The camera behaviors every operation mode shares.
|
|
1832
|
+
*
|
|
1833
|
+
* The wheel SCROLLS, as it does on a map and on every page: a
|
|
1834
|
+
* graph of many nodes is taller than the screen, and a wheel
|
|
1835
|
+
* that zoomed instead made it a thing to be looked at from afar
|
|
1836
|
+
* or read through a keyhole -- never scrolled. Zoom is Ctrl +
|
|
1837
|
+
* wheel (which is also what a trackpad pinch arrives as), the
|
|
1838
|
+
* toolbar's +/-, and the two-finger pinch of g6_touch_gestures.
|
|
1839
|
+
* `enable` on the scroll keeps the two apart: a Ctrl wheel is
|
|
1840
|
+
* the zoom's, so the scroll stands aside for it.
|
|
1841
|
+
************************************************************/
|
|
1842
|
+
function camera_behaviors()
|
|
1843
|
+
{
|
|
1844
|
+
return [
|
|
1845
|
+
{
|
|
1846
|
+
type: "scroll-canvas",
|
|
1847
|
+
key: "scroll-canvas",
|
|
1848
|
+
enable: (event) => !(event && (event.ctrlKey || event.metaKey)),
|
|
1849
|
+
},
|
|
1850
|
+
{
|
|
1851
|
+
type: "zoom-canvas",
|
|
1852
|
+
key: "zoom-canvas",
|
|
1853
|
+
trigger: ["Control"],
|
|
1854
|
+
},
|
|
1855
|
+
];
|
|
1856
|
+
}
|
|
1798
1857
|
|
|
1799
1858
|
function layout_direction(gobj)
|
|
1800
1859
|
{
|
|
@@ -1916,58 +1975,105 @@ function create_topic_node(gobj, desc, record)
|
|
|
1916
1975
|
|
|
1917
1976
|
//log_warning(`create node ==> ${node_name}`);
|
|
1918
1977
|
|
|
1919
|
-
let
|
|
1978
|
+
let pills_html = pills_html_of_key(gobj, fold_node_key(desc.topic_name, String(record.id)));
|
|
1979
|
+
let shape = node_shape_of(gobj, node_name, desc, record, geometry, pills_html);
|
|
1980
|
+
|
|
1981
|
+
let node_def = {
|
|
1982
|
+
id: node_name,
|
|
1983
|
+
type: shape.type,
|
|
1984
|
+
style: Object.assign({x: x, y: y}, shape.style),
|
|
1985
|
+
data: {
|
|
1986
|
+
// This 4 keys are available in user `data` of G6 Node.
|
|
1987
|
+
topic_name: desc.topic_name,
|
|
1988
|
+
desc: desc,
|
|
1989
|
+
record: record,
|
|
1990
|
+
graph_props: geometry, // from __graphs__ (or _geometry fallback)
|
|
1991
|
+
/* The ports in schema order, whatever shape the node
|
|
1992
|
+
* wears: the tree layout ranks the children by them, and
|
|
1993
|
+
* a closed node carries no ports on its style. */
|
|
1994
|
+
port_keys: build_ports(gobj, desc).map((p) => p.key),
|
|
1995
|
+
}
|
|
1996
|
+
};
|
|
1997
|
+
|
|
1998
|
+
graph.addNodeData([node_def]);
|
|
1999
|
+
}
|
|
2000
|
+
|
|
2001
|
+
/************************************************************
|
|
2002
|
+
* The shape of a record node: OPEN (the card, with its ports)
|
|
2003
|
+
* or CLOSED (a coloured square), by `node_mode` and the nodes
|
|
2004
|
+
* toggled against it.
|
|
2005
|
+
*
|
|
2006
|
+
* Both answer {type, style} WITHOUT x/y, which belong to the node
|
|
2007
|
+
* and not to its shape, and a node moves between the two on the
|
|
2008
|
+
* fly: G6 rebuilds the element when `type` changes, and the
|
|
2009
|
+
* style is MERGED, which is why each shape also writes the keys
|
|
2010
|
+
* the other one would leave behind (`labelText`, `ports`).
|
|
2011
|
+
************************************************************/
|
|
2012
|
+
function node_shape_of(gobj, node_id, desc, record, geometry, pills_html)
|
|
2013
|
+
{
|
|
2014
|
+
if(node_is_compact(gobj, node_id)) {
|
|
2015
|
+
return compact_shape_of(gobj, desc, record, {});
|
|
2016
|
+
}
|
|
2017
|
+
return card_shape_of(gobj, desc, record, geometry, pills_html);
|
|
2018
|
+
}
|
|
2019
|
+
|
|
2020
|
+
function node_is_compact(gobj, node_id)
|
|
2021
|
+
{
|
|
2022
|
+
let priv = gobj.priv;
|
|
2023
|
+
if(!priv._open_nodes) {
|
|
2024
|
+
priv._open_nodes = new Set();
|
|
2025
|
+
}
|
|
2026
|
+
let compact = (gobj_read_str_attr(gobj, "node_mode") === "compact");
|
|
2027
|
+
return priv._open_nodes.has(node_id)? !compact : compact;
|
|
2028
|
+
}
|
|
1920
2029
|
|
|
1921
|
-
|
|
2030
|
+
/************************************************************
|
|
2031
|
+
* The CARD: an html node, the tier's size, the ports, the
|
|
2032
|
+
* saved geometry on top. The flags (focus, selection, anchor)
|
|
2033
|
+
* are not painted here: repaint_cards() reads them where they
|
|
2034
|
+
* live and rebuilds the html.
|
|
2035
|
+
************************************************************/
|
|
2036
|
+
function card_shape_of(gobj, desc, record, geometry, pills_html)
|
|
2037
|
+
{
|
|
2038
|
+
let priv = gobj.priv;
|
|
1922
2039
|
let node_treedb_type = desc.node_treedb_type;
|
|
2040
|
+
let label = node_label(desc, record);
|
|
2041
|
+
let ports = build_ports(gobj, desc);
|
|
1923
2042
|
|
|
1924
2043
|
let style = {
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
2044
|
+
fill: desc.color,
|
|
2045
|
+
stroke: getStrokeColor(desc.color),
|
|
2046
|
+
lineWidth: 1,
|
|
2047
|
+
lineDash: [],
|
|
2048
|
+
halo: false,
|
|
2049
|
+
labelText: "", /* a card says its name inside; the label is the square's */
|
|
1931
2050
|
};
|
|
1932
2051
|
|
|
1933
2052
|
/* The pills are part of the card, so the card is taller when it
|
|
1934
2053
|
* has any: a row of them on a 96px card sat on the subtitle. */
|
|
1935
|
-
let pills_html = pills_html_of_key(gobj, fold_node_key(desc.topic_name, String(record.id)));
|
|
1936
2054
|
let size = card_size_for(node_treedb_type, !!pills_html);
|
|
2055
|
+
style.size = size;
|
|
2056
|
+
style.dx = -size[0] / 2;
|
|
2057
|
+
style.dy = -size[1] / 2;
|
|
1937
2058
|
|
|
1938
2059
|
if(node_treedb_type === 'child') {
|
|
1939
2060
|
// Pure child (LEAF): smallest tier. Rounded-rect chip,
|
|
1940
2061
|
// same card language as entities, lighter.
|
|
1941
|
-
node_graph_type = 'html';
|
|
1942
|
-
style.size = size;
|
|
1943
|
-
style.dx = -size[0] / 2;
|
|
1944
|
-
style.dy = -size[1] / 2;
|
|
1945
2062
|
style.innerHTML = build_chip_innerHTML(
|
|
1946
|
-
desc.color, priv.theme, record.icon,
|
|
1947
|
-
node_label(desc, record), record.id
|
|
2063
|
+
desc.color, priv.theme, record.icon, label, record.id
|
|
1948
2064
|
);
|
|
1949
|
-
|
|
1950
2065
|
} else if(node_treedb_type === 'extended') {
|
|
1951
2066
|
// Extended (structural / INTERMEDIATE): middle tier. Card
|
|
1952
2067
|
// with name; "structural" style (neutral grey, dashed
|
|
1953
2068
|
// border) to read as a container/junction.
|
|
1954
|
-
node_graph_type = 'html';
|
|
1955
|
-
style.size = size;
|
|
1956
|
-
style.dx = -size[0] / 2;
|
|
1957
|
-
style.dy = -size[1] / 2;
|
|
1958
2069
|
style.innerHTML = build_node_innerHTML(
|
|
1959
|
-
desc.color, priv.theme, record.icon,
|
|
2070
|
+
desc.color, priv.theme, record.icon, label,
|
|
1960
2071
|
desc.topic_name, true, record.id, false, false, false, pills_html
|
|
1961
2072
|
);
|
|
1962
|
-
|
|
1963
2073
|
} else {
|
|
1964
2074
|
// Hierarchical entity (ROOT / container): largest tier.
|
|
1965
|
-
node_graph_type = 'html';
|
|
1966
|
-
style.size = size;
|
|
1967
|
-
style.dx = -size[0] / 2;
|
|
1968
|
-
style.dy = -size[1] / 2;
|
|
1969
2075
|
style.innerHTML = build_node_innerHTML(
|
|
1970
|
-
desc.color, priv.theme, record.icon,
|
|
2076
|
+
desc.color, priv.theme, record.icon, label,
|
|
1971
2077
|
desc.topic_name, false, record.id, false, false, false, pills_html
|
|
1972
2078
|
);
|
|
1973
2079
|
}
|
|
@@ -1980,10 +2086,8 @@ function create_topic_node(gobj, desc, record)
|
|
|
1980
2086
|
let def_size = topic_defaults.size;
|
|
1981
2087
|
if(!geometry.size && Array.isArray(def_size) && def_size.length > 0) {
|
|
1982
2088
|
style.size = [...def_size];
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
style.dy = -(def_size.length > 1 ? def_size[1] : def_size[0]) / 2;
|
|
1986
|
-
}
|
|
2089
|
+
style.dx = -def_size[0] / 2;
|
|
2090
|
+
style.dy = -(def_size.length > 1 ? def_size[1] : def_size[0]) / 2;
|
|
1987
2091
|
}
|
|
1988
2092
|
if(!geometry.portR && topic_defaults.portR > 0) {
|
|
1989
2093
|
style.portR = topic_defaults.portR;
|
|
@@ -1994,35 +2098,20 @@ function create_topic_node(gobj, desc, record)
|
|
|
1994
2098
|
let saved_size = geometry.size;
|
|
1995
2099
|
if(Array.isArray(saved_size) && saved_size.length > 0) {
|
|
1996
2100
|
style.size = saved_size;
|
|
1997
|
-
// Recalculate dx/dy
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
style.dy = -(saved_size.length > 1 ? saved_size[1] : saved_size[0]) / 2;
|
|
2001
|
-
}
|
|
2101
|
+
// Recalculate dx/dy to keep content centered
|
|
2102
|
+
style.dx = -saved_size[0] / 2;
|
|
2103
|
+
style.dy = -(saved_size.length > 1 ? saved_size[1] : saved_size[0]) / 2;
|
|
2002
2104
|
}
|
|
2003
2105
|
if(geometry.portR > 0) {
|
|
2004
2106
|
style.portR = geometry.portR;
|
|
2005
2107
|
}
|
|
2006
2108
|
|
|
2007
|
-
let node_def = {
|
|
2008
|
-
id: node_name,
|
|
2009
|
-
type: node_graph_type,
|
|
2010
|
-
style: style,
|
|
2011
|
-
data: {
|
|
2012
|
-
// This 4 keys are available in user `data` of G6 Node.
|
|
2013
|
-
topic_name: desc.topic_name,
|
|
2014
|
-
desc: desc,
|
|
2015
|
-
record: record,
|
|
2016
|
-
graph_props: geometry // from __graphs__ (or _geometry fallback)
|
|
2017
|
-
}
|
|
2018
|
-
};
|
|
2019
|
-
|
|
2020
2109
|
if(json_size(ports)) {
|
|
2021
2110
|
json_object_update_missing(style, {
|
|
2022
2111
|
port: true,
|
|
2023
2112
|
ports: ports,
|
|
2024
|
-
portR: node_treedb_type === 'child' ?
|
|
2025
|
-
portLineWidth:
|
|
2113
|
+
portR: node_treedb_type === 'child' ? PORT_R_CHILD : PORT_R_ENTITY,
|
|
2114
|
+
portLineWidth: PORT_LINE_WIDTH,
|
|
2026
2115
|
});
|
|
2027
2116
|
|
|
2028
2117
|
// Restore per-port radius: saved geometry first, then topic defaults
|
|
@@ -2040,11 +2129,135 @@ function create_topic_node(gobj, desc, record)
|
|
|
2040
2129
|
}
|
|
2041
2130
|
}
|
|
2042
2131
|
}
|
|
2132
|
+
} else {
|
|
2133
|
+
style.port = false;
|
|
2134
|
+
style.ports = [];
|
|
2135
|
+
}
|
|
2136
|
+
|
|
2137
|
+
return {type: 'html', style: style};
|
|
2138
|
+
}
|
|
2139
|
+
|
|
2140
|
+
/************************************************************
|
|
2141
|
+
* The SQUARE: a G6 `rect` of the topic's colour, no ports, the
|
|
2142
|
+
* name as a label under it when `node_labels` says so. A native
|
|
2143
|
+
* shape and not html on purpose: G6's own stroke and halo paint
|
|
2144
|
+
* on it, so the focus, the selection and the anchor are three
|
|
2145
|
+
* keys of the style instead of a rebuilt innerHTML.
|
|
2146
|
+
*
|
|
2147
|
+
* flags {highlight, selected, anchored}
|
|
2148
|
+
************************************************************/
|
|
2149
|
+
function compact_shape_of(gobj, desc, record, flags)
|
|
2150
|
+
{
|
|
2151
|
+
let priv = gobj.priv;
|
|
2152
|
+
let dark = (priv.theme === "dark");
|
|
2153
|
+
let f = flags || {};
|
|
2154
|
+
let tier = desc.node_treedb_type;
|
|
2155
|
+
let side = COMPACT_SIZE[tier] || COMPACT_SIZE.hierarchical;
|
|
2156
|
+
let labels = gobj_read_bool_attr(gobj, "node_labels");
|
|
2157
|
+
|
|
2158
|
+
let stroke = getStrokeColor(desc.color);
|
|
2159
|
+
let line_width = 1.5;
|
|
2160
|
+
if(f.selected) {
|
|
2161
|
+
stroke = SELECT_RING;
|
|
2162
|
+
line_width = 3;
|
|
2163
|
+
}
|
|
2164
|
+
if(f.highlight) {
|
|
2165
|
+
stroke = HIGHLIGHT_COLOR;
|
|
2166
|
+
line_width = 3;
|
|
2167
|
+
}
|
|
2168
|
+
/* Dashed says "structural", as the extended card does; the
|
|
2169
|
+
* anchor's mark is a tighter dash, as on the card. */
|
|
2170
|
+
let dash = [];
|
|
2171
|
+
if(tier === 'extended') {
|
|
2172
|
+
dash = [4, 3];
|
|
2173
|
+
}
|
|
2174
|
+
if(f.anchored) {
|
|
2175
|
+
dash = [2, 2];
|
|
2176
|
+
line_width = Math.max(line_width, 2.5);
|
|
2177
|
+
}
|
|
2178
|
+
|
|
2179
|
+
let style = {
|
|
2180
|
+
size: [side, side],
|
|
2181
|
+
radius: Math.round(side / 5),
|
|
2182
|
+
fill: desc.color,
|
|
2183
|
+
stroke: stroke,
|
|
2184
|
+
lineWidth: line_width,
|
|
2185
|
+
lineDash: dash,
|
|
2186
|
+
halo: !!f.highlight,
|
|
2187
|
+
haloStroke: HIGHLIGHT_HALO,
|
|
2188
|
+
haloLineWidth: 8,
|
|
2189
|
+
port: false,
|
|
2190
|
+
ports: [],
|
|
2191
|
+
labelText: labels? node_label(desc, record) : "",
|
|
2192
|
+
labelPlacement: 'bottom',
|
|
2193
|
+
labelFontSize: 11,
|
|
2194
|
+
labelFill: dark? '#e6e9ef' : '#2e333d',
|
|
2195
|
+
labelMaxWidth: 150,
|
|
2196
|
+
labelWordWrap: true,
|
|
2197
|
+
labelMaxLines: 1,
|
|
2198
|
+
labelTextOverflow: 'ellipsis',
|
|
2199
|
+
labelBackground: true,
|
|
2200
|
+
labelBackgroundFill: dark? '#1b2230' : '#ffffff',
|
|
2201
|
+
labelBackgroundOpacity: 0.85,
|
|
2202
|
+
labelBackgroundRadius: 3,
|
|
2203
|
+
labelPadding: [1, 3],
|
|
2204
|
+
};
|
|
2205
|
+
return {type: 'rect', style: style};
|
|
2206
|
+
}
|
|
2207
|
+
|
|
2208
|
+
/************************************************************
|
|
2209
|
+
* Give these record nodes the shape their mode says, then
|
|
2210
|
+
* repaint them with the flags they carry. A `+N` chip is not a
|
|
2211
|
+
* record and keeps its own shape.
|
|
2212
|
+
************************************************************/
|
|
2213
|
+
function reshape_nodes(gobj, ids)
|
|
2214
|
+
{
|
|
2215
|
+
let priv = gobj.priv;
|
|
2216
|
+
let graph = priv.graph;
|
|
2217
|
+
if(!graph) {
|
|
2218
|
+
return;
|
|
2219
|
+
}
|
|
2220
|
+
let updates = [];
|
|
2221
|
+
let touched = new Set();
|
|
2222
|
+
for(let id of ids) {
|
|
2223
|
+
let nd;
|
|
2224
|
+
try {
|
|
2225
|
+
nd = graph.getNodeData(id);
|
|
2226
|
+
} catch(e) {
|
|
2227
|
+
continue;
|
|
2228
|
+
}
|
|
2229
|
+
if(!nd || !nd.data || !nd.data.desc || is_more_node(nd)) {
|
|
2230
|
+
continue;
|
|
2231
|
+
}
|
|
2232
|
+
let geometry = nd.data.graph_props || {};
|
|
2233
|
+
let shape = node_shape_of(
|
|
2234
|
+
gobj, id, nd.data.desc, nd.data.record || {}, geometry, pills_html_of(gobj, nd)
|
|
2235
|
+
);
|
|
2236
|
+
updates.push({id: id, type: shape.type, style: shape.style});
|
|
2237
|
+
touched.add(id);
|
|
2238
|
+
}
|
|
2239
|
+
if(!updates.length) {
|
|
2240
|
+
return;
|
|
2241
|
+
}
|
|
2242
|
+
try {
|
|
2243
|
+
graph.updateNodeData(updates);
|
|
2244
|
+
} catch(e) {
|
|
2245
|
+
log_error(`${gobj_short_name(gobj)}: cannot reshape the nodes: ${e}`);
|
|
2246
|
+
return;
|
|
2043
2247
|
}
|
|
2248
|
+
repaint_cards(gobj, touched);
|
|
2249
|
+
}
|
|
2044
2250
|
|
|
2045
|
-
|
|
2046
|
-
|
|
2251
|
+
function all_record_node_ids(gobj)
|
|
2252
|
+
{
|
|
2253
|
+
let graph = gobj.priv.graph;
|
|
2254
|
+
let ids = [];
|
|
2255
|
+
for(let nd of ((graph && graph.getNodeData()) || [])) {
|
|
2256
|
+
if(nd && nd.data && nd.data.desc && !is_more_node(nd)) {
|
|
2257
|
+
ids.push(nd.id);
|
|
2258
|
+
}
|
|
2047
2259
|
}
|
|
2260
|
+
return ids;
|
|
2048
2261
|
}
|
|
2049
2262
|
|
|
2050
2263
|
/************************************************************
|
|
@@ -3336,7 +3549,7 @@ class ManualLayout extends BaseLayout
|
|
|
3336
3549
|
}
|
|
3337
3550
|
|
|
3338
3551
|
/************************************************************
|
|
3339
|
-
* Custom G6 layouts: TreedbTreeLayout /
|
|
3552
|
+
* Custom G6 layouts: TreedbTreeLayout / TreedbRadialLayout.
|
|
3340
3553
|
*
|
|
3341
3554
|
* Thin adapters over treedb_layout.js: the nodes with their sizes,
|
|
3342
3555
|
* the edges with the RANK of the hook they leave by (the position
|
|
@@ -3354,8 +3567,13 @@ function treedb_layout_input(data)
|
|
|
3354
3567
|
let size = (node.style && node.style.size) || [172, 96];
|
|
3355
3568
|
let w = is_array(size)? size[0] : size;
|
|
3356
3569
|
let h = is_array(size)? (size.length > 1? size[1] : size[0]) : size;
|
|
3570
|
+
/* The hook order comes from `data.port_keys`, written when
|
|
3571
|
+
* the node is born: a CLOSED node has no ports on its style,
|
|
3572
|
+
* and its children still have to sit in schema order. */
|
|
3357
3573
|
let ports = (node.style && is_array(node.style.ports))? node.style.ports : [];
|
|
3358
|
-
|
|
3574
|
+
let keys = (node.data && is_array(node.data.port_keys))
|
|
3575
|
+
? node.data.port_keys : ports.map((p) => p.key);
|
|
3576
|
+
ports_of.set(node.id, keys);
|
|
3359
3577
|
return {id: node.id, w: w, h: h};
|
|
3360
3578
|
});
|
|
3361
3579
|
|
|
@@ -3386,14 +3604,6 @@ class TreedbTreeLayout extends BaseLayout
|
|
|
3386
3604
|
}
|
|
3387
3605
|
}
|
|
3388
3606
|
|
|
3389
|
-
class TreedbOutlineLayout extends BaseLayout
|
|
3390
|
-
{
|
|
3391
|
-
async execute(data, options) {
|
|
3392
|
-
let input = treedb_layout_input(data);
|
|
3393
|
-
return treedb_layout_output(layout_outline(input.nodes, input.edges, options));
|
|
3394
|
-
}
|
|
3395
|
-
}
|
|
3396
|
-
|
|
3397
3607
|
class TreedbRadialLayout extends BaseLayout
|
|
3398
3608
|
{
|
|
3399
3609
|
async execute(data, options) {
|
|
@@ -4521,7 +4731,7 @@ function get_port_radius(gobj, node_id, port_key)
|
|
|
4521
4731
|
break;
|
|
4522
4732
|
}
|
|
4523
4733
|
}
|
|
4524
|
-
return style.portR ||
|
|
4734
|
+
return style.portR || PORT_R_ENTITY;
|
|
4525
4735
|
}
|
|
4526
4736
|
|
|
4527
4737
|
/************************************************************
|
|
@@ -4560,7 +4770,7 @@ function detect_port_click(gobj, node_id, canvasX, canvasY)
|
|
|
4560
4770
|
const style = nodeData.style || {};
|
|
4561
4771
|
const {w, h} = extract_size(style);
|
|
4562
4772
|
const ports = style.ports || [];
|
|
4563
|
-
const defaultR = style.portR ||
|
|
4773
|
+
const defaultR = style.portR || PORT_R_ENTITY;
|
|
4564
4774
|
|
|
4565
4775
|
let best_key = null;
|
|
4566
4776
|
let best_dist = Infinity;
|
|
@@ -4812,7 +5022,7 @@ function enter_linking_mode(gobj)
|
|
|
4812
5022
|
if(p.r != null) {
|
|
4813
5023
|
p.r = p.r + 4;
|
|
4814
5024
|
} else {
|
|
4815
|
-
p.r = (style.portR ||
|
|
5025
|
+
p.r = (style.portR || PORT_R_ENTITY) + 4;
|
|
4816
5026
|
}
|
|
4817
5027
|
}
|
|
4818
5028
|
}
|
|
@@ -5040,7 +5250,7 @@ function find_hook_at_point(gobj, canvasX, canvasY)
|
|
|
5040
5250
|
let pl = p.placement || [0.5, 0.5];
|
|
5041
5251
|
let px = pos[0] + (pl[0] - 0.5) * w;
|
|
5042
5252
|
let py = pos[1] + (pl[1] - 0.5) * h;
|
|
5043
|
-
let r = p.r != null ? p.r : (style.portR ||
|
|
5253
|
+
let r = p.r != null ? p.r : (style.portR || PORT_R_ENTITY);
|
|
5044
5254
|
let dx = canvasX - px;
|
|
5045
5255
|
let dy = canvasY - py;
|
|
5046
5256
|
if(Math.sqrt(dx * dx + dy * dy) <= r + 6) {
|
|
@@ -6159,9 +6369,17 @@ function repaint_cards(gobj, ids)
|
|
|
6159
6369
|
} catch(e) {
|
|
6160
6370
|
continue; /* gone since it was listed */
|
|
6161
6371
|
}
|
|
6372
|
+
let anchored = (priv.anchor_state === "on" && id === priv.anchor_id);
|
|
6373
|
+
/* A closed node paints its flags on its own stroke. */
|
|
6374
|
+
if(nd && nd.data && nd.data.desc && node_is_compact(gobj, id)) {
|
|
6375
|
+
updates.push({id: id, style: compact_shape_of(
|
|
6376
|
+
gobj, nd.data.desc, nd.data.record || {},
|
|
6377
|
+
{highlight: focus.has(id), selected: selected.has(id), anchored: anchored}
|
|
6378
|
+
).style});
|
|
6379
|
+
continue;
|
|
6380
|
+
}
|
|
6162
6381
|
let html = node_innerHTML_of(
|
|
6163
|
-
nd, priv.theme, focus.has(id), selected.has(id),
|
|
6164
|
-
priv.anchor_state === "on" && id === priv.anchor_id,
|
|
6382
|
+
nd, priv.theme, focus.has(id), selected.has(id), anchored,
|
|
6165
6383
|
pills_html_of(gobj, nd)
|
|
6166
6384
|
);
|
|
6167
6385
|
if(html !== null) {
|
|
@@ -6219,9 +6437,16 @@ function refresh_html_nodes_theme(gobj, theme)
|
|
|
6219
6437
|
for(let i = 0; i < nodes.length; i++) {
|
|
6220
6438
|
let id = nodes[i].id;
|
|
6221
6439
|
let nd = graph.getNodeData(id);
|
|
6440
|
+
let anchored = (priv.anchor_state === "on" && id === priv.anchor_id);
|
|
6441
|
+
if(nd && nd.data && nd.data.desc && node_is_compact(gobj, id)) {
|
|
6442
|
+
updates.push({id: id, style: compact_shape_of(
|
|
6443
|
+
gobj, nd.data.desc, nd.data.record || {},
|
|
6444
|
+
{highlight: highlighted.has(id), selected: selected.has(id), anchored: anchored}
|
|
6445
|
+
).style});
|
|
6446
|
+
continue;
|
|
6447
|
+
}
|
|
6222
6448
|
let html = node_innerHTML_of(
|
|
6223
|
-
nd, theme, highlighted.has(id), selected.has(id),
|
|
6224
|
-
priv.anchor_state === "on" && id === priv.anchor_id,
|
|
6449
|
+
nd, theme, highlighted.has(id), selected.has(id), anchored,
|
|
6225
6450
|
pills_html_of(gobj, nd)
|
|
6226
6451
|
);
|
|
6227
6452
|
if(html === null) {
|
|
@@ -7170,8 +7395,16 @@ function build_node_context_menu(gobj, node_id)
|
|
|
7170
7395
|
{
|
|
7171
7396
|
let items = [];
|
|
7172
7397
|
|
|
7173
|
-
|
|
7174
|
-
|
|
7398
|
+
inject_svg_icons();
|
|
7399
|
+
/* Open or close THIS node against the mode: the door a finger
|
|
7400
|
+
* has to it (a double click is the mouse's). */
|
|
7401
|
+
if(node_is_compact(gobj, node_id)) {
|
|
7402
|
+
items.push(ctx_item('g6-icon-fullscreen', t('open node'), 'toggle_node_mode'));
|
|
7403
|
+
} else {
|
|
7404
|
+
items.push(ctx_item('g6-icon-fullscreen-exit', t('close node'), 'toggle_node_mode'));
|
|
7405
|
+
}
|
|
7406
|
+
|
|
7407
|
+
if(gobj.priv.edit_mode && !node_is_compact(gobj, node_id)) {
|
|
7175
7408
|
items.push(ctx_item('g6-icon-resize', t('resize all'), 'resize_all_nodes'));
|
|
7176
7409
|
items.push(ctx_item('g6-icon-resize', t('resize topic nodes'), 'resize_topic_nodes'));
|
|
7177
7410
|
}
|
|
@@ -7213,6 +7446,11 @@ function handle_context_menu_click(gobj, value)
|
|
|
7213
7446
|
let priv = gobj.priv;
|
|
7214
7447
|
|
|
7215
7448
|
switch(value) {
|
|
7449
|
+
case 'toggle_node_mode':
|
|
7450
|
+
if(priv._context_node_id) {
|
|
7451
|
+
gobj_send_event(gobj, "EV_TOGGLE_NODE_MODE", {id: priv._context_node_id}, gobj);
|
|
7452
|
+
}
|
|
7453
|
+
break;
|
|
7216
7454
|
case 'resize_all_nodes':
|
|
7217
7455
|
copy_size_to_nodes(gobj, false);
|
|
7218
7456
|
break;
|
|
@@ -7971,8 +8209,9 @@ async function reconcile_fold_apply(gobj, opts, visible)
|
|
|
7971
8209
|
let desc = nd && nd.data && nd.data.desc;
|
|
7972
8210
|
let geometry = (nd && nd.data && nd.data.graph_props) || {};
|
|
7973
8211
|
/* A saved size is the owner's: only a card on the default
|
|
7974
|
-
* size grows and shrinks with its pills.
|
|
7975
|
-
|
|
8212
|
+
* size grows and shrinks with its pills. A closed node has
|
|
8213
|
+
* no pills to grow for. */
|
|
8214
|
+
if(desc && !is_array(geometry.size) && !node_is_compact(gobj, id)) {
|
|
7976
8215
|
let size = card_size_for(desc.node_treedb_type, !!sig);
|
|
7977
8216
|
resize.push({id: id, style: {
|
|
7978
8217
|
size: size, dx: -size[0] / 2, dy: -size[1] / 2,
|
|
@@ -8105,6 +8344,7 @@ function ac_clear_data(gobj, event, kw, src)
|
|
|
8105
8344
|
priv._fold_model = null;
|
|
8106
8345
|
priv._fold_state = null;
|
|
8107
8346
|
priv._pill_sig = null;
|
|
8347
|
+
priv._open_nodes = null;
|
|
8108
8348
|
|
|
8109
8349
|
graph_remove_plugin(gobj, "history");
|
|
8110
8350
|
update_history_buttons(gobj);
|
|
@@ -8855,6 +9095,97 @@ function ac_set_loose_topics(gobj, event, kw, src)
|
|
|
8855
9095
|
return 0;
|
|
8856
9096
|
}
|
|
8857
9097
|
|
|
9098
|
+
/************************************************************
|
|
9099
|
+
* The shape of every record: open cards or closed squares.
|
|
9100
|
+
* A change of mode forgets the nodes toggled against the old
|
|
9101
|
+
* one -- they were exceptions to a rule that no longer holds --
|
|
9102
|
+
* and lays the graph out again, because a square and a card
|
|
9103
|
+
* do not occupy the same room.
|
|
9104
|
+
************************************************************/
|
|
9105
|
+
function ac_set_node_mode(gobj, event, kw, src)
|
|
9106
|
+
{
|
|
9107
|
+
let priv = gobj.priv;
|
|
9108
|
+
let mode = (kw && kw.node_mode === "compact")? "compact" : "expanded";
|
|
9109
|
+
|
|
9110
|
+
gobj_write_str_attr(gobj, "node_mode", mode);
|
|
9111
|
+
priv._open_nodes = new Set();
|
|
9112
|
+
if(!priv.graph || !priv._fold_model) {
|
|
9113
|
+
return 0; /* applied when the records arrive */
|
|
9114
|
+
}
|
|
9115
|
+
reshape_nodes(gobj, all_record_node_ids(gobj));
|
|
9116
|
+
reconcile_fold(gobj, {relayout: true, fit: true});
|
|
9117
|
+
return 0;
|
|
9118
|
+
}
|
|
9119
|
+
|
|
9120
|
+
/************************************************************
|
|
9121
|
+
* The name under a closed square, on or off. Only the closed
|
|
9122
|
+
* nodes change, and nothing moves: the label hangs outside the
|
|
9123
|
+
* square and the layout measures the square.
|
|
9124
|
+
************************************************************/
|
|
9125
|
+
function ac_set_node_labels(gobj, event, kw, src)
|
|
9126
|
+
{
|
|
9127
|
+
let priv = gobj.priv;
|
|
9128
|
+
|
|
9129
|
+
gobj_write_bool_attr(gobj, "node_labels", !!(kw && kw.node_labels));
|
|
9130
|
+
if(!priv.graph) {
|
|
9131
|
+
return 0;
|
|
9132
|
+
}
|
|
9133
|
+
let ids = all_record_node_ids(gobj).filter((id) => node_is_compact(gobj, id));
|
|
9134
|
+
reshape_nodes(gobj, ids);
|
|
9135
|
+
return 0;
|
|
9136
|
+
}
|
|
9137
|
+
|
|
9138
|
+
/************************************************************
|
|
9139
|
+
* ONE node opened or closed against the mode (a double click,
|
|
9140
|
+
* or the context menu). It holds still on screen while the
|
|
9141
|
+
* rest of the graph makes room, as a fold does.
|
|
9142
|
+
************************************************************/
|
|
9143
|
+
function ac_toggle_node_mode(gobj, event, kw, src)
|
|
9144
|
+
{
|
|
9145
|
+
let priv = gobj.priv;
|
|
9146
|
+
let node_id = (kw && kw.id) || "";
|
|
9147
|
+
|
|
9148
|
+
if(!priv.graph || !node_id) {
|
|
9149
|
+
log_error(`${gobj_short_name(gobj)}: toggle node mode without a node`);
|
|
9150
|
+
return -1;
|
|
9151
|
+
}
|
|
9152
|
+
if(!priv._open_nodes) {
|
|
9153
|
+
priv._open_nodes = new Set();
|
|
9154
|
+
}
|
|
9155
|
+
if(priv._open_nodes.has(node_id)) {
|
|
9156
|
+
priv._open_nodes.delete(node_id);
|
|
9157
|
+
} else {
|
|
9158
|
+
priv._open_nodes.add(node_id);
|
|
9159
|
+
}
|
|
9160
|
+
let vp = yui_graph_viewport_of(priv.graph, node_id);
|
|
9161
|
+
reshape_nodes(gobj, [node_id]);
|
|
9162
|
+
if(priv._fold_model) {
|
|
9163
|
+
reconcile_fold(gobj, {relayout: true, keep: node_id, keep_vp: vp});
|
|
9164
|
+
}
|
|
9165
|
+
return 0;
|
|
9166
|
+
}
|
|
9167
|
+
|
|
9168
|
+
/************************************************************
|
|
9169
|
+
* A double click on a node: open it if closed, close it if open.
|
|
9170
|
+
* A `+N` chip is not a record and has no shape to toggle.
|
|
9171
|
+
************************************************************/
|
|
9172
|
+
function ac_node_dblclick(gobj, event, kw, src)
|
|
9173
|
+
{
|
|
9174
|
+
let priv = gobj.priv;
|
|
9175
|
+
let node_id = kw && kw.evt && kw.evt.target? kw.evt.target.id : "";
|
|
9176
|
+
let nd = null;
|
|
9177
|
+
try {
|
|
9178
|
+
nd = priv.graph.getNodeData(node_id);
|
|
9179
|
+
} catch(e) {
|
|
9180
|
+
nd = null;
|
|
9181
|
+
}
|
|
9182
|
+
if(!nd || !nd.data || !nd.data.desc || is_more_node(nd)) {
|
|
9183
|
+
return 0;
|
|
9184
|
+
}
|
|
9185
|
+
gobj_send_event(gobj, "EV_TOGGLE_NODE_MODE", {id: node_id}, gobj);
|
|
9186
|
+
return 0;
|
|
9187
|
+
}
|
|
9188
|
+
|
|
8858
9189
|
/************************************************************
|
|
8859
9190
|
* Tell whoever asked how many nodes the term matched. The count is
|
|
8860
9191
|
* the answer to the question the box asks; without it "nothing moved"
|
|
@@ -9419,6 +9750,10 @@ function create_gclass(gclass_name)
|
|
|
9419
9750
|
["EV_SET_HIDDEN_TOPICS", ac_set_hidden_topics, null],
|
|
9420
9751
|
["EV_SET_MAIN_TOPIC", ac_set_main_topic, null],
|
|
9421
9752
|
["EV_SET_LOOSE_TOPICS", ac_set_loose_topics, null],
|
|
9753
|
+
["EV_SET_NODE_MODE", ac_set_node_mode, null],
|
|
9754
|
+
["EV_SET_NODE_LABELS", ac_set_node_labels, null],
|
|
9755
|
+
["EV_TOGGLE_NODE_MODE", ac_toggle_node_mode, null],
|
|
9756
|
+
["EV_NODE_DBLCLICK", ac_node_dblclick, null],
|
|
9422
9757
|
["EV_CENTER", ac_center, null],
|
|
9423
9758
|
["EV_FULLSCREEN", ac_fullscreen, null],
|
|
9424
9759
|
["EV_SET_LAYOUT", ac_set_layout, null],
|
|
@@ -9479,6 +9814,10 @@ function create_gclass(gclass_name)
|
|
|
9479
9814
|
["EV_SET_HIDDEN_TOPICS", 0],
|
|
9480
9815
|
["EV_SET_MAIN_TOPIC", 0],
|
|
9481
9816
|
["EV_SET_LOOSE_TOPICS", 0],
|
|
9817
|
+
["EV_SET_NODE_MODE", 0],
|
|
9818
|
+
["EV_SET_NODE_LABELS", 0],
|
|
9819
|
+
["EV_TOGGLE_NODE_MODE", 0],
|
|
9820
|
+
["EV_NODE_DBLCLICK", 0],
|
|
9482
9821
|
["EV_LEGEND_STATE", event_flag_t.EVF_OUTPUT_EVENT],
|
|
9483
9822
|
["EV_CENTER", 0],
|
|
9484
9823
|
["EV_FULLSCREEN", 0],
|