@yuneta/gobj-ui 7.23.81 → 7.23.83

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yuneta/gobj-ui",
3
- "version": "7.23.81",
3
+ "version": "7.23.83",
4
4
  "type": "module",
5
5
  "main": "dist/gobj-ui.cjs.js",
6
6
  "module": "dist/gobj-ui.es.js",
@@ -263,16 +263,34 @@ const PORT_R_CHILD = 8;
263
263
  const PORT_LINE_WIDTH = 2;
264
264
 
265
265
  /*
266
- * The CLOSED shape of a record: a rounded square of the topic's
267
- * colour, no ports, no text -- topology and nothing else. The
268
- * three tiers keep their order of size, so a root is still bigger
269
- * 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.
270
275
  */
271
- 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 = {
272
286
  hierarchical: 32,
273
287
  extended: 28,
274
288
  child: 22,
275
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';
276
294
 
277
295
  /***************************************************************
278
296
  * Internal layout and operation mode definitions
@@ -398,8 +416,8 @@ SDATA(data_type_t.DTP_STRING, "main_topic", 0, "", "The topic t
398
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"),
399
417
 
400
418
  /*---------------- Node shape ----------------*/
401
- 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)"),
402
- 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"),
403
421
  SDATA(data_type_t.DTP_BOOLEAN, "confirm_delete_node", 0, true, "Ask confirmation before deleting a node"),
404
422
  SDATA(data_type_t.DTP_BOOLEAN, "confirm_unlink_edge", 0, true, "Ask confirmation before unlinking an edge"),
405
423
 
@@ -2020,20 +2038,47 @@ function create_topic_node(gobj, desc, record)
2020
2038
  ************************************************************/
2021
2039
  function node_shape_of(gobj, node_id, desc, record, geometry, pills_html)
2022
2040
  {
2023
- if(node_is_compact(gobj, node_id)) {
2024
- 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);
2025
2048
  }
2026
- return card_shape_of(gobj, desc, record, geometry, pills_html);
2027
2049
  }
2028
2050
 
2029
- 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)
2030
2055
  {
2031
2056
  let priv = gobj.priv;
2032
2057
  if(!priv._open_nodes) {
2033
2058
  priv._open_nodes = new Set();
2034
2059
  }
2035
- let compact = (gobj_read_str_attr(gobj, "node_mode") === "compact");
2036
- 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;
2037
2082
  }
2038
2083
 
2039
2084
  /************************************************************
@@ -2115,74 +2160,139 @@ function card_shape_of(gobj, desc, record, geometry, pills_html)
2115
2160
  style.portR = geometry.portR;
2116
2161
  }
2117
2162
 
2118
- if(json_size(ports)) {
2119
- json_object_update_missing(style, {
2120
- port: true,
2121
- ports: ports,
2122
- portR: node_treedb_type === 'child' ? PORT_R_CHILD : PORT_R_ENTITY,
2123
- portLineWidth: PORT_LINE_WIDTH,
2124
- });
2163
+ dress_ports(gobj, style, ports, geometry, topic_defaults,
2164
+ node_treedb_type === 'child' ? PORT_R_CHILD : PORT_R_ENTITY, false);
2125
2165
 
2126
- // Restore per-port radius: saved geometry first, then topic defaults
2127
- let port_sizes = geometry.port_sizes;
2128
- let default_port_sizes = topic_defaults ? topic_defaults.port_sizes : null;
2129
- if(is_object(port_sizes) || is_object(default_port_sizes)) {
2130
- for(let i = 0; i < style.ports.length; i++) {
2131
- let key = style.ports[i].key;
2132
- let r = is_object(port_sizes) ? port_sizes[key] : null;
2133
- if(r == null && is_object(default_port_sizes)) {
2134
- r = default_port_sizes[key];
2135
- }
2136
- if(r != null) {
2137
- style.ports[i].r = r;
2138
- }
2139
- }
2140
- }
2166
+ return {type: 'treedb-card', style: style};
2167
+ }
2141
2168
 
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
- }
2161
- } else {
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)) {
2162
2179
  style.port = false;
2163
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;
2164
2191
  }
2165
2192
 
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);
2166
2250
  return {type: 'treedb-card', style: style};
2167
2251
  }
2168
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;
2275
+ }
2276
+
2169
2277
  /************************************************************
2170
- * The SQUARE: a G6 `rect` of the topic's colour, no ports, the
2171
- * name as a label under it when `node_labels` says so. A native
2172
- * shape and not html on purpose: G6's own stroke and halo paint
2173
- * on it, so the focus, the selection and the anchor are three
2174
- * 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.
2175
2284
  *
2176
2285
  * flags {highlight, selected, anchored}
2177
2286
  ************************************************************/
