@yuneta/gobj-ui 7.23.80 → 7.23.82

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';
@@ -262,16 +263,34 @@ const PORT_R_CHILD = 8;
262
263
  const PORT_LINE_WIDTH = 2;
263
264
 
264
265
  /*
265
- * The CLOSED shape of a record: a rounded square of the topic's
266
- * colour, no ports, no text -- topology and nothing else. The
267
- * three tiers keep their order of size, so a root is still bigger
268
- * than a leaf when nothing says which is which.
266
+ * The three VIEWS of a record (`node_mode`), the same three the
267
+ * gobj tree view has: `expanded` is the card with its pills and
268
+ * its ports; `compact` is a one-line pill with the name inside
269
+ * and small ports, the half-way house; `shape` is a figure of the
270
+ * topic's colour -- a square, a circle, a diamond... chosen in the
271
+ * node's properties -- no ports, no text, topology and nothing
272
+ * else. The three tiers keep their order of size in every view,
273
+ * so a root is still bigger than a leaf when nothing says which
274
+ * is which.
269
275
  */
270
- const COMPACT_SIZE = {
276
+ const NODE_MODES = ['expanded', 'compact', 'shape'];
277
+
278
+ const PILL_SIZE = {
279
+ hierarchical: [200, 30],
280
+ extended: [180, 28],
281
+ child: [160, 26],
282
+ };
283
+ const PORT_R_PILL = 6;
284
+
285
+ const SHAPE_SIZE = {
271
286
  hierarchical: 32,
272
287
  extended: 28,
273
288
  child: 22,
274
289
  };
290
+ /* The figures a `shape` node can be, by G6's own node types
291
+ * (`square` is G6's `rect` with rounded corners). */
292
+ const NODE_SHAPES = ['square', 'circle', 'diamond', 'triangle', 'hexagon', 'star'];
293
+ const DEFAULT_NODE_SHAPE = 'square';
275
294
 
276
295
  /***************************************************************
277
296
  * Internal layout and operation mode definitions
@@ -397,8 +416,8 @@ SDATA(data_type_t.DTP_STRING, "main_topic", 0, "", "The topic t
397
416
  SDATA(data_type_t.DTP_LIST, "loose_topics", 0, "[]", "Topics whose loose records (no parent, in a topic the schema hangs from the main one) are shown as roots"),
398
417
 
399
418
  /*---------------- Node shape ----------------*/
400
- SDATA(data_type_t.DTP_STRING, "node_mode", 0, "expanded", "How a record is drawn: `expanded` = the card with its ports (the only shape a link can be edited on); `compact` = a rounded square of the topic's colour, no ports, no text -- the topology alone. One node can be toggled against it (EV_TOGGLE_NODE_MODE)"),
401
- SDATA(data_type_t.DTP_BOOLEAN, "node_labels", 0, true, "A compact node says its name under the square. Off, it is a coloured square and nothing else"),
419
+ SDATA(data_type_t.DTP_STRING, "node_mode", 0, "expanded", "How a record is drawn: `expanded` = the card with its pills and ports; `compact` = a one-line pill with the name and small ports; `shape` = a figure of the topic's colour (square, circle, diamond... per node or topic, from the node properties), no ports, no text -- the topology alone. One node can be toggled against it (EV_TOGGLE_NODE_MODE)"),
420
+ SDATA(data_type_t.DTP_BOOLEAN, "node_labels", 0, true, "A `shape` node says its name under the figure. Off, it is a coloured figure and nothing else"),
402
421
  SDATA(data_type_t.DTP_BOOLEAN, "confirm_delete_node", 0, true, "Ask confirmation before deleting a node"),
403
422
  SDATA(data_type_t.DTP_BOOLEAN, "confirm_unlink_edge", 0, true, "Ask confirmation before unlinking an edge"),
404
423
 
@@ -446,6 +465,8 @@ let PRIVATE_DATA = {
446
465
  _edge_icon_el: null, // floating properties icon element
447
466
  _edge_delete_el: null, // floating delete icon element for edge
448
467
  _edge_popover_el: null, // edge properties popover element
468
+ _port_icon_el: null, // floating port properties icon element
469
+ _port_popover_el: null, // port properties popover element
449
470
  _node_icon_el: null, // floating node properties icon element
450
471
  _node_delete_el: null, // floating delete icon element for node
451
472
  _node_popover_el: null, // node properties popover element
@@ -839,6 +860,7 @@ function register_layouts(gobj)
839
860
  register(ExtensionCategory.LAYOUT, 'treedb-tree', TreedbTreeLayout);
840
861
  register(ExtensionCategory.LAYOUT, 'treedb-radial', TreedbRadialLayout);
841
862
  register(ExtensionCategory.NODE, 'light', LightNode);
863
+ register(ExtensionCategory.NODE, 'treedb-card', TreedbCard);
842
864
  }
843
865
  }
844
866
 
@@ -940,6 +962,7 @@ function configure_events(gobj)
940
962
  update_edge_icon_position(gobj);
941
963
  update_node_icon_position(gobj);
942
964
  update_link_icon_position(gobj);
965
+ update_port_icon_position(gobj);
943
966
  });
944
967
 
