@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
@@ -5,6 +5,8 @@
5
5
  *
6
6
  * Parses a JSON config to build:
7
7
  * - Layers (z-stacked): base, overlay, popup, modal, notification, loading
8
+ * (the overlay holds the WINDOW layer, where the windows live,
9
+ * under the drawer: yui_shell_window_layer())
8
10
  * - Zones (inside base layer): top, top-sub, left, center, right,
9
11
  * bottom-sub, bottom
10
12
  * - Menus (primary + submenus) rendered via C_YUI_NAV — one nav
@@ -65,7 +67,9 @@ import {
65
67
 
66
68
  import { resolve_route, normalize_route } from "./route_resolver.js";
67
69
  import { text_node } from "./yui_text.js";
68
- import { badge_node, badge_tone, paint_badge } from "./yui_badge.js";
70
+ import {
71
+ badge_node, badge_tone, badge_text, paint_badge, name_control_with_badge,
72
+ } from "./yui_badge.js";
69
73
  import { build_nav_map } from "./route_map_model.js";
70
74
  import {
71
75
  section_index_target,
@@ -83,6 +87,19 @@ const GCLASS_NAME = "C_YUI_SHELL";
83
87
  * one shell; tests that create several get the most recent. */
84
88
  let __last_shell__ = null;
85
89
 
90
+ /* The gobjs that keep DOM in a shell's layers without being its
91
+ * children: a C_YUI_WINDOW parented to the app, the dock of
92
+ * C_YUI_WINDOW_MANAGER. Each is sent EV_SHELL_CHANGED when a shell is
93
+ * built or destroyed (yui_shell_watch_changes()), because the place
94
+ * it lives in belongs to a shell that it outlives. A list of the
95
+ * module, not of a shell: the gobj is there BEFORE the next shell
96
+ * exists, and it has to be told when that one is built. */
97
+ let __shell_watchers__ = [];
98
+
99
+ /* The z-order of the windows (yui_shell_raise_window()): ONE counter
100
+ * for every window of the page, docked or not. */
101
+ let __window_z__ = 10;
102
+
86
103
  /* Zones rendered inside the base layer. */
87
104
  const ZONE_IDS = ["top", "top-sub", "left", "center", "right", "bottom-sub", "bottom"];
88
105
 
@@ -116,6 +133,9 @@ SDATA_END()
116
133
  let PRIVATE_DATA = {
117
134
  zones: {},
118
135
  layers: {},
136
+ /* The windows' stacking context, inside the overlay layer: see
137
+ * build_ui() and yui_shell_window_layer(). */
138
+ window_layer: null,
119
139
  stages: {},
120
140
  navs: [],
121
141
  item_index: {},
@@ -124,8 +144,10 @@ let PRIVATE_DATA = {
124
144
  * every nav the shell creates is handed it (`badges`), so a nav built
125
145
  * or rebuilt after the call draws it too. */
126
146
  nav_badges: {},
147
+ /* The live region a changed badge is said in (announce_badge()). */
148
+ badge_status: null,
127
149
  /* Parent item id -> the items yui_shell_set_submenu() put in place:
128
- * menu items as much as the declared ones (nav_item_declared). */
150
+ * menu items as much as the declared ones (nav_item_of). */
129
151
  dynamic_items: {},
130
152
  /* Section route -> last position visited inside it. Mirrors the
131
153
  * url for the duration of the page; see remember_position(). */
@@ -255,6 +277,11 @@ function mt_create(gobj)
255
277
  __last_shell__ = gobj;
256
278
 
257
279
  build_ui(gobj);
280
+
281
+ /* A window left in the body by a shell that went (a logout), or
282
+ * created while there was none, lives in THIS shell from now on:
283
+ * in its window layer, under its dialogs and toasts again. */
284
+ tell_shell_watchers(gobj);
258
285
  }
259
286
 
260
287
  /***************************************************************
@@ -607,15 +634,27 @@ function mt_destroy(gobj)
607
634
  * when stop is skipped. */
608
635
  close_toolbar_dropdown(gobj);
609
636
 
637
+ if(__last_shell__ === gobj) {
638
+ __last_shell__ = null;
639
+ }
640
+
610
641
  let $container = gobj_read_attr(gobj, "$container");
611
642
  if($container && $container.parentNode) {
612
643
  $container.parentNode.removeChild($container);
613
644
  }
614
- gobj_write_attr(gobj, "$container", null);
615
645
 
616
- if(__last_shell__ === gobj) {
617
- __last_shell__ = null;
618
- }
646
+ /* Its layers go with it, and what lives there without being its
647
+ * child does not: the developer window, a window of the app, the
648
+ * dock. Since 7.26.31 a window with no `$parent` lives in this
649
+ * shell, so a logout that destroyed the shell left such a window
650
+ * alive with its DOM detached, and the next shell never placed it
651
+ * again -- invisible, and the first click on its menu entry closed
652
+ * the invisible one. Told now -- with `__last_shell__` cleared, so
653
+ * the place they find is the body, and with the $container still
654
+ * readable, so each can tell whether it was inside. */
655
+ tell_shell_watchers(gobj);
656
+
657
+ gobj_write_attr(gobj, "$container", null);
619
658
  }
620
659
 
621
660
 
@@ -816,6 +855,30 @@ function build_ui(gobj)
816
855
  priv.layers[id] = $layer;
817
856
  }
818
857
 
858
+ /* The windows' own stacking context, inside the overlay layer
859
+ * (yui_shell_window_layer()). A window takes its z-index from the
860
+ * dock -- a counter that grows on every focus -- or 3, and in the
861
+ * overlay layer itself that number competed with the drawer (18):
862
+ * after a few focus changes a drawer opened UNDER the windows.
863
+ * Here no number a window takes reaches the drawer, nor the
864
+ * layers above. */
865
+ let $windows = createElement2(
866
+ ["div", {class: "SHELL_WINDOW_LAYER yui-window-layer"}]
867
+ );
868
+ priv.layers.overlay.appendChild($windows);
869
+ priv.window_layer = $windows;
870
+
871
+ /* Where the shell SAYS that a badge changed (announce_badge()): one
872
+ * live region, always in the tree -- never hidden, never inside a
873
+ * control -- so the first count is said as well as the next, and
874
+ * with the name of what it counts. */
875
+ let $status = createElement2(
876
+ ["div", {class: "SHELL_BADGE_STATUS is-sr-only", role: "status",
877
+ "aria-live": "polite", "aria-atomic": "true"}]
878
+ );
879
+ $container.appendChild($status);
880
+ priv.badge_status = $status;
881
+
819
882
  /* Base layer holds the grid of zones */
820
883
  let $base = priv.layers.base;
821
884
  $base.classList.add("yui-base-grid");
@@ -1899,17 +1962,19 @@ function toolbar_link_attrs(it)
1899
1962
  function build_toolbar_action_item(gobj, it)
1900
1963
  {
1901
1964
  let children = [];
1965
+ let badge = null;
1902
1966
  if(!empty_string(it.icon)) {
1903
1967
  /* The badge is anchored to the ICON, not to the button: with a
1904
1968
  * label on (desktop) the button is wide, and a badge pinned to
1905
1969
  * its corner would float far from the glyph it counts. The
1906
1970
  * count, its tone and their rules: yui_badge.js. */
1971
+ badge = badge_node(it.badge, it.badge_tone,
1972
+ "TOOLBAR_ITEM_BADGE yui-badge yui-toolbar-badge",
1973
+ `C_YUI_SHELL: toolbar item '${it.id || "?"}'`);
1907
1974
  children.push(["span", {class: "icon yui-toolbar-icon"},
1908
1975
  [
1909
1976
  ["i", {class: it.icon, "aria-hidden": "true"}],
1910
- badge_node(it.badge, it.badge_tone,
1911
- "TOOLBAR_ITEM_BADGE yui-badge yui-toolbar-badge",
1912
- `C_YUI_SHELL: toolbar item '${it.id || "?"}'`)
1977
+ badge
1913
1978
  ]
1914
1979
  ]);
1915
1980
  }
@@ -1945,6 +2010,11 @@ function build_toolbar_action_item(gobj, it)
1945
2010
  btn_attrs.title = tip;
1946
2011
  btn_attrs["data-i18n-title"] = tip;
1947
2012
  }
2013
+ if(badge && !empty_string(aria_key)) {
2014
+ /* The count is part of its name: inside a <button> the badge is
2015
+ * no node of its own for a reader (yui_badge.js). */
2016
+ name_control_with_badge(btn_attrs, badge);
2017
+ }
1948
2018
  let link = toolbar_link_attrs(it);
1949
2019
  if(link) {
1950
2020
  delete btn_attrs.type; /* meaningless, and invalid, on an <a> */
@@ -3195,37 +3265,43 @@ function update_secondary_nav_visibility(gobj, entry)
3195
3265
  }
3196
3266
 
3197
3267
  /************************************************************
3198
- * Does a menu of this shell declare an item `item_id`: an item
3199
- * of a menu of app_config.json, of one of their submenus, or
3200
- * one given to yui_shell_set_submenu() since.
3268
+ * The item `item_id` a menu of this shell declares, null when
3269
+ * none does: an item of a menu of app_config.json, of one of
3270
+ * their submenus, or one given to yui_shell_set_submenu() since
3271
+ * -- which comes first, because it replaced what the submenu
3272
+ * declared and is what the navs draw.
3201
3273
  ************************************************************/
3202
- function nav_item_declared(gobj, item_id)
3274
+ function nav_item_of(gobj, item_id)
3203
3275
  {
3204
3276
  let priv = gobj.priv;
3205
3277
  let config = gobj_read_attr(gobj, "config") || {};
3206
3278
  let menus = is_object(config.menu) ? config.menu : {};
3207
- let holds = (items) => is_array(items) && items.some((it) => it && it.id === item_id);
3279
+ let find = (items) => (is_array(items) &&
3280
+ items.find((it) => it && it.id === item_id)) || null;
3208
3281
 
3282
+ for(let parent_item_id in priv.dynamic_items) {
3283
+ let it = find(priv.dynamic_items[parent_item_id]);
3284
+ if(it) {
3285
+ return it;
3286
+ }
3287
+ }
3209
3288
  for(let menu_id in menus) {
3210
3289
  let items = menus[menu_id] && menus[menu_id].items;
3211
3290
  if(!is_array(items)) {
3212
3291
  continue;
3213
3292
  }
3214
- if(holds(items)) {
3215
- return true;
3293
+ let it = find(items);
3294
+ if(it) {
3295
+ return it;
3216
3296
  }
3217
- for(let it of items) {
3218
- if(it && it.submenu && holds(it.submenu.items)) {
3219
- return true;
3297
+ for(let parent of items) {
3298
+ let sub = parent && parent.submenu && find(parent.submenu.items);
3299
+ if(sub) {
3300
+ return sub;
3220
3301
  }
3221
3302
  }
3222
3303
  }
3223
- for(let parent_item_id in priv.dynamic_items) {
3224
- if(holds(priv.dynamic_items[parent_item_id])) {
3225
- return true;
3226
- }
3227
- }
3228
- return false;
3304
+ return null;
3229
3305
  }
3230
3306
 
3231
3307
  /************************************************************
@@ -3248,6 +3324,71 @@ function shell_navs(gobj)
3248
3324
  return navs;
3249
3325
  }
3250
3326
 
3327
+ /************************************************************
3328
+ * Say a badge that changed, with the name of what it counts:
3329
+ * "Alarms, 3", in the shell's live region (build_ui()).
3330
+ *
3331
+ * The badge itself is no live region (yui_badge.js): its number
3332
+ * alone said nothing, hidden while empty it missed the first
3333
+ * count, and inside a <button> it was not there at all. The
3334
+ * setters call this only when the text a reader would see
3335
+ * changes and is not empty: a count going away is not news, and
3336
+ * the same count again is not either.
3337
+ *
3338
+ * `name_key` is the key of the control's name; the phrase is a
3339
+ * key of its own, so a language orders and punctuates it its
3340
+ * way. Said once: a live region needs no re-translation.
3341
+ ************************************************************/
3342
+ function announce_badge(gobj, name_key, text)
3343
+ {
3344
+ let priv = gobj.priv;
3345
+ let $status = priv.badge_status;
3346
+ if(!$status) {
3347
+ log_error(`C_YUI_SHELL: no live region to say badge '${name_key}': ${text}`);
3348
+ return;
3349
+ }
3350
+ let message;
3351
+ if(typeof priv.translator === "function") {
3352
+ const tr = priv.translator;
3353
+ message = tr("{{name}}, {{n}}", {name: tr(name_key), n: text});
3354
+ } else {
3355
+ message = `${name_key}, ${text}`;
3356
+ }
3357
+ /* Written even when it reads the same: the callers filter what is
3358
+ * no news, and a count that went away and came back IS news -- a
3359
+ * new text node is a change a reader announces. */
3360
+ $status.textContent = message;
3361
+ }
3362
+
3363
+ /************************************************************
3364
+ * A shell was built (mt_create) or is being destroyed
3365
+ * (mt_destroy): tell every gobj that keeps DOM in a shell's
3366
+ * layers without being its child (yui_shell_watch_changes()).
3367
+ * Each one decides in its own action what it means for it --
3368
+ * the trace shows EV_SHELL_CHANGED arriving, from a `!!` shell
3369
+ * when it is the one going.
3370
+ *
3371
+ * An event, not a call into the window's module: the shell does
3372
+ * not import its watchers, and the move is work, so it crosses
3373
+ * their FSM. A watcher that is being destroyed is one whose
3374
+ * mt_destroy did not unwatch: it is dropped, and said.
3375
+ ************************************************************/
3376
+ function tell_shell_watchers(gobj)
3377
+ {
3378
+ for(let watcher of __shell_watchers__.slice()) {
3379
+ if(gobj_is_destroying(watcher)) {
3380
+ log_error(
3381
+ `C_YUI_SHELL: a destroyed gobj still watches the shell ` +
3382
+ `(${gobj_short_name(watcher)}): it must call ` +
3383
+ `yui_shell_unwatch_changes() in its mt_destroy`
3384
+ );
3385
+ yui_shell_unwatch_changes(watcher);
3386
+ continue;
3387
+ }
3388
+ gobj_send_event(watcher, "EV_SHELL_CHANGED", {}, gobj);
3389
+ }
3390
+ }
3391
+
3251
3392
 
3252
3393
 
3253
3394
 
@@ -3903,6 +4044,95 @@ function yui_shell_zone(shell_gobj, zone_id)
3903
4044
  return $zone;
3904
4045
  }
3905
4046
 
4047
+ /************************************************************
4048
+ * The DOM where a C_YUI_WINDOW lives when its host names no
4049
+ * `$parent`: the WINDOW layer, a stacking context of its own
4050
+ * inside the overlay layer -- above the content, below the
4051
+ * drawer, the toolbar's dropdowns, the dialogs and the toasts,
4052
+ * whatever z-index a window takes there. Null with no shell (the
4053
+ * window then lives in document.body).
4054
+ ************************************************************/
4055
+ function yui_shell_window_layer(shell_gobj)
4056
+ {
4057
+ if(!shell_gobj || !shell_gobj.priv) {
4058
+ return null;
4059
+ }
4060
+ let priv = shell_gobj.priv;
4061
+ return priv.window_layer || (priv.layers && priv.layers.overlay) || null;
4062
+ }
4063
+
4064
+ /************************************************************
4065
+ * Put a window on top of the others: the next number of ONE
4066
+ * counter, the same for a window of the dock and for a floating
4067
+ * one.
4068
+ *
4069
+ * Up to 7.26.34 there were two orders: the dock raised its windows
4070
+ * to a counter of its own (11, 12, ...), and a window without a
4071
+ * dock sat at 3 and was raised by being moved to the end of its
4072
+ * parent -- so a window opened LATER without a dock (a treedb's
4073
+ * raw json) opened UNDER any docked one, and no click could bring
4074
+ * it up. They share the window layer, and this one number orders
4075
+ * them all. It grows for ever, and it can: nothing else lives in
4076
+ * the window layer's stacking context.
4077
+ *
4078
+ * A number, not a move in the DOM: moving an element takes its
4079
+ * focus, its scroll positions and the pointer press that started
4080
+ * on it.
4081
+ ************************************************************/
4082
+ function yui_shell_raise_window($el)
4083
+ {
4084
+ if(!$el || !$el.style) {
4085
+ log_error("C_YUI_SHELL: yui_shell_raise_window() needs an element");
4086
+ return;
4087
+ }
4088
+ __window_z__ += 1;
4089
+ $el.style.zIndex = String(__window_z__);
4090
+ }
4091
+
4092
+ /************************************************************
4093
+ * Ask to be told (EV_SHELL_CHANGED, an INPUT event the gclass of
4094
+ * `gobj` must take in its FSM) when a shell is built or
4095
+ * destroyed.
4096
+ *
4097
+ * For a gobj that keeps DOM in a shell's layers without being
4098
+ * its child -- a window parented to the app, the dock. A shell's
4099
+ * children are destroyed with it; such a gobj is not, and its
4100
+ * place goes with the shell: a logout destroys the shell, the
4101
+ * login builds another. Told, it takes its place again in the
4102
+ * action.
4103
+ *
4104
+ * Idempotent. Pair it with yui_shell_unwatch_changes() in
4105
+ * mt_destroy, or the next shell sends the event to a destroyed
4106
+ * gobj.
4107
+ *
4108
+ * function mt_create(gobj) {
4109
+ * ...
4110
+ * yui_shell_watch_changes(gobj);
4111
+ * }
4112
+ * function mt_destroy(gobj) {
4113
+ * yui_shell_unwatch_changes(gobj);
4114
+ * ...
4115
+ * }
4116
+ ************************************************************/
4117
+ function yui_shell_watch_changes(gobj)
4118
+ {
4119
+ if(!is_gobj(gobj)) {
4120
+ log_error("C_YUI_SHELL: yui_shell_watch_changes() needs a gobj");
4121
+ return;
4122
+ }
4123
+ if(__shell_watchers__.indexOf(gobj) < 0) {
4124
+ __shell_watchers__.push(gobj);
4125
+ }
4126
+ }
4127
+
4128
+ function yui_shell_unwatch_changes(gobj)
4129
+ {
4130
+ let idx = __shell_watchers__.indexOf(gobj);
4131
+ if(idx >= 0) {
4132
+ __shell_watchers__.splice(idx, 1);
4133
+ }
4134
+ }
4135
+
3906
4136
  /************************************************************
3907
4137
  * Programmatic navigation.
3908
4138
  *
@@ -4303,7 +4533,17 @@ function yui_shell_set_toolbar_item_badge(shell_gobj, item_id, value, tone)
4303
4533
  if(!$badge) {
4304
4534
  return;
4305
4535
  }
4536
+ let before = $badge.hasAttribute("hidden") ? "" : $badge.textContent;
4306
4537
  paint_badge($badge, value, tone);
4538
+
4539
+ /* Said with the item's name when what a reader sees changed: the
4540
+ * badge is in the button's name, but nobody is told it moved. */
4541
+ let now = badge_text(value);
4542
+ if(now && now !== before) {
4543
+ let $item = $badge.closest("[data-toolbar-item-id]");
4544
+ const name_key = ($item && $item.getAttribute("data-i18n-aria-label")) || item_id;
4545
+ announce_badge(shell_gobj, /* i18n: data */ name_key, now);
4546
+ }
4307
4547
  }
4308
4548
 
4309
4549
  /************************************************************
@@ -4346,16 +4586,28 @@ function yui_shell_set_nav_item_badge(shell_gobj, item_id, value, tone)
4346
4586
  return;
4347
4587
  }
4348
4588
  let priv = shell_gobj.priv;
4349
- if(!priv || !nav_item_declared(shell_gobj, item_id)) {
4589
+ let item = priv ? nav_item_of(shell_gobj, item_id) : null;
4590
+ if(!item) {
4350
4591
  return;
4351
4592
  }
4352
4593
  let badge = (value === undefined) ? null : value;
4594
+ /* What a reader saw before: the last value set for the id, or the
4595
+ * badge its declaration seeds. */
4596
+ let was = priv.nav_badges[item_id];
4597
+ let before = badge_text(was ? was.badge : item.badge);
4353
4598
  priv.nav_badges[item_id] = {badge: badge, badge_tone: tone};
4354
4599
 
4355
4600
  for(let nav of shell_navs(shell_gobj)) {
4356
4601
  gobj_send_event(nav, "EV_SET_ITEM_BADGE",
4357
4602
  {item_id: item_id, badge: badge, badge_tone: tone}, shell_gobj);
4358
4603
  }
4604
+
4605
+ /* Said ONCE, here, with the item's name -- not by each nav that
4606
+ * draws the item (a rail, an icon bar, a drawer). */
4607
+ let now = badge_text(badge);
4608
+ if(now && now !== before) {
4609
+ announce_badge(shell_gobj, /* i18n: data */ item.name || item_id, now);
4610
+ }
4359
4611
  }
4360
4612
 
4361
4613
  /************************************************************
@@ -4385,6 +4637,10 @@ export {
4385
4637
  register_c_yui_shell,
4386
4638
  yui_shell_of,
4387
4639
  yui_shell_zone,
4640
+ yui_shell_window_layer,
4641
+ yui_shell_raise_window,
4642
+ yui_shell_watch_changes,
4643
+ yui_shell_unwatch_changes,
4388
4644
  yui_shell_navigate,
4389
4645
  yui_shell_last_route_under,
4390
4646
  yui_shell_last_route_outside,
@@ -92,7 +92,7 @@ import {
92
92
  set_active_state,
93
93
  set_pressed_state,
94
94
  } from "./lib_graph.js";
95
- import {yui_shell_show_error, yui_shell_show_modal, yui_shell_popup_layer} from "./shell_modals.js";
95
+ import {yui_shell_show_error, yui_shell_show_modal} from "./shell_modals.js";
96
96
  import {yui_shell_of, yui_shell_set_sub_routes, yui_shell_navigate, yui_shell_previous_route}
97
97
  from "./c_yui_shell.js";
98
98
 
@@ -1243,9 +1243,13 @@ function open_json_viewer(gobj)
1243
1243
  });
1244
1244
  gobj_start(jv); /* mounted: now it can measure itself */
1245
1245
  } else {
1246
- let $win_parent = (shell && yui_shell_popup_layer(shell)) ||
1247
- (typeof document !== "undefined" && document.getElementById("top-layer")) ||
1248
- null;
1246
+ /* The WINDOW layer, where every window lives (the legacy
1247
+ * '#top-layer' when a page has one): no `$parent` takes it. It
1248
+ * was the shell's popup layer -- above the drawer, in the way of
1249
+ * the toolbar's dropdowns (they opened BEHIND it where they met),
1250
+ * and in an order of its own no other window could rise above. */
1251
+ let $win_parent = (typeof document !== "undefined" &&
1252
+ document.getElementById("top-layer")) || null;
1249
1253
 
1250
1254
  priv.json_win = gobj_create_service(
1251
1255
  `treedb-jsonwin-${clean_name(gobj_name(gobj))}`,
@@ -1414,8 +1418,16 @@ function request_print_tranger(gobj, path)
1414
1418
  }, gobj);
