@yuneta/gobj-ui 7.26.34 → 7.26.36

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 (43) hide show
  1. package/README.md +223 -53
  2. package/dist/gobj-ui.cjs.js +1109 -143
  3. package/dist/gobj-ui.es.js +1109 -143
  4. package/package.json +2 -2
  5. package/scripts/validate-locales.mjs +672 -96
  6. package/src/c_yui_form.js +186 -9
  7. package/src/c_yui_gclass.js +2 -0
  8. package/src/c_yui_gobj_tree_js.js +2 -0
  9. package/src/c_yui_json.js +2 -0
  10. package/src/c_yui_json_graph.js +2 -0
  11. package/src/c_yui_nav.js +23 -5
  12. package/src/c_yui_shell.css +16 -0
  13. package/src/c_yui_shell.js +282 -26
  14. package/src/c_yui_treedb_graph.js +26 -8
  15. package/src/c_yui_treedb_topic_with_form.js +40 -18
  16. package/src/c_yui_treedb_topics.js +34 -11
  17. package/src/c_yui_window.js +132 -32
  18. package/src/c_yui_window_manager.js +250 -46
  19. package/src/form_json_editor_names.js +448 -0
  20. package/src/form_json_editor_names.wiring.test.js +595 -0
  21. package/src/form_label_i18n.wiring.test.js +169 -0
  22. package/src/i18n_key_functions.js +14 -6
  23. package/src/i18n_key_functions.test.js +124 -2
  24. package/src/lib_maplibre.js +26 -2
  25. package/src/lib_maplibre_relocalize.test.js +78 -0
  26. package/src/named_controls.wiring.test.js +468 -0
  27. package/src/nav_cards_helpers.js +9 -1
  28. package/src/nav_item_badge.wiring.test.js +182 -1
  29. package/src/shell_route_map.js +25 -8
  30. package/src/table_editor_name.wiring.test.js +201 -0
  31. package/src/treedb_column_title.wiring.test.js +151 -0
  32. package/src/treedb_json_drill.wiring.test.js +41 -1
  33. package/src/window_layer.wiring.test.js +348 -43
  34. package/src/window_show.wiring.test.js +6 -2
  35. package/src/window_stacking.wiring.test.js +334 -0
  36. package/src/yui_badge.js +58 -13
  37. package/src/yui_badge.test.js +40 -3
  38. package/src/yui_dev.js +2 -1
  39. package/src/yui_file_field.js +2 -0
  40. package/src/yui_frontend_view.js +2 -1
  41. package/src/yui_gclass_view.js +9 -6
  42. package/src/yui_tabulator_i18n.js +42 -0
  43. package/src/yui_tabulator_i18n.test.js +48 -1
package/src/c_yui_form.js CHANGED
@@ -49,11 +49,13 @@ import {
49
49
  gclass_find_by_name,
50
50
  gobj_is_destroying,
51
51
  html_markup,
52
+ gobj_post_event,
53
+ gobj_unsubscribe_event,
52
54
  } from "@yuneta/gobj-js";
53
55
 
54
56
  import {t} from "i18next";
55
57
 
56
- import {yui_tabulator_name_row_selects} from "./yui_tabulator_i18n.js";
58
+ import {yui_tabulator_name_row_selects, yui_tabulator_name_editor} from "./yui_tabulator_i18n.js";
57
59
  import "tom-select/dist/css/tom-select.css"; // Import Tom-Select CSS
58
60
  import TomSelect from "tom-select"; // Import Tom-Select JS
59
61
 
@@ -92,6 +94,12 @@ import "./tabulator.css";
92
94
  import {is_time_field} from "./json_view_helpers.js";
93
95
  import {date_to_datetime_local, datetime_local_to_epoch} from "./form_time_value.js";
94
96
  import {parse_json_field, json_editor_value} from "./form_json_field.js";
97
+ import {
98
+ json_editor_menu_items,
99
+ json_editor_context_menu_items,
100
+ name_json_editor_menu,
101
+ } from "./form_json_editor_names.js";
102
+ import {yui_shell_of} from "./c_yui_shell.js";
95
103
  import {text_node, text_title_formatter, text_list_item_formatter} from "./yui_text.js";
