@yuneta/gobj-ui 7.23.19 → 7.23.20

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.
@@ -72,6 +72,11 @@ 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 {
76
+ is_pure_collection,
77
+ has_branch,
78
+ count_branches,
79
+ } from "./json_view_helpers.js";
75
80
 
76
81
  /***************************************************************
77
82
  * Constants
@@ -770,7 +775,7 @@ function build_graph(gobj)
770
775
  /************************************************************
771
776
  * Build cell value HTML (matching old mx_json_viewer colors)
772
777
  ************************************************************/
773
- function build_cell_html(key, value, type, dark, matched)
778
+ function build_cell_html(key, value, type, dark, matched, fold)
774
779
  {
775
780
  /*
776
781
  * A match is a LIGHT chip on BOTH themes, so what sits on it is
@@ -813,6 +818,19 @@ function build_cell_html(key, value, type, dark, matched)
813
818
  let row = `<span style="color:${key_color}">• ${escapeHtml(String(key))}: </span>` +
814
819
  `<span style="color:${color}">${display_value}</span>`;
815
820
 
821
+ /*
822
+ * A fold chip ON THE ROW, for a container that gets no card of its
823
+ * own: the control belongs next to the key it folds, which is
824
+ * where a tree puts it, and it is the only handle those children
825
+ * have -- their parent's header chip folds every branch it has,
826
+ * not this one.
827
+ */
828
+ if(fold) {
829
+ row += `<span style="margin-left:8px;">` +
830
+ fold_toggle_html(fold.path, true, fold.folded, fold.branches, fold.colors) +
831
+ `</span>`;
832
+ }
833
+
816
834
  if(matched) {
817
835
  /* The highlight is baked into the card's MARKUP and not set as
818
836
  * a G6 node state: the key shape of an `html` node is a DOM
@@ -985,6 +1003,10 @@ function build_json_nodes(gobj, path, kw, nodes, edges, parent_id)
985
1003
  let pending_complex = [];
986
1004
  let card_matched = false;
987
1005
 
1006
+ let colors = json_card_style(
1007
+ is_dict ? GROUP_COLORS.dict : GROUP_COLORS.list, dark
1008
+ );
1009
+
988
1010
  let entries = is_dict ? Object.entries(kw) : kw.map((v, i) => [i, v]);
989
1011
 
990
1012
  for(let [key, value] of entries) {
@@ -996,32 +1018,64 @@ function build_json_nodes(gobj, path, kw, nodes, edges, parent_id)
996
1018
  }
997
1019
 
998
1020
  /*
999
- * A non-empty container gets a CARD OF ITS OWN, so a row for it
1000
- * here would be the same thing drawn twice -- and the second
1001
- * drawing is the one that does not scale. An array of 14 dicts
1002
- * printed fourteen `0: {13}` rows above fourteen cards saying
1003
- * the same; an array of a thousand prints a thousand-row card
1004
- * nobody can read, beside the thousand cards it duplicates.
1021
+ * EVERY key is a row, containers included: `cols` is one key of
1022
+ * the topic dict exactly like `pkey` and `tkey`, and a drawing
1023
+ * that leaves it out of the card does not say what the document
1024
+ * says.
1005
1025
  *
1006
- * What is left in the body is exactly the SCALARS: what this
1007
- * node IS. What it CONTAINS is the edges, and the count now
1008
- * rides in the header for the case where the body comes out
1009
- * empty.
1026
+ * What a container key does NOT get is its contents repeated
1027
+ * underneath it -- the row says `[14]` and the fourteen cards
1028
+ * say the rest. That is the part which does not scale: an array
1029
+ * of a thousand dicts would otherwise print a thousand-row card
1030
+ * beside the thousand cards it duplicates.
1010
1031
  */
1011
- if((is_object(value) && json_object_size(value) > 0) ||
1012
- (is_array(value) && value.length > 0)) {
1032
+ let fold = null;
1033
+ if(has_branch(value)) {
1013
1034
  pending_complex.push({key: key, value: value});
1014
- continue;
1035
+
1036
+ /*
1037
+ * A child that gets no card of its own is folded from HERE
1038
+ * or from nowhere.
1039
+ */
1040
+ if(is_pure_collection(value)) {
1041
+ let child_path = add_segment(path, key);
1042
+ fold = {
1043
+ path: child_path,
1044
+ folded: priv.collapsed.has(child_path),
1045
+ branches: count_branches(value),
1046
+ colors: colors,
1047
+ };
1048
+ }
1015
1049
  }
1016
1050
 
1017
- lines.push(build_cell_html(key, value, type, dark, matched));
1051
+ lines.push(build_cell_html(key, value, type, dark, matched, fold));
1018
1052
  }
1019
1053
 
1020
- let label = get_group_label(path) || (is_dict ? "{}" : "[]");
1021
- if(pending_complex.length > 0) {
1022
- label += is_dict? ` {${pending_complex.length}}`: ` [${pending_complex.length}]`;
1054
+ /*
1055
+ * A pure collection is not drawn: its row above names it and its
1056
+ * children hang from THIS card's parent. The root always gets a
1057
+ * card, or a document that is a plain list of records would have
1058
+ * nothing to hang anything from.
1059
+ */
1060
+ if(parent_id && is_pure_collection(kw)) {
1061
+ if(priv.collapsed.has(path)) {
1062
+ return parent_id;
1063
+ }
1064
+ for(let pending of pending_complex) {
1065
+ build_json_nodes(
1066
+ gobj,
1067
+ add_segment(path, pending.key),
1068
+ pending.value,
1069
+ nodes,
1070
+ edges,
1071
+ parent_id
1072
+ );
1073
+ }
1074
+ return parent_id;
1023
1075
  }
1024
1076
 
1077
+ let label = get_group_label(path) || (is_dict ? "{}" : "[]");
1078
+
1025
1079
  /*
1026
1080
  * Folded: keep the card, drop the branch, and SAY how much went
1027
1081
  * with it. A cut that is not marked reads as a document that ends
@@ -1032,9 +1086,6 @@ function build_json_nodes(gobj, path, kw, nodes, edges, parent_id)
1032
1086
  */
1033
1087
  let foldable = pending_complex.length > 0;
1034
1088
  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
1089
  let border_color = card_matched? (dark? "#ffb300": "#ff8f00"): colors.border;
1039
1090
  let border_width = card_matched? 2: 1;
1040
1091
 
@@ -1190,8 +1241,11 @@ function load_json(gobj)
1190
1241
 
1191
1242
  /************************************************************
1192
1243
  * Every path that HAS a branch to fold, in the same recursion
1193
- * build_json_nodes uses — so "collapse all" can never name a
1194
- * card that does not exist. The root is excluded: folding it
1244
+ * build_json_nodes uses — so "collapse all" can never name
1245
+ * something with no handle. The handle is a card's header chip
1246
+ * for a container that gets a card, and the chip on its ROW in
1247
+ * the parent for a pure collection, which gets none; both are
1248
+ * keyed by this same path. The root is excluded: folding it
1195
1249
  * would leave an empty canvas.
1196
1250
  ************************************************************/
1197
1251
  function collect_foldable_paths(value, path, out, is_root)
@@ -1202,20 +1256,21 @@ function collect_foldable_paths(value, path, out, is_root)
1202
1256
  return false;
1203
1257
  }
1204
1258
 
1205
- let has_branch = false;
1259
+ /* `found_branch` and not `has_branch`: that name belongs to the
1260
+ * helper above, and a local would shadow it here. */
1261
+ let found_branch = false;
1206
1262
  let entries = is_dict ? Object.entries(value) : value.map((v, i) => [i, v]);
1207
1263
  for(let [key, child] of entries) {
1208
- if((is_object(child) && json_object_size(child) > 0) ||
1209
- (is_array(child) && child.length > 0)) {
1210
- has_branch = true;
1264
+ if(has_branch(child)) {
1265
+ found_branch = true;
1211
1266
  collect_foldable_paths(child, add_segment(path, key), out, false);
1212
1267
  }
1213
1268
  }
1214
1269
 
1215
- if(has_branch && !is_root) {
1270
+ if(found_branch && !is_root) {
1216
1271
  out.push(path);
1217
1272
  }
1218
- return has_branch;
1273
+ return found_branch;
1219
1274
  }
1220
1275
 
1221
1276
  /************************************************************
@@ -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
+ });