@yuneta/gobj-ui 7.23.83 → 7.23.85

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.
@@ -58,6 +58,7 @@ import {
58
58
  gobj_short_name,
59
59
  gobj_is_destroying,
60
60
  gobj_subscribe_event,
61
+ gobj_unsubscribe_event,
61
62
  gobj_publish_event,
62
63
  gobj_send_event,
63
64
  gobj_post_event,
@@ -143,7 +144,7 @@ import {
143
144
  } from '@antv/g6';
144
145
 
145
146
  import {Circle as CircleGeometry, Rect as RectGeometry} from '@antv/g';
146
- import i18next, {t} from "i18next";
147
+ import {t} from "i18next";
147
148
 
148
149
  import {inject_svg_icons} from "./lib_icons.js";
149
150
  import {ensure_drag_canvas_patch} from "./g6_drag_canvas_touch.js";
@@ -157,6 +158,7 @@ import {
157
158
  consume_long_press_click,
158
159
  } from "./g6_touch_gestures.js";
159
160
  import {yui_theme_now, yui_watch_theme} from "./yui_theme.js";
161
+ import {yui_shell_of} from "./c_yui_shell.js";
160
162
 
161
163
  /***************************************************************
162
164
  * YuiToolbar — G6 Toolbar subclass that adds per-item className
@@ -465,11 +467,14 @@ let PRIVATE_DATA = {
465
467
  _edge_icon_el: null, // floating properties icon element
466
468
  _edge_delete_el: null, // floating delete icon element for edge
467
469
  _edge_popover_el: null, // edge properties popover element
470
+ _edge_preview_undo: null, // ...and how to undo what it previewed
468
471
  _port_icon_el: null, // floating port properties icon element
469
472
  _port_popover_el: null, // port properties popover element
473
+ _port_preview_undo: null, // ...and how to undo what it previewed
470
474
  _node_icon_el: null, // floating node properties icon element
471
475
  _node_delete_el: null, // floating delete icon element for node
472
476
  _node_popover_el: null, // node properties popover element
477
+ _node_preview_undo: null, // ...and how to undo what it previewed
473
478
  _node_detail_el: null, // read-only detail popover (on click)
474
479
  _detail_node_id: null, // node id the detail popover is showing
475
480
  _delete_confirm_el: null, // delete confirmation popover
@@ -613,6 +618,18 @@ function mt_writing(gobj, path)
613
618
  ***************************************************************/
614
619
  function mt_start(gobj)
615
620
  {
621
+ /* A language change reaches the toolbars and the `+N` chips as
622
+ * the shell's EVENT, not as a raw i18next listener: what the
623
+ * toolbar draws and what a chip says are built here, out of
624
+ * reach of refresh_language(), so somebody has to repaint them
625
+ * -- and that somebody is an action of this machine, where the
626
+ * `machine` trace can see it. Same wiring as C_YUI_PERIOD and
627
+ * C_YUI_GOBJ_TREE_JS. */
628
+ let shell = yui_shell_of(gobj);
629
+ if(shell) {
630
+ gobj_subscribe_event(shell, "EV_LANGUAGE_CHANGED", {}, gobj);
631
+ }
632
+
616
633
  return 0;
617
634
  }
618
635
 
@@ -621,6 +638,11 @@ function mt_start(gobj)
621
638
  ***************************************************************/
622
639
  function mt_stop(gobj)
623
640
  {
641
+ let shell = yui_shell_of(gobj);
642
+ if(shell) {
643
+ gobj_unsubscribe_event(shell, "EV_LANGUAGE_CHANGED", {}, gobj);
644
+ }
645
+
624
646
  return 0;
625
647
  }
626
648
 
@@ -631,11 +653,6 @@ function mt_destroy(gobj)
631
653
  {
632
654
  let priv = gobj.priv;
633
655
 
634
- if(priv._on_language_changed) {
635
- i18next.off('languageChanged', priv._on_language_changed);
636
- priv._on_language_changed = null;
637
- }
638
-
639
656
  uninstall_fold_listener(gobj);
640
657
 
641
658
  if(priv._on_pointerdown_focus) {
@@ -1022,12 +1039,6 @@ function configure_events(gobj)
1022
1039
  }
1023
1040
  });
1024
1041
 
1025
- // Re-render G6 toolbars (and the `+N` chips' titles) when language changes
1026
- priv._on_language_changed = () => {
1027
- update_toolbar(gobj);
1028
- repaint_more_chips(gobj);
1029
- };
1030
- i18next.on('languageChanged', priv._on_language_changed);
1031
1042
 