2178
- function compact_shape_of(gobj, desc, record, flags)
2287
+ function figure_shape_of(gobj, desc, record, geometry, flags)
2179
2288
  {
2180
2289
  let priv = gobj.priv;
2181
2290
  let dark = (priv.theme === "dark");
2182
2291
  let f = flags || {};
2183
2292
  let tier = desc.node_treedb_type;
2184
- let side = COMPACT_SIZE[tier] || COMPACT_SIZE.hierarchical;
2293
+ let side = SHAPE_SIZE[tier] || SHAPE_SIZE.hierarchical;
2185
2294
  let labels = gobj_read_bool_attr(gobj, "node_labels");
2295
+ let figure = node_figure_of(gobj, desc, geometry || {});
2186
2296
 
2187
2297
  let stroke = getStrokeColor(desc.color);
2188
2298
  let line_width = 1.5;
@@ -2207,7 +2317,7 @@ function compact_shape_of(gobj, desc, record, flags)
2207
2317
 
2208
2318
  let style = {
2209
2319
  size: [side, side],
2210
- radius: Math.round(side / 5),
2320
+ radius: (figure === 'square')? Math.round(side / 5) : 0,
2211
2321
  fill: desc.color,
2212
2322
  stroke: stroke,
2213
2323
  lineWidth: line_width,
@@ -2231,7 +2341,7 @@ function compact_shape_of(gobj, desc, record, flags)
2231
2341
  labelBackgroundRadius: 3,
2232
2342
  labelPadding: [1, 3],
2233
2343
  };
2234
- return {type: 'rect', style: style};
2344
+ return {type: (figure === 'square')? 'rect' : figure, style: style};
2235
2345
  }
2236
2346
 
2237
2347
  /************************************************************
@@ -6326,6 +6436,17 @@ function show_node_popover(gobj)
6326
6436
  create_form_label(popover, 'line width');
6327
6437
  let lwInput = create_form_number_input(popover, currentLW, 1, 20, preview_node);
6328
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
+
6329
6450
  // Apply-to scope
6330
6451
  create_form_label(popover, 'apply to');
6331
6452
  let scopeSelect = create_form_select(popover, [
@@ -6363,7 +6484,8 @@ function show_node_popover(gobj)
6363
6484
  fillInput.value,
6364
6485
  strokeInput.value,
6365
6486
  parseInt(lwInput.value) || 1,
6366
- scopeSelect.value
6487
+ scopeSelect.value,
6488
+ shapeSelect.value
6367
6489
  );
6368
6490
  },
6369
6491
  },
@@ -6763,18 +6885,22 @@ function repaint_cards(gobj, ids)
6763
6885
  continue; /* gone since it was listed */
6764
6886
  }
6765
6887
  let anchored = (priv.anchor_state === "on" && id === priv.anchor_id);
6766
- /* A closed node paints its flags on its own stroke. */
6767
- if(nd && nd.data && nd.data.desc && node_is_compact(gobj, id)) {
6768
- updates.push({id: id, style: compact_shape_of(
6769
- 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 || {},
6770
6894
  {highlight: focus.has(id), selected: selected.has(id), anchored: anchored}
6771
6895
  ).style});
6772
6896
  continue;
6773
6897
  }
6774
- let html = node_innerHTML_of(
6775
- nd, priv.theme, focus.has(id), selected.has(id), anchored,
6776
- pills_html_of(gobj, nd)
6777
- );
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
+ );
6778
6904
  if(html !== null) {
6779
6905
  updates.push({id: id, style: {innerHTML: html}});
6780
6906
  }
@@ -6831,17 +6957,20 @@ function refresh_html_nodes_theme(gobj, theme)
6831
6957
  let id = nodes[i].id;
6832
6958
  let nd = graph.getNodeData(id);
6833
6959
  let anchored = (priv.anchor_state === "on" && id === priv.anchor_id);
6834
- if(nd && nd.data && nd.data.desc && node_is_compact(gobj, id)) {
6835
- updates.push({id: id, style: compact_shape_of(
6836
- 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 || {},
6837
6964
  {highlight: highlighted.has(id), selected: selected.has(id), anchored: anchored}
6838
6965
  ).style});
6839
6966
  continue;
6840
6967
  }
6841
- let html = node_innerHTML_of(
6842
- nd, theme, highlighted.has(id), selected.has(id), anchored,
6843
- pills_html_of(gobj, nd)
6844
- );
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
+ );
6845
6974
  if(html === null) {
6846
6975
  html = more_innerHTML_of(gobj, nd, theme); /* a `+N` chip, or null */
6847
6976
  }
@@ -7551,7 +7680,7 @@ function hide_unlink_confirm(gobj)
7551
7680
  * Apply node properties to one or more nodes.
7552
7681
  * scope: 'this', 'same_topic', 'all'
7553
7682
  ************************************************************/
