@yuneta/gobj-ui 7.23.79 → 7.23.81

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.
@@ -137,6 +137,7 @@ import {
137
137
  HistoryEvent,
138
138
  EdgeEvent,
139
139
  Circle,
140
+ HTML,
140
141
  Toolbar,
141
142
  register,
142
143
  } from '@antv/g6';
@@ -446,6 +447,8 @@ let PRIVATE_DATA = {
446
447
  _edge_icon_el: null, // floating properties icon element
447
448
  _edge_delete_el: null, // floating delete icon element for edge
448
449
  _edge_popover_el: null, // edge properties popover element
450
+ _port_icon_el: null, // floating port properties icon element
451
+ _port_popover_el: null, // port properties popover element
449
452
  _node_icon_el: null, // floating node properties icon element
450
453
  _node_delete_el: null, // floating delete icon element for node
451
454
  _node_popover_el: null, // node properties popover element
@@ -839,6 +842,7 @@ function register_layouts(gobj)
839
842
  register(ExtensionCategory.LAYOUT, 'treedb-tree', TreedbTreeLayout);
840
843
  register(ExtensionCategory.LAYOUT, 'treedb-radial', TreedbRadialLayout);
841
844
  register(ExtensionCategory.NODE, 'light', LightNode);
845
+ register(ExtensionCategory.NODE, 'treedb-card', TreedbCard);
842
846
  }
843
847
  }
844
848
 
@@ -940,6 +944,7 @@ function configure_events(gobj)
940
944
  update_edge_icon_position(gobj);
941
945
  update_node_icon_position(gobj);
942
946
  update_link_icon_position(gobj);
947
+ update_port_icon_position(gobj);
943
948
  });
944
949
 
945
950
  graph.on(NodeEvent.DRAG_END, (evt) => {
@@ -974,6 +979,7 @@ function configure_events(gobj)
974
979
  update_edge_icon_position(gobj);
975
980
  update_node_icon_position(gobj);
976
981
  update_link_icon_position(gobj);
982
+ update_port_icon_position(gobj);
977
983
  update_zoom_readout(gobj);
978
984
 
979
985
  /*
@@ -2132,12 +2138,32 @@ function card_shape_of(gobj, desc, record, geometry, pills_html)
2132
2138
  }
2133
2139
  }
2134
2140
  }
2141
+
2142
+ /* And the SHAPE of each port, the same way: the node's own,
2143
+ * then the topic's per-port default, then the topic's one
2144
+ * shape for every port (`all ports` in the port popover). */
2145
+ let port_shapes = geometry.port_shapes;
2146
+ let default_port_shapes = topic_defaults ? topic_defaults.port_shapes : null;
2147
+ let default_shape = topic_defaults ? topic_defaults.port_shape : null;
2148
+ for(let i = 0; i < style.ports.length; i++) {
2149
+ let key = style.ports[i].key;
2150
+ let shape = is_object(port_shapes) ? port_shapes[key] : null;
2151
+ if(!shape && is_object(default_port_shapes)) {
2152
+ shape = default_port_shapes[key];
2153
+ }
2154
+ if(!shape) {
2155
+ shape = default_shape;
2156
+ }
2157
+ if(shape && str_in_list(PORT_SHAPES, shape)) {
2158
+ style.ports[i].shape = shape;
2159
+ }
2160
+ }
2135
2161
  } else {
2136
2162
  style.port = false;
2137
2163
  style.ports = [];
2138
2164
  }
2139
2165
 
2140
- return {type: 'html', style: style};
2166
+ return {type: 'treedb-card', style: style};
2141
2167
  }
2142
2168
 