96
104
  import { TabulatorFull as Tabulator } from "tabulator-tables"; // Import Full Tabulator JS
97
105
  // import { Tabulator } from "tabulator-tables"; // Import light Tabulator JS
@@ -170,6 +178,11 @@ let PRIVATE_DATA = {
170
178
  * table by id (a kw is plain json), see table_identity(). */
171
179
  tables: null,
172
180
  table_seq: 0,
181
+ /* The menus of the json editors are to be named: posted once, and
182
+ * named in ac_name_json_editors (form_json_editor_names.js). */
183
+ json_names_posted: false,
184
+ /* The shell this form hears the language change from. */
185
+ lang_shell: null,
173
186
  };
174
187
 
175
188
  let __gclass__ = null;
@@ -223,6 +236,18 @@ function mt_destroy(gobj)
223
236
  ************************************************************/
224
237
  function mt_start(gobj)
225
238
  {
239
+ let priv = gobj.priv;
240
+
241
+ /* The shell publishes the language switch (yui_shell_language_changed):
242
+ * what a json editor draws in its menu is drawn again in the new
243
+ * language (ac_language_changed). Its aria-labels carry their key and
244
+ * refresh_language() reaches them; a title and a button's text do not. */
245
+ let shell = yui_shell_of(gobj);
246
+ if(shell) {
247
+ gobj_subscribe_event(shell, "EV_LANGUAGE_CHANGED", {}, gobj);
248
+ priv.lang_shell = shell;
249
+ }
250
+
226
251
  let record = gobj_read_attr(gobj, "record");
227
252
  if(record) {
228
253
  gobj_send_event(gobj, "EV_LOAD_RECORD", record, gobj);
@@ -234,6 +259,14 @@ function mt_start(gobj)
234
259
  ************************************************************/
235
260
  function mt_stop(gobj)
236
261
  {
262
+ let priv = gobj.priv;
263
+
264
+ /* The shell it subscribed to, if it is still there: a logout may
265
+ * have destroyed it, and its subscriptions with it. */
266
+ if(priv.lang_shell && !gobj_is_destroying(priv.lang_shell)) {
267
+ gobj_unsubscribe_event(priv.lang_shell, "EV_LANGUAGE_CHANGED", {}, gobj);
268
+ }
269
+ priv.lang_shell = null;
237
270
  }
238
271
 
239
272
 
@@ -737,6 +770,82 @@ function name_form_control($field, conf)
737
770
  }
738
771
  }
739
772
 