1032
1043
  /* Clicking a CARD is clicking a DOM element inside the container,
1033
1044
  * and that takes the keyboard away from G6's canvas -- the only
@@ -1865,6 +1876,17 @@ const LR_LAYOUTS = ["dagre"];
1865
1876
  * toolbar's +/-, and the two-finger pinch of g6_touch_gestures.
1866
1877
  * `enable` on the scroll keeps the two apart: a Ctrl wheel is
1867
1878
  * the zoom's, so the scroll stands aside for it.
1879
+ *
1880
+ * And the two have to name the SAME key, or the gesture falls
1881
+ * between them: the scroll stood aside for Meta as well, while
1882
+ * the zoom listened for Control alone, so Cmd + wheel neither
1883
+ * scrolled nor zoomed. It is ONE key and not two because a G6
1884
+ * `trigger` is a CHORD, not a list of alternatives -- its
1885
+ * `Shortcut.match()` compares the held keys to the bound ones
1886
+ * as a SET, so ["Control", "Meta"] means both keys down at
1887
+ * once. Control it is, on every platform: it is what a
1888
+ * trackpad pinch arrives as, so a Mac keeps its pinch and
1889
+ * gains nothing to press by mistake.
1868
1890
  ************************************************************/
1869
1891
  function camera_behaviors()
1870
1892
  {
@@ -1872,7 +1894,7 @@ function camera_behaviors()
1872
1894
  {
1873
1895
  type: "scroll-canvas",
1874
1896
  key: "scroll-canvas",
1875
- enable: (event) => !(event && (event.ctrlKey || event.metaKey)),
1897
+ enable: (event) => !(event && event.ctrlKey),
1876
1898
  },
1877
1899
  {
1878
1900
  type: "zoom-canvas",
@@ -2081,6 +2103,54 @@ function topic_defaults_of(gobj, topic_name)
2081
2103
  topic_props.defaults : null;
2082
2104
  }
2083
2105
 
2106
+ /************************************************************
2107
+ * What a node is PAINTED with: its own colours, then the
2108
+ * topic's default, then the topic's colour.
2109
+ *
2110
+ * A card is html, and its html was built with `desc.color`
2111
+ * every time it was repainted -- so a colour chosen in the node
2112
+ * properties popover lasted until the next selection, focus,
2113
+ * theme or pill change, and was never written to `__graphs__`
2114
+ * at all. Only the `shape` view kept it, because a figure is a
2115
+ * native G6 node and `style.fill` is what paints it. The FIGURE
2116
+ * was already remembered (`node_shape`); this is the wiring the
2117
+ * colour was missing.
2118
+ ************************************************************/
2119
+ function node_paint_of(gobj, desc, geometry)
2120
+ {
2121
+ let defaults = topic_defaults_of(gobj, desc.topic_name);
2122
+ let g = is_object(geometry)? geometry : {};
2123
+ let fill = g.fill || (defaults && defaults.fill) || desc.color;
2124
+ let stroke = g.stroke || (defaults && defaults.stroke) || getStrokeColor(fill);
2125
+ let line_width = g.lineWidth || (defaults && defaults.lineWidth) || 1;
2126
+ return {fill: fill, stroke: stroke, lineWidth: line_width};
2127
+ }
2128
+
2129
+ /************************************************************
2130
+ * Write a chosen paint into an entry of `__graphs__` -- a
2131
+ * node's or a topic's defaults.
2132
+ *
2133
+ * A colour nobody chose is not saved: the topic's own colour,
2134
+ * with the stroke and the line width that go with it, is what a
2135
+ * card wears anyway, and writing it down would freeze today's
2136
+ * palette on that card -- the trap the SIZES fell into in
2137
+ * 7.23.80, and the palette is assigned by POSITION (`ac_descs`),
2138
+ * so it moves when a topic is added. Choosing it back MEANS
2139
+ * forgetting the entry.
2140
+ ************************************************************/
2141
+ function paint_into(entry, desc, fill, stroke, line_width)
2142
+ {
2143
+ if(fill === desc.color && stroke === getStrokeColor(desc.color) && line_width === 1) {
2144
+ delete entry.fill;
2145
+ delete entry.stroke;
2146
+ delete entry.lineWidth;
2147
+ return;
2148
+ }
2149
+ entry.fill = fill;
2150
+ entry.stroke = stroke;
2151
+ entry.lineWidth = line_width;
2152
+ }
2153
+
2084
2154
  /************************************************************
2085
2155
  * The CARD: an html node, the tier's size, the ports, the
2086
2156
  * saved geometry on top. The flags (focus, selection, anchor)
@@ -2093,11 +2163,12 @@ function card_shape_of(gobj, desc, record, geometry, pills_html)
2093
2163
  let node_treedb_type = desc.node_treedb_type;
2094
2164
  let label = node_label(desc, record);
2095
2165
  let ports = build_ports(gobj, desc);
2166
+ let paint = node_paint_of(gobj, desc, geometry);
2096
2167
 
2097
2168
  let style = {
2098
- fill: desc.color,
2099
- stroke: getStrokeColor(desc.color),
2100
- lineWidth: 1,
2169
+ fill: paint.fill,
2170
+ stroke: paint.stroke,
2171
+ lineWidth: paint.lineWidth,
2101
2172
  lineDash: [],
2102
2173
  halo: false,
2103
2174
  labelText: "", /* a card says its name inside; the label is the square's */
@@ -2114,20 +2185,20 @@ function card_shape_of(gobj, desc, record, geometry, pills_html)
2114
2185
  // Pure child (LEAF): smallest tier. Rounded-rect chip,
2115
2186
  // same card language as entities, lighter.
2116
2187
  style.innerHTML = build_chip_innerHTML(
2117
- desc.color, priv.theme, record.icon, label, record.id
2188
+ paint.fill, priv.theme, record.icon, label, record.id
2118
2189
  );
2119
2190
  } else if(node_treedb_type === 'extended') {
2120
2191
  // Extended (structural / INTERMEDIATE): middle tier. Card
2121
2192
  // with name; "structural" style (neutral grey, dashed
2122
2193
  // border) to read as a container/junction.
2123
2194
  style.innerHTML = build_node_innerHTML(
2124
- desc.color, priv.theme, record.icon, label,
2195
+ paint.fill, priv.theme, record.icon, label,
2125
2196
  desc.topic_name, true, record.id, false, false, false, pills_html
2126
2197
  );
2127
2198
  } else {
2128
2199
  // Hierarchical entity (ROOT / container): largest tier.
2129
2200
  style.innerHTML = build_node_innerHTML(
2130
- desc.color, priv.theme, record.icon, label,
2201
+ paint.fill, priv.theme, record.icon, label,
2131
2202
  desc.topic_name, false, record.id, false, false, false, pills_html
2132
2203
  );
2133
2204
  }
