@yuneta/gobj-ui 7.23.82 → 7.23.84

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.82",
3
+ "version": "7.23.84",
4
4
  "type": "module",
5
5
  "main": "dist/gobj-ui.cjs.js",
6
6
  "module": "dist/gobj-ui.es.js",
@@ -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",
@@ -4046,6 +4068,42 @@ function hide_overlay(gobj, ...keys)
4046
4068
  /************************************************************
4047
4069
  * Enable the save button (mark graph as dirty).
4048
4070
  ************************************************************/
4071
+ /************************************************************
4072
+ * A popover that PREVIEWS its changes owes an undo.
4073
+ *
4074
+ * What the preview writes is not the reader's answer until
4075
+ * `apply` is pressed, and `cancel` used to be the only way
4076
+ * back: every other way out of the popover -- a click on the
4077
+ * canvas, the icon pressed again, the element going away --
4078
+ * left the preview standing on the element, and the next Save
4079
+ * wrote it to `__graphs__`. So the undo is kept beside the
4080
+ * popover and run by whoever hides it; `apply` drops it first,
4081
+ * because then the preview IS the answer.
4082
+ ************************************************************/
4083
+ function set_preview_undo(gobj, key, undo)
4084
+ {
4085
+ gobj.priv[key] = undo;
4086
+ }
4087
+
4088
+ function drop_preview_undo(gobj, key)
4089
+ {
4090
+ gobj.priv[key] = null;
4091
+ }
4092
+
4093
+ function run_preview_undo(gobj, key)
4094
+ {
4095
+ let undo = gobj.priv[key];
4096
+ gobj.priv[key] = null;
4097
+ if(!undo || gobj_is_destroying(gobj)) {
4098
+ return;
4099
+ }
4100
+ try {
4101
+ undo();
4102
+ } catch(e) {
4103
+ log_error(`${gobj_short_name(gobj)}: cannot undo the preview: ${e}`);
4104
+ }
4105
+ }
4106
+
4049
4107
  function mark_graph_dirty(gobj)
4050
4108
  {
4051
4109
  let $container = gobj_read_attr(gobj, "$container");
@@ -4990,6 +5048,34 @@ function port_hit_slop_world(graph)
4990
5048
  return slop / zoom;
4991
5049
  }
4992
5050
 
5051
+ /************************************************************
5052
+ * Whether a point landed on a port, by the port's SHAPE.
5053
+ *
5054
+ * A port was a circle and the test was a radius; since 7.23.81
5055
+ * it can be a square, a diamond or a triangle too, and a circle
5056
+ * around a diamond answers for the air off its corners while
5057
+ * missing the middle of its edges. `r` is the drawn radius with
5058
+ * the hit slop already added, and the shapes are the ones
5059
+ * TreedbCard.drawPortShapes() draws from it.
5060
+ *
5061
+ * dx, dy the point, relative to the port's centre
5062
+ ************************************************************/
5063
+ function port_shape_hit(shape, dx, dy, r)
5064
+ {
5065
+ switch(shape) {
5066
+ case 'square':
5067
+ return Math.abs(dx) <= r && Math.abs(dy) <= r;
5068
+ case 'diamond':
5069
+ return Math.abs(dx) + Math.abs(dy) <= r;
5070
+ case 'triangle':
5071
+ /* (0,-r), (r,r), (-r,r): under the base and inside the
5072
+ * two slanted sides, which meet at 2|x| - y = r. */
5073
+ return dy <= r && (2 * Math.abs(dx) - dy) <= r;
5074
+ default:
5075
+ return Math.hypot(dx, dy) <= r;
5076
+ }
5077
+ }
5078
+
4993
5079
  /************************************************************
4994
5080
  * Detect if a click in canvas coordinates hits a port.
4995
5081
  * Returns the port key string, or null if no port hit.
@@ -5021,8 +5107,9 @@ function detect_port_click(gobj, node_id, canvasX, canvasY)
5021
5107
  let dist = Math.sqrt(dx * dx + dy * dy);
5022
5108
 
5023
5109
  /* 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) {
5110
+ * overlapping is not a tie -- it is the closer one. The
5111
+ * distance ranks them; the SHAPE says who is in the running. */
5112
+ if(port_shape_hit(ports[i].shape, dx, dy, r + slop) && dist < best_dist) {
5026
5113
  best_dist = dist;
5027
5114
  best_key = ports[i].key;
5028
5115
  }