2143
2169
  /************************************************************
@@ -2732,26 +2758,62 @@ function update_geometry(gobj, node_id)
2732
2758
  topic_props.nodes = {};
2733
2759
  }
2734
2760
 
2735
- // Extract geometry from render style
2761
+ /* Extract geometry from render style -- the POSITION always, the
2762
+ * SIZE only when somebody chose it. A card on the tier's default
2763
+ * used to be saved with that default, so every Save froze the
2764
+ * library's size of the day into `__graphs__` and a later default
2765
+ * (bigger ports, 7.23.75/79) reached no saved treedb: the same
2766
+ * trap as the invented cascade coordinates, for the size. And a
2767
+ * CLOSED node is a square: its render size is not the card's and
2768
+ * is never saved as one -- the entry keeps what it had. */
2736
2769
  let node_props = topic_props.nodes[record.id] || {};
2737
- json_object_update(
2738
- node_props,
2739
- kw_clone_by_keys(gobj, style, ["x", "y", "size", "portR"])
2740
- );
2770
+ let picked = kw_clone_by_keys(gobj, style, ["x", "y", "size", "portR"]);
2771
+ if(node_is_compact(gobj, node_id)) {
2772
+ delete picked.size;
2773
+ delete picked.portR;
2774
+ } else {
2775
+ let desc = nodedata.data.desc;
2776
+ let def_size = card_size_for(desc.node_treedb_type, !!pills_html_of(gobj, nodedata));
2777
+ if(is_array(picked.size) && picked.size[0] === def_size[0] &&
2778
+ picked.size[1] === def_size[1]) {
2779
+ delete picked.size;
2780
+ delete node_props.size;
2781
+ }
2782
+ let def_r = (desc.node_treedb_type === 'child')? PORT_R_CHILD : PORT_R_ENTITY;
2783
+ if(picked.portR === def_r) {
2784
+ delete picked.portR;
2785
+ delete node_props.portR;
2786
+ }
2787
+ }
2788
+ json_object_update(node_props, picked);
2741
2789
 
2742
2790
  // Save per-port radius values (only ports with custom r)
2743
- let node_style = nodedata.style || {};
2744
- let ports = node_style.ports || [];
2745
- let port_sizes = {};
2746
- for(let i = 0; i < ports.length; i++) {
2747
- if(ports[i].r != null) {
2748
- port_sizes[ports[i].key] = ports[i].r;
2791
+ if(!node_is_compact(gobj, node_id)) {
2792
+ let node_style = nodedata.style || {};
2793
+ let ports = node_style.ports || [];
2794
+ let port_sizes = {};
2795
+ for(let i = 0; i < ports.length; i++) {
2796
+ if(ports[i].r != null) {
2797
+ port_sizes[ports[i].key] = ports[i].r;
2798
+ }
2799
+ }
2800
+ if(Object.keys(port_sizes).length > 0) {
2801
+ node_props.port_sizes = port_sizes;
2802
+ } else {
2803
+ delete node_props.port_sizes;
2804
+ }
2805
+ // And the shape of each port that is not a circle
2806
+ let port_shapes = {};
2807
+ for(let i = 0; i < ports.length; i++) {
2808
+ if(ports[i].shape && ports[i].shape !== 'circle') {
2809
+ port_shapes[ports[i].key] = ports[i].shape;
2810
+ }
2811
+ }
2812
+ if(Object.keys(port_shapes).length > 0) {
2813
+ node_props.port_shapes = port_shapes;
2814
+ } else {
2815
+ delete node_props.port_shapes;
2749
2816
  }
2750
- }
2751
- if(Object.keys(port_sizes).length > 0) {
2752
- node_props.port_sizes = port_sizes;
2753
- } else {
2754
- delete node_props.port_sizes;
2755
2817
  }
2756
2818
 
2757
2819
  topic_props.nodes[record.id] = node_props;
@@ -3461,6 +3523,7 @@ function graph_resize(gobj, width, height)
3461
3523
  update_port_resize_handles_position(gobj);
3462
3524
  update_node_icon_position(gobj);
3463
3525
  update_link_icon_position(gobj);
3526
+ update_port_icon_position(gobj);
3464
3527
  }
3465
3528
 
3466
3529
  function graph_write_behaviors(gobj, behaviors)
@@ -3496,6 +3559,64 @@ function graph_set_behavior(gobj, behavior, set)
3496
3559
  });