@@ -2230,11 +2301,12 @@ function pill_shape_of(gobj, desc, record, geometry)
2230
2301
  let tier = desc.node_treedb_type;
2231
2302
  let size = PILL_SIZE[tier] || PILL_SIZE.hierarchical;
2232
2303
  let ports = build_ports(gobj, desc);
2304
+ let paint = node_paint_of(gobj, desc, geometry);
2233
2305
 
2234
2306
  let style = {
2235
- fill: desc.color,
2236
- stroke: getStrokeColor(desc.color),
2237
- lineWidth: 1,
2307
+ fill: paint.fill,
2308
+ stroke: paint.stroke,
2309
+ lineWidth: paint.lineWidth,
2238
2310
  lineDash: [],
2239
2311
  halo: false,
2240
2312
  labelText: "",
@@ -2242,7 +2314,7 @@ function pill_shape_of(gobj, desc, record, geometry)
2242
2314
  dx: -size[0] / 2,
2243
2315
  dy: -size[1] / 2,
2244
2316
  innerHTML: build_chip_innerHTML(
2245
- desc.color, priv.theme, record.icon, node_label(desc, record), record.id
2317
+ paint.fill, priv.theme, record.icon, node_label(desc, record), record.id
2246
2318
  ),
2247
2319
  };
2248
2320
  dress_ports(gobj, style, ports, geometry, topic_defaults_of(gobj, desc.topic_name),
@@ -2256,7 +2328,7 @@ function pill_innerHTML_of(gobj, nd, theme, highlight, selected, anchored)
2256
2328
  let desc = nd.data.desc;
2257
2329
  let record = nd.data.record || {};
2258
2330
  return build_chip_innerHTML(
2259
- desc.color, theme, record.icon, node_label(desc, record), record.id,
2331
+ node_fill_of(nd), theme, record.icon, node_label(desc, record), record.id,
2260
2332
  highlight, selected, anchored
2261
2333
  );
2262
2334
  }
@@ -2293,9 +2365,10 @@ function figure_shape_of(gobj, desc, record, geometry, flags)
2293
2365
  let side = SHAPE_SIZE[tier] || SHAPE_SIZE.hierarchical;
2294
2366
  let labels = gobj_read_bool_attr(gobj, "node_labels");
2295
2367
  let figure = node_figure_of(gobj, desc, geometry || {});
