@yuneta/gobj-ui 7.26.27 → 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 +458 -22
  2. package/dist/gobj-ui.cjs.js +1063 -216
  3. package/dist/gobj-ui.es.js +1064 -217
  4. package/package.json +1 -1
  5. package/scripts/validate-locales.mjs +2269 -316
  6. package/src/c_g6_nodes_tree.js +1086 -253
  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 +241 -5
  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 +16 -3
  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
@@ -5,7 +5,7 @@ import { createJSONEditor } from "vanilla-jsoneditor";
5
5
  import { TabulatorFull } from "tabulator-tables";
6
6
  import * as maplibregl from "maplibre-gl";
7
7
  import uPlot from "uplot";
8
- import { BaseLayout, BrushSelect, CanvasEvent, Circle, CommonEvent, DragCanvas, EdgeEvent, ExtensionCategory, Graph, HTML, HistoryEvent, NodeEvent, Polyline, Toolbar, ZoomCanvas, register } from "@antv/g6";
8
+ import { BaseLayout, BrushSelect, CanvasEvent, Circle, CommonEvent, DragCanvas, EdgeEvent, ExtensionCategory, Graph, GraphEvent, HTML, HistoryEvent, NodeEvent, Polyline, Toolbar, ZoomCanvas, register } from "@antv/g6";
9
9
  //#region \0rolldown/runtime.js
10
10
  var __create = Object.create;
11
11
  var __defProp = Object.defineProperty;
@@ -1843,7 +1843,7 @@ function build_toolbar$1(gobj, config) {
1843
1843
  }
1844
1844
  priv.avatar_nodes = [];
1845
1845
  priv.conn_nodes = [];