3497
3560
  }
3498
3561
 
3562
+ /************************************************************
3563
+ * Custom G6 node: TreedbCard -- G6's html node with ports that
3564
+ * have a SHAPE.
3565
+ *
3566
+ * G6 draws every port as a circle (`drawPortShapes` upserts a
3567
+ * `Circle`, nothing in the port style says otherwise). A port's
3568
+ * `shape` -- `circle`, `square`, `diamond`, `triangle` -- picks
3569
+ * the G shape here, by the names G6 registers itself, and `r`
3570
+ * stays the one size: the half-side of the square, the half-
3571
+ * diagonal of the diamond, the half-height of the triangle. The
3572
+ * hit test, the resize handles and the edge's landing point all
3573
+ * read `r` and none of them care about the outline.
3574
+ ************************************************************/
3575
+ const PORT_SHAPES = ['circle', 'square', 'diamond', 'triangle'];
3576
+
3577
+ class TreedbCard extends HTML
3578
+ {
3579
+ drawPortShapes(attributes, container) {
3580
+ const portsStyle = this.getPortsStyle(attributes);
3581
+ Object.keys(portsStyle).forEach((key) => {
3582
+ const style = portsStyle[key];
3583
+ const shapeKey = `port-${key}`;
3584
+ if(style === false) {
3585
+ this.upsert(shapeKey, 'circle', false, container);
3586
+ return;
3587
+ }
3588
+ let r = Number(style.r) || 0;
3589
+ switch(style.shape) {
3590
+ case 'square':
3591
+ this.upsert(shapeKey, 'rect', Object.assign({}, style, {
3592
+ x: -r, y: -r, width: 2 * r, height: 2 * r,
3593
+ }), container);
3594
+ break;
3595
+ case 'diamond':
3596
+ this.upsert(shapeKey, 'polygon', Object.assign({}, style, {
3597
+ points: [[0, -r], [r, 0], [0, r], [-r, 0]],
3598
+ }), container);
3599
+ break;
3600
+ case 'triangle':
3601
+ this.upsert(shapeKey, 'polygon', Object.assign({}, style, {
3602
+ points: [[0, -r], [r, r], [-r, r]],
3603
+ }), container);
3604
+ break;
3605
+ default:
3606
+ this.upsert(shapeKey, 'circle', style, container);
3607
+ break;
3608
+ }
3609
+ });
3610
+ }
3611
+ }
3612
+
3613
+ /* A record's card is `treedb-card`; `html` is what it was until
3614
+ * 7.23.81 and what a `+N` chip still is. */
3615
+ function is_card_type(type)
3616
+ {
3617
+ return type === 'treedb-card' || type === 'html';
3618
+ }
3619
+
3499
3620
  /************************************************************
3500
3621
  * Custom G6 node: LightNode (circle with status indicator)
3501
3622
  ************************************************************/
@@ -4637,7 +4758,7 @@ function start_node_resize(gobj, e, mx, my)
4637
4758
  }
4638
4759
 
4639
4760
  // HTML nodes: update dx, dy to keep content centered
4640
- if(nodeType === 'html') {
4761
+ if(is_card_type(nodeType)) {
4641
4762
  updateStyle.dx = -newW / 2;
4642
4763
  updateStyle.dy = -newH / 2;
4643
4764
  }
@@ -4818,6 +4939,7 @@ function select_port(gobj, node_id, port_key)
4818
4939
 
4819
4940
  show_port_resize_handles(gobj);
4820
4941
  show_link_icon_if_fkey(gobj);
4942
+ show_port_icon(gobj);
4821
4943
  }
4822
4944
 