1415
1419
  if(ret) {
1416
1420
  log_error(ret);
1417
- answer_print_tranger_refused(gobj, path,
1418
- {error: /* i18n: data */ refusal_text(ret) || "print-tranger failed"});
1421
+ /* The transport's own words travel as `error`, text the viewer
1422
+ * draws as it came. Our fallback is a KEY, and travels as `i18n`:
1423
+ * as `error` it was drawn raw, "print-tranger failed" in English
1424
+ * in every language. */
1425
+ const refusal = refusal_text(ret);
1426
+ if(refusal) {
1427
+ answer_print_tranger_refused(gobj, path, {error: /* i18n: data */ refusal});
1428
+ } else {
1429
+ answer_print_tranger_refused(gobj, path, {i18n: "print-tranger failed"});
1430
+ }
1419
1431
  return;
1420
1432
  }
1421
1433
  if(path) {
@@ -2173,9 +2185,15 @@ function ac_mt_command_answer(gobj, event, kw, src)
2173
2185
  priv.drills_in_flight.clear();
2174
2186
  }
2175
2187
  if(result < 0) {
2176
- if(path) {
2188
+ if(path && comment) {
2189
+ gobj_send_event(jv, "EV_SUBTREE_ERROR",
2190
+ {path: path, error: comment}, gobj); // the backend's words
2191
+ } else if(path) {
2192
+ /* No word from the backend: our fallback, a KEY, as `i18n`
2193
+ * -- the viewer draws it translated, and it changes
2194
+ * language with the app. As `error` it was drawn raw. */
2177
2195
  gobj_send_event(jv, "EV_SUBTREE_ERROR",
2178
- {path: path, error: comment || "print-tranger failed"}, gobj);
2196
+ {path: path, i18n: "print-tranger failed"}, gobj);
2179
2197
  } else {
2180
2198
  yui_shell_show_error(yui_shell_of(gobj),
2181
2199
  /* i18n: data */ comment || "print-tranger failed", {t: t});
@@ -64,7 +64,6 @@ import {
64
64
  import {
65
65
  yui_shell_confirm_yesnocancel,
66
66
  yui_shell_confirm_ok,
67
- yui_shell_popup_layer,
68
67
  yui_shell_show_modal,
69
68
  } from "./shell_modals.js";
70
69
 
@@ -78,7 +77,8 @@ import {attach_clear} from "./yui_inputs.js";
78
77
 
79
78
  import {yui_tabulator_lang, yui_tabulator_relocalize,
80
79
  yui_tabulator_name_filters,
81
- yui_tabulator_name_row_selects} from "./yui_tabulator_i18n.js";
80
+ yui_tabulator_name_row_selects,
81
+ yui_tabulator_name_editor} from "./yui_tabulator_i18n.js";
82
82
 
83
83
  import {
84
84
  yui_selection_column,
@@ -287,13 +287,6 @@ let PRIVATE_DATA = {
287
287
 
288
288
  let __gclass__ = null;
289
289
 
290
- /* Where to mount the edit/delete modal. Under a C_YUI_SHELL use
291
- * its popup layer (z 20): the shell confirms live on the modal
292
- * layer (z 99), so they always paint above this dialog — a
293
- * body-mounted Bulma `.modal` would cover them (the shell is a
294
- * stacking context below body-level modals). The legacy
295
- * C_YUI_MAIN "#popup-layer" and document.body (Bulma `.modal` is
296
- * position:fixed) remain the shell-less fallbacks. */
297
290
  /* True on a phone-width viewport (Bulma's mobile breakpoint). */
298
291
  function is_mobile()
299
292
  {
@@ -341,7 +334,7 @@ function present_json_popup(gobj, $box, opts)
341
334
  opts.name,
342
335
  "C_YUI_WINDOW",
343
336
  {
344
- $parent: popup_mount_layer(gobj),
337
+ $parent: popup_mount_layer(),
345
338
  subscriber: null,
346
339
  modal: false,
347
340
  showMax: true,
@@ -408,13 +401,18 @@ function retire_popup_window(win)
408
401
  }
409
402
  }
410
403
 
411
- function popup_mount_layer(gobj)
404
+ /************************************************************
405
+ * Where a json window of this view lives: null, the WINDOW
406
+ * layer of the shell, where every window lives -- below the
407
+ * shell's dialogs, so a confirm asked over it paints above it
408
+ * (the legacy C_YUI_MAIN '#popup-layer' when a page has one).
409
+ * It was the shell's popup layer: above the drawer, in the way
410
+ * of the toolbar's dropdowns, and in an order of its own no
411
+ * other window could rise above.
412
+ ************************************************************/
413
+ function popup_mount_layer()
412
414
  {
413
- let $layer = yui_shell_popup_layer(yui_shell_of(gobj));
414
- if($layer) {
415
- return $layer;
416
- }
417
- return document.getElementById("popup-layer") || document.body;
415
+ return document.getElementById("popup-layer") || null;
418
416
  }
419
417
 
420
418
 
@@ -1144,8 +1142,25 @@ function col_title_formatter(gobj, col)
1144
1142
  let text = col_label(col, topic_name);
1145
1143
  let attrs = {};
1146
1144
  const NO_I18N = "\x00";
1147
- if(t(col.id, {defaultValue: NO_I18N}) !== NO_I18N) {
1148
- attrs.i18n = col.id;
1145
+ /* The key a language change re-translates the header by is the
1146
+ * one the cascade TOOK -- '<topic>.<col>' before '<col>', as
1147
+ * col_label() reads them. Up to 7.26.34 it was always the bare
1148
+ * '<col>': a header translated by its topic's key alone carried
1149
+ * none (and stayed in its language), one translated by both went
1150
+ * to the bare column's text. The shell's apps did not show it --
1151
+ * their view rebuilds its columns on EV_LANGUAGE_CHANGED, after
1152
+ * refresh_language(), and this formatter runs the cascade again
1153
+ * -- but a refresh_language() alone did. The column is the
1154
+ * host's schema: data. */
1155
+ let key = null;
1156
+ if(topic_name &&
1157
+ t(/* i18n: data */ topic_name + "." + col.id, {defaultValue: NO_I18N}) !== NO_I18N) {
1158
+ key = topic_name + "." + col.id;
1159
+ } else if(t(col.id, {defaultValue: NO_I18N}) !== NO_I18N) {
1160
+ key = col.id;
1161
+ }
1162
+ if(key) {
1163
+ attrs.i18n = /* i18n: data */ key;
1149
1164
  }
1150
1165
  return createElement2(['span', attrs, text_node(text)]);
1151
1166
  };
@@ -1312,6 +1327,13 @@ function create_tabulator(gobj)
1312
1327
  return gobj_read_bool_attr(gobj, "editable") &&
1313
1328
  !gobj_read_bool_attr(gobj, "readonly");
1314
1329
  };
1330
+
1331
+ /* The box Tabulator draws in the cell being edited is named after
1332
+ * its column -- the header's own cascade -- in the language of
1333
+ * that moment. */
1334
+ yui_tabulator_name_editor(colDef, function() {
1335
+ return col_label(col, gobj_read_str_attr(gobj, "topic_name"));
1336
+ });
1315
1337
  }
1316
1338
 
1317
1339
  const FILTERABLE = [