1846
- let toolbar_aria = tb.aria_label || "Toolbar";
1846
+ const toolbar_aria = tb.aria_label || "toolbar";
1847
1847
  let $bar = createElement2([
1848
1848
  "nav",
1849
1849
  {
@@ -2054,8 +2054,8 @@ function build_toolbar_brand_item(gobj, it) {
2054
2054
  ************************************************************/
2055
2055
  function build_toolbar_avatar_item(gobj, it) {
2056
2056
  let priv = gobj.priv;
2057
- let aria_key = it.aria_label || it.name || it.id || "User menu";
2058
- let i18n_aria = it.aria_label || it.name || "User menu";
2057
+ const aria_key = it.aria_label || it.name || it.id || "user menu";
2058
+ const i18n_aria = it.aria_label || it.name || "user menu";
2059
2059
  let action_type = it.action && it.action.type || "";
2060
2060
  let attrs = {
2061
2061
  class: "navbar-item yui-toolbar-item yui-toolbar-avatar is-unselectable",
@@ -2131,8 +2131,8 @@ function attach_context_action(gobj, $item, it) {
2131
2131
  ************************************************************/
2132
2132
  function build_toolbar_connection_item(gobj, it) {
2133
2133
  let priv = gobj.priv;
2134
- let aria_key = it.aria_label || it.name || it.id || "backend connection";
2135
- let i18n_aria = it.aria_label || it.name || "backend connection";
2134
+ const aria_key = it.aria_label || it.name || it.id || "backend connection";
2135
+ const i18n_aria = it.aria_label || it.name || "backend connection";
2136
2136
  let attrs = {
2137
2137
  class: "navbar-item yui-toolbar-item yui-toolbar-conn is-unselectable is-disconnected",
2138
2138
  type: "button",
@@ -2238,8 +2238,8 @@ function toggle_toolbar_dropdown(gobj, item, action, $trigger) {
2238
2238
  function open_toolbar_dropdown(gobj, item, action, $trigger) {
2239
2239
  let priv = gobj.priv;
2240
2240
  if (!priv || !priv.layers || !priv.layers.popup) return;
2241
- let aria_key = item && (item.aria_label || item.name || item.id) || "Menu";
2242
- let i18n_aria = item && (item.aria_label || item.name) || "Menu";
2241
+ const aria_key = item && (item.aria_label || item.name || item.id) || "menu";
2242
+ const i18n_aria = item && (item.aria_label || item.name) || "menu";
2243
2243
  let $panel = createElement2(["div", {
2244
2244
  class: "yui-toolbar-dropdown-panel",
2245
2245
  role: "menu",
@@ -3776,19 +3776,22 @@ function build_dialog(shell, message, buttons, opts) {
3776
3776
  * "Delete" and "Cancel", keys that no locale carries, so a dialog
3777
3777
  * that took the defaults read in English in every language.
3778
3778
  *
3779
- * Written as `i18n:` entries so the validate-locales script of a
3780
- * consumer finds them when it scans this module: an app that
3781
- * mounts these dialogs must define the five keys.
3779
+ * An app that mounts these dialogs must define the five keys. A
3780
+ * consumer's validate-locales reads them where build_dialog() gets
3781
+ * them (a const, `DEFAULT_LABEL.ok`, it follows), and in
3782
+ * SHELL_MODALS_I18N_KEYS, which a test holds to this table. (Up to
3783
+ * 7.26.27 they were written as `i18n:` entries, which only a regex
3784
+ * scan of `i18n: "..."` happened to see.)
3782
3785
  ***************************************************************/
3783
3786
  var DEFAULT_LABEL = Object.freeze({
3784
- ok: Object.freeze({ i18n: "ok" }),
3785
- yes: Object.freeze({ i18n: "yes" }),
3786
- no: Object.freeze({ i18n: "no" }),
3787
- delete: Object.freeze({ i18n: "delete" }),
3788
- cancel: Object.freeze({ i18n: "cancel" })
3787
+ ok: "ok",
3788
+ yes: "yes",
3789
+ no: "no",
3790
+ delete: "delete",
3791
+ cancel: "cancel"
3789
3792
  });
3790
3793
  function yui_shell_confirm_ok(shell, message, opts) {
3791
- let label = opts && opts.ok_label || DEFAULT_LABEL.ok.i18n;
3794
+ const label = opts && opts.ok_label || DEFAULT_LABEL.ok;
3792
3795
  if (!opts || !opts.type) opts = Object.assign({}, opts, { type: "success" });
3793
3796
  return build_dialog(shell, message, [{
3794
3797
  label,
@@ -3797,8 +3800,8 @@ function yui_shell_confirm_ok(shell, message, opts) {
3797
3800
  }], opts).then(() => void 0);
3798
3801
  }
3799
3802
  function yui_shell_confirm_yesno(shell, message, opts) {
3800
- let yes_label = opts && opts.yes_label || DEFAULT_LABEL.yes.i18n;
3801
- let no_label = opts && opts.no_label || DEFAULT_LABEL.no.i18n;
3803
+ const yes_label = opts && opts.yes_label || DEFAULT_LABEL.yes;
3804
+ const no_label = opts && opts.no_label || DEFAULT_LABEL.no;
3802
3805
  return build_dialog(shell, message, [{
3803
3806
  label: yes_label,
3804
3807
  value: "yes",
@@ -3823,8 +3826,8 @@ function yui_shell_confirm_yesno(shell, message, opts) {
3823
3826
  * pressed.
3824
3827
  ***************************************************************/
3825
3828
  function yui_shell_confirm_danger(shell, message, opts) {
3826
- let confirm_label = opts && opts.confirm_label || DEFAULT_LABEL.delete.i18n;
3827
- let cancel_label = opts && opts.cancel_label || DEFAULT_LABEL.cancel.i18n;
3829
+ const confirm_label = opts && opts.confirm_label || DEFAULT_LABEL.delete;
3830
+ const cancel_label = opts && opts.cancel_label || DEFAULT_LABEL.cancel;
3828
3831
  if (!opts || !opts.type) opts = Object.assign({}, opts, { type: "danger" });
3829
3832
  return build_dialog(shell, message, [{
3830
3833
  label: confirm_label,
@@ -3836,9 +3839,9 @@ function yui_shell_confirm_danger(shell, message, opts) {
3836
3839
  }], opts).then((v) => v === "confirm");
3837
3840
  }
3838
3841
  function yui_shell_confirm_yesnocancel(shell, message, opts) {
3839
- let yes_label = opts && opts.yes_label || DEFAULT_LABEL.yes.i18n;
3840
- let no_label = opts && opts.no_label || DEFAULT_LABEL.no.i18n;
3841
- let cancel_label = opts && opts.cancel_label || DEFAULT_LABEL.cancel.i18n;
3842
+ const yes_label = opts && opts.yes_label || DEFAULT_LABEL.yes;
3843
+ const no_label = opts && opts.no_label || DEFAULT_LABEL.no;
3844
+ const cancel_label = opts && opts.cancel_label || DEFAULT_LABEL.cancel;
3842
3845
  return build_dialog(shell, message, [
3843
3846
  {
3844
3847
  label: yes_label,
@@ -4492,7 +4495,7 @@ function set_max_icon(gobj, maximized) {
4492
4495
  if (!$container) return;
4493
4496
  let $btn = $container.querySelector(".wc-max");
4494
4497
  if ($btn) {
4495
- let key = maximized ? "restore" : "maximize";
4498
+ const key = maximized ? "restore" : "maximize";
4496
4499
  $btn.innerHTML = maximized ? WC_RESTORE : WC_MAX;
4497
4500
  $btn.setAttribute("title", t(key));
4498
4501
  $btn.setAttribute("data-i18n-title", key);
@@ -5594,7 +5597,7 @@ function render_header(gobj) {
5594
5597
  let $discard = $container.querySelector(".yui-pager-discard");
5595
5598
  $back.classList.toggle("is-hidden", !model.show_back);
5596
5599
  let $back_icon = $back.querySelector("i");
5597
- let back_key = model.back_kind === "close" ? "close" : "back";
5600
+ const back_key = model.back_kind === "close" ? "close" : "back";
5598
5601
  $back_icon.className = back_key === "close" ? "yi-xmark" : "yi-arrow-left";
5599
5602
  $back.setAttribute("title", t(back_key));
5600
5603
  $back.setAttribute("data-i18n-title", back_key);
@@ -12749,7 +12752,7 @@ function set_pressed_state($container, selector, set) {
12749
12752
  *
12750
12753
  * Utilities for graph, using Bulma
12751
12754
  *
12752
- * Copyright (c) 2025, ArtGins.
12755
+ * Copyright (c) 2025-2026, ArtGins.
12753
12756
  * All Rights Reserved.
12754
12757
  ***********************************************************************/
12755
12758
  /************************************************************
@@ -12778,15 +12781,46 @@ function removeChildElements($element) {
12778
12781
  }
12779
12782
  /************************************************************
12780
12783
  * Disable/Enable elements
12781
- ************************************************************/
12784
+ *
12785
+ * A disabled control says so to the keyboard and to a screen
12786
+ * reader too, not only to the pointer:
12787
+ * - `disabled`, what the stylesheets and the click guards
12788
+ * read (on a `div` drawn as a button it means nothing to
12789
+ * the browser itself);
12790
+ * - `aria-disabled="true"`, what a reader announces -- without
12791
+ * it a disabled toolbar item was heard as a button that
12792
+ * works;
12793
+ * - `tabindex="-1"`, so Tab does not stop on a control that
12794
+ * does nothing.
12795
+ *
12796
+ * Enabled again, it gets back the tabindex it had, kept in
12797
+ * `data-enabled-tabindex` while it is disabled (empty: it had
12798
+ * none, and it gets none). Whoever BUILDS a control disabled
12799
+ * writes that attribute itself, with the tabindex the control
12800
+ * takes once enabled (see toolbar_item_html in
12801
+ * c_g6_nodes_tree.js). An element that keeps the focus while it
12802
+ * is disabled keeps it: tabindex -1 still takes a focus, it is
12803
+ * only left out of the Tab order.
12804
+ ************************************************************/
12805
+ var ENABLED_TABINDEX = "data-enabled-tabindex";
12782
12806
  function disableElements($container, selector) {
12783
12807
  $container.querySelectorAll(selector).forEach((el) => {
12784
12808
  el.setAttribute("disabled", "");
12809
+ el.setAttribute("aria-disabled", "true");
12810
+ if (!el.hasAttribute(ENABLED_TABINDEX)) el.setAttribute(ENABLED_TABINDEX, el.getAttribute("tabindex") ?? "");
12811
+ el.setAttribute("tabindex", "-1");
12785
12812
  });
12786
12813
  }
12787
12814
  function enableElements($container, selector) {
12788
12815
  $container.querySelectorAll(selector).forEach((el) => {
12789
12816
  if (el.hasAttribute("disabled")) el.removeAttribute("disabled");
12817
+ el.removeAttribute("aria-disabled");
12818
+ if (el.hasAttribute(ENABLED_TABINDEX)) {
12819
+ let tabindex = el.getAttribute(ENABLED_TABINDEX);
12820
+ el.removeAttribute(ENABLED_TABINDEX);
12821
+ if (tabindex === "") el.removeAttribute("tabindex");
12822
+ else el.setAttribute("tabindex", tabindex);
12823
+ }
12790
12824
  });
12791
12825
  }
12792
12826
  /************************************************************
@@ -13169,7 +13203,7 @@ function yui_graph_update_anchor($container, state) {
13169
13203
  set_pressed_state($container, ".EV_TOGGLE_ANCHOR", state === "on");
13170
13204
  $btn.classList.toggle("color_pending_state", state === "arming");
13171
13205
  $btn.setAttribute("aria-pressed", state === "on" ? "true" : "false");
13172
- let key = state === "arming" ? "click the element to centre on" : state === "on" ? "centred: click to release" : "anchor view";
13206
+ const key = state === "arming" ? "click the element to centre on" : state === "on" ? "centred: click to release" : "anchor view";
13173
13207
  $btn.setAttribute("title", t(key));
13174
13208
  $btn.setAttribute("data-i18n-title", key);
13175
13209
  $btn.setAttribute("aria-label", t(key));
@@ -13783,6 +13817,73 @@ function g6_event_kw(evt) {
13783
13817
  };
13784
13818
  }
13785
13819
  //#endregion
13820
+ //#region src/g6_canvas_tab_order.js
13821
+ /***********************************************************************
13822
+ * g6_canvas_tab_order.js
13823
+ *
13824
+ * The canvases of a G6 graph in the page's Tab order: the main
13825
+ * one in its place, the other layers out of it.
13826
+ *
13827
+ * G6 stacks four canvases (background, main, label, transient)
13828
+ * and gives each of them `tabIndex = 1` (configCanvasDom, when it
13829
+ * builds the Canvas and again on a renderer change). A POSITIVE
13830
+ * tabindex goes before every control of the page: Tab from a
13831
+ * graph went to its other canvases, then to the top of the page
13832
+ * and through every control there, and only then to the graph's
13833
+ * own buttons. The main canvas -- the one G6 listens on, and the
13834
+ * one a view puts the keyboard on -- takes 0, so Tab goes from it
13835
+ * to the graph's toolbars and buttons in DOM order; the other
13836
+ * layers receive nothing (`pointer-events: none`) and take -1.
13837
+ *
13838
+ * Every view that creates a G6 Graph calls
13839
+ * keep_canvas_tab_order() right after `new Graph()`: the order is
13840
+ * put right each time G6 writes the 1 -- once the canvas is built
13841
+ * (on the first render(), whichever function of the view calls
13842
+ * it), and after a renderer change. One place, so a view with
13843
+ * several render() calls cannot miss the one that builds the
13844
+ * canvas.
13845
+ *
13846
+ * Copyright (c) 2026, ArtGins.
13847
+ * All Rights Reserved.
13848
+ ***********************************************************************/
13849
+ /************************************************************
13850
+ * Main canvas 0, every other layer -1. Returns 0, or -1 with
13851
+ * the cause logged.
13852
+ ************************************************************/
13853
+ function set_canvas_tab_order(gobj, graph) {
13854
+ let canvas = graph ? graph.getCanvas() : null;
13855
+ if (!canvas || typeof canvas.getLayers !== "function") {
13856
+ log_error(`${gobj_short_name(gobj)}: no canvas to set the Tab order of`);
13857
+ return -1;
13858
+ }
13859
+ let ret = 0;
13860
+ let layers = canvas.getLayers() || {};
13861
+ for (let name of Object.keys(layers)) {
13862
+ let $el = null;
13863
+ try {
13864
+ $el = layers[name].getContextService().getDomElement();
13865
+ } catch (e) {
13866
+ $el = null;
13867
+ }
13868
+ if (!$el) {
13869
+ log_error(`${gobj_short_name(gobj)}: canvas layer '${name}' has no element`);
13870
+ ret = -1;
13871
+ continue;
13872
+ }
13873
+ $el.tabIndex = name === "main" ? 0 : -1;
13874
+ }
13875
+ return ret;
13876
+ }
13877
+ /************************************************************
13878
+ * Keep the order for the life of `graph`: on every moment G6
13879
+ * writes its own.
13880
+ ************************************************************/
13881
+ function keep_canvas_tab_order(gobj, graph) {
13882
+ for (let ev of [GraphEvent.AFTER_CANVAS_INIT, GraphEvent.AFTER_RENDERER_CHANGE]) graph.on(ev, () => {
13883
+ set_canvas_tab_order(gobj, graph);
13884
+ });
13885
+ }
13886
+ //#endregion
13786
13887
  //#region src/c_yui_json_graph.js
13787
13888
  /***********************************************************************
13788
13889
  * c_yui_json_graph.js
@@ -14286,6 +14387,7 @@ function build_graph$4(gobj) {
14286
14387
  behaviors: BEHAVIORS_READING$1.slice()
14287
14388
  });
14288
14389
  if (priv.theme) graph.setTheme(priv.theme);
14390
+ keep_canvas_tab_order(gobj, graph);
14289
14391
  graph.on(NodeEvent.CLICK, (evt) => {
14290
14392
  gobj_send_event(gobj, "EV_NODE_CLICK", g6_event_kw(evt), gobj);
14291
14393
  });
@@ -18203,6 +18305,7 @@ function build_graph$3(gobj) {
18203
18305
  } catch (e) {
18204
18306
  log_error(`${gobj_short_name(gobj)}: the theme was refused: ${e}`);
18205
18307
  }
18308
+ keep_canvas_tab_order(gobj, priv.graph);
18206
18309
  priv.graph.on("aftertransform", () => {
18207
18310
  yui_graph_update_zoom(gobj_read_attr(gobj, "$container"), priv.graph);
18208
18311
  });
@@ -21706,6 +21809,7 @@ function build_graph$2(gobj) {
21706
21809
  behaviors: BEHAVIORS_READING.slice()
21707
21810
  });
21708
21811
  if (priv.theme) graph.setTheme(priv.theme);
21812
+ keep_canvas_tab_order(gobj, graph);
21709
21813
  graph.on("aftertransform", () => {
21710
21814
  yui_graph_update_zoom(gobj_read_attr(gobj, "$container"), priv.graph);
21711
21815
  let z = priv.last_zoom;
@@ -23522,7 +23626,7 @@ function build_ui$7(gobj) {
23522
23626
  if (layout !== "drawer") {
23523
23627
  $container.setAttribute("role", "navigation");
23524
23628
  if (!$container.hasAttribute("aria-label")) {
23525
- let label = gobj_read_attr(gobj, "nav_label") || "navigation";
23629
+ const label = gobj_read_attr(gobj, "nav_label") || "navigation";
23526
23630
  $container.setAttribute("aria-label", t(label));
23527
23631
  $container.setAttribute("data-i18n-aria-label", label);
23528
23632
  }
@@ -23783,14 +23887,17 @@ function render_backbar(gobj) {
23783
23887
  let label = gobj_read_attr(gobj, "nav_label") || "";
23784
23888
  let label_attrs = { class: "yui-nav-label" };
23785
23889
  if (label) label_attrs.i18n = label;
23890
+ const name_key = label || "back";
23786
23891
  let a_attrs = {
23787
23892
  class: "yui-nav-item yui-nav-back",
23788
23893
  href: back_route ? "#" + back_route : "#",
23789
23894
  "data-item-id": "__index__",
23790
23895
  "data-route": back_route,
23791
- "aria-label": label || "back"
23896
+ title: t(name_key),
23897
+ "data-i18n-title": name_key,
23898
+ "aria-label": t(name_key),
23899
+ "data-i18n-aria-label": name_key
23792
23900
  };
23793
- if (label) a_attrs["data-i18n-aria-label"] = label;
23794
23901
  return createElement2([
23795
23902
  "div",
23796
23903
  {},
@@ -23817,7 +23924,7 @@ function render_drawer(gobj, items) {
23817
23924
  wrap.className = "yui-drawer";
23818
23925
  wrap.setAttribute("role", "dialog");
23819
23926
  wrap.setAttribute("aria-modal", "true");
23820
- let nav_label = gobj_read_attr(gobj, "nav_label") || "navigation";
23927
+ const nav_label = gobj_read_attr(gobj, "nav_label") || "navigation";
23821
23928
  wrap.setAttribute("aria-label", t(nav_label));
23822
23929
  wrap.setAttribute("data-i18n-aria-label", nav_label);
23823
23930
  let back = document.createElement("div");
@@ -25612,7 +25719,7 @@ function show_load_error$1(gobj, message) {
25612
25719
  $container.insertBefore($err, $container.firstChild);
25613
25720
  }
25614
25721
  let treedb = gobj_read_attr(gobj, "treedb_name") || "";
25615
- $err.textContent = (treedb ? `${treedb}: ` : "") + (message || "cannot load treedb");
25722
+ $err.textContent = (treedb ? `${treedb}: ` : "") + (message || t("cannot load treedb"));
25616
25723
  }
25617
25724
  /************************************************************
25618
25725
  * Destroy UI
@@ -27990,7 +28097,6 @@ function yui_selection_bar(t, opts) {
27990
28097
  if (typeof o.on_clear === "function") o.on_clear(e);
27991
28098
  } }
27992
28099
  ]);
27993
- $clear._yui_label = "clear selection";
27994
28100
  let $el = createElement2([
27995
28101
  "div",
27996
28102
  {
@@ -28006,7 +28112,7 @@ function yui_selection_bar(t, opts) {
28006
28112
  $count.textContent = t("{{n}} selected", { n: count });
28007
28113
  for (let $b of $buttons.concat([$clear])) {
28008
28114
  let $label = label_of($b);
28009
- let key = $b._yui_label;
28115
+ const key = $b === $clear ? "clear selection" : $b._yui_label;
28010
28116
  if ($label && key) $label.textContent = t(key);
28011
28117
  $b.title = key ? t(key) : "";
28012
28118
  $b.setAttribute("aria-label", $b.title);
@@ -30088,7 +30194,7 @@ function open_form_dialog(gobj, mode, record) {
30088
30194
  return;
30089
30195
  }
30090
30196
  let pkey = desc.pkey || "id";
30091
- let title = mode === "create" ? "new" : mode === "view" ? "view" : "edit";
30197
+ const title = mode === "create" ? "new" : mode === "view" ? "view" : "edit";
30092
30198
  let title_prefix = "";
30093
30199
  if (mode !== "create") {
30094
30200
  let rid = record ? String(record[pkey] ?? "") : "";
@@ -60769,17 +60875,41 @@ function css_color_or_null(value) {
60769
60875
  * The markup of one toolbar item. Everything that goes into it is
60770
60876
  * escaped, the read-out branch too: its text, title and value are
60771
60877
  * values (a zoom figure, a translated title), never markup.
60878
+ *
60879
+ * An item is a BUTTON for the keyboard too: `role="button"` and
60880
+ * `tabindex="0"`, and YuiToolbar presses it on Enter or Space.
60881
+ * `title_key` is the i18n key of `title`: written as
60882
+ * `data-i18n-title` / `data-i18n-aria-label`, so the name carries
60883
+ * its key like every other control's.
60884
+ *
60885
+ * A `disabled` item is born the way disableElements() (lib_graph.js)
60886
+ * leaves one: `aria-disabled`, out of the Tab order, and the
60887
+ * tabindex it takes once enabled kept in `data-enabled-tabindex`,
60888
+ * where enableElements() finds it. A toggle (`pressed` given, true
60889
+ * or false) says its state in `aria-pressed`, as its look says it
60890
+ * with `pressed_state`. `data-control` names the CONTROL the item
60891
+ * is, whatever state it shows: `control` when the item gives one
60892
+ * (full screen is `request-fullscreen` or `exit-fullscreen`, and
60893
+ * one control), its `value` otherwise -- it is what a rebuild puts
60894
+ * the focus back on.
60772
60895
  ***************************************************************/
60773
60896
  function toolbar_item_html(item) {
60774
60897
  if (item.separator) return `<div class="g6-toolbar-sep" aria-hidden="true"></div>`;
60775
60898
  const extra_class = item.className ? ` ${escapeHtml(String(item.className))}` : "";
60776
60899
  const title = escapeHtml(String(item.title ?? ""));
60777
- if (item.readout) return `<div class="g6-toolbar-readout${extra_class}" title="${title}" aria-label="${title}">${escapeHtml(String(item.text ?? ""))}</div>`;
60778
- const disabled = item.disabled ? " disabled" : "";
60900
+ let i18n_attrs = "";
60901
+ if (item.title_key) {
60902
+ const key = escapeHtml(String(item.title_key));
60903
+ i18n_attrs = ` data-i18n-title="${key}" data-i18n-aria-label="${key}"`;
60904
+ }
60905
+ if (item.readout) return `<div class="g6-toolbar-readout${extra_class}" title="${title}" aria-label="${title}"${i18n_attrs}>${escapeHtml(String(item.text ?? ""))}</div>`;
60906
+ const state = item.disabled ? " tabindex=\"-1\" aria-disabled=\"true\" data-enabled-tabindex=\"0\" disabled" : " tabindex=\"0\"";
60907
+ const pressed = item.pressed === void 0 ? "" : ` aria-pressed="${item.pressed ? "true" : "false"}"`;
60908
+ const control = escapeHtml(String(item.control ?? item.value));
60779
60909
  const is_text = item.text !== void 0;
60780
60910
  const kind_class = is_text ? " g6-toolbar-item-text" : "";
60781
60911
  const body = is_text ? `<span class="g6-toolbar-text">${escapeHtml(String(item.text))}</span>` : `<svg aria-hidden="true" focusable="false"><use xlink:href="#${escapeHtml(String(item.id))}"></use></svg>`;
60782
- return `<div class="g6-toolbar-item${kind_class}${extra_class}" value="${escapeHtml(String(item.value))}" title="${title}" aria-label="${title}"${disabled}>${body}</div>`;
60912
+ return `<div class="g6-toolbar-item${kind_class}${extra_class}" role="button"${state}${pressed} value="${escapeHtml(String(item.value))}" data-control="${control}" title="${title}" aria-label="${title}"${i18n_attrs}>${body}</div>`;
60783
60913
  }
60784
60914
  /***************************************************************
60785
60915
  * YuiToolbar — G6 Toolbar subclass that adds per-item className
@@ -60794,6 +60924,11 @@ function toolbar_item_html(item) {
60794
60924
  * readout — a text item that REPORTS instead of acting (the
60795
60925
  * zoom level); it is not a button
60796
60926
  * separator — a group divider, not an item
60927
+ * title_key — the i18n key of `title` (see toolbar_item_html)
60928
+ * pressed — boolean; the item is a TOGGLE, and this is its
60929
+ * state (`aria-pressed`)
60930
+ * control — the identity of the control when its `value`
60931
+ * changes with its state (full screen enter/exit)
60797
60932
  *
60798
60933
  * The base class fires onClick only for elements whose class list
60799
60934
  * contains `g6-toolbar-item`, so a separator and a readout carry
@@ -60804,12 +60939,117 @@ function toolbar_item_html(item) {
60804
60939
  * functions (set_submit_state, disableElements, …) work on toolbar
60805
60940
  * icons exactly like on regular Bulma/FA buttons — no special
60806
60941
  * icon-specific helpers needed.
60942
+ *
60943
+ * The items are buttons for the KEYBOARD as well:
60944
+ * - Enter or Space CLICKS the focused item, and the click bubbles
60945
+ * to the base class's own handler, so the key reaches the same
60946
+ * onClick a pointer does. A `disabled` item takes the key and
60947
+ * does nothing, as its `pointer-events: none` does for a press.
60948
+ * - A POINTER press does not move the focus to an item (it stays
60949
+ * on the canvas, where the graph's keys are heard), the way the
60950
+ * floating buttons keep it (see keep_focus_on_press).
60951
+ * - A disabled item is out of the Tab order and says it is
60952
+ * disabled (`aria-disabled`), from its first render
60953
+ * (toolbar_item_html) and on every change (disableElements /
60954
+ * enableElements in lib_graph.js).
60955
+ * - A rebuild (update) replaces the innerHTML, the focused item
60956
+ * with it: the focus goes back to the item of the same CONTROL
60957
+ * (`data-control`: Enter on "enter full screen" lands on "exit
60958
+ * full screen"), or to the canvas when there is none. And a
60959
+ * toolbar that goes away (destroy) hands it to the canvas
60960
+ * first. A removed element that held the focus leaves it on
60961
+ * <body> in Firefox, which fires no focusout for it, and the
60962
+ * graph goes deaf.
60807
60963
  ***************************************************************/
60808
60964
  var YuiToolbar = class extends Toolbar {
60965
+ constructor(context, options) {
60966
+ super(context, options);
60967
+ this.$element.addEventListener("pointerdown", keep_focus_on_toolbar_press);
60968
+ this.$element.addEventListener("keydown", press_toolbar_item_on_key);
60969
+ }
60970
+ async update(options) {
60971
+ const control = focused_toolbar_item_control(this.$element);
60972
+ await super.update(options);
60973
+ if (control !== null) refocus_toolbar_item(this.$element, control);
60974
+ }
60975
+ destroy() {
60976
+ if (!this.destroyed && this.$element) {
60977
+ this.$element.removeEventListener("pointerdown", keep_focus_on_toolbar_press);
60978
+ this.$element.removeEventListener("keydown", press_toolbar_item_on_key);
60979
+ if (this.$element.contains(document.activeElement)) focus_main_canvas(this.$element.parentNode);
60980
+ }
60981
+ super.destroy();
60982
+ }
60809
60983
  async getDOMContent() {
60810
60984
  return (await this.options.getItems()).map(toolbar_item_html).join("");
60811
60985
  }
60812
60986
  };
60987
+ /***************************************************************
60988
+ * A POINTER press on a button of this view does not take the
60989
+ * focus. The container's capture listener has just put it on the
60990
+ * canvas, where the graph's keys are heard; a press that moved it
60991
+ * on to the button would leave it on something this view removes
60992
+ * -- Escape deselects and takes the floating buttons away, the ✕
60993
+ * closes its popover, a rebuild replaces the toolbar -- and
60994
+ * Firefox fires no focusout for a removed element: the focus falls
60995
+ * to <body> and the graph hears nothing until it is clicked again.
60996
+ * Cancelling the pointerdown cancels the mousedown that moves the
60997
+ * focus; the click still comes, and Tab still reaches the button.
60998
+ ***************************************************************/
60999
+ function keep_focus_on_press(el) {
61000
+ el.addEventListener("pointerdown", (e) => {
61001
+ e.preventDefault();
61002
+ });
61003
+ }
61004
+ function keep_focus_on_toolbar_press(e) {
61005
+ let target = e.target;
61006
+ if (target && typeof target.closest === "function" && target.closest(".g6-toolbar-item")) e.preventDefault();
61007
+ }
61008
+ /***************************************************************
61009
+ * The keyboard to the graph: the focus on its main canvas.
61010
+ ***************************************************************/
61011
+ function focus_main_canvas($container) {
61012
+ if (!$container) return;
61013
+ let $canvas = main_canvas_of($container);
61014
+ if ($canvas && $canvas.isConnected && typeof $canvas.focus === "function") $canvas.focus({ preventScroll: true });
61015
+ }
61016
+ /***************************************************************
61017
+ * Enter or Space on a toolbar item clicks it; the click bubbles
61018
+ * to G6's toolbar handler, which reads the item's `value`.
61019
+ ***************************************************************/
61020
+ function press_toolbar_item_on_key(e) {
61021
+ if (e.key !== "Enter" && e.key !== " ") return;
61022
+ let target = e.target;
61023
+ let item = target && typeof target.closest === "function" ? target.closest(".g6-toolbar-item") : null;
61024
+ if (!item) return;
61025
+ e.preventDefault();
61026
+ e.stopPropagation();
61027
+ if (item.hasAttribute("disabled")) return;
61028
+ item.click();
61029
+ }
61030
+ /***************************************************************
61031
+ * The control (`data-control`) of the toolbar item that has the
61032
+ * focus, or null.
61033
+ ***************************************************************/
61034
+ function focused_toolbar_item_control($toolbar) {
61035
+ let active = document.activeElement;
61036
+ if (!$toolbar || !active || active === $toolbar || !$toolbar.contains(active)) return null;
61037
+ if (!active.classList || !active.classList.contains("g6-toolbar-item")) return null;
61038
+ return active.getAttribute("data-control");
61039
+ }
61040
+ /***************************************************************
61041
+ * After a rebuild: the focus on the item of that control, in
61042
+ * whatever state the rebuild shows it, or on the canvas when the
61043
+ * rebuild has none (a fold took it away).
61044
+ ***************************************************************/
61045
+ function refocus_toolbar_item($toolbar, control) {
61046
+ let items = $toolbar.querySelectorAll(".g6-toolbar-item");
61047
+ for (let i = 0; i < items.length; i++) if (items[i].getAttribute("data-control") === control) {
61048
+ items[i].focus({ preventScroll: true });
61049
+ return;
61050
+ }
61051
+ focus_main_canvas($toolbar.parentNode);
61052
+ }
60813
61053
  register(ExtensionCategory.PLUGIN, "yui-toolbar", YuiToolbar);
60814
61054
  ensure_drag_canvas_patch();
60815
61055
  ensure_brush_select_owned();
@@ -60974,6 +61214,7 @@ var PRIVATE_DATA$4 = {
60974
61214
  _saved_graph_properties: {},
60975
61215
  _graphs_writes_owed: {},
60976
61216
  _graphs_load: 0,
61217
+ _save_lit: false,
60977
61218
  yet_showed: false,
60978
61219
  edit_mode: false,
60979
61220
  operation_mode: null,
@@ -61007,6 +61248,7 @@ var PRIVATE_DATA$4 = {
61007
61248
  _detail_node_id: null,
61008
61249
  _delete_confirm_el: null,
61009
61250
  _unlink_confirm_el: null,
61251
+ _confirm_questions: {},
61010
61252
  _create_popover_el: null,
61011
61253
  _context_node_id: null,
61012
61254
  _context_port_key: null,
@@ -61015,6 +61257,7 @@ var PRIVATE_DATA$4 = {
61015
61257
  _link_source: null,
61016
61258
  _link_icon_el: null,
61017
61259
  _link_drag_svg: null,
61260
+ _link_drag: null,
61018
61261
  _link_valid_hooks: [],
61019
61262
  _link_saved_styles: [],
61020
61263
  _focus_topic: null,
@@ -61258,6 +61501,7 @@ function build_graph$1(gobj) {
61258
61501
  plugins: []
61259
61502
  });
61260
61503
  graph.setTheme(priv.theme);
61504
+ keep_canvas_tab_order(gobj, graph);
61261
61505
  priv._unforward_wheel = yui_graph_forward_wheel(graph, priv.$container, { selector: ".TREEDB_CARD, .TREEDB_PILLS" });
61262
61506
  graph.setLayout(layout);
61263
61507
  priv.graph_rendered = false;
@@ -61415,8 +61659,6 @@ function update_toolbar(gobj) {
61415
61659
  if (graph_get_plugin(gobj, "toolbar")) graph.updatePlugin({ key: "toolbar" });
61416
61660
  configure_toolbar_edit(gobj);
61417
61661
  if (graph_get_plugin(gobj, "toolbar-edit")) graph.updatePlugin({ key: "toolbar-edit" });
61418
- update_zoom_selection_button(gobj);
61419
- update_selection_mode_button(gobj);
61420
61662
  }
61421
61663
  /************************************************************
61422
61664
  * The floating toolbars follow the theme.
@@ -61467,13 +61709,15 @@ function toolbar_fold_item(gobj) {
61467
61709
  id: "g6-icon-toolbar-show",
61468
61710
  value: "toolbar-toggle",
61469
61711
  className: "EV_TOGGLE_TOOLBARS",
61470
- title: t("show toolbar")
61712
+ title: t("show toolbar"),
61713
+ title_key: "show toolbar"
61471
61714
  };
61472
61715
  return {
61473
61716
  id: "g6-icon-toolbar-hide",
61474
61717
  value: "toolbar-toggle",
61475
61718
  className: "EV_TOGGLE_TOOLBARS",
61476
- title: t("hide toolbar")
61719
+ title: t("hide toolbar"),
61720
+ title_key: "hide toolbar"
61477
61721
  };
61478
61722
  }
61479
61723
  function toolbar_style(extra) {
@@ -61539,38 +61783,44 @@ function configure_toolbar(gobj) {
61539
61783
  id: "g6-icon-zoom-in",
61540
61784
  value: "zoom-in",
61541
61785
  className: "EV_ZOOM_IN",
61542
- title: t("zoom in")
61786
+ title: t("zoom in"),
61787
+ title_key: "zoom in"
61543
61788
  },
61544
61789
  {
61545
61790
  id: "g6-icon-zoom-out",
61546
61791
  value: "zoom-out",
61547
61792
  className: "EV_ZOOM_OUT",
61548
- title: t("zoom out")
61793
+ title: t("zoom out"),
61794
+ title_key: "zoom out"
61549
61795
  },
61550
61796
  {
61551
61797
  readout: true,
61552
61798
  className: "G6_ZOOM_LEVEL",
61553
61799
  text: zoom_percent_text(gobj),
61554
- title: t("zoom level")
61800
+ title: t("zoom level"),
61801
+ title_key: "zoom level"
61555
61802
  },
61556
61803
  { separator: true },
61557
61804
  {
61558
61805
  id: "g6-icon-fit",
61559
61806
  value: "auto-fit",
61560
61807
  className: "EV_AUTO_FIT",
61561
- title: t("auto fit")
61808
+ title: t("auto fit"),
61809
+ title_key: "auto fit"
61562
61810
  },
61563
61811
  {
61564
61812
  text: "1:1",
61565
61813
  value: "reset",
61566
61814
  className: "EV_ZOOM_RESET",
61567
- title: t("actual size")
61815
+ title: t("actual size"),
61816
+ title_key: "actual size"
61568
61817
  },
61569
61818
  {
61570
61819
  id: "g6-icon-anchor",
61571
61820
  value: "anchor",
61572
61821
  className: "EV_TOGGLE_ANCHOR" + (priv.anchor_state === "on" ? " pressed_state" : "") + (priv.anchor_state === "arming" ? " color_pending_state" : ""),
61573
- title: anchor_title(priv.anchor_state)
61822
+ pressed: priv.anchor_state === "on",
61823
+ ...anchor_title(priv.anchor_state)
61574
61824
  }
61575
61825
  ];
61576
61826
  if (priv.edit_mode) items.push({
@@ -61578,21 +61828,26 @@ function configure_toolbar(gobj) {
61578
61828
  value: "fit-selection",
61579
61829
  className: "EV_ZOOM_SELECTION",
61580
61830
  title: t("zoom to selection"),
61581
- disabled: true
61831
+ title_key: "zoom to selection",
61832
+ disabled: !selection_is_painted(gobj)
61582
61833
  });
61583
61834
  if (gobj_read_bool_attr(gobj, "with_fullscreen")) {
61584
61835
  items.push({ separator: true });
61585
61836
  if (priv.is_fullscreen) items.push({
61586
61837
  id: "g6-icon-fullscreen-exit",
61587
61838
  value: "exit-fullscreen",
61839
+ control: "fullscreen",
61588
61840
  className: "EV_EXIT_FULLSCREEN",
61589
- title: t("exit full screen")
61841
+ title: t("exit full screen"),
61842
+ title_key: "exit full screen"
61590
61843
  });
61591
61844
  else items.push({
61592
61845
  id: "g6-icon-fullscreen",
61593
61846
  value: "request-fullscreen",
61847
+ control: "fullscreen",
61594
61848
  className: "EV_REQUEST_FULLSCREEN",
61595
- title: t("enter full screen")
61849
+ title: t("enter full screen"),
61850
+ title_key: "enter full screen"
61596
61851
  });
61597
61852
  }
61598
61853
  if (toolbars_can_collapse(gobj)) {
@@ -61653,46 +61908,53 @@ function configure_toolbar_edit(gobj) {
61653
61908
  position: "left-top",
61654
61909
  style: toolbar_style(),
61655
61910
  getItems: () => {
61911
+ let lit = edit_buttons_lit(gobj);
61656
61912
  return [
61657
61913
  {
61658
61914
  id: "g6-icon-plus",
61659
61915
  value: "create-node",
61660
61916
  className: "EV_CREATE_NODE_BTN color_create_state",
61661
- title: t("create node")
61917
+ title: t("create node"),
61918
+ title_key: "create node"
61662
61919
  },
61663
61920
  {
61664
61921
  id: "g6-icon-select-mode",
61665
61922
  value: "selection-mode",
61666
61923
  className: "EV_TOGGLE_SELECTION_MODE" + (priv.selection_mode ? " pressed_state" : ""),
61667
- title: t("selection mode")
61924
+ pressed: !!priv.selection_mode,
61925
+ title: t("selection mode"),
61926
+ title_key: "selection mode"
61668
61927
  },
61669
61928
  {
61670
61929
  id: "g6-icon-save",
61671
61930
  value: "save",
61672
- className: "EV_SAVE_GRAPH",
61931
+ className: "EV_SAVE_GRAPH" + (lit.save ? " color_submit_state" : ""),
61673
61932
  title: t("save"),
61674
- disabled: true
61933
+ title_key: "save",
61934
+ disabled: !lit.save
61675
61935
  },
61676
61936
  {
61677
61937
  id: "g6-icon-undo",
61678
61938
  value: "undo",
61679
- className: "EV_HISTORY_UNDO",
61939
+ className: "EV_HISTORY_UNDO" + (lit.undo ? " color_active_state" : ""),
61680
61940
  title: t("undo"),
61681
- disabled: true
61941
+ title_key: "undo",
61942
+ disabled: !lit.undo
61682
61943
  },
61683
61944
  {
61684
61945
  id: "g6-icon-redo",
61685
61946
  value: "redo",
61686
- className: "EV_HISTORY_REDO",
61947
+ className: "EV_HISTORY_REDO" + (lit.redo ? " color_active_state" : ""),
61687
61948
  title: t("redo"),
61688
- disabled: true
61949
+ title_key: "redo",
61950
+ disabled: !lit.redo
61689
61951
  }
61690
61952
  ];
61691
61953
  },
61692
61954
  onClick: (value) => {
61693
61955
  switch (value) {
61694
61956
  case "create-node":
61695
- toggle_create_popover(gobj);
61957
+ gobj_send_event(gobj, "EV_TOGGLE_CREATE_POPOVER", {}, gobj);
61696
61958
  break;
61697
61959
  case "selection-mode":
61698
61960
  gobj_send_event(gobj, "EV_TOGGLE_SELECTION_MODE", {}, gobj);
@@ -62753,28 +63015,50 @@ function update_history_buttons(gobj) {
62753
63015
  if (history.canUndo()) {
62754
63016
  enableElements($container, ".EV_HISTORY_UNDO");
62755
63017
  set_active_state($container, ".EV_HISTORY_UNDO", true);
62756
- enableElements($container, ".EV_SAVE_GRAPH");
62757
- set_submit_state($container, ".EV_SAVE_GRAPH", true);
63018
+ light_save_button(gobj, true);
62758
63019
  } else {
62759
63020
  disableElements($container, ".EV_HISTORY_UNDO");
62760
63021
  set_active_state($container, ".EV_HISTORY_UNDO", false);
62761
- if (graphs_write_is_owed(gobj)) {
62762
- enableElements($container, ".EV_SAVE_GRAPH");
62763
- set_submit_state($container, ".EV_SAVE_GRAPH", true);
62764
- } else {
62765
- disableElements($container, ".EV_SAVE_GRAPH");
62766
- set_submit_state($container, ".EV_SAVE_GRAPH", false);
62767
- }
63022
+ light_save_button(gobj, graphs_write_is_owed(gobj));
62768
63023
  }
62769
63024
  } else {
62770
63025
  disableElements($container, ".EV_HISTORY_REDO");
62771
63026
  set_active_state($container, ".EV_HISTORY_REDO", false);
62772
63027
  disableElements($container, ".EV_HISTORY_UNDO");
62773
63028
  set_active_state($container, ".EV_HISTORY_UNDO", false);
63029
+ light_save_button(gobj, graphs_write_is_owed(gobj));
62774
63030
  }
62775
63031
  }
62776
63032
  }
62777
63033
  /************************************************************
63034
+ * What Save, Undo and Redo show when the edit toolbar is
63035
+ * built (its getItems): Undo and Redo what the history can do
63036
+ * now; Save what it was last told (`_save_lit`, see
63037
+ * light_save_button), because more than the history lights
63038
+ * it -- a change the history does not record
63039
+ * (mark_graph_dirty), a write owed -- and update_history_
63040
+ * buttons() is where those are weighed.
63041
+ ************************************************************/
63042
+ function edit_buttons_lit(gobj) {
63043
+ let history = graph_get_plugin(gobj, "history");
63044
+ return {
63045
+ undo: !!history && history.canUndo(),
63046
+ redo: !!history && history.canRedo(),
63047
+ save: !!gobj.priv._save_lit
63048
+ };
63049
+ }
63050
+ /************************************************************
63051
+ * Light the Save, or put it out: on the live element, and in
63052
+ * `_save_lit`, where the next rebuild of the toolbar reads it.
63053
+ ************************************************************/
63054
+ function light_save_button(gobj, lit) {
63055
+ let $container = gobj_read_attr(gobj, "$container");
63056
+ gobj.priv._save_lit = !!lit;
63057
+ if (lit) enableElements($container, ".EV_SAVE_GRAPH");
63058
+ else disableElements($container, ".EV_SAVE_GRAPH");
63059
+ set_submit_state($container, ".EV_SAVE_GRAPH", !!lit);
63060
+ }
63061
+ /************************************************************
62778
63062
  * Graph utility functions
62779
63063
  *
62780
63064
  * From the G6 documentation:
@@ -63650,20 +63934,59 @@ function floating_popover_dx() {
63650
63934
  return 4 + floating_icon_size() + 4;
63651
63935
  }
63652
63936
  /************************************************************
63937
+ * A key press IS the click on an element drawn as a button.
63938
+ *
63939
+ * Enter or Space clicks it, so the keyboard reaches the very
63940
+ * handler the pointer does, whatever that handler is. Stopped
63941
+ * here, as the click is: this view's container forwards every
63942
+ * keydown to G6 (its shortcuts, and EV_KEY_DOWN), and a key
63943
+ * that pressed a button is not the graph's. Space would scroll
63944
+ * the page as well. Any other key goes on to the graph, so
63945
+ * Escape and Delete keep doing there what they do.
63946
+ ************************************************************/
63947
+ function click_on_enter_or_space(el) {
63948
+ el.addEventListener("keydown", (e) => {
63949
+ if (e.key === "Enter" || e.key === " ") {
63950
+ e.preventDefault();
63951
+ e.stopPropagation();
63952
+ el.click();
63953
+ }
63954
+ });
63955
+ }
63956
+ /************************************************************
63653
63957
  * Create a floating circular icon button.
63654
- * @param {string} svgKey - key into SVG_ICONS
63655
- * @param {string} color - border & text color (e.g. '#1890ff')
63656
- * @param {number} left - CSS left in px
63657
- * @param {number} top - CSS top in px
63658
- * @param {string} title - tooltip text
63659
- * @param {Function} onClick
63958
+ *
63959
+ * A button for the keyboard too: `role` and `tabindex`, and
63960
+ * Enter or Space click it (click_on_enter_or_space). Its name
63961
+ * carries its KEY, so refresh_language() renames it on a
63962
+ * language switch; a key handed in here goes in
63963
+ * G6_NODES_TREE_I18N_KEYS, because no scan sees it.
63964
+ *
63965
+ * Its click does one thing: it sends `event` to `gobj`, with
63966
+ * `kw` -- the plain identity of what the button belongs to --
63967
+ * and the ACTION does the work, where the `machine` trace sees
63968
+ * it. A pointer press does not take the focus
63969
+ * (keep_focus_on_press).
63970
+ *
63971
+ * @param {string} svgKey - key into SVG_ICONS
63972
+ * @param {string} color - border & text color (e.g. '#1890ff')
63973
+ * @param {number} left - CSS left in px
63974
+ * @param {number} top - CSS top in px
63975
+ * @param {string} title_key - i18n key of its title and aria-label
63976
+ * @param {object} gobj - who hears the click
63977
+ * @param {string} event - an input event of this gclass
63978
+ * @param {object} kw - plain JSON identity, copied per click
63660
63979
  * @returns {HTMLElement}
63661
63980
  ************************************************************/
63662
- function create_floating_icon(svgKey, color, left, top, title, onClick) {
63981
+ function create_floating_icon(svgKey, color, left, top, title_key, gobj, event, kw) {
63663
63982
  const size = floating_icon_size();
63664
63983
  const el = document.createElement("div");
63665
- el.title = title;
63666
- el.setAttribute("aria-label", title);
63984
+ el.setAttribute("role", "button");
63985
+ el.setAttribute("tabindex", "0");
63986
+ el.setAttribute("title", t(title_key));
63987
+ el.setAttribute("data-i18n-title", title_key);
63988
+ el.setAttribute("aria-label", t(title_key));
63989
+ el.setAttribute("data-i18n-aria-label", title_key);
63667
63990
  el.innerHTML = SVG_ICONS[svgKey] || "";
63668
63991
  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 + ";";
63669
63992
  const $svg = el.querySelector("svg");
@@ -63674,19 +63997,33 @@ function create_floating_icon(svgKey, color, left, top, title, onClick) {
63674
63997
  }
63675
63998
  el.addEventListener("click", (e) => {
63676
63999
  e.stopPropagation();
63677
- onClick();
64000
+ gobj_send_event(gobj, event, json_deep_copy(kw || {}), gobj);
63678
64001
  });
64002
+ keep_focus_on_press(el);
64003
+ click_on_enter_or_space(el);
63679
64004
  return el;
63680
64005
  }
63681
64006
  /************************************************************
63682
64007
  * Remove one or more overlay elements from priv and the DOM.
64008
+ *
64009
+ * An overlay that holds the focus -- a floating button reached
64010
+ * with Tab, the ✕ of the detail popover, a field of a popover --
64011
+ * gives it back to the canvas BEFORE it goes. Removing the
64012
+ * focused element is a focus loss the browsers do not report
64013
+ * alike: Chromium fires a focusout, which _on_focusout_restore
64014
+ * answers; Firefox fires none and leaves the focus on <body>,
64015
+ * and the graph is deaf to Escape, Delete and ctrl+A until the
64016
+ * next click on it.
64017
+ *
63683
64018
  * @param {object} gobj
63684
64019
  * @param {...string} keys - priv property names to remove
63685
64020
  ************************************************************/
63686
64021
  function hide_overlay(gobj, ...keys) {
63687
64022
  let priv = gobj.priv;
63688
64023
  for (let key of keys) if (priv[key]) {
63689
- priv[key].remove();
64024
+ let el = priv[key];
64025
+ if (el.contains(document.activeElement) && !gobj_is_destroying(gobj)) focus_main_canvas(priv.$container);
64026
+ el.remove();
63690
64027
  priv[key] = null;
63691
64028
  }
63692
64029
  }
@@ -63731,9 +64068,7 @@ function graphs_write_is_owed(gobj) {
63731
64068
  return Object.keys(gobj.priv._graphs_writes_owed).length > 0;
63732
64069
  }
63733
64070
  function mark_graph_dirty(gobj) {
63734
- let $container = gobj_read_attr(gobj, "$container");
63735
- enableElements($container, ".EV_SAVE_GRAPH");
63736
- set_submit_state($container, ".EV_SAVE_GRAPH", true);
64071
+ light_save_button(gobj, true);
63737
64072
  }
63738
64073
  /************************************************************
63739
64074
  * Extract width/height from a style's size property.
@@ -63748,13 +64083,15 @@ function extract_size(style) {
63748
64083
  }
63749
64084
  /************************************************************
63750
64085
  * Show a pair of floating icons (gear + trash) next to an
63751
- * element. Returns {icon_el, delete_el}.
64086
+ * element. The titles are i18n KEYS (see create_floating_icon);
64087
+ * each click sends its event to `gobj` with the same identity
64088
+ * `kw`. Returns {icon_el, delete_el}.
63752
64089
  ************************************************************/
63753
- function show_dual_icons(gobj, x, y, gear_title, gear_click, trash_title, trash_click) {
64090
+ function show_dual_icons(gobj, x, y, gear_key, gear_event, trash_key, trash_event, kw) {
63754
64091
  let priv = gobj.priv;
63755
- const icon_el = create_floating_icon("gear", "#1890ff", x, y, gear_title, gear_click);
64092
+ const icon_el = create_floating_icon("gear", "#1890ff", x, y, gear_key, gobj, gear_event, kw);
63756
64093
  priv.$container.appendChild(icon_el);
63757
- const delete_el = create_floating_icon("trash", "#ff4d4f", x, y + floating_icon_step(), trash_title, trash_click);
64094
+ const delete_el = create_floating_icon("trash", "#ff4d4f", x, y + floating_icon_step(), trash_key, gobj, trash_event, kw);
63758
64095
  priv.$container.appendChild(delete_el);
63759
64096
  return {
63760
64097
  icon_el,
@@ -63846,10 +64183,22 @@ function create_form_select(parent, options, marginBottom) {
63846
64183
  return select;
63847
64184
  }
63848
64185
  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;";
63849
- function create_form_button_row(parent, buttons) {
64186
+ /************************************************************
64187
+ * The buttons of a popover: each one SENDS an input event of
64188
+ * this gclass to `gobj`, and the action does the work, where
64189
+ * the `machine` trace sees it (V20.5 #4). A button is
64190
+ * {i18n, style, event, kw, form}
64191
+ * `kw` is the plain identity of what the popover is about
64192
+ * (copied per click); `form` names the controls whose `value`
64193
+ * goes in the kw too, as the control says it -- a string, the
64194
+ * action parses it. The kw says what was asked for, in the
64195
+ * trace: the identity and what the form held when it was
64196
+ * pressed.
64197
+ ************************************************************/
64198
+ function create_form_button_row(parent, buttons, gobj) {
63850
64199
  let btnRow = document.createElement("div");
63851
64200
  btnRow.style.cssText = "display:flex;gap:8px;";
63852
- for (let { i18n, style, onClick } of buttons) {
64201
+ for (let { i18n, style, event, kw, form } of buttons) {
63853
64202
  let btn = document.createElement("button");
63854
64203
  btn.textContent = t(i18n);
63855
64204
  btn.setAttribute("data-i18n", i18n);
@@ -63860,18 +64209,28 @@ function create_form_button_row(parent, buttons) {
63860
64209
  btn.style.cssText = style + touch_control_css();
63861
64210
  btn.addEventListener("click", (e) => {
63862
64211
  e.stopPropagation();
63863
- onClick();
64212
+ gobj_send_event(gobj, event, form_kw(kw, form), gobj);
63864
64213
  });
63865
64214
  btnRow.appendChild(btn);
63866
64215
  }
63867
64216
  parent.appendChild(btnRow);
63868
64217
  return btnRow;
63869
64218
  }
64219
+ function form_kw(kw, form) {
64220
+ let out = json_deep_copy(kw || {});
64221
+ for (let [name, control] of Object.entries(form || {})) out[name] = String(control.value);
64222
+ return out;
64223
+ }
63870
64224
  /************************************************************
63871
64225
  * Show a confirmation popover next to a target element.
63872
- * Returns the popover element.
64226
+ *
64227
+ * `question` is the plain identity of what is asked, {what,
64228
+ * id|ids}: the confirm button sends it with EV_CONFIRMED and the
64229
+ * cancel button with EV_CANCEL_CONFIRM, and the actions answer.
64230
+ * `priv_key` is where the popover is kept, and it must be the
64231
+ * one CONFIRM_POPOVER_OF names for `question.what`.
63873
64232
  ************************************************************/
63874
- function show_confirm_popover(gobj, target_el, message, confirm_text, confirm_color, onConfirm, priv_key) {
64233
+ function show_confirm_popover(gobj, target_el, message, confirm_text, confirm_color, question, priv_key) {
63875
64234
  hide_overlay(gobj, priv_key);
63876
64235
  let priv = gobj.priv;
63877
64236
  let containerRect = priv.$container.getBoundingClientRect();
@@ -63892,14 +64251,17 @@ function show_confirm_popover(gobj, target_el, message, confirm_text, confirm_co
63892
64251
  create_form_button_row(popover, [{
63893
64252
  i18n: "cancel",
63894
64253
  style: BTN_STYLE_CANCEL,
63895
- onClick: () => hide_overlay(gobj, priv_key)
64254
+ event: "EV_CANCEL_CONFIRM",
64255
+ kw: question
63896
64256
  }, {
63897
64257
  i18n: confirm_text,
63898
64258
  style: "flex:1;padding:6px;background:" + confirm_color + ";color:#fff;border:none;border-radius:4px;cursor:pointer;font-size:13px;font-weight:500;",
63899
- onClick: onConfirm
63900
- }]);
64259
+ event: "EV_CONFIRMED",
64260
+ kw: question
64261
+ }], gobj);
63901
64262
  priv.$container.appendChild(popover);
63902
64263
  priv[priv_key] = popover;
64264
+ priv._confirm_questions[priv_key] = json_deep_copy(question);
63903
64265
  clamp_popover_position(gobj, popover);
63904
64266
  }
63905
64267
  /************************************************************
@@ -63977,27 +64339,30 @@ function paint_selection(gobj, ids) {
63977
64339
  * Zoom-to-selection can only act on a selection.
63978
64340
  *
63979
64341
  * The attribute is toggled on the element instead of re-rendering
63980
- * the toolbar, which would rebuild its innerHTML -- and with it
63981
- * drop the disabled state of every OTHER button -- each time a
63982
- * card is ticked.
64342
+ * the toolbar, which would rebuild its whole innerHTML each time a
64343
+ * card is ticked. A rebuild asks the same question
64344
+ * (selection_is_painted) in the toolbar's getItems().
63983
64345
  ************************************************************/
64346
+ function selection_is_painted(gobj) {
64347
+ return (gobj.priv._selected_paint_ids || []).length > 0;
64348
+ }
63984
64349
  function update_zoom_selection_button(gobj) {
63985
- let priv = gobj.priv;
63986
64350
  let $container = gobj_read_attr(gobj, "$container");
63987
64351
  if (!$container) return;
63988
64352
  if (!$container.querySelector(".EV_ZOOM_SELECTION")) return;
63989
- if ((priv._selected_paint_ids || []).length > 0) enableElements($container, ".EV_ZOOM_SELECTION");
64353
+ if (selection_is_painted(gobj)) enableElements($container, ".EV_ZOOM_SELECTION");
63990
64354
  else disableElements($container, ".EV_ZOOM_SELECTION");
63991
64355
  }
63992
64356
  /************************************************************
63993
64357
  * The selection mode is a state, so it has to LOOK like one:
63994
64358
  * a PRESSED toggle, not a button wearing one of the palette's
63995
- * action colours (see `pressed_state` in lib_graph.css).
64359
+ * action colours (see `pressed_state` in lib_graph.css). And
64360
+ * it SAYS it, in `aria-pressed`, for whoever does not see it.
63996
64361
  *
63997
64362
  * Toggled on the element rather than re-rendered with the
63998
64363
  * toolbar, for the same reason as the button above: a
63999
- * re-render rebuilds the innerHTML and drops what every OTHER
64000
- * button was told.
64364
+ * re-render rebuilds the whole innerHTML. A rebuild draws the
64365
+ * same state, baked in the edit toolbar's getItems().
64001
64366
  ************************************************************/
64002
64367
  function update_selection_mode_button(gobj) {
64003
64368
  let priv = gobj.priv;
@@ -64005,6 +64370,9 @@ function update_selection_mode_button(gobj) {
64005
64370
  if (!$container) return;
64006
64371
  if (!$container.querySelector(".EV_TOGGLE_SELECTION_MODE")) return;
64007
64372
  set_pressed_state($container, ".EV_TOGGLE_SELECTION_MODE", priv.selection_mode);
64373
+ $container.querySelectorAll(".EV_TOGGLE_SELECTION_MODE").forEach(($el) => {
64374
+ $el.setAttribute("aria-pressed", priv.selection_mode ? "true" : "false");
64375
+ });
64008
64376
  }
64009
64377
  /************************************************************
64010
64378
  * Select a SET of nodes: what a marquee and a shift-click do.
@@ -64459,13 +64827,18 @@ function show_link_icon_if_fkey(gobj) {
64459
64827
  let canvasPos = get_port_canvas_position(gobj, priv._selected_node_id, priv._selected_port_key);
64460
64828
  if (!canvasPos) return;
64461
64829
  let vp = graph.getViewportByCanvas([canvasPos.x, canvasPos.y]);
64462
- let icon = create_floating_icon("link", "#52c41a", vp[0] + floating_icon_size() / 2 + 4, vp[1] + floating_icon_dy(), t("link to hook"), () => {
64463
- if (!priv._linking_mode) enter_linking_mode(gobj);
64464
- });
64830
+ let identity = {
64831
+ node_id: priv._selected_node_id,
64832
+ port_key: priv._selected_port_key
64833
+ };
64834
+ 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);
64465
64835
  icon.addEventListener("pointerdown", (e) => {
64466
64836
  e.stopPropagation();
64467
64837
  e.preventDefault();
64468
- start_link_drag(gobj, e);
64838
+ gobj_send_event(gobj, "EV_START_LINK_DRAG", Object.assign(json_deep_copy(identity), {
64839
+ client_x: e.clientX,
64840
+ client_y: e.clientY
64841
+ }), gobj);
64469
64842
  });
64470
64843
  priv.$container.appendChild(icon);
64471
64844
  priv._link_icon_el = icon;
@@ -64515,7 +64888,10 @@ function show_port_icon(gobj) {
64515
64888
  if (!priv._selected_node_id || !priv._selected_port_key) return;
64516
64889
  let xy = port_icon_xy(gobj);
64517
64890
  if (!xy) return;
64518
- let icon = create_floating_icon("gear", "#1890ff", xy.x, xy.y, t("port properties"), () => toggle_port_popover(gobj));
64891
+ let icon = create_floating_icon("gear", "#1890ff", xy.x, xy.y, "port properties", gobj, "EV_TOGGLE_PORT_POPOVER", {
64892
+ node_id: priv._selected_node_id,
64893
+ port_key: priv._selected_port_key
64894
+ });
64519
64895
  priv.$container.appendChild(icon);
64520
64896
  priv._port_icon_el = icon;
64521
64897
  }
@@ -64625,17 +65001,26 @@ function show_port_popover(gobj) {
64625
65001
  create_form_button_row(popover, [{
64626
65002
  i18n: "cancel",
64627
65003
  style: BTN_STYLE_CANCEL,
64628
- onClick: () => {
64629
- hide_port_popover(gobj);
65004
+ event: "EV_CANCEL_PORT_POPOVER",
65005
+ kw: {
65006
+ node_id,
65007
+ port_key
64630
65008
  }
64631
65009
  }, {
64632
65010
  i18n: "apply",
64633
65011
  style: "flex:1;padding:6px;background:#1890ff;color:#fff;border:none;border-radius:4px;cursor:pointer;font-size:13px;font-weight:500;",
64634
- onClick: () => {
64635
- drop_preview_undo(gobj, "_port_preview_undo");
64636
- apply_port_properties(gobj, node_id, port_key, shapeSelect.value, parseInt(rInput.value) || orig_r, scopeSelect.value);
65012
+ event: "EV_APPLY_PORT_POPOVER",
65013
+ kw: {
65014
+ node_id,
65015
+ port_key,
65016
+ r_before: orig_r
65017
+ },
65018
+ form: {
65019
+ shape: shapeSelect,
65020
+ r: rInput,
65021
+ scope: scopeSelect
64637
65022
  }
64638
- }]);
65023
+ }], gobj);
64639
65024
  priv.$container.appendChild(popover);
64640
65025
  priv._port_popover_el = popover;
64641
65026
  set_preview_undo(gobj, "_port_preview_undo", undo_port);
@@ -64852,8 +65237,16 @@ function try_complete_link(gobj, clicked_node_id, clicked_port_key) {
64852
65237
  /************************************************************
64853
65238
  * Drag-link: drag from link icon to a hook port.
64854
65239
  * Draws a temporary SVG line from the fkey port to the cursor.
65240
+ *
65241
+ * Run by ac_start_link_drag. The pointer's MOVES only draw the
65242
+ * line and the cursor -- the drag's own picture, no action, and
65243
+ * one event per move would bury the machine trace -- while the
65244
+ * END of the gesture is an event: EV_LINK_DRAG_END {client_x,
65245
+ * client_y, dragged} on the release, EV_LINK_DRAG_CANCEL when
65246
+ * the browser takes the pointer. Their actions take the drag
65247
+ * down (end_link_drag) and link, or leave the linking mode.
64855
65248
  ************************************************************/
64856
- function start_link_drag(gobj, e) {
65249
+ function start_link_drag(gobj, client_x, client_y) {
64857
65250
  let priv = gobj.priv;
64858
65251
  let graph = priv.graph;
64859
65252
  if (!priv._linking_mode) enter_linking_mode(gobj);
@@ -64867,8 +65260,8 @@ function start_link_drag(gobj, e) {
64867
65260
  let line = document.createElementNS("http://www.w3.org/2000/svg", "line");
64868
65261
  line.setAttribute("x1", startX);
64869
65262
  line.setAttribute("y1", startY);
64870
- line.setAttribute("x2", e.clientX - containerRect.left);
64871
- line.setAttribute("y2", e.clientY - containerRect.top);
65263
+ line.setAttribute("x2", client_x - containerRect.left);
65264
+ line.setAttribute("y2", client_y - containerRect.top);
64872
65265
  line.setAttribute("stroke", "#52c41a");
64873
65266
  line.setAttribute("stroke-width", "2");
64874
65267
  line.setAttribute("stroke-dasharray", "6 3");
@@ -64887,41 +65280,47 @@ function start_link_drag(gobj, e) {
64887
65280
  priv.$container.style.cursor = hoverTarget ? "copy" : "not-allowed";
64888
65281
  }
64889
65282
  function onPointerUp(ev) {
64890
- document.removeEventListener("pointermove", onPointerMove);
64891
- document.removeEventListener("pointerup", onPointerUp);
64892
- document.removeEventListener("pointercancel", onPointerCancel);
64893
- priv.$container.style.cursor = "";
64894
- if (priv._link_drag_svg) {
64895
- priv._link_drag_svg.remove();
64896
- priv._link_drag_svg = null;
64897
- }
64898
- if (!dragged) return;
64899
- let cx = ev.clientX - containerRect.left;
64900
- let cy = ev.clientY - containerRect.top;
64901
- let canvasPt = graph.getCanvasByViewport([cx, cy]);
64902
- let target = find_hook_at_point(gobj, canvasPt[0], canvasPt[1]);
64903
- if (target) try_complete_link(gobj, target.node_id, target.port_key);
64904
- else exit_linking_mode(gobj);
65283
+ gobj_send_event(gobj, "EV_LINK_DRAG_END", {
65284
+ client_x: ev.clientX,
65285
+ client_y: ev.clientY,
65286
+ dragged
65287
+ }, gobj);
64905
65288
  }
64906
65289
  function onPointerCancel(ev) {
65290
+ gobj_send_event(gobj, "EV_LINK_DRAG_CANCEL", {}, gobj);
65291
+ }
65292
+ function unwire() {
64907
65293
  document.removeEventListener("pointermove", onPointerMove);
64908
65294
  document.removeEventListener("pointerup", onPointerUp);
64909
65295
  document.removeEventListener("pointercancel", onPointerCancel);
64910
- priv.$container.style.cursor = "";
64911
- if (priv._link_drag_svg) {
64912
- priv._link_drag_svg.remove();
64913
- priv._link_drag_svg = null;
64914
- }
64915
- exit_linking_mode(gobj);
64916
65296
  }
64917
65297
  document.addEventListener("pointermove", onPointerMove);
64918
65298
  document.addEventListener("pointerup", onPointerUp);
64919
65299
  document.addEventListener("pointercancel", onPointerCancel);
64920
- gobj.priv._drag_unwire = function() {
64921
- document.removeEventListener("pointermove", onPointerMove);
64922
- document.removeEventListener("pointerup", onPointerUp);
64923
- document.removeEventListener("pointercancel", onPointerCancel);
65300
+ priv._link_drag = {
65301
+ left: containerRect.left,
65302
+ top: containerRect.top,
65303
+ unwire
64924
65304
  };
65305
+ priv._drag_unwire = unwire;
65306
+ }
65307
+ /************************************************************
65308
+ * Take a link drag down: its listeners, its line, its cursor.
65309
+ * What was linked, or not, is the caller's.
65310
+ ************************************************************/
65311
+ function end_link_drag(gobj) {
65312
+ let priv = gobj.priv;
65313
+ let drag = priv._link_drag;
65314
+ priv._link_drag = null;
65315
+ if (drag) {
65316
+ drag.unwire();
65317
+ if (priv._drag_unwire === drag.unwire) priv._drag_unwire = null;
65318
+ }
65319
+ priv.$container.style.cursor = "";
65320
+ if (priv._link_drag_svg) {
65321
+ priv._link_drag_svg.remove();
65322
+ priv._link_drag_svg = null;
65323
+ }
64925
65324
  }
64926
65325
  /************************************************************
64927
65326
  * Find if a canvas point hits a valid hook port during
@@ -65211,7 +65610,7 @@ function show_edge_icon(gobj) {
65211
65610
  if (!priv._selected_edge_id) return;
65212
65611
  let mid = get_edge_viewport_midpoint(gobj, priv._selected_edge_id);
65213
65612
  if (!mid) return;
65214
- let icons = show_dual_icons(gobj, mid.x + 4, mid.y + floating_icon_dy(), t("edge properties"), () => toggle_edge_popover(gobj), t("unlink"), () => gobj_send_event(gobj, "EV_REQUEST_UNLINK_EDGE", {}, gobj));
65613
+ 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 });
65215
65614
  priv._edge_icon_el = icons.icon_el;
65216
65615
  priv._edge_delete_el = icons.delete_el;
65217
65616
  }
@@ -65321,17 +65720,19 @@ function show_edge_popover(gobj) {
65321
65720
  create_form_button_row(popover, [{
65322
65721
  i18n: "cancel",
65323
65722
  style: BTN_STYLE_CANCEL,
65324
- onClick: () => {
65325
- deselect_edge(gobj);
65326
- }
65723
+ event: "EV_CANCEL_EDGE_POPOVER",
65724
+ kw: { edge_id }
65327
65725
  }, {
65328
65726
  i18n: "apply",
65329
65727
  style: "flex:1;padding:6px;background:#52c41a;color:#fff;border:none;border-radius:4px;cursor:pointer;font-size:13px;font-weight:500;",
65330
- onClick: () => {
65331
- drop_preview_undo(gobj, "_edge_preview_undo");
65332
- apply_edge_properties(gobj, edge_id, parseInt(lwInput.value) || 2, colorInput.value, scopeSelect.value);
65333
- }
65334
- }]);
65728
+ event: "EV_APPLY_EDGE_POPOVER",
65729
+ kw: { edge_id },
65730
+ form: {
65731
+ line_width: lwInput,
65732
+ color: colorInput,
65733
+ scope: scopeSelect
65734
+ }
65735
+ }], gobj);
65335
65736
  priv.$container.appendChild(popover);
65336
65737
  priv._edge_popover_el = popover;
65337
65738
  set_preview_undo(gobj, "_edge_preview_undo", undo_edge);
@@ -65368,7 +65769,7 @@ function show_node_icon(gobj) {
65368
65769
  if (!priv._selected_node_id || priv._selected_port_key) return;
65369
65770
  let rect = get_node_viewport_rect(gobj, priv._selected_node_id);
65370
65771
  if (!rect) return;
65371
- let icons = show_dual_icons(gobj, rect.right + 4, rect.top + floating_icon_dy(), t("node properties"), () => toggle_node_popover(gobj), t("delete node"), () => gobj_send_event(gobj, "EV_REQUEST_DELETE_NODE", {}, gobj));
65772
+ 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 });
65372
65773
  priv._node_icon_el = icons.icon_el;
65373
65774
  priv._node_delete_el = icons.delete_el;
65374
65775
  }
@@ -65482,17 +65883,24 @@ function show_node_popover(gobj) {
65482
65883
  create_form_button_row(popover, [{
65483
65884
  i18n: "cancel",
65484
65885
  style: BTN_STYLE_CANCEL,
65485
- onClick: () => {
65486
- hide_node_popover(gobj);
65487
- }
65886
+ event: "EV_CANCEL_NODE_POPOVER",
65887
+ kw: { node_id }
65488
65888
  }, {
65489
65889
  i18n: "apply",
65490
65890
  style: "flex:1;padding:6px;background:#1890ff;color:#fff;border:none;border-radius:4px;cursor:pointer;font-size:13px;font-weight:500;",
65491
- onClick: () => {
65492
- drop_preview_undo(gobj, "_node_preview_undo");
65493
- apply_node_properties(gobj, node_id, fillInput.value, strokeInput.value, parseInt(lwInput.value) || 1, scopeSelect.value, shapeSelect.value !== orig_figure ? shapeSelect.value : "");
65891
+ event: "EV_APPLY_NODE_POPOVER",
65892
+ kw: {
65893
+ node_id,
65894
+ shape_before: orig_figure
65895
+ },
65896
+ form: {
65897
+ fill: fillInput,
65898
+ stroke: strokeInput,
65899
+ line_width: lwInput,
65900
+ scope: scopeSelect,
65901
+ shape: shapeSelect
65494
65902
  }
65495
- }]);
65903
+ }], gobj);
65496
65904
  priv.$container.appendChild(popover);
65497
65905
  priv._node_popover_el = popover;
65498
65906
  set_preview_undo(gobj, "_node_preview_undo", undo_node);
@@ -65512,6 +65920,39 @@ function hide_node_detail(gobj) {
65512
65920
  gobj.priv._detail_node_id = null;
65513
65921
  hide_overlay(gobj, "_node_detail_el");
65514
65922
  }
65923
+ /************************************************************
65924
+ * The ✕ of the detail popover. Built as an element and not in
65925
+ * the popover's markup, so that it is a whole button: named
65926
+ * with its key (a language switch renames it), reachable by
65927
+ * the keyboard and pressed by Enter or Space as by the pointer.
65928
+ * Its click sends EV_CLOSE_NODE_DETAIL {node_id} and nothing
65929
+ * else; a pointer press does not take the focus.
65930
+ ************************************************************/
65931
+ function create_detail_close(gobj, color, node_id) {
65932
+ let el = document.createElement("div");
65933
+ el.className = "g6-detail-close";
65934
+ el.setAttribute("role", "button");
65935
+ el.setAttribute("tabindex", "0");
65936
+ el.setAttribute("title", t("close"));
65937
+ el.setAttribute("data-i18n-title", "close");
65938
+ el.setAttribute("aria-label", t("close"));
65939
+ el.setAttribute("data-i18n-aria-label", "close");
65940
+ el.style.color = color;
65941
+ el.textContent = "×";
65942
+ el.addEventListener("click", (e) => {
65943
+ e.stopPropagation();
65944
+ gobj_send_event(gobj, "EV_CLOSE_NODE_DETAIL", { node_id }, gobj);
65945
+ });
65946
+ keep_focus_on_press(el);
65947
+ click_on_enter_or_space(el);
65948
+ return el;
65949
+ }
65950
+ function detail_div(class_name, text) {
65951
+ let el = document.createElement("div");
65952
+ if (class_name) el.className = class_name;
65953
+ if (text !== void 0) el.textContent = text;
65954
+ return el;
65955
+ }
65515
65956
  function show_node_detail_popover(gobj, node_id) {
65516
65957
  let priv = gobj.priv;
65517
65958
  let graph = priv.graph;
@@ -65541,7 +65982,19 @@ function show_node_detail_popover(gobj, node_id) {
65541
65982
  popover.style.overflow = "auto";
65542
65983
  popover.style.padding = "0 12px 12px";
65543
65984
  popover.style.fontFamily = "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif";
65544
- let rows = "";
65985
+ let head = detail_div("g6-detail-head");
65986
+ head.style.background = bg;
65987
+ let names = detail_div("");
65988
+ names.style.flex = "1 1 auto";
65989
+ names.style.minWidth = "0";
65990
+ names.appendChild(detail_div("g6-detail-id", id));
65991
+ let topic_el = detail_div("g6-detail-topic", topic);
65992
+ topic_el.style.color = sub;
65993
+ names.appendChild(topic_el);
65994
+ head.appendChild(names);
65995
+ head.appendChild(create_detail_close(gobj, sub, node_id));
65996
+ popover.appendChild(head);
65997
+ let n_rows = 0;
65545
65998
  for (let key of Object.keys(record)) {
65546
65999
  if (key.indexOf("__") === 0) continue;
65547
66000
  let v = record[key];
@@ -65550,14 +66003,21 @@ function show_node_detail_popover(gobj, node_id) {
65550
66003
  else if (typeof v === "object") vs = JSON.stringify(v);
65551
66004
  else vs = String(v);
65552
66005
  if (vs.length > 240) vs = vs.slice(0, 240) + "…";
65553
- rows += `<div class="g6-detail-row" style="border-top:1px solid ${rowbd};"><div class="g6-detail-key" style="color:${sub};">${escapeHtml(key)}</div><div class="g6-detail-val">${escapeHtml(vs)}</div></div>`;
66006
+ let row = detail_div("g6-detail-row");
66007
+ row.style.borderTop = `1px solid ${rowbd}`;
66008
+ let key_el = detail_div("g6-detail-key", key);
66009
+ key_el.style.color = sub;
66010
+ row.appendChild(key_el);
66011
+ row.appendChild(detail_div("g6-detail-val", vs));
66012
+ popover.appendChild(row);
66013
+ n_rows++;
66014
+ }
66015
+ if (!n_rows) {
66016
+ let empty = detail_div("g6-detail-empty", t("no fields"));
66017
+ empty.setAttribute("data-i18n", "no fields");
66018
+ empty.style.color = sub;
66019
+ popover.appendChild(empty);
65554
66020
  }
65555
- popover.innerHTML = `<div class="g6-detail-head" style="background:${bg};"><div style="flex:1 1 auto;min-width:0;"><div class="g6-detail-id">${escapeHtml(id)}</div><div class="g6-detail-topic" style="color:${sub};">${escapeHtml(topic)}</div></div><div class="g6-detail-close" role="button" tabindex="0" aria-label="${escapeHtml(t("close"))}" title="${escapeHtml(t("close"))}" style="color:${sub};">×</div></div>` + (rows || `<div class="g6-detail-empty" style="color:${sub};">${escapeHtml(t("no fields"))}</div>`);
65556
- let close_el = popover.querySelector(".g6-detail-close");
65557
- if (close_el) close_el.addEventListener("click", (e) => {
65558
- e.stopPropagation();
65559
- hide_node_detail(gobj);
65560
- });
65561
66021
  priv.$container.appendChild(popover);
65562
66022
  priv._node_detail_el = popover;
65563
66023
  priv._detail_node_id = node_id;
@@ -66079,10 +66539,10 @@ function request_delete_selection(gobj) {
66079
66539
  execute_delete_selection(gobj, nodes);
66080
66540
  return;
66081
66541
  }
66082
- show_confirm_popover(gobj, null, delete_question(nodes), "delete", "#ff4d4f", () => gobj_send_event(gobj, "EV_CONFIRMED", {
66542
+ show_confirm_popover(gobj, null, delete_question(nodes), "delete", "#ff4d4f", {
66083
66543
  what: "delete_selection",
66084
66544
  ids: nodes.map((nd) => String(nd.id))
66085
- }, gobj), "_delete_confirm_el");
66545
+ }, "_delete_confirm_el");
66086
66546
  }
66087
66547
  function execute_delete_selection(gobj, nodes) {
66088
66548
  let priv = gobj.priv;
@@ -66099,10 +66559,10 @@ function execute_delete_selection(gobj, nodes) {
66099
66559
  ************************************************************/
66100
66560
  function show_delete_confirm(gobj, nodeData) {
66101
66561
  let priv = gobj.priv;
66102
- show_confirm_popover(gobj, priv._node_delete_el, delete_question([nodeData]), "delete", "#ff4d4f", () => gobj_send_event(gobj, "EV_CONFIRMED", {
66562
+ show_confirm_popover(gobj, priv._node_delete_el, delete_question([nodeData]), "delete", "#ff4d4f", {
66103
66563
  what: "delete_node",
66104
66564
  id: String(nodeData.id)
66105
- }, gobj), "_delete_confirm_el");
66565
+ }, "_delete_confirm_el");
66106
66566
  }
66107
66567
  function hide_delete_confirm(gobj) {
66108
66568
  hide_overlay(gobj, "_delete_confirm_el");
@@ -66151,6 +66611,7 @@ function show_create_popover(gobj) {
66151
66611
  topicLabel.appendChild(document.createTextNode(":"));
66152
66612
  popover.appendChild(topicLabel);
66153
66613
  let topicSelect = document.createElement("select");
66614
+ topicSelect.className = "G6_CREATE_TOPIC";
66154
66615
  topicSelect.setAttribute("title", t("topic"));
66155
66616
  topicSelect.setAttribute("data-i18n-title", "topic");
66156
66617
  topicSelect.setAttribute("aria-label", t("topic"));
@@ -66190,6 +66651,7 @@ function show_create_popover(gobj) {
66190
66651
  idLabel.appendChild(document.createTextNode(":"));
66191
66652
  popover.appendChild(idLabel);
66192
66653
  let idInput = document.createElement("input");
66654
+ idInput.className = "G6_CREATE_ID";
66193
66655
  idInput.type = "text";
66194
66656
  idInput.placeholder = t("node id");
66195
66657
  idInput.setAttribute("data-i18n-placeholder", "node id");
@@ -66200,39 +66662,31 @@ function show_create_popover(gobj) {
66200
66662
  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();
66201
66663
  popover.appendChild(idInput);
66202
66664
  let errorEl = document.createElement("div");
66665
+ errorEl.className = "G6_CREATE_ERROR";
66203
66666
  errorEl.style.cssText = "color:#ff4d4f;font-size:12px;margin-bottom:8px;display:none;";
66204
66667
  popover.appendChild(errorEl);
66205
- let createBtn;
66206
- create_form_button_row(popover, [{
66668
+ let createBtn = create_form_button_row(popover, [{
66207
66669
  i18n: "cancel",
66208
66670
  style: BTN_STYLE_CANCEL,
66209
- onClick: () => hide_create_popover(gobj)
66671
+ event: "EV_CLOSE_CREATE_POPOVER",
66672
+ kw: {}
66210
66673
  }, {
66211
66674
  i18n: "create",
66212
66675
  style: "flex:1;padding:6px;background:#1890ff;color:#fff;border:none;border-radius:4px;cursor:pointer;font-size:13px;font-weight:500;",
66213
- onClick: () => {
66214
- let node_id = idInput.value.trim();
66215
- if (!node_id) {
66216
- errorEl.textContent = t("node id required");
66217
- errorEl.setAttribute("data-i18n", "node id required");
66218
- errorEl.style.display = "block";
66219
- idInput.focus();
66220
- return;
66221
- }
66222
- gobj_send_event(gobj, "EV_SUBMIT_NEW_NODE", {
66223
- topic_name: String(topicSelect.value),
66224
- id: node_id
66225
- }, gobj);
66676
+ event: "EV_SUBMIT_NEW_NODE",
66677
+ kw: {},
66678
+ form: {
66679
+ topic_name: topicSelect,
66680
+ id: idInput
66226
66681
  }
66227
- }]);
66228
- createBtn = popover.querySelector("button:last-child");
66682
+ }], gobj).lastElementChild;
66229
66683
  idInput.addEventListener("keydown", (e) => {
66230
66684
  if (e.key === "Enter") {
66231
66685
  e.preventDefault();
66232
66686
  createBtn.click();
66233
66687
  } else if (e.key === "Escape") {
66234
66688
  e.preventDefault();
66235
- hide_create_popover(gobj);
66689
+ gobj_send_event(gobj, "EV_CLOSE_CREATE_POPOVER", {}, gobj);
66236
66690
  }
66237
66691
  });
66238
66692
  priv.$container.appendChild(popover);
@@ -66282,10 +66736,10 @@ function execute_unlink_edge(gobj, edgeData) {
66282
66736
  function show_unlink_confirm(gobj, edgeData) {
66283
66737
  let priv = gobj.priv;
66284
66738
  const d = edgeData.data;
66285
- show_confirm_popover(gobj, priv._edge_delete_el, t("unlink") + " " + d.child_id + " → " + d.parent_id + "?\n" + t("neither record is deleted"), "unlink", "#ff4d4f", () => gobj_send_event(gobj, "EV_CONFIRMED", {
66739
+ show_confirm_popover(gobj, priv._edge_delete_el, t("unlink") + " " + d.child_id + " → " + d.parent_id + "?\n" + t("neither record is deleted"), "unlink", "#ff4d4f", {
66286
66740
  what: "unlink_edge",
66287
66741
  id: String(edgeData.id)
66288
- }, gobj), "_unlink_confirm_el");
66742
+ }, "_unlink_confirm_el");
66289
66743
  }
66290
66744
  function hide_unlink_confirm(gobj) {
66291
66745
  hide_overlay(gobj, "_unlink_confirm_el");
@@ -67656,9 +68110,7 @@ function ac_load_data(gobj, event, kw, src) {
67656
68110
  ************************************************************/
67657
68111
  function ac_save_graph(gobj, event, kw, src) {
67658
68112
  if (gobj.priv.edit_mode) {
67659
- let $container = gobj_read_attr(gobj, "$container");
67660
- disableElements($container, ".EV_SAVE_GRAPH");
67661
- set_submit_state($container, ".EV_SAVE_GRAPH", false);
68113
+ light_save_button(gobj, false);
67662
68114
  let history = graph_get_plugin(gobj, "history");
67663
68115
  if (history) history.clear();
67664
68116
  save_geometry(gobj);
@@ -67935,12 +68387,22 @@ function ac_center_anchor(gobj, event, kw, src) {
67935
68387
  return 0;
67936
68388
  }
67937
68389
  /************************************************************
67938
- * What the anchor button says it will do next.
68390
+ * What the anchor button says it will do next: its `title`,
68391
+ * and the `title_key` it is translated from.
67939
68392
  ************************************************************/
67940
68393
  function anchor_title(state) {
67941
- if (state === "arming") return t("click the element to centre on");
67942
- if (state === "on") return t("centred: click to release");
67943
- return t("anchor view");
68394
+ if (state === "arming") return {
68395
+ title: t("click the element to centre on"),
68396
+ title_key: "click the element to centre on"
68397
+ };
68398
+ if (state === "on") return {
68399
+ title: t("centred: click to release"),
68400
+ title_key: "centred: click to release"
68401
+ };
68402
+ return {
68403
+ title: t("anchor view"),
68404
+ title_key: "anchor view"
68405
+ };
67944
68406
  }
67945
68407
  /************************************************************
67946
68408
  * Every camera action ends the same way once an anchor is
@@ -68356,8 +68818,8 @@ function ac_set_layout$1(gobj, event, kw, src) {
68356
68818
  function ac_set_operation_mode$1(gobj, event, kw, src) {
68357
68819
  gobj_write_attr(gobj, "operation_mode", kw.operation_mode);
68358
68820
  configure_behaviour(gobj);
68359
- update_toolbar(gobj);
68360
68821
  sync_history_plugin(gobj);
68822
+ update_toolbar(gobj);
68361
68823
  return 0;
68362
68824
  }
68363
68825
  /************************************************************
@@ -68539,25 +69001,134 @@ function ac_context_menu_item(gobj, event, kw, src) {
68539
69001
  return 0;
68540
69002
  }
68541
69003
  /************************************************************
68542
- * The delete icon floating beside the selected node.
69004
+ * Is the identity a floating button was drawn with still what
69005
+ * is selected?
69006
+ *
69007
+ * The buttons are removed with their selection and drawn again
69008
+ * for the next one, so a click carrying another identity is a
69009
+ * broken invariant -- said loudly, never acted on: it would
69010
+ * delete, unlink or edit what the reader is not looking at.
69011
+ ************************************************************/
69012
+ function floating_button_is_current(gobj, event, kw) {
69013
+ let priv = gobj.priv;
69014
+ let want = {
69015
+ node_id: priv._selected_node_id || "",
69016
+ port_key: priv._selected_port_key || "",
69017
+ edge_id: priv._selected_edge_id || ""
69018
+ };
69019
+ let keys = Object.keys(kw || {});
69020
+ let matches = keys.length > 0 && keys.every((k) => {
69021
+ return Object.prototype.hasOwnProperty.call(want, k) && !!kw[k] && kw[k] === want[k];
69022
+ });
69023
+ if (!matches) log_error(`${gobj_short_name(gobj)}: ${event} for ${JSON.stringify(kw || {})}, but the selection is ${JSON.stringify(want)}`);
69024
+ return matches;
69025
+ }
69026
+ /************************************************************
69027
+ * {node_id} -- the delete icon floating beside the selected
69028
+ * node.
68543
69029
  ************************************************************/
68544
69030
  function ac_request_delete_node(gobj, event, kw, src) {
69031
+ if (!floating_button_is_current(gobj, event, kw)) return -1;
68545
69032
  request_delete_node(gobj);
68546
69033
  return 0;
68547
69034
  }
68548
69035
  /************************************************************
68549
- * The unlink icon floating beside the selected edge.
69036
+ * {edge_id} -- the unlink icon floating beside the selected
69037
+ * edge.
68550
69038
  ************************************************************/
68551
69039
  function ac_request_unlink_edge(gobj, event, kw, src) {
69040
+ if (!floating_button_is_current(gobj, event, kw)) return -1;
68552
69041
  request_unlink_edge(gobj);
68553
69042
  return 0;
68554
69043
  }
68555
69044
  /************************************************************
69045
+ * {node_id} -- the gear beside the selected node: its
69046
+ * properties popover, opened or closed.
69047
+ ************************************************************/
69048
+ function ac_toggle_node_popover(gobj, event, kw, src) {
69049
+ if (!floating_button_is_current(gobj, event, kw)) return -1;
69050
+ toggle_node_popover(gobj);
69051
+ return 0;
69052
+ }
69053
+ /************************************************************
69054
+ * {edge_id} -- the gear beside the selected edge.
69055
+ ************************************************************/
69056
+ function ac_toggle_edge_popover(gobj, event, kw, src) {
69057
+ if (!floating_button_is_current(gobj, event, kw)) return -1;
69058
+ toggle_edge_popover(gobj);
69059
+ return 0;
69060
+ }
69061
+ /************************************************************
69062
+ * {node_id, port_key} -- the gear beside the selected port.
69063
+ ************************************************************/
69064
+ function ac_toggle_port_popover(gobj, event, kw, src) {
69065
+ if (!floating_button_is_current(gobj, event, kw)) return -1;
69066
+ toggle_port_popover(gobj);
69067
+ return 0;
69068
+ }
69069
+ /************************************************************
69070
+ * {node_id, port_key} -- the link icon of a selected fkey port,
69071
+ * CLICKED (a press that drags enters the mode on its own, from
69072
+ * start_link_drag): the compatible hooks light up, and the next
69073
+ * click on one of them links.
69074
+ ************************************************************/
69075
+ function ac_enter_linking_mode(gobj, event, kw, src) {
69076
+ let priv = gobj.priv;
69077
+ if (!floating_button_is_current(gobj, event, kw)) return -1;
69078
+ if (!priv._linking_mode) enter_linking_mode(gobj);
69079
+ return 0;
69080
+ }
69081
+ /************************************************************
69082
+ * {node_id} -- the ✕ of the detail popover.
69083
+ ************************************************************/
69084
+ function ac_close_node_detail(gobj, event, kw, src) {
69085
+ let priv = gobj.priv;
69086
+ let node_id = kw && kw.node_id || "";
69087
+ if (!priv._node_detail_el || !node_id || node_id !== priv._detail_node_id) {
69088
+ log_error(`${gobj_short_name(gobj)}: ${event} for '${node_id}', but the detail popover shows '${priv._detail_node_id || ""}'`);
69089
+ return -1;
69090
+ }
69091
+ hide_node_detail(gobj);
69092
+ return 0;
69093
+ }
69094
+ /************************************************************
69095
+ * {} -- the create button of the edit toolbar: the create
69096
+ * popover, opened or closed. Only in edition, where that
69097
+ * toolbar is.
69098
+ ************************************************************/
69099
+ function ac_toggle_create_popover(gobj, event, kw, src) {
69100
+ if (!gobj.priv.edit_mode) {
69101
+ log_error(`${gobj_short_name(gobj)}: ${event} outside edition`);
69102
+ return -1;
69103
+ }
69104
+ toggle_create_popover(gobj);
69105
+ return 0;
69106
+ }
69107
+ /************************************************************
69108
+ * {} -- the cancel of the create popover, or Escape in its id.
69109
+ ************************************************************/
69110
+ function ac_close_create_popover(gobj, event, kw, src) {
69111
+ if (!gobj.priv._create_popover_el) {
69112
+ log_error(`${gobj_short_name(gobj)}: ${event} with no create popover open`);
69113
+ return -1;
69114
+ }
69115
+ hide_create_popover(gobj);
69116
+ return 0;
69117
+ }
69118
+ /************************************************************
68556
69119
  * {topic_name, id} -- the create popover was filled in.
69120
+ *
69121
+ * An empty id with the popover open is the person's to fix,
69122
+ * not a fault: it is said IN the popover, where they typed.
68557
69123
  ************************************************************/
68558
69124
  function ac_submit_new_node(gobj, event, kw, src) {
68559
- let topic_name = kw && kw.topic_name ? kw.topic_name : "";
68560
- let node_id = kw && kw.id ? kw.id : "";
69125
+ let priv = gobj.priv;
69126
+ let topic_name = kw && kw.topic_name ? String(kw.topic_name) : "";
69127
+ let node_id = kw && kw.id ? String(kw.id).trim() : "";
69128
+ if (topic_name && !node_id && priv._create_popover_el) {
69129
+ say_node_id_required(gobj);
69130
+ return 0;
69131
+ }
68561
69132
  if (!topic_name || !node_id) {
68562
69133
  log_error(`${gobj_short_name(gobj)}: ${event} without topic or id`);
68563
69134
  return -1;
@@ -68565,6 +69136,168 @@ function ac_submit_new_node(gobj, event, kw, src) {
68565
69136
  execute_create_node(gobj, topic_name, node_id);
68566
69137
  return 0;
68567
69138
  }
69139
+ function say_node_id_required(gobj) {
69140
+ let $popover = gobj.priv._create_popover_el;
69141
+ let $error = $popover.querySelector(".G6_CREATE_ERROR");
69142
+ let $id = $popover.querySelector(".G6_CREATE_ID");
69143
+ if (!$error || !$id) {
69144
+ log_error(`${gobj_short_name(gobj)}: the create popover has no id or no error line`);
69145
+ return;
69146
+ }
69147
+ $error.textContent = t("node id required");
69148
+ $error.setAttribute("data-i18n", "node id required");
69149
+ $error.style.display = "block";
69150
+ $id.focus();
69151
+ }
69152
+ /************************************************************
69153
+ * Is `popover_key` open, and for the selection `kw` names?
69154
+ * `keys` are the identity in `kw` (the rest is what the form
69155
+ * held). A button of a popover that is not the one open is a
69156
+ * broken invariant -- said loudly, never acted on.
69157
+ ************************************************************/
69158
+ function popover_is_current(gobj, event, kw, popover_key, keys) {
69159
+ if (!gobj.priv[popover_key]) {
69160
+ log_error(`${gobj_short_name(gobj)}: ${event} for ${JSON.stringify(kw || {})}, but its popover is not open`);
69161
+ return false;
69162
+ }
69163
+ let identity = {};
69164
+ for (let k of keys) identity[k] = kw ? kw[k] : void 0;
69165
+ return floating_button_is_current(gobj, event, identity);
69166
+ }
69167
+ /************************************************************
69168
+ * {node_id, shape_before, fill, stroke, line_width, scope,
69169
+ * shape} -- apply of the node popover.
69170
+ ************************************************************/
69171
+ function ac_apply_node_popover(gobj, event, kw, src) {
69172
+ if (!popover_is_current(gobj, event, kw, "_node_popover_el", ["node_id"])) return -1;
69173
+ let shape = String(kw.shape || "");
69174
+ drop_preview_undo(gobj, "_node_preview_undo");
69175
+ apply_node_properties(gobj, kw.node_id, kw.fill, kw.stroke, parseInt(kw.line_width) || 1, kw.scope, shape !== kw.shape_before ? shape : "");
69176
+ return 0;
69177
+ }
69178
+ /************************************************************
69179
+ * {node_id} -- cancel of the node popover, which undoes the
69180
+ * preview.
69181
+ ************************************************************/
69182
+ function ac_cancel_node_popover(gobj, event, kw, src) {
69183
+ if (!popover_is_current(gobj, event, kw, "_node_popover_el", ["node_id"])) return -1;
69184
+ hide_node_popover(gobj);
69185
+ return 0;
69186
+ }
69187
+ /************************************************************
69188
+ * {edge_id, line_width, color, scope} -- apply of the edge
69189
+ * popover.
69190
+ ************************************************************/
69191
+ function ac_apply_edge_popover(gobj, event, kw, src) {
69192
+ if (!popover_is_current(gobj, event, kw, "_edge_popover_el", ["edge_id"])) return -1;
69193
+ drop_preview_undo(gobj, "_edge_preview_undo");
69194
+ apply_edge_properties(gobj, kw.edge_id, parseInt(kw.line_width) || 2, kw.color, kw.scope);
69195
+ return 0;
69196
+ }
69197
+ /************************************************************
69198
+ * {edge_id} -- cancel of the edge popover: the edge is let go,
69199
+ * which undoes the preview.
69200
+ ************************************************************/
69201
+ function ac_cancel_edge_popover(gobj, event, kw, src) {
69202
+ if (!popover_is_current(gobj, event, kw, "_edge_popover_el", ["edge_id"])) return -1;
69203
+ deselect_edge(gobj);
69204
+ return 0;
69205
+ }
69206
+ /************************************************************
69207
+ * {node_id, port_key, r_before, shape, r, scope} -- apply of
69208
+ * the port popover.
69209
+ ************************************************************/
69210
+ function ac_apply_port_popover(gobj, event, kw, src) {
69211
+ if (!popover_is_current(gobj, event, kw, "_port_popover_el", ["node_id", "port_key"])) return -1;
69212
+ drop_preview_undo(gobj, "_port_preview_undo");
69213
+ apply_port_properties(gobj, kw.node_id, kw.port_key, kw.shape, parseInt(kw.r) || kw.r_before, kw.scope);
69214
+ return 0;
69215
+ }
69216
+ /************************************************************
69217
+ * {node_id, port_key} -- cancel of the port popover, which
69218
+ * undoes the preview.
69219
+ ************************************************************/
69220
+ function ac_cancel_port_popover(gobj, event, kw, src) {
69221
+ if (!popover_is_current(gobj, event, kw, "_port_popover_el", ["node_id", "port_key"])) return -1;
69222
+ hide_port_popover(gobj);
69223
+ return 0;
69224
+ }
69225
+ var CONFIRM_POPOVER_OF = {
69226
+ delete_selection: "_delete_confirm_el",
69227
+ delete_node: "_delete_confirm_el",
69228
+ unlink_edge: "_unlink_confirm_el"
69229
+ };
69230
+ function same_question(a, b) {
69231
+ 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);
69232
+ }
69233
+ /************************************************************
69234
+ * {what, id|ids} -- the person said no in a confirm popover:
69235
+ * the question it carries must be the one open.
69236
+ ************************************************************/
69237
+ function ac_cancel_confirm(gobj, event, kw, src) {
69238
+ let priv = gobj.priv;
69239
+ let key = CONFIRM_POPOVER_OF[kw && kw.what || ""];
69240
+ let asked = key ? priv._confirm_questions[key] : null;
69241
+ if (!key || !priv[key] || !same_question(asked, kw)) {
69242
+ log_error(`${gobj_short_name(gobj)}: ${event} for ${JSON.stringify(kw || {})}, but the question open is ${JSON.stringify(priv[key] && asked || {})}`);
69243
+ return -1;
69244
+ }
69245
+ hide_overlay(gobj, key);
69246
+ return 0;
69247
+ }
69248
+ /************************************************************
69249
+ * {node_id, port_key, client_x, client_y} -- a press on the
69250
+ * link icon of the selected fkey port, which may be a drag.
69251
+ ************************************************************/
69252
+ function ac_start_link_drag(gobj, event, kw, src) {
69253
+ let priv = gobj.priv;
69254
+ if (!floating_button_is_current(gobj, event, {
69255
+ node_id: kw && kw.node_id,
69256
+ port_key: kw && kw.port_key
69257
+ })) return -1;
69258
+ if (priv._link_drag) {
69259
+ log_warning(`${gobj_short_name(gobj)}: ${event} with a link drag still in flight`);
69260
+ end_link_drag(gobj);
69261
+ }
69262
+ start_link_drag(gobj, Number(kw.client_x) || 0, Number(kw.client_y) || 0);
69263
+ return 0;
69264
+ }
69265
+ /************************************************************
69266
+ * {client_x, client_y, dragged} -- the pointer of a link drag
69267
+ * went up. Not dragged, it was a click: the click comes next
69268
+ * (EV_ENTER_LINKING_MODE). Dragged, it links to the hook it was
69269
+ * dropped on, or leaves the linking mode.
69270
+ ************************************************************/
69271
+ function ac_link_drag_end(gobj, event, kw, src) {
69272
+ let priv = gobj.priv;
69273
+ let drag = priv._link_drag;
69274
+ if (!drag) {
69275
+ log_error(`${gobj_short_name(gobj)}: ${event} with no link drag in flight`);
69276
+ return -1;
69277
+ }
69278
+ end_link_drag(gobj);
69279
+ if (!kw || !kw.dragged) return 0;
69280
+ let cx = Number(kw.client_x) - drag.left;
69281
+ let cy = Number(kw.client_y) - drag.top;
69282
+ let canvasPt = priv.graph.getCanvasByViewport([cx, cy]);
69283
+ let target = find_hook_at_point(gobj, canvasPt[0], canvasPt[1]);
69284
+ if (target) try_complete_link(gobj, target.node_id, target.port_key);
69285
+ else exit_linking_mode(gobj);
69286
+ return 0;
69287
+ }
69288
+ /************************************************************
69289
+ * {} -- the browser took the pointer of a link drag: no drop,
69290
+ * and the linking mode goes.
69291
+ ************************************************************/
69292
+ function ac_link_drag_cancel(gobj, event, kw, src) {
69293
+ if (!gobj.priv._link_drag) {
69294
+ log_error(`${gobj_short_name(gobj)}: ${event} with no link drag in flight`);
69295
+ return -1;
69296
+ }
69297
+ end_link_drag(gobj);
69298
+ exit_linking_mode(gobj);
69299
+ return 0;
69300
+ }
68568
69301
  /************************************************************
68569
69302
  * {what, id|ids} -- the person said yes in a confirm popover.
68570
69303
  *
@@ -68821,16 +69554,101 @@ function create_gclass$4(gclass_name) {
68821
69554
  ac_request_unlink_edge,
68822
69555
  null
68823
69556
  ],
69557
+ [
69558
+ "EV_TOGGLE_NODE_POPOVER",
69559
+ ac_toggle_node_popover,
69560
+ null
69561
+ ],
69562
+ [
69563
+ "EV_TOGGLE_EDGE_POPOVER",
69564
+ ac_toggle_edge_popover,
69565
+ null
69566
+ ],
69567
+ [
69568
+ "EV_TOGGLE_PORT_POPOVER",
69569
+ ac_toggle_port_popover,
69570
+ null
69571
+ ],
69572
+ [
69573
+ "EV_ENTER_LINKING_MODE",
69574
+ ac_enter_linking_mode,
69575
+ null
69576
+ ],
69577
+ [
69578
+ "EV_CLOSE_NODE_DETAIL",
69579
+ ac_close_node_detail,
69580
+ null
69581
+ ],
69582
+ [
69583
+ "EV_TOGGLE_CREATE_POPOVER",
69584
+ ac_toggle_create_popover,
69585
+ null
69586
+ ],
69587
+ [
69588
+ "EV_CLOSE_CREATE_POPOVER",
69589
+ ac_close_create_popover,
69590
+ null
69591
+ ],
68824
69592
  [
68825
69593
  "EV_SUBMIT_NEW_NODE",
68826
69594
  ac_submit_new_node,
68827
69595
  null
68828
69596
  ],
69597
+ [
69598
+ "EV_APPLY_NODE_POPOVER",
69599
+ ac_apply_node_popover,
69600
+ null
69601
+ ],
69602
+ [
69603
+ "EV_CANCEL_NODE_POPOVER",
69604
+ ac_cancel_node_popover,
69605
+ null
69606
+ ],
69607
+ [
69608
+ "EV_APPLY_EDGE_POPOVER",
69609
+ ac_apply_edge_popover,
69610
+ null
69611
+ ],
69612
+ [
69613
+ "EV_CANCEL_EDGE_POPOVER",
69614
+ ac_cancel_edge_popover,
69615
+ null
69616
+ ],
69617
+ [
69618
+ "EV_APPLY_PORT_POPOVER",
69619
+ ac_apply_port_popover,
69620
+ null
69621
+ ],
69622
+ [
69623
+ "EV_CANCEL_PORT_POPOVER",
69624
+ ac_cancel_port_popover,
69625
+ null
69626
+ ],
68829
69627
  [
68830
69628
  "EV_CONFIRMED",
68831
69629
  ac_confirmed$1,
68832
69630
  null
68833
69631
  ],
69632
+ [
69633
+ "EV_CANCEL_CONFIRM",
69634
+ ac_cancel_confirm,
69635
+ null
69636
+ ],
69637
+ [
69638
+ "EV_START_LINK_DRAG",
69639
+ ac_start_link_drag,
69640
+ null
69641
+ ],
69642
+ [
69643
+ "EV_LINK_DRAG_END",
69644
+ ac_link_drag_end,
69645
+ null
69646
+ ],
69647
+ [
69648
+ "EV_LINK_DRAG_CANCEL",
69649
+ ac_link_drag_cancel,
69650
+ null
69651
+ ],
68834
69652
  [
68835
69653
  "EV_CANVAS_CLICK",
68836
69654
  ac_canvas_click,
@@ -69051,8 +69869,25 @@ function create_gclass$4(gclass_name) {
69051
69869
  ["EV_CONTEXT_MENU_ITEM", 0],
69052
69870
  ["EV_REQUEST_DELETE_NODE", 0],
69053
69871
  ["EV_REQUEST_UNLINK_EDGE", 0],
69872
+ ["EV_TOGGLE_NODE_POPOVER", 0],
69873
+ ["EV_TOGGLE_EDGE_POPOVER", 0],
69874
+ ["EV_TOGGLE_PORT_POPOVER", 0],
69875
+ ["EV_ENTER_LINKING_MODE", 0],
69876
+ ["EV_CLOSE_NODE_DETAIL", 0],
69877
+ ["EV_TOGGLE_CREATE_POPOVER", 0],
69878
+ ["EV_CLOSE_CREATE_POPOVER", 0],
69054
69879
  ["EV_SUBMIT_NEW_NODE", 0],
69880
+ ["EV_APPLY_NODE_POPOVER", 0],
69881
+ ["EV_CANCEL_NODE_POPOVER", 0],
69882
+ ["EV_APPLY_EDGE_POPOVER", 0],
69883
+ ["EV_CANCEL_EDGE_POPOVER", 0],
69884
+ ["EV_APPLY_PORT_POPOVER", 0],
69885
+ ["EV_CANCEL_PORT_POPOVER", 0],
69055
69886
  ["EV_CONFIRMED", 0],
69887
+ ["EV_CANCEL_CONFIRM", 0],
69888
+ ["EV_START_LINK_DRAG", 0],
69889
+ ["EV_LINK_DRAG_END", 0],
69890
+ ["EV_LINK_DRAG_CANCEL", 0],
69056
69891
  ["EV_CANVAS_CLICK", 0],
69057
69892
  ["EV_NODE_DRAG_END", 0],
69058
69893
  ["EV_BRUSH_SELECT", 0],
@@ -69402,7 +70237,7 @@ function show_load_error(gobj, message) {
69402
70237
  $container.insertBefore($err, $container.firstChild);
69403
70238
  }
69404
70239
  let treedb = gobj_read_attr(gobj, "treedb_name") || "";
69405
- $err.textContent = (treedb ? `${treedb}: ` : "") + (message || "cannot load treedb");
70240
+ $err.textContent = (treedb ? `${treedb}: ` : "") + (message || t("cannot load treedb"));
69406
70241
  }
69407
70242
  /************************************************************
69408
70243
  * Destroy UI
@@ -71087,7 +71922,7 @@ function refresh_legend(gobj) {
71087
71922
  },
71088
71923
  hidden ? `${entry.total}` : `${entry.visible}/${entry.total}`
71089
71924
  ]);
71090
- let body_key = is_main ? "main topic" : hidden ? "show topic" : "hide topic";
71925
+ const body_key = is_main ? "main topic" : hidden ? "show topic" : "hide topic";
71091
71926
  let body_attrs = {
71092
71927
  class: "GRAPH_LEGEND_ITEM button" + (focused ? " GRAPH_LEGEND_FOCUSED" : "") + (is_main ? " GRAPH_LEGEND_MAIN" : ""),
71093
71928
  type: "button",
@@ -72347,6 +73182,7 @@ function build_graph(gobj) {
72347
73182
  }
72348
73183
  priv.graph = graph;
72349
73184
  graph.setTheme(dark ? "dark" : "light");
73185
+ keep_canvas_tab_order(gobj, graph);
72350
73186
  graph.on("aftertransform", () => {
72351
73187
  yui_graph_update_zoom($container, priv.graph);
72352
73188
  });
@@ -77660,7 +78496,7 @@ function ac_mt_command_answer(gobj, event, kw, src) {
77660
78496
  }
77661
78497
  return late_write_done(gobj, command, topic_name, data);
77662
78498
  }
77663
- if (result < 0) return end_writes(gobj, comment || t("the treedb refused the write"));
78499
+ if (result < 0) return end_writes(gobj, comment || "the treedb refused the write");
77664
78500
  priv.save_wrote = true;
77665
78501
  let record = null;
77666
78502
  if (command === "delete-node") forget_record(gobj, topic_name, kw_get_str(gobj, md, "record_id", "", 0));
@@ -78176,7 +79012,8 @@ function confirm_column_delete(gobj, verdict) {
78176
79012
  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",
78177
79013
  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"
78178
79014
  };
78179
- let message = "delete this column?";
79015
+ const message = "delete this column?";
79016
+ let $warning = null;
78180
79017
  if (NOTES[verdict]) {
78181
79018
  let children = [[
78182
79019
  "p",
@@ -78206,13 +79043,13 @@ function confirm_column_delete(gobj, verdict) {
78206
79043
  String(c.scanned)
78207
79044
  ]]
78208
79045
  ]);
78209
- message = createElement2([
79046
+ $warning = createElement2([
78210
79047
  "div",
78211
79048
  { class: "SCHEMA_COLUMN_DATA_WARNING" },
78212
79049
  children
78213
79050
  ]);
78214
79051
  }
78215
- confirm_then(gobj, message, `${c.topic}.${c.col}`, {
79052
+ confirm_then(gobj, $warning ? $warning : message, `${c.topic}.${c.col}`, {
78216
79053
  what: "column",
78217
79054
  topic: c.topic,
78218
79055
  col: c.col
@@ -81777,7 +82614,9 @@ function build_control_bar() {
81777
82614
  type: "button",
81778
82615
  title: t("machine trace shape"),
81779
82616
  "data-i18n-title": "machine trace shape",
81780
- "data-i18n": "simple mach"
82617
+ "data-i18n": "simple mach",
82618
+ "aria-label": t("simple mach"),
82619
+ "data-i18n-aria-label": "simple mach"
81781
82620
  },
81782
82621
  t("simple mach"),
81783
82622
  { click: (ev) => {
@@ -81851,7 +82690,9 @@ function build_control_bar() {
81851
82690
  type: "button",
81852
82691
  title: t(OUT_TITLES[v]),
81853
82692
  "data-i18n-title": OUT_TITLES[v],
81854
- "data-i18n": label
82693
+ "data-i18n": label,
82694
+ "aria-label": t(label),
82695
+ "data-i18n-aria-label": label
81855
82696
  },
81856
82697
  t(label),
81857
82698
  { click: (ev) => {
@@ -81879,7 +82720,9 @@ function build_control_bar() {
81879
82720
  type: "button",
81880
82721
  title: t("show this section in the expanded view"),
81881
82722
  "data-i18n-title": "show this section in the expanded view",
81882
- "data-i18n": label
82723
+ "data-i18n": label,
82724
+ "aria-label": t(label),
82725
+ "data-i18n-aria-label": label
81883
82726
  },
81884
82727
  t(label),
81885
82728
  { click: (ev) => {
@@ -81978,7 +82821,9 @@ function build_control_bar() {
81978
82821
  type: "button",
81979
82822
  title: t("copy visible traffic to clipboard"),
81980
82823
  "data-i18n-title": "copy visible traffic to clipboard",
81981
- "data-i18n": "copy"
82824
+ "data-i18n": "copy",
82825
+ "aria-label": t("copy"),
82826
+ "data-i18n-aria-label": "copy"
81982
82827
  },
81983
82828
  t("copy"),
81984
82829
  { click: (ev) => {
@@ -82000,7 +82845,9 @@ function build_control_bar() {
82000
82845
  type: "button",
82001
82846
  title: t("clear captured traffic"),
82002
82847
  "data-i18n-title": "clear captured traffic",
82003
- "data-i18n": "clear"
82848
+ "data-i18n": "clear",
82849
+ "aria-label": t("clear"),
82850
+ "data-i18n-aria-label": "clear"
82004
82851
  },
82005
82852
  t("clear"),
82006
82853
  { click: (ev) => {