4823
4945
  /************************************************************
@@ -4829,6 +4951,8 @@ function deselect_port(gobj)
4829
4951
 
4830
4952
  exit_linking_mode(gobj);
4831
4953
  hide_link_icon(gobj);
4954
+ hide_port_icon(gobj);
4955
+ hide_port_popover(gobj);
4832
4956
  hide_port_resize_handles(gobj);
4833
4957
  priv._selected_port_key = null;
4834
4958
  }
@@ -4916,6 +5040,272 @@ function update_link_icon_position(gobj)
4916
5040
  }
4917
5041
  }
4918
5042
 
5043
+ /************************************************************
5044
+ * Port properties icon and popover: the gear beside a selected
5045
+ * port, as the node has its own. The popover has the two things
5046
+ * a port IS on screen -- its shape and its radius -- and the
5047
+ * scope: this port, the same port of every card of the topic
5048
+ * (the hook or fkey column), or every port there is.
5049
+ ************************************************************/
5050
+ function port_icon_xy(gobj)
5051
+ {
5052
+ let priv = gobj.priv;
5053
+ let graph = priv.graph;
5054
+ let canvasPos = get_port_canvas_position(
5055
+ gobj, priv._selected_node_id, priv._selected_port_key
5056
+ );
5057
+ if(!canvasPos) {
5058
+ return null;
5059
+ }
5060
+ let vp = graph.getViewportByCanvas([canvasPos.x, canvasPos.y]);
5061
+ /* Under the link icon when there is one (an fkey), in its
5062
+ * place otherwise. */
5063
+ let below = priv._link_icon_el? floating_icon_step() : 0;
5064
+ return {
5065
+ x: vp[0] + floating_icon_size() / 2 + 4,
5066
+ y: vp[1] + floating_icon_dy() + below,
5067
+ };
5068
+ }
5069
+
5070
+ function show_port_icon(gobj)
5071
+ {
5072
+ hide_port_icon(gobj);
5073
+
5074
+ let priv = gobj.priv;
5075
+ if(!priv._selected_node_id || !priv._selected_port_key) {
5076
+ return;
5077
+ }
5078
+ let xy = port_icon_xy(gobj);
5079
+ if(!xy) {
5080
+ return;
5081
+ }
5082
+ let icon = create_floating_icon(
5083
+ 'gear', '#1890ff', xy.x, xy.y,
5084
+ t('port properties'), () => toggle_port_popover(gobj)
5085
+ );
5086
+ priv.$container.appendChild(icon);
5087
+ priv._port_icon_el = icon;
5088
+ }
5089
+
5090
+ function hide_port_icon(gobj)
5091
+ {
5092
+ hide_overlay(gobj, '_port_icon_el');
5093
+ }
5094
+
5095
+ function update_port_icon_position(gobj)
5096
+ {
5097
+ let priv = gobj.priv;
5098
+ if(!priv._port_icon_el || !priv._selected_node_id || !priv._selected_port_key) {
5099
+ return;
5100
+ }
5101
+ try {
5102
+ let xy = port_icon_xy(gobj);
5103
+ if(!xy) {
5104
+ hide_port_icon(gobj);
5105
+ hide_port_popover(gobj);
5106
+ return;
5107
+ }
5108
+ priv._port_icon_el.style.left = xy.x + 'px';
5109
+ priv._port_icon_el.style.top = xy.y + 'px';
5110
+ if(priv._port_popover_el) {
5111
+ priv._port_popover_el.style.left = (xy.x + floating_popover_dx()) + 'px';
5112
+ priv._port_popover_el.style.top = xy.y + 'px';
5113
+ clamp_popover_position(gobj, priv._port_popover_el);
5114
+ }
5115
+ } catch(e) {
5116
+ hide_port_icon(gobj);
5117
+ hide_port_popover(gobj);
5118
+ }
5119
+ }
5120
+
5121
+ function toggle_port_popover(gobj)
5122
+ {
5123
+ let priv = gobj.priv;
5124
+ if(priv._port_popover_el) {
5125
+ hide_port_popover(gobj);
5126
+ } else {
5127
+ show_port_popover(gobj);
5128
+ }
5129
+ }
5130
+
5131
+ function hide_port_popover(gobj)
5132
+ {
5133
+ hide_overlay(gobj, '_port_popover_el');
5134
+ }
5135
+
5136
+ /* The ports of a node with ONE of them rewritten. */
5137
+ function ports_with(ports, port_key, changes)
5138
+ {
5139
+ return (ports || []).map((p) => (p.key === port_key)? Object.assign({}, p, changes) : p);
5140
+ }
5141
+
5142
+ function show_port_popover(gobj)
5143
+ {
5144
+ hide_port_popover(gobj);
5145
+
5146
+ let priv = gobj.priv;
5147
+ let graph = priv.graph;
5148
+ let node_id = priv._selected_node_id;
5149
+ let port_key = priv._selected_port_key;
5150
+ if(!node_id || !port_key) {
5151
+ return;
5152
+ }
5153
+ let nodeData = graph.getNodeData(node_id);
5154
+ if(!nodeData || !nodeData.style) {
5155
+ return;
5156
+ }
5157
+ let ports = nodeData.style.ports || [];
5158
+ let port = ports.find((p) => p.key === port_key);
5159
+ if(!port) {
5160
+ return;
5161
+ }
5162
+ let orig_shape = port.shape || 'circle';
5163
+ let orig_r = get_port_radius(gobj, node_id, port_key);
5164
+
5165
+ let xy = port_icon_xy(gobj);
5166
+ if(!xy) {
5167
+ return;
5168
+ }
5169
+ const popover = create_popover_base(
5170
+ xy.x + floating_popover_dx(), xy.y, 'g6-port-popover', '#d9d9d9', 180
5171
+ );
5172
+
5173
+ /* Live preview on THIS port; the others wait for apply. */
5174
+ function preview_port() {
5175
+ let r = parseInt(rInput.value) || orig_r;
5176
+ graph.updateNodeData([{id: node_id, style: {
5177
+ ports: ports_with(nodeData.style.ports, port_key, {shape: shapeSelect.value, r: r}),
5178
+ }}]);
5179
+ graph.draw().then(() => {
5180
+ update_port_resize_handles_position(gobj);
5181
+ });
5182
+ }
5183
+
5184
+ create_form_label(popover, 'shape');
5185
+ let shapeSelect = create_form_select(popover, PORT_SHAPES.map((shape) => {
5186
+ return {value: shape, label: t(shape)};
5187
+ }));
5188
+ shapeSelect.value = orig_shape;
5189
+ shapeSelect.addEventListener('change', preview_port);
5190
+
5191
+ create_form_label(popover, 'radius');
5192
+ let rInput = create_form_number_input(popover, orig_r, 2, 40, preview_port);
5193
+
5194
+ create_form_label(popover, 'apply to');
5195
+ let scopeSelect = create_form_select(popover, [
5196
+ {value: 'this', label: t('this port')},
5197
+ {value: 'same_topic', label: t('same port in topic')},
5198
+ {value: 'all', label: t('all ports')},
5199
+ ]);
5200
+
5201
+ create_form_button_row(popover, [
5202
+ {
5203
+ text: 'cancel',
5204
+ style: BTN_STYLE_CANCEL,
5205
+ onClick: () => {
5206
+ graph.updateNodeData([{id: node_id, style: {
5207
+ ports: ports_with(nodeData.style.ports, port_key, {shape: orig_shape, r: orig_r}),
5208
+ }}]);
5209
+ graph.draw().then(() => {
5210
+ update_port_resize_handles_position(gobj);
5211
+ });
5212
+ hide_port_popover(gobj);
5213
+ },
5214
+ },
5215
+ {
5216
+ text: 'apply',
5217
+ style: 'flex:1;padding:6px;background:#1890ff;color:#fff;border:none;' +
5218
+ 'border-radius:4px;cursor:pointer;font-size:13px;font-weight:500;',
5219
+ onClick: () => {
5220
+ apply_port_properties(gobj, node_id, port_key,
5221
+ shapeSelect.value, parseInt(rInput.value) || orig_r, scopeSelect.value
5222
+ );
5223
+ },
5224
+ },
5225
+ ]);
5226
+
5227
+ priv.$container.appendChild(popover);
5228
+ priv._port_popover_el = popover;
5229
+ clamp_popover_position(gobj, popover);
5230
+ }
5231
+
5232
+ /************************************************************
5233
+ * Apply a port's shape and radius to one port, to the same port
5234
+ * of every card of the topic, or to every port -- and remember
5235
+ * the choice as the topic's default, the way `resize all ports`
5236
+ * does, so a card that arrives later is born with it.
5237
+ * scope: 'this', 'same_topic', 'all'
5238
+ ************************************************************/
5239
+ function apply_port_properties(gobj, node_id, port_key, shape, r, scope)
5240
+ {
5241
+ let priv = gobj.priv;
5242
+ let graph = priv.graph;
5243
+
5244
+ let nodeData = graph.getNodeData(node_id);
5245
+ if(!nodeData || !nodeData.data) {
5246
+ return;
5247
+ }
5248
+ let source_topic = nodeData.data.topic_name;
5249
+
5250
+ if(scope === 'same_topic') {
5251
+ if(!is_object(priv._graph_properties[source_topic])) {
5252
+ priv._graph_properties[source_topic] = {};
5253
+ }
5254
+ let defaults = priv._graph_properties[source_topic].defaults || {};
5255
+ defaults.port_sizes = Object.assign({}, defaults.port_sizes, {[port_key]: r});
5256
+ defaults.port_shapes = Object.assign({}, defaults.port_shapes, {[port_key]: shape});
5257
+ priv._graph_properties[source_topic].defaults = defaults;
5258
+ } else if(scope === 'all') {
5259
+ for(const topic_name of Object.keys(priv.descs || {})) {
5260
+ if(!is_object(priv._graph_properties[topic_name])) {
5261
+ priv._graph_properties[topic_name] = {};
5262
+ }
5263
+ let defaults = priv._graph_properties[topic_name].defaults || {};
5264
+ defaults.portR = r;
5265
+ defaults.port_shape = shape;
5266
+ delete defaults.port_sizes;
5267
+ delete defaults.port_shapes;
5268
+ priv._graph_properties[topic_name].defaults = defaults;
5269
+ }
5270
+ }
5271
+
5272
+ let updates = [];
5273
+ for(let nd of (graph.getNodeData() || [])) {
5274
+ if(!nd || !nd.data || !nd.data.desc || !nd.style || !is_array(nd.style.ports)) {
5275
+ continue;
5276
+ }
5277
+ let new_ports;
5278
+ if(scope === 'this') {
5279
+ if(nd.id !== node_id) {
5280
+ continue;
5281
+ }
5282
+ new_ports = ports_with(nd.style.ports, port_key, {shape: shape, r: r});
5283
+ } else if(scope === 'same_topic') {
5284
+ if(nd.data.topic_name !== source_topic) {
5285
+ continue;
5286
+ }
5287
+ new_ports = ports_with(nd.style.ports, port_key, {shape: shape, r: r});
5288
+ } else {
5289
+ new_ports = nd.style.ports.map((p) => Object.assign({}, p, {shape: shape, r: r}));
5290
+ }
5291
+ let upd = {ports: new_ports};
5292
+ if(scope === 'all') {
5293
+ upd.portR = r;
5294
+ }
5295
+ updates.push({id: nd.id, style: upd});
5296
+ }
5297
+
5298
+ if(updates.length > 0) {
5299
+ graph.updateNodeData(updates);
5300
+ graph.draw().then(() => {
5301
+ update_port_resize_handles_position(gobj);
5302
+ update_port_icon_position(gobj);
5303
+ mark_graph_dirty(gobj);
5304
+ });
5305
+ }
5306
+ hide_port_popover(gobj);
5307
+ }
5308
+
4919
5309
  /************************************************************
4920
5310
  * Get a column descriptor by field id from a topic desc.
4921
5311
  ************************************************************/