773
+ /************************************************************
774
+ * The ✕ of ONE selected option of a Tom Select: its
775
+ * remove_button plugin calls this for every option it draws.
776
+ *
777
+ * The plugin's own was a div titled "Remove" in English -- never
778
+ * translated, no aria-label -- with tabindex -1, and the form
779
+ * turns Backspace deletion off (no_backspace_delete): a keyboard
780
+ * could not take one option out at all. Named here through t()
781
+ * with its key (so a language change reaches it), and pressed by
782
+ * Enter or Space as the clear button is: the key clicks it, which
783
+ * runs the plugin's own handler.
784
+ ************************************************************/
785
+ function remove_option_button()
786
+ {
787
+ const KEY = "remove this option";
788
+ let $b = createElement2(['div', {
789
+ class: 'remove', role: 'button', tabindex: '0',
790
+ title: t(KEY), 'data-i18n-title': KEY,
791
+ 'aria-label': t(KEY), 'data-i18n-aria-label': KEY
792
+ }, '×']);
793
+ $b.addEventListener("keydown", function(evt) {
794
+ if(evt.key === "Enter" || evt.key === " ") {
795
+ evt.preventDefault(); // Space would scroll the page
796
+ evt.stopPropagation(); // a key that pressed it is not Tom Select's
797
+ $b.click();
798
+ }
799
+ });
800
+ return $b;
801
+ }
802
+
803
+ /************************************************************
804
+ * The `onRenderMenu` of a json editor of this form.
805
+ *
806
+ * The editor calls it with the items it is about to draw: they go
807
+ * back translated (json_editor_menu_items(), data for the editor
808
+ * to draw). What no item can carry -- an aria-label, the state of
809
+ * the mode buttons -- is written on the drawn buttons, after the
810
+ * render, by the form's FSM (EV_NAME_JSON_EDITORS): the callback
811
+ * only posts the event, once until it is handled. Posted, not
812
+ * sent: when it runs the editor has not drawn the menu yet.
813
+ *
814
+ * A new function each call: handing an editor a new one is what
815
+ * makes it draw its menu again (ac_language_changed).
816
+ ************************************************************/
817
+ function json_editor_render_menu(gobj)
818
+ {
819
+ return function(items) {
820
+ ask_json_editor_names(gobj);
821
+ return json_editor_menu_items(items);
822
+ };
823
+ }
824
+
825
+ /************************************************************
826
+ * The `onRenderContextMenu` of a json editor of this form: the
827
+ * same, for the menu it opens on a right click or Ctrl+Q. It is
828
+ * built each time it opens, in the language of that moment.
829
+ ************************************************************/
830
+ function json_editor_render_context_menu(gobj)
831
+ {
832
+ return function(items, context) {
833
+ ask_json_editor_names(gobj);
834
+ return json_editor_context_menu_items(items, context);
835
+ };
836
+ }
837
+
838
+ /* The editors' menus are to be named, once their render is on the
839
+ * page: posted once until EV_NAME_JSON_EDITORS is handled. */
840
+ function ask_json_editor_names(gobj)
841
+ {
842
+ let priv = gobj.priv;
843
+ if(!priv.json_names_posted && !gobj_is_destroying(gobj)) {
844
+ priv.json_names_posted = true;
845
+ gobj_post_event(gobj, "EV_NAME_JSON_EDITORS", {}, gobj);
846
+ }
847
+ }
848
+
740
849
  function build_form(gobj)
741
850
  {
742
851
  let form_id = gobj_read_str_attr(gobj, "form_id");
@@ -803,11 +912,16 @@ function build_html_form(gobj, $form, prefix, template)
803
912
  : [field_desc.name];
804
913
  const NO_I18N = "\x00";
805
914
  html_field_conf.label = t(label_keys, {defaultValue: field_desc.header});
806
- /* A column of the template: the host's schema, data */
807
- html_field_conf.label_i18n =
808
- (t(/* i18n: data */ field_desc.name, {defaultValue: NO_I18N}) !== NO_I18N)
809
- ? field_desc.name
810
- : null;
915
+ /* The key it is re-translated by is the one the cascade TOOK: the
916
+ * first of `label_keys` that has a translation. Up to 7.26.34 only
917
+ * the bare column was asked -- a label translated by its
918
+ * '<topic>.<col>' alone carried its own translated TEXT as the
919
+ * key, and never changed language again; one translated by both
920
+ * switched to the bare column's text at the first language
921
+ * change. A column of the template is the host's schema: data. */
922
+ html_field_conf.label_i18n = label_keys.find(
923
+ (k) => t(/* i18n: data */ k, {defaultValue: NO_I18N}) !== NO_I18N
924
+ ) || null;
811
925
  html_field_conf.placeholder = field_desc.placeholder?t(/* i18n: data */ field_desc.placeholder):'';
812
926
 
813
927
  let placeholders = gobj_read_attr(gobj, "placeholders");
@@ -1625,7 +1739,13 @@ function create_form_field(
1625
1739
  },
1626
1740
  'no_active_items': {},
1627
1741
  'no_backspace_delete': {},
1628
- 'remove_button': {}
1742
+ /* Tom Select drew the ✕ of each selected option with
1743
+ * an English title ("Remove") and no name, out of the
1744
+ * keyboard's reach: drawn here instead
1745
+ * (remove_option_button). */
1746
+ 'remove_button': {
1747
+ html: remove_option_button
1748
+ }
1629
1749
  }