@@ -5240,6 +5327,7 @@ function toggle_port_popover(gobj)
5240
5327
 
5241
5328
  function hide_port_popover(gobj)
5242
5329
  {
5330
+ run_preview_undo(gobj, '_port_preview_undo');
5243
5331
  hide_overlay(gobj, '_port_popover_el');
5244
5332
  }
5245
5333
 
@@ -5280,6 +5368,17 @@ function show_port_popover(gobj)
5280
5368
  xy.x + floating_popover_dx(), xy.y, 'g6-port-popover', '#d9d9d9', 180
5281
5369
  );
5282
5370
 
5371
+ /* What the preview writes on this port, undone: whoever hides
5372
+ * the popover without applying runs it (see set_preview_undo). */
5373
+ let undo_port = () => {
5374
+ graph.updateNodeData([{id: node_id, style: {
5375
+ ports: ports_with(nodeData.style.ports, port_key, {shape: orig_shape, r: orig_r}),
5376
+ }}]);
5377
+ graph.draw().then(() => {
5378
+ update_port_resize_handles_position(gobj);
5379
+ });
5380
+ };
5381
+
5283
5382
  /* Live preview on THIS port; the others wait for apply. */
5284
5383
  function preview_port() {
5285
5384
  let r = parseInt(rInput.value) || orig_r;
@@ -5313,13 +5412,7 @@ function show_port_popover(gobj)
5313
5412
  text: 'cancel',
5314
5413
  style: BTN_STYLE_CANCEL,
5315
5414
  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);
5415
+ hide_port_popover(gobj); /* which undoes the preview */
5323
5416
  },
5324
5417
  },
5325
5418
  {
@@ -5327,6 +5420,7 @@ function show_port_popover(gobj)
5327
5420
  style: 'flex:1;padding:6px;background:#1890ff;color:#fff;border:none;' +
5328
5421
  'border-radius:4px;cursor:pointer;font-size:13px;font-weight:500;',
5329
5422
  onClick: () => {
5423
+ drop_preview_undo(gobj, '_port_preview_undo');
5330
5424
  apply_port_properties(gobj, node_id, port_key,
5331
5425
  shapeSelect.value, parseInt(rInput.value) || orig_r, scopeSelect.value
5332
5426
  );
@@ -5336,6 +5430,7 @@ function show_port_popover(gobj)
5336
5430
 
5337
5431
  priv.$container.appendChild(popover);
5338
5432
  priv._port_popover_el = popover;
5433
+ set_preview_undo(gobj, '_port_preview_undo', undo_port);
5339
5434
  clamp_popover_position(gobj, popover);
5340
5435
  }
5341
5436
 
@@ -6184,6 +6279,12 @@ function show_edge_popover(gobj)
6184
6279
  'g6-edge-popover', '#d9d9d9', 180
6185
6280
  );
6186
6281
 
6282
+ /* What the preview writes, undone (see set_preview_undo). */
6283
+ let undo_edge = () => {
6284
+ graph.updateEdgeData([{ id: edge_id, style: { lineWidth: origLW, stroke: origStroke } }]);
6285
+ graph.draw();
6286
+ };
6287
+
6187
6288
  // Live preview: update the selected edge in real time
6188
6289
  function preview_edge() {
6189
6290
  let lw = parseInt(lwInput.value) || 2;
@@ -6213,9 +6314,7 @@ function show_edge_popover(gobj)
6213
6314
  text: 'cancel',
6214
6315
  style: BTN_STYLE_CANCEL,
6215
6316
  onClick: () => {
6216
- graph.updateEdgeData([{ id: edge_id, style: { lineWidth: origLW, stroke: origStroke } }]);
6217
- graph.draw();
6218
- deselect_edge(gobj);
6317
+ deselect_edge(gobj); /* which undoes the preview */
6219
6318
  },
6220
6319
  },