2368
+ let paint = node_paint_of(gobj, desc, geometry);
2296
2369
 
2297
- let stroke = getStrokeColor(desc.color);
2298
- let line_width = 1.5;
2370
+ let stroke = paint.stroke;
2371
+ let line_width = Math.max(paint.lineWidth, 1.5);
2299
2372
  if(f.selected) {
2300
2373
  stroke = SELECT_RING;
2301
2374
  line_width = 3;
@@ -2318,7 +2391,7 @@ function figure_shape_of(gobj, desc, record, geometry, flags)
2318
2391
  let style = {
2319
2392
  size: [side, side],
2320
2393
  radius: (figure === 'square')? Math.round(side / 5) : 0,
2321
- fill: desc.color,
2394
+ fill: paint.fill,
2322
2395
  stroke: stroke,
2323
2396
  lineWidth: line_width,
2324
2397
  lineDash: dash,
@@ -4046,6 +4119,42 @@ function hide_overlay(gobj, ...keys)
4046
4119
  /************************************************************
4047
4120
  * Enable the save button (mark graph as dirty).
4048
4121
  ************************************************************/
4122
+ /************************************************************
4123
+ * A popover that PREVIEWS its changes owes an undo.
4124
+ *
4125
+ * What the preview writes is not the reader's answer until
4126
+ * `apply` is pressed, and `cancel` used to be the only way
4127
+ * back: every other way out of the popover -- a click on the
4128
+ * canvas, the icon pressed again, the element going away --
4129
+ * left the preview standing on the element, and the next Save
4130
+ * wrote it to `__graphs__`. So the undo is kept beside the
4131
+ * popover and run by whoever hides it; `apply` drops it first,
4132
+ * because then the preview IS the answer.
4133
+ ************************************************************/
4134
+ function set_preview_undo(gobj, key, undo)
4135
+ {
4136
+ gobj.priv[key] = undo;
4137
+ }
4138
+
4139
+ function drop_preview_undo(gobj, key)
4140
+ {
4141
+ gobj.priv[key] = null;
4142
+ }
4143
+
4144
+ function run_preview_undo(gobj, key)
4145
+ {
4146
+ let undo = gobj.priv[key];
4147
+ gobj.priv[key] = null;
4148
+ if(!undo || gobj_is_destroying(gobj)) {
4149
+ return;
4150
+ }
4151
+ try {
4152
+ undo();
4153
+ } catch(e) {
4154
+ log_error(`${gobj_short_name(gobj)}: cannot undo the preview: ${e}`);
4155
+ }
4156
+ }
4157
+
4049
4158
  function mark_graph_dirty(gobj)
4050
4159
  {
4051
4160
  let $container = gobj_read_attr(gobj, "$container");
@@ -4990,6 +5099,34 @@ function port_hit_slop_world(graph)
4990
5099
  return slop / zoom;
4991
5100
  }
4992
5101
 
5102
+ /************************************************************
5103
+ * Whether a point landed on a port, by the port's SHAPE.
5104
+ *
5105
+ * A port was a circle and the test was a radius; since 7.23.81
5106
+ * it can be a square, a diamond or a triangle too, and a circle
5107
+ * around a diamond answers for the air off its corners while
5108
+ * missing the middle of its edges. `r` is the drawn radius with
5109
+ * the hit slop already added, and the shapes are the ones
5110
+ * TreedbCard.drawPortShapes() draws from it.
5111
+ *
5112
+ * dx, dy the point, relative to the port's centre
5113
+ ************************************************************/
5114
+ function port_shape_hit(shape, dx, dy, r)
5115
+ {
5116
+ switch(shape) {
5117
+ case 'square':
5118
+ return Math.abs(dx) <= r && Math.abs(dy) <= r;
5119
+ case 'diamond':
5120
+ return Math.abs(dx) + Math.abs(dy) <= r;
5121
+ case 'triangle':
5122
+ /* (0,-r), (r,r), (-r,r): under the base and inside the
5123
+ * two slanted sides, which meet at 2|x| - y = r. */
5124
+ return dy <= r && (2 * Math.abs(dx) - dy) <= r;
5125
+ default:
5126
+ return Math.hypot(dx, dy) <= r;
5127
+ }
5128
+ }
5129
+
4993
5130
  /************************************************************
4994
5131
  * Detect if a click in canvas coordinates hits a port.
4995
5132
  * Returns the port key string, or null if no port hit.
@@ -5021,8 +5158,9 @@ function detect_port_click(gobj, node_id, canvasX, canvasY)
5021
5158
  let dist = Math.sqrt(dx * dx + dy * dy);
5022
5159
 
5023
5160
  /* Nearest wins, so the enlarged areas of two ports
5024
- * overlapping is not a tie -- it is the closer one. */
5025
- if(dist <= r + slop && dist < best_dist) {
5161
+ * overlapping is not a tie -- it is the closer one. The
5162
+ * distance ranks them; the SHAPE says who is in the running. */
5163
+ if(port_shape_hit(ports[i].shape, dx, dy, r + slop) && dist < best_dist) {
5026
5164
  best_dist = dist;
5027
5165
  best_key = ports[i].key;
5028
5166
  }
@@ -5240,6 +5378,7 @@ function toggle_port_popover(gobj)
5240
5378
 
5241
5379
  function hide_port_popover(gobj)
5242
5380
  {
5381
+ run_preview_undo(gobj, '_port_preview_undo');
5243
5382
  hide_overlay(gobj, '_port_popover_el');
5244
5383
  }
5245
5384
 
@@ -5280,6 +5419,17 @@ function show_port_popover(gobj)
5280
5419
  xy.x + floating_popover_dx(), xy.y, 'g6-port-popover', '#d9d9d9', 180
5281
5420
  );
5282
5421
 
5422
+ /* What the preview writes on this port, undone: whoever hides
5423
+ * the popover without applying runs it (see set_preview_undo). */
5424
+ let undo_port = () => {
5425
+ graph.updateNodeData([{id: node_id, style: {
5426
+ ports: ports_with(nodeData.style.ports, port_key, {shape: orig_shape, r: orig_r}),
5427
+ }}]);
5428
+ graph.draw().then(() => {
5429
+ update_port_resize_handles_position(gobj);
5430
+ });
5431
+ };
5432
+
5283
5433
  /* Live preview on THIS port; the others wait for apply. */
5284
5434
  function preview_port() {
5285
5435
  let r = parseInt(rInput.value) || orig_r;
@@ -5313,13 +5463,7 @@ function show_port_popover(gobj)
5313
5463
  text: 'cancel',
5314
5464
  style: BTN_STYLE_CANCEL,
5315
5465
  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);
5466
+ hide_port_popover(gobj); /* which undoes the preview */
5323
5467
  },
5324
5468
  },