1630
1750
  });
1631
1751
  $extend.tom_select.on('change', function() {
@@ -1705,6 +1825,10 @@ function create_form_field(
1705
1825
  timestampTag: function({field, value, path}) {
1706
1826
  return is_time_field(field);
1707
1827
  },
1828
+ /* Its menu and its context menu, in the app's
1829
+ * language, and named (form_json_editor_names.js). */
1830
+ onRenderMenu: json_editor_render_menu(gobj),
1831
+ onRenderContextMenu: json_editor_render_context_menu(gobj),
1708
1832
  }
1709
1833
  });
1710
1834
  break;
@@ -2550,6 +2674,11 @@ function template2columns(gobj, columns, template, sub_elements)
2550
2674
  if(readonly) {
2551
2675
  delete column.editor;
2552
2676
  }
2677
+ /* The box Tabulator draws in a cell being edited is named
2678
+ * after its column, in the language of that moment. */
2679
+ yui_tabulator_name_editor(column, function() {
2680
+ return t(/* i18n: data */ field_desc.header || field_desc.name); // the host's schema
2681
+ });
2553
2682
  columns.push(column);
2554
2683
  }
2555
2684
  });
@@ -3762,6 +3891,50 @@ function ac_timeout(gobj, event, kw, src)
3762
3891
  return 0;
3763
3892
  }
3764
3893
 
3894
+ /************************************************************
3895
+ * EV_NAME_JSON_EDITORS {} -- a menu of a json editor of this
3896
+ * form was drawn, its menu or its context menu (posted by
3897
+ * ask_json_editor_names): name its buttons. Every editor of the
3898
+ * form: naming is idempotent, and a kw is plain json, which an
3899
+ * editor is not.
3900
+ ************************************************************/
3901
+ function ac_name_json_editors(gobj, event, kw, src)
3902
+ {
3903
+ let priv = gobj.priv;
3904
+ priv.json_names_posted = false;
3905
+
3906
+ let $container = gobj_read_attr(gobj, "$container");
3907
+ if(!$container) {
3908
+ log_error(`${gobj_short_name(gobj)}: ${event} without a container`);
3909
+ return -1;
3910
+ }
3911
+ for(let $editor of $container.querySelectorAll(".jsoneditor")) {
3912
+ name_json_editor_menu($editor);
3913
+ }
3914
+ return 0;
3915
+ }
3916
+
3917
+ /************************************************************
3918
+ * EV_LANGUAGE_CHANGED {} -- from the shell. A json editor draws
3919
+ * its menu once: its titles and the text of its mode buttons stay
3920
+ * in the old language until it draws it again, so each is told to
3921
+ * (a new onRenderMenu), and the render names it again.
3922
+ ************************************************************/
3923
+ function ac_language_changed(gobj, event, kw, src)
3924
+ {
3925
+ let $container = gobj_read_attr(gobj, "$container");
3926
+ if(!$container) {
3927
+ return 0; // a form between builds has no editor to tell
3928
+ }
3929
+ for(let $editor of $container.querySelectorAll(".jsoneditor")) {
3930
+ let editor = $editor.jsoneditor;
3931
+ if(editor && typeof editor.updateProps === "function") {
3932
+ editor.updateProps({onRenderMenu: json_editor_render_menu(gobj)});
3933
+ }
3934
+ }
3935
+ return 0;
3936
+ }
3937
+
3765
3938
 
3766
3939
 