945
968
  graph.on(NodeEvent.DRAG_END, (evt) => {
@@ -974,6 +997,7 @@ function configure_events(gobj)
974
997
  update_edge_icon_position(gobj);
975
998
  update_node_icon_position(gobj);
976
999
  update_link_icon_position(gobj);
1000
+ update_port_icon_position(gobj);
977
1001
  update_zoom_readout(gobj);
978
1002
 
979
1003
  /*
@@ -2014,20 +2038,47 @@ function create_topic_node(gobj, desc, record)
2014
2038
  ************************************************************/
2015
2039
  function node_shape_of(gobj, node_id, desc, record, geometry, pills_html)
2016
2040
  {
2017
- if(node_is_compact(gobj, node_id)) {
2018
- return compact_shape_of(gobj, desc, record, {});
2041
+ switch(node_mode_of(gobj, node_id)) {
2042
+ case 'shape':
2043
+ return figure_shape_of(gobj, desc, record, geometry, {});
2044
+ case 'compact':
2045
+ return pill_shape_of(gobj, desc, record, geometry);
2046
+ default:
2047
+ return card_shape_of(gobj, desc, record, geometry, pills_html);
2019
2048
  }
2020
- return card_shape_of(gobj, desc, record, geometry, pills_html);
2021
2049
  }
2022
2050
 
2023
- function node_is_compact(gobj, node_id)
2051
+ /* The view of ONE node: the graph's `node_mode`, unless the node
2052
+ * was toggled against it -- an expanded node in a compact or shape
2053
+ * graph, a compact one in an expanded graph. */
2054
+ function node_mode_of(gobj, node_id)
2024
2055
  {
2025
2056
  let priv = gobj.priv;
2026
2057
  if(!priv._open_nodes) {
2027
2058
  priv._open_nodes = new Set();
2028
2059
  }
2029
- let compact = (gobj_read_str_attr(gobj, "node_mode") === "compact");
2030
- return priv._open_nodes.has(node_id)? !compact : compact;
2060
+ let mode = gobj_read_str_attr(gobj, "node_mode");
2061
+ if(!mode || !str_in_list(NODE_MODES, mode)) {
2062
+ mode = 'expanded';
2063
+ }
2064
+ if(priv._open_nodes.has(node_id)) {
2065
+ return (mode === 'expanded')? 'compact' : 'expanded';
2066
+ }
2067
+ return mode;
2068
+ }
2069
+
2070
+ /* Not the full card: a pill or a figure. What the size bookkeeping
2071
+ * asks -- neither has a size anybody chose, or pills to grow for. */
2072
+ function node_is_compact(gobj, node_id)
2073
+ {
2074
+ return node_mode_of(gobj, node_id) !== 'expanded';
2075
+ }
2076
+
2077
+ function topic_defaults_of(gobj, topic_name)
2078
+ {
2079
+ let topic_props = gobj.priv._graph_properties[topic_name];
2080
+ return (is_object(topic_props) && is_object(topic_props.defaults))?
2081
+ topic_props.defaults : null;
2031
2082
  }
2032
2083
 
2033
2084
  /************************************************************
@@ -2109,54 +2160,139 @@ function card_shape_of(gobj, desc, record, geometry, pills_html)
2109
2160
  style.portR = geometry.portR;
2110
2161
  }
2111
2162
 
2112
- if(json_size(ports)) {
2113
- json_object_update_missing(style, {
2114
- port: true,
2115
- ports: ports,
2116
- portR: node_treedb_type === 'child' ? PORT_R_CHILD : PORT_R_ENTITY,
2117
- portLineWidth: PORT_LINE_WIDTH,
2118
- });
2163
+ dress_ports(gobj, style, ports, geometry, topic_defaults,
2164
+ node_treedb_type === 'child' ? PORT_R_CHILD : PORT_R_ENTITY, false);
2119
2165
 
2120
- // Restore per-port radius: saved geometry first, then topic defaults
2121
- let port_sizes = geometry.port_sizes;
2122
- let default_port_sizes = topic_defaults ? topic_defaults.port_sizes : null;
2123
- if(is_object(port_sizes) || is_object(default_port_sizes)) {
2124
- for(let i = 0; i < style.ports.length; i++) {
2125
- let key = style.ports[i].key;
2126
- let r = is_object(port_sizes) ? port_sizes[key] : null;
2127
- if(r == null && is_object(default_port_sizes)) {
2128
- r = default_port_sizes[key];
2129
- }
2130
- if(r != null) {
2131
- style.ports[i].r = r;
2132
- }
2133
- }
2134
- }
2135
- } else {
2166
+ return {type: 'treedb-card', style: style};
2167
+ }
2168
+
2169
+ /************************************************************
2170
+ * The ports of a card or a pill, dressed: the node-level radius
2171
+ * (saved, topic default, or the tier's), each port's own radius
2172
+ * and shape (saved first, then the topic's per-port defaults, then
2173
+ * the topic's one shape for every port). `fixed_r` -- the pill --
2174
+ * ignores every saved radius: a pill has one size of port.
2175
+ ************************************************************/
2176
+ function dress_ports(gobj, style, ports, geometry, topic_defaults, r_default, fixed_r)
2177
+ {
2178
+ if(!json_size(ports)) {
2136
2179
  style.port = false;
2137
2180
  style.ports = [];
2181
+ return;
2182
+ }
2183
+ json_object_update_missing(style, {
2184
+ port: true,
2185
+ ports: ports,
2186
+ portR: r_default,
2187
+ portLineWidth: PORT_LINE_WIDTH,
2188
+ });
2189
+ if(fixed_r) {
2190
+ style.portR = r_default;
2138
2191
  }
2139
2192
 
2140
- return {type: 'html', style: style};
2193
+ let port_sizes = fixed_r? null : geometry.port_sizes;
2194
+ let default_port_sizes = (!fixed_r && topic_defaults)? topic_defaults.port_sizes : null;
2195
+ let port_shapes = geometry.port_shapes;
2196
+ let default_port_shapes = topic_defaults? topic_defaults.port_shapes : null;
2197
+ let default_shape = topic_defaults? topic_defaults.port_shape : null;
2198
+ for(let i = 0; i < style.ports.length; i++) {
2199
+ let key = style.ports[i].key;
2200
+ let r = is_object(port_sizes)? port_sizes[key] : null;
2201
+ if(r == null && is_object(default_port_sizes)) {
2202
+ r = default_port_sizes[key];
2203
+ }
2204
+ if(r != null) {
2205
+ style.ports[i].r = r;
2206
+ }
2207
+ let shape = is_object(port_shapes)? port_shapes[key] : null;
2208
+ if(!shape && is_object(default_port_shapes)) {
2209
+ shape = default_port_shapes[key];
2210
+ }
2211
+ if(!shape) {
2212
+ shape = default_shape;
2213
+ }
2214
+ if(shape && str_in_list(PORT_SHAPES, shape)) {
2215
+ style.ports[i].shape = shape;
2216
+ }
2217
+ }
2218
+ }
2219
+
2220
+ /************************************************************
2221
+ * The PILL: one line with the name inside, the tint and the
2222
+ * border of the topic, small ports on its edges -- the half-way
2223
+ * house between the card and the figure, and the same one-line
2224
+ * chip a leaf record already is, at the tier's width. No pills:
2225
+ * a hook is opened from the card (expand the node).
2226
+ ************************************************************/
2227
+ function pill_shape_of(gobj, desc, record, geometry)
2228
+ {
2229
+ let priv = gobj.priv;
2230
+ let tier = desc.node_treedb_type;
2231
+ let size = PILL_SIZE[tier] || PILL_SIZE.hierarchical;
2232
+ let ports = build_ports(gobj, desc);
2233
+
2234
+ let style = {
2235
+ fill: desc.color,
2236
+ stroke: getStrokeColor(desc.color),
2237
+ lineWidth: 1,
2238
+ lineDash: [],
2239
+ halo: false,
2240
+ labelText: "",
2241
+ size: [...size],
2242
+ dx: -size[0] / 2,
2243
+ dy: -size[1] / 2,
2244
+ innerHTML: build_chip_innerHTML(
2245
+ desc.color, priv.theme, record.icon, node_label(desc, record), record.id
2246
+ ),
2247
+ };
2248
+ dress_ports(gobj, style, ports, geometry, topic_defaults_of(gobj, desc.topic_name),
2249
+ PORT_R_PILL, true);
2250
+ return {type: 'treedb-card', style: style};
2251
+ }
2252
+
2253
+ /* The pill's html with the flags it carries right now. */
2254
+ function pill_innerHTML_of(gobj, nd, theme, highlight, selected, anchored)
2255
+ {
2256
+ let desc = nd.data.desc;
2257
+ let record = nd.data.record || {};
2258
+ return build_chip_innerHTML(
2259
+ desc.color, theme, record.icon, node_label(desc, record), record.id,
2260
+ highlight, selected, anchored
2261
+ );
2262
+ }
2263
+
2264
+ /* The figure a `shape` node wears: the node's own, then the
2265
+ * topic's default (the node properties popover writes both), then
2266
+ * a square. */
2267
+ function node_figure_of(gobj, desc, geometry)
2268
+ {
2269
+ let shape = geometry? geometry.node_shape : null;
2270
+ if(!shape) {
2271
+ let defaults = topic_defaults_of(gobj, desc.topic_name);
2272
+ shape = defaults? defaults.node_shape : null;
2273
+ }
2274
+ return (shape && str_in_list(NODE_SHAPES, shape))? shape : DEFAULT_NODE_SHAPE;
2141
2275
  }
2142
2276
 
2143
2277
  /************************************************************
2144
- * The SQUARE: a G6 `rect` of the topic's colour, no ports, the
2145
- * name as a label under it when `node_labels` says so. A native
2146
- * shape and not html on purpose: G6's own stroke and halo paint
2147
- * on it, so the focus, the selection and the anchor are three
2148
- * keys of the style instead of a rebuilt innerHTML.
2278
+ * The FIGURE: a native G6 node of the topic's colour -- a square,
2279
+ * a circle, a diamond, a triangle, a hexagon, a star -- no ports,
2280
+ * the name as a label under it when `node_labels` says so. Native
2281
+ * and not html on purpose: G6's own stroke and halo paint on it,
2282
+ * so the focus, the selection and the anchor are three keys of
2283
+ * the style instead of a rebuilt innerHTML.
2149
2284
  *
2150
2285
  * flags {highlight, selected, anchored}
2151
2286
  ************************************************************/
2152
- function compact_shape_of(gobj, desc, record, flags)
2287
+ function figure_shape_of(gobj, desc, record, geometry, flags)
2153
2288
  {
2154
2289
  let priv = gobj.priv;
2155
2290
  let dark = (priv.theme === "dark");
2156
2291
  let f = flags || {};
2157
2292
  let tier = desc.node_treedb_type;
2158
- let side = COMPACT_SIZE[tier] || COMPACT_SIZE.hierarchical;
2293
+ let side = SHAPE_SIZE[tier] || SHAPE_SIZE.hierarchical;
2159
2294
  let labels = gobj_read_bool_attr(gobj, "node_labels");
2295
+ let figure = node_figure_of(gobj, desc, geometry || {});
2160
2296
 
2161
2297
  let stroke = getStrokeColor(desc.color);
2162
2298
  let line_width = 1.5;
@@ -2181,7 +2317,7 @@ function compact_shape_of(gobj, desc, record, flags)
2181
2317
 
2182
2318
  let style = {
2183
2319
  size: [side, side],
2184
- radius: Math.round(side / 5),
2320
+ radius: (figure === 'square')? Math.round(side / 5) : 0,
2185
2321
  fill: desc.color,
2186
2322
  stroke: stroke,
2187
2323
  lineWidth: line_width,
@@ -2205,7 +2341,7 @@ function compact_shape_of(gobj, desc, record, flags)
2205
2341
  labelBackgroundRadius: 3,
2206
2342
  labelPadding: [1, 3],
2207
2343
  };
2208
- return {type: 'rect', style: style};
2344
+ return {type: (figure === 'square')? 'rect' : figure, style: style};
2209
2345
  }
2210
2346
 
2211
2347
  /************************************************************
@@ -2776,6 +2912,18 @@ function update_geometry(gobj, node_id)
2776
2912
  } else {
2777
2913
  delete node_props.port_sizes;
2778
2914
  }
2915
+ // And the shape of each port that is not a circle
2916
+ let port_shapes = {};
2917
+ for(let i = 0; i < ports.length; i++) {
2918
+ if(ports[i].shape && ports[i].shape !== 'circle') {
2919
+ port_shapes[ports[i].key] = ports[i].shape;
2920
+ }
2921
+ }
2922
+ if(Object.keys(port_shapes).length > 0) {
2923
+ node_props.port_shapes = port_shapes;
2924
+ } else {
2925
+ delete node_props.port_shapes;
2926
+ }
2779
2927
  }
2780
2928
 
2781
2929
  topic_props.nodes[record.id] = node_props;
@@ -3485,6 +3633,7 @@ function graph_resize(gobj, width, height)
3485
3633
  update_port_resize_handles_position(gobj);
3486
3634
  update_node_icon_position(gobj);
3487
3635
  update_link_icon_position(gobj);
3636
+ update_port_icon_position(gobj);
3488
3637
  }
3489
3638
 
3490
3639
  function graph_write_behaviors(gobj, behaviors)
@@ -3520,6 +3669,64 @@ function graph_set_behavior(gobj, behavior, set)
3520
3669
  });
3521
3670
  }
3522
3671
 
3672
+ /************************************************************
3673
+ * Custom G6 node: TreedbCard -- G6's html node with ports that
3674
+ * have a SHAPE.
3675
+ *
3676
+ * G6 draws every port as a circle (`drawPortShapes` upserts a
3677
+ * `Circle`, nothing in the port style says otherwise). A port's
3678
+ * `shape` -- `circle`, `square`, `diamond`, `triangle` -- picks
3679
+ * the G shape here, by the names G6 registers itself, and `r`
3680
+ * stays the one size: the half-side of the square, the half-
3681
+ * diagonal of the diamond, the half-height of the triangle. The
3682
+ * hit test, the resize handles and the edge's landing point all
3683
+ * read `r` and none of them care about the outline.
3684
+ ************************************************************/
3685
+ const PORT_SHAPES = ['circle', 'square', 'diamond', 'triangle'];
3686
+
3687
+ class TreedbCard extends HTML
3688
+ {
3689
+ drawPortShapes(attributes, container) {
3690
+ const portsStyle = this.getPortsStyle(attributes);
3691
+ Object.keys(portsStyle).forEach((key) => {
3692
+ const style = portsStyle[key];
3693
+ const shapeKey = `port-${key}`;
3694
+ if(style === false) {
3695
+ this.upsert(shapeKey, 'circle', false, container);
3696
+ return;
3697
+ }
3698
+ let r = Number(style.r) || 0;
3699
+ switch(style.shape) {
3700
+ case 'square':
3701
+ this.upsert(shapeKey, 'rect', Object.assign({}, style, {
3702
+ x: -r, y: -r, width: 2 * r, height: 2 * r,
3703
+ }), container);
3704
+ break;
3705
+ case 'diamond':
3706
+ this.upsert(shapeKey, 'polygon', Object.assign({}, style, {
3707
+ points: [[0, -r], [r, 0], [0, r], [-r, 0]],
3708
+ }), container);
3709
+ break;
3710
+ case 'triangle':
3711
+ this.upsert(shapeKey, 'polygon', Object.assign({}, style, {
3712
+ points: [[0, -r], [r, r], [-r, r]],
3713
+ }), container);
3714
+ break;
3715
+ default:
3716
+ this.upsert(shapeKey, 'circle', style, container);
3717
+ break;
3718
+ }
3719
+ });
3720
+ }
3721
+ }
3722
+
3723
+ /* A record's card is `treedb-card`; `html` is what it was until
3724
+ * 7.23.81 and what a `+N` chip still is. */
3725
+ function is_card_type(type)
3726
+ {
3727
+ return type === 'treedb-card' || type === 'html';
3728
+ }
3729
+
3523
3730
  /************************************************************
3524
3731
  * Custom G6 node: LightNode (circle with status indicator)
3525
3732
  ************************************************************/
@@ -4661,7 +4868,7 @@ function start_node_resize(gobj, e, mx, my)
4661
4868
  }
4662
4869
 
4663
4870
  // HTML nodes: update dx, dy to keep content centered
4664
- if(nodeType === 'html') {
4871
+ if(is_card_type(nodeType)) {
4665
4872
  updateStyle.dx = -newW / 2;
4666
4873
  updateStyle.dy = -newH / 2;
4667
4874
  }
@@ -4842,6 +5049,7 @@ function select_port(gobj, node_id, port_key)
4842
5049
 
4843
5050
  show_port_resize_handles(gobj);
4844
5051
  show_link_icon_if_fkey(gobj);
5052
+ show_port_icon(gobj);
4845
5053
  }
4846
5054
 
4847
5055
  /************************************************************
@@ -4853,6 +5061,8 @@ function deselect_port(gobj)
4853
5061
 
4854
5062
  exit_linking_mode(gobj);
4855
5063
  hide_link_icon(gobj);
5064
+ hide_port_icon(gobj);
5065
+ hide_port_popover(gobj);
4856
5066
  hide_port_resize_handles(gobj);
4857
5067
  priv._selected_port_key = null;
4858
5068
  }
@@ -4940,6 +5150,272 @@ function update_link_icon_position(gobj)
4940
5150
  }
4941
5151
  }
4942
5152
 
5153
+ /************************************************************
5154
+ * Port properties icon and popover: the gear beside a selected
5155
+ * port, as the node has its own. The popover has the two things
5156
+ * a port IS on screen -- its shape and its radius -- and the
5157
+ * scope: this port, the same port of every card of the topic
5158
+ * (the hook or fkey column), or every port there is.
5159
+ ************************************************************/
5160
+ function port_icon_xy(gobj)
5161
+ {
5162
+ let priv = gobj.priv;
5163
+ let graph = priv.graph;
5164
+ let canvasPos = get_port_canvas_position(
5165
+ gobj, priv._selected_node_id, priv._selected_port_key
5166
+ );
5167
+ if(!canvasPos) {
5168
+ return null;
5169
+ }
5170
+ let vp = graph.getViewportByCanvas([canvasPos.x, canvasPos.y]);
5171
+ /* Under the link icon when there is one (an fkey), in its
5172
+ * place otherwise. */
5173
+ let below = priv._link_icon_el? floating_icon_step() : 0;
5174
+ return {
5175
+ x: vp[0] + floating_icon_size() / 2 + 4,
5176
+ y: vp[1] + floating_icon_dy() + below,
5177
+ };
5178
+ }
5179
+
5180
+ function show_port_icon(gobj)
5181
+ {
5182
+ hide_port_icon(gobj);
5183
+
5184
+ let priv = gobj.priv;
5185
+ if(!priv._selected_node_id || !priv._selected_port_key) {
5186
+ return;
5187
+ }
5188
+ let xy = port_icon_xy(gobj);
5189
+ if(!xy) {
5190
+ return;
5191
+ }
5192
+ let icon = create_floating_icon(
5193
+ 'gear', '#1890ff', xy.x, xy.y,
5194
+ t('port properties'), () => toggle_port_popover(gobj)
5195
+ );
5196
+ priv.$container.appendChild(icon);
5197
+ priv._port_icon_el = icon;
5198
+ }
5199
+
5200
+ function hide_port_icon(gobj)
5201
+ {
5202
+ hide_overlay(gobj, '_port_icon_el');
5203
+ }
5204
+
5205
+ function update_port_icon_position(gobj)
5206
+ {
5207
+ let priv = gobj.priv;
5208
+ if(!priv._port_icon_el || !priv._selected_node_id || !priv._selected_port_key) {
5209
+ return;
5210
+ }
5211
+ try {
5212
+ let xy = port_icon_xy(gobj);
5213
+ if(!xy) {
5214
+ hide_port_icon(gobj);
5215
+ hide_port_popover(gobj);
5216
+ return;
5217
+ }
5218
+ priv._port_icon_el.style.left = xy.x + 'px';
5219
+ priv._port_icon_el.style.top = xy.y + 'px';
5220
+ if(priv._port_popover_el) {
5221
+ priv._port_popover_el.style.left = (xy.x + floating_popover_dx()) + 'px';
5222
+ priv._port_popover_el.style.top = xy.y + 'px';
5223
+ clamp_popover_position(gobj, priv._port_popover_el);
5224
+ }
5225
+ } catch(e) {
5226
+ hide_port_icon(gobj);
5227
+ hide_port_popover(gobj);
5228
+ }
5229
+ }
5230
+
5231
+ function toggle_port_popover(gobj)
5232
+ {
5233
+ let priv = gobj.priv;
5234
+ if(priv._port_popover_el) {
5235
+ hide_port_popover(gobj);
5236
+ } else {
5237
+ show_port_popover(gobj);
5238
+ }
5239
+ }
5240
+
5241
+ function hide_port_popover(gobj)
5242
+ {
5243
+ hide_overlay(gobj, '_port_popover_el');
5244
+ }
5245
+
5246
+ /* The ports of a node with ONE of them rewritten. */
5247
+ function ports_with(ports, port_key, changes)
5248
+ {
5249
+ return (ports || []).map((p) => (p.key === port_key)? Object.assign({}, p, changes) : p);
5250
+ }
5251
+
5252
+ function show_port_popover(gobj)
5253
+ {
5254
+ hide_port_popover(gobj);
5255
+
5256
+ let priv = gobj.priv;
5257
+ let graph = priv.graph;
5258
+ let node_id = priv._selected_node_id;
5259
+ let port_key = priv._selected_port_key;
5260
+ if(!node_id || !port_key) {
5261
+ return;
5262
+ }
5263
+ let nodeData = graph.getNodeData(node_id);
5264
+ if(!nodeData || !nodeData.style) {
5265
+ return;
5266
+ }
5267
+ let ports = nodeData.style.ports || [];
5268
+ let port = ports.find((p) => p.key === port_key);
5269
+ if(!port) {
5270
+ return;
5271
+ }
5272
+ let orig_shape = port.shape || 'circle';
5273
+ let orig_r = get_port_radius(gobj, node_id, port_key);
5274
+
5275
+ let xy = port_icon_xy(gobj);
5276
+ if(!xy) {
5277
+ return;
5278
+ }
5279
+ const popover = create_popover_base(
5280
+ xy.x + floating_popover_dx(), xy.y, 'g6-port-popover', '#d9d9d9', 180
5281
+ );
5282
+
5283
+ /* Live preview on THIS port; the others wait for apply. */
5284
+ function preview_port() {
5285
+ let r = parseInt(rInput.value) || orig_r;
5286
+ graph.updateNodeData([{id: node_id, style: {
5287
+ ports: ports_with(nodeData.style.ports, port_key, {shape: shapeSelect.value, r: r}),
5288
+ }}]);
5289
+ graph.draw().then(() => {
5290
+ update_port_resize_handles_position(gobj);
5291
+ });
5292
+ }
5293
+
5294
+ create_form_label(popover, 'shape');
5295
+ let shapeSelect = create_form_select(popover, PORT_SHAPES.map((shape) => {
5296
+ return {value: shape, label: t(shape)};
5297
+ }));
5298
+ shapeSelect.value = orig_shape;
5299
+ shapeSelect.addEventListener('change', preview_port);
5300
+
5301
+ create_form_label(popover, 'radius');
5302
+ let rInput = create_form_number_input(popover, orig_r, 2, 40, preview_port);
5303
+
5304
+ create_form_label(popover, 'apply to');
5305
+ let scopeSelect = create_form_select(popover, [
5306
+ {value: 'this', label: t('this port')},
5307
+ {value: 'same_topic', label: t('same port in topic')},
5308
+ {value: 'all', label: t('all ports')},
5309
+ ]);
5310
+
5311
+ create_form_button_row(popover, [
5312
+ {
5313
+ text: 'cancel',
5314
+ style: BTN_STYLE_CANCEL,
5315
+ onClick: () => {
5316
+ graph.updateNodeData([{id: node_id, style: {
5317
+ ports: ports_with(nodeData.style.ports, port_key, {shape: orig_shape, r: orig_r}),
5318
+ }}]);
5319
+ graph.draw().then(() => {
5320
+ update_port_resize_handles_position(gobj);
5321
+ });
5322
+ hide_port_popover(gobj);
5323
+ },
5324
+ },
5325
+ {
5326
+ text: 'apply',
5327
+ style: 'flex:1;padding:6px;background:#1890ff;color:#fff;border:none;' +
5328
+ 'border-radius:4px;cursor:pointer;font-size:13px;font-weight:500;',
5329
+ onClick: () => {
5330
+ apply_port_properties(gobj, node_id, port_key,
5331
+ shapeSelect.value, parseInt(rInput.value) || orig_r, scopeSelect.value
5332
+ );
5333
+ },
5334
+ },
5335
+ ]);
5336
+
5337
+ priv.$container.appendChild(popover);
5338
+ priv._port_popover_el = popover;
5339
+ clamp_popover_position(gobj, popover);
5340
+ }
5341
+
5342
+ /************************************************************
5343
+ * Apply a port's shape and radius to one port, to the same port
5344
+ * of every card of the topic, or to every port -- and remember
5345
+ * the choice as the topic's default, the way `resize all ports`
5346
+ * does, so a card that arrives later is born with it.
5347
+ * scope: 'this', 'same_topic', 'all'
5348
+ ************************************************************/
5349
+ function apply_port_properties(gobj, node_id, port_key, shape, r, scope)
5350
+ {
5351
+ let priv = gobj.priv;
5352
+ let graph = priv.graph;
5353
+
5354
+ let nodeData = graph.getNodeData(node_id);
5355
+ if(!nodeData || !nodeData.data) {
5356
+ return;
5357
+ }
5358
+ let source_topic = nodeData.data.topic_name;
5359
+
5360
+ if(scope === 'same_topic') {
5361
+ if(!is_object(priv._graph_properties[source_topic])) {
5362
+ priv._graph_properties[source_topic] = {};
5363
+ }
5364
+ let defaults = priv._graph_properties[source_topic].defaults || {};
5365
+ defaults.port_sizes = Object.assign({}, defaults.port_sizes, {[port_key]: r});
5366
+ defaults.port_shapes = Object.assign({}, defaults.port_shapes, {[port_key]: shape});
5367
+ priv._graph_properties[source_topic].defaults = defaults;
5368
+ } else if(scope === 'all') {
5369
+ for(const topic_name of Object.keys(priv.descs || {})) {
5370
+ if(!is_object(priv._graph_properties[topic_name])) {
5371
+ priv._graph_properties[topic_name] = {};
5372
+ }
5373
+ let defaults = priv._graph_properties[topic_name].defaults || {};
5374
+ defaults.portR = r;
5375
+ defaults.port_shape = shape;
5376
+ delete defaults.port_sizes;
5377
+ delete defaults.port_shapes;
5378
+ priv._graph_properties[topic_name].defaults = defaults;
5379
+ }
5380
+ }
5381
+
5382
+ let updates = [];
5383
+ for(let nd of (graph.getNodeData() || [])) {
5384
+ if(!nd || !nd.data || !nd.data.desc || !nd.style || !is_array(nd.style.ports)) {
5385
+ continue;
5386
+ }
5387
+ let new_ports;
5388
+ if(scope === 'this') {
5389
+ if(nd.id !== node_id) {
5390
+ continue;
5391
+ }
5392
+ new_ports = ports_with(nd.style.ports, port_key, {shape: shape, r: r});
5393
+ } else if(scope === 'same_topic') {
5394
+ if(nd.data.topic_name !== source_topic) {
5395
+ continue;
5396
+ }
5397
+ new_ports = ports_with(nd.style.ports, port_key, {shape: shape, r: r});
5398
+ } else {
5399
+ new_ports = nd.style.ports.map((p) => Object.assign({}, p, {shape: shape, r: r}));
5400
+ }
5401
+ let upd = {ports: new_ports};
5402
+ if(scope === 'all') {
5403
+ upd.portR = r;
5404
+ }
5405
+ updates.push({id: nd.id, style: upd});
5406
+ }
5407
+
5408
+ if(updates.length > 0) {
5409
+ graph.updateNodeData(updates);
5410
+ graph.draw().then(() => {
5411
+ update_port_resize_handles_position(gobj);
5412
+ update_port_icon_position(gobj);
5413
+ mark_graph_dirty(gobj);
5414
+ });
5415
+ }
5416
+ hide_port_popover(gobj);
5417
+ }
5418
+
4943
5419
  /************************************************************
4944
5420
  * Get a column descriptor by field id from a topic desc.
4945
5421
  ************************************************************/
@@ -5960,6 +6436,17 @@ function show_node_popover(gobj)
5960
6436
  create_form_label(popover, 'line width');
5961
6437
  let lwInput = create_form_number_input(popover, currentLW, 1, 20, preview_node);
5962
6438
 
6439
+ /* The figure the node wears in the `shape` view. No preview:
6440
+ * it shows only in that view, and applying it is the same
6441
+ * reshape either way. */
6442
+ create_form_label(popover, 'shape');
6443
+ let shapeSelect = create_form_select(popover, NODE_SHAPES.map((shape) => {
6444
+ return {value: shape, label: t(shape)};
6445
+ }));
6446
+ shapeSelect.value = nodeData.data && nodeData.data.desc
6447
+ ? node_figure_of(gobj, nodeData.data.desc, nodeData.data.graph_props || {})
6448
+ : DEFAULT_NODE_SHAPE;
6449
+
5963
6450
  // Apply-to scope
5964
6451
  create_form_label(popover, 'apply to');
5965
6452
  let scopeSelect = create_form_select(popover, [
@@ -5997,7 +6484,8 @@ function show_node_popover(gobj)
5997
6484
  fillInput.value,
5998
6485
  strokeInput.value,
5999
6486
  parseInt(lwInput.value) || 1,
6000
- scopeSelect.value
6487
+ scopeSelect.value,
6488
+ shapeSelect.value
6001
6489
  );
6002
6490
  },
6003
6491
  },
@@ -6397,18 +6885,22 @@ function repaint_cards(gobj, ids)
6397
6885
  continue; /* gone since it was listed */
6398
6886
  }