7554
- function apply_node_properties(gobj, node_id, fill, stroke, lineWidth, scope)
7683
+ function apply_node_properties(gobj, node_id, fill, stroke, lineWidth, scope, shape)
7555
7684
  {
7556
7685
  let priv = gobj.priv;
7557
7686
  let graph = priv.graph;
@@ -7565,6 +7694,23 @@ function apply_node_properties(gobj, node_id, fill, stroke, lineWidth, scope)
7565
7694
  let updates = [];
7566
7695
  const nodes = graph.getData().nodes;
7567
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
+
7568
7714
  for(let i = 0; i < nodes.length; i++) {
7569
7715
  let nd = graph.getNodeData(nodes[i].id);
7570
7716
  if(!nd || !nd.data || !nd.data.desc) {
@@ -7578,6 +7724,13 @@ function apply_node_properties(gobj, node_id, fill, stroke, lineWidth, scope)
7578
7724
  continue;
7579
7725
  }
7580
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
+
7581
7734
  let updateStyle = { fill: fill, stroke: stroke, lineWidth: lineWidth };
7582
7735
  if(nd.data.desc.node_treedb_type === 'hierarchical') {
7583
7736
  let record = nd.data.record || {};
@@ -7593,6 +7746,9 @@ function apply_node_properties(gobj, node_id, fill, stroke, lineWidth, scope)
7593
7746
 
7594
7747
  if(updates.length > 0) {
7595
7748
  graph.updateNodeData(updates);
7749
+ if(reshaped.length) {
7750
+ reshape_nodes(gobj, reshaped);
7751
+ }
7596
7752
  graph.draw().then(() => {
7597
7753
  mark_graph_dirty(gobj);
7598
7754
  });
@@ -7601,6 +7757,32 @@ function apply_node_properties(gobj, node_id, fill, stroke, lineWidth, scope)
7601
7757
  hide_node_popover(gobj);
7602
7758
  }
7603
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
+
7604
7786
  /************************************************************
7605
7787
  * Apply edge properties to one or more edges.
7606
7788
  * scope: 'this', 'same_type', 'all'
@@ -7792,9 +7974,9 @@ function build_node_context_menu(gobj, node_id)
7792
7974
  /* Open or close THIS node against the mode: the door a finger
7793
7975
  * has to it (a double click is the mouse's). */
7794
7976
  if(node_is_compact(gobj, node_id)) {
7795
- 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'));
7796
7978
  } else {
7797
- 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'));
7798
7980
  }
7799
7981
 
7800
7982
  if(gobj.priv.edit_mode && !node_is_compact(gobj, node_id)) {
@@ -8148,7 +8330,7 @@ function copy_size_to_ports(gobj, same_topic_only)
8148
8330
  /* A card with pills is this much taller; the row sits under the subtitle. */
8149
8331
  const PILL_ROW_HEIGHT = 24;
8150
8332
  const MORE_CHIP_SIZE = [76, 26];
8151
- /* 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
8152
8334
  * read as the biggest thing in the row, so it takes the closed
8153
8335
  * scale -- the count and nothing else. */
8154
8336
  const MORE_CHIP_SIZE_COMPACT = [40, 22];
@@ -8162,7 +8344,7 @@ function more_chip_is_compact(gobj, nd)
8162
8344
  if(parent_id) {
8163
8345
  return node_is_compact(gobj, parent_id);
8164
8346
  }
8165
- return gobj_read_str_attr(gobj, "node_mode") === "compact";
8347
+ return gobj_read_str_attr(gobj, "node_mode") !== "expanded";
8166
8348
  }
8167
8349
 
8168
8350
  function more_chip_style(gobj, nd, color, rest)
@@ -9678,7 +9860,7 @@ function ac_set_loose_topics(gobj, event, kw, src)
9678
9860
  function ac_set_node_mode(gobj, event, kw, src)
9679
9861
  {
9680
9862
  let priv = gobj.priv;
9681
- 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";
9682
9864
 
9683
9865
  gobj_write_str_attr(gobj, "node_mode", mode);
9684
9866
  priv._open_nodes = new Set();
@@ -9692,9 +9874,9 @@ function ac_set_node_mode(gobj, event, kw, src)
9692
9874
  }
9693
9875
 
9694
9876
  /************************************************************
9695
- * The name under a closed square, on or off. Only the closed
9696
- * nodes change, and nothing moves: the label hangs outside the
9697
- * 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.
9698
9880
  ************************************************************/
9699
9881
  function ac_set_node_labels(gobj, event, kw, src)
9700
9882
  {
@@ -9704,7 +9886,7 @@ function ac_set_node_labels(gobj, event, kw, src)
9704
9886
  if(!priv.graph) {
9705
9887
  return 0;
9706
9888
  }
9707
- 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');
9708
9890
  reshape_nodes(gobj, ids);
9709
9891
  return 0;
9710
9892
  }