3767
3940
  /***************************
@@ -3877,7 +4050,9 @@ function create_gclass(gclass_name)
3877
4050
  ["EV_REFRESH", ac_refresh, null],
3878
4051
  ["EV_SHOW", ac_show, null],
3879
4052
  ["EV_HIDE", ac_hide, null],
3880
- ["EV_TIMEOUT", ac_timeout, null]
4053
+ ["EV_TIMEOUT", ac_timeout, null],
4054
+ ["EV_NAME_JSON_EDITORS", ac_name_json_editors, null],
4055
+ ["EV_LANGUAGE_CHANGED", ac_language_changed, null]
3881
4056
  ]]
3882
4057
  ];
3883
4058
 
@@ -3901,7 +4076,9 @@ function create_gclass(gclass_name)
3901
4076
  ["EV_REFRESH", 0],
3902
4077
  ["EV_SHOW", 0],
3903
4078
  ["EV_HIDE", 0],
3904
- ["EV_TIMEOUT", 0]
4079
+ ["EV_TIMEOUT", 0],
4080
+ ["EV_NAME_JSON_EDITORS", 0],
4081
+ ["EV_LANGUAGE_CHANGED", 0]
3905
4082
  ];
3906
4083
 
3907
4084
  __gclass__ = gclass_create(
@@ -321,6 +321,8 @@ function make_toolbar(gobj)
321
321
  'data-i18n-placeholder': "search",
322
322
  /* A placeholder is not a name: it is gone as soon as
323
323
  * something is typed. */
324
+ title: t("search"),
325
+ 'data-i18n-title': "search",
324
326
  'aria-label': t("search"),
325
327
  'data-i18n-aria-label': "search"}, [], {
326
328
  input: function(evt) {
@@ -920,6 +920,8 @@ function make_find_items(gobj)
920
920
  * cannot reach it: it needs its own key. */
921
921
  placeholder: t('search'),
922
922
  'data-i18n-placeholder': 'search',
923
+ title: t('search'),
924
+ 'data-i18n-title': 'search',
923
925
  'aria-label': t('search'),
924
926
  'data-i18n-aria-label': 'search'
925
927
  }, [], {
package/src/c_yui_json.js CHANGED
@@ -394,6 +394,8 @@ function make_toolbar(gobj)
394
394
  'data-i18n-placeholder': "search",
395
395
  /* A placeholder is not a name: it is gone as soon as
396
396
  * something is typed. */
397
+ title: t("search"),
398
+ 'data-i18n-title': "search",
397
399
  'aria-label': t("search"),
398
400
  'data-i18n-aria-label': "search"}, [], {
399
401
  input: function(evt) {
@@ -565,6 +565,8 @@ function make_toolbar(gobj)
565
565
  * cannot reach it: it needs its own key. */
566
566
  placeholder: t('search'),
567
567
  'data-i18n-placeholder': 'search',
568
+ title: t('search'),
569
+ 'data-i18n-title': 'search',
568
570
  'aria-label': t('search'),
569
571
  'data-i18n-aria-label': 'search'
570
572
  }, [], {
package/src/c_yui_nav.js CHANGED
@@ -109,7 +109,7 @@ import {
109
109
  bulma_hidden_class,
110
110
  } from "./shell_show_on.js";
111
111
  import {text_node} from "./yui_text.js";
112
- import {badge_node, badge_tone, paint_badge} from "./yui_badge.js";
112
+ import {badge_node, badge_tone, paint_badge, name_control_with_badge} from "./yui_badge.js";
113
113
 
114
114
  /***************************************************************
115
115
  * Constants
@@ -478,6 +478,8 @@ function render_tabs(gobj, items)
478
478
  a_attrs.title = tip;
479
479
  a_attrs["data-i18n-title"] = tip;
480
480
  }
481
+ /* The count is part of its name (yui_badge.js) */
482
+ name_control_with_badge(a_attrs, badge_el);
481
483
  /* Per-item state class (e.g. a "disconnected" colour) rides on
482
484
  * the <li>, alongside Bulma's own is-active. */
483
485
  let li_class = empty_string(it.class) ? "" : String(it.class);
@@ -539,17 +541,25 @@ function render_breadcrumb(gobj, items)
539
541
  let label = it.name || it.id || "";
540
542
  children.push(["span", {class: "yui-nav-label", i18n: label}, text_node(label)]);
541
543
  }
544
+ /* Named and titled like every other item of the nav: up to
545
+ * 7.26.34 a crumb had no title, and no name when it had no
546
+ * `name` (an icon crumb read as nothing). */
542
547
  let a_attrs = {
543
548
  class: "yui-nav-item yui-nav-crumb",
544
549
  href: it.route ? "#" + it.route : "#",
545
550
  "data-item-id": it.id,
546
551
  "data-route": it.route || "",
547
- "data-disabled": it.disabled ? "1" : "0"
552
+ "data-disabled": it.disabled ? "1" : "0",
553
+ "aria-label": it.name || it.id
548
554
  };
549
555
  if(it.name) {
550
- a_attrs["aria-label"] = it.name;
551
556
  a_attrs["data-i18n-aria-label"] = it.name;
552
557
  }
558
+ let tip = it.tooltip || it.aria_label || it.name;
559
+ if(tip) {
560
+ a_attrs.title = tip;
561
+ a_attrs["data-i18n-title"] = tip;
562
+ }
553
563
  lis.push(["li", {}, [["a", a_attrs, children]]]);
554
564
  }
555
565
 
@@ -676,16 +686,20 @@ function render_accordion(gobj, items)
676
686
  "aria-expanded": "false",
677
687
  "aria-controls": body_id
678
688
  };