5325
5469
  {
@@ -5327,6 +5471,7 @@ function show_port_popover(gobj)
5327
5471
  style: 'flex:1;padding:6px;background:#1890ff;color:#fff;border:none;' +
5328
5472
  'border-radius:4px;cursor:pointer;font-size:13px;font-weight:500;',
5329
5473
  onClick: () => {
5474
+ drop_preview_undo(gobj, '_port_preview_undo');
5330
5475
  apply_port_properties(gobj, node_id, port_key,
5331
5476
  shapeSelect.value, parseInt(rInput.value) || orig_r, scopeSelect.value
5332
5477
  );
@@ -5336,6 +5481,7 @@ function show_port_popover(gobj)
5336
5481
 
5337
5482
  priv.$container.appendChild(popover);
5338
5483
  priv._port_popover_el = popover;
5484
+ set_preview_undo(gobj, '_port_preview_undo', undo_port);
5339
5485
  clamp_popover_position(gobj, popover);
5340
5486
  }
5341
5487
 
@@ -6184,6 +6330,12 @@ function show_edge_popover(gobj)
6184
6330
  'g6-edge-popover', '#d9d9d9', 180
6185
6331
  );
6186
6332
 
6333
+ /* What the preview writes, undone (see set_preview_undo). */
6334
+ let undo_edge = () => {
6335
+ graph.updateEdgeData([{ id: edge_id, style: { lineWidth: origLW, stroke: origStroke } }]);
6336
+ graph.draw();
6337
+ };
6338
+
6187
6339
  // Live preview: update the selected edge in real time
6188
6340
  function preview_edge() {
6189
6341
  let lw = parseInt(lwInput.value) || 2;
@@ -6213,9 +6365,7 @@ function show_edge_popover(gobj)
6213
6365
  text: 'cancel',
6214
6366
  style: BTN_STYLE_CANCEL,
6215
6367
  onClick: () => {
6216
- graph.updateEdgeData([{ id: edge_id, style: { lineWidth: origLW, stroke: origStroke } }]);
6217
- graph.draw();
6218
- deselect_edge(gobj);
6368
+ deselect_edge(gobj); /* which undoes the preview */
6219
6369
  },
6220
6370
  },
