@yuneta/gobj-ui 7.23.20 → 7.23.22

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.
@@ -9474,6 +9474,23 @@ function getStrokeColor(fillColor, theme = "light", factor = .2) {
9474
9474
  const adjust = (v) => lighten ? Math.min(255, Math.floor(v + (255 - v) * factor)) : Math.max(0, Math.floor(v * (1 - factor)));
9475
9475
  return `rgba(${adjust(r)}, ${adjust(g)}, ${adjust(b)}, ${a.toFixed(3)})`;
9476
9476
  }
9477
+ /************************************************************
9478
+ * Returns the proportional position (between 0 and 1) of a specific point,
9479
+ * centered and spaced with margins.
9480
+ *
9481
+ * index - Index of the point (0 to count-1)
9482
+ * count - Total number of points
9483
+ * margin - Total margin space (default 0.2 means 10% on each end)
9484
+ ************************************************************/
9485
+ function getPointPosition(count, index, margin = .2) {
9486
+ if (count <= 0 || index < 0 || index >= count) {
9487
+ (0, _yuneta_gobj_js.log_error)("Invalid count or index");
9488
+ return .5;
9489
+ }
9490
+ const start = margin / 2;
9491
+ const step = (1 - margin / 2 - start) / count;
9492
+ return start + index * step + step / 2;
9493
+ }
9477
9494
  //#endregion
9478
9495
  //#region src/yui_graph_camera.js
9479
9496
  /***********************************************************************
@@ -10942,6 +10959,16 @@ function build_graph$3(gobj) {
10942
10959
  }
10943
10960
  }
10944
10961
  /************************************************************
10962
+ * The port a container key opens on its card.
10963
+ *
10964
+ * Prefixed so it can never collide with anything G6 puts in
10965
+ * the same namespace, and stringified because an array index
10966
+ * is a number and a port key is a string.
10967
+ ************************************************************/
10968
+ function port_key(key) {
10969
+ return "p_" + String(key);
10970
+ }
10971
+ /************************************************************
10945
10972
  * Build cell value HTML (matching old mx_json_viewer colors)
10946
10973
  ************************************************************/