689
+ /* A section head reads as a line: its badge follows the label */
690
+ let head_badge = item_badge_node(gobj, it, false);
679
691
  if(!empty_string(head_text)) {
680
692
  head_attrs.i18n = head_text;
681
693
  head_attrs.title = head_text;
682
694
  head_attrs["data-i18n-title"] = head_text;
683
695
  head_attrs["aria-label"] = head_text;
684
696
  head_attrs["data-i18n-aria-label"] = head_text;
697
+ /* The count is part of its name: inside a <button> the
698
+ * badge is no node of its own for a reader (yui_badge.js) */
699
+ name_control_with_badge(head_attrs, head_badge);
685
700
  }
686
- /* A section head reads as a line: its badge follows the label */
687
701
  let $hdr = createElement2(["button", head_attrs,
688
- [text_node(head_text), item_badge_node(gobj, it, false)]]);
702
+ [text_node(head_text), head_badge]]);
689
703
  $container.appendChild($hdr);
690
704
 
691
705
  let $ul = createElement2(
@@ -807,6 +821,8 @@ function item_li(gobj, it, opts)
807
821
  a_attrs["aria-disabled"] = "true";
808
822
  a_attrs["tabindex"] = "-1";
809
823
  }
824
+ /* The count is part of its name (yui_badge.js) */
825
+ name_control_with_badge(a_attrs, badge_el);
810
826
 
811
827
  return ["li", {},
812
828
  ["a", a_attrs, children]
@@ -863,6 +879,8 @@ function item_iconbar(gobj, it, opts)
863
879
  a_attrs.title = tip;
864
880
  a_attrs["data-i18n-title"] = tip;
865
881
  }
882
+ /* The count is part of its name (yui_badge.js) */
883
+ name_control_with_badge(a_attrs, badge_el);
866
884
  return ["div", {class: "level-item"},
867
885
  ["a", a_attrs, children]
868
886
  ];
@@ -61,6 +61,22 @@
61
61
  .yui-layer-loading.is-active,
62
62
  .yui-layer-modal.is-active { pointer-events: auto; }
63
63
 
64
+ /* The WINDOW layer, inside the overlay layer: the windows' own
65
+ * stacking context. A window takes its z-index from the dock (a
66
+ * counter that grows on every focus) or 3, and here no such number
67
+ * can reach the drawer (z-index 18, a sibling of this layer in the
68
+ * overlay) nor the layers above. It covers the screen like a layer,
69
+ * so it lets the pointer through and its windows take it. */
70
+ .yui-layer > .yui-window-layer {
71
+ position: absolute;
72
+ inset: 0;
73
+ z-index: 1;
74
+ pointer-events: none;
75
+ }
76
+ .yui-window-layer > * {
77
+ pointer-events: auto;
78
+ }
79
+
64
80
  /*============================================================
65
81
  * Base grid (zones)
66
82
  *