@@ -7410,6 +7800,11 @@ function build_node_context_menu(gobj, node_id)
7410
7800
  if(gobj.priv.edit_mode && !node_is_compact(gobj, node_id)) {
7411
7801
  items.push(ctx_item('g6-icon-resize', t('resize all'), 'resize_all_nodes'));
7412
7802
  items.push(ctx_item('g6-icon-resize', t('resize topic nodes'), 'resize_topic_nodes'));
7803
+ /* The way back: the sizes somebody saved -- or that a Save
7804
+ * froze before 7.23.80 -- forgotten, the library's defaults
7805
+ * back on every card. */
7806
+ items.push(ctx_item('g6-icon-undo', t('reset sizes'), 'reset_all_sizes'));
7807
+ items.push(ctx_item('g6-icon-undo', t('reset topic sizes'), 'reset_topic_sizes'));
7413
7808
  }
7414
7809
 
7415
7810
  return items;
@@ -7424,6 +7819,9 @@ function build_port_context_menu(gobj, node_id, port_key)
7424
7819
 
7425
7820
  if(gobj.priv.edit_mode) {
7426
7821
  inject_svg_icons();
7822
+ /* The popover, for a finger: the gear beside the port needs
7823
+ * the port selected first, and a long press is the menu. */
7824
+ items.push(ctx_item('g6-icon-edit', t('port properties'), 'port_properties'));
7427
7825
  items.push(ctx_item('g6-icon-resize', t('resize all ports'), 'resize_all_ports'));
7428
7826
  items.push(ctx_item('g6-icon-resize', t('resize topic ports'), 'resize_topic_ports'));
7429
7827
  }
