@yuneta/gobj-ui 7.26.26 → 7.26.28

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.
Files changed (44) hide show
  1. package/README.md +473 -19
  2. package/dist/gobj-ui.cjs.js +1100 -231
  3. package/dist/gobj-ui.es.js +1101 -232
  4. package/package.json +1 -1
  5. package/scripts/validate-locales.mjs +2334 -275
  6. package/src/c_g6_nodes_tree.js +1143 -260
  7. package/src/c_yui_form.js +11 -1
  8. package/src/c_yui_fsm_graph.js +5 -0
  9. package/src/c_yui_gclass.js +11 -0
  10. package/src/c_yui_gobj_tree_js.js +17 -0
  11. package/src/c_yui_json_graph.js +5 -0
  12. package/src/c_yui_nav.js +9 -6
  13. package/src/c_yui_pager.js +1 -1
  14. package/src/c_yui_period.js +11 -4
  15. package/src/c_yui_schema_editor.js +19 -7
  16. package/src/c_yui_shell.js +7 -7
  17. package/src/c_yui_treedb_graph.js +15 -6
  18. package/src/c_yui_treedb_schema.js +5 -0
  19. package/src/c_yui_treedb_topic_with_form.js +10 -10
  20. package/src/c_yui_treedb_topics.js +6 -6
  21. package/src/c_yui_window.js +2 -2
  22. package/src/c_yui_wizard.js +8 -1
  23. package/src/form_json_field.js +7 -0
  24. package/src/g6_canvas_tab_order.js +84 -0
  25. package/src/g6_canvas_tab_order.wiring.test.js +232 -0
  26. package/src/g6_nodes_tree_controls.test.js +547 -0
  27. package/src/g6_nodes_tree_focus.wiring.test.js +1360 -0
  28. package/src/g6_nodes_tree_i18n_keys.test.js +275 -0
  29. package/src/i18n_key_functions.js +146 -0
  30. package/src/i18n_key_functions.test.js +1204 -0
  31. package/src/lib_graph.js +38 -1
  32. package/src/nav_backbar_name.wiring.test.js +82 -0
  33. package/src/schema_flags.js +47 -0
  34. package/src/schema_import.js +7 -0
  35. package/src/shell_modals.js +20 -16
  36. package/src/shell_route_map.js +12 -2
  37. package/src/wizard_helpers.js +8 -0
  38. package/src/yui_dev.js +44 -33
  39. package/src/yui_dev_controls.test.js +174 -0
  40. package/src/yui_gclass_view.js +2 -2
  41. package/src/yui_graph_camera.js +1 -1
  42. package/src/yui_install.js +10 -8
  43. package/src/yui_table_select.js +2 -2
  44. package/src/yui_time.js +18 -3
@@ -1848,7 +1848,7 @@ function build_toolbar$1(gobj, config) {
1848
1848
  }
1849
1849
  priv.avatar_nodes = [];
1850
1850
  priv.conn_nodes = [];