10947
10974
  function build_cell_html(key, value, type, dark, matched, fold) {
@@ -11004,6 +11031,8 @@ function build_cell_html(key, value, type, dark, matched, fold) {
11004
11031
  * forever. `+3` and `−` need no language.
11005
11032
  ************************************************************/
11006
11033
  var FOLD_SPACER = `<span style="width:24px; flex:0 0 auto;"></span>`;
11034
+ var CARD_HEADER_H$1 = 24;
11035
+ var CARD_ROW_H$1 = 22;
11007
11036
  function fold_toggle_html(path, foldable, folded, branches, colors) {
11008
11037
  if (!foldable) return FOLD_SPACER;
11009
11038
  let label = folded ? "+" + branches : "&#8722;";
@@ -11084,7 +11113,7 @@ function get_group_label(path) {
11084
11113
  /************************************************************
11085
11114
  * Recursively build nodes and edges from JSON
11086
11115
  ************************************************************/
11087
- function build_json_nodes(gobj, path, kw, nodes, edges, parent_id) {
11116
+ function build_json_nodes(gobj, path, kw, nodes, edges, parent_id, parent_port) {
11088
11117
  let group_id = gen_node_id$1(gobj);
11089
11118
  let is_dict = (0, _yuneta_gobj_js.is_object)(kw);
11090
11119
  let is_list = (0, _yuneta_gobj_js.is_array)(kw);
@@ -11108,7 +11137,9 @@ function build_json_nodes(gobj, path, kw, nodes, edges, parent_id) {
11108
11137
  if (has_branch(value)) {
11109
11138
  pending_complex.push({
11110
11139
  key,
11111
- value
11140
+ value,
11141
+ port: port_key(key),
11142
+ row: lines.length
11112
11143
  });
11113
11144
  if (is_pure_collection(value)) {
11114
11145
  let child_path = add_segment(path, key);
@@ -11124,7 +11155,7 @@ function build_json_nodes(gobj, path, kw, nodes, edges, parent_id) {
11124
11155
  }
11125
11156
  if (parent_id && is_pure_collection(kw)) {
11126
11157
  if (priv.collapsed.has(path)) return parent_id;
11127
- for (let pending of pending_complex) build_json_nodes(gobj, add_segment(path, pending.key), pending.value, nodes, edges, parent_id);
11158
+ for (let pending of pending_complex) build_json_nodes(gobj, add_segment(path, pending.key), pending.value, nodes, edges, parent_id, parent_port);
11128
11159
  return parent_id;
11129
11160
  }
11130
11161
  let label = get_group_label(path) || (is_dict ? "{}" : "[]");
@@ -11134,6 +11165,7 @@ function build_json_nodes(gobj, path, kw, nodes, edges, parent_id) {
11134
11165
  let border_width = card_matched ? 2 : 1;
11135
11166
  let content_html = lines.map((line) => `<div style="font-family:monospace; font-size:13px; padding:1px 6px; white-space:nowrap;">${line}</div>`).join("");
11136
11167
  let min_width = Math.max(180, label.length * 9);
11168
+ let card_height = CARD_HEADER_H$1 + lines.length * CARD_ROW_H$1;
11137
11169
  let node_html = `
11138
11170
  <div style="
11139
11171
  min-width: ${min_width}px;
@@ -11155,6 +11187,17 @@ function build_json_nodes(gobj, path, kw, nodes, edges, parent_id) {
11155
11187
  ">${FOLD_SPACER}<span style="flex:1 1 auto; text-align:center;">${(0, _yuneta_gobj_js.escapeHtml)(label)}</span>${fold_toggle_html(path, foldable, folded, pending_complex.length, colors)}</div>
11156
11188
  ${content_html}
11157
11189
  </div>`;
11190
+ let ports = [];
11191
+ for (let pending of pending_complex) {
11192
+ let child_colors = json_card_style((0, _yuneta_gobj_js.is_object)(pending.value) ? GROUP_COLORS.dict : GROUP_COLORS.list, dark);
11193
+ let row_mid = CARD_HEADER_H$1 + pending.row * CARD_ROW_H$1 + CARD_ROW_H$1 / 2;
11194
+ ports.push({
11195
+ key: pending.port,
11196
+ placement: [1, row_mid / card_height],
11197
+ fill: child_colors.border,
11198
+ stroke: colors.border
11199
+ });
11200
+ }
11158
11201
  let node = {
11159
11202
  id: group_id,
11160
11203
  type: "html",
@@ -11164,22 +11207,30 @@ function build_json_nodes(gobj, path, kw, nodes, edges, parent_id) {
11164
11207
  },
11165
11208
  style: {
11166
11209
  innerHTML: node_html,
11167
- size: [min_width, 24 + lines.length * 22],
11210
+ size: [min_width, card_height],
11168
11211
  dx: -(min_width / 2),
11169
- dy: -((24 + lines.length * 22) / 2)
11212
+ dy: -(card_height / 2),
11213
+ port: ports.length > 0,
11214
+ ports,
11215
+ portR: 4,
11216
+ portLineWidth: 1
11170
11217
  }
11171
11218
  };
11172
11219
  nodes.push(node);
11173
- if (parent_id) edges.push({
11174
- source: parent_id,
11175
- target: group_id,
11176
- style: {
11177
- stroke: colors.border,
11178
- lineWidth: 1
11179
- }
11180
- });
11220
+ if (parent_id) {
11221
+ let edge = {
11222
+ source: parent_id,
11223
+ target: group_id,
11224
+ style: {
11225
+ stroke: colors.border,
11226
+ lineWidth: 1
11227
+ }
11228
+ };
11229
+ if (parent_port) edge.style.sourcePort = parent_port;
11230
+ edges.push(edge);
11231
+ }
11181
11232
  if (folded) return group_id;
11182
- for (let pending of pending_complex) build_json_nodes(gobj, add_segment(path, pending.key), pending.value, nodes, edges, group_id);
11233
+ for (let pending of pending_complex) build_json_nodes(gobj, add_segment(path, pending.key), pending.value, nodes, edges, group_id, pending.port);
11183
11234
  return group_id;
11184
11235
  }
11185
11236
  /************************************************************
@@ -49032,43 +49083,6 @@ function configure_behaviour(gobj) {
49032
49083
  graph_write_behaviors(gobj, behaviors);
49033
49084
  }
49034
49085
  /************************************************************
49035
- * Returns the proportional position (between 0 and 1) of a specific point,
49036
- * centered and spaced with margins.
49037
- *
49038
- * index - Index of the point (0 to count-1)
49039
- * count - Total number of points
49040
- * margin - Total margin space (default 0.2 means 10% on each end)
49041
- ************************************************************/
49042
- function getPointPosition(count, index, margin = .2) {
49043
- if (count <= 0 || index < 0 || index >= count) {
49044
- (0, _yuneta_gobj_js.log_error)("Invalid count or index");
49045
- return .5;
49046
- }
49047
- const start = margin / 2;
49048
- const step = (1 - margin / 2 - start) / count;
49049
- return start + index * step + step / 2;
49050
- }
49051
- /************************************************************
49052
- * Count hooks and fkeys in topic desc, classify node type
49053
- ************************************************************/
49054
- function calculate_hooks_fkeys_counter(desc) {
49055
- let cols = desc.cols;
49056
- desc.hooks_counter = 0;
49057
- desc.fkeys_counter = 0;
49058
- for (let i = 0; i < cols.length; i++) {
49059
- let col = cols[i];
49060
- switch ((0, _yuneta_gobj_js.treedb_get_field_desc)(col).type) {
49061
- case "hook":
49062
- desc.hooks_counter++;
49063
- break;
49064
- case "fkey": desc.fkeys_counter++;
49065
- }
49066
- }
49067
- if (desc.hooks_counter === 0) desc.node_treedb_type = "child";
49068
- else if (desc.fkeys_counter === 0) desc.node_treedb_type = "extended";
49069
- else desc.node_treedb_type = "hierarchical";
49070
- }
49071
- /************************************************************
49072
49086
  * Build ports for a topic desc
49073
49087
  ************************************************************/
49074
49088
  function build_ports(gobj, desc) {
@@ -9346,6 +9346,14 @@ function yui_toolbar(attrs = {}, items = []) {
9346
9346
  }
9347
9347
  //#endregion
9348
9348
  //#region src/lib_graph.js
9349
+ /***********************************************************************
9350
+ * lib_graph.js
9351
+ *
9352
+ * Utilities for graph, using Bulma
9353
+ *
9354
+ * Copyright (c) 2025, ArtGins.
9355
+ * All Rights Reserved.
9356
+ ***********************************************************************/
9349
9357
  /************************************************************
9350
9358
  * Add class to elements selected
9351
9359
  ************************************************************/
@@ -9461,6 +9469,23 @@ function getStrokeColor(fillColor, theme = "light", factor = .2) {
9461
9469
  const adjust = (v) => lighten ? Math.min(255, Math.floor(v + (255 - v) * factor)) : Math.max(0, Math.floor(v * (1 - factor)));
9462
9470
  return `rgba(${adjust(r)}, ${adjust(g)}, ${adjust(b)}, ${a.toFixed(3)})`;
9463
9471
  }
9472
+ /************************************************************
9473
+ * Returns the proportional position (between 0 and 1) of a specific point,
9474
+ * centered and spaced with margins.
9475
+ *
9476
+ * index - Index of the point (0 to count-1)
9477
+ * count - Total number of points
9478
+ * margin - Total margin space (default 0.2 means 10% on each end)
9479
+ ************************************************************/
9480
+ function getPointPosition(count, index, margin = .2) {
9481
+ if (count <= 0 || index < 0 || index >= count) {
9482
+ log_error("Invalid count or index");
9483
+ return .5;
9484
+ }
9485
+ const start = margin / 2;
9486
+ const step = (1 - margin / 2 - start) / count;
9487
+ return start + index * step + step / 2;
9488
+ }
9464
9489
  //#endregion
9465
9490
  //#region src/yui_graph_camera.js
9466
9491
  /***********************************************************************
@@ -10929,6 +10954,16 @@ function build_graph$3(gobj) {
10929
10954
  }
10930
10955
  }
10931
10956
  /************************************************************
10957
+ * The port a container key opens on its card.
10958
+ *
10959
+ * Prefixed so it can never collide with anything G6 puts in
10960
+ * the same namespace, and stringified because an array index
10961
+ * is a number and a port key is a string.
10962
+ ************************************************************/
10963
+ function port_key(key) {
10964
+ return "p_" + String(key);
10965
+ }
10966
+ /************************************************************
10932
10967
  * Build cell value HTML (matching old mx_json_viewer colors)
10933
10968
  ************************************************************/
10934
10969
  function build_cell_html(key, value, type, dark, matched, fold) {
@@ -10991,6 +11026,8 @@ function build_cell_html(key, value, type, dark, matched, fold) {
10991
11026
  * forever. `+3` and `−` need no language.
10992
11027
  ************************************************************/
10993
11028
  var FOLD_SPACER = `<span style="width:24px; flex:0 0 auto;"></span>`;
11029
+ var CARD_HEADER_H$1 = 24;
11030
+ var CARD_ROW_H$1 = 22;
10994
11031
  function fold_toggle_html(path, foldable, folded, branches, colors) {
10995
11032
  if (!foldable) return FOLD_SPACER;
10996
11033
  let label = folded ? "+" + branches : "&#8722;";
@@ -11071,7 +11108,7 @@ function get_group_label(path) {
11071
11108
  /************************************************************
11072
11109
  * Recursively build nodes and edges from JSON
11073
11110
  ************************************************************/
11074
- function build_json_nodes(gobj, path, kw, nodes, edges, parent_id) {
11111
+ function build_json_nodes(gobj, path, kw, nodes, edges, parent_id, parent_port) {
11075
11112
  let group_id = gen_node_id$1(gobj);
11076
11113
  let is_dict = is_object(kw);
11077
11114
  let is_list = is_array(kw);
@@ -11095,7 +11132,9 @@ function build_json_nodes(gobj, path, kw, nodes, edges, parent_id) {
11095
11132
  if (has_branch(value)) {
11096
11133
  pending_complex.push({
11097
11134
  key,
11098
- value
11135
+ value,
11136
+ port: port_key(key),
11137
+ row: lines.length
11099
11138
  });
11100
11139
  if (is_pure_collection(value)) {
11101
11140
  let child_path = add_segment(path, key);
@@ -11111,7 +11150,7 @@ function build_json_nodes(gobj, path, kw, nodes, edges, parent_id) {
11111
11150
  }
11112
11151
  if (parent_id && is_pure_collection(kw)) {
11113
11152
  if (priv.collapsed.has(path)) return parent_id;
11114
- for (let pending of pending_complex) build_json_nodes(gobj, add_segment(path, pending.key), pending.value, nodes, edges, parent_id);
11153
+ for (let pending of pending_complex) build_json_nodes(gobj, add_segment(path, pending.key), pending.value, nodes, edges, parent_id, parent_port);
11115
11154
  return parent_id;
11116
11155
  }
11117
11156
  let label = get_group_label(path) || (is_dict ? "{}" : "[]");
@@ -11121,6 +11160,7 @@ function build_json_nodes(gobj, path, kw, nodes, edges, parent_id) {
11121
11160
  let border_width = card_matched ? 2 : 1;
11122
11161
  let content_html = lines.map((line) => `<div style="font-family:monospace; font-size:13px; padding:1px 6px; white-space:nowrap;">${line}</div>`).join("");
11123
11162
  let min_width = Math.max(180, label.length * 9);
11163
+ let card_height = CARD_HEADER_H$1 + lines.length * CARD_ROW_H$1;
11124
11164
  let node_html = `
11125
11165
  <div style="
11126
11166
  min-width: ${min_width}px;
@@ -11142,6 +11182,17 @@ function build_json_nodes(gobj, path, kw, nodes, edges, parent_id) {
11142
11182
  ">${FOLD_SPACER}<span style="flex:1 1 auto; text-align:center;">${escapeHtml(label)}</span>${fold_toggle_html(path, foldable, folded, pending_complex.length, colors)}</div>
11143
11183
  ${content_html}
11144
11184
  </div>`;
11185
+ let ports = [];
11186
+ for (let pending of pending_complex) {
11187
+ let child_colors = json_card_style(is_object(pending.value) ? GROUP_COLORS.dict : GROUP_COLORS.list, dark);
11188
+ let row_mid = CARD_HEADER_H$1 + pending.row * CARD_ROW_H$1 + CARD_ROW_H$1 / 2;
11189
+ ports.push({
11190
+ key: pending.port,
11191
+ placement: [1, row_mid / card_height],
11192
+ fill: child_colors.border,
11193
+ stroke: colors.border
11194
+ });
11195
+ }
11145
11196
  let node = {
11146
11197
  id: group_id,
11147
11198
  type: "html",
@@ -11151,22 +11202,30 @@ function build_json_nodes(gobj, path, kw, nodes, edges, parent_id) {
11151
11202
  },
11152
11203
  style: {
11153
11204
  innerHTML: node_html,
11154
- size: [min_width, 24 + lines.length * 22],
11205
+ size: [min_width, card_height],
11155
11206
  dx: -(min_width / 2),
11156
- dy: -((24 + lines.length * 22) / 2)
11207
+ dy: -(card_height / 2),
11208
+ port: ports.length > 0,
11209
+ ports,
11210
+ portR: 4,
11211
+ portLineWidth: 1
11157
11212
  }
11158
11213
  };
11159
11214
  nodes.push(node);
11160
- if (parent_id) edges.push({
11161
- source: parent_id,
11162
- target: group_id,
11163
- style: {
11164
- stroke: colors.border,
11165
- lineWidth: 1
11166
- }
11167
- });
11215
+ if (parent_id) {
11216
+ let edge = {
11217
+ source: parent_id,
11218
+ target: group_id,
11219
+ style: {
11220
+ stroke: colors.border,
11221
+ lineWidth: 1
11222
+ }
11223
+ };
11224
+ if (parent_port) edge.style.sourcePort = parent_port;
11225
+ edges.push(edge);
11226
+ }
11168
11227
  if (folded) return group_id;
11169
- for (let pending of pending_complex) build_json_nodes(gobj, add_segment(path, pending.key), pending.value, nodes, edges, group_id);
11228
+ for (let pending of pending_complex) build_json_nodes(gobj, add_segment(path, pending.key), pending.value, nodes, edges, group_id, pending.port);
11170
11229
  return group_id;
11171
11230
  }
11172
11231
  /************************************************************
@@ -49019,43 +49078,6 @@ function configure_behaviour(gobj) {
49019
49078
  graph_write_behaviors(gobj, behaviors);
49020
49079
  }
49021
49080
  /************************************************************
49022
- * Returns the proportional position (between 0 and 1) of a specific point,
49023
- * centered and spaced with margins.
49024
- *
49025
- * index - Index of the point (0 to count-1)
49026
- * count - Total number of points
49027
- * margin - Total margin space (default 0.2 means 10% on each end)
49028
- ************************************************************/
49029
- function getPointPosition(count, index, margin = .2) {
49030
- if (count <= 0 || index < 0 || index >= count) {
49031
- log_error("Invalid count or index");
49032
- return .5;
49033
- }
49034
- const start = margin / 2;
49035
- const step = (1 - margin / 2 - start) / count;
49036
- return start + index * step + step / 2;
49037
- }
49038
- /************************************************************
49039
- * Count hooks and fkeys in topic desc, classify node type
49040
- ************************************************************/
49041
- function calculate_hooks_fkeys_counter(desc) {
49042
- let cols = desc.cols;
49043
- desc.hooks_counter = 0;
49044
- desc.fkeys_counter = 0;
49045
- for (let i = 0; i < cols.length; i++) {
49046
- let col = cols[i];
49047
- switch (treedb_get_field_desc(col).type) {
49048
- case "hook":
49049
- desc.hooks_counter++;
49050
- break;
49051
- case "fkey": desc.fkeys_counter++;
49052
- }
49053
- }
49054
- if (desc.hooks_counter === 0) desc.node_treedb_type = "child";
49055
- else if (desc.fkeys_counter === 0) desc.node_treedb_type = "extended";
49056
- else desc.node_treedb_type = "hierarchical";
49057
- }
49058
- /************************************************************
49059
49081
  * Build ports for a topic desc
49060
49082
  ************************************************************/
49061
49083
  function build_ports(gobj, desc) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yuneta/gobj-ui",
3
- "version": "7.23.20",
3
+ "version": "7.23.22",
4
4
  "type": "module",
5
5
  "main": "dist/gobj-ui.cjs.js",
6
6
  "module": "dist/gobj-ui.es.js",
@@ -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
  ************************************************************/
@@ -772,6 +772,18 @@ function build_graph(gobj)
772
772
  }
773
773
  }
774
774
 
775
+ /************************************************************
776
+ * The port a container key opens on its card.
777
+ *
778
+ * Prefixed so it can never collide with anything G6 puts in
779
+ * the same namespace, and stringified because an array index
780
+ * is a number and a port key is a string.
781
+ ************************************************************/
782
+ function port_key(key)
783
+ {
784
+ return "p_" + String(key);
785
+ }
786
+
775
787
  /************************************************************
776
788
  * Build cell value HTML (matching old mx_json_viewer colors)
777
789
  ************************************************************/
@@ -872,6 +884,20 @@ function build_cell_html(key, value, type, dark, matched, fold)
872
884
  ************************************************************/
873
885
  const FOLD_SPACER = `<span style="width:24px; flex:0 0 auto;"></span>`;
874
886
 
887
+ /*
888
+ * The card's geometry, in ONE place.
889
+ *
890
+ * A container key's port is placed by computing WHICH ROW it is, so a
891
+ * row height changed in the markup and not here would slide every port
892
+ * off the line it belongs to -- silently, because nothing would fail:
893
+ * the dots would just stop pointing at anything.
894
+ *
895
+ * These are the rendered heights of the header (`padding: 3px 8px` on
896
+ * 12px bold) and of one body row (`padding: 1px 6px` on 13px monospace).
897
+ */
898
+ const CARD_HEADER_H = 24;
899
+ const CARD_ROW_H = 22;
900
+
875
901
  function fold_toggle_html(path, foldable, folded, branches, colors)
876
902
  {
877
903
  if(!foldable) {
@@ -981,7 +1007,7 @@ function get_group_label(path)
981
1007
  /************************************************************
982
1008
  * Recursively build nodes and edges from JSON
983
1009
  ************************************************************/
984
- function build_json_nodes(gobj, path, kw, nodes, edges, parent_id)
1010
+ function build_json_nodes(gobj, path, kw, nodes, edges, parent_id, parent_port)
985
1011
  {
986
1012
  let group_id = gen_node_id(gobj);
987
1013
  let is_dict = is_object(kw);
@@ -1031,7 +1057,11 @@ function build_json_nodes(gobj, path, kw, nodes, edges, parent_id)
1031
1057
  */
1032
1058
  let fold = null;
1033
1059
  if(has_branch(value)) {
1034
- pending_complex.push({key: key, value: value});
1060
+ /* `lines.length` is the index the row of this entry is about
1061
+ * to take, and it is what puts the port on that line. */
1062
+ pending_complex.push({
1063
+ key: key, value: value, port: port_key(key), row: lines.length
1064
+ });
1035
1065
 
1036
1066
  /*
1037
1067
  * A child that gets no card of its own is folded from HERE
@@ -1062,13 +1092,21 @@ function build_json_nodes(gobj, path, kw, nodes, edges, parent_id)
1062
1092
  return parent_id;
1063
1093
  }
1064
1094
  for(let pending of pending_complex) {
1095
+ /*
1096
+ * `parent_port` and not one of ours: this collection has no
1097
+ * card, so the port its row opened in the PARENT is the one
1098
+ * every one of these lines leaves from -- fourteen columns
1099
+ * out of the single `cols` port, which is what the document
1100
+ * says and what the reader traces back.
1101
+ */
1065
1102
  build_json_nodes(
1066
1103
  gobj,
1067
1104
  add_segment(path, pending.key),
1068
1105
  pending.value,
1069
1106
  nodes,
1070
1107
  edges,
1071
- parent_id
1108
+ parent_id,
1109
+ parent_port
1072
1110
  );
1073
1111
  }
1074
1112
  return parent_id;
@@ -1097,6 +1135,7 @@ function build_json_nodes(gobj, path, kw, nodes, edges, parent_id)
1097
1135
  ).join("");
1098
1136
 
1099
1137
  let min_width = Math.max(180, label.length * 9);
1138
+ let card_height = CARD_HEADER_H + lines.length * CARD_ROW_H;
1100
1139
 
1101
1140
  let node_html = `
1102
1141
  <div style="
@@ -1120,6 +1159,41 @@ function build_json_nodes(gobj, path, kw, nodes, edges, parent_id)
1120
1159
  ${content_html}
1121
1160
  </div>`;
1122
1161
 
1162
+ /*
1163
+ * ONE PORT PER CONTAINER KEY, ON THE LINE OF ITS OWN ROW.
1164
+ *
1165
+ * Without ports each card was a single anchor: fourteen lines came
1166
+ * out of one point, and which row a line belonged to was a guess
1167
+ * the reader made from where it landed.
1168
+ *
1169
+ * Spread along the bottom edge they were distinguishable but still
1170
+ * not ATTACHED to anything -- the reader had to count dots, count
1171
+ * rows, and trust that the two orders matched. Placed at the row's
1172
+ * own height there is nothing to match: the line leaves from beside
1173
+ * the key it belongs to, which is the whole point of a port.
1174
+ *
1175
+ * On the RIGHT edge, and exactly ON it (`x = 1`) rather than inside:
1176
+ * an html node draws its HTML in a DOM layer above the canvas, so a
1177
+ * port fully inside the box would be painted under the card and
1178
+ * never seen. Half of it sticks out.
1179
+ *
1180
+ * Coloured by what the row LEADS TO -- a dict or a list -- which is
1181
+ * the one thing the port can say that the row does not.
1182
+ */
1183
+ let ports = [];
1184
+ for(let pending of pending_complex) {
1185
+ let child_colors = json_card_style(
1186
+ is_object(pending.value)? GROUP_COLORS.dict: GROUP_COLORS.list, dark
1187
+ );
1188
+ let row_mid = CARD_HEADER_H + pending.row * CARD_ROW_H + CARD_ROW_H / 2;
1189
+ ports.push({
1190
+ key: pending.port,
1191
+ placement: [1, row_mid / card_height],
1192
+ fill: child_colors.border,
1193
+ stroke: colors.border,
1194
+ });
1195
+ }
1196
+
1123
1197
  let node = {
1124
1198
  id: group_id,
1125
1199
  type: 'html',
@@ -1129,25 +1203,33 @@ function build_json_nodes(gobj, path, kw, nodes, edges, parent_id)
1129
1203
  },
1130
1204
  style: {
1131
1205
  innerHTML: node_html,
1132
- size: [min_width, 24 + lines.length * 22],
1206
+ size: [min_width, card_height],
1133
1207
  dx: -(min_width / 2),
1134
- dy: -((24 + lines.length * 22) / 2),
1208
+ dy: -(card_height / 2),
1209
+ port: ports.length > 0,
1210
+ ports: ports,
1211
+ portR: 4,
1212
+ portLineWidth: 1,
1135
1213
  },
1136
1214
  };
1137
1215
  nodes.push(node);
1138
1216
 
1139
1217
  /*
1140
- * Edge from parent to this group
1218
+ * Edge from parent to this group, out of the port its key opened.
1141
1219
  */
1142
1220
  if(parent_id) {
1143
- edges.push({
1221
+ let edge = {
1144
1222
  source: parent_id,
1145
1223
  target: group_id,
1146
1224
  style: {
1147
1225
  stroke: colors.border,
1148
1226
  lineWidth: 1,
1149
1227
  }
1150
- });
1228
+ };
1229
+ if(parent_port) {
1230
+ edge.style.sourcePort = parent_port;
1231
+ }
1232
+ edges.push(edge);
1151
1233
  }
1152
1234
 
1153
1235
  /*
@@ -1163,7 +1245,8 @@ function build_json_nodes(gobj, path, kw, nodes, edges, parent_id)
1163
1245
  pending.value,
1164
1246
  nodes,
1165
1247
  edges,
1166
- group_id
1248
+ group_id,
1249
+ pending.port
1167
1250
  );
1168
1251
  }
1169
1252
 
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
  };