@@ -7460,6 +7858,18 @@ function handle_context_menu_click(gobj, value)
7460
7858
  case 'resize_topic_nodes':
7461
7859
  copy_size_to_nodes(gobj, true);
7462
7860
  break;
7861
+ case 'reset_all_sizes':
7862
+ reset_sizes(gobj, false);
7863
+ break;
7864
+ case 'reset_topic_sizes':
7865
+ reset_sizes(gobj, true);
7866
+ break;
7867
+ case 'port_properties':
7868
+ if(priv._context_node_id && priv._context_port_key) {
7869
+ select_port(gobj, priv._context_node_id, priv._context_port_key);
7870
+ show_port_popover(gobj);
7871
+ }
7872
+ break;
7463
7873
  case 'resize_all_ports':
7464
7874
  copy_size_to_ports(gobj, false);
7465
7875
  break;
@@ -7546,7 +7956,7 @@ function copy_size_to_nodes(gobj, same_topic_only)
7546
7956
  }
7547
7957
 
7548
7958
  // Recalculate dx/dy for HTML nodes
7549
- if(nd.type === 'html') {
7959
+ if(is_card_type(nd.type)) {
7550
7960
  updateStyle.dx = -source_size[0] / 2;
7551
7961
  let h = source_size.length > 1 ? source_size[1] : source_size[0];
7552
7962
  updateStyle.dy = -h / 2;
@@ -7564,6 +7974,75 @@ function copy_size_to_nodes(gobj, same_topic_only)
7564
7974
  }
7565
7975
  }