1851
- let toolbar_aria = tb.aria_label || "Toolbar";
1851
+ const toolbar_aria = tb.aria_label || "toolbar";
1852
1852
  let $bar = (0, _yuneta_gobj_js.createElement2)([
1853
1853
  "nav",
1854
1854
  {
@@ -2059,8 +2059,8 @@ function build_toolbar_brand_item(gobj, it) {
2059
2059
  ************************************************************/
2060
2060
  function build_toolbar_avatar_item(gobj, it) {
2061
2061
  let priv = gobj.priv;
2062
- let aria_key = it.aria_label || it.name || it.id || "User menu";
2063
- let i18n_aria = it.aria_label || it.name || "User menu";
2062
+ const aria_key = it.aria_label || it.name || it.id || "user menu";
2063
+ const i18n_aria = it.aria_label || it.name || "user menu";
2064
2064
  let action_type = it.action && it.action.type || "";
2065
2065
  let attrs = {
2066
2066
  class: "navbar-item yui-toolbar-item yui-toolbar-avatar is-unselectable",
@@ -2136,8 +2136,8 @@ function attach_context_action(gobj, $item, it) {
2136
2136
  ************************************************************/
2137
2137
  function build_toolbar_connection_item(gobj, it) {
2138
2138
  let priv = gobj.priv;
2139
- let aria_key = it.aria_label || it.name || it.id || "backend connection";
2140
- let i18n_aria = it.aria_label || it.name || "backend connection";
2139
+ const aria_key = it.aria_label || it.name || it.id || "backend connection";
2140
+ const i18n_aria = it.aria_label || it.name || "backend connection";
2141
2141
  let attrs = {
2142
2142
  class: "navbar-item yui-toolbar-item yui-toolbar-conn is-unselectable is-disconnected",
2143
2143
  type: "button",
@@ -2243,8 +2243,8 @@ function toggle_toolbar_dropdown(gobj, item, action, $trigger) {
2243
2243
  function open_toolbar_dropdown(gobj, item, action, $trigger) {
2244
2244
  let priv = gobj.priv;
2245
2245
  if (!priv || !priv.layers || !priv.layers.popup) return;
2246
- let aria_key = item && (item.aria_label || item.name || item.id) || "Menu";
2247
- let i18n_aria = item && (item.aria_label || item.name) || "Menu";
2246
+ const aria_key = item && (item.aria_label || item.name || item.id) || "menu";
2247
+ const i18n_aria = item && (item.aria_label || item.name) || "menu";
2248
2248
  let $panel = (0, _yuneta_gobj_js.createElement2)(["div", {
2249
2249
  class: "yui-toolbar-dropdown-panel",
2250
2250
  role: "menu",
@@ -3781,19 +3781,22 @@ function build_dialog(shell, message, buttons, opts) {
3781
3781
  * "Delete" and "Cancel", keys that no locale carries, so a dialog
3782
3782
  * that took the defaults read in English in every language.
3783
3783
  *
3784
- * Written as `i18n:` entries so the validate-locales script of a
3785
- * consumer finds them when it scans this module: an app that
3786
- * mounts these dialogs must define the five keys.
3784
+ * An app that mounts these dialogs must define the five keys. A
3785
+ * consumer's validate-locales reads them where build_dialog() gets
3786
+ * them (a const, `DEFAULT_LABEL.ok`, it follows), and in
3787
+ * SHELL_MODALS_I18N_KEYS, which a test holds to this table. (Up to
3788
+ * 7.26.27 they were written as `i18n:` entries, which only a regex
3789
+ * scan of `i18n: "..."` happened to see.)
3787
3790
  ***************************************************************/
3788
3791
  var DEFAULT_LABEL = Object.freeze({
3789
- ok: Object.freeze({ i18n: "ok" }),
3790
- yes: Object.freeze({ i18n: "yes" }),
3791
- no: Object.freeze({ i18n: "no" }),
3792
- delete: Object.freeze({ i18n: "delete" }),
3793
- cancel: Object.freeze({ i18n: "cancel" })
3792
+ ok: "ok",
3793
+ yes: "yes",
3794
+ no: "no",
3795
+ delete: "delete",
3796
+ cancel: "cancel"
3794
3797
  });
3795
3798
  function yui_shell_confirm_ok(shell, message, opts) {
3796
- let label = opts && opts.ok_label || DEFAULT_LABEL.ok.i18n;
3799
+ const label = opts && opts.ok_label || DEFAULT_LABEL.ok;
3797
3800
  if (!opts || !opts.type) opts = Object.assign({}, opts, { type: "success" });
3798
3801
  return build_dialog(shell, message, [{
3799
3802
  label,
@@ -3802,8 +3805,8 @@ function yui_shell_confirm_ok(shell, message, opts) {
3802
3805
  }], opts).then(() => void 0);
3803
3806
  }
3804
3807
  function yui_shell_confirm_yesno(shell, message, opts) {
3805
- let yes_label = opts && opts.yes_label || DEFAULT_LABEL.yes.i18n;
3806
- let no_label = opts && opts.no_label || DEFAULT_LABEL.no.i18n;
3808
+ const yes_label = opts && opts.yes_label || DEFAULT_LABEL.yes;
3809
+ const no_label = opts && opts.no_label || DEFAULT_LABEL.no;
3807
3810
  return build_dialog(shell, message, [{
3808
3811
  label: yes_label,
3809
3812
  value: "yes",
@@ -3828,8 +3831,8 @@ function yui_shell_confirm_yesno(shell, message, opts) {
3828
3831
  * pressed.
3829
3832
  ***************************************************************/
3830
3833
  function yui_shell_confirm_danger(shell, message, opts) {
3831
- let confirm_label = opts && opts.confirm_label || DEFAULT_LABEL.delete.i18n;
3832
- let cancel_label = opts && opts.cancel_label || DEFAULT_LABEL.cancel.i18n;
3834
+ const confirm_label = opts && opts.confirm_label || DEFAULT_LABEL.delete;
3835
+ const cancel_label = opts && opts.cancel_label || DEFAULT_LABEL.cancel;
3833
3836
  if (!opts || !opts.type) opts = Object.assign({}, opts, { type: "danger" });
3834
3837
  return build_dialog(shell, message, [{
3835
3838
  label: confirm_label,
@@ -3841,9 +3844,9 @@ function yui_shell_confirm_danger(shell, message, opts) {
3841
3844
  }], opts).then((v) => v === "confirm");
3842
3845
  }
3843
3846
  function yui_shell_confirm_yesnocancel(shell, message, opts) {
3844
- let yes_label = opts && opts.yes_label || DEFAULT_LABEL.yes.i18n;
3845
- let no_label = opts && opts.no_label || DEFAULT_LABEL.no.i18n;
3846
- let cancel_label = opts && opts.cancel_label || DEFAULT_LABEL.cancel.i18n;
3847
+ const yes_label = opts && opts.yes_label || DEFAULT_LABEL.yes;
3848
+ const no_label = opts && opts.no_label || DEFAULT_LABEL.no;
3849
+ const cancel_label = opts && opts.cancel_label || DEFAULT_LABEL.cancel;
3847
3850
  return build_dialog(shell, message, [
3848
3851
  {
3849
3852
  label: yes_label,
@@ -4497,7 +4500,7 @@ function set_max_icon(gobj, maximized) {
4497
4500
  if (!$container) return;
4498
4501
  let $btn = $container.querySelector(".wc-max");
4499
4502
  if ($btn) {
4500
- let key = maximized ? "restore" : "maximize";
4503
+ const key = maximized ? "restore" : "maximize";
4501
4504
  $btn.innerHTML = maximized ? WC_RESTORE : WC_MAX;
4502
4505
  $btn.setAttribute("title", (0, i18next.t)(key));
4503
4506
  $btn.setAttribute("data-i18n-title", key);
@@ -5599,7 +5602,7 @@ function render_header(gobj) {
5599
5602
  let $discard = $container.querySelector(".yui-pager-discard");
5600
5603
  $back.classList.toggle("is-hidden", !model.show_back);
5601
5604
  let $back_icon = $back.querySelector("i");
5602
- let back_key = model.back_kind === "close" ? "close" : "back";
5605
+ const back_key = model.back_kind === "close" ? "close" : "back";
5603
5606
  $back_icon.className = back_key === "close" ? "yi-xmark" : "yi-arrow-left";
5604
5607
  $back.setAttribute("title", (0, i18next.t)(back_key));
5605
5608
  $back.setAttribute("data-i18n-title", back_key);
@@ -8988,7 +8991,7 @@ function destroy_widgets(gobj, $container) {
8988
8991
  * ids would break the association for both. The key is the
8989
8992
  * label's own, so a language change reaches it.
8990
8993
  ************************************************************/
8991
- function name_form_control($field, conf) {
8994
+ function name_form_control$1($field, conf) {
8992
8995
  if (!$field || typeof $field.querySelector !== "function" || !conf) return;
8993
8996
  let key = conf.label_i18n || conf.label || "";
8994
8997
  if (!key) return;
@@ -9059,7 +9062,7 @@ function build_html_form(gobj, $form, prefix, template) {
9059
9062
  if (placeholders[key]) html_field_conf.placeholder = placeholders[key];
9060
9063
  if (html_field_conf.tag) {
9061
9064
  let $field = create_form_field(gobj, $form, prefix, field_desc, html_field_conf);
9062
- name_form_control($field, html_field_conf);
9065
+ name_form_control$1($field, html_field_conf);
9063
9066
  $form.appendChild($field);
9064
9067
  }
9065
9068
  });
@@ -12754,7 +12757,7 @@ function set_pressed_state($container, selector, set) {
12754
12757
  *
12755
12758
  * Utilities for graph, using Bulma
12756
12759
  *
12757
- * Copyright (c) 2025, ArtGins.
12760
+ * Copyright (c) 2025-2026, ArtGins.
12758
12761
  * All Rights Reserved.
12759
12762
  ***********************************************************************/
12760
12763
  /************************************************************
@@ -12783,15 +12786,46 @@ function removeChildElements($element) {
12783
12786
  }
12784
12787
  /************************************************************
12785
12788
  * Disable/Enable elements
12786
- ************************************************************/
12789
+ *
12790
+ * A disabled control says so to the keyboard and to a screen
12791
+ * reader too, not only to the pointer:
12792
+ * - `disabled`, what the stylesheets and the click guards
12793
+ * read (on a `div` drawn as a button it means nothing to
12794
+ * the browser itself);
12795
+ * - `aria-disabled="true"`, what a reader announces -- without
12796
+ * it a disabled toolbar item was heard as a button that
12797
+ * works;
12798
+ * - `tabindex="-1"`, so Tab does not stop on a control that
12799
+ * does nothing.
12800
+ *
12801
+ * Enabled again, it gets back the tabindex it had, kept in
12802
+ * `data-enabled-tabindex` while it is disabled (empty: it had
12803
+ * none, and it gets none). Whoever BUILDS a control disabled
12804
+ * writes that attribute itself, with the tabindex the control
12805
+ * takes once enabled (see toolbar_item_html in
12806
+ * c_g6_nodes_tree.js). An element that keeps the focus while it
12807
+ * is disabled keeps it: tabindex -1 still takes a focus, it is
12808
+ * only left out of the Tab order.
12809
+ ************************************************************/
12810
+ var ENABLED_TABINDEX = "data-enabled-tabindex";
12787
12811
  function disableElements($container, selector) {
12788
12812
  $container.querySelectorAll(selector).forEach((el) => {
12789
12813
  el.setAttribute("disabled", "");
12814
+ el.setAttribute("aria-disabled", "true");
12815
+ if (!el.hasAttribute(ENABLED_TABINDEX)) el.setAttribute(ENABLED_TABINDEX, el.getAttribute("tabindex") ?? "");
12816
+ el.setAttribute("tabindex", "-1");
12790
12817
  });
12791
12818
  }
12792
12819
  function enableElements($container, selector) {
12793
12820
  $container.querySelectorAll(selector).forEach((el) => {
12794
12821
  if (el.hasAttribute("disabled")) el.removeAttribute("disabled");
12822
+ el.removeAttribute("aria-disabled");
12823
+ if (el.hasAttribute(ENABLED_TABINDEX)) {
12824
+ let tabindex = el.getAttribute(ENABLED_TABINDEX);
12825
+ el.removeAttribute(ENABLED_TABINDEX);
12826
+ if (tabindex === "") el.removeAttribute("tabindex");
12827
+ else el.setAttribute("tabindex", tabindex);
12828
+ }
12795
12829
  });
12796
12830
  }
12797
12831
  /************************************************************
@@ -13174,7 +13208,7 @@ function yui_graph_update_anchor($container, state) {
13174
13208
  set_pressed_state($container, ".EV_TOGGLE_ANCHOR", state === "on");
13175
13209
  $btn.classList.toggle("color_pending_state", state === "arming");
13176
13210
  $btn.setAttribute("aria-pressed", state === "on" ? "true" : "false");
13177
- let key = state === "arming" ? "click the element to centre on" : state === "on" ? "centred: click to release" : "anchor view";
13211
+ const key = state === "arming" ? "click the element to centre on" : state === "on" ? "centred: click to release" : "anchor view";
13178
13212
  $btn.setAttribute("title", (0, i18next.t)(key));
13179
13213
  $btn.setAttribute("data-i18n-title", key);
13180
13214
  $btn.setAttribute("aria-label", (0, i18next.t)(key));
@@ -13788,6 +13822,73 @@ function g6_event_kw(evt) {
13788
13822
  };
13789
13823
  }
13790
13824
  //#endregion
13825
+ //#region src/g6_canvas_tab_order.js
13826
+ /***********************************************************************
13827
+ * g6_canvas_tab_order.js
13828
+ *
13829
+ * The canvases of a G6 graph in the page's Tab order: the main
13830
+ * one in its place, the other layers out of it.
13831
+ *
13832
+ * G6 stacks four canvases (background, main, label, transient)
13833
+ * and gives each of them `tabIndex = 1` (configCanvasDom, when it
13834
+ * builds the Canvas and again on a renderer change). A POSITIVE
13835
+ * tabindex goes before every control of the page: Tab from a
13836
+ * graph went to its other canvases, then to the top of the page
13837
+ * and through every control there, and only then to the graph's
13838
+ * own buttons. The main canvas -- the one G6 listens on, and the
13839
+ * one a view puts the keyboard on -- takes 0, so Tab goes from it
13840
+ * to the graph's toolbars and buttons in DOM order; the other
13841
+ * layers receive nothing (`pointer-events: none`) and take -1.
13842
+ *
13843
+ * Every view that creates a G6 Graph calls
13844
+ * keep_canvas_tab_order() right after `new Graph()`: the order is
13845
+ * put right each time G6 writes the 1 -- once the canvas is built
13846
+ * (on the first render(), whichever function of the view calls
13847
+ * it), and after a renderer change. One place, so a view with
13848
+ * several render() calls cannot miss the one that builds the
13849
+ * canvas.
13850
+ *
13851
+ * Copyright (c) 2026, ArtGins.
13852
+ * All Rights Reserved.
13853
+ ***********************************************************************/
13854
+ /************************************************************
13855
+ * Main canvas 0, every other layer -1. Returns 0, or -1 with
13856
+ * the cause logged.
13857
+ ************************************************************/
13858
+ function set_canvas_tab_order(gobj, graph) {
13859
+ let canvas = graph ? graph.getCanvas() : null;
13860
+ if (!canvas || typeof canvas.getLayers !== "function") {
13861
+ (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: no canvas to set the Tab order of`);
13862
+ return -1;
13863
+ }
13864
+ let ret = 0;
13865
+ let layers = canvas.getLayers() || {};
13866
+ for (let name of Object.keys(layers)) {
13867
+ let $el = null;
13868
+ try {
13869
+ $el = layers[name].getContextService().getDomElement();
13870
+ } catch (e) {
13871
+ $el = null;
13872
+ }
13873
+ if (!$el) {
13874
+ (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: canvas layer '${name}' has no element`);
13875
+ ret = -1;
13876
+ continue;
13877
+ }
13878
+ $el.tabIndex = name === "main" ? 0 : -1;
13879
+ }
13880
+ return ret;
13881
+ }
13882
+ /************************************************************
13883
+ * Keep the order for the life of `graph`: on every moment G6
13884
+ * writes its own.
13885
+ ************************************************************/
13886
+ function keep_canvas_tab_order(gobj, graph) {
13887
+ for (let ev of [_antv_g6.GraphEvent.AFTER_CANVAS_INIT, _antv_g6.GraphEvent.AFTER_RENDERER_CHANGE]) graph.on(ev, () => {
13888
+ set_canvas_tab_order(gobj, graph);
13889
+ });
13890
+ }
13891
+ //#endregion
13791
13892
  //#region src/c_yui_json_graph.js
13792
13893
  /***********************************************************************
13793
13894
  * c_yui_json_graph.js
@@ -14291,6 +14392,7 @@ function build_graph$4(gobj) {
14291
14392
  behaviors: BEHAVIORS_READING$1.slice()
14292
14393
  });
14293
14394
  if (priv.theme) graph.setTheme(priv.theme);
14395
+ keep_canvas_tab_order(gobj, graph);
14294
14396
  graph.on(_antv_g6.NodeEvent.CLICK, (evt) => {
14295
14397
  (0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_NODE_CLICK", g6_event_kw(evt), gobj);
14296
14398
  });
@@ -18208,6 +18310,7 @@ function build_graph$3(gobj) {
18208
18310
  } catch (e) {
18209
18311
  (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: the theme was refused: ${e}`);
18210
18312
  }
18313
+ keep_canvas_tab_order(gobj, priv.graph);
18211
18314
  priv.graph.on("aftertransform", () => {
18212
18315
  yui_graph_update_zoom((0, _yuneta_gobj_js.gobj_read_attr)(gobj, "$container"), priv.graph);
18213
18316
  });
@@ -21711,6 +21814,7 @@ function build_graph$2(gobj) {
21711
21814
  behaviors: BEHAVIORS_READING.slice()
21712
21815
  });
21713
21816
  if (priv.theme) graph.setTheme(priv.theme);
21817
+ keep_canvas_tab_order(gobj, graph);
21714
21818
  graph.on("aftertransform", () => {
21715
21819
  yui_graph_update_zoom((0, _yuneta_gobj_js.gobj_read_attr)(gobj, "$container"), priv.graph);
21716
21820
  let z = priv.last_zoom;
@@ -23527,7 +23631,7 @@ function build_ui$7(gobj) {
23527
23631
  if (layout !== "drawer") {
23528
23632
  $container.setAttribute("role", "navigation");
23529
23633
  if (!$container.hasAttribute("aria-label")) {
23530
- let label = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "nav_label") || "navigation";
23634
+ const label = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "nav_label") || "navigation";
23531
23635
  $container.setAttribute("aria-label", (0, i18next.t)(label));
23532
23636
  $container.setAttribute("data-i18n-aria-label", label);
23533
23637
  }
@@ -23788,14 +23892,17 @@ function render_backbar(gobj) {
23788
23892
  let label = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "nav_label") || "";
23789
23893
  let label_attrs = { class: "yui-nav-label" };
23790
23894
  if (label) label_attrs.i18n = label;
23895
+ const name_key = label || "back";
23791
23896
  let a_attrs = {
23792
23897
  class: "yui-nav-item yui-nav-back",
23793
23898
  href: back_route ? "#" + back_route : "#",
23794
23899
  "data-item-id": "__index__",
23795
23900
  "data-route": back_route,
23796
- "aria-label": label || "back"
23901
+ title: (0, i18next.t)(name_key),
23902
+ "data-i18n-title": name_key,
23903
+ "aria-label": (0, i18next.t)(name_key),
23904
+ "data-i18n-aria-label": name_key
23797
23905
  };
23798
- if (label) a_attrs["data-i18n-aria-label"] = label;
23799
23906
  return (0, _yuneta_gobj_js.createElement2)([
23800
23907
  "div",
23801
23908
  {},
@@ -23822,7 +23929,7 @@ function render_drawer(gobj, items) {
23822
23929
  wrap.className = "yui-drawer";
23823
23930
  wrap.setAttribute("role", "dialog");
23824
23931
  wrap.setAttribute("aria-modal", "true");
23825
- let nav_label = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "nav_label") || "navigation";
23932
+ const nav_label = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "nav_label") || "navigation";
23826
23933
  wrap.setAttribute("aria-label", (0, i18next.t)(nav_label));
23827
23934
  wrap.setAttribute("data-i18n-aria-label", nav_label);
23828
23935
  let back = document.createElement("div");
@@ -25617,7 +25724,7 @@ function show_load_error$1(gobj, message) {
25617
25724
  $container.insertBefore($err, $container.firstChild);
25618
25725
  }
25619
25726
  let treedb = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "treedb_name") || "";
25620
- $err.textContent = (treedb ? `${treedb}: ` : "") + (message || "cannot load treedb");
25727
+ $err.textContent = (treedb ? `${treedb}: ` : "") + (message || (0, i18next.t)("cannot load treedb"));
25621
25728
  }
25622
25729
  /************************************************************
25623
25730
  * Destroy UI
@@ -27995,7 +28102,6 @@ function yui_selection_bar(t, opts) {
27995
28102
  if (typeof o.on_clear === "function") o.on_clear(e);
27996
28103
  } }
27997
28104
  ]);
27998
- $clear._yui_label = "clear selection";
27999
28105
  let $el = (0, _yuneta_gobj_js.createElement2)([
28000
28106
  "div",
28001
28107
  {
@@ -28011,7 +28117,7 @@ function yui_selection_bar(t, opts) {
28011
28117
  $count.textContent = t("{{n}} selected", { n: count });
28012
28118
  for (let $b of $buttons.concat([$clear])) {
28013
28119
  let $label = label_of($b);
28014
- let key = $b._yui_label;
28120
+ const key = $b === $clear ? "clear selection" : $b._yui_label;
28015
28121
  if ($label && key) $label.textContent = t(key);
28016
28122
  $b.title = key ? t(key) : "";
28017
28123
  $b.setAttribute("aria-label", $b.title);
@@ -30093,7 +30199,7 @@ function open_form_dialog(gobj, mode, record) {
30093
30199
  return;
30094
30200
  }
30095
30201
  let pkey = desc.pkey || "id";
30096
- let title = mode === "create" ? "new" : mode === "view" ? "view" : "edit";
30202
+ const title = mode === "create" ? "new" : mode === "view" ? "view" : "edit";
30097
30203
  let title_prefix = "";
30098
30204
  if (mode !== "create") {
30099
30205
  let rid = record ? String(record[pkey] ?? "") : "";
@@ -60774,17 +60880,41 @@ function css_color_or_null(value) {
60774
60880
  * The markup of one toolbar item. Everything that goes into it is
60775
60881
  * escaped, the read-out branch too: its text, title and value are
60776
60882
  * values (a zoom figure, a translated title), never markup.
60883
+ *
60884
+ * An item is a BUTTON for the keyboard too: `role="button"` and
60885
+ * `tabindex="0"`, and YuiToolbar presses it on Enter or Space.
60886
+ * `title_key` is the i18n key of `title`: written as
60887
+ * `data-i18n-title` / `data-i18n-aria-label`, so the name carries
60888
+ * its key like every other control's.
60889
+ *
60890
+ * A `disabled` item is born the way disableElements() (lib_graph.js)
60891
+ * leaves one: `aria-disabled`, out of the Tab order, and the
60892
+ * tabindex it takes once enabled kept in `data-enabled-tabindex`,
60893
+ * where enableElements() finds it. A toggle (`pressed` given, true
60894
+ * or false) says its state in `aria-pressed`, as its look says it
60895
+ * with `pressed_state`. `data-control` names the CONTROL the item
60896
+ * is, whatever state it shows: `control` when the item gives one
60897
+ * (full screen is `request-fullscreen` or `exit-fullscreen`, and
60898
+ * one control), its `value` otherwise -- it is what a rebuild puts
60899
+ * the focus back on.
60777
60900
  ***************************************************************/
60778
60901
  function toolbar_item_html(item) {
60779
60902
  if (item.separator) return `<div class="g6-toolbar-sep" aria-hidden="true"></div>`;
60780
60903
  const extra_class = item.className ? ` ${(0, _yuneta_gobj_js.escapeHtml)(String(item.className))}` : "";
60781
60904
  const title = (0, _yuneta_gobj_js.escapeHtml)(String(item.title ?? ""));
60782
- if (item.readout) return `<div class="g6-toolbar-readout${extra_class}" title="${title}" aria-label="${title}">${(0, _yuneta_gobj_js.escapeHtml)(String(item.text ?? ""))}</div>`;
60783
- const disabled = item.disabled ? " disabled" : "";
60905
+ let i18n_attrs = "";
60906
+ if (item.title_key) {
60907
+ const key = (0, _yuneta_gobj_js.escapeHtml)(String(item.title_key));
60908
+ i18n_attrs = ` data-i18n-title="${key}" data-i18n-aria-label="${key}"`;
60909
+ }
60910
+ if (item.readout) return `<div class="g6-toolbar-readout${extra_class}" title="${title}" aria-label="${title}"${i18n_attrs}>${(0, _yuneta_gobj_js.escapeHtml)(String(item.text ?? ""))}</div>`;
60911
+ const state = item.disabled ? " tabindex=\"-1\" aria-disabled=\"true\" data-enabled-tabindex=\"0\" disabled" : " tabindex=\"0\"";
60912
+ const pressed = item.pressed === void 0 ? "" : ` aria-pressed="${item.pressed ? "true" : "false"}"`;
60913
+ const control = (0, _yuneta_gobj_js.escapeHtml)(String(item.control ?? item.value));
60784
60914
  const is_text = item.text !== void 0;
60785
60915
  const kind_class = is_text ? " g6-toolbar-item-text" : "";
60786
60916
  const body = is_text ? `<span class="g6-toolbar-text">${(0, _yuneta_gobj_js.escapeHtml)(String(item.text))}</span>` : `<svg aria-hidden="true" focusable="false"><use xlink:href="#${(0, _yuneta_gobj_js.escapeHtml)(String(item.id))}"></use></svg>`;
60787
- return `<div class="g6-toolbar-item${kind_class}${extra_class}" value="${(0, _yuneta_gobj_js.escapeHtml)(String(item.value))}" title="${title}" aria-label="${title}"${disabled}>${body}</div>`;
60917
+ return `<div class="g6-toolbar-item${kind_class}${extra_class}" role="button"${state}${pressed} value="${(0, _yuneta_gobj_js.escapeHtml)(String(item.value))}" data-control="${control}" title="${title}" aria-label="${title}"${i18n_attrs}>${body}</div>`;
60788
60918
  }
60789
60919
  /***************************************************************
60790
60920
  * YuiToolbar — G6 Toolbar subclass that adds per-item className
@@ -60799,6 +60929,11 @@ function toolbar_item_html(item) {
60799
60929
  * readout — a text item that REPORTS instead of acting (the
60800
60930
  * zoom level); it is not a button
60801
60931
  * separator — a group divider, not an item
60932
+ * title_key — the i18n key of `title` (see toolbar_item_html)
60933
+ * pressed — boolean; the item is a TOGGLE, and this is its
60934
+ * state (`aria-pressed`)
60935
+ * control — the identity of the control when its `value`
60936
+ * changes with its state (full screen enter/exit)
60802
60937
  *
60803
60938
  * The base class fires onClick only for elements whose class list
60804
60939
  * contains `g6-toolbar-item`, so a separator and a readout carry
@@ -60809,12 +60944,117 @@ function toolbar_item_html(item) {
60809
60944
  * functions (set_submit_state, disableElements, …) work on toolbar
60810
60945
  * icons exactly like on regular Bulma/FA buttons — no special
60811
60946
  * icon-specific helpers needed.
60947
+ *
60948
+ * The items are buttons for the KEYBOARD as well:
60949
+ * - Enter or Space CLICKS the focused item, and the click bubbles
60950
+ * to the base class's own handler, so the key reaches the same
60951
+ * onClick a pointer does. A `disabled` item takes the key and
60952
+ * does nothing, as its `pointer-events: none` does for a press.
60953
+ * - A POINTER press does not move the focus to an item (it stays
60954
+ * on the canvas, where the graph's keys are heard), the way the
60955
+ * floating buttons keep it (see keep_focus_on_press).
60956
+ * - A disabled item is out of the Tab order and says it is
60957
+ * disabled (`aria-disabled`), from its first render
60958
+ * (toolbar_item_html) and on every change (disableElements /
60959
+ * enableElements in lib_graph.js).
60960
+ * - A rebuild (update) replaces the innerHTML, the focused item
60961
+ * with it: the focus goes back to the item of the same CONTROL
60962
+ * (`data-control`: Enter on "enter full screen" lands on "exit
60963
+ * full screen"), or to the canvas when there is none. And a
60964
+ * toolbar that goes away (destroy) hands it to the canvas
60965
+ * first. A removed element that held the focus leaves it on
60966
+ * <body> in Firefox, which fires no focusout for it, and the
60967
+ * graph goes deaf.
60812
60968
  ***************************************************************/
60813
60969
  var YuiToolbar = class extends _antv_g6.Toolbar {
60970
+ constructor(context, options) {
60971
+ super(context, options);
60972
+ this.$element.addEventListener("pointerdown", keep_focus_on_toolbar_press);
60973
+ this.$element.addEventListener("keydown", press_toolbar_item_on_key);
60974
+ }
60975
+ async update(options) {
60976
+ const control = focused_toolbar_item_control(this.$element);
60977
+ await super.update(options);
60978
+ if (control !== null) refocus_toolbar_item(this.$element, control);
60979
+ }
60980
+ destroy() {
60981
+ if (!this.destroyed && this.$element) {
60982
+ this.$element.removeEventListener("pointerdown", keep_focus_on_toolbar_press);
60983
+ this.$element.removeEventListener("keydown", press_toolbar_item_on_key);
60984
+ if (this.$element.contains(document.activeElement)) focus_main_canvas(this.$element.parentNode);
60985
+ }
60986
+ super.destroy();
60987
+ }
60814
60988
  async getDOMContent() {
60815
60989
  return (await this.options.getItems()).map(toolbar_item_html).join("");
60816
60990
  }
60817
60991
  };
60992
+ /***************************************************************
60993
+ * A POINTER press on a button of this view does not take the
60994
+ * focus. The container's capture listener has just put it on the
60995
+ * canvas, where the graph's keys are heard; a press that moved it
60996
+ * on to the button would leave it on something this view removes
60997
+ * -- Escape deselects and takes the floating buttons away, the ✕
60998
+ * closes its popover, a rebuild replaces the toolbar -- and
60999
+ * Firefox fires no focusout for a removed element: the focus falls
61000
+ * to <body> and the graph hears nothing until it is clicked again.
61001
+ * Cancelling the pointerdown cancels the mousedown that moves the
61002
+ * focus; the click still comes, and Tab still reaches the button.
61003
+ ***************************************************************/
61004
+ function keep_focus_on_press(el) {
61005
+ el.addEventListener("pointerdown", (e) => {
61006
+ e.preventDefault();
61007
+ });
61008
+ }
61009
+ function keep_focus_on_toolbar_press(e) {
61010
+ let target = e.target;
61011
+ if (target && typeof target.closest === "function" && target.closest(".g6-toolbar-item")) e.preventDefault();
61012
+ }
61013
+ /***************************************************************
61014
+ * The keyboard to the graph: the focus on its main canvas.
61015
+ ***************************************************************/
61016
+ function focus_main_canvas($container) {
61017
+ if (!$container) return;
61018
+ let $canvas = main_canvas_of($container);
61019
+ if ($canvas && $canvas.isConnected && typeof $canvas.focus === "function") $canvas.focus({ preventScroll: true });
61020
+ }
61021
+ /***************************************************************
61022
+ * Enter or Space on a toolbar item clicks it; the click bubbles
61023
+ * to G6's toolbar handler, which reads the item's `value`.
61024
+ ***************************************************************/
61025
+ function press_toolbar_item_on_key(e) {
61026
+ if (e.key !== "Enter" && e.key !== " ") return;
61027
+ let target = e.target;
61028
+ let item = target && typeof target.closest === "function" ? target.closest(".g6-toolbar-item") : null;
61029
+ if (!item) return;
61030
+ e.preventDefault();
61031
+ e.stopPropagation();
61032
+ if (item.hasAttribute("disabled")) return;
61033
+ item.click();
61034
+ }
61035
+ /***************************************************************
61036
+ * The control (`data-control`) of the toolbar item that has the
61037
+ * focus, or null.
61038
+ ***************************************************************/
61039
+ function focused_toolbar_item_control($toolbar) {
61040
+ let active = document.activeElement;
61041
+ if (!$toolbar || !active || active === $toolbar || !$toolbar.contains(active)) return null;
61042
+ if (!active.classList || !active.classList.contains("g6-toolbar-item")) return null;
61043
+ return active.getAttribute("data-control");
61044
+ }
61045
+ /***************************************************************
61046
+ * After a rebuild: the focus on the item of that control, in
61047
+ * whatever state the rebuild shows it, or on the canvas when the
61048
+ * rebuild has none (a fold took it away).
61049
+ ***************************************************************/
61050
+ function refocus_toolbar_item($toolbar, control) {
61051
+ let items = $toolbar.querySelectorAll(".g6-toolbar-item");
61052
+ for (let i = 0; i < items.length; i++) if (items[i].getAttribute("data-control") === control) {
61053
+ items[i].focus({ preventScroll: true });
61054
+ return;
61055
+ }
61056
+ focus_main_canvas($toolbar.parentNode);
61057
+ }
60818
61058
  (0, _antv_g6.register)(_antv_g6.ExtensionCategory.PLUGIN, "yui-toolbar", YuiToolbar);
60819
61059
  ensure_drag_canvas_patch();
60820
61060
  ensure_brush_select_owned();
@@ -60979,6 +61219,7 @@ var PRIVATE_DATA$4 = {
60979
61219
  _saved_graph_properties: {},
60980
61220
  _graphs_writes_owed: {},
60981
61221
  _graphs_load: 0,
61222
+ _save_lit: false,
60982
61223
  yet_showed: false,
60983
61224
  edit_mode: false,
60984
61225
  operation_mode: null,
@@ -61012,6 +61253,7 @@ var PRIVATE_DATA$4 = {
61012
61253
  _detail_node_id: null,
61013
61254
  _delete_confirm_el: null,
61014
61255
  _unlink_confirm_el: null,
61256
+ _confirm_questions: {},
61015
61257
  _create_popover_el: null,
61016
61258
  _context_node_id: null,
61017
61259
  _context_port_key: null,
@@ -61020,6 +61262,7 @@ var PRIVATE_DATA$4 = {
61020
61262
  _link_source: null,
61021
61263
  _link_icon_el: null,
61022
61264
  _link_drag_svg: null,
61265
+ _link_drag: null,
61023
61266
  _link_valid_hooks: [],
61024
61267
  _link_saved_styles: [],
61025
61268
  _focus_topic: null,
@@ -61263,6 +61506,7 @@ function build_graph$1(gobj) {
61263
61506
  plugins: []
61264
61507
  });
61265
61508
  graph.setTheme(priv.theme);
61509
+ keep_canvas_tab_order(gobj, graph);
61266
61510
  priv._unforward_wheel = yui_graph_forward_wheel(graph, priv.$container, { selector: ".TREEDB_CARD, .TREEDB_PILLS" });
61267
61511
  graph.setLayout(layout);
61268
61512
  priv.graph_rendered = false;
@@ -61420,8 +61664,6 @@ function update_toolbar(gobj) {
61420
61664
  if (graph_get_plugin(gobj, "toolbar")) graph.updatePlugin({ key: "toolbar" });
61421
61665
  configure_toolbar_edit(gobj);
61422
61666
  if (graph_get_plugin(gobj, "toolbar-edit")) graph.updatePlugin({ key: "toolbar-edit" });
61423
- update_zoom_selection_button(gobj);
61424
- update_selection_mode_button(gobj);
61425
61667
  }
61426
61668
  /************************************************************
61427
61669
  * The floating toolbars follow the theme.
@@ -61472,13 +61714,15 @@ function toolbar_fold_item(gobj) {
61472
61714
  id: "g6-icon-toolbar-show",
61473
61715
  value: "toolbar-toggle",
61474
61716
  className: "EV_TOGGLE_TOOLBARS",
61475
- title: (0, i18next.t)("show toolbar")
61717
+ title: (0, i18next.t)("show toolbar"),
61718
+ title_key: "show toolbar"
61476
61719
  };
61477
61720
  return {
61478
61721
  id: "g6-icon-toolbar-hide",
61479
61722
  value: "toolbar-toggle",
61480
61723
  className: "EV_TOGGLE_TOOLBARS",
61481
- title: (0, i18next.t)("hide toolbar")
61724
+ title: (0, i18next.t)("hide toolbar"),
61725
+ title_key: "hide toolbar"
61482
61726
  };
61483
61727
  }
61484
61728
  function toolbar_style(extra) {
@@ -61544,38 +61788,44 @@ function configure_toolbar(gobj) {
61544
61788
  id: "g6-icon-zoom-in",
61545
61789
  value: "zoom-in",
61546
61790
  className: "EV_ZOOM_IN",
61547
- title: (0, i18next.t)("zoom in")
61791
+ title: (0, i18next.t)("zoom in"),
61792
+ title_key: "zoom in"
61548
61793
  },
61549
61794
  {
61550
61795
  id: "g6-icon-zoom-out",
61551
61796
  value: "zoom-out",
61552
61797
  className: "EV_ZOOM_OUT",
61553
- title: (0, i18next.t)("zoom out")
61798
+ title: (0, i18next.t)("zoom out"),
61799
+ title_key: "zoom out"
61554
61800
  },
61555
61801
  {
61556
61802
  readout: true,
61557
61803
  className: "G6_ZOOM_LEVEL",
61558
61804
  text: zoom_percent_text(gobj),
61559
- title: (0, i18next.t)("zoom level")
61805
+ title: (0, i18next.t)("zoom level"),
61806
+ title_key: "zoom level"
61560
61807
  },
61561
61808
  { separator: true },
61562
61809
  {
61563
61810
  id: "g6-icon-fit",
61564
61811
  value: "auto-fit",
61565
61812
  className: "EV_AUTO_FIT",
61566
- title: (0, i18next.t)("auto fit")
61813
+ title: (0, i18next.t)("auto fit"),
61814
+ title_key: "auto fit"
61567
61815
  },
61568
61816
  {
61569
61817
  text: "1:1",
61570
61818
  value: "reset",
61571
61819
  className: "EV_ZOOM_RESET",
61572
- title: (0, i18next.t)("actual size")
61820
+ title: (0, i18next.t)("actual size"),
61821
+ title_key: "actual size"
61573
61822
  },
61574
61823
  {
61575
61824
  id: "g6-icon-anchor",
61576
61825
  value: "anchor",
61577
61826
  className: "EV_TOGGLE_ANCHOR" + (priv.anchor_state === "on" ? " pressed_state" : "") + (priv.anchor_state === "arming" ? " color_pending_state" : ""),
61578
- title: anchor_title(priv.anchor_state)
61827
+ pressed: priv.anchor_state === "on",
61828
+ ...anchor_title(priv.anchor_state)
61579
61829
  }
61580
61830
  ];
61581
61831
  if (priv.edit_mode) items.push({
@@ -61583,21 +61833,26 @@ function configure_toolbar(gobj) {
61583
61833
  value: "fit-selection",
61584
61834
  className: "EV_ZOOM_SELECTION",
61585
61835
  title: (0, i18next.t)("zoom to selection"),
61586
- disabled: true
61836
+ title_key: "zoom to selection",
61837
+ disabled: !selection_is_painted(gobj)
61587
61838
  });
61588
61839
  if ((0, _yuneta_gobj_js.gobj_read_bool_attr)(gobj, "with_fullscreen")) {
61589
61840
  items.push({ separator: true });
61590
61841
  if (priv.is_fullscreen) items.push({
61591
61842
  id: "g6-icon-fullscreen-exit",
61592
61843
  value: "exit-fullscreen",
61844
+ control: "fullscreen",
61593
61845
  className: "EV_EXIT_FULLSCREEN",
61594
- title: (0, i18next.t)("exit full screen")
61846
+ title: (0, i18next.t)("exit full screen"),
61847
+ title_key: "exit full screen"
61595
61848
  });
61596
61849
  else items.push({
61597
61850
  id: "g6-icon-fullscreen",
61598
61851
  value: "request-fullscreen",
61852
+ control: "fullscreen",
61599
61853
  className: "EV_REQUEST_FULLSCREEN",
61600
- title: (0, i18next.t)("enter full screen")
61854
+ title: (0, i18next.t)("enter full screen"),
61855
+ title_key: "enter full screen"
61601
61856
  });
61602
61857
  }
61603
61858
  if (toolbars_can_collapse(gobj)) {
@@ -61658,46 +61913,53 @@ function configure_toolbar_edit(gobj) {
61658
61913
  position: "left-top",
61659
61914
  style: toolbar_style(),
61660
61915
  getItems: () => {
61916
+ let lit = edit_buttons_lit(gobj);
61661
61917
  return [
61662
61918
  {
61663
61919
  id: "g6-icon-plus",
61664
61920
  value: "create-node",
61665
61921
  className: "EV_CREATE_NODE_BTN color_create_state",
61666
- title: (0, i18next.t)("create node")
61922
+ title: (0, i18next.t)("create node"),
61923
+ title_key: "create node"
61667
61924
  },
61668
61925
  {
61669
61926
  id: "g6-icon-select-mode",
61670
61927
  value: "selection-mode",
61671
61928
  className: "EV_TOGGLE_SELECTION_MODE" + (priv.selection_mode ? " pressed_state" : ""),
61672
- title: (0, i18next.t)("selection mode")
61929
+ pressed: !!priv.selection_mode,
61930
+ title: (0, i18next.t)("selection mode"),
61931
+ title_key: "selection mode"
61673
61932
  },
61674
61933
  {
61675
61934
  id: "g6-icon-save",
61676
61935
  value: "save",
61677
- className: "EV_SAVE_GRAPH",
61936
+ className: "EV_SAVE_GRAPH" + (lit.save ? " color_submit_state" : ""),
61678
61937
  title: (0, i18next.t)("save"),
61679
- disabled: true
61938
+ title_key: "save",
61939
+ disabled: !lit.save
61680
61940
  },
61681
61941
  {
61682
61942
  id: "g6-icon-undo",
61683
61943
  value: "undo",
61684
- className: "EV_HISTORY_UNDO",
61944
+ className: "EV_HISTORY_UNDO" + (lit.undo ? " color_active_state" : ""),
61685
61945
  title: (0, i18next.t)("undo"),
61686
- disabled: true
61946
+ title_key: "undo",
61947
+ disabled: !lit.undo
61687
61948
  },
61688
61949
  {
61689
61950
  id: "g6-icon-redo",
61690
61951
  value: "redo",
61691
- className: "EV_HISTORY_REDO",
61952
+ className: "EV_HISTORY_REDO" + (lit.redo ? " color_active_state" : ""),
61692
61953
  title: (0, i18next.t)("redo"),
61693
- disabled: true
61954
+ title_key: "redo",
61955
+ disabled: !lit.redo
61694
61956
  }
61695
61957
  ];
61696
61958
  },
61697
61959
  onClick: (value) => {
61698
61960
  switch (value) {
61699
61961
  case "create-node":
61700
- toggle_create_popover(gobj);
61962
+ (0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_TOGGLE_CREATE_POPOVER", {}, gobj);
61701
61963
  break;
61702
61964
  case "selection-mode":
61703
61965
  (0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_TOGGLE_SELECTION_MODE", {}, gobj);
@@ -62758,28 +63020,50 @@ function update_history_buttons(gobj) {
62758
63020
  if (history.canUndo()) {
62759
63021
  enableElements($container, ".EV_HISTORY_UNDO");
62760
63022
  set_active_state($container, ".EV_HISTORY_UNDO", true);
62761
- enableElements($container, ".EV_SAVE_GRAPH");
62762
- set_submit_state($container, ".EV_SAVE_GRAPH", true);
63023
+ light_save_button(gobj, true);
62763
63024
  } else {
62764
63025
  disableElements($container, ".EV_HISTORY_UNDO");
62765
63026
  set_active_state($container, ".EV_HISTORY_UNDO", false);
62766
- if (graphs_write_is_owed(gobj)) {
62767
- enableElements($container, ".EV_SAVE_GRAPH");
62768
- set_submit_state($container, ".EV_SAVE_GRAPH", true);
62769
- } else {
62770
- disableElements($container, ".EV_SAVE_GRAPH");
62771
- set_submit_state($container, ".EV_SAVE_GRAPH", false);
62772
- }
63027
+ light_save_button(gobj, graphs_write_is_owed(gobj));
62773
63028
  }
62774
63029
  } else {
62775
63030
  disableElements($container, ".EV_HISTORY_REDO");
62776
63031
  set_active_state($container, ".EV_HISTORY_REDO", false);
62777
63032
  disableElements($container, ".EV_HISTORY_UNDO");
62778
63033
  set_active_state($container, ".EV_HISTORY_UNDO", false);
63034
+ light_save_button(gobj, graphs_write_is_owed(gobj));
62779
63035
  }
62780
63036
  }
62781
63037
  }
62782
63038
  /************************************************************
63039
+ * What Save, Undo and Redo show when the edit toolbar is
63040
+ * built (its getItems): Undo and Redo what the history can do
63041
+ * now; Save what it was last told (`_save_lit`, see
63042
+ * light_save_button), because more than the history lights
63043
+ * it -- a change the history does not record
63044
+ * (mark_graph_dirty), a write owed -- and update_history_
63045
+ * buttons() is where those are weighed.
63046
+ ************************************************************/
63047
+ function edit_buttons_lit(gobj) {
63048
+ let history = graph_get_plugin(gobj, "history");
63049
+ return {
63050
+ undo: !!history && history.canUndo(),
63051
+ redo: !!history && history.canRedo(),
63052
+ save: !!gobj.priv._save_lit
63053
+ };
63054
+ }
63055
+ /************************************************************
63056
+ * Light the Save, or put it out: on the live element, and in
63057
+ * `_save_lit`, where the next rebuild of the toolbar reads it.
63058
+ ************************************************************/
63059
+ function light_save_button(gobj, lit) {
63060
+ let $container = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "$container");
63061
+ gobj.priv._save_lit = !!lit;
63062
+ if (lit) enableElements($container, ".EV_SAVE_GRAPH");
63063
+ else disableElements($container, ".EV_SAVE_GRAPH");
63064
+ set_submit_state($container, ".EV_SAVE_GRAPH", !!lit);
63065
+ }
63066
+ /************************************************************
62783
63067
  * Graph utility functions
62784
63068
  *
62785
63069
  * From the G6 documentation:
@@ -63655,20 +63939,59 @@ function floating_popover_dx() {
63655
63939
  return 4 + floating_icon_size() + 4;
63656
63940
  }
63657
63941
  /************************************************************
63942
+ * A key press IS the click on an element drawn as a button.
63943
+ *
63944
+ * Enter or Space clicks it, so the keyboard reaches the very
63945
+ * handler the pointer does, whatever that handler is. Stopped
63946
+ * here, as the click is: this view's container forwards every
63947
+ * keydown to G6 (its shortcuts, and EV_KEY_DOWN), and a key
63948
+ * that pressed a button is not the graph's. Space would scroll
63949
+ * the page as well. Any other key goes on to the graph, so
63950
+ * Escape and Delete keep doing there what they do.
63951
+ ************************************************************/
63952
+ function click_on_enter_or_space(el) {
63953
+ el.addEventListener("keydown", (e) => {
63954
+ if (e.key === "Enter" || e.key === " ") {
63955
+ e.preventDefault();
63956
+ e.stopPropagation();
63957
+ el.click();
63958
+ }
63959
+ });
63960
+ }
63961
+ /************************************************************
63658
63962
  * Create a floating circular icon button.
63659
- * @param {string} svgKey - key into SVG_ICONS
63660
- * @param {string} color - border & text color (e.g. '#1890ff')
63661
- * @param {number} left - CSS left in px
63662
- * @param {number} top - CSS top in px
63663
- * @param {string} title - tooltip text
63664
- * @param {Function} onClick
63963
+ *
63964
+ * A button for the keyboard too: `role` and `tabindex`, and
63965
+ * Enter or Space click it (click_on_enter_or_space). Its name
63966
+ * carries its KEY, so refresh_language() renames it on a
63967
+ * language switch; a key handed in here goes in
63968
+ * G6_NODES_TREE_I18N_KEYS, because no scan sees it.
63969
+ *
63970
+ * Its click does one thing: it sends `event` to `gobj`, with
63971
+ * `kw` -- the plain identity of what the button belongs to --
63972
+ * and the ACTION does the work, where the `machine` trace sees
63973
+ * it. A pointer press does not take the focus
63974
+ * (keep_focus_on_press).
63975
+ *
63976
+ * @param {string} svgKey - key into SVG_ICONS
63977
+ * @param {string} color - border & text color (e.g. '#1890ff')
63978
+ * @param {number} left - CSS left in px
63979
+ * @param {number} top - CSS top in px
63980
+ * @param {string} title_key - i18n key of its title and aria-label
63981
+ * @param {object} gobj - who hears the click
63982
+ * @param {string} event - an input event of this gclass
63983
+ * @param {object} kw - plain JSON identity, copied per click
63665
63984
  * @returns {HTMLElement}
63666
63985
  ************************************************************/
63667
- function create_floating_icon(svgKey, color, left, top, title, onClick) {
63986
+ function create_floating_icon(svgKey, color, left, top, title_key, gobj, event, kw) {
63668
63987
  const size = floating_icon_size();
63669
63988
  const el = document.createElement("div");
63670
- el.title = title;
63671
- el.setAttribute("aria-label", title);
63989
+ el.setAttribute("role", "button");
63990
+ el.setAttribute("tabindex", "0");
63991
+ el.setAttribute("title", (0, i18next.t)(title_key));
63992
+ el.setAttribute("data-i18n-title", title_key);
63993
+ el.setAttribute("aria-label", (0, i18next.t)(title_key));
63994
+ el.setAttribute("data-i18n-aria-label", title_key);
63672
63995
  el.innerHTML = SVG_ICONS[svgKey] || "";
63673
63996
  el.style.cssText = "position:absolute;left:" + left + "px;top:" + top + "px;width:" + size + "px;height:" + size + "px;display:flex;align-items:center;justify-content:center;background:#fff;border:1px solid " + color + ";border-radius:50%;cursor:pointer;pointer-events:all;z-index:11;touch-action:none;box-shadow:0 2px 6px rgba(0,0,0,0.15);color:" + color + ";";
63674
63997
  const $svg = el.querySelector("svg");
@@ -63679,19 +64002,33 @@ function create_floating_icon(svgKey, color, left, top, title, onClick) {
63679
64002
  }
63680
64003
  el.addEventListener("click", (e) => {
63681
64004
  e.stopPropagation();
63682
- onClick();
64005
+ (0, _yuneta_gobj_js.gobj_send_event)(gobj, event, (0, _yuneta_gobj_js.json_deep_copy)(kw || {}), gobj);
63683
64006
  });
64007
+ keep_focus_on_press(el);
64008
+ click_on_enter_or_space(el);
63684
64009
  return el;
63685
64010
  }
63686
64011
  /************************************************************
63687
64012
  * Remove one or more overlay elements from priv and the DOM.
64013
+ *
64014
+ * An overlay that holds the focus -- a floating button reached
64015
+ * with Tab, the ✕ of the detail popover, a field of a popover --
64016
+ * gives it back to the canvas BEFORE it goes. Removing the
64017
+ * focused element is a focus loss the browsers do not report
64018
+ * alike: Chromium fires a focusout, which _on_focusout_restore
64019
+ * answers; Firefox fires none and leaves the focus on <body>,
64020
+ * and the graph is deaf to Escape, Delete and ctrl+A until the
64021
+ * next click on it.
64022
+ *
63688
64023
  * @param {object} gobj
63689
64024
  * @param {...string} keys - priv property names to remove
63690
64025
  ************************************************************/
63691
64026
  function hide_overlay(gobj, ...keys) {
63692
64027
  let priv = gobj.priv;
63693
64028
  for (let key of keys) if (priv[key]) {
63694
- priv[key].remove();
64029
+ let el = priv[key];
64030
+ if (el.contains(document.activeElement) && !(0, _yuneta_gobj_js.gobj_is_destroying)(gobj)) focus_main_canvas(priv.$container);
64031
+ el.remove();
63695
64032
  priv[key] = null;
63696
64033
  }
63697
64034
  }
@@ -63736,9 +64073,7 @@ function graphs_write_is_owed(gobj) {
63736
64073
  return Object.keys(gobj.priv._graphs_writes_owed).length > 0;
63737
64074
  }
63738
64075
  function mark_graph_dirty(gobj) {
63739
- let $container = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "$container");
63740
- enableElements($container, ".EV_SAVE_GRAPH");
63741
- set_submit_state($container, ".EV_SAVE_GRAPH", true);
64076
+ light_save_button(gobj, true);
63742
64077
  }
63743
64078
  /************************************************************
63744
64079
  * Extract width/height from a style's size property.
@@ -63753,13 +64088,15 @@ function extract_size(style) {
63753
64088
  }
63754
64089
  /************************************************************
63755
64090
  * Show a pair of floating icons (gear + trash) next to an
63756
- * element. Returns {icon_el, delete_el}.
64091
+ * element. The titles are i18n KEYS (see create_floating_icon);
64092
+ * each click sends its event to `gobj` with the same identity
64093
+ * `kw`. Returns {icon_el, delete_el}.
63757
64094
  ************************************************************/
63758
- function show_dual_icons(gobj, x, y, gear_title, gear_click, trash_title, trash_click) {
64095
+ function show_dual_icons(gobj, x, y, gear_key, gear_event, trash_key, trash_event, kw) {
63759
64096
  let priv = gobj.priv;
63760
- const icon_el = create_floating_icon("gear", "#1890ff", x, y, gear_title, gear_click);
64097
+ const icon_el = create_floating_icon("gear", "#1890ff", x, y, gear_key, gobj, gear_event, kw);
63761
64098
  priv.$container.appendChild(icon_el);
63762
- const delete_el = create_floating_icon("trash", "#ff4d4f", x, y + floating_icon_step(), trash_title, trash_click);
64099
+ const delete_el = create_floating_icon("trash", "#ff4d4f", x, y + floating_icon_step(), trash_key, gobj, trash_event, kw);
63763
64100
  priv.$container.appendChild(delete_el);
63764
64101
  return {
63765
64102
  icon_el,
@@ -63793,19 +64130,34 @@ function touch_control_css() {
63793
64130
  if (!coarse_pointer()) return "";
63794
64131
  return "min-height:44px;font-size:16px;";
63795
64132
  }
64133
+ var form_control_seq = 0;
63796
64134
  function create_form_label(parent, text) {
63797
64135
  let label = document.createElement("label");
63798
64136
  label.textContent = (0, i18next.t)(text);
64137
+ label.setAttribute("data-i18n", text);
63799
64138
  label.style.cssText = "display:block;margin-bottom:4px;font-weight:500;";
63800
64139
  parent.appendChild(label);
63801
64140
  return label;
63802
64141
  }
64142
+ function name_form_control(parent, control) {
64143
+ let label = parent.lastElementChild;
64144
+ if (!label || label.tagName !== "LABEL") return;
64145
+ let key = label.getAttribute("data-i18n");
64146
+ if (!key) return;
64147
+ control.id = "g6-form-control-" + ++form_control_seq;
64148
+ label.htmlFor = control.id;
64149
+ control.setAttribute("title", (0, i18next.t)(key));
64150
+ control.setAttribute("data-i18n-title", key);
64151
+ control.setAttribute("aria-label", (0, i18next.t)(key));
64152
+ control.setAttribute("data-i18n-aria-label", key);
64153
+ }
63803
64154
  function create_form_color_input(parent, value, onInput) {
63804
64155
  let input = document.createElement("input");
63805
64156
  input.type = "color";
63806
64157
  input.value = value;
63807
64158
  input.style.cssText = "width:100%;height:30px;padding:0;border:1px solid var(--bulma-border-weak, #d9d9d9);border-radius:4px;cursor:pointer;margin-bottom:10px;" + touch_control_css();
63808
64159
  if (onInput) input.addEventListener("input", onInput);
64160
+ name_form_control(parent, input);
63809
64161
  parent.appendChild(input);
63810
64162
  return input;
63811
64163
  }
@@ -63817,6 +64169,7 @@ function create_form_number_input(parent, value, min, max, onInput) {
63817
64169
  input.value = value;
63818
64170
  input.style.cssText = "width:100%;padding:4px 6px;background:var(--bulma-scheme-main-bis, #fff);color:var(--bulma-text-strong, #333);border:1px solid var(--bulma-border-weak, #d9d9d9);border-radius:4px;box-sizing:border-box;margin-bottom:10px;" + touch_control_css();
63819
64171
  if (onInput) input.addEventListener("input", onInput);
64172
+ name_form_control(parent, input);
63820
64173
  parent.appendChild(input);
63821
64174
  return input;
63822
64175
  }
@@ -63826,17 +64179,31 @@ function create_form_select(parent, options, marginBottom) {
63826
64179
  for (let opt of options) {
63827
64180
  let o = document.createElement("option");
63828
64181
  o.value = opt.value;
63829
- o.textContent = opt.label;
64182
+ o.textContent = (0, i18next.t)(opt.i18n);
64183
+ o.setAttribute("data-i18n", opt.i18n);
63830
64184
  select.appendChild(o);
63831
64185
  }
64186
+ name_form_control(parent, select);
63832
64187
  parent.appendChild(select);
63833
64188
  return select;
63834
64189
  }
63835
64190
  var BTN_STYLE_CANCEL = "flex:1;padding:6px;background:var(--bulma-scheme-main-bis, #fff);color:var(--bulma-text-strong, #333);border:1px solid var(--bulma-border-weak, #d9d9d9);border-radius:4px;cursor:pointer;font-size:13px;font-weight:500;";
63836
- function create_form_button_row(parent, buttons) {
64191
+ /************************************************************
64192
+ * The buttons of a popover: each one SENDS an input event of
64193
+ * this gclass to `gobj`, and the action does the work, where
64194
+ * the `machine` trace sees it (V20.5 #4). A button is
64195
+ * {i18n, style, event, kw, form}
64196
+ * `kw` is the plain identity of what the popover is about
64197
+ * (copied per click); `form` names the controls whose `value`
64198
+ * goes in the kw too, as the control says it -- a string, the
64199
+ * action parses it. The kw says what was asked for, in the
64200
+ * trace: the identity and what the form held when it was
64201
+ * pressed.
64202
+ ************************************************************/
64203
+ function create_form_button_row(parent, buttons, gobj) {
63837
64204
  let btnRow = document.createElement("div");
63838
64205
  btnRow.style.cssText = "display:flex;gap:8px;";
63839
- for (let { i18n, style, onClick } of buttons) {
64206
+ for (let { i18n, style, event, kw, form } of buttons) {
63840
64207
  let btn = document.createElement("button");
63841
64208
  btn.textContent = (0, i18next.t)(i18n);
63842
64209
  btn.setAttribute("data-i18n", i18n);
@@ -63847,18 +64214,28 @@ function create_form_button_row(parent, buttons) {
63847
64214
  btn.style.cssText = style + touch_control_css();
63848
64215
  btn.addEventListener("click", (e) => {
63849
64216
  e.stopPropagation();
63850
- onClick();
64217
+ (0, _yuneta_gobj_js.gobj_send_event)(gobj, event, form_kw(kw, form), gobj);
63851
64218
  });
63852
64219
  btnRow.appendChild(btn);
63853
64220
  }
63854
64221
  parent.appendChild(btnRow);
63855
64222
  return btnRow;
63856
64223
  }
64224
+ function form_kw(kw, form) {
64225
+ let out = (0, _yuneta_gobj_js.json_deep_copy)(kw || {});
64226
+ for (let [name, control] of Object.entries(form || {})) out[name] = String(control.value);
64227
+ return out;
64228
+ }
63857
64229
  /************************************************************
63858
64230
  * Show a confirmation popover next to a target element.
63859
- * Returns the popover element.
64231
+ *
64232
+ * `question` is the plain identity of what is asked, {what,
64233
+ * id|ids}: the confirm button sends it with EV_CONFIRMED and the
64234
+ * cancel button with EV_CANCEL_CONFIRM, and the actions answer.
64235
+ * `priv_key` is where the popover is kept, and it must be the
64236
+ * one CONFIRM_POPOVER_OF names for `question.what`.
63860
64237
  ************************************************************/
63861
- function show_confirm_popover(gobj, target_el, message, confirm_text, confirm_color, onConfirm, priv_key) {
64238
+ function show_confirm_popover(gobj, target_el, message, confirm_text, confirm_color, question, priv_key) {
63862
64239
  hide_overlay(gobj, priv_key);
63863
64240
  let priv = gobj.priv;
63864
64241
  let containerRect = priv.$container.getBoundingClientRect();
@@ -63879,14 +64256,17 @@ function show_confirm_popover(gobj, target_el, message, confirm_text, confirm_co
63879
64256
  create_form_button_row(popover, [{
63880
64257
  i18n: "cancel",
63881
64258
  style: BTN_STYLE_CANCEL,
63882
- onClick: () => hide_overlay(gobj, priv_key)
64259
+ event: "EV_CANCEL_CONFIRM",
64260
+ kw: question
63883
64261
  }, {
63884
64262
  i18n: confirm_text,
63885
64263
  style: "flex:1;padding:6px;background:" + confirm_color + ";color:#fff;border:none;border-radius:4px;cursor:pointer;font-size:13px;font-weight:500;",
63886
- onClick: onConfirm
63887
- }]);
64264
+ event: "EV_CONFIRMED",
64265
+ kw: question
64266
+ }], gobj);
63888
64267
  priv.$container.appendChild(popover);
63889
64268
  priv[priv_key] = popover;
64269
+ priv._confirm_questions[priv_key] = (0, _yuneta_gobj_js.json_deep_copy)(question);
63890
64270
  clamp_popover_position(gobj, popover);
63891
64271
  }
63892
64272
  /************************************************************
@@ -63964,27 +64344,30 @@ function paint_selection(gobj, ids) {
63964
64344
  * Zoom-to-selection can only act on a selection.
63965
64345
  *
63966
64346
  * The attribute is toggled on the element instead of re-rendering
63967
- * the toolbar, which would rebuild its innerHTML -- and with it
63968
- * drop the disabled state of every OTHER button -- each time a
63969
- * card is ticked.
64347
+ * the toolbar, which would rebuild its whole innerHTML each time a
64348
+ * card is ticked. A rebuild asks the same question
64349
+ * (selection_is_painted) in the toolbar's getItems().
63970
64350
  ************************************************************/
64351
+ function selection_is_painted(gobj) {
64352
+ return (gobj.priv._selected_paint_ids || []).length > 0;
64353
+ }
63971
64354
  function update_zoom_selection_button(gobj) {
63972
- let priv = gobj.priv;
63973
64355
  let $container = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "$container");
63974
64356
  if (!$container) return;
63975
64357
  if (!$container.querySelector(".EV_ZOOM_SELECTION")) return;
63976
- if ((priv._selected_paint_ids || []).length > 0) enableElements($container, ".EV_ZOOM_SELECTION");
64358
+ if (selection_is_painted(gobj)) enableElements($container, ".EV_ZOOM_SELECTION");
63977
64359
  else disableElements($container, ".EV_ZOOM_SELECTION");
63978
64360
  }
63979
64361
  /************************************************************
63980
64362
  * The selection mode is a state, so it has to LOOK like one:
63981
64363
  * a PRESSED toggle, not a button wearing one of the palette's
63982
- * action colours (see `pressed_state` in lib_graph.css).
64364
+ * action colours (see `pressed_state` in lib_graph.css). And
64365
+ * it SAYS it, in `aria-pressed`, for whoever does not see it.
63983
64366
  *
63984
64367
  * Toggled on the element rather than re-rendered with the
63985
64368
  * toolbar, for the same reason as the button above: a
63986
- * re-render rebuilds the innerHTML and drops what every OTHER
63987
- * button was told.
64369
+ * re-render rebuilds the whole innerHTML. A rebuild draws the
64370
+ * same state, baked in the edit toolbar's getItems().
63988
64371
  ************************************************************/
63989
64372
  function update_selection_mode_button(gobj) {
63990
64373
  let priv = gobj.priv;
@@ -63992,6 +64375,9 @@ function update_selection_mode_button(gobj) {
63992
64375
  if (!$container) return;
63993
64376
  if (!$container.querySelector(".EV_TOGGLE_SELECTION_MODE")) return;
63994
64377
  set_pressed_state($container, ".EV_TOGGLE_SELECTION_MODE", priv.selection_mode);
64378
+ $container.querySelectorAll(".EV_TOGGLE_SELECTION_MODE").forEach(($el) => {
64379
+ $el.setAttribute("aria-pressed", priv.selection_mode ? "true" : "false");
64380
+ });
63995
64381
  }
63996
64382
  /************************************************************
63997
64383
  * Select a SET of nodes: what a marquee and a shift-click do.
@@ -64446,13 +64832,18 @@ function show_link_icon_if_fkey(gobj) {
64446
64832
  let canvasPos = get_port_canvas_position(gobj, priv._selected_node_id, priv._selected_port_key);
64447
64833
  if (!canvasPos) return;
64448
64834
  let vp = graph.getViewportByCanvas([canvasPos.x, canvasPos.y]);
64449
- let icon = create_floating_icon("link", "#52c41a", vp[0] + floating_icon_size() / 2 + 4, vp[1] + floating_icon_dy(), (0, i18next.t)("link to hook"), () => {
64450
- if (!priv._linking_mode) enter_linking_mode(gobj);
64451
- });
64835
+ let identity = {
64836
+ node_id: priv._selected_node_id,
64837
+ port_key: priv._selected_port_key
64838
+ };
64839
+ let icon = create_floating_icon("link", "#52c41a", vp[0] + floating_icon_size() / 2 + 4, vp[1] + floating_icon_dy(), "link to hook", gobj, "EV_ENTER_LINKING_MODE", identity);
64452
64840
  icon.addEventListener("pointerdown", (e) => {
64453
64841
  e.stopPropagation();
64454
64842
  e.preventDefault();
64455
- start_link_drag(gobj, e);
64843
+ (0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_START_LINK_DRAG", Object.assign((0, _yuneta_gobj_js.json_deep_copy)(identity), {
64844
+ client_x: e.clientX,
64845
+ client_y: e.clientY
64846
+ }), gobj);
64456
64847
  });
64457
64848
  priv.$container.appendChild(icon);
64458
64849
  priv._link_icon_el = icon;
@@ -64502,7 +64893,10 @@ function show_port_icon(gobj) {
64502
64893
  if (!priv._selected_node_id || !priv._selected_port_key) return;
64503
64894
  let xy = port_icon_xy(gobj);
64504
64895
  if (!xy) return;
64505
- let icon = create_floating_icon("gear", "#1890ff", xy.x, xy.y, (0, i18next.t)("port properties"), () => toggle_port_popover(gobj));
64896
+ let icon = create_floating_icon("gear", "#1890ff", xy.x, xy.y, "port properties", gobj, "EV_TOGGLE_PORT_POPOVER", {
64897
+ node_id: priv._selected_node_id,
64898
+ port_key: priv._selected_port_key
64899
+ });
64506
64900
  priv.$container.appendChild(icon);
64507
64901
  priv._port_icon_el = icon;
64508
64902
  }
@@ -64587,7 +64981,7 @@ function show_port_popover(gobj) {
64587
64981
  let shapeSelect = create_form_select(popover, PORT_SHAPES.map((shape) => {
64588
64982
  return {
64589
64983
  value: shape,
64590
- label: (0, i18next.t)(shape)
64984
+ i18n: shape
64591
64985
  };
64592
64986
  }));
64593
64987
  shapeSelect.value = orig_shape;
@@ -64598,31 +64992,40 @@ function show_port_popover(gobj) {
64598
64992
  let scopeSelect = create_form_select(popover, [
64599
64993
  {
64600
64994
  value: "this",
64601
- label: (0, i18next.t)("this port")
64995
+ i18n: "this port"
64602
64996
  },
64603
64997
  {
64604
64998
  value: "same_topic",
64605
- label: (0, i18next.t)("same port in topic")
64999
+ i18n: "same port in topic"
64606
65000
  },
64607
65001
  {
64608
65002
  value: "all",
64609
- label: (0, i18next.t)("all ports")
65003
+ i18n: "all ports"
64610
65004
  }
64611
65005
  ]);
64612
65006
  create_form_button_row(popover, [{
64613
65007
  i18n: "cancel",
64614
65008
  style: BTN_STYLE_CANCEL,
64615
- onClick: () => {
64616
- hide_port_popover(gobj);
65009
+ event: "EV_CANCEL_PORT_POPOVER",
65010
+ kw: {
65011
+ node_id,
65012
+ port_key
64617
65013
  }
64618
65014
  }, {
64619
65015
  i18n: "apply",
64620
65016
  style: "flex:1;padding:6px;background:#1890ff;color:#fff;border:none;border-radius:4px;cursor:pointer;font-size:13px;font-weight:500;",
64621
- onClick: () => {
64622
- drop_preview_undo(gobj, "_port_preview_undo");
64623
- apply_port_properties(gobj, node_id, port_key, shapeSelect.value, parseInt(rInput.value) || orig_r, scopeSelect.value);
65017
+ event: "EV_APPLY_PORT_POPOVER",
65018
+ kw: {
65019
+ node_id,
65020
+ port_key,
65021
+ r_before: orig_r
65022
+ },
65023
+ form: {
65024
+ shape: shapeSelect,
65025
+ r: rInput,
65026
+ scope: scopeSelect
64624
65027
  }
64625
- }]);
65028
+ }], gobj);
64626
65029
  priv.$container.appendChild(popover);
64627
65030
  priv._port_popover_el = popover;
64628
65031
  set_preview_undo(gobj, "_port_preview_undo", undo_port);
@@ -64839,8 +65242,16 @@ function try_complete_link(gobj, clicked_node_id, clicked_port_key) {
64839
65242
  /************************************************************
64840
65243
  * Drag-link: drag from link icon to a hook port.
64841
65244
  * Draws a temporary SVG line from the fkey port to the cursor.
65245
+ *
65246
+ * Run by ac_start_link_drag. The pointer's MOVES only draw the
65247
+ * line and the cursor -- the drag's own picture, no action, and
65248
+ * one event per move would bury the machine trace -- while the
65249
+ * END of the gesture is an event: EV_LINK_DRAG_END {client_x,
65250
+ * client_y, dragged} on the release, EV_LINK_DRAG_CANCEL when
65251
+ * the browser takes the pointer. Their actions take the drag
65252
+ * down (end_link_drag) and link, or leave the linking mode.
64842
65253
  ************************************************************/
64843
- function start_link_drag(gobj, e) {
65254
+ function start_link_drag(gobj, client_x, client_y) {
64844
65255
  let priv = gobj.priv;
64845
65256
  let graph = priv.graph;
64846
65257
  if (!priv._linking_mode) enter_linking_mode(gobj);
@@ -64854,8 +65265,8 @@ function start_link_drag(gobj, e) {
64854
65265
  let line = document.createElementNS("http://www.w3.org/2000/svg", "line");
64855
65266
  line.setAttribute("x1", startX);
64856
65267
  line.setAttribute("y1", startY);
64857
- line.setAttribute("x2", e.clientX - containerRect.left);
64858
- line.setAttribute("y2", e.clientY - containerRect.top);
65268
+ line.setAttribute("x2", client_x - containerRect.left);
65269
+ line.setAttribute("y2", client_y - containerRect.top);
64859
65270
  line.setAttribute("stroke", "#52c41a");
64860
65271
  line.setAttribute("stroke-width", "2");
64861
65272
  line.setAttribute("stroke-dasharray", "6 3");
@@ -64874,41 +65285,47 @@ function start_link_drag(gobj, e) {
64874
65285
  priv.$container.style.cursor = hoverTarget ? "copy" : "not-allowed";
64875
65286
  }
64876
65287
  function onPointerUp(ev) {
64877
- document.removeEventListener("pointermove", onPointerMove);
64878
- document.removeEventListener("pointerup", onPointerUp);
64879
- document.removeEventListener("pointercancel", onPointerCancel);
64880
- priv.$container.style.cursor = "";
64881
- if (priv._link_drag_svg) {
64882
- priv._link_drag_svg.remove();
64883
- priv._link_drag_svg = null;
64884
- }
64885
- if (!dragged) return;
64886
- let cx = ev.clientX - containerRect.left;
64887
- let cy = ev.clientY - containerRect.top;
64888
- let canvasPt = graph.getCanvasByViewport([cx, cy]);
64889
- let target = find_hook_at_point(gobj, canvasPt[0], canvasPt[1]);
64890
- if (target) try_complete_link(gobj, target.node_id, target.port_key);
64891
- else exit_linking_mode(gobj);
65288
+ (0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_LINK_DRAG_END", {
65289
+ client_x: ev.clientX,
65290
+ client_y: ev.clientY,
65291
+ dragged
65292
+ }, gobj);
64892
65293
  }
64893
65294
  function onPointerCancel(ev) {
65295
+ (0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_LINK_DRAG_CANCEL", {}, gobj);
65296
+ }
65297
+ function unwire() {
64894
65298
  document.removeEventListener("pointermove", onPointerMove);
64895
65299
  document.removeEventListener("pointerup", onPointerUp);
64896
65300
  document.removeEventListener("pointercancel", onPointerCancel);
64897
- priv.$container.style.cursor = "";
64898
- if (priv._link_drag_svg) {
64899
- priv._link_drag_svg.remove();
64900
- priv._link_drag_svg = null;
64901
- }
64902
- exit_linking_mode(gobj);
64903
65301
  }
64904
65302
  document.addEventListener("pointermove", onPointerMove);
64905
65303
  document.addEventListener("pointerup", onPointerUp);
64906
65304
  document.addEventListener("pointercancel", onPointerCancel);
64907
- gobj.priv._drag_unwire = function() {
64908
- document.removeEventListener("pointermove", onPointerMove);
64909
- document.removeEventListener("pointerup", onPointerUp);
64910
- document.removeEventListener("pointercancel", onPointerCancel);
65305
+ priv._link_drag = {
65306
+ left: containerRect.left,
65307
+ top: containerRect.top,
65308
+ unwire
64911
65309
  };
65310
+ priv._drag_unwire = unwire;
65311
+ }
65312
+ /************************************************************
65313
+ * Take a link drag down: its listeners, its line, its cursor.
65314
+ * What was linked, or not, is the caller's.
65315
+ ************************************************************/
65316
+ function end_link_drag(gobj) {
65317
+ let priv = gobj.priv;
65318
+ let drag = priv._link_drag;
65319
+ priv._link_drag = null;
65320
+ if (drag) {
65321
+ drag.unwire();
65322
+ if (priv._drag_unwire === drag.unwire) priv._drag_unwire = null;
65323
+ }
65324
+ priv.$container.style.cursor = "";
65325
+ if (priv._link_drag_svg) {
65326
+ priv._link_drag_svg.remove();
65327
+ priv._link_drag_svg = null;
65328
+ }
64912
65329
  }
64913
65330
  /************************************************************
64914
65331
  * Find if a canvas point hits a valid hook port during
@@ -65198,7 +65615,7 @@ function show_edge_icon(gobj) {
65198
65615
  if (!priv._selected_edge_id) return;
65199
65616
  let mid = get_edge_viewport_midpoint(gobj, priv._selected_edge_id);
65200
65617
  if (!mid) return;
65201
- let icons = show_dual_icons(gobj, mid.x + 4, mid.y + floating_icon_dy(), (0, i18next.t)("edge properties"), () => toggle_edge_popover(gobj), (0, i18next.t)("unlink"), () => (0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_REQUEST_UNLINK_EDGE", {}, gobj));
65618
+ let icons = show_dual_icons(gobj, mid.x + 4, mid.y + floating_icon_dy(), "edge properties", "EV_TOGGLE_EDGE_POPOVER", "unlink", "EV_REQUEST_UNLINK_EDGE", { edge_id: priv._selected_edge_id });
65202
65619
  priv._edge_icon_el = icons.icon_el;
65203
65620
  priv._edge_delete_el = icons.delete_el;
65204
65621
  }
@@ -65294,31 +65711,33 @@ function show_edge_popover(gobj) {
65294
65711
  let scopeSelect = create_form_select(popover, [
65295
65712
  {
65296
65713
  value: "this",
65297
- label: (0, i18next.t)("this edge")
65714
+ i18n: "this edge"
65298
65715
  },
65299
65716
  {
65300
65717
  value: "same_type",
65301
- label: (0, i18next.t)("same type edges")
65718
+ i18n: "same type edges"
65302
65719
  },
65303
65720
  {
65304
65721
  value: "all",
65305
- label: (0, i18next.t)("all edges")
65722
+ i18n: "all edges"
65306
65723
  }
65307
65724
  ]);
65308
65725
  create_form_button_row(popover, [{
65309
65726
  i18n: "cancel",
65310
65727
  style: BTN_STYLE_CANCEL,
65311
- onClick: () => {
65312
- deselect_edge(gobj);
65313
- }
65728
+ event: "EV_CANCEL_EDGE_POPOVER",
65729
+ kw: { edge_id }
65314
65730
  }, {
65315
65731
  i18n: "apply",
65316
65732
  style: "flex:1;padding:6px;background:#52c41a;color:#fff;border:none;border-radius:4px;cursor:pointer;font-size:13px;font-weight:500;",
65317
- onClick: () => {
65318
- drop_preview_undo(gobj, "_edge_preview_undo");
65319
- apply_edge_properties(gobj, edge_id, parseInt(lwInput.value) || 2, colorInput.value, scopeSelect.value);
65320
- }
65321
- }]);
65733
+ event: "EV_APPLY_EDGE_POPOVER",
65734
+ kw: { edge_id },
65735
+ form: {
65736
+ line_width: lwInput,
65737
+ color: colorInput,
65738
+ scope: scopeSelect
65739
+ }
65740
+ }], gobj);
65322
65741
  priv.$container.appendChild(popover);
65323
65742
  priv._edge_popover_el = popover;
65324
65743
  set_preview_undo(gobj, "_edge_preview_undo", undo_edge);
@@ -65355,7 +65774,7 @@ function show_node_icon(gobj) {
65355
65774
  if (!priv._selected_node_id || priv._selected_port_key) return;
65356
65775
  let rect = get_node_viewport_rect(gobj, priv._selected_node_id);
65357
65776
  if (!rect) return;
65358
- let icons = show_dual_icons(gobj, rect.right + 4, rect.top + floating_icon_dy(), (0, i18next.t)("node properties"), () => toggle_node_popover(gobj), (0, i18next.t)("delete node"), () => (0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_REQUEST_DELETE_NODE", {}, gobj));
65777
+ let icons = show_dual_icons(gobj, rect.right + 4, rect.top + floating_icon_dy(), "node properties", "EV_TOGGLE_NODE_POPOVER", "delete node", "EV_REQUEST_DELETE_NODE", { node_id: priv._selected_node_id });
65359
65778
  priv._node_icon_el = icons.icon_el;
65360
65779
  priv._node_delete_el = icons.delete_el;
65361
65780
  }
@@ -65446,7 +65865,7 @@ function show_node_popover(gobj) {
65446
65865
  let shapeSelect = create_form_select(popover, NODE_SHAPES.map((shape) => {
65447
65866
  return {
65448
65867
  value: shape,
65449
- label: (0, i18next.t)(shape)
65868
+ i18n: shape
65450
65869
  };
65451
65870
  }));
65452
65871
  shapeSelect.value = nodeData.data && nodeData.data.desc ? node_figure_of(gobj, nodeData.data.desc, nodeData.data.graph_props || {}) : DEFAULT_NODE_SHAPE;
@@ -65455,31 +65874,38 @@ function show_node_popover(gobj) {
65455
65874
  let scopeSelect = create_form_select(popover, [
65456
65875
  {
65457
65876
  value: "this",
65458
- label: (0, i18next.t)("this node")
65877
+ i18n: "this node"
65459
65878
  },
65460
65879
  {
65461
65880
  value: "same_topic",
65462
- label: (0, i18next.t)("same topic nodes")
65881
+ i18n: "same topic nodes"
65463
65882
  },
65464
65883
  {
65465
65884
  value: "all",
65466
- label: (0, i18next.t)("all nodes")
65885
+ i18n: "all nodes"
65467
65886
  }
65468
65887
  ]);
65469
65888
  create_form_button_row(popover, [{
65470
65889
  i18n: "cancel",
65471
65890
  style: BTN_STYLE_CANCEL,
65472
- onClick: () => {
65473
- hide_node_popover(gobj);
65474
- }
65891
+ event: "EV_CANCEL_NODE_POPOVER",
65892
+ kw: { node_id }
65475
65893
  }, {
65476
65894
  i18n: "apply",
65477
65895
  style: "flex:1;padding:6px;background:#1890ff;color:#fff;border:none;border-radius:4px;cursor:pointer;font-size:13px;font-weight:500;",
65478
- onClick: () => {
65479
- drop_preview_undo(gobj, "_node_preview_undo");
65480
- apply_node_properties(gobj, node_id, fillInput.value, strokeInput.value, parseInt(lwInput.value) || 1, scopeSelect.value, shapeSelect.value !== orig_figure ? shapeSelect.value : "");
65896
+ event: "EV_APPLY_NODE_POPOVER",
65897
+ kw: {
65898
+ node_id,
65899
+ shape_before: orig_figure
65900
+ },
65901
+ form: {
65902
+ fill: fillInput,
65903
+ stroke: strokeInput,
65904
+ line_width: lwInput,
65905
+ scope: scopeSelect,
65906
+ shape: shapeSelect
65481
65907
  }
65482
- }]);
65908
+ }], gobj);
65483
65909
  priv.$container.appendChild(popover);
65484
65910
  priv._node_popover_el = popover;
65485
65911
  set_preview_undo(gobj, "_node_preview_undo", undo_node);
@@ -65499,6 +65925,39 @@ function hide_node_detail(gobj) {
65499
65925
  gobj.priv._detail_node_id = null;
65500
65926
  hide_overlay(gobj, "_node_detail_el");
65501
65927
  }
65928
+ /************************************************************
65929
+ * The ✕ of the detail popover. Built as an element and not in
65930
+ * the popover's markup, so that it is a whole button: named
65931
+ * with its key (a language switch renames it), reachable by
65932
+ * the keyboard and pressed by Enter or Space as by the pointer.
65933
+ * Its click sends EV_CLOSE_NODE_DETAIL {node_id} and nothing
65934
+ * else; a pointer press does not take the focus.
65935
+ ************************************************************/
65936
+ function create_detail_close(gobj, color, node_id) {
65937
+ let el = document.createElement("div");
65938
+ el.className = "g6-detail-close";
65939
+ el.setAttribute("role", "button");
65940
+ el.setAttribute("tabindex", "0");
65941
+ el.setAttribute("title", (0, i18next.t)("close"));
65942
+ el.setAttribute("data-i18n-title", "close");
65943
+ el.setAttribute("aria-label", (0, i18next.t)("close"));
65944
+ el.setAttribute("data-i18n-aria-label", "close");
65945
+ el.style.color = color;
65946
+ el.textContent = "×";
65947
+ el.addEventListener("click", (e) => {
65948
+ e.stopPropagation();
65949
+ (0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_CLOSE_NODE_DETAIL", { node_id }, gobj);
65950
+ });
65951
+ keep_focus_on_press(el);
65952
+ click_on_enter_or_space(el);
65953
+ return el;
65954
+ }
65955
+ function detail_div(class_name, text) {
65956
+ let el = document.createElement("div");
65957
+ if (class_name) el.className = class_name;
65958
+ if (text !== void 0) el.textContent = text;
65959
+ return el;
65960
+ }
65502
65961
  function show_node_detail_popover(gobj, node_id) {
65503
65962
  let priv = gobj.priv;
65504
65963
  let graph = priv.graph;
@@ -65528,7 +65987,19 @@ function show_node_detail_popover(gobj, node_id) {
65528
65987
  popover.style.overflow = "auto";
65529
65988
  popover.style.padding = "0 12px 12px";
65530
65989
  popover.style.fontFamily = "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif";
65531
- let rows = "";
65990
+ let head = detail_div("g6-detail-head");
65991
+ head.style.background = bg;
65992
+ let names = detail_div("");
65993
+ names.style.flex = "1 1 auto";
65994
+ names.style.minWidth = "0";
65995
+ names.appendChild(detail_div("g6-detail-id", id));
65996
+ let topic_el = detail_div("g6-detail-topic", topic);
65997
+ topic_el.style.color = sub;
65998
+ names.appendChild(topic_el);
65999
+ head.appendChild(names);
66000
+ head.appendChild(create_detail_close(gobj, sub, node_id));
66001
+ popover.appendChild(head);
66002
+ let n_rows = 0;
65532
66003
  for (let key of Object.keys(record)) {
65533
66004
  if (key.indexOf("__") === 0) continue;
65534
66005
  let v = record[key];
@@ -65537,14 +66008,21 @@ function show_node_detail_popover(gobj, node_id) {
65537
66008
  else if (typeof v === "object") vs = JSON.stringify(v);
65538
66009
  else vs = String(v);
65539
66010
  if (vs.length > 240) vs = vs.slice(0, 240) + "…";
65540
- rows += `<div class="g6-detail-row" style="border-top:1px solid ${rowbd};"><div class="g6-detail-key" style="color:${sub};">${(0, _yuneta_gobj_js.escapeHtml)(key)}</div><div class="g6-detail-val">${(0, _yuneta_gobj_js.escapeHtml)(vs)}</div></div>`;
66011
+ let row = detail_div("g6-detail-row");
66012
+ row.style.borderTop = `1px solid ${rowbd}`;
66013
+ let key_el = detail_div("g6-detail-key", key);
66014
+ key_el.style.color = sub;
66015
+ row.appendChild(key_el);
66016
+ row.appendChild(detail_div("g6-detail-val", vs));
66017
+ popover.appendChild(row);
66018
+ n_rows++;
66019
+ }
66020
+ if (!n_rows) {
66021
+ let empty = detail_div("g6-detail-empty", (0, i18next.t)("no fields"));
66022
+ empty.setAttribute("data-i18n", "no fields");
66023
+ empty.style.color = sub;
66024
+ popover.appendChild(empty);
65541
66025
  }
65542
- popover.innerHTML = `<div class="g6-detail-head" style="background:${bg};"><div style="flex:1 1 auto;min-width:0;"><div class="g6-detail-id">${(0, _yuneta_gobj_js.escapeHtml)(id)}</div><div class="g6-detail-topic" style="color:${sub};">${(0, _yuneta_gobj_js.escapeHtml)(topic)}</div></div><div class="g6-detail-close" role="button" tabindex="0" aria-label="${(0, _yuneta_gobj_js.escapeHtml)((0, i18next.t)("close"))}" title="${(0, _yuneta_gobj_js.escapeHtml)((0, i18next.t)("close"))}" style="color:${sub};">×</div></div>` + (rows || `<div class="g6-detail-empty" style="color:${sub};">${(0, _yuneta_gobj_js.escapeHtml)((0, i18next.t)("no fields"))}</div>`);
65543
- let close_el = popover.querySelector(".g6-detail-close");
65544
- if (close_el) close_el.addEventListener("click", (e) => {
65545
- e.stopPropagation();
65546
- hide_node_detail(gobj);
65547
- });
65548
66026
  priv.$container.appendChild(popover);
65549
66027
  priv._node_detail_el = popover;
65550
66028
  priv._detail_node_id = node_id;
@@ -66066,10 +66544,10 @@ function request_delete_selection(gobj) {
66066
66544
  execute_delete_selection(gobj, nodes);
66067
66545
  return;
66068
66546
  }
66069
- show_confirm_popover(gobj, null, delete_question(nodes), "delete", "#ff4d4f", () => (0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_CONFIRMED", {
66547
+ show_confirm_popover(gobj, null, delete_question(nodes), "delete", "#ff4d4f", {
66070
66548
  what: "delete_selection",
66071
66549
  ids: nodes.map((nd) => String(nd.id))
66072
- }, gobj), "_delete_confirm_el");
66550
+ }, "_delete_confirm_el");
66073
66551
  }
66074
66552
  function execute_delete_selection(gobj, nodes) {
66075
66553
  let priv = gobj.priv;
@@ -66086,10 +66564,10 @@ function execute_delete_selection(gobj, nodes) {
66086
66564
  ************************************************************/
66087
66565
  function show_delete_confirm(gobj, nodeData) {
66088
66566
  let priv = gobj.priv;
66089
- show_confirm_popover(gobj, priv._node_delete_el, delete_question([nodeData]), "delete", "#ff4d4f", () => (0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_CONFIRMED", {
66567
+ show_confirm_popover(gobj, priv._node_delete_el, delete_question([nodeData]), "delete", "#ff4d4f", {
66090
66568
  what: "delete_node",
66091
66569
  id: String(nodeData.id)
66092
- }, gobj), "_delete_confirm_el");
66570
+ }, "_delete_confirm_el");
66093
66571
  }
66094
66572
  function hide_delete_confirm(gobj) {
66095
66573
  hide_overlay(gobj, "_delete_confirm_el");
@@ -66138,6 +66616,7 @@ function show_create_popover(gobj) {
66138
66616
  topicLabel.appendChild(document.createTextNode(":"));
66139
66617
  popover.appendChild(topicLabel);
66140
66618
  let topicSelect = document.createElement("select");
66619
+ topicSelect.className = "G6_CREATE_TOPIC";
66141
66620
  topicSelect.setAttribute("title", (0, i18next.t)("topic"));
66142
66621
  topicSelect.setAttribute("data-i18n-title", "topic");
66143
66622
  topicSelect.setAttribute("aria-label", (0, i18next.t)("topic"));
@@ -66170,9 +66649,14 @@ function show_create_popover(gobj) {
66170
66649
  });
66171
66650
  let idLabel = document.createElement("div");
66172
66651
  idLabel.style.cssText = "margin-bottom:4px;font-weight:500;";
66173
- idLabel.textContent = "Id:";
66652
+ let idWord = document.createElement("span");
66653
+ idWord.textContent = (0, i18next.t)("node id");
66654
+ idWord.setAttribute("data-i18n", "node id");
66655
+ idLabel.appendChild(idWord);
66656
+ idLabel.appendChild(document.createTextNode(":"));
66174
66657
  popover.appendChild(idLabel);
66175
66658
  let idInput = document.createElement("input");
66659
+ idInput.className = "G6_CREATE_ID";
66176
66660
  idInput.type = "text";
66177
66661
  idInput.placeholder = (0, i18next.t)("node id");
66178
66662
  idInput.setAttribute("data-i18n-placeholder", "node id");
@@ -66183,39 +66667,31 @@ function show_create_popover(gobj) {
66183
66667
  idInput.style.cssText = "width:100%;padding:6px;border:1px solid #d9d9d9;border-radius:4px;font-size:13px;margin-bottom:10px;box-sizing:border-box;" + touch_control_css();
66184
66668
  popover.appendChild(idInput);
66185
66669
  let errorEl = document.createElement("div");
66670
+ errorEl.className = "G6_CREATE_ERROR";
66186
66671
  errorEl.style.cssText = "color:#ff4d4f;font-size:12px;margin-bottom:8px;display:none;";
66187
66672
  popover.appendChild(errorEl);
66188
- let createBtn;
66189
- create_form_button_row(popover, [{
66673
+ let createBtn = create_form_button_row(popover, [{
66190
66674
  i18n: "cancel",
66191
66675
  style: BTN_STYLE_CANCEL,
66192
- onClick: () => hide_create_popover(gobj)
66676
+ event: "EV_CLOSE_CREATE_POPOVER",
66677
+ kw: {}
66193
66678
  }, {
66194
66679
  i18n: "create",
66195
66680
  style: "flex:1;padding:6px;background:#1890ff;color:#fff;border:none;border-radius:4px;cursor:pointer;font-size:13px;font-weight:500;",
66196
- onClick: () => {
66197
- let node_id = idInput.value.trim();
66198
- if (!node_id) {
66199
- errorEl.textContent = (0, i18next.t)("node id required");
66200
- errorEl.setAttribute("data-i18n", "node id required");
66201
- errorEl.style.display = "block";
66202
- idInput.focus();
66203
- return;
66204
- }
66205
- (0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_SUBMIT_NEW_NODE", {
66206
- topic_name: String(topicSelect.value),
66207
- id: node_id
66208
- }, gobj);
66681
+ event: "EV_SUBMIT_NEW_NODE",
66682
+ kw: {},
66683
+ form: {
66684
+ topic_name: topicSelect,
66685
+ id: idInput
66209
66686
  }
66210
- }]);
66211
- createBtn = popover.querySelector("button:last-child");
66687
+ }], gobj).lastElementChild;
66212
66688
  idInput.addEventListener("keydown", (e) => {
66213
66689
  if (e.key === "Enter") {
66214
66690
  e.preventDefault();
66215
66691
  createBtn.click();
66216
66692
  } else if (e.key === "Escape") {
66217
66693
  e.preventDefault();
66218
- hide_create_popover(gobj);
66694
+ (0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_CLOSE_CREATE_POPOVER", {}, gobj);
66219
66695
  }
66220
66696
  });
66221
66697
  priv.$container.appendChild(popover);
@@ -66265,10 +66741,10 @@ function execute_unlink_edge(gobj, edgeData) {
66265
66741
  function show_unlink_confirm(gobj, edgeData) {
66266
66742
  let priv = gobj.priv;
66267
66743
  const d = edgeData.data;
66268
- show_confirm_popover(gobj, priv._edge_delete_el, (0, i18next.t)("unlink") + " " + d.child_id + " → " + d.parent_id + "?\n" + (0, i18next.t)("neither record is deleted"), "unlink", "#ff4d4f", () => (0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_CONFIRMED", {
66744
+ show_confirm_popover(gobj, priv._edge_delete_el, (0, i18next.t)("unlink") + " " + d.child_id + " → " + d.parent_id + "?\n" + (0, i18next.t)("neither record is deleted"), "unlink", "#ff4d4f", {
66269
66745
  what: "unlink_edge",
66270
66746
  id: String(edgeData.id)
66271
- }, gobj), "_unlink_confirm_el");
66747
+ }, "_unlink_confirm_el");
66272
66748
  }
66273
66749
  function hide_unlink_confirm(gobj) {
66274
66750
  hide_overlay(gobj, "_unlink_confirm_el");
@@ -67639,9 +68115,7 @@ function ac_load_data(gobj, event, kw, src) {
67639
68115
  ************************************************************/
67640
68116
  function ac_save_graph(gobj, event, kw, src) {
67641
68117
  if (gobj.priv.edit_mode) {
67642
- let $container = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "$container");
67643
- disableElements($container, ".EV_SAVE_GRAPH");
67644
- set_submit_state($container, ".EV_SAVE_GRAPH", false);
68118
+ light_save_button(gobj, false);
67645
68119
  let history = graph_get_plugin(gobj, "history");
67646
68120
  if (history) history.clear();
67647
68121
  save_geometry(gobj);
@@ -67918,12 +68392,22 @@ function ac_center_anchor(gobj, event, kw, src) {
67918
68392
  return 0;
67919
68393
  }
67920
68394
  /************************************************************
67921
- * What the anchor button says it will do next.
68395
+ * What the anchor button says it will do next: its `title`,
68396
+ * and the `title_key` it is translated from.
67922
68397
  ************************************************************/
67923
68398
  function anchor_title(state) {
67924
- if (state === "arming") return (0, i18next.t)("click the element to centre on");
67925
- if (state === "on") return (0, i18next.t)("centred: click to release");
67926
- return (0, i18next.t)("anchor view");
68399
+ if (state === "arming") return {
68400
+ title: (0, i18next.t)("click the element to centre on"),
68401
+ title_key: "click the element to centre on"
68402
+ };
68403
+ if (state === "on") return {
68404
+ title: (0, i18next.t)("centred: click to release"),
68405
+ title_key: "centred: click to release"
68406
+ };
68407
+ return {
68408
+ title: (0, i18next.t)("anchor view"),
68409
+ title_key: "anchor view"
68410
+ };
67927
68411
  }
67928
68412
  /************************************************************
67929
68413
  * Every camera action ends the same way once an anchor is
@@ -68339,8 +68823,8 @@ function ac_set_layout$1(gobj, event, kw, src) {
68339
68823
  function ac_set_operation_mode$1(gobj, event, kw, src) {
68340
68824
  (0, _yuneta_gobj_js.gobj_write_attr)(gobj, "operation_mode", kw.operation_mode);
68341
68825
  configure_behaviour(gobj);
68342
- update_toolbar(gobj);
68343
68826
  sync_history_plugin(gobj);
68827
+ update_toolbar(gobj);
68344
68828
  return 0;
68345
68829
  }
68346
68830
  /************************************************************
@@ -68522,25 +69006,134 @@ function ac_context_menu_item(gobj, event, kw, src) {
68522
69006
  return 0;
68523
69007
  }
68524
69008
  /************************************************************
68525
- * The delete icon floating beside the selected node.
69009
+ * Is the identity a floating button was drawn with still what
69010
+ * is selected?
69011
+ *
69012
+ * The buttons are removed with their selection and drawn again
69013
+ * for the next one, so a click carrying another identity is a
69014
+ * broken invariant -- said loudly, never acted on: it would
69015
+ * delete, unlink or edit what the reader is not looking at.
69016
+ ************************************************************/
69017
+ function floating_button_is_current(gobj, event, kw) {
69018
+ let priv = gobj.priv;
69019
+ let want = {
69020
+ node_id: priv._selected_node_id || "",
69021
+ port_key: priv._selected_port_key || "",
69022
+ edge_id: priv._selected_edge_id || ""
69023
+ };
69024
+ let keys = Object.keys(kw || {});
69025
+ let matches = keys.length > 0 && keys.every((k) => {
69026
+ return Object.prototype.hasOwnProperty.call(want, k) && !!kw[k] && kw[k] === want[k];
69027
+ });
69028
+ if (!matches) (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: ${event} for ${JSON.stringify(kw || {})}, but the selection is ${JSON.stringify(want)}`);
69029
+ return matches;
69030
+ }
69031
+ /************************************************************
69032
+ * {node_id} -- the delete icon floating beside the selected
69033
+ * node.
68526
69034
  ************************************************************/
68527
69035
  function ac_request_delete_node(gobj, event, kw, src) {
69036
+ if (!floating_button_is_current(gobj, event, kw)) return -1;
68528
69037
  request_delete_node(gobj);
68529
69038
  return 0;
68530
69039
  }
68531
69040
  /************************************************************
68532
- * The unlink icon floating beside the selected edge.
69041
+ * {edge_id} -- the unlink icon floating beside the selected
69042
+ * edge.
68533
69043
  ************************************************************/
68534
69044
  function ac_request_unlink_edge(gobj, event, kw, src) {
69045
+ if (!floating_button_is_current(gobj, event, kw)) return -1;
68535
69046
  request_unlink_edge(gobj);
68536
69047
  return 0;
68537
69048
  }
68538
69049
  /************************************************************
69050
+ * {node_id} -- the gear beside the selected node: its
69051
+ * properties popover, opened or closed.
69052
+ ************************************************************/
69053
+ function ac_toggle_node_popover(gobj, event, kw, src) {
69054
+ if (!floating_button_is_current(gobj, event, kw)) return -1;
69055
+ toggle_node_popover(gobj);
69056
+ return 0;
69057
+ }
69058
+ /************************************************************
69059
+ * {edge_id} -- the gear beside the selected edge.
69060
+ ************************************************************/
69061
+ function ac_toggle_edge_popover(gobj, event, kw, src) {
69062
+ if (!floating_button_is_current(gobj, event, kw)) return -1;
69063
+ toggle_edge_popover(gobj);
69064
+ return 0;
69065
+ }
69066
+ /************************************************************
69067
+ * {node_id, port_key} -- the gear beside the selected port.
69068
+ ************************************************************/
69069
+ function ac_toggle_port_popover(gobj, event, kw, src) {
69070
+ if (!floating_button_is_current(gobj, event, kw)) return -1;
69071
+ toggle_port_popover(gobj);
69072
+ return 0;
69073
+ }
69074
+ /************************************************************
69075
+ * {node_id, port_key} -- the link icon of a selected fkey port,
69076
+ * CLICKED (a press that drags enters the mode on its own, from
69077
+ * start_link_drag): the compatible hooks light up, and the next
69078
+ * click on one of them links.
69079
+ ************************************************************/
69080
+ function ac_enter_linking_mode(gobj, event, kw, src) {
69081
+ let priv = gobj.priv;
69082
+ if (!floating_button_is_current(gobj, event, kw)) return -1;
69083
+ if (!priv._linking_mode) enter_linking_mode(gobj);
69084
+ return 0;
69085
+ }
69086
+ /************************************************************
69087
+ * {node_id} -- the ✕ of the detail popover.
69088
+ ************************************************************/
69089
+ function ac_close_node_detail(gobj, event, kw, src) {
69090
+ let priv = gobj.priv;
69091
+ let node_id = kw && kw.node_id || "";
69092
+ if (!priv._node_detail_el || !node_id || node_id !== priv._detail_node_id) {
69093
+ (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: ${event} for '${node_id}', but the detail popover shows '${priv._detail_node_id || ""}'`);
69094
+ return -1;
69095
+ }
69096
+ hide_node_detail(gobj);
69097
+ return 0;
69098
+ }
69099
+ /************************************************************
69100
+ * {} -- the create button of the edit toolbar: the create
69101
+ * popover, opened or closed. Only in edition, where that
69102
+ * toolbar is.
69103
+ ************************************************************/
69104
+ function ac_toggle_create_popover(gobj, event, kw, src) {
69105
+ if (!gobj.priv.edit_mode) {
69106
+ (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: ${event} outside edition`);
69107
+ return -1;
69108
+ }
69109
+ toggle_create_popover(gobj);
69110
+ return 0;
69111
+ }
69112
+ /************************************************************
69113
+ * {} -- the cancel of the create popover, or Escape in its id.
69114
+ ************************************************************/
69115
+ function ac_close_create_popover(gobj, event, kw, src) {
69116
+ if (!gobj.priv._create_popover_el) {
69117
+ (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: ${event} with no create popover open`);
69118
+ return -1;
69119
+ }
69120
+ hide_create_popover(gobj);
69121
+ return 0;
69122
+ }
69123
+ /************************************************************
68539
69124
  * {topic_name, id} -- the create popover was filled in.
69125
+ *
69126
+ * An empty id with the popover open is the person's to fix,
69127
+ * not a fault: it is said IN the popover, where they typed.
68540
69128
  ************************************************************/
68541
69129
  function ac_submit_new_node(gobj, event, kw, src) {
68542
- let topic_name = kw && kw.topic_name ? kw.topic_name : "";
68543
- let node_id = kw && kw.id ? kw.id : "";
69130
+ let priv = gobj.priv;
69131
+ let topic_name = kw && kw.topic_name ? String(kw.topic_name) : "";
69132
+ let node_id = kw && kw.id ? String(kw.id).trim() : "";
69133
+ if (topic_name && !node_id && priv._create_popover_el) {
69134
+ say_node_id_required(gobj);
69135
+ return 0;
69136
+ }
68544
69137
  if (!topic_name || !node_id) {
68545
69138
  (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: ${event} without topic or id`);
68546
69139
  return -1;
@@ -68548,6 +69141,168 @@ function ac_submit_new_node(gobj, event, kw, src) {
68548
69141
  execute_create_node(gobj, topic_name, node_id);
68549
69142
  return 0;
68550
69143
  }
69144
+ function say_node_id_required(gobj) {
69145
+ let $popover = gobj.priv._create_popover_el;
69146
+ let $error = $popover.querySelector(".G6_CREATE_ERROR");
69147
+ let $id = $popover.querySelector(".G6_CREATE_ID");
69148
+ if (!$error || !$id) {
69149
+ (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: the create popover has no id or no error line`);
69150
+ return;
69151
+ }
69152
+ $error.textContent = (0, i18next.t)("node id required");
69153
+ $error.setAttribute("data-i18n", "node id required");
69154
+ $error.style.display = "block";
69155
+ $id.focus();
69156
+ }
69157
+ /************************************************************
69158
+ * Is `popover_key` open, and for the selection `kw` names?
69159
+ * `keys` are the identity in `kw` (the rest is what the form
69160
+ * held). A button of a popover that is not the one open is a
69161
+ * broken invariant -- said loudly, never acted on.
69162
+ ************************************************************/
69163
+ function popover_is_current(gobj, event, kw, popover_key, keys) {
69164
+ if (!gobj.priv[popover_key]) {
69165
+ (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: ${event} for ${JSON.stringify(kw || {})}, but its popover is not open`);
69166
+ return false;
69167
+ }
69168
+ let identity = {};
69169
+ for (let k of keys) identity[k] = kw ? kw[k] : void 0;
69170
+ return floating_button_is_current(gobj, event, identity);
69171
+ }
69172
+ /************************************************************
69173
+ * {node_id, shape_before, fill, stroke, line_width, scope,
69174
+ * shape} -- apply of the node popover.
69175
+ ************************************************************/
69176
+ function ac_apply_node_popover(gobj, event, kw, src) {
69177
+ if (!popover_is_current(gobj, event, kw, "_node_popover_el", ["node_id"])) return -1;
69178
+ let shape = String(kw.shape || "");
69179
+ drop_preview_undo(gobj, "_node_preview_undo");
69180
+ apply_node_properties(gobj, kw.node_id, kw.fill, kw.stroke, parseInt(kw.line_width) || 1, kw.scope, shape !== kw.shape_before ? shape : "");
69181
+ return 0;
69182
+ }
69183
+ /************************************************************
69184
+ * {node_id} -- cancel of the node popover, which undoes the
69185
+ * preview.
69186
+ ************************************************************/
69187
+ function ac_cancel_node_popover(gobj, event, kw, src) {
69188
+ if (!popover_is_current(gobj, event, kw, "_node_popover_el", ["node_id"])) return -1;
69189
+ hide_node_popover(gobj);
69190
+ return 0;
69191
+ }
69192
+ /************************************************************
69193
+ * {edge_id, line_width, color, scope} -- apply of the edge
69194
+ * popover.
69195
+ ************************************************************/
69196
+ function ac_apply_edge_popover(gobj, event, kw, src) {
69197
+ if (!popover_is_current(gobj, event, kw, "_edge_popover_el", ["edge_id"])) return -1;
69198
+ drop_preview_undo(gobj, "_edge_preview_undo");
69199
+ apply_edge_properties(gobj, kw.edge_id, parseInt(kw.line_width) || 2, kw.color, kw.scope);
69200
+ return 0;
69201
+ }
69202
+ /************************************************************
69203
+ * {edge_id} -- cancel of the edge popover: the edge is let go,
69204
+ * which undoes the preview.
69205
+ ************************************************************/
69206
+ function ac_cancel_edge_popover(gobj, event, kw, src) {
69207
+ if (!popover_is_current(gobj, event, kw, "_edge_popover_el", ["edge_id"])) return -1;
69208
+ deselect_edge(gobj);
69209
+ return 0;
69210
+ }
69211
+ /************************************************************
69212
+ * {node_id, port_key, r_before, shape, r, scope} -- apply of
69213
+ * the port popover.
69214
+ ************************************************************/
69215
+ function ac_apply_port_popover(gobj, event, kw, src) {
69216
+ if (!popover_is_current(gobj, event, kw, "_port_popover_el", ["node_id", "port_key"])) return -1;
69217
+ drop_preview_undo(gobj, "_port_preview_undo");
69218
+ apply_port_properties(gobj, kw.node_id, kw.port_key, kw.shape, parseInt(kw.r) || kw.r_before, kw.scope);
69219
+ return 0;
69220
+ }
69221
+ /************************************************************
69222
+ * {node_id, port_key} -- cancel of the port popover, which
69223
+ * undoes the preview.
69224
+ ************************************************************/
69225
+ function ac_cancel_port_popover(gobj, event, kw, src) {
69226
+ if (!popover_is_current(gobj, event, kw, "_port_popover_el", ["node_id", "port_key"])) return -1;
69227
+ hide_port_popover(gobj);
69228
+ return 0;
69229
+ }
69230
+ var CONFIRM_POPOVER_OF = {
69231
+ delete_selection: "_delete_confirm_el",
69232
+ delete_node: "_delete_confirm_el",
69233
+ unlink_edge: "_unlink_confirm_el"
69234
+ };
69235
+ function same_question(a, b) {
69236
+ return !!a && !!b && a.what === b.what && JSON.stringify(a.id ?? null) === JSON.stringify(b.id ?? null) && JSON.stringify(a.ids ?? null) === JSON.stringify(b.ids ?? null);
69237
+ }
69238
+ /************************************************************
69239
+ * {what, id|ids} -- the person said no in a confirm popover:
69240
+ * the question it carries must be the one open.
69241
+ ************************************************************/
69242
+ function ac_cancel_confirm(gobj, event, kw, src) {
69243
+ let priv = gobj.priv;
69244
+ let key = CONFIRM_POPOVER_OF[kw && kw.what || ""];
69245
+ let asked = key ? priv._confirm_questions[key] : null;
69246
+ if (!key || !priv[key] || !same_question(asked, kw)) {
69247
+ (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: ${event} for ${JSON.stringify(kw || {})}, but the question open is ${JSON.stringify(priv[key] && asked || {})}`);
69248
+ return -1;
69249
+ }
69250
+ hide_overlay(gobj, key);
69251
+ return 0;
69252
+ }
69253
+ /************************************************************
69254
+ * {node_id, port_key, client_x, client_y} -- a press on the
69255
+ * link icon of the selected fkey port, which may be a drag.
69256
+ ************************************************************/
69257
+ function ac_start_link_drag(gobj, event, kw, src) {
69258
+ let priv = gobj.priv;
69259
+ if (!floating_button_is_current(gobj, event, {
69260
+ node_id: kw && kw.node_id,
69261
+ port_key: kw && kw.port_key
69262
+ })) return -1;
69263
+ if (priv._link_drag) {
69264
+ (0, _yuneta_gobj_js.log_warning)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: ${event} with a link drag still in flight`);
69265
+ end_link_drag(gobj);
69266
+ }
69267
+ start_link_drag(gobj, Number(kw.client_x) || 0, Number(kw.client_y) || 0);
69268
+ return 0;
69269
+ }
69270
+ /************************************************************
69271
+ * {client_x, client_y, dragged} -- the pointer of a link drag
69272
+ * went up. Not dragged, it was a click: the click comes next
69273
+ * (EV_ENTER_LINKING_MODE). Dragged, it links to the hook it was
69274
+ * dropped on, or leaves the linking mode.
69275
+ ************************************************************/
69276
+ function ac_link_drag_end(gobj, event, kw, src) {
69277
+ let priv = gobj.priv;
69278
+ let drag = priv._link_drag;
69279
+ if (!drag) {
69280
+ (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: ${event} with no link drag in flight`);
69281
+ return -1;
69282
+ }
69283
+ end_link_drag(gobj);
69284
+ if (!kw || !kw.dragged) return 0;
69285
+ let cx = Number(kw.client_x) - drag.left;
69286
+ let cy = Number(kw.client_y) - drag.top;
69287
+ let canvasPt = priv.graph.getCanvasByViewport([cx, cy]);
69288
+ let target = find_hook_at_point(gobj, canvasPt[0], canvasPt[1]);
69289
+ if (target) try_complete_link(gobj, target.node_id, target.port_key);
69290
+ else exit_linking_mode(gobj);
69291
+ return 0;
69292
+ }
69293
+ /************************************************************
69294
+ * {} -- the browser took the pointer of a link drag: no drop,
69295
+ * and the linking mode goes.
69296
+ ************************************************************/
69297
+ function ac_link_drag_cancel(gobj, event, kw, src) {
69298
+ if (!gobj.priv._link_drag) {
69299
+ (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: ${event} with no link drag in flight`);
69300
+ return -1;
69301
+ }
69302
+ end_link_drag(gobj);
69303
+ exit_linking_mode(gobj);
69304
+ return 0;
69305
+ }
68551
69306
  /************************************************************
68552
69307
  * {what, id|ids} -- the person said yes in a confirm popover.
68553
69308
  *
@@ -68804,16 +69559,101 @@ function create_gclass$4(gclass_name) {
68804
69559
  ac_request_unlink_edge,
68805
69560
  null
68806
69561
  ],
69562
+ [
69563
+ "EV_TOGGLE_NODE_POPOVER",
69564
+ ac_toggle_node_popover,
69565
+ null
69566
+ ],
69567
+ [
69568
+ "EV_TOGGLE_EDGE_POPOVER",
69569
+ ac_toggle_edge_popover,
69570
+ null
69571
+ ],
69572
+ [
69573
+ "EV_TOGGLE_PORT_POPOVER",
69574
+ ac_toggle_port_popover,
69575
+ null
69576
+ ],
69577
+ [
69578
+ "EV_ENTER_LINKING_MODE",
69579
+ ac_enter_linking_mode,
69580
+ null
69581
+ ],
69582
+ [
69583
+ "EV_CLOSE_NODE_DETAIL",
69584
+ ac_close_node_detail,
69585
+ null
69586
+ ],
69587
+ [
69588
+ "EV_TOGGLE_CREATE_POPOVER",
69589
+ ac_toggle_create_popover,
69590
+ null
69591
+ ],
69592
+ [
69593
+ "EV_CLOSE_CREATE_POPOVER",
69594
+ ac_close_create_popover,
69595
+ null
69596
+ ],
68807
69597
  [
68808
69598
  "EV_SUBMIT_NEW_NODE",
68809
69599
  ac_submit_new_node,
68810
69600
  null
68811
69601
  ],
69602
+ [
69603
+ "EV_APPLY_NODE_POPOVER",
69604
+ ac_apply_node_popover,
69605
+ null
69606
+ ],
69607
+ [
69608
+ "EV_CANCEL_NODE_POPOVER",
69609
+ ac_cancel_node_popover,
69610
+ null
69611
+ ],
69612
+ [
69613
+ "EV_APPLY_EDGE_POPOVER",
69614
+ ac_apply_edge_popover,
69615
+ null
69616
+ ],
69617
+ [
69618
+ "EV_CANCEL_EDGE_POPOVER",
69619
+ ac_cancel_edge_popover,
69620
+ null
69621
+ ],
69622
+ [
69623
+ "EV_APPLY_PORT_POPOVER",
69624
+ ac_apply_port_popover,
69625
+ null
69626
+ ],
69627
+ [
69628
+ "EV_CANCEL_PORT_POPOVER",
69629
+ ac_cancel_port_popover,
69630
+ null
69631
+ ],
68812
69632
  [
68813
69633
  "EV_CONFIRMED",
68814
69634
  ac_confirmed$1,
68815
69635
  null
68816
69636
  ],
69637
+ [
69638
+ "EV_CANCEL_CONFIRM",
69639
+ ac_cancel_confirm,
69640
+ null
69641
+ ],
69642
+ [
69643
+ "EV_START_LINK_DRAG",
69644
+ ac_start_link_drag,
69645
+ null
69646
+ ],
69647
+ [
69648
+ "EV_LINK_DRAG_END",
69649
+ ac_link_drag_end,
69650
+ null
69651
+ ],
69652
+ [
69653
+ "EV_LINK_DRAG_CANCEL",
69654
+ ac_link_drag_cancel,
69655
+ null
69656
+ ],
68817
69657
  [
68818
69658
  "EV_CANVAS_CLICK",
68819
69659
  ac_canvas_click,
@@ -69034,8 +69874,25 @@ function create_gclass$4(gclass_name) {
69034
69874
  ["EV_CONTEXT_MENU_ITEM", 0],
69035
69875
  ["EV_REQUEST_DELETE_NODE", 0],
69036
69876
  ["EV_REQUEST_UNLINK_EDGE", 0],
69877
+ ["EV_TOGGLE_NODE_POPOVER", 0],
69878
+ ["EV_TOGGLE_EDGE_POPOVER", 0],
69879
+ ["EV_TOGGLE_PORT_POPOVER", 0],
69880
+ ["EV_ENTER_LINKING_MODE", 0],
69881
+ ["EV_CLOSE_NODE_DETAIL", 0],
69882
+ ["EV_TOGGLE_CREATE_POPOVER", 0],
69883
+ ["EV_CLOSE_CREATE_POPOVER", 0],
69037
69884
  ["EV_SUBMIT_NEW_NODE", 0],
69885
+ ["EV_APPLY_NODE_POPOVER", 0],
69886
+ ["EV_CANCEL_NODE_POPOVER", 0],
69887
+ ["EV_APPLY_EDGE_POPOVER", 0],
69888
+ ["EV_CANCEL_EDGE_POPOVER", 0],
69889
+ ["EV_APPLY_PORT_POPOVER", 0],
69890
+ ["EV_CANCEL_PORT_POPOVER", 0],
69038
69891
  ["EV_CONFIRMED", 0],
69892
+ ["EV_CANCEL_CONFIRM", 0],
69893
+ ["EV_START_LINK_DRAG", 0],
69894
+ ["EV_LINK_DRAG_END", 0],
69895
+ ["EV_LINK_DRAG_CANCEL", 0],
69039
69896
  ["EV_CANVAS_CLICK", 0],
69040
69897
  ["EV_NODE_DRAG_END", 0],
69041
69898
  ["EV_BRUSH_SELECT", 0],
@@ -69385,7 +70242,7 @@ function show_load_error(gobj, message) {
69385
70242
  $container.insertBefore($err, $container.firstChild);
69386
70243
  }
69387
70244
  let treedb = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "treedb_name") || "";
69388
- $err.textContent = (treedb ? `${treedb}: ` : "") + (message || "cannot load treedb");
70245
+ $err.textContent = (treedb ? `${treedb}: ` : "") + (message || (0, i18next.t)("cannot load treedb"));
69389
70246
  }
69390
70247
  /************************************************************
69391
70248
  * Destroy UI
@@ -71070,7 +71927,7 @@ function refresh_legend(gobj) {
71070
71927
  },
71071
71928
  hidden ? `${entry.total}` : `${entry.visible}/${entry.total}`
71072
71929
  ]);
71073
- let body_key = is_main ? "main topic" : hidden ? "show topic" : "hide topic";
71930
+ const body_key = is_main ? "main topic" : hidden ? "show topic" : "hide topic";
71074
71931
  let body_attrs = {
71075
71932
  class: "GRAPH_LEGEND_ITEM button" + (focused ? " GRAPH_LEGEND_FOCUSED" : "") + (is_main ? " GRAPH_LEGEND_MAIN" : ""),
71076
71933
  type: "button",
@@ -72330,6 +73187,7 @@ function build_graph(gobj) {
72330
73187
  }
72331
73188
  priv.graph = graph;
72332
73189
  graph.setTheme(dark ? "dark" : "light");
73190
+ keep_canvas_tab_order(gobj, graph);
72333
73191
  graph.on("aftertransform", () => {
72334
73192
  yui_graph_update_zoom($container, priv.graph);
72335
73193
  });
@@ -77643,7 +78501,7 @@ function ac_mt_command_answer(gobj, event, kw, src) {
77643
78501
  }
77644
78502
  return late_write_done(gobj, command, topic_name, data);
77645
78503
  }
77646
- if (result < 0) return end_writes(gobj, comment || (0, i18next.t)("the treedb refused the write"));
78504
+ if (result < 0) return end_writes(gobj, comment || "the treedb refused the write");
77647
78505
  priv.save_wrote = true;
77648
78506
  let record = null;
77649
78507
  if (command === "delete-node") forget_record(gobj, topic_name, (0, _yuneta_gobj_js.kw_get_str)(gobj, md, "record_id", "", 0));
@@ -78159,7 +79017,8 @@ function confirm_column_delete(gobj, verdict) {
78159
79017
  capped: "the records read hold no value in this column, but not all were read: the rest may hold values that no reader will show any more",
78160
79018
  unknown: "the records of this topic could not be read: if they hold values in this column, they keep them, but no reader shows them any more"
78161
79019
  };
78162
- let message = "delete this column?";
79020
+ const message = "delete this column?";
79021
+ let $warning = null;
78163
79022
  if (NOTES[verdict]) {
78164
79023
  let children = [[
78165
79024
  "p",
@@ -78189,13 +79048,13 @@ function confirm_column_delete(gobj, verdict) {
78189
79048
  String(c.scanned)
78190
79049
  ]]
78191
79050
  ]);
78192
- message = (0, _yuneta_gobj_js.createElement2)([
79051
+ $warning = (0, _yuneta_gobj_js.createElement2)([
78193
79052
  "div",
78194
79053
  { class: "SCHEMA_COLUMN_DATA_WARNING" },
78195
79054
  children
78196
79055
  ]);
78197
79056
  }
78198
- confirm_then(gobj, message, `${c.topic}.${c.col}`, {
79057
+ confirm_then(gobj, $warning ? $warning : message, `${c.topic}.${c.col}`, {
78199
79058
  what: "column",
78200
79059
  topic: c.topic,
78201
79060
  col: c.col
@@ -81760,7 +82619,9 @@ function build_control_bar() {
81760
82619
  type: "button",
81761
82620
  title: (0, i18next.t)("machine trace shape"),
81762
82621
  "data-i18n-title": "machine trace shape",
81763
- "data-i18n": "simple mach"
82622
+ "data-i18n": "simple mach",
82623
+ "aria-label": (0, i18next.t)("simple mach"),
82624
+ "data-i18n-aria-label": "simple mach"
81764
82625
  },
81765
82626
  (0, i18next.t)("simple mach"),
81766
82627
  { click: (ev) => {
@@ -81834,7 +82695,9 @@ function build_control_bar() {
81834
82695
  type: "button",
81835
82696
  title: (0, i18next.t)(OUT_TITLES[v]),
81836
82697
  "data-i18n-title": OUT_TITLES[v],
81837
- "data-i18n": label
82698
+ "data-i18n": label,
82699
+ "aria-label": (0, i18next.t)(label),
82700
+ "data-i18n-aria-label": label
81838
82701
  },
81839
82702
  (0, i18next.t)(label),
81840
82703
  { click: (ev) => {
@@ -81862,7 +82725,9 @@ function build_control_bar() {
81862
82725
  type: "button",
81863
82726
  title: (0, i18next.t)("show this section in the expanded view"),
81864
82727
  "data-i18n-title": "show this section in the expanded view",
81865
- "data-i18n": label
82728
+ "data-i18n": label,
82729
+ "aria-label": (0, i18next.t)(label),
82730
+ "data-i18n-aria-label": label
81866
82731
  },
81867
82732
  (0, i18next.t)(label),
81868
82733
  { click: (ev) => {
@@ -81961,7 +82826,9 @@ function build_control_bar() {
81961
82826
  type: "button",
81962
82827
  title: (0, i18next.t)("copy visible traffic to clipboard"),
81963
82828
  "data-i18n-title": "copy visible traffic to clipboard",
81964
- "data-i18n": "copy"
82829
+ "data-i18n": "copy",
82830
+ "aria-label": (0, i18next.t)("copy"),
82831
+ "data-i18n-aria-label": "copy"
81965
82832
  },
81966
82833
  (0, i18next.t)("copy"),
81967
82834
  { click: (ev) => {
@@ -81983,7 +82850,9 @@ function build_control_bar() {
81983
82850
  type: "button",
81984
82851
  title: (0, i18next.t)("clear captured traffic"),
81985
82852
  "data-i18n-title": "clear captured traffic",
81986
- "data-i18n": "clear"
82853
+ "data-i18n": "clear",
82854
+ "aria-label": (0, i18next.t)("clear"),
82855
+ "data-i18n-aria-label": "clear"
81987
82856
  },
81988
82857
  (0, i18next.t)("clear"),
81989
82858
  { click: (ev) => {