6399
6887
  let anchored = (priv.anchor_state === "on" && id === priv.anchor_id);
6400
- /* A closed node paints its flags on its own stroke. */
6401
- if(nd && nd.data && nd.data.desc && node_is_compact(gobj, id)) {
6402
- updates.push({id: id, style: compact_shape_of(
6403
- gobj, nd.data.desc, nd.data.record || {},
6888
+ /* A figure paints its flags on its own stroke; a pill in
6889
+ * its one-line html; a card in its own. */
6890
+ let mode = (nd && nd.data && nd.data.desc)? node_mode_of(gobj, id) : 'expanded';
6891
+ if(mode === 'shape') {
6892
+ updates.push({id: id, style: figure_shape_of(
6893
+ gobj, nd.data.desc, nd.data.record || {}, nd.data.graph_props || {},
6404
6894
  {highlight: focus.has(id), selected: selected.has(id), anchored: anchored}
6405
6895
  ).style});
6406
6896
  continue;
6407
6897
  }
6408
- let html = node_innerHTML_of(
6409
- nd, priv.theme, focus.has(id), selected.has(id), anchored,
6410
- pills_html_of(gobj, nd)
6411
- );
6898
+ let html = (mode === 'compact')
6899
+ ? pill_innerHTML_of(gobj, nd, priv.theme, focus.has(id), selected.has(id), anchored)
6900
+ : node_innerHTML_of(
6901
+ nd, priv.theme, focus.has(id), selected.has(id), anchored,
6902
+ pills_html_of(gobj, nd)
6903
+ );
6412
6904
  if(html !== null) {
6413
6905
  updates.push({id: id, style: {innerHTML: html}});
6414
6906
  }
@@ -6465,17 +6957,20 @@ function refresh_html_nodes_theme(gobj, theme)
6465
6957
  let id = nodes[i].id;
6466
6958
  let nd = graph.getNodeData(id);
6467
6959
  let anchored = (priv.anchor_state === "on" && id === priv.anchor_id);
6468
- if(nd && nd.data && nd.data.desc && node_is_compact(gobj, id)) {
6469
- updates.push({id: id, style: compact_shape_of(
6470
- gobj, nd.data.desc, nd.data.record || {},
6960
+ let mode = (nd && nd.data && nd.data.desc)? node_mode_of(gobj, id) : 'expanded';
6961
+ if(mode === 'shape') {
6962
+ updates.push({id: id, style: figure_shape_of(
6963
+ gobj, nd.data.desc, nd.data.record || {}, nd.data.graph_props || {},
6471
6964
  {highlight: highlighted.has(id), selected: selected.has(id), anchored: anchored}
6472
6965
  ).style});
6473
6966
  continue;
6474
6967
  }
6475
- let html = node_innerHTML_of(
6476
- nd, theme, highlighted.has(id), selected.has(id), anchored,
6477
- pills_html_of(gobj, nd)
6478
- );
6968
+ let html = (mode === 'compact')
6969
+ ? pill_innerHTML_of(gobj, nd, theme, highlighted.has(id), selected.has(id), anchored)
6970
+ : node_innerHTML_of(
6971
+ nd, theme, highlighted.has(id), selected.has(id), anchored,
6972
+ pills_html_of(gobj, nd)
6973
+ );
6479
6974
  if(html === null) {
6480
6975
  html = more_innerHTML_of(gobj, nd, theme); /* a `+N` chip, or null */
6481
6976
  }
@@ -7185,7 +7680,7 @@ function hide_unlink_confirm(gobj)
7185
7680
  * Apply node properties to one or more nodes.
7186
7681
  * scope: 'this', 'same_topic', 'all'
7187
7682
  ************************************************************/
7188
- function apply_node_properties(gobj, node_id, fill, stroke, lineWidth, scope)
7683
+ function apply_node_properties(gobj, node_id, fill, stroke, lineWidth, scope, shape)
7189
7684
  {
7190
7685
  let priv = gobj.priv;
7191
7686
  let graph = priv.graph;
@@ -7199,6 +7694,23 @@ function apply_node_properties(gobj, node_id, fill, stroke, lineWidth, scope)
7199
7694
  let updates = [];
7200
7695
  const nodes = graph.getData().nodes;
7201
7696
 
7697
+ /* The figure: written where Save reads it (the node's entry in
7698
+ * `__graphs__`), and as the topic's default for the wider
7699
+ * scopes, so a record that arrives later is born with it. */
7700
+ let figure = (shape && str_in_list(NODE_SHAPES, shape))? shape : "";
7701
+ let reshaped = [];
7702
+ if(figure && scope !== 'this') {
7703
+ let topics = (scope === 'same_topic')? [source_topic] : Object.keys(priv.descs || {});
7704
+ for(let topic_name of topics) {
7705
+ if(!is_object(priv._graph_properties[topic_name])) {
7706
+ priv._graph_properties[topic_name] = {};
7707
+ }
7708
+ let defaults = priv._graph_properties[topic_name].defaults || {};
7709
+ defaults.node_shape = figure;
7710
+ priv._graph_properties[topic_name].defaults = defaults;
7711
+ }
7712
+ }
7713
+
7202
7714
  for(let i = 0; i < nodes.length; i++) {
7203
7715
  let nd = graph.getNodeData(nodes[i].id);
7204
7716
  if(!nd || !nd.data || !nd.data.desc) {
@@ -7212,6 +7724,13 @@ function apply_node_properties(gobj, node_id, fill, stroke, lineWidth, scope)
7212
7724
  continue;
7213
7725
  }
7214
7726
 
7727
+ if(figure) {
7728
+ node_props_entry(gobj, nd).node_shape = figure;
7729
+ if(node_mode_of(gobj, nodes[i].id) === 'shape') {
7730
+ reshaped.push(nodes[i].id);
7731
+ }
7732
+ }
7733
+
7215
7734
  let updateStyle = { fill: fill, stroke: stroke, lineWidth: lineWidth };
7216
7735
  if(nd.data.desc.node_treedb_type === 'hierarchical') {
7217
7736
  let record = nd.data.record || {};
@@ -7227,6 +7746,9 @@ function apply_node_properties(gobj, node_id, fill, stroke, lineWidth, scope)
7227
7746
 
7228
7747
  if(updates.length > 0) {
7229
7748
  graph.updateNodeData(updates);
7749
+ if(reshaped.length) {
7750
+ reshape_nodes(gobj, reshaped);
7751
+ }
7230
7752
  graph.draw().then(() => {
7231
7753
  mark_graph_dirty(gobj);
7232
7754
  });
@@ -7235,6 +7757,32 @@ function apply_node_properties(gobj, node_id, fill, stroke, lineWidth, scope)
7235
7757
  hide_node_popover(gobj);
7236
7758
  }
7237
7759
 
7760
+ /************************************************************
7761
+ * The node's entry in `_graph_properties` -- the object Save
7762
+ * reads and writes -- created if it has none yet, and the node's
7763
+ * `data.graph_props` pointed at it, so a key written here is
7764
+ * read by the next reshape and survives the next Save.
7765
+ ************************************************************/
7766
+ function node_props_entry(gobj, nd)
7767
+ {
7768
+ let priv = gobj.priv;
7769
+ let topic_name = nd.data.topic_name;
7770
+ let record = nd.data.record || {};
7771
+
7772
+ if(!is_object(priv._graph_properties[topic_name])) {
7773
+ priv._graph_properties[topic_name] = {};
7774
+ }
7775
+ let topic_props = priv._graph_properties[topic_name];
7776
+ if(!is_object(topic_props.nodes)) {
7777
+ topic_props.nodes = {};
7778
+ }
7779
+ if(!is_object(topic_props.nodes[record.id])) {
7780
+ topic_props.nodes[record.id] = is_object(nd.data.graph_props)? nd.data.graph_props : {};
7781
+ }
7782
+ nd.data.graph_props = topic_props.nodes[record.id];
7783
+ return nd.data.graph_props;
7784
+ }
7785
+
7238
7786
  /************************************************************
7239
7787
  * Apply edge properties to one or more edges.
7240
7788
  * scope: 'this', 'same_type', 'all'
@@ -7426,9 +7974,9 @@ function build_node_context_menu(gobj, node_id)
7426
7974
  /* Open or close THIS node against the mode: the door a finger
7427
7975
  * has to it (a double click is the mouse's). */
7428
7976
  if(node_is_compact(gobj, node_id)) {
7429
- items.push(ctx_item('g6-icon-fullscreen', t('open node'), 'toggle_node_mode'));
7977
+ items.push(ctx_item('g6-icon-fullscreen', t('expand node'), 'toggle_node_mode'));
7430
7978
  } else {
7431
- items.push(ctx_item('g6-icon-fullscreen-exit', t('close node'), 'toggle_node_mode'));
7979
+ items.push(ctx_item('g6-icon-fullscreen-exit', t('collapse node'), 'toggle_node_mode'));
7432
7980
  }
7433
7981
 
7434
7982
  if(gobj.priv.edit_mode && !node_is_compact(gobj, node_id)) {
@@ -7453,6 +8001,9 @@ function build_port_context_menu(gobj, node_id, port_key)
7453
8001
 
7454
8002
  if(gobj.priv.edit_mode) {
7455
8003
  inject_svg_icons();
8004
+ /* The popover, for a finger: the gear beside the port needs
8005
+ * the port selected first, and a long press is the menu. */
8006
+ items.push(ctx_item('g6-icon-edit', t('port properties'), 'port_properties'));
7456
8007
  items.push(ctx_item('g6-icon-resize', t('resize all ports'), 'resize_all_ports'));
7457
8008
  items.push(ctx_item('g6-icon-resize', t('resize topic ports'), 'resize_topic_ports'));
7458
8009
  }
@@ -7495,6 +8046,12 @@ function handle_context_menu_click(gobj, value)
7495
8046
  case 'reset_topic_sizes':
7496
8047
  reset_sizes(gobj, true);
7497
8048
  break;
8049
+ case 'port_properties':
8050
+ if(priv._context_node_id && priv._context_port_key) {
8051
+ select_port(gobj, priv._context_node_id, priv._context_port_key);
8052
+ show_port_popover(gobj);
8053
+ }
8054
+ break;
7498
8055
  case 'resize_all_ports':
7499
8056
  copy_size_to_ports(gobj, false);
7500
8057
  break;
@@ -7581,7 +8138,7 @@ function copy_size_to_nodes(gobj, same_topic_only)
7581
8138
  }
7582
8139
 
7583
8140
  // Recalculate dx/dy for HTML nodes
7584
- if(nd.type === 'html') {
8141
+ if(is_card_type(nd.type)) {
7585
8142
  updateStyle.dx = -source_size[0] / 2;
7586
8143
  let h = source_size.length > 1 ? source_size[1] : source_size[0];
7587
8144
  updateStyle.dy = -h / 2;
@@ -7773,7 +8330,7 @@ function copy_size_to_ports(gobj, same_topic_only)
7773
8330
  /* A card with pills is this much taller; the row sits under the subtitle. */
7774
8331
  const PILL_ROW_HEIGHT = 24;
7775
8332
  const MORE_CHIP_SIZE = [76, 26];
7776
- /* The chip beside CLOSED nodes: a square of 22 next to a chip of 76
8333
+ /* The chip beside pills and figures: a square of 22 next to a chip of 76
7777
8334
  * read as the biggest thing in the row, so it takes the closed
7778
8335
  * scale -- the count and nothing else. */
7779
8336
  const MORE_CHIP_SIZE_COMPACT = [40, 22];
@@ -7787,7 +8344,7 @@ function more_chip_is_compact(gobj, nd)
7787
8344
  if(parent_id) {
7788
8345
  return node_is_compact(gobj, parent_id);
7789
8346
  }
7790
- return gobj_read_str_attr(gobj, "node_mode") === "compact";
8347
+ return gobj_read_str_attr(gobj, "node_mode") !== "expanded";
7791
8348
  }
7792
8349
 
7793
8350
  function more_chip_style(gobj, nd, color, rest)
@@ -8418,6 +8975,7 @@ async function reconcile_fold_apply(gobj, opts, visible)
8418
8975
  update_edge_icon_position(gobj);
8419
8976
  update_node_icon_position(gobj);
8420
8977
  update_link_icon_position(gobj);
8978
+ update_port_icon_position(gobj);
8421
8979
 
8422
8980
  return changed;
8423
8981
  }
@@ -9302,7 +9860,7 @@ function ac_set_loose_topics(gobj, event, kw, src)
9302
9860
  function ac_set_node_mode(gobj, event, kw, src)
9303
9861
  {
9304
9862
  let priv = gobj.priv;
9305
- let mode = (kw && kw.node_mode === "compact")? "compact" : "expanded";
9863
+ let mode = (kw && kw.node_mode && str_in_list(NODE_MODES, kw.node_mode))? kw.node_mode : "expanded";
9306
9864
 
9307
9865
  gobj_write_str_attr(gobj, "node_mode", mode);
9308
9866
  priv._open_nodes = new Set();
@@ -9316,9 +9874,9 @@ function ac_set_node_mode(gobj, event, kw, src)
9316
9874
  }
9317
9875
 
9318
9876
  /************************************************************
9319
- * The name under a closed square, on or off. Only the closed
9320
- * nodes change, and nothing moves: the label hangs outside the
9321
- * square and the layout measures the square.
9877
+ * The name under a figure, on or off. Only the `shape` nodes
9878
+ * change, and nothing moves: the label hangs outside the figure
9879
+ * and the layout measures the figure.
9322
9880
  ************************************************************/
9323
9881
  function ac_set_node_labels(gobj, event, kw, src)
9324
9882
  {
@@ -9328,7 +9886,7 @@ function ac_set_node_labels(gobj, event, kw, src)
9328
9886
  if(!priv.graph) {
9329
9887
  return 0;
9330
9888
  }
9331
- let ids = all_record_node_ids(gobj).filter((id) => node_is_compact(gobj, id));
9889
+ let ids = all_record_node_ids(gobj).filter((id) => node_mode_of(gobj, id) === 'shape');
9332
9890
  reshape_nodes(gobj, ids);
9333
9891
  return 0;
9334
9892
  }