6221
6320
  {
@@ -6223,6 +6322,7 @@ function show_edge_popover(gobj)
6223
6322
  style: 'flex:1;padding:6px;background:#52c41a;color:#fff;border:none;' +
6224
6323
  'border-radius:4px;cursor:pointer;font-size:13px;font-weight:500;',
6225
6324
  onClick: () => {
6325
+ drop_preview_undo(gobj, '_edge_preview_undo');
6226
6326
  apply_edge_properties(gobj, edge_id,
6227
6327
  parseInt(lwInput.value) || 2,
6228
6328
  colorInput.value,
@@ -6234,6 +6334,7 @@ function show_edge_popover(gobj)
6234
6334
 
6235
6335
  priv.$container.appendChild(popover);
6236
6336
  priv._edge_popover_el = popover;
6337
+ set_preview_undo(gobj, '_edge_preview_undo', undo_edge);
6237
6338
 
6238
6339
  // Clamp popover inside the container
6239
6340
  clamp_popover_position(gobj, popover);
@@ -6278,6 +6379,7 @@ function clamp_popover_position(gobj, popover)
6278
6379
 
6279
6380
  function hide_edge_popover(gobj)
6280
6381
  {
6382
+ run_preview_undo(gobj, '_edge_preview_undo');
6281
6383
  hide_overlay(gobj, '_edge_popover_el');
6282
6384
  }
6283
6385
 
@@ -6404,6 +6506,22 @@ function show_node_popover(gobj)
6404
6506
  let node_graph_type = nodeData.data && nodeData.data.desc ?
6405
6507
  nodeData.data.desc.node_treedb_type : null;
6406
6508
 
6509
+ /* What the preview writes, undone (see set_preview_undo). */
6510
+ let undo_node = () => {
6511
+ let restoreStyle = { fill: origFill, stroke: origStroke, lineWidth: origLW };
6512
+ if(node_graph_type === 'hierarchical') {
6513
+ let record = nodeData.data.record || {};
6514
+ restoreStyle.innerHTML = build_node_innerHTML(
6515
+ origFill, priv.theme, record.icon,
6516
+ node_label(nodeData.data.desc, record),
6517
+ nodeData.data.desc.topic_name, false, record.id,
6518
+ false, false, false, pills_html_of(gobj, nodeData)
6519
+ );
6520
+ }
6521
+ graph.updateNodeData([{ id: node_id, style: restoreStyle }]);
6522
+ graph.draw();
6523
+ };
6524
+
6407
6525
  // Live preview
6408
6526
  function preview_node() {
6409
6527
  let fill = fillInput.value;
@@ -6460,19 +6578,7 @@ function show_node_popover(gobj)
6460
6578
  text: 'cancel',
6461
6579
  style: BTN_STYLE_CANCEL,
6462
6580
  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);
6581
+ hide_node_popover(gobj); /* which undoes the preview */
6476
6582
  },
6477
6583
  },
6478
6584
  {
@@ -6480,6 +6586,7 @@ function show_node_popover(gobj)
6480
6586
  style: 'flex:1;padding:6px;background:#1890ff;color:#fff;border:none;' +
6481
6587
  'border-radius:4px;cursor:pointer;font-size:13px;font-weight:500;',
6482
6588
  onClick: () => {
6589
+ drop_preview_undo(gobj, '_node_preview_undo');
6483
6590
  apply_node_properties(gobj, node_id,
6484
6591
  fillInput.value,
6485
6592
  strokeInput.value,
@@ -6493,6 +6600,7 @@ function show_node_popover(gobj)
6493
6600
 
6494
6601
  priv.$container.appendChild(popover);
6495
6602
  priv._node_popover_el = popover;
6603
+ set_preview_undo(gobj, '_node_preview_undo', undo_node);
6496
6604
 
6497
6605
  // Clamp popover inside the container
6498
6606
  clamp_popover_position(gobj, popover);
@@ -6500,6 +6608,7 @@ function show_node_popover(gobj)
6500
6608
 
6501
6609
  function hide_node_popover(gobj)
6502
6610
  {
6611
+ run_preview_undo(gobj, '_node_preview_undo');
6503
6612
  hide_overlay(gobj, '_node_popover_el');
6504
6613
  }
6505
6614
 
@@ -8980,6 +9089,19 @@ async function reconcile_fold_apply(gobj, opts, visible)
8980
9089
  return changed;
8981
9090
  }
8982
9091
 
9092
+ /************************************************************
9093
+ * The application changed language: what this gclass DRAWS has
9094
+ * to be drawn again. `refresh_language()` reaches what carries
9095
+ * an i18n key in the DOM; a G6 toolbar and the html of a chip
9096
+ * are built here and carry none.
9097
+ ************************************************************/
9098
+ function ac_language_changed(gobj, event, kw, src)
9099
+ {
9100
+ update_toolbar(gobj);
9101
+ repaint_more_chips(gobj);
9102
+ return 0;
9103
+ }
9104
+
8983
9105
  /************************************************************
8984
9106
  * Repaint the `+N` chips (theme or language changed).
8985
9107
  ************************************************************/
@@ -10527,6 +10649,7 @@ function create_gclass(gclass_name)
10527
10649
  ["EV_HIDE", ac_hide, null],
10528
10650
  ["EV_RESIZE", ac_resize, null],
10529
10651
  ["EV_THEME", ac_theme, null],
10652
+ ["EV_LANGUAGE_CHANGED", ac_language_changed, null],
10530
10653
  ]]
10531
10654
  ];
