@yuneta/gobj-ui 7.23.19 → 7.23.21
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/dist/gobj-ui.cjs.js +351 -282
- package/dist/gobj-ui.es.js +359 -282
- package/package.json +1 -1
- package/src/c_g6_nodes_tree.js +1 -53
- package/src/c_yui_json_graph.js +152 -35
- package/src/json_view_helpers.js +67 -0
- package/src/json_view_helpers.test.js +67 -0
- package/src/lib_graph.js +55 -0
package/package.json
CHANGED
package/src/c_g6_nodes_tree.js
CHANGED
|
@@ -80,6 +80,7 @@ import {
|
|
|
80
80
|
set_cancel_state,
|
|
81
81
|
set_active_state,
|
|
82
82
|
set_pressed_state,
|
|
83
|
+
getPointPosition,
|
|
83
84
|
getStrokeColor,
|
|
84
85
|
} from "./lib_graph.js";
|
|
85
86
|
|
|
@@ -1598,59 +1599,6 @@ function configure_behaviour(gobj)
|
|
|
1598
1599
|
graph_write_behaviors(gobj, behaviors);
|
|
1599
1600
|
}
|
|
1600
1601
|
|
|
1601
|
-
/************************************************************
|
|
1602
|
-
* Returns the proportional position (between 0 and 1) of a specific point,
|
|
1603
|
-
* centered and spaced with margins.
|
|
1604
|
-
*
|
|
1605
|
-
* index - Index of the point (0 to count-1)
|
|
1606
|
-
* count - Total number of points
|
|
1607
|
-
* margin - Total margin space (default 0.2 means 10% on each end)
|
|
1608
|
-
************************************************************/
|
|
1609
|
-
function getPointPosition(count, index, margin = 0.2)
|
|
1610
|
-
{
|
|
1611
|
-
if(count <= 0 || index < 0 || index >= count) {
|
|
1612
|
-
log_error("Invalid count or index");
|
|
1613
|
-
return 0.5;
|
|
1614
|
-
}
|
|
1615
|
-
|
|
1616
|
-
const start = margin / 2;
|
|
1617
|
-
const end = 1 - margin / 2;
|
|
1618
|
-
const step = (end - start) / count;
|
|
1619
|
-
|
|
1620
|
-
return start + index * step + step / 2;
|
|
1621
|
-
}
|
|
1622
|
-
|
|
1623
|
-
/************************************************************
|
|
1624
|
-
* Count hooks and fkeys in topic desc, classify node type
|
|
1625
|
-
************************************************************/
|
|
1626
|
-
function calculate_hooks_fkeys_counter(desc)
|
|
1627
|
-
{
|
|
1628
|
-
let cols = desc.cols;
|
|
1629
|
-
desc.hooks_counter = 0;
|
|
1630
|
-
desc.fkeys_counter = 0;
|
|
1631
|
-
|
|
1632
|
-
for(let i=0; i<cols.length; i++) {
|
|
1633
|
-
let col = cols[i];
|
|
1634
|
-
const field_desc = treedb_get_field_desc(col);
|
|
1635
|
-
switch(field_desc.type) {
|
|
1636
|
-
case "hook":
|
|
1637
|
-
desc.hooks_counter++;
|
|
1638
|
-
break;
|
|
1639
|
-
case "fkey":
|
|
1640
|
-
desc.fkeys_counter++;
|
|
1641
|
-
break;
|
|
1642
|
-
}
|
|
1643
|
-
}
|
|
1644
|
-
|
|
1645
|
-
if(desc.hooks_counter === 0) {
|
|
1646
|
-
desc.node_treedb_type = 'child';
|
|
1647
|
-
} else if(desc.fkeys_counter === 0) {
|
|
1648
|
-
desc.node_treedb_type = 'extended';
|
|
1649
|
-
} else {
|
|
1650
|
-
desc.node_treedb_type = 'hierarchical';
|
|
1651
|
-
}
|
|
1652
|
-
}
|
|
1653
|
-
|
|
1654
1602
|
/************************************************************
|
|
1655
1603
|
* Build ports for a topic desc
|
|
1656
1604
|
************************************************************/
|
package/src/c_yui_json_graph.js
CHANGED
|
@@ -72,6 +72,12 @@ import {
|
|
|
72
72
|
import { ensure_drag_canvas_patch } from "./g6_drag_canvas_touch.js";
|
|
73
73
|
import { ensure_pinch_zoom_patch } from "./g6_touch_gestures.js";
|
|
74
74
|
import { yui_is_dark, yui_theme_now, yui_watch_theme } from "./yui_theme.js";
|
|
75
|
+
import { getPointPosition } from "./lib_graph.js";
|
|
76
|
+
import {
|
|
77
|
+
is_pure_collection,
|
|
78
|
+
has_branch,
|
|
79
|
+
count_branches,
|
|
80
|
+
} from "./json_view_helpers.js";
|
|
75
81
|
|
|
76
82
|
/***************************************************************
|
|
77
83
|
* Constants
|
|
@@ -767,10 +773,22 @@ function build_graph(gobj)
|
|
|
767
773
|
}
|
|
768
774
|
}
|
|
769
775
|
|
|
776
|
+
/************************************************************
|
|
777
|
+
* The port a container key opens on its card.
|
|
778
|
+
*
|
|
779
|
+
* Prefixed so it can never collide with anything G6 puts in
|
|
780
|
+
* the same namespace, and stringified because an array index
|
|
781
|
+
* is a number and a port key is a string.
|
|
782
|
+
************************************************************/
|
|
783
|
+
function port_key(key)
|
|
784
|
+
{
|
|
785
|
+
return "p_" + String(key);
|
|
786
|
+
}
|
|
787
|
+
|
|
770
788
|
/************************************************************
|
|
771
789
|
* Build cell value HTML (matching old mx_json_viewer colors)
|
|
772
790
|
************************************************************/
|
|
773
|
-
function build_cell_html(key, value, type, dark, matched)
|
|
791
|
+
function build_cell_html(key, value, type, dark, matched, fold)
|
|
774
792
|
{
|
|
775
793
|
/*
|
|
776
794
|
* A match is a LIGHT chip on BOTH themes, so what sits on it is
|
|
@@ -813,6 +831,19 @@ function build_cell_html(key, value, type, dark, matched)
|
|
|
813
831
|
let row = `<span style="color:${key_color}">• ${escapeHtml(String(key))}: </span>` +
|
|
814
832
|
`<span style="color:${color}">${display_value}</span>`;
|
|
815
833
|
|
|
834
|
+
/*
|
|
835
|
+
* A fold chip ON THE ROW, for a container that gets no card of its
|
|
836
|
+
* own: the control belongs next to the key it folds, which is
|
|
837
|
+
* where a tree puts it, and it is the only handle those children
|
|
838
|
+
* have -- their parent's header chip folds every branch it has,
|
|
839
|
+
* not this one.
|
|
840
|
+
*/
|
|
841
|
+
if(fold) {
|
|
842
|
+
row += `<span style="margin-left:8px;">` +
|
|
843
|
+
fold_toggle_html(fold.path, true, fold.folded, fold.branches, fold.colors) +
|
|
844
|
+
`</span>`;
|
|
845
|
+
}
|
|
846
|
+
|
|
816
847
|
if(matched) {
|
|
817
848
|
/* The highlight is baked into the card's MARKUP and not set as
|
|
818
849
|
* a G6 node state: the key shape of an `html` node is a DOM
|
|
@@ -963,7 +994,7 @@ function get_group_label(path)
|
|
|
963
994
|
/************************************************************
|
|
964
995
|
* Recursively build nodes and edges from JSON
|
|
965
996
|
************************************************************/
|
|
966
|
-
function build_json_nodes(gobj, path, kw, nodes, edges, parent_id)
|
|
997
|
+
function build_json_nodes(gobj, path, kw, nodes, edges, parent_id, parent_port)
|
|
967
998
|
{
|
|
968
999
|
let group_id = gen_node_id(gobj);
|
|
969
1000
|
let is_dict = is_object(kw);
|
|
@@ -985,6 +1016,10 @@ function build_json_nodes(gobj, path, kw, nodes, edges, parent_id)
|
|
|
985
1016
|
let pending_complex = [];
|
|
986
1017
|
let card_matched = false;
|
|
987
1018
|
|
|
1019
|
+
let colors = json_card_style(
|
|
1020
|
+
is_dict ? GROUP_COLORS.dict : GROUP_COLORS.list, dark
|
|
1021
|
+
);
|
|
1022
|
+
|
|
988
1023
|
let entries = is_dict ? Object.entries(kw) : kw.map((v, i) => [i, v]);
|
|
989
1024
|
|
|
990
1025
|
for(let [key, value] of entries) {
|
|
@@ -996,32 +1031,72 @@ function build_json_nodes(gobj, path, kw, nodes, edges, parent_id)
|
|
|
996
1031
|
}
|
|
997
1032
|
|
|
998
1033
|
/*
|
|
999
|
-
*
|
|
1000
|
-
*
|
|
1001
|
-
*
|
|
1002
|
-
*
|
|
1003
|
-
* the same; an array of a thousand prints a thousand-row card
|
|
1004
|
-
* nobody can read, beside the thousand cards it duplicates.
|
|
1034
|
+
* EVERY key is a row, containers included: `cols` is one key of
|
|
1035
|
+
* the topic dict exactly like `pkey` and `tkey`, and a drawing
|
|
1036
|
+
* that leaves it out of the card does not say what the document
|
|
1037
|
+
* says.
|
|
1005
1038
|
*
|
|
1006
|
-
* What
|
|
1007
|
-
*
|
|
1008
|
-
*
|
|
1009
|
-
*
|
|
1039
|
+
* What a container key does NOT get is its contents repeated
|
|
1040
|
+
* underneath it -- the row says `[14]` and the fourteen cards
|
|
1041
|
+
* say the rest. That is the part which does not scale: an array
|
|
1042
|
+
* of a thousand dicts would otherwise print a thousand-row card
|
|
1043
|
+
* beside the thousand cards it duplicates.
|
|
1010
1044
|
*/
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
pending_complex.push({key: key, value: value});
|
|
1014
|
-
|
|
1045
|
+
let fold = null;
|
|
1046
|
+
if(has_branch(value)) {
|
|
1047
|
+
pending_complex.push({key: key, value: value, port: port_key(key)});
|
|
1048
|
+
|
|
1049
|
+
/*
|
|
1050
|
+
* A child that gets no card of its own is folded from HERE
|
|
1051
|
+
* or from nowhere.
|
|
1052
|
+
*/
|
|
1053
|
+
if(is_pure_collection(value)) {
|
|
1054
|
+
let child_path = add_segment(path, key);
|
|
1055
|
+
fold = {
|
|
1056
|
+
path: child_path,
|
|
1057
|
+
folded: priv.collapsed.has(child_path),
|
|
1058
|
+
branches: count_branches(value),
|
|
1059
|
+
colors: colors,
|
|
1060
|
+
};
|
|
1061
|
+
}
|
|
1015
1062
|
}
|
|
1016
1063
|
|
|
1017
|
-
lines.push(build_cell_html(key, value, type, dark, matched));
|
|
1064
|
+
lines.push(build_cell_html(key, value, type, dark, matched, fold));
|
|
1018
1065
|
}
|
|
1019
1066
|
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1067
|
+
/*
|
|
1068
|
+
* A pure collection is not drawn: its row above names it and its
|
|
1069
|
+
* children hang from THIS card's parent. The root always gets a
|
|
1070
|
+
* card, or a document that is a plain list of records would have
|
|
1071
|
+
* nothing to hang anything from.
|
|
1072
|
+
*/
|
|
1073
|
+
if(parent_id && is_pure_collection(kw)) {
|
|
1074
|
+
if(priv.collapsed.has(path)) {
|
|
1075
|
+
return parent_id;
|
|
1076
|
+
}
|
|
1077
|
+
for(let pending of pending_complex) {
|
|
1078
|
+
/*
|
|
1079
|
+
* `parent_port` and not one of ours: this collection has no
|
|
1080
|
+
* card, so the port its row opened in the PARENT is the one
|
|
1081
|
+
* every one of these lines leaves from -- fourteen columns
|
|
1082
|
+
* out of the single `cols` port, which is what the document
|
|
1083
|
+
* says and what the reader traces back.
|
|
1084
|
+
*/
|
|
1085
|
+
build_json_nodes(
|
|
1086
|
+
gobj,
|
|
1087
|
+
add_segment(path, pending.key),
|
|
1088
|
+
pending.value,
|
|
1089
|
+
nodes,
|
|
1090
|
+
edges,
|
|
1091
|
+
parent_id,
|
|
1092
|
+
parent_port
|
|
1093
|
+
);
|
|
1094
|
+
}
|
|
1095
|
+
return parent_id;
|
|
1023
1096
|
}
|
|
1024
1097
|
|
|
1098
|
+
let label = get_group_label(path) || (is_dict ? "{}" : "[]");
|
|
1099
|
+
|
|
1025
1100
|
/*
|
|
1026
1101
|
* Folded: keep the card, drop the branch, and SAY how much went
|
|
1027
1102
|
* with it. A cut that is not marked reads as a document that ends
|
|
@@ -1032,9 +1107,6 @@ function build_json_nodes(gobj, path, kw, nodes, edges, parent_id)
|
|
|
1032
1107
|
*/
|
|
1033
1108
|
let foldable = pending_complex.length > 0;
|
|
1034
1109
|
let folded = foldable && priv.collapsed.has(path);
|
|
1035
|
-
let colors = json_card_style(
|
|
1036
|
-
is_dict ? GROUP_COLORS.dict : GROUP_COLORS.list, dark
|
|
1037
|
-
);
|
|
1038
1110
|
let border_color = card_matched? (dark? "#ffb300": "#ff8f00"): colors.border;
|
|
1039
1111
|
let border_width = card_matched? 2: 1;
|
|
1040
1112
|
|
|
@@ -1069,6 +1141,38 @@ function build_json_nodes(gobj, path, kw, nodes, edges, parent_id)
|
|
|
1069
1141
|
${content_html}
|
|
1070
1142
|
</div>`;
|
|
1071
1143
|
|
|
1144
|
+
/*
|
|
1145
|
+
* ONE PORT PER CONTAINER KEY, and every line leaves from its own.
|
|
1146
|
+
*
|
|
1147
|
+
* Without them each card was a single anchor: fourteen lines came
|
|
1148
|
+
* out of one point, and which row a line belonged to was a guess
|
|
1149
|
+
* the reader had to make from where it landed. A port makes the row
|
|
1150
|
+
* the ORIGIN, which is the same thing the treedb graph does with a
|
|
1151
|
+
* hook column and the reason `getPointPosition` is now shared.
|
|
1152
|
+
*
|
|
1153
|
+
* On the BOTTOM edge because the layouts put children below, and
|
|
1154
|
+
* exactly ON it (`y = 1`) rather than inside: an html node draws its
|
|
1155
|
+
* HTML in a DOM layer above the canvas, so a port fully inside the
|
|
1156
|
+
* box would be painted under the card and never seen. Half of it
|
|
1157
|
+
* sticks out.
|
|
1158
|
+
*
|
|
1159
|
+
* Coloured by what the row LEADS TO -- a dict or a list -- which is
|
|
1160
|
+
* the one thing the port can say that the row does not.
|
|
1161
|
+
*/
|
|
1162
|
+
let ports = [];
|
|
1163
|
+
for(let i = 0; i < pending_complex.length; i++) {
|
|
1164
|
+
let pending = pending_complex[i];
|
|
1165
|
+
let child_colors = json_card_style(
|
|
1166
|
+
is_object(pending.value)? GROUP_COLORS.dict: GROUP_COLORS.list, dark
|
|
1167
|
+
);
|
|
1168
|
+
ports.push({
|
|
1169
|
+
key: pending.port,
|
|
1170
|
+
placement: [getPointPosition(pending_complex.length, i), 1],
|
|
1171
|
+
fill: child_colors.border,
|
|
1172
|
+
stroke: colors.border,
|
|
1173
|
+
});
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1072
1176
|
let node = {
|
|
1073
1177
|
id: group_id,
|
|
1074
1178
|
type: 'html',
|
|
@@ -1081,22 +1185,30 @@ function build_json_nodes(gobj, path, kw, nodes, edges, parent_id)
|
|
|
1081
1185
|
size: [min_width, 24 + lines.length * 22],
|
|
1082
1186
|
dx: -(min_width / 2),
|
|
1083
1187
|
dy: -((24 + lines.length * 22) / 2),
|
|
1188
|
+
port: ports.length > 0,
|
|
1189
|
+
ports: ports,
|
|
1190
|
+
portR: 4,
|
|
1191
|
+
portLineWidth: 1,
|
|
1084
1192
|
},
|
|
1085
1193
|
};
|
|
1086
1194
|
nodes.push(node);
|
|
1087
1195
|
|
|
1088
1196
|
/*
|
|
1089
|
-
* Edge from parent to this group
|
|
1197
|
+
* Edge from parent to this group, out of the port its key opened.
|
|
1090
1198
|
*/
|
|
1091
1199
|
if(parent_id) {
|
|
1092
|
-
|
|
1200
|
+
let edge = {
|
|
1093
1201
|
source: parent_id,
|
|
1094
1202
|
target: group_id,
|
|
1095
1203
|
style: {
|
|
1096
1204
|
stroke: colors.border,
|
|
1097
1205
|
lineWidth: 1,
|
|
1098
1206
|
}
|
|
1099
|
-
}
|
|
1207
|
+
};
|
|
1208
|
+
if(parent_port) {
|
|
1209
|
+
edge.style.sourcePort = parent_port;
|
|
1210
|
+
}
|
|
1211
|
+
edges.push(edge);
|
|
1100
1212
|
}
|
|
1101
1213
|
|
|
1102
1214
|
/*
|
|
@@ -1112,7 +1224,8 @@ function build_json_nodes(gobj, path, kw, nodes, edges, parent_id)
|
|
|
1112
1224
|
pending.value,
|
|
1113
1225
|
nodes,
|
|
1114
1226
|
edges,
|
|
1115
|
-
group_id
|
|
1227
|
+
group_id,
|
|
1228
|
+
pending.port
|
|
1116
1229
|
);
|
|
1117
1230
|
}
|
|
1118
1231
|
|
|
@@ -1190,8 +1303,11 @@ function load_json(gobj)
|
|
|
1190
1303
|
|
|
1191
1304
|
/************************************************************
|
|
1192
1305
|
* Every path that HAS a branch to fold, in the same recursion
|
|
1193
|
-
* build_json_nodes uses — so "collapse all" can never name
|
|
1194
|
-
*
|
|
1306
|
+
* build_json_nodes uses — so "collapse all" can never name
|
|
1307
|
+
* something with no handle. The handle is a card's header chip
|
|
1308
|
+
* for a container that gets a card, and the chip on its ROW in
|
|
1309
|
+
* the parent for a pure collection, which gets none; both are
|
|
1310
|
+
* keyed by this same path. The root is excluded: folding it
|
|
1195
1311
|
* would leave an empty canvas.
|
|
1196
1312
|
************************************************************/
|
|
1197
1313
|
function collect_foldable_paths(value, path, out, is_root)
|
|
@@ -1202,20 +1318,21 @@ function collect_foldable_paths(value, path, out, is_root)
|
|
|
1202
1318
|
return false;
|
|
1203
1319
|
}
|
|
1204
1320
|
|
|
1205
|
-
|
|
1321
|
+
/* `found_branch` and not `has_branch`: that name belongs to the
|
|
1322
|
+
* helper above, and a local would shadow it here. */
|
|
1323
|
+
let found_branch = false;
|
|
1206
1324
|
let entries = is_dict ? Object.entries(value) : value.map((v, i) => [i, v]);
|
|
1207
1325
|
for(let [key, child] of entries) {
|
|
1208
|
-
if((
|
|
1209
|
-
|
|
1210
|
-
has_branch = true;
|
|
1326
|
+
if(has_branch(child)) {
|
|
1327
|
+
found_branch = true;
|
|
1211
1328
|
collect_foldable_paths(child, add_segment(path, key), out, false);
|
|
1212
1329
|
}
|
|
1213
1330
|
}
|
|
1214
1331
|
|
|
1215
|
-
if(
|
|
1332
|
+
if(found_branch && !is_root) {
|
|
1216
1333
|
out.push(path);
|
|
1217
1334
|
}
|
|
1218
|
-
return
|
|
1335
|
+
return found_branch;
|
|
1219
1336
|
}
|
|
1220
1337
|
|
|
1221
1338
|
/************************************************************
|
package/src/json_view_helpers.js
CHANGED
|
@@ -314,3 +314,70 @@ export function container_label(value, max_chars)
|
|
|
314
314
|
}
|
|
315
315
|
return text;
|
|
316
316
|
}
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
/************************************************************
|
|
320
|
+
* A dict, in the JSON sense: not an array and not null.
|
|
321
|
+
************************************************************/
|
|
322
|
+
function is_plain_object(v)
|
|
323
|
+
{
|
|
324
|
+
return v !== null && typeof v === "object" && !Array.isArray(v);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/************************************************************
|
|
328
|
+
* Does this container hold ONLY other containers?
|
|
329
|
+
*
|
|
330
|
+
* Such a value is not a thing, it is a LIST of things -- and a
|
|
331
|
+
* list is fully described by the row that names it in its
|
|
332
|
+
* parent, plus the edges to what it holds. Drawing it as a
|
|
333
|
+
* card of its own put a node in the middle of the graph with no
|
|
334
|
+
* data in it, saying nothing the parent's row did not already
|
|
335
|
+
* say, and pushed everything below it one level down.
|
|
336
|
+
*
|
|
337
|
+
* An EMPTY container inside counts as content: `{0}` is
|
|
338
|
+
* something the card has to show, and there is no child card
|
|
339
|
+
* to show it instead.
|
|
340
|
+
************************************************************/
|
|
341
|
+
export function is_pure_collection(value)
|
|
342
|
+
{
|
|
343
|
+
let entries = is_plain_object(value)? Object.entries(value)
|
|
344
|
+
: Array.isArray(value)? value.map((v, i) => [i, v])
|
|
345
|
+
: [];
|
|
346
|
+
|
|
347
|
+
if(entries.length === 0) {
|
|
348
|
+
return false;
|
|
349
|
+
}
|
|
350
|
+
for(let [, v] of entries) {
|
|
351
|
+
if(!has_branch(v)) {
|
|
352
|
+
return false;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
return true;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/************************************************************
|
|
359
|
+
* A value that gets a card, or children, of its own.
|
|
360
|
+
************************************************************/
|
|
361
|
+
export function has_branch(value)
|
|
362
|
+
{
|
|
363
|
+
return (is_plain_object(value) && Object.keys(value).length > 0) ||
|
|
364
|
+
(Array.isArray(value) && value.length > 0);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/************************************************************
|
|
368
|
+
* How many branches a container has, which is what the fold
|
|
369
|
+
* chip counts.
|
|
370
|
+
************************************************************/
|
|
371
|
+
export function count_branches(value)
|
|
372
|
+
{
|
|
373
|
+
let entries = is_plain_object(value)? Object.values(value)
|
|
374
|
+
: Array.isArray(value)? value
|
|
375
|
+
: [];
|
|
376
|
+
let n = 0;
|
|
377
|
+
for(let v of entries) {
|
|
378
|
+
if(has_branch(v)) {
|
|
379
|
+
n++;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
return n;
|
|
383
|
+
}
|
|
@@ -18,6 +18,9 @@ import {
|
|
|
18
18
|
json_text_dump,
|
|
19
19
|
pick_view_mode,
|
|
20
20
|
container_label,
|
|
21
|
+
is_pure_collection,
|
|
22
|
+
has_branch,
|
|
23
|
+
count_branches,
|
|
21
24
|
} from "./json_view_helpers.js";
|
|
22
25
|
|
|
23
26
|
|
|
@@ -243,3 +246,67 @@ test("container_label: cut at max_chars, and said with an ellipsis", () => {
|
|
|
243
246
|
expect(container_label({id: uuid}, 0)).toBe(uuid);
|
|
244
247
|
expect(container_label({id: uuid}, undefined)).toBe(uuid);
|
|
245
248
|
});
|
|
249
|
+
|
|
250
|
+
/***************************************************************
|
|
251
|
+
* has_branch() / count_branches() / is_pure_collection()
|
|
252
|
+
***************************************************************/
|
|
253
|
+
test("has_branch: only a NON-EMPTY container branches", () => {
|
|
254
|
+
expect(has_branch({a: 1})).toBe(true);
|
|
255
|
+
expect(has_branch([1])).toBe(true);
|
|
256
|
+
expect(has_branch({})).toBe(false);
|
|
257
|
+
expect(has_branch([])).toBe(false);
|
|
258
|
+
expect(has_branch("x")).toBe(false);
|
|
259
|
+
expect(has_branch(0)).toBe(false);
|
|
260
|
+
expect(has_branch(null)).toBe(false);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
test("count_branches: counts what gets a card, not what is there", () => {
|
|
264
|
+
expect(count_branches({a: 1, b: {}, c: {x: 1}, d: [1]})).toBe(2);
|
|
265
|
+
expect(count_branches([{}, [], {a: 1}])).toBe(1);
|
|
266
|
+
expect(count_branches({a: 1})).toBe(0);
|
|
267
|
+
expect(count_branches("x")).toBe(0);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
test("is_pure_collection: a LIST of things, not a thing", () => {
|
|
271
|
+
/* Every entry a container with something in it. */
|
|
272
|
+
expect(is_pure_collection([{a: 1}, {b: 2}])).toBe(true);
|
|
273
|
+
expect(is_pure_collection({x: {a: 1}, y: [1]})).toBe(true);
|
|
274
|
+
|
|
275
|
+
/* One scalar is enough to make it a thing with content of its own,
|
|
276
|
+
* and so is an EMPTY container: `{0}` has no child card to show it. */
|
|
277
|
+
expect(is_pure_collection([{a: 1}, 2])).toBe(false);
|
|
278
|
+
expect(is_pure_collection({x: {a: 1}, n: 3})).toBe(false);
|
|
279
|
+
expect(is_pure_collection([{a: 1}, {}])).toBe(false);
|
|
280
|
+
|
|
281
|
+
expect(is_pure_collection([])).toBe(false);
|
|
282
|
+
expect(is_pure_collection({})).toBe(false);
|
|
283
|
+
expect(is_pure_collection("x")).toBe(false);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
test("is_pure_collection: the schema this came from", () => {
|
|
287
|
+
/* The `devices` topic descriptor, cut to shape: `cols` is one key of
|
|
288
|
+
* the dict like `pkey` is, and the only one that branches. */
|
|
289
|
+
let col = {id: "id", header: "Device", type: "string", flag: [], enum: {}};
|
|
290
|
+
let topic = {
|
|
291
|
+
topic_name: "devices",
|
|
292
|
+
pkey: "id",
|
|
293
|
+
pkey2s: [], /* empty: a row, not a branch */
|
|
294
|
+
tkey: "tm",
|
|
295
|
+
system_flag: 1,
|
|
296
|
+
topic_version: 3,
|
|
297
|
+
cols: [col, col, col],
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
/* The topic is NOT a pure collection: it has six scalar rows, so it
|
|
301
|
+
* keeps its card. */
|
|
302
|
+
expect(is_pure_collection(topic)).toBe(false);
|
|
303
|
+
expect(count_branches(topic)).toBe(1);
|
|
304
|
+
|
|
305
|
+
/* `cols` IS one: nothing but columns. It gets no card of its own --
|
|
306
|
+
* its row in the topic names it, and the columns hang from the topic. */
|
|
307
|
+
expect(is_pure_collection(topic.cols)).toBe(true);
|
|
308
|
+
expect(count_branches(topic.cols)).toBe(3);
|
|
309
|
+
|
|
310
|
+
/* A column has content, so it keeps its card. */
|
|
311
|
+
expect(is_pure_collection(col)).toBe(false);
|
|
312
|
+
});
|
package/src/lib_graph.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
***********************************************************************/
|
|
9
9
|
import {
|
|
10
10
|
gobj_read_attr,
|
|
11
|
+
log_error,
|
|
11
12
|
} from "@yuneta/gobj-js";
|
|
12
13
|
|
|
13
14
|
// import {t} from "i18next";
|
|
@@ -195,6 +196,59 @@ function getStrokeColor(fillColor, theme = 'light', factor = 0.2) {
|
|
|
195
196
|
//=======================================================================
|
|
196
197
|
// Expose the class via the global object
|
|
197
198
|
//=======================================================================
|
|
199
|
+
/************************************************************
|
|
200
|
+
* Returns the proportional position (between 0 and 1) of a specific point,
|
|
201
|
+
* centered and spaced with margins.
|
|
202
|
+
*
|
|
203
|
+
* index - Index of the point (0 to count-1)
|
|
204
|
+
* count - Total number of points
|
|
205
|
+
* margin - Total margin space (default 0.2 means 10% on each end)
|
|
206
|
+
************************************************************/
|
|
207
|
+
function getPointPosition(count, index, margin = 0.2)
|
|
208
|
+
{
|
|
209
|
+
if(count <= 0 || index < 0 || index >= count) {
|
|
210
|
+
log_error("Invalid count or index");
|
|
211
|
+
return 0.5;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const start = margin / 2;
|
|
215
|
+
const end = 1 - margin / 2;
|
|
216
|
+
const step = (end - start) / count;
|
|
217
|
+
|
|
218
|
+
return start + index * step + step / 2;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/************************************************************
|
|
222
|
+
* Count hooks and fkeys in topic desc, classify node type
|
|
223
|
+
************************************************************/
|
|
224
|
+
function calculate_hooks_fkeys_counter(desc)
|
|
225
|
+
{
|
|
226
|
+
let cols = desc.cols;
|
|
227
|
+
desc.hooks_counter = 0;
|
|
228
|
+
desc.fkeys_counter = 0;
|
|
229
|
+
|
|
230
|
+
for(let i=0; i<cols.length; i++) {
|
|
231
|
+
let col = cols[i];
|
|
232
|
+
const field_desc = treedb_get_field_desc(col);
|
|
233
|
+
switch(field_desc.type) {
|
|
234
|
+
case "hook":
|
|
235
|
+
desc.hooks_counter++;
|
|
236
|
+
break;
|
|
237
|
+
case "fkey":
|
|
238
|
+
desc.fkeys_counter++;
|
|
239
|
+
break;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if(desc.hooks_counter === 0) {
|
|
244
|
+
desc.node_treedb_type = 'child';
|
|
245
|
+
} else if(desc.fkeys_counter === 0) {
|
|
246
|
+
desc.node_treedb_type = 'extended';
|
|
247
|
+
} else {
|
|
248
|
+
desc.node_treedb_type = 'hierarchical';
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
198
252
|
export {
|
|
199
253
|
addClasses,
|
|
200
254
|
removeClasses,
|
|
@@ -207,4 +261,5 @@ export {
|
|
|
207
261
|
set_active_state,
|
|
208
262
|
set_pressed_state,
|
|
209
263
|
getStrokeColor,
|
|
264
|
+
getPointPosition,
|
|
210
265
|
};
|