6221
6371
  {
@@ -6223,6 +6373,7 @@ function show_edge_popover(gobj)
6223
6373
  style: 'flex:1;padding:6px;background:#52c41a;color:#fff;border:none;' +
6224
6374
  'border-radius:4px;cursor:pointer;font-size:13px;font-weight:500;',
6225
6375
  onClick: () => {
6376
+ drop_preview_undo(gobj, '_edge_preview_undo');
6226
6377
  apply_edge_properties(gobj, edge_id,
6227
6378
  parseInt(lwInput.value) || 2,
6228
6379
  colorInput.value,
@@ -6234,6 +6385,7 @@ function show_edge_popover(gobj)
6234
6385
 
6235
6386
  priv.$container.appendChild(popover);
6236
6387
  priv._edge_popover_el = popover;
6388
+ set_preview_undo(gobj, '_edge_preview_undo', undo_edge);
6237
6389
 
6238
6390
  // Clamp popover inside the container
6239
6391
  clamp_popover_position(gobj, popover);
@@ -6278,6 +6430,7 @@ function clamp_popover_position(gobj, popover)
6278
6430
 
6279
6431
  function hide_edge_popover(gobj)
6280
6432
  {
6433
+ run_preview_undo(gobj, '_edge_preview_undo');
6281
6434
  hide_overlay(gobj, '_edge_popover_el');
6282
6435
  }
6283
6436
 
@@ -6401,27 +6554,26 @@ function show_node_popover(gobj)
6401
6554
  'g6-node-popover', '#d9d9d9', 180
6402
6555
  );
6403
6556
 
6404
- let node_graph_type = nodeData.data && nodeData.data.desc ?
6405
- nodeData.data.desc.node_treedb_type : null;
6557
+ /* Paint the node with these colours, whatever it is wearing:
6558
+ * the style for a figure, and the html of a card or a pill,
6559
+ * which repaint_cards() rebuilds from the style it is given
6560
+ * here (node_fill_of). */
6561
+ let paint_node = (fill, stroke, lw) => {
6562
+ graph.updateNodeData([{
6563
+ id: node_id, style: {fill: fill, stroke: stroke, lineWidth: lw},
6564
+ }]);
6565
+ repaint_cards(gobj, new Set([node_id]));
6566
+ graph.draw();
6567
+ };
6568
+
6569
+ /* What the preview writes, undone (see set_preview_undo). */
6570
+ let undo_node = () => {
6571
+ paint_node(origFill, origStroke, origLW);
6572
+ };
6406
6573
 
6407
6574
  // Live preview
6408
6575
  function preview_node() {
6409
- let fill = fillInput.value;
6410
- let stroke = strokeInput.value;
6411
- let lw = parseInt(lwInput.value) || 1;
6412
- let updateStyle = { fill: fill, stroke: stroke, lineWidth: lw };
6413
-
6414
- if(node_graph_type === 'hierarchical') {
6415
- let record = nodeData.data.record || {};
6416
- updateStyle.innerHTML = build_node_innerHTML(
6417
- fill, priv.theme, record.icon,
6418
- node_label(nodeData.data.desc, record),
6419
- nodeData.data.desc.topic_name, false, record.id,
6420
- false, false, false, pills_html_of(gobj, nodeData)
6421
- );
6422
- }
6423
- graph.updateNodeData([{ id: node_id, style: updateStyle }]);
6424
- graph.draw();
6576
+ paint_node(fillInput.value, strokeInput.value, parseInt(lwInput.value) || 1);
6425
6577
  }
6426
6578
 
6427
6579
  // Fill color
@@ -6460,19 +6612,7 @@ function show_node_popover(gobj)
6460
6612
  text: 'cancel',
6461
6613
  style: BTN_STYLE_CANCEL,
6462
6614
  onClick: () => {
6463
- let restoreStyle = { fill: origFill, stroke: origStroke, lineWidth: origLW };
6464
- if(node_graph_type === 'hierarchical') {
6465
- let record = nodeData.data.record || {};
6466
- restoreStyle.innerHTML = build_node_innerHTML(
6467
- origFill, priv.theme, record.icon,
6468
- node_label(nodeData.data.desc, record),
6469
- nodeData.data.desc.topic_name, false, record.id,
6470
- false, false, false, pills_html_of(gobj, nodeData)
6471
- );
6472
- }
6473
- graph.updateNodeData([{ id: node_id, style: restoreStyle }]);
6474
- graph.draw();
6475
- hide_node_popover(gobj);
6615
+ hide_node_popover(gobj); /* which undoes the preview */
6476
6616
  },
6477
6617
  },
6478
6618
  {
@@ -6480,6 +6620,7 @@ function show_node_popover(gobj)
6480
6620
  style: 'flex:1;padding:6px;background:#1890ff;color:#fff;border:none;' +
6481
6621
  'border-radius:4px;cursor:pointer;font-size:13px;font-weight:500;',
6482
6622
  onClick: () => {
6623
+ drop_preview_undo(gobj, '_node_preview_undo');
6483
6624
  apply_node_properties(gobj, node_id,
6484
6625
  fillInput.value,
6485
6626
  strokeInput.value,
@@ -6493,6 +6634,7 @@ function show_node_popover(gobj)
6493
6634
 
6494
6635
  priv.$container.appendChild(popover);
6495
6636
  priv._node_popover_el = popover;
6637
+ set_preview_undo(gobj, '_node_preview_undo', undo_node);
6496
6638
 
6497
6639
  // Clamp popover inside the container
6498
6640
  clamp_popover_position(gobj, popover);
@@ -6500,6 +6642,7 @@ function show_node_popover(gobj)
6500
6642
 
6501
6643
  function hide_node_popover(gobj)
6502
6644
  {
6645
+ run_preview_undo(gobj, '_node_preview_undo');
6503
6646
  hide_overlay(gobj, '_node_popover_el');
6504
6647
  }
6505
6648
 
@@ -6824,6 +6967,16 @@ function refresh_minimap(gobj)
6824
6967
  * same three-way choice — it lived inline in the theme refresh and is
6825
6968
  * shared now. Returns null for a node that carries no desc.
6826
6969
  ************************************************************/
6970
+ /* The colour a node is WEARING, from its live style: the shape
6971
+ * functions put the node's own paint there when it has one (see
6972
+ * node_paint_of), and a repaint has to keep it -- rebuilding the
6973
+ * html from `desc.color` is what threw away every colour applied
6974
+ * to a card at the first selection. */
6975
+ function node_fill_of(nd)
6976
+ {
6977
+ return (nd && nd.style && nd.style.fill) || nd.data.desc.color;
6978
+ }
6979
+
6827
6980
  function node_innerHTML_of(nd, theme, highlight, selected, anchored, pills_html)
6828
6981
  {
6829
6982
  if(!nd || !nd.data || !nd.data.desc) {
@@ -6832,22 +6985,23 @@ function node_innerHTML_of(nd, theme, highlight, selected, anchored, pills_html)
6832
6985
  let desc = nd.data.desc;
6833
6986
  let record = nd.data.record || {};
6834
6987
  let label = node_label(desc, record);
6988
+ let fill = node_fill_of(nd);
6835
6989
 
6836
6990
  switch(desc.node_treedb_type) {
6837
6991
  case 'child':
6838
6992
  return build_chip_innerHTML(
6839
- desc.color, theme, record.icon, label, record.id,
6993
+ fill, theme, record.icon, label, record.id,
6840
6994
  highlight, selected, anchored
6841
6995
  );
6842
6996
  case 'extended':
6843
6997
  return build_node_innerHTML(
6844
- desc.color, theme, record.icon, label,
6998
+ fill, theme, record.icon, label,
6845
6999
  desc.topic_name, true, record.id, highlight, selected, anchored,
6846
7000
  pills_html
6847
7001
  );
6848
7002
  case 'hierarchical':
6849
7003
  return build_node_innerHTML(
6850
- desc.color, theme, record.icon, label,
7004
+ fill, theme, record.icon, label,
6851
7005
  desc.topic_name, false, record.id, highlight, selected, anchored,
6852
7006
  pills_html
6853
7007
  );
@@ -7692,21 +7846,33 @@ function apply_node_properties(gobj, node_id, fill, stroke, lineWidth, scope, sh
7692
7846
 
7693
7847
  let source_topic = nodeData.data.desc ? nodeData.data.desc.topic_name : null;
7694
7848
  let updates = [];
7849
+ let repaint = new Set();
7695
7850
  const nodes = graph.getData().nodes;
7696
7851
 
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. */
7852
+ /* The figure AND the colours: written where Save reads them (the
7853
+ * node's entry in `__graphs__`), and as the topic's default for
7854
+ * the wider scopes, so a record that arrives later is born with
7855
+ * them. The colours used to be written on the live style alone,
7856
+ * which on an html card is under the html: the first repaint
7857
+ * rebuilt that html from the topic's colour and the choice was
7858
+ * gone, and nothing of it ever reached the store. */
7700
7859
  let figure = (shape && str_in_list(NODE_SHAPES, shape))? shape : "";
7701
7860
  let reshaped = [];
7702
- if(figure && scope !== 'this') {
7861
+ if(scope !== 'this') {
7703
7862
  let topics = (scope === 'same_topic')? [source_topic] : Object.keys(priv.descs || {});
7704
7863
  for(let topic_name of topics) {
7864
+ let desc = priv.descs[topic_name];
7865
+ if(!desc) {
7866
+ continue;
7867
+ }
7705
7868
  if(!is_object(priv._graph_properties[topic_name])) {
7706
7869
  priv._graph_properties[topic_name] = {};
7707
7870
  }
7708
7871
  let defaults = priv._graph_properties[topic_name].defaults || {};
7709
- defaults.node_shape = figure;
7872
+ if(figure) {
7873
+ defaults.node_shape = figure;
7874
+ }
7875
+ paint_into(defaults, desc, fill, stroke, lineWidth);
7710
7876
  priv._graph_properties[topic_name].defaults = defaults;
7711
7877
  }
7712
7878
  }
@@ -7724,24 +7890,21 @@ function apply_node_properties(gobj, node_id, fill, stroke, lineWidth, scope, sh
7724
7890
  continue;
7725
7891
  }
7726
7892
 
7893
+ let entry = node_props_entry(gobj, nd);
7727
7894
  if(figure) {
7728
- node_props_entry(gobj, nd).node_shape = figure;
7895
+ entry.node_shape = figure;
7729
7896
  if(node_mode_of(gobj, nodes[i].id) === 'shape') {
7730
7897
  reshaped.push(nodes[i].id);
7731
7898
  }
7732
7899
  }
7900
+ paint_into(entry, nd.data.desc, fill, stroke, lineWidth);
7733
7901
 
7902
+ /* The style for the figure, the html for the card and the
7903
+ * pill: every tier is repainted, and the repaint reads the
7904
+ * style it is given here (node_fill_of). */
7734
7905
  let updateStyle = { fill: fill, stroke: stroke, lineWidth: lineWidth };
7735
- if(nd.data.desc.node_treedb_type === 'hierarchical') {
7736
- let record = nd.data.record || {};
7737
- updateStyle.innerHTML = build_node_innerHTML(
7738
- fill, priv.theme, record.icon,
7739
- node_label(nd.data.desc, record),
7740
- nd.data.desc.topic_name, false, record.id,
7741
- false, false, false, pills_html_of(gobj, nd)
7742
- );
7743
- }
7744
7906
  updates.push({ id: nodes[i].id, style: updateStyle });
7907
+ repaint.add(nodes[i].id);
7745
7908
  }
7746
7909
 
7747
7910
  if(updates.length > 0) {
@@ -7749,6 +7912,7 @@ function apply_node_properties(gobj, node_id, fill, stroke, lineWidth, scope, sh
7749
7912
  if(reshaped.length) {
7750
7913
  reshape_nodes(gobj, reshaped);
7751
7914
  }
7915
+ repaint_cards(gobj, repaint);
7752
7916
  graph.draw().then(() => {
7753
7917
  mark_graph_dirty(gobj);
7754
7918
  });
@@ -8980,6 +9144,19 @@ async function reconcile_fold_apply(gobj, opts, visible)
8980
9144
  return changed;
8981
9145
  }
8982
9146
 
9147
+ /************************************************************
9148
+ * The application changed language: what this gclass DRAWS has
9149
+ * to be drawn again. `refresh_language()` reaches what carries
9150
+ * an i18n key in the DOM; a G6 toolbar and the html of a chip
9151
+ * are built here and carry none.
9152
+ ************************************************************/
9153
+ function ac_language_changed(gobj, event, kw, src)
9154
+ {
9155
+ update_toolbar(gobj);
9156
+ repaint_more_chips(gobj);
9157
+ return 0;
9158
+ }
9159
+
8983
9160
  /************************************************************
8984
9161
  * Repaint the `+N` chips (theme or language changed).
8985
9162
  ************************************************************/
@@ -10527,6 +10704,7 @@ function create_gclass(gclass_name)
10527
10704
  ["EV_HIDE", ac_hide, null],
10528
10705
  ["EV_RESIZE", ac_resize, null],
10529
10706
  ["EV_THEME", ac_theme, null],
10707
+ ["EV_LANGUAGE_CHANGED", ac_language_changed, null],
10530
10708
  ]]
10531
10709
  ];
10532
10710
 
@@ -10621,6 +10799,7 @@ function create_gclass(gclass_name)
10621
10799
  ["EV_HIDE", 0],
10622
10800
  ["EV_RESIZE", 0],
10623
10801
  ["EV_THEME", 0],
10802
+ ["EV_LANGUAGE_CHANGED", 0],
10624
10803
  ];
10625
10804
 
10626
10805
  /*----------------------------------------*