10532
10655
 
@@ -10621,6 +10744,7 @@ function create_gclass(gclass_name)
10621
10744
  ["EV_HIDE", 0],
10622
10745
  ["EV_RESIZE", 0],
10623
10746
  ["EV_THEME", 0],
10747
+ ["EV_LANGUAGE_CHANGED", 0],
10624
10748
  ];
10625
10749
 
10626
10750
  /*----------------------------------------*
@@ -173,6 +173,7 @@ let PRIVATE_DATA = {
173
173
  layout: null,
174
174
  _topics_subscribed: {},
175
175
  _legend_state: null, /* the engine's last EV_LEGEND_STATE */
176
+ _find_result: null, /* ...and its last EV_FIND_RESULT */
176
177
  _links_subscribed: false, /* EV_TREEDB_NODE_LINKED/UNLINKED (treedb-wide) */
177
178
 
178
179
  is_pinhold_window: false, // inherited of v6, todo review
@@ -303,6 +304,18 @@ function mt_start(gobj)
303
304
  {
304
305
  let priv = gobj.priv;
305
306
 
307
+ /* The legend is built from the engine's EV_LEGEND_STATE, which
308
+ * arrives after a reconcile and not when the language changes:
309
+ * a chip's title says an ACTION (show / hide this topic), and
310
+ * the strip would keep the old language until somebody folded
311
+ * something. The keys travel in `data-i18n-title` too, so this
312
+ * is the belt to that pair of braces -- and it is what repaints
313
+ * the counted line of the find box, which has no key at all. */
314
+ let shell = yui_shell_of(gobj);
315
+ if(shell) {
316
+ gobj_subscribe_event(shell, "EV_LANGUAGE_CHANGED", {}, gobj);
317
+ }
318
+
306
319
  if(priv.gobj_nodes_tree) {
307
320
  gobj_start(priv.gobj_nodes_tree);
308
321
  }
@@ -323,6 +336,7 @@ function mt_stop(gobj)
323
336
  let shell = yui_shell_of(gobj);
324
337
  if(shell) {
325
338
  yui_shell_set_sub_routes(shell, gobj_read_str_attr(gobj, "base_route"), null);
339
+ gobj_unsubscribe_event(shell, "EV_LANGUAGE_CHANGED", {}, gobj);
326
340
  }
327
341
  close_json_viewer(gobj);
328
342
  gobj_stop_children(gobj);
@@ -547,26 +561,31 @@ function option_label(name)
547
561
  ************************************************************/
548
562
  function node_shape_items(gobj, wide)
549
563
  {
550
- /* `value` explicit on every option, as on the two selects
551
- * above: a translated <option> without one answers with its
552
- * TEXT, and the mode the child is told is a word it does not
553
- * know. */
554
- let options = NODE_MODES.map((mode) =>
555
- ['option', {value: mode, 'data-i18n': NODE_MODE_LABELS[mode]}, t(NODE_MODE_LABELS[mode])]
556
- );
557
- return [
558
- ['span', {class: 'GRAPH_NODE_MODE_LABEL is-hidden-mobile',
559
- style: 'padding-left:10px; padding-right:5px;', i18n: 'nodes'}, 'nodes'],
560
- ['div', {class: 'select'}, [
561
- ['select', {class: 'GRAPH_NODE_MODE_SELECT',
562
- title: t('nodes'), 'data-i18n-title': 'nodes',
563
- 'aria-label': t('nodes'), 'data-i18n-aria-label': 'nodes'}, options]
564
- ], {
565
- change: (evt) => {
566
- evt.stopPropagation();
567
- gobj_send_event(gobj, "EV_SET_NODE_MODE", {node_mode: evt.target.value}, gobj);
564
+ /* Three push buttons in one group, not a select: a view is
565
+ * picked at a glance and in one press, and the pressed one
566
+ * SAYS which view is on -- a closed list says nothing until it
567
+ * is opened. Icons, because a card, a line and a figure draw
568
+ * themselves better than three words. */
569
+ let mode_buttons = NODE_MODES.map((mode) => {
570
+ let key = NODE_MODE_LABELS[mode];
571
+ return ['button', {class: 'GRAPH_NODE_MODE_BTN button', type: 'button',
572
+ 'data-mode': mode,
573
+ style: {height: wide, width: '2.5em'},
574
+ title: t(key), 'data-i18n-title': key,
575
+ 'aria-label': t(key), 'data-i18n-aria-label': key,
576
+ 'aria-pressed': 'false'},
577
+ ['i', {style: 'font-size:1.5em; color:inherit;', class: NODE_MODE_ICONS[mode]}],
578
+ {
579
+ click: (evt) => {
580
+ evt.stopPropagation();
581
+ gobj_send_event(gobj, "EV_SET_NODE_MODE", {node_mode: mode}, gobj);
582
+ }
568
583
  }
569
- }],
584
+ ];
585
+ });
586
+ return [
587
+ ['div', {class: 'GRAPH_NODE_MODE buttons has-addons',
588
+ style: 'margin:0 0 0 .5rem; flex:0 0 auto; flex-wrap:nowrap;'}, mode_buttons],
570
589
  ['button', {class: 'GRAPH_NODE_LABELS button', type: 'button',
571
590
  style: {height: wide, width: '2.5em'},
572
591
  title: t('node labels'), 'data-i18n-title': 'node labels',
@@ -583,17 +602,23 @@ function node_shape_items(gobj, wide)
583
602
  ];
584
603
  }
585
604
 
586
- /* The three views of a record, and the label of each in the
587
- * select. The values are the child's `node_mode`. */
605
+ /* The three views of a record: the child's `node_mode`, the i18n
606
+ * key of each, and the glyph -- a card, a line, a figure. */
588
607
  const NODE_MODES = ['expanded', 'compact', 'shape'];
589
608
  const NODE_MODE_LABELS = {
590
609
  expanded: 'full',
591
610
  compact: 'compact',
592
611
  shape: 'shape',
593
612
  };
613
+ const NODE_MODE_ICONS = {
614
+ expanded: 'yi-square',
615
+ compact: 'yi-bars',
616
+ shape: 'yi-hexagon-nodes',
617
+ };
594
618
 
595
- /* The select and the labels toggle, from the attrs. The toggle
596
- * only means something on figures, so it is disabled elsewhere.
619
+ /* The pressed view and the labels toggle, from the attrs. The
620
+ * toggle only means something on figures, so it is disabled
621
+ * elsewhere.
597
622
  * `$root` is whatever holds the toolbar: the toolbar itself before
598
623
  * the view is mounted, the view's container after. */
599
624
  function refresh_node_shape_buttons(gobj, $root)
@@ -606,9 +631,10 @@ function refresh_node_shape_buttons(gobj, $root)
606
631
  mode = 'expanded';
607
632
  }
608
633
  let labels = gobj_read_bool_attr(gobj, "node_labels");
609
- let $mode = $root.querySelector('.GRAPH_NODE_MODE_SELECT');
610
- if($mode) {
611
- $mode.value = mode;
634
+ for(let $btn of $root.querySelectorAll('.GRAPH_NODE_MODE_BTN')) {
635
+ let on = ($btn.getAttribute('data-mode') === mode);
636
+ $btn.classList.toggle('pressed_state', on);
637
+ $btn.setAttribute('aria-pressed', on? 'true' : 'false');
612
638
  }
613
639
  set_pressed_state($root, '.GRAPH_NODE_LABELS', labels);
614
640
  let $labels = $root.querySelector('.GRAPH_NODE_LABELS');
@@ -2511,6 +2537,8 @@ function refresh_legend(gobj)
2511
2537
  hidden? `${entry.total}` : `${entry.visible}/${entry.total}`]
2512
2538
  );
2513
2539
 
2540
+ let body_key = is_main? 'main topic' : (hidden? 'show topic' : 'hide topic');
2541
+
2514
2542
  /* Built as an object first: an attribute that is `undefined`
2515
2543
  * is still SET by createElement2 (to the string "undefined"),
2516
2544
  * and a `disabled="undefined"` button is a disabled button --
@@ -2521,7 +2549,14 @@ function refresh_legend(gobj)
2521
2549
  (is_main? ' GRAPH_LEGEND_MAIN' : ''),
2522
2550
  type: 'button',
2523
2551
  style: 'gap:0; flex:0 0 auto;' + (focused? ' ' + LEGEND_FOCUSED_STYLE : ''),
2524
- title: is_main? t('main topic') : (hidden? t('show topic') : t('hide topic')),
2552
+ /* The KEY as well as the text: the strip is redrawn only
2553
+ * when the engine speaks again (EV_LEGEND_STATE), and a
2554
+ * language change is not that -- refresh_language() reaches
2555
+ * what carries `data-i18n-title` and nothing else. The key
2556
+ * is picked here because the chip is rebuilt whenever the
2557
+ * state behind it changes. */
2558
+ title: t(body_key),
2559
+ 'data-i18n-title': body_key,
2525
2560
  'aria-label': topic_name,
2526
2561
  'aria-pressed': hidden? 'false' : 'true',
2527
2562
  };
@@ -2548,7 +2583,9 @@ function refresh_legend(gobj)
2548
2583
  extras.push(['button', {class: 'GRAPH_LEGEND_STAR button is-small pressed_state',
2549
2584
  type: 'button', style: 'padding:0 .4rem;',
2550
2585
  title: t('main topic'), 'data-i18n-title': 'main topic',
2551
- 'aria-label': t('main topic'), 'aria-pressed': 'true'},
2586
+ 'aria-label': t('main topic'),
2587
+ 'data-i18n-aria-label': 'main topic',
2588
+ 'aria-pressed': 'true'},
2552
2589
  [['span', {style: LEGEND_GLYPH_STYLE}, '★']], {
2553
2590
  click: (evt) => {
2554
2591
  evt.stopPropagation();
@@ -2565,7 +2602,8 @@ function refresh_legend(gobj)
2565
2602
  extras.push(['button', {class: 'GRAPH_LEGEND_STAR button is-small',
2566
2603
  type: 'button', style: 'padding:0 .4rem;',
2567
2604
  title: t('main topic'), 'data-i18n-title': 'main topic',
2568
- 'aria-label': t('main topic')},
2605
+ 'aria-label': t('main topic'),
2606
+ 'data-i18n-aria-label': 'main topic'},
2569
2607
  [['span', {style: LEGEND_GLYPH_STYLE}, '☆']], {
2570
2608
  click: (evt) => {
2571
2609
  evt.stopPropagation();
@@ -2585,6 +2623,7 @@ function refresh_legend(gobj)
2585
2623
  type: 'button', style: 'padding:0 .45rem;',
2586
2624
  title: t('loose records'), 'data-i18n-title': 'loose records',
2587
2625
  'aria-label': t('loose records'),
2626
+ 'data-i18n-aria-label': 'loose records',
2588
2627
  'aria-pressed': entry.loose_shown? 'true' : 'false'
2589
2628
  }, `+${entry.loose}`, {
2590
2629
  click: (evt) => {
@@ -2602,6 +2641,7 @@ function refresh_legend(gobj)
2602
2641
  type: 'button', style: 'padding:0 .4rem;',
2603
2642
  title: t('highlight topic'), 'data-i18n-title': 'highlight topic',
2604
2643
  'aria-label': t('highlight topic'),
2644
+ 'data-i18n-aria-label': 'highlight topic',
2605
2645
  'aria-pressed': focused? 'true' : 'false'
2606
2646
  }, [['i', {class: 'yi-location-crosshairs', style: LEGEND_GLYPH_STYLE}]], {
2607
2647
  click: (evt) => {
@@ -2621,6 +2661,20 @@ function refresh_legend(gobj)
2621
2661
  }
2622
2662
  }
2623
2663
 
2664
+ /************************************************************
2665
+ * The application changed language. `refresh_language()` has
2666
+ * already been over the document by now (the shell runs it
2667
+ * before publishing), so what is left is what carries no key:
2668
+ * the legend, whose chips say an ACTION that depends on the
2669
+ * state, and the find box's counted line.
2670
+ ************************************************************/
2671
+ function ac_language_changed(gobj, event, kw, src)
2672
+ {
2673
+ refresh_legend(gobj);
2674
+ refresh_find_result(gobj);
2675
+ return 0;
2676
+ }
2677
+
2624
2678
  /************************************************************
2625
2679
  * The engine said what is drawn: repaint the strip.
2626
2680
  ************************************************************/
@@ -2836,6 +2890,18 @@ function ac_toggle_node_labels(gobj, event, kw, src)
2836
2890
  ************************************************************/
2837
2891
  function ac_find_result(gobj, event, kw, src)
2838
2892
  {
2893
+ /* Kept, because the line is COMPOSED -- a counted word and a
2894
+ * count of hidden topics -- so no i18n key in the DOM can
2895
+ * re-translate it: its owner repaints it from the last answer
2896
+ * when the language changes. */
2897
+ gobj.priv._find_result = kw || null;
2898
+ refresh_find_result(gobj);
2899
+ return 0;
2900
+ }
2901
+
2902
+ function refresh_find_result(gobj)
2903
+ {
2904
+ let kw = gobj.priv._find_result;
2839
2905
  let $container = gobj_read_attr(gobj, "$container");
2840
2906
  if(!$container) {
2841
2907
  return 0;
@@ -2992,6 +3058,7 @@ function create_gclass(gclass_name)
2992
3058
  ["EV_SHOW", ac_show, null],
2993
3059
  ["EV_HIDE", ac_hide, null],
2994
3060
  ["EV_TRANSPORT_STATE", ac_transport_state, null],
3061
+ ["EV_LANGUAGE_CHANGED", ac_language_changed, null],
2995
3062
  ]]
2996
3063
  ];
2997
3064
 
@@ -3040,6 +3107,7 @@ function create_gclass(gclass_name)
3040
3107
  event_flag_t.EVF_OUTPUT_EVENT | event_flag_t.EVF_NO_WARN_SUBS],
3041
3108
  ["EV_SHOW", 0],
3042
3109
  ["EV_HIDE", 0],
3110
+ ["EV_LANGUAGE_CHANGED", 0],
3043
3111
  ["EV_TRANSPORT_STATE", 0],
3044
3112
  ];
3045
3113