7566
7976
 
7977
+ /************************************************************
7978
+ * Forget every SAVED size: the card sizes, the port radii and the
7979
+ * per-port radii, per node and as topic defaults (`resize all`),
7980
+ * for one topic or for all of them. The cards go back to the
7981
+ * library's defaults on the spot, and Save writes the cleared
7982
+ * entries -- a size nobody chose is not written back (see
7983
+ * update_geometry). Positions and edge styles are not touched:
7984
+ * they are a different choice.
7985
+ ************************************************************/
7986
+ function reset_sizes(gobj, same_topic_only)
7987
+ {
7988
+ let priv = gobj.priv;
7989
+ let graph = priv.graph;
7990
+
7991
+ let topic = "";
7992
+ if(same_topic_only) {
7993
+ let nd = priv._context_node_id? graph.getNodeData(priv._context_node_id) : null;
7994
+ if(!nd || !nd.data || !nd.data.topic_name) {
7995
+ log_error(`${gobj_short_name(gobj)}: reset topic sizes without a node`);
7996
+ return;
7997
+ }
7998
+ topic = nd.data.topic_name;
7999
+ }
8000
+ const SIZE_KEYS = ["size", "portR", "port_sizes"];
8001
+ let strip = (props) => {
8002
+ if(!is_object(props)) {
8003
+ return;
8004
+ }
8005
+ for(let k of SIZE_KEYS) {
8006
+ delete props[k];
8007
+ }
8008
+ };
8009
+
8010
+ /* The saved entries, on screen or folded away. */
8011
+ for(const [topic_name, props] of Object.entries(priv._graph_properties || {})) {
8012
+ if(topic && topic_name !== topic) {
8013
+ continue;
8014
+ }
8015
+ if(is_object(props.nodes)) {
8016
+ for(let node_props of Object.values(props.nodes)) {
8017
+ strip(node_props);
8018
+ }
8019
+ }
8020
+ strip(props.defaults);
8021
+ }
8022
+
8023
+ /* The nodes on screen: their own copy of the entry, then the shape. */
8024
+ let ids = [];
8025
+ for(let nd of (graph.getNodeData() || [])) {
8026
+ if(!nd || !nd.data || !nd.data.desc || is_more_node(nd)) {
8027
+ continue;
8028
+ }
8029
+ if(topic && nd.data.topic_name !== topic) {
8030
+ continue;
8031
+ }
8032
+ strip(nd.data.graph_props);
8033
+ if(nd.data.record) {
8034
+ strip(nd.data.record._geometry);
8035
+ }
8036
+ ids.push(nd.id);
8037
+ }
8038
+ reshape_nodes(gobj, ids);
8039
+ graph.draw().then(() => {
8040
+ update_resize_handles_position(gobj);
8041
+ update_port_resize_handles_position(gobj);
8042
+ mark_graph_dirty(gobj);
8043
+ });
8044
+ }
8045
+
7567
8046
  /************************************************************
7568
8047
  * Copy the selected port's radius to matching ports.
7569
8048
  * If same_topic_only=true, only ports in same-topic nodes.
@@ -8314,6 +8793,7 @@ async function reconcile_fold_apply(gobj, opts, visible)
8314
8793
  update_edge_icon_position(gobj);
8315
8794
  update_node_icon_position(gobj);
8316
8795
  update_link_icon_position(gobj);
8796
+ update_port_icon_position(gobj);
8317
8797
 
8318
8798
  return changed;
8319
8799
  }
package/src/lib_graph.css CHANGED
@@ -86,6 +86,7 @@
86
86
  .graph-container .g6-create-popover,
87
87
  .graph-container .g6-node-popover,
88
88
  .graph-container .g6-edge-popover,
89
+ .graph-container .g6-port-popover,
89
90
  .graph-container .g6-node-detail {
90
91
  -webkit-user-select: text;
91
92
  user-select: text;
@@ -99,6 +100,7 @@
99
100
  .graph-container .g6-create-popover,
100
101
  .graph-container .g6-node-popover,
101
102
  .graph-container .g6-edge-popover,
103
+ .graph-container .g6-port-popover,
102
104
  .graph-container .g6-node-detail,
103
105
  .graph-container .g6-contextmenu {
104
106